diff --git a/.dependency-cruiser.cjs b/.dependency-cruiser.cjs index 40a9785aeaf..84d01599727 100644 --- a/.dependency-cruiser.cjs +++ b/.dependency-cruiser.cjs @@ -4,7 +4,7 @@ module.exports = { { name: "only-type-imports", 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: { path: ["(^|/)src/@types", "(^|/)src/enums"], }, @@ -14,7 +14,7 @@ module.exports = { }, { name: "no-circular-at-runtime", - severity: "warn", + severity: "error", comment: "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) ", @@ -34,7 +34,7 @@ module.exports = { "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 " + "files (.d.ts), tsconfig.json and some of the babel and webpack configs.", - severity: "warn", + severity: "error", from: { orphan: true, pathNot: [ @@ -42,8 +42,7 @@ module.exports = { "[.]d[.]ts$", // TypeScript declaration files "(^|/)tsconfig[.]json$", // TypeScript config "(^|/)(?:babel|webpack)[.]config[.](?:js|cjs|mjs|ts|cts|mts|json)$", // other configs - // anything in src/@types - "(^|/)src/@types/", + "(^|/)test/.+[.]setup[.]ts", // Vitest setup files ], }, to: {}, @@ -53,7 +52,7 @@ module.exports = { comment: "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.", - severity: "warn", + severity: "error", from: {}, to: { dependencyTypes: ["core"], @@ -86,7 +85,7 @@ module.exports = { comment: "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.", - severity: "warn", + severity: "error", from: {}, to: { dependencyTypes: ["deprecated"], @@ -122,7 +121,7 @@ module.exports = { "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 " + "maintenance problems later on.", - severity: "warn", + severity: "error", from: {}, to: { 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", @@ -188,7 +187,7 @@ module.exports = { "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 " + "add an exception to your dependency-cruiser configuration.", - severity: "warn", + severity: "error", from: {}, to: { dependencyTypes: ["npm-peer"], @@ -196,6 +195,7 @@ module.exports = { }, ], options: { + exclude: ["src/plugins/vite/*", "src/vite.env.d.ts"], /* Which modules not to follow further when encountered */ doNotFollow: { /* path: an array of regular expressions in strings to match against */ @@ -218,7 +218,7 @@ module.exports = { module systems it knows of. It's the default because it's the safe option It might come at a performance penalty, though. moduleSystems: ['amd', 'cjs', 'es6', 'tsd'] - + As in practice only commonjs ('cjs') and ecmascript modules ('es6') are widely used, you can limit the moduleSystems to those. */ @@ -226,7 +226,7 @@ module.exports = { // moduleSystems: ['cjs', 'es6'], /* prefix for links in html and svg output (e.g. 'https://github.com/you/yourrepo/blob/main/' - to open it on your online repo or `vscode://file/${process.cwd()}/` to + to open it on your online repo or `vscode://file/${process.cwd()}/` to open it in visual studio code), */ // prefix: `vscode://file/${process.cwd()}/`, @@ -235,7 +235,7 @@ module.exports = { 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 */ - // tsPreCompilationDeps: false, + tsPreCompilationDeps: true, /* 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 @@ -271,7 +271,7 @@ module.exports = { to './webpack.conf.js'. The (optional) `env` and `arguments` attributes contain the parameters - to be passed if your webpack config is a function and takes them (see + to be passed if your webpack config is a function and takes them (see webpack documentation for details) */ // webpackConfig: { @@ -322,8 +322,8 @@ module.exports = { A list of alias fields in package.jsons See [this specification](https://github.com/defunctzombie/package-browser-field-spec) and the webpack [resolve.alias](https://webpack.js.org/configuration/resolve/#resolvealiasfields) - documentation - + documentation + Defaults to an empty array (= don't use alias fields). */ // aliasFields: ["browser"], diff --git a/.env.development b/.env.development index e4e5053016f..91c228d6761 100644 --- a/.env.development +++ b/.env.development @@ -1,6 +1,7 @@ VITE_BYPASS_LOGIN=1 VITE_BYPASS_TUTORIAL=0 VITE_SERVER_URL=http://localhost:8001 +# IDs for discord/google auth go unused due to VITE_BYPASS_LOGIN VITE_DISCORD_CLIENT_ID=1234567890 VITE_GOOGLE_CLIENT_ID=1234567890 VITE_I18N_DEBUG=0 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 79ab1bdc38a..979b94f84d6 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -8,7 +8,7 @@ # Art Team /public/**/*.png @pagefaultgames/art-team -/public/**/*.json @pagefaultgames/art-team +/public/**/*.json @pagefaultgames/art-team /public/images @pagefaultgames/art-team /public/battle-anims @pagefaultgames/art-team diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index a25a2f807f3..c7d8b1e4d9c 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -2,25 +2,28 @@ + ## What are the changes the user will see? @@ -66,11 +69,11 @@ Do the reviewers need to do something special in order to test your changes? - [ ] Have I provided a clear explanation of the changes? - [ ] Have I tested the changes manually? - [ ] Are all unit tests still passing? (`npm run test:silent`) - - [ ] Have I created new automated tests (`npm run create-test`) or updated existing tests related to the PR's changes? + - [ ] Have I created new automated tests (`npm run test:create`) or updated existing tests related to the PR's changes? - [ ] Have I provided screenshots/videos of the changes (if applicable)? - [ ] Have I made sure that any UI change works for both UI themes (default and legacy)? Are there any localization additions or changes? If so: - [ ] Has a locales PR been created on the [locales](https://github.com/pagefaultgames/pokerogue-locales) repo? - [ ] If so, please leave a link to it here: -- [ ] Has the translation team been contacted for proofreading/translation? \ No newline at end of file +- [ ] Has the translation team been contacted for proofreading/translation? diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 00190e477d5..a233a2fccab 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -35,7 +35,7 @@ jobs: ssh-keyscan -H ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts - name: Deploy build on server if: github.event_name == 'push' && github.ref_name == 'main' - run: | + run: | rsync --del --no-times --checksum -vrm dist/* ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:${{ secrets.DESTINATION_DIR }} ssh -t ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} "~/prmanifest --inpath ${{ secrets.DESTINATION_DIR }} --outpath ${{ secrets.DESTINATION_DIR }}/manifest.json" - name: Purge Cloudflare Cache diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml new file mode 100644 index 00000000000..0fdbc8b1952 --- /dev/null +++ b/.github/workflows/linting.yml @@ -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 \ No newline at end of file diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml deleted file mode 100644 index d9592662998..00000000000 --- a/.github/workflows/quality.yml +++ /dev/null @@ -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 \ No newline at end of file diff --git a/biome.jsonc b/biome.jsonc index 82ce7c308dc..d183334ad58 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -28,7 +28,6 @@ ".vscode/*", "*.css", // TODO? "*.html", // TODO? - "src/overrides.ts", // TODO: these files are too big and complex, ignore them until their respective refactors "src/data/moves/move.ts", @@ -47,8 +46,8 @@ "correctness": { "noUndeclaredVariables": "off", "noUnusedVariables": "error", - "noSwitchDeclarations": "warn", // TODO: refactor and make this an error - "noVoidTypeReturn": "warn", // TODO: Refactor and make this an error + "noSwitchDeclarations": "error", + "noVoidTypeReturn": "error", "noUnusedImports": "error" }, "style": { @@ -85,7 +84,7 @@ "useLiteralKeys": "off", "noForEach": "off", // Foreach vs for of is not that simple. "noUselessSwitchCase": "off", // Explicit > Implicit - "noUselessConstructor": "warn", // TODO: Refactor and make this an error + "noUselessConstructor": "error", "noBannedTypes": "warn" // TODO: Refactor and make this an error }, "nursery": { @@ -120,6 +119,28 @@ } } } + }, + + // Overrides to prevent unused import removal inside `overrides.ts` and enums files (for TSDoc linkcodes) + { + "include": ["src/overrides.ts", "src/enums/*"], + "linter": { + "rules": { + "correctness": { + "noUnusedImports": "off" + } + } + } + }, + { + "include": ["src/overrides.ts"], + "linter": { + "rules": { + "style": { + "useImportType": "off" + } + } + } } ] } diff --git a/global.d.ts b/global.d.ts index c896a4983e4..d2ed6438c0b 100644 --- a/global.d.ts +++ b/global.d.ts @@ -7,7 +7,7 @@ declare global { * Only used in testing. * Can technically be undefined/null but for ease of use we are going to assume it is always defined. * Used to load i18n files exclusively. - * + * * To set up your own server in a test see `game_data.test.ts` */ var server: SetupServerApi; diff --git a/package.json b/package.json index 36c3c2b919f..cd2b7b18494 100644 --- a/package.json +++ b/package.json @@ -18,9 +18,9 @@ "eslint": "eslint --fix .", "eslint-ci": "eslint .", "biome": "biome check --write --changed --no-errors-on-unmatched", - "biome-ci": "biome ci --diagnostic-level=error --reporter=github --changed --no-errors-on-unmatched", + "biome-ci": "biome ci --diagnostic-level=error --reporter=github --no-errors-on-unmatched", "docs": "typedoc", - "depcruise": "depcruise src", + "depcruise": "depcruise src test", "depcruise:graph": "depcruise src --output-type dot | node dependency-graph.js > dependency-graph.svg", "postinstall": "npx lefthook install && npx lefthook run post-merge", "update-version:patch": "npm version patch --force --no-git-tag-version", diff --git a/public/images/types_ca.png b/public/images/types_ca.png index e85c84ed9c9..a1c295d7b9b 100644 Binary files a/public/images/types_ca.png and b/public/images/types_ca.png differ diff --git a/public/images/types_es-ES.png b/public/images/types_es-ES.png index 8a321e515c4..3596b7f6a0a 100644 Binary files a/public/images/types_es-ES.png and b/public/images/types_es-ES.png differ diff --git a/public/images/types_es-MX.png b/public/images/types_es-MX.png index 134a68258cc..ad159a7c6bb 100644 Binary files a/public/images/types_es-MX.png and b/public/images/types_es-MX.png differ diff --git a/public/images/types_pt-BR.png b/public/images/types_pt-BR.png index 88e3dd98e9d..71bb1471ff2 100644 Binary files a/public/images/types_pt-BR.png and b/public/images/types_pt-BR.png differ diff --git a/public/images/types_ru.png b/public/images/types_ru.png index 571e162e8dc..1e0615f721f 100644 Binary files a/public/images/types_ru.png and b/public/images/types_ru.png differ diff --git a/public/images/types_tr.png b/public/images/types_tr.png index 8b644f1041c..93eae27caa0 100644 Binary files a/public/images/types_tr.png and b/public/images/types_tr.png differ diff --git a/public/images/types_zh-CN.png b/public/images/types_zh-CN.png index a1a41a663fd..e1927ad9d3b 100644 Binary files a/public/images/types_zh-CN.png and b/public/images/types_zh-CN.png differ diff --git a/public/locales b/public/locales index 4dab23d6a78..fade123e20f 160000 --- a/public/locales +++ b/public/locales @@ -1 +1 @@ -Subproject commit 4dab23d6a78b6cf32db43c9953e3c2000f448007 +Subproject commit fade123e20ff951e199d7c0466686fe8c5511643 diff --git a/scripts/create-test/create-test.js b/scripts/create-test/create-test.js index e71994472f3..d5cac5cd408 100644 --- a/scripts/create-test/create-test.js +++ b/scripts/create-test/create-test.js @@ -48,7 +48,7 @@ async function promptTestType() { { type: "list", 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"], }, ]); diff --git a/scripts/create-test/test-boilerplate.ts b/scripts/create-test/test-boilerplate.ts index 40912be6c5f..1fbd3c8040a 100644 --- a/scripts/create-test/test-boilerplate.ts +++ b/scripts/create-test/test-boilerplate.ts @@ -24,7 +24,7 @@ describe("{{description}}", () => { game.override .ability(AbilityId.BALL_FETCH) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH) diff --git a/src/@types/ability-types.ts b/src/@types/ability-types.ts index 5d21aaaa844..6f21a012b64 100644 --- a/src/@types/ability-types.ts +++ b/src/@types/ability-types.ts @@ -1,11 +1,27 @@ -import type { AbAttr } from "#app/data/abilities/ab-attrs/ab-attr"; +import type { AbAttr } from "#app/data/abilities/ability"; import type Move from "#app/data/moves/move"; import type Pokemon from "#app/field/pokemon"; import type { BattleStat } from "#enums/stat"; +import type { AbAttrConstructorMap } from "#app/data/abilities/ability"; -export type AbAttrApplyFunc = (attr: TAttr, passive: boolean) => void; -export type AbAttrSuccessFunc = (attr: TAttr, passive: boolean) => boolean; +// Intentionally re-export all types from the ability attributes module +export type * from "#app/data/abilities/ability"; + +export type AbAttrApplyFunc = (attr: TAttr, passive: boolean, ...args: any[]) => void; +export type AbAttrSuccessFunc = (attr: TAttr, passive: boolean, ...args: any[]) => boolean; export type AbAttrCondition = (pokemon: Pokemon) => boolean; export type PokemonAttackCondition = (user: Pokemon | null, target: Pokemon | null, move: Move) => boolean; export type PokemonDefendCondition = (target: Pokemon, user: Pokemon, move: Move) => boolean; export type PokemonStatStageChangeCondition = (target: Pokemon, statsChanged: BattleStat[], stages: number) => boolean; + +/** + * Union type of all ability attribute class names as strings + */ +export type AbAttrString = keyof AbAttrConstructorMap; + +/** + * Map of ability attribute class names to an instance of the class. + */ +export type AbAttrMap = { + [K in keyof AbAttrConstructorMap]: InstanceType; +}; diff --git a/src/@types/modifier-types.ts b/src/@types/modifier-types.ts new file mode 100644 index 00000000000..80b92c35622 --- /dev/null +++ b/src/@types/modifier-types.ts @@ -0,0 +1,32 @@ +/** + * Re-exports of all the types defined in the modifier module. + */ + +import type Pokemon from "#app/field/pokemon"; +import type { ModifierConstructorMap } from "#app/modifier/modifier"; +import type { ModifierType, WeightedModifierType } from "#app/modifier/modifier-type"; +export type ModifierTypeFunc = () => ModifierType; +export type WeightedModifierTypeWeightFunc = (party: Pokemon[], rerollCount?: number) => number; + +export type { ModifierConstructorMap } from "#app/modifier/modifier"; + +/** + * Map of modifier names to their respective instance types + */ +export type ModifierInstanceMap = { + [K in keyof ModifierConstructorMap]: InstanceType; +}; + +/** + * Union type of all modifier constructors. + */ +export type ModifierClass = ModifierConstructorMap[keyof ModifierConstructorMap]; + +/** + * Union type of all modifier names as strings. + */ +export type ModifierString = keyof ModifierConstructorMap; + +export type ModifierPool = { + [tier: string]: WeightedModifierType[]; +}; diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 5586691a48d..b802466ee19 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -4,7 +4,8 @@ import type Pokemon from "#app/field/pokemon"; import { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon"; import type { PokemonSpeciesFilter } 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 { fixedInt, getIvsFromId, @@ -58,23 +59,15 @@ import { getEnemyModifierTypesForWave, getLuckString, getLuckTextTint, - getModifierPoolForType, - getModifierType, getPartyLuckValue, - ModifierPoolType, - modifierTypes, PokemonHeldItemModifierType, } from "#app/modifier/modifier-type"; +import { getModifierType } from "./utils/modifier-utils"; +import { modifierTypes } from "./data/data-lists"; +import { getModifierPoolForType } from "./utils/modifier-utils"; +import { ModifierPoolType } from "#enums/modifier-pool-type"; import AbilityBar from "#app/ui/ability-bar"; -import { - applyAbAttrs, - applyPostBattleInitAbAttrs, - applyPostItemLostAbAttrs, - BlockItemTheftAbAttr, - DoubleBattleChanceAbAttr, - PostBattleInitAbAttr, - PostItemLostAbAttr, -} from "#app/data/abilities/ability"; +import { applyAbAttrs, applyPostBattleInitAbAttrs, applyPostItemLostAbAttrs } from "./data/abilities/apply-ab-attrs"; import { allAbilities } from "./data/data-lists"; import type { FixedBattleConfig } from "#app/battle"; import Battle from "#app/battle"; @@ -145,14 +138,13 @@ import { LoadingScene } from "#app/loading-scene"; import type { MovePhase } from "#app/phases/move-phase"; import { ShopCursorTarget } from "#app/enums/shop-cursor-target"; import MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; +import { allMysteryEncounters, mysteryEncountersByBiome } from "#app/data/mystery-encounters/mystery-encounters"; import { - allMysteryEncounters, ANTI_VARIANCE_WEIGHT_MODIFIER, AVERAGE_ENCOUNTERS_PER_RUN_TARGET, BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT, MYSTERY_ENCOUNTER_SPAWN_MAX_WEIGHT, - mysteryEncountersByBiome, -} from "#app/data/mystery-encounters/mystery-encounters"; +} from "./constants"; import { MysteryEncounterSaveData } from "#app/data/mystery-encounters/mystery-encounter-save-data"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; @@ -1264,7 +1256,7 @@ export default class BattleScene extends SceneBase { const doubleChance = new NumberHolder(newWaveIndex % 10 === 0 ? 32 : 8); this.applyModifiers(DoubleBattleChanceBoosterModifier, true, doubleChance); for (const p of playerField) { - applyAbAttrs(DoubleBattleChanceAbAttr, p, null, false, doubleChance); + applyAbAttrs("DoubleBattleChanceAbAttr", p, null, false, doubleChance); } return Math.max(doubleChance.value, 1); } @@ -1469,7 +1461,7 @@ export default class BattleScene extends SceneBase { for (const pokemon of this.getPlayerParty()) { pokemon.resetBattleAndWaveData(); pokemon.resetTera(); - applyPostBattleInitAbAttrs(PostBattleInitAbAttr, pokemon); + applyPostBattleInitAbAttrs("PostBattleInitAbAttr", pokemon); if ( pokemon.hasSpecies(SpeciesId.TERAPAGOS) || (this.gameMode.isClassic && this.currentBattle.waveIndex > 180 && this.currentBattle.waveIndex <= 190) @@ -1644,6 +1636,9 @@ export default class BattleScene extends SceneBase { case SpeciesId.TATSUGIRI: case SpeciesId.PALDEA_TAUROS: return randSeedInt(species.forms.length); + case SpeciesId.MAUSHOLD: + case SpeciesId.DUDUNSPARCE: + return !randSeedInt(4) ? 1 : 0; case SpeciesId.PIKACHU: if (this.currentBattle?.battleType === BattleType.TRAINER && this.currentBattle?.waveIndex < 30) { return 0; // Ban Cosplay and Partner Pika from Trainers before wave 30 @@ -2100,12 +2095,15 @@ export default class BattleScene extends SceneBase { } getMaxExpLevel(ignoreLevelCap = false): number { - if (Overrides.LEVEL_CAP_OVERRIDE > 0) { - return Overrides.LEVEL_CAP_OVERRIDE; + const capOverride = Overrides.LEVEL_CAP_OVERRIDE ?? 0; + if (capOverride > 0) { + return capOverride; } - if (ignoreLevelCap || Overrides.LEVEL_CAP_OVERRIDE < 0) { + + if (ignoreLevelCap || capOverride < 0) { return Number.MAX_SAFE_INTEGER; } + const waveIndex = Math.ceil((this.currentBattle?.waveIndex || 1) / 10) * 10; const difficultyWaveIndex = this.gameMode.getWaveForDifficulty(waveIndex); const baseLevel = (1 + difficultyWaveIndex / 2 + Math.pow(difficultyWaveIndex / 25, 2)) * 1.2; @@ -2745,7 +2743,7 @@ export default class BattleScene extends SceneBase { const cancelled = new BooleanHolder(false); if (source && source.isPlayer() !== target.isPlayer()) { - applyAbAttrs(BlockItemTheftAbAttr, source, cancelled); + applyAbAttrs("BlockItemTheftAbAttr", source, cancelled); } if (cancelled.value) { @@ -2785,13 +2783,13 @@ export default class BattleScene extends SceneBase { if (target.isPlayer()) { this.addModifier(newItemModifier, ignoreUpdate, playSound, false, instant); if (source && itemLost) { - applyPostItemLostAbAttrs(PostItemLostAbAttr, source, false); + applyPostItemLostAbAttrs("PostItemLostAbAttr", source, false); } return true; } this.addEnemyModifier(newItemModifier, ignoreUpdate, instant); if (source && itemLost) { - applyPostItemLostAbAttrs(PostItemLostAbAttr, source, false); + applyPostItemLostAbAttrs("PostItemLostAbAttr", source, false); } return true; } @@ -2814,7 +2812,7 @@ export default class BattleScene extends SceneBase { const cancelled = new BooleanHolder(false); if (source && source.isPlayer() !== target.isPlayer()) { - applyAbAttrs(BlockItemTheftAbAttr, source, cancelled); + applyAbAttrs("BlockItemTheftAbAttr", source, cancelled); } if (cancelled.value) { @@ -2969,6 +2967,13 @@ export default class BattleScene extends SceneBase { ) { 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) { if (modifier instanceof PersistentModifier) { @@ -3498,17 +3503,13 @@ export default class BattleScene extends SceneBase { sessionEncounterRate + Math.min(currentRunDiffFromAvg * ANTI_VARIANCE_WEIGHT_MODIFIER, MYSTERY_ENCOUNTER_SPAWN_MAX_WEIGHT / 2); - const successRate = isNullOrUndefined(Overrides.MYSTERY_ENCOUNTER_RATE_OVERRIDE) - ? favoredEncounterRate - : Overrides.MYSTERY_ENCOUNTER_RATE_OVERRIDE!; + const successRate = Overrides.MYSTERY_ENCOUNTER_RATE_OVERRIDE ?? favoredEncounterRate; - // 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 = - encounteredEvents.length === 0 || - waveIndex - encounteredEvents[encounteredEvents.length - 1].waveIndex > 3 || - !isNullOrUndefined(Overrides.MYSTERY_ENCOUNTER_RATE_OVERRIDE); + encounteredEvents.length === 0 || waveIndex - encounteredEvents[encounteredEvents.length - 1].waveIndex > 3; - if (canSpawn) { + if (canSpawn || Overrides.MYSTERY_ENCOUNTER_RATE_OVERRIDE !== null) { let roll = MYSTERY_ENCOUNTER_SPAWN_MAX_WEIGHT; // Always rolls the check on the same offset to ensure no RNG changes from reloading session this.executeWithSeedOffset( diff --git a/src/battle.ts b/src/battle.ts index 0cf01a0873d..245705f4801 100644 --- a/src/battle.ts +++ b/src/battle.ts @@ -13,7 +13,7 @@ import { import Trainer from "./field/trainer"; import { TrainerVariant } from "#enums/trainer-variant"; import type { GameMode } from "./game-mode"; -import { MoneyMultiplierModifier, PokemonHeldItemModifier } from "./modifier/modifier"; +import { MoneyMultiplierModifier, type PokemonHeldItemModifier } from "./modifier/modifier"; import type { PokeballType } from "#enums/pokeball"; import { trainerConfigs } from "#app/data/trainers/trainer-config"; import { SpeciesFormKey } from "#enums/species-form-key"; @@ -30,7 +30,7 @@ import i18next from "#app/plugins/i18n"; import type MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; import type { CustomModifierSettings } from "#app/modifier/modifier-type"; -import { ModifierTier } from "#app/modifier/modifier-tier"; +import { ModifierTier } from "#enums/modifier-tier"; import type { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { BattleType } from "#enums/battle-type"; import { ClassicFixedBossWaves } from "#enums/fixed-boss-waves"; @@ -173,7 +173,7 @@ export default class Battle { this.postBattleLoot.push( ...globalScene .findModifiers( - m => m instanceof PokemonHeldItemModifier && m.pokemonId === enemyPokemon.id && m.isTransferable, + m => m.is("PokemonHeldItemModifier") && m.pokemonId === enemyPokemon.id && m.isTransferable, false, ) .map(i => { diff --git a/src/constants.ts b/src/constants.ts index 62056ecc0d4..f3b37563d11 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -54,3 +54,43 @@ export const defaultStarterSpecies: SpeciesId[] = [ ]; export const saveKey = "x0i2O7WRiANTqPmZ"; // Temporary; secure encryption is not yet necessary + +/** + * Spawn chance: (BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT + WIGHT_INCREMENT_ON_SPAWN_MISS * ) / MYSTERY_ENCOUNTER_SPAWN_MAX_WEIGHT + */ +export const BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT = 3; + +/** + * The divisor for determining ME spawns, defines the "maximum" weight required for a spawn + * If spawn_weight === MYSTERY_ENCOUNTER_SPAWN_MAX_WEIGHT, 100% chance to spawn a ME + */ +export const MYSTERY_ENCOUNTER_SPAWN_MAX_WEIGHT = 256; + +/** + * When an ME spawn roll fails, WEIGHT_INCREMENT_ON_SPAWN_MISS is added to future rolls for ME spawn checks. + * These values are cleared whenever the next ME spawns, and spawn weight returns to BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT + */ +export const WEIGHT_INCREMENT_ON_SPAWN_MISS = 3; + +/** + * Specifies the target average for total ME spawns in a single Classic run. + * Used by anti-variance mechanic to check whether a run is above or below the target on a given wave. + */ +export const AVERAGE_ENCOUNTERS_PER_RUN_TARGET = 12; + +/** + * Will increase/decrease the chance of spawning a ME based on the current run's total MEs encountered vs AVERAGE_ENCOUNTERS_PER_RUN_TARGET + * Example: + * AVERAGE_ENCOUNTERS_PER_RUN_TARGET = 17 (expects avg 1 ME every 10 floors) + * ANTI_VARIANCE_WEIGHT_MODIFIER = 15 + * + * On wave 20, if 1 ME has been encountered, the difference from expected average is 0 MEs. + * So anti-variance adds 0/256 to the spawn weight check for ME spawn. + * + * On wave 20, if 0 MEs have been encountered, the difference from expected average is 1 ME. + * So anti-variance adds 15/256 to the spawn weight check for ME spawn. + * + * On wave 20, if 2 MEs have been encountered, the difference from expected average is -1 ME. + * So anti-variance adds -15/256 to the spawn weight check for ME spawn. + */ +export const ANTI_VARIANCE_WEIGHT_MODIFIER = 15; diff --git a/src/data/abilities/ab-attrs/ab-attr.ts b/src/data/abilities/ab-attrs/ab-attr.ts deleted file mode 100644 index 24fbb6dc338..00000000000 --- a/src/data/abilities/ab-attrs/ab-attr.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { AbAttrCondition } from "#app/@types/ability-types"; -import type Pokemon from "#app/field/pokemon"; -import type { BooleanHolder } from "#app/utils/common"; - -export abstract class AbAttr { - public showAbility: boolean; - private extraCondition: AbAttrCondition; - - /** - * @param showAbility - Whether to show this ability as a flyout during battle; default `true`. - * Should be kept in parity with mainline where possible. - */ - constructor(showAbility = true) { - this.showAbility = showAbility; - } - - /** - * Applies ability effects without checking conditions - * @param _pokemon - The pokemon to apply this ability to - * @param _passive - Whether or not the ability is a passive - * @param _simulated - Whether the call is simulated - * @param _args - Extra args passed to the function. Handled by child classes. - * @see {@linkcode canApply} - */ - apply( - _pokemon: Pokemon, - _passive: boolean, - _simulated: boolean, - _cancelled: BooleanHolder | null, - _args: any[], - ): void {} - - getTriggerMessage(_pokemon: Pokemon, _abilityName: string, ..._args: any[]): string | null { - return null; - } - - getCondition(): AbAttrCondition | null { - return this.extraCondition || null; - } - - addCondition(condition: AbAttrCondition): AbAttr { - this.extraCondition = condition; - return this; - } - - /** - * Returns a boolean describing whether the ability can be applied under current conditions - * @param _pokemon - The pokemon to apply this ability to - * @param _passive - Whether or not the ability is a passive - * @param _simulated - Whether the call is simulated - * @param _args - Extra args passed to the function. Handled by child classes. - * @returns `true` if the ability can be applied, `false` otherwise - * @see {@linkcode apply} - */ - canApply(_pokemon: Pokemon, _passive: boolean, _simulated: boolean, _args: any[]): boolean { - return true; - } -} diff --git a/src/data/abilities/ability-class.ts b/src/data/abilities/ability-class.ts deleted file mode 100644 index 10bd01f3987..00000000000 --- a/src/data/abilities/ability-class.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { AbilityId } from "#enums/ability-id"; -import type { AbAttrCondition } from "#app/@types/ability-types"; -import type { AbAttr } from "#app/data/abilities/ab-attrs/ab-attr"; -import i18next from "i18next"; -import type { Localizable } from "#app/@types/locales"; -import type { Constructor } from "#app/utils/common"; - -export class Ability implements Localizable { - public id: AbilityId; - - private nameAppend: string; - public name: string; - public description: string; - public generation: number; - public isBypassFaint: boolean; - public isIgnorable: boolean; - public isSuppressable = true; - public isCopiable = true; - public isReplaceable = true; - public attrs: AbAttr[]; - public conditions: AbAttrCondition[]; - - constructor(id: AbilityId, generation: number) { - this.id = id; - - this.nameAppend = ""; - this.generation = generation; - this.attrs = []; - this.conditions = []; - - this.isSuppressable = true; - this.isCopiable = true; - this.isReplaceable = true; - - this.localize(); - } - - public get isSwappable(): boolean { - return this.isCopiable && this.isReplaceable; - } - localize(): void { - const i18nKey = AbilityId[this.id] - .split("_") - .filter(f => f) - .map((f, i) => (i ? `${f[0]}${f.slice(1).toLowerCase()}` : f.toLowerCase())) - .join("") as string; - - this.name = this.id ? `${i18next.t(`ability:${i18nKey}.name`) as string}${this.nameAppend}` : ""; - this.description = this.id ? (i18next.t(`ability:${i18nKey}.description`) as string) : ""; - } - - /** - * Get all ability attributes that match `attrType` - * @param attrType any attribute that extends {@linkcode AbAttr} - * @returns Array of attributes that match `attrType`, Empty Array if none match. - */ - getAttrs(attrType: Constructor): T[] { - return this.attrs.filter((a): a is T => a instanceof attrType); - } - - /** - * Check if an ability has an attribute that matches `attrType` - * @param attrType any attribute that extends {@linkcode AbAttr} - * @returns true if the ability has attribute `attrType` - */ - hasAttr(attrType: Constructor): boolean { - return this.attrs.some(attr => attr instanceof attrType); - } - - attr>(AttrType: T, ...args: ConstructorParameters): Ability { - const attr = new AttrType(...args); - this.attrs.push(attr); - - return this; - } - - conditionalAttr>( - condition: AbAttrCondition, - AttrType: T, - ...args: ConstructorParameters - ): Ability { - const attr = new AttrType(...args); - attr.addCondition(condition); - this.attrs.push(attr); - - return this; - } - - bypassFaint(): Ability { - this.isBypassFaint = true; - return this; - } - - ignorable(): Ability { - this.isIgnorable = true; - return this; - } - - unsuppressable(): Ability { - this.isSuppressable = false; - return this; - } - - uncopiable(): Ability { - this.isCopiable = false; - return this; - } - - unreplaceable(): Ability { - this.isReplaceable = false; - return this; - } - - condition(condition: AbAttrCondition): Ability { - this.conditions.push(condition); - - return this; - } - - partial(): this { - this.nameAppend += " (P)"; - return this; - } - - unimplemented(): this { - this.nameAppend += " (N)"; - return this; - } - - /** - * Internal flag used for developers to document edge cases. When using this, please be sure to document the edge case. - * @returns the ability - */ - edgeCase(): this { - return this; - } -} diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index 128e772217f..70195d6a152 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -7,8 +7,8 @@ import { isNullOrUndefined, randSeedItem, randSeedInt, - type Constructor, randSeedFloat, + coerceArray, } from "#app/utils/common"; import { getPokemonNameWithAffix } from "#app/messages"; import { GroundedTag } from "#app/data/battler-tags"; @@ -24,24 +24,21 @@ import { allMoves } from "../data-lists"; import { ArenaTagSide } from "#enums/arena-tag-side"; import { BerryModifier, HitHealModifier, PokemonHeldItemModifier } from "#app/modifier/modifier"; import { TerrainType } from "#app/data/terrain"; +import { pokemonFormChanges } from "../pokemon-forms"; import { - SpeciesFormChangeRevertWeatherFormTrigger, SpeciesFormChangeWeatherTrigger, + SpeciesFormChangeAbilityTrigger, } from "../pokemon-forms/form-change-triggers"; -import { SpeciesFormChangeAbilityTrigger } from "../pokemon-forms/form-change-triggers"; import i18next from "i18next"; import { Command } from "#enums/command"; import { BerryModifierType } from "#app/modifier/modifier-type"; import { getPokeballName } from "#app/data/pokeball"; import { BattleType } from "#enums/battle-type"; -import type { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; import { globalScene } from "#app/global-scene"; import { allAbilities } from "#app/data/data-lists"; -import { AbAttr } from "#app/data/abilities/ab-attrs/ab-attr"; -import { Ability } from "#app/data/abilities/ability-class"; // 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 { PokemonAnimType } from "#enums/pokemon-anim-type"; import { StatusEffect } from "#enums/status-effect"; @@ -55,12 +52,16 @@ import { SwitchType } from "#enums/switch-type"; import { MoveFlags } from "#enums/MoveFlags"; import { MoveTarget } from "#enums/MoveTarget"; import { MoveCategory } from "#enums/MoveCategory"; -import type { BerryType } from "#enums/berry-type"; 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 { noAbilityTypeOverrideMoves } from "#app/data/moves/invalid-moves"; +import { MoveUseMode } from "#enums/move-use-mode"; // 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 { PokemonMove } from "../moves/pokemon-move"; import type Pokemon from "#app/field/pokemon"; @@ -71,13 +72,225 @@ import type { PokemonDefendCondition, PokemonStatStageChangeCondition, PokemonAttackCondition, - AbAttrApplyFunc, - AbAttrSuccessFunc, + AbAttrString, + AbAttrMap, } from "#app/@types/ability-types"; import type { BattlerIndex } from "#enums/battler-index"; import type Move from "#app/data/moves/move"; 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 { applyAbAttrs } from "./apply-ab-attrs"; + +export class Ability implements Localizable { + public id: AbilityId; + + private nameAppend: string; + public name: string; + public description: string; + public generation: number; + public readonly postSummonPriority: number; + public isBypassFaint: boolean; + public isIgnorable: boolean; + public isSuppressable = true; + public isCopiable = true; + public isReplaceable = true; + public attrs: AbAttr[]; + public conditions: AbAttrCondition[]; + + constructor(id: AbilityId, generation: number, postSummonPriority = 0) { + this.id = id; + + this.nameAppend = ""; + this.generation = generation; + this.postSummonPriority = postSummonPriority; + this.attrs = []; + this.conditions = []; + + this.localize(); + } + + public get isSwappable(): boolean { + return this.isCopiable && this.isReplaceable; + } + + localize(): void { + const i18nKey = AbilityId[this.id] + .split("_") + .filter(f => f) + .map((f, i) => (i ? `${f[0]}${f.slice(1).toLowerCase()}` : f.toLowerCase())) + .join("") as string; + + this.name = this.id ? `${i18next.t(`ability:${i18nKey}.name`) as string}${this.nameAppend}` : ""; + this.description = this.id ? (i18next.t(`ability:${i18nKey}.description`) as string) : ""; + } + + /** + * Get all ability attributes that match `attrType` + * @param attrType - any attribute that extends {@linkcode AbAttr} + * @returns Array of attributes that match `attrType`, Empty Array if none match. + */ + getAttrs(attrType: T): AbAttrMap[T][] { + const targetAttr = AbilityAttrs[attrType]; + if (!targetAttr) { + return []; + } + return this.attrs.filter((a): a is AbAttrMap[T] => a instanceof targetAttr); + } + + /** + * Check if an ability has an attribute that matches `attrType` + * @param attrType - any attribute that extends {@linkcode AbAttr} + * @returns true if the ability has attribute `attrType` + */ + hasAttr(attrType: T): boolean { + const targetAttr = AbilityAttrs[attrType]; + if (!targetAttr) { + return false; + } + return this.attrs.some(attr => attr instanceof targetAttr); + } + + attr>(AttrType: T, ...args: ConstructorParameters): Ability { + const attr = new AttrType(...args); + this.attrs.push(attr); + + return this; + } + + conditionalAttr>( + condition: AbAttrCondition, + AttrType: T, + ...args: ConstructorParameters + ): Ability { + const attr = new AttrType(...args); + attr.addCondition(condition); + this.attrs.push(attr); + + return this; + } + + bypassFaint(): Ability { + this.isBypassFaint = true; + return this; + } + + ignorable(): Ability { + this.isIgnorable = true; + return this; + } + + unsuppressable(): Ability { + this.isSuppressable = false; + return this; + } + + uncopiable(): Ability { + this.isCopiable = false; + return this; + } + + unreplaceable(): Ability { + this.isReplaceable = false; + return this; + } + + condition(condition: AbAttrCondition): Ability { + this.conditions.push(condition); + + return this; + } + + partial(): this { + this.nameAppend += " (P)"; + return this; + } + + unimplemented(): this { + this.nameAppend += " (N)"; + return this; + } + + /** + * Internal flag used for developers to document edge cases. When using this, please be sure to document the edge case. + * @returns the ability + */ + edgeCase(): this { + return this; + } +} + +export abstract class AbAttr { + public showAbility: boolean; + private extraCondition: AbAttrCondition; + + /** + * Return whether this attribute is of the given type. + * + * @remarks + * Used to avoid requiring the caller to have imported the specific attribute type, avoiding circular dependencies. + * + * @param attr - The attribute to check against + * @returns Whether the attribute is an instance of the given type + */ + public is(attr: K): this is AbAttrMap[K] { + const targetAttr = AbilityAttrs[attr]; + if (!targetAttr) { + return false; + } + return this instanceof targetAttr; + } + + /** + * @param showAbility - Whether to show this ability as a flyout during battle; default `true`. + * Should be kept in parity with mainline where possible. + */ + constructor(showAbility = true) { + this.showAbility = showAbility; + } + + /** + * Applies ability effects without checking conditions + * @param _pokemon - The pokemon to apply this ability to + * @param _passive - Whether or not the ability is a passive + * @param _simulated - Whether the call is simulated + * @param _args - Extra args passed to the function. Handled by child classes. + * @see {@linkcode canApply} + */ + apply( + _pokemon: Pokemon, + _passive: boolean, + _simulated: boolean, + _cancelled: BooleanHolder | null, + _args: any[], + ): void {} + + getTriggerMessage(_pokemon: Pokemon, _abilityName: string, ..._args: any[]): string | null { + return null; + } + + getCondition(): AbAttrCondition | null { + return this.extraCondition || null; + } + + addCondition(condition: AbAttrCondition): AbAttr { + this.extraCondition = condition; + return this; + } + + /** + * Returns a boolean describing whether the ability can be applied under current conditions + * @param _pokemon - The pokemon to apply this ability to + * @param _passive - Whether or not the ability is a passive + * @param _simulated - Whether the call is simulated + * @param _args - Extra args passed to the function. Handled by child classes. + * @returns `true` if the ability can be applied, `false` otherwise + * @see {@linkcode apply} + */ + canApply(_pokemon: Pokemon, _passive: boolean, _simulated: boolean, _args: any[]): boolean { + return true; + } +} export class BlockRecoilDamageAttr extends AbAttr { constructor() { @@ -131,11 +344,11 @@ export class DoubleBattleChanceAbAttr extends AbAttr { } export class PostBattleInitAbAttr extends AbAttr { - canApplyPostBattleInit(_pokemon: Pokemon, _passive: boolean, _simulated: boolean, _args: any[]): boolean { + canApplyPostBattleInit(_pokemon: Pokemon, _passive: boolean, _simulated: boolean, _args?: any[]): boolean { return true; } - applyPostBattleInit(_pokemon: Pokemon, _passive: boolean, _simulated: boolean, _args: any[]): void {} + applyPostBattleInit(_pokemon: Pokemon, _passive: boolean, _simulated: boolean, _args?: any[]): void {} } export class PostBattleInitFormChangeAbAttr extends PostBattleInitAbAttr { @@ -147,7 +360,7 @@ export class PostBattleInitFormChangeAbAttr extends PostBattleInitAbAttr { this.formFunc = formFunc; } - override canApplyPostBattleInit(pokemon: Pokemon, _passive: boolean, simulated: boolean, _args: any[]): boolean { + override canApplyPostBattleInit(pokemon: Pokemon, _passive: boolean, simulated: boolean, _args: never[]): boolean { const formIndex = this.formFunc(pokemon); return formIndex !== pokemon.formIndex && !simulated; } @@ -1478,7 +1691,7 @@ export class PostDefendContactDamageAbAttr extends PostDefendAbAttr { return ( !simulated && move.doesFlagEffectApply({ flag: MoveFlags.MAKES_CONTACT, user: attacker, target: pokemon }) && - !attacker.hasAbilityWithAttr(BlockNonDirectDamageAbAttr) + !attacker.hasAbilityWithAttr("BlockNonDirectDamageAbAttr") ); } @@ -1657,7 +1870,7 @@ export class PostDefendAbilityGiveAbAttr extends PostDefendAbAttr { return ( move.doesFlagEffectApply({ flag: MoveFlags.MAKES_CONTACT, user: attacker, target: pokemon }) && attacker.getAbility().isSuppressable && - !attacker.getAbility().hasAttr(PostDefendAbilityGiveAbAttr) + !attacker.getAbility().hasAttr("PostDefendAbilityGiveAbAttr") ); } @@ -1704,7 +1917,7 @@ export class PostDefendMoveDisableAbAttr extends PostDefendAbAttr { _args: any[], ): boolean { return ( - attacker.getTag(BattlerTagType.DISABLED) === null && + isNullOrUndefined(attacker.getTag(BattlerTagType.DISABLED)) && move.doesFlagEffectApply({ flag: MoveFlags.MAKES_CONTACT, user: attacker, target: pokemon }) && (this.chance === -1 || pokemon.randBattleSeedInt(100) < this.chance) ); @@ -1924,9 +2137,7 @@ export class FieldMultiplyStatAbAttr extends AbAttr { this.canStack || (!hasApplied.value && this.stat === stat && - checkedPokemon - .getAbilityAttrs(FieldMultiplyStatAbAttr) - .every(attr => (attr as FieldMultiplyStatAbAttr).stat !== stat)) + checkedPokemon.getAbilityAttrs("FieldMultiplyStatAbAttr").every(attr => attr.stat !== stat)) ); } @@ -2524,7 +2735,6 @@ export class AllyStatMultiplierAbAttr extends AbAttr { /** * Takes effect whenever a move succesfully executes, such as gorilla tactics' move-locking. * (More specifically, whenever a move is pushed to the move history) - * @extends AbAttr */ export class ExecutedMoveAbAttr extends AbAttr { canApplyExecutedMove(_pokemon: Pokemon, _simulated: boolean): boolean { @@ -2535,16 +2745,16 @@ export class ExecutedMoveAbAttr extends AbAttr { } /** - * Ability attribute for Gorilla Tactics - * @extends ExecutedMoveAbAttr + * Ability attribute for {@linkcode AbilityId.GORILLA_TACTICS | Gorilla Tactics} + * to lock the user into its first selected move. */ export class GorillaTacticsAbAttr extends ExecutedMoveAbAttr { constructor(showAbility = false) { super(showAbility); } - override canApplyExecutedMove(pokemon: Pokemon, simulated: boolean): boolean { - return simulated || !pokemon.getTag(BattlerTagType.GORILLA_TACTICS); + override canApplyExecutedMove(pokemon: Pokemon, _simulated: boolean): boolean { + return !pokemon.getTag(BattlerTagType.GORILLA_TACTICS); } override applyExecutedMove(pokemon: Pokemon, simulated: boolean): void { @@ -2650,7 +2860,7 @@ export class PostAttackApplyStatusEffectAbAttr extends PostAttackAbAttr { if ( super.canApplyPostAttack(pokemon, passive, simulated, attacker, move, hitResult, args) && (simulated || - (!attacker.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && + (!attacker.hasAbilityWithAttr("IgnoreMoveEffectsAbAttr") && pokemon !== attacker && (!this.contactRequired || move.doesFlagEffectApply({ flag: MoveFlags.MAKES_CONTACT, user: attacker, target: pokemon })) && @@ -2715,7 +2925,7 @@ export class PostAttackApplyBattlerTagAbAttr extends PostAttackAbAttr { /**Battler tags inflicted by abilities post attacking are also considered additional effects.*/ return ( super.canApplyPostAttack(pokemon, passive, simulated, attacker, move, hitResult, args) && - !attacker.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && + !attacker.hasAbilityWithAttr("IgnoreMoveEffectsAbAttr") && pokemon !== attacker && (!this.contactRequired || move.doesFlagEffectApply({ flag: MoveFlags.MAKES_CONTACT, user: attacker, target: pokemon })) && @@ -3308,8 +3518,8 @@ export class PostSummonStatStageChangeAbAttr extends PostSummonAbAttr { for (const opponent of pokemon.getOpponents()) { const cancelled = new BooleanHolder(false); if (this.intimidate) { - applyAbAttrs(IntimidateImmunityAbAttr, opponent, cancelled, simulated); - applyAbAttrs(PostIntimidateStatStageChangeAbAttr, opponent, cancelled, simulated); + applyAbAttrs("IntimidateImmunityAbAttr", opponent, cancelled, simulated); + applyAbAttrs("PostIntimidateStatStageChangeAbAttr", opponent, cancelled, simulated); if (opponent.getTag(BattlerTagType.SUBSTITUTE)) { cancelled.value = true; @@ -3761,27 +3971,32 @@ export class PostSummonFormChangeByWeatherAbAttr extends PostSummonAbAttr { 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 { - const isCastformWithForecast = - pokemon.species.speciesId === SpeciesId.CASTFORM && this.ability === AbilityId.FORECAST; - const isCherrimWithFlowerGift = - pokemon.species.speciesId === SpeciesId.CHERRIM && this.ability === AbilityId.FLOWER_GIFT; - return isCastformWithForecast || isCherrimWithFlowerGift; + return !!pokemonFormChanges[pokemon.species.speciesId]?.some( + fc => fc.findTrigger(SpeciesFormChangeWeatherTrigger) && fc.canChange(pokemon), + ); } /** - * Calls the {@linkcode BattleScene.triggerPokemonFormChange | triggerPokemonFormChange} for both - * {@linkcode SpeciesFormChange.SpeciesFormChangeWeatherTrigger | SpeciesFormChangeWeatherTrigger} and - * {@linkcode SpeciesFormChange.SpeciesFormChangeWeatherTrigger | SpeciesFormChangeRevertWeatherFormTrigger} if it - * is the specific Pokemon and ability - * @param {Pokemon} pokemon the Pokemon with this ability - * @param _passive n/a - * @param _args n/a + * Trigger the pokemon's forme change by invoking + * {@linkcode BattleScene.triggerPokemonFormChange | triggerPokemonFormChange} + * + * @param pokemon - The Pokemon with this ability + * @param _passive - unused + * @param simulated - unused + * @param _args - unused */ override applyPostSummon(pokemon: Pokemon, _passive: boolean, simulated: boolean, _args: any[]): void { if (!simulated) { globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeWeatherTrigger); - globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeRevertWeatherFormTrigger); } } } @@ -4407,7 +4622,7 @@ export class ConditionalUserFieldProtectStatAbAttr extends PreStatStageChangeAbA * @param stat The stat being affected * @param cancelled Holds whether the stat change was already prevented. * @param args Args[0] is the target pokemon of the stat change. - * @returns + * @returns `true` if the ability can be applied */ override canApplyPreStatStageChange( _pokemon: Pokemon, @@ -4481,7 +4696,7 @@ export class PreApplyBattlerTagImmunityAbAttr extends PreApplyBattlerTagAbAttr { constructor(immuneTagTypes: BattlerTagType | BattlerTagType[]) { super(true); - this.immuneTagTypes = Array.isArray(immuneTagTypes) ? immuneTagTypes : [immuneTagTypes]; + this.immuneTagTypes = coerceArray(immuneTagTypes); } override canApplyPreApplyBattlerTag( @@ -4568,17 +4783,17 @@ export class BlockCritAbAttr extends AbAttr { } /** - * Apply the block crit ability by setting the value in the provided boolean holder to false - * @param args - [0] is a boolean holder representing whether the attack can crit + * Apply the block crit ability by setting the value in the provided boolean holder to `true`. + * @param args - `[0]`: A {@linkcode BooleanHolder} containing whether the attack is prevented from critting. */ override apply( _pokemon: Pokemon, _passive: boolean, _simulated: boolean, _cancelled: BooleanHolder, - args: [BooleanHolder, ...any], + args: [BooleanHolder], ): void { - args[0].value = false; + args[0].value = true; } } @@ -5091,10 +5306,11 @@ export class PostWeatherChangeFormChangeAbAttr extends PostWeatherChangeAbAttr { /** * Calls {@linkcode Arena.triggerWeatherBasedFormChangesToNormal | triggerWeatherBasedFormChangesToNormal} when the * weather changed to form-reverting weather, otherwise calls {@linkcode Arena.triggerWeatherBasedFormChanges | triggerWeatherBasedFormChanges} - * @param {Pokemon} _pokemon the Pokemon with this ability - * @param _passive n/a - * @param _weather n/a - * @param _args n/a + * @param _pokemon - The Pokemon with this ability + * @param _passive - unused + * @param simulated - unused + * @param _weather - unused + * @param _args - unused */ override applyPostWeatherChange( _pokemon: Pokemon, @@ -5243,7 +5459,7 @@ export class PostWeatherLapseDamageAbAttr extends PostWeatherLapseAbAttr { _weather: Weather | null, _args: any[], ): boolean { - return !pokemon.hasAbilityWithAttr(BlockNonDirectDamageAbAttr); + return !pokemon.hasAbilityWithAttr("BlockNonDirectDamageAbAttr"); } override applyPostWeatherLapse( @@ -5544,7 +5760,7 @@ export class RepeatBerryNextTurnAbAttr extends PostTurnAbAttr { } // uncomment to make cheek pouch work with cud chew - // applyAbAttrs(HealFromBerryUseAbAttr, pokemon, new BooleanHolder(false)); + // applyAbAttrs("HealFromBerryUseAbAttr", pokemon, new BooleanHolder(false)); } /** @@ -5671,7 +5887,7 @@ export class PostTurnHurtIfSleepingAbAttr extends PostTurnAbAttr { .some( opp => (opp.status?.effect === StatusEffect.SLEEP || opp.hasAbility(AbilityId.COMATOSE)) && - !opp.hasAbilityWithAttr(BlockNonDirectDamageAbAttr) && + !opp.hasAbilityWithAttr("BlockNonDirectDamageAbAttr") && !opp.switchOutStatus, ); } @@ -5686,7 +5902,7 @@ export class PostTurnHurtIfSleepingAbAttr extends PostTurnAbAttr { for (const opp of pokemon.getOpponents()) { if ( (opp.status?.effect === StatusEffect.SLEEP || opp.hasAbility(AbilityId.COMATOSE)) && - !opp.hasAbilityWithAttr(BlockNonDirectDamageAbAttr) && + !opp.hasAbilityWithAttr("BlockNonDirectDamageAbAttr") && !opp.switchOutStatus ) { if (!simulated) { @@ -5854,14 +6070,19 @@ export class PostDancingMoveAbAttr extends PostMoveUsedAbAttr { ): void { if (!simulated) { 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 (move.getMove().is("AttackMove") || move.getMove().is("StatusMove")) { 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")) { // 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, + ); } } } @@ -6326,8 +6547,8 @@ export class PostFaintContactDamageAbAttr extends PostFaintAbAttr { attacker !== undefined && move.doesFlagEffectApply({ flag: MoveFlags.MAKES_CONTACT, user: attacker, target: pokemon }); const cancelled = new BooleanHolder(false); - globalScene.getField(true).map(p => applyAbAttrs(FieldPreventExplosiveMovesAbAttr, p, cancelled, simulated)); - return !(!diedToDirectDamage || cancelled.value || attacker!.hasAbilityWithAttr(BlockNonDirectDamageAbAttr)); + globalScene.getField(true).map(p => applyAbAttrs("FieldPreventExplosiveMovesAbAttr", p, cancelled, simulated)); + return !(!diedToDirectDamage || cancelled.value || attacker!.hasAbilityWithAttr("BlockNonDirectDamageAbAttr")); } override applyPostFaint( @@ -6486,7 +6707,7 @@ export class FlinchStatStageChangeAbAttr extends FlinchEffectAbAttr { constructor(stats: BattleStat[], stages: number) { super(); - this.stats = Array.isArray(stats) ? stats : [stats]; + this.stats = stats; this.stages = stages; } @@ -7160,64 +7381,6 @@ export class TerrainEventTypeChangeAbAttr extends PostSummonAbAttr { } } -function applySingleAbAttrs( - pokemon: Pokemon, - passive: boolean, - attrType: Constructor, - applyFunc: AbAttrApplyFunc, - successFunc: AbAttrSuccessFunc, - args: any[], - gainedMidTurn = false, - simulated = false, - messages: string[] = [], -) { - if (!pokemon?.canApplyAbility(passive) || (passive && pokemon.getPassiveAbility().id === pokemon.getAbility().id)) { - return; - } - - const ability = passive ? pokemon.getPassiveAbility() : pokemon.getAbility(); - if ( - gainedMidTurn && - ability.getAttrs(attrType).some(attr => attr instanceof PostSummonAbAttr && !attr.shouldActivateOnGain()) - ) { - return; - } - - for (const attr of ability.getAttrs(attrType)) { - const condition = attr.getCondition(); - let abShown = false; - if ((condition && !condition(pokemon)) || !successFunc(attr, passive)) { - continue; - } - - globalScene.phaseManager.setPhaseQueueSplice(); - - if (attr.showAbility && !simulated) { - globalScene.phaseManager.queueAbilityDisplay(pokemon, passive, true); - abShown = true; - } - const message = attr.getTriggerMessage(pokemon, ability.name, args); - if (message) { - if (!simulated) { - globalScene.phaseManager.queueMessage(message); - } - messages.push(message); - } - - applyFunc(attr, passive); - - if (abShown) { - globalScene.phaseManager.queueAbilityDisplay(pokemon, passive, false); - } - - if (!simulated) { - pokemon.waveData.abilitiesApplied.add(ability.id); - } - - globalScene.phaseManager.clearPhaseQueueSplice(); - } -} - class ForceSwitchOutHelper { constructor(private switchType: SwitchType) {} @@ -7227,6 +7390,7 @@ class ForceSwitchOutHelper { * @param pokemon The {@linkcode Pokemon} attempting to switch out. * @returns `true` if the switch is successful */ + // TODO: Make this cancel pending move phases on the switched out target public switchOutLogic(pokemon: Pokemon): boolean { const switchOutTarget = pokemon; /** @@ -7329,7 +7493,7 @@ class ForceSwitchOutHelper { if (player) { const blockedByAbility = new BooleanHolder(false); - applyAbAttrs(ForceSwitchOutImmunityAbAttr, opponent, blockedByAbility); + applyAbAttrs("ForceSwitchOutImmunityAbAttr", opponent, blockedByAbility); return !blockedByAbility.value; } @@ -7367,7 +7531,7 @@ class ForceSwitchOutHelper { */ public getFailedText(target: Pokemon): string | null { const blockedByAbility = new BooleanHolder(false); - applyAbAttrs(ForceSwitchOutImmunityAbAttr, target, blockedByAbility); + applyAbAttrs("ForceSwitchOutImmunityAbAttr", target, blockedByAbility); return blockedByAbility.value ? i18next.t("moveTriggers:cannotBeSwitchedOut", { pokemonName: getPokemonNameWithAffix(target) }) : null; @@ -7522,646 +7686,233 @@ export class PostDamageForceSwitchAbAttr extends PostDamageAbAttr { this.helper.switchOutLogic(pokemon); } } -function applyAbAttrsInternal( - attrType: Constructor, - pokemon: Pokemon | null, - applyFunc: AbAttrApplyFunc, - successFunc: AbAttrSuccessFunc, - args: any[], - simulated = false, - messages: string[] = [], - gainedMidTurn = false, -) { - for (const passive of [false, true]) { - if (pokemon) { - applySingleAbAttrs(pokemon, passive, attrType, applyFunc, successFunc, args, gainedMidTurn, simulated, messages); - globalScene.phaseManager.clearPhaseQueueSplice(); - } - } -} - -export function applyAbAttrs( - attrType: Constructor, - pokemon: Pokemon, - cancelled: BooleanHolder | null, - simulated = false, - ...args: any[] -): void { - applyAbAttrsInternal( - attrType, - pokemon, - (attr, passive) => attr.apply(pokemon, passive, simulated, cancelled, args), - (attr, passive) => attr.canApply(pokemon, passive, simulated, args), - args, - simulated, - ); -} - -export function applyPostBattleInitAbAttrs( - attrType: Constructor, - pokemon: Pokemon, - simulated = false, - ...args: any[] -): void { - applyAbAttrsInternal( - attrType, - pokemon, - (attr, passive) => attr.applyPostBattleInit(pokemon, passive, simulated, args), - (attr, passive) => attr.canApplyPostBattleInit(pokemon, passive, simulated, args), - args, - simulated, - ); -} - -export function applyPreDefendAbAttrs( - attrType: Constructor, - pokemon: Pokemon, - attacker: Pokemon, - move: Move | null, - cancelled: BooleanHolder | null, - simulated = false, - ...args: any[] -): void { - applyAbAttrsInternal( - attrType, - pokemon, - (attr, passive) => attr.applyPreDefend(pokemon, passive, simulated, attacker, move, cancelled, args), - (attr, passive) => attr.canApplyPreDefend(pokemon, passive, simulated, attacker, move, cancelled, args), - args, - simulated, - ); -} - -export function applyPostDefendAbAttrs( - attrType: Constructor, - pokemon: Pokemon, - attacker: Pokemon, - move: Move, - hitResult: HitResult | null, - simulated = false, - ...args: any[] -): void { - applyAbAttrsInternal( - attrType, - pokemon, - (attr, passive) => attr.applyPostDefend(pokemon, passive, simulated, attacker, move, hitResult, args), - (attr, passive) => attr.canApplyPostDefend(pokemon, passive, simulated, attacker, move, hitResult, args), - args, - simulated, - ); -} - -export function applyPostMoveUsedAbAttrs( - attrType: Constructor, - pokemon: Pokemon, - move: PokemonMove, - source: Pokemon, - targets: BattlerIndex[], - simulated = false, - ...args: any[] -): void { - applyAbAttrsInternal( - attrType, - pokemon, - (attr, _passive) => attr.applyPostMoveUsed(pokemon, move, source, targets, simulated, args), - (attr, _passive) => attr.canApplyPostMoveUsed(pokemon, move, source, targets, simulated, args), - args, - simulated, - ); -} - -export function applyStatMultiplierAbAttrs( - attrType: Constructor, - pokemon: Pokemon, - stat: BattleStat, - statValue: NumberHolder, - simulated = false, - ...args: any[] -): void { - applyAbAttrsInternal( - attrType, - pokemon, - (attr, passive) => attr.applyStatStage(pokemon, passive, simulated, stat, statValue, args), - (attr, passive) => attr.canApplyStatStage(pokemon, passive, simulated, stat, statValue, args), - args, - ); -} /** - * Applies an ally's Stat multiplier attribute - * @param attrType - {@linkcode AllyStatMultiplierAbAttr} should always be AllyStatMultiplierAbAttr for the time being - * @param pokemon - The {@linkcode Pokemon} with the ability - * @param stat - The type of the checked {@linkcode Stat} - * @param statValue - {@linkcode NumberHolder} containing the value of the checked stat - * @param checkedPokemon - The {@linkcode Pokemon} with the checked stat - * @param ignoreAbility - Whether or not the ability should be ignored by the pokemon or its move. - * @param args - unused + * Map of all ability attribute constructors, for use with the `.is` method. */ -export function applyAllyStatMultiplierAbAttrs( - attrType: Constructor, - pokemon: Pokemon, - stat: BattleStat, - statValue: NumberHolder, - simulated = false, - checkedPokemon: Pokemon, - ignoreAbility: boolean, - ...args: any[] -): void { - applyAbAttrsInternal( - attrType, - pokemon, - (attr, passive) => - attr.applyAllyStat(pokemon, passive, simulated, stat, statValue, checkedPokemon, ignoreAbility, args), - (attr, passive) => - attr.canApplyAllyStat(pokemon, passive, simulated, stat, statValue, checkedPokemon, ignoreAbility, args), - args, - simulated, - ); -} - -export function applyPostSetStatusAbAttrs( - attrType: Constructor, - pokemon: Pokemon, - effect: StatusEffect, - sourcePokemon?: Pokemon | null, - simulated = false, - ...args: any[] -): void { - applyAbAttrsInternal( - attrType, - pokemon, - (attr, passive) => attr.applyPostSetStatus(pokemon, sourcePokemon, passive, effect, simulated, args), - (attr, passive) => attr.canApplyPostSetStatus(pokemon, sourcePokemon, passive, effect, simulated, args), - args, - simulated, - ); -} - -export function applyPostDamageAbAttrs( - attrType: Constructor, - pokemon: Pokemon, - damage: number, - _passive: boolean, - simulated = false, - args: any[], - source?: Pokemon, -): void { - applyAbAttrsInternal( - attrType, - pokemon, - (attr, passive) => attr.applyPostDamage(pokemon, damage, passive, simulated, args, source), - (attr, passive) => attr.canApplyPostDamage(pokemon, damage, passive, simulated, args, source), - args, - ); -} +const AbilityAttrs = Object.freeze({ + BlockRecoilDamageAttr, + DoubleBattleChanceAbAttr, + PostBattleInitAbAttr, + PostBattleInitFormChangeAbAttr, + PostTeraFormChangeStatChangeAbAttr, + ClearWeatherAbAttr, + ClearTerrainAbAttr, + PreDefendAbAttr, + PreDefendFullHpEndureAbAttr, + BlockItemTheftAbAttr, + StabBoostAbAttr, + ReceivedMoveDamageMultiplierAbAttr, + AlliedFieldDamageReductionAbAttr, + ReceivedTypeDamageMultiplierAbAttr, + TypeImmunityAbAttr, + AttackTypeImmunityAbAttr, + TypeImmunityHealAbAttr, + NonSuperEffectiveImmunityAbAttr, + FullHpResistTypeAbAttr, + PostDefendAbAttr, + FieldPriorityMoveImmunityAbAttr, + PostStatStageChangeAbAttr, + MoveImmunityAbAttr, + WonderSkinAbAttr, + MoveImmunityStatStageChangeAbAttr, + ReverseDrainAbAttr, + PostDefendStatStageChangeAbAttr, + PostDefendHpGatedStatStageChangeAbAttr, + PostDefendApplyArenaTrapTagAbAttr, + PostDefendApplyBattlerTagAbAttr, + PostDefendTypeChangeAbAttr, + PostDefendTerrainChangeAbAttr, + PostDefendContactApplyStatusEffectAbAttr, + EffectSporeAbAttr, + PostDefendContactApplyTagChanceAbAttr, + PostDefendCritStatStageChangeAbAttr, + PostDefendContactDamageAbAttr, + PostDefendPerishSongAbAttr, + PostDefendWeatherChangeAbAttr, + PostDefendAbilitySwapAbAttr, + PostDefendAbilityGiveAbAttr, + PostDefendMoveDisableAbAttr, + PostStatStageChangeStatStageChangeAbAttr, + PreAttackAbAttr, + MoveEffectChanceMultiplierAbAttr, + IgnoreMoveEffectsAbAttr, + VariableMovePowerAbAttr, + FieldPreventExplosiveMovesAbAttr, + FieldMultiplyStatAbAttr, + MoveTypeChangeAbAttr, + PokemonTypeChangeAbAttr, + AddSecondStrikeAbAttr, + DamageBoostAbAttr, + MovePowerBoostAbAttr, + MoveTypePowerBoostAbAttr, + LowHpMoveTypePowerBoostAbAttr, + VariableMovePowerBoostAbAttr, + FieldMovePowerBoostAbAttr, + PreAttackFieldMoveTypePowerBoostAbAttr, + FieldMoveTypePowerBoostAbAttr, + UserFieldMoveTypePowerBoostAbAttr, + AllyMoveCategoryPowerBoostAbAttr, + StatMultiplierAbAttr, + PostAttackAbAttr, + AllyStatMultiplierAbAttr, + ExecutedMoveAbAttr, + GorillaTacticsAbAttr, + PostAttackStealHeldItemAbAttr, + PostAttackApplyStatusEffectAbAttr, + PostAttackContactApplyStatusEffectAbAttr, + PostAttackApplyBattlerTagAbAttr, + PostDefendStealHeldItemAbAttr, + PostSetStatusAbAttr, + SynchronizeStatusAbAttr, + PostVictoryAbAttr, + PostVictoryFormChangeAbAttr, + PostKnockOutAbAttr, + PostKnockOutStatStageChangeAbAttr, + CopyFaintedAllyAbilityAbAttr, + IgnoreOpponentStatStagesAbAttr, + IntimidateImmunityAbAttr, + PostIntimidateStatStageChangeAbAttr, + PostSummonAbAttr, + PostSummonRemoveEffectAbAttr, + PostSummonRemoveArenaTagAbAttr, + PostSummonAddArenaTagAbAttr, + PostSummonMessageAbAttr, + PostSummonUnnamedMessageAbAttr, + PostSummonAddBattlerTagAbAttr, + PostSummonRemoveBattlerTagAbAttr, + PostSummonStatStageChangeAbAttr, + PostSummonAllyHealAbAttr, + PostSummonClearAllyStatStagesAbAttr, + DownloadAbAttr, + PostSummonWeatherChangeAbAttr, + PostSummonTerrainChangeAbAttr, + PostSummonHealStatusAbAttr, + PostSummonFormChangeAbAttr, + PostSummonCopyAbilityAbAttr, + PostSummonUserFieldRemoveStatusEffectAbAttr, + PostSummonCopyAllyStatsAbAttr, + PostSummonTransformAbAttr, + PostSummonWeatherSuppressedFormChangeAbAttr, + PostSummonFormChangeByWeatherAbAttr, + CommanderAbAttr, + PreSwitchOutAbAttr, + PreSwitchOutResetStatusAbAttr, + PreSwitchOutClearWeatherAbAttr, + PreSwitchOutHealAbAttr, + PreSwitchOutFormChangeAbAttr, + PreLeaveFieldAbAttr, + PreLeaveFieldClearWeatherAbAttr, + PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr, + PreStatStageChangeAbAttr, + ReflectStatStageChangeAbAttr, + ProtectStatAbAttr, + ConfusionOnStatusEffectAbAttr, + PreSetStatusAbAttr, + PreSetStatusEffectImmunityAbAttr, + StatusEffectImmunityAbAttr, + UserFieldStatusEffectImmunityAbAttr, + ConditionalUserFieldStatusEffectImmunityAbAttr, + ConditionalUserFieldProtectStatAbAttr, + PreApplyBattlerTagAbAttr, + PreApplyBattlerTagImmunityAbAttr, + BattlerTagImmunityAbAttr, + UserFieldBattlerTagImmunityAbAttr, + ConditionalUserFieldBattlerTagImmunityAbAttr, + BlockCritAbAttr, + BonusCritAbAttr, + MultCritAbAttr, + ConditionalCritAbAttr, + BlockNonDirectDamageAbAttr, + BlockStatusDamageAbAttr, + BlockOneHitKOAbAttr, + ChangeMovePriorityAbAttr, + IgnoreContactAbAttr, + PreWeatherEffectAbAttr, + PreWeatherDamageAbAttr, + SuppressWeatherEffectAbAttr, + ForewarnAbAttr, + FriskAbAttr, + PostWeatherChangeAbAttr, + PostWeatherChangeFormChangeAbAttr, + PostWeatherLapseAbAttr, + PostWeatherLapseHealAbAttr, + PostWeatherLapseDamageAbAttr, + PostTerrainChangeAbAttr, + PostTurnAbAttr, + PostTurnStatusHealAbAttr, + PostTurnResetStatusAbAttr, + PostTurnRestoreBerryAbAttr, + RepeatBerryNextTurnAbAttr, + MoodyAbAttr, + SpeedBoostAbAttr, + PostTurnHealAbAttr, + PostTurnFormChangeAbAttr, + PostTurnHurtIfSleepingAbAttr, + FetchBallAbAttr, + PostBiomeChangeAbAttr, + PostBiomeChangeWeatherChangeAbAttr, + PostBiomeChangeTerrainChangeAbAttr, + PostMoveUsedAbAttr, + PostDancingMoveAbAttr, + PostItemLostAbAttr, + PostItemLostApplyBattlerTagAbAttr, + StatStageChangeMultiplierAbAttr, + StatStageChangeCopyAbAttr, + BypassBurnDamageReductionAbAttr, + ReduceBurnDamageAbAttr, + DoubleBerryEffectAbAttr, + PreventBerryUseAbAttr, + HealFromBerryUseAbAttr, + RunSuccessAbAttr, + CheckTrappedAbAttr, + ArenaTrapAbAttr, + MaxMultiHitAbAttr, + PostBattleAbAttr, + PostBattleLootAbAttr, + PostFaintAbAttr, + PostFaintUnsuppressedWeatherFormChangeAbAttr, + PostFaintContactDamageAbAttr, + PostFaintHPDamageAbAttr, + RedirectMoveAbAttr, + RedirectTypeMoveAbAttr, + BlockRedirectAbAttr, + ReduceStatusEffectDurationAbAttr, + FlinchEffectAbAttr, + FlinchStatStageChangeAbAttr, + IncreasePpAbAttr, + ForceSwitchOutImmunityAbAttr, + ReduceBerryUseThresholdAbAttr, + WeightMultiplierAbAttr, + SyncEncounterNatureAbAttr, + MoveAbilityBypassAbAttr, + AlwaysHitAbAttr, + IgnoreProtectOnContactAbAttr, + InfiltratorAbAttr, + ReflectStatusMoveAbAttr, + NoTransformAbilityAbAttr, + NoFusionAbilityAbAttr, + IgnoreTypeImmunityAbAttr, + IgnoreTypeStatusEffectImmunityAbAttr, + MoneyAbAttr, + PostSummonStatStageChangeOnArenaAbAttr, + FormBlockDamageAbAttr, + PreSummonAbAttr, + IllusionPreSummonAbAttr, + IllusionBreakAbAttr, + PostDefendIllusionBreakAbAttr, + IllusionPostBattleAbAttr, + BypassSpeedChanceAbAttr, + PreventBypassSpeedChanceAbAttr, + TerrainEventTypeChangeAbAttr, + PostDamageAbAttr, + PostDamageForceSwitchAbAttr, +}); /** - * Applies a field Stat multiplier attribute - * @param attrType {@linkcode FieldMultiplyStatAbAttr} should always be FieldMultiplyBattleStatAbAttr for the time being - * @param pokemon {@linkcode Pokemon} the Pokemon applying this ability - * @param stat {@linkcode Stat} the type of the checked stat - * @param statValue {@linkcode NumberHolder} the value of the checked stat - * @param checkedPokemon {@linkcode Pokemon} the Pokemon with the checked stat - * @param hasApplied {@linkcode BooleanHolder} whether or not a FieldMultiplyBattleStatAbAttr has already affected this stat - * @param args unused + * A map of of all {@linkcode AbAttr} constructors */ -export function applyFieldStatMultiplierAbAttrs( - attrType: Constructor, - pokemon: Pokemon, - stat: Stat, - statValue: NumberHolder, - checkedPokemon: Pokemon, - hasApplied: BooleanHolder, - simulated = false, - ...args: any[] -): void { - applyAbAttrsInternal( - attrType, - pokemon, - (attr, passive) => - attr.applyFieldStat(pokemon, passive, simulated, stat, statValue, checkedPokemon, hasApplied, args), - (attr, passive) => - attr.canApplyFieldStat(pokemon, passive, simulated, stat, statValue, checkedPokemon, hasApplied, args), - args, - ); -} - -export function applyPreAttackAbAttrs( - attrType: Constructor, - pokemon: Pokemon, - defender: Pokemon | null, - move: Move, - simulated = false, - ...args: any[] -): void { - applyAbAttrsInternal( - attrType, - pokemon, - (attr, passive) => attr.applyPreAttack(pokemon, passive, simulated, defender, move, args), - (attr, passive) => attr.canApplyPreAttack(pokemon, passive, simulated, defender, move, args), - args, - simulated, - ); -} - -export function applyExecutedMoveAbAttrs( - attrType: Constructor, - pokemon: Pokemon, - simulated = false, - ...args: any[] -): void { - applyAbAttrsInternal( - attrType, - pokemon, - attr => attr.applyExecutedMove(pokemon, simulated), - attr => attr.canApplyExecutedMove(pokemon, simulated), - args, - simulated, - ); -} - -export function applyPostAttackAbAttrs( - attrType: Constructor, - pokemon: Pokemon, - defender: Pokemon, - move: Move, - hitResult: HitResult | null, - simulated = false, - ...args: any[] -): void { - applyAbAttrsInternal( - attrType, - pokemon, - (attr, passive) => attr.applyPostAttack(pokemon, passive, simulated, defender, move, hitResult, args), - (attr, passive) => attr.canApplyPostAttack(pokemon, passive, simulated, defender, move, hitResult, args), - args, - simulated, - ); -} - -export function applyPostKnockOutAbAttrs( - attrType: Constructor, - pokemon: Pokemon, - knockedOut: Pokemon, - simulated = false, - ...args: any[] -): void { - applyAbAttrsInternal( - attrType, - pokemon, - (attr, passive) => attr.applyPostKnockOut(pokemon, passive, simulated, knockedOut, args), - (attr, passive) => attr.canApplyPostKnockOut(pokemon, passive, simulated, knockedOut, args), - args, - simulated, - ); -} - -export function applyPostVictoryAbAttrs( - attrType: Constructor, - pokemon: Pokemon, - simulated = false, - ...args: any[] -): void { - applyAbAttrsInternal( - attrType, - pokemon, - (attr, passive) => attr.applyPostVictory(pokemon, passive, simulated, args), - (attr, passive) => attr.canApplyPostVictory(pokemon, passive, simulated, args), - args, - simulated, - ); -} - -export function applyPostSummonAbAttrs( - attrType: Constructor, - pokemon: Pokemon, - simulated = false, - ...args: any[] -): void { - applyAbAttrsInternal( - attrType, - pokemon, - (attr, passive) => attr.applyPostSummon(pokemon, passive, simulated, args), - (attr, passive) => attr.canApplyPostSummon(pokemon, passive, simulated, args), - args, - simulated, - ); -} - -export function applyPreSummonAbAttrs(attrType: Constructor, pokemon: Pokemon, ...args: any[]): void { - applyAbAttrsInternal( - attrType, - pokemon, - (attr, passive) => attr.applyPreSummon(pokemon, passive, args), - (attr, passive) => attr.canApplyPreSummon(pokemon, passive, args), - args, - ); -} - -export function applyPreSwitchOutAbAttrs( - attrType: Constructor, - pokemon: Pokemon, - simulated = false, - ...args: any[] -): void { - applyAbAttrsInternal( - attrType, - pokemon, - (attr, passive) => attr.applyPreSwitchOut(pokemon, passive, simulated, args), - (attr, passive) => attr.canApplyPreSwitchOut(pokemon, passive, simulated, args), - args, - simulated, - ); -} - -export function applyPreLeaveFieldAbAttrs( - attrType: Constructor, - pokemon: Pokemon, - simulated = false, - ...args: any[] -): void { - applyAbAttrsInternal( - attrType, - pokemon, - (attr, passive) => attr.applyPreLeaveField(pokemon, passive, simulated, args), - (attr, passive) => attr.canApplyPreLeaveField(pokemon, passive, simulated, args), - args, - simulated, - ); -} - -export function applyPreStatStageChangeAbAttrs( - attrType: Constructor, - pokemon: Pokemon | null, - stat: BattleStat, - cancelled: BooleanHolder, - simulated = false, - ...args: any[] -): void { - applyAbAttrsInternal( - attrType, - pokemon, - (attr, passive) => attr.applyPreStatStageChange(pokemon, passive, simulated, stat, cancelled, args), - (attr, passive) => attr.canApplyPreStatStageChange(pokemon, passive, simulated, stat, cancelled, args), - args, - simulated, - ); -} - -export function applyPostStatStageChangeAbAttrs( - attrType: Constructor, - pokemon: Pokemon, - stats: BattleStat[], - stages: number, - selfTarget: boolean, - simulated = false, - ...args: any[] -): void { - applyAbAttrsInternal( - attrType, - pokemon, - (attr, _passive) => attr.applyPostStatStageChange(pokemon, simulated, stats, stages, selfTarget, args), - (attr, _passive) => attr.canApplyPostStatStageChange(pokemon, simulated, stats, stages, selfTarget, args), - args, - simulated, - ); -} - -export function applyPreSetStatusAbAttrs( - attrType: Constructor, - pokemon: Pokemon, - effect: StatusEffect | undefined, - cancelled: BooleanHolder, - simulated = false, - ...args: any[] -): void { - applyAbAttrsInternal( - attrType, - pokemon, - (attr, passive) => attr.applyPreSetStatus(pokemon, passive, simulated, effect, cancelled, args), - (attr, passive) => attr.canApplyPreSetStatus(pokemon, passive, simulated, effect, cancelled, args), - args, - simulated, - ); -} - -export function applyPreApplyBattlerTagAbAttrs( - attrType: Constructor, - pokemon: Pokemon, - tag: BattlerTag, - cancelled: BooleanHolder, - simulated = false, - ...args: any[] -): void { - applyAbAttrsInternal( - attrType, - pokemon, - (attr, passive) => attr.applyPreApplyBattlerTag(pokemon, passive, simulated, tag, cancelled, args), - (attr, passive) => attr.canApplyPreApplyBattlerTag(pokemon, passive, simulated, tag, cancelled, args), - args, - simulated, - ); -} - -export function applyPreWeatherEffectAbAttrs( - attrType: Constructor, - pokemon: Pokemon, - weather: Weather | null, - cancelled: BooleanHolder, - simulated = false, - ...args: any[] -): void { - applyAbAttrsInternal( - attrType, - pokemon, - (attr, passive) => attr.applyPreWeatherEffect(pokemon, passive, simulated, weather, cancelled, args), - (attr, passive) => attr.canApplyPreWeatherEffect(pokemon, passive, simulated, weather, cancelled, args), - args, - simulated, - ); -} - -export function applyPostTurnAbAttrs( - attrType: Constructor, - pokemon: Pokemon, - simulated = false, - ...args: any[] -): void { - applyAbAttrsInternal( - attrType, - pokemon, - (attr, passive) => attr.applyPostTurn(pokemon, passive, simulated, args), - (attr, passive) => attr.canApplyPostTurn(pokemon, passive, simulated, args), - args, - simulated, - ); -} - -export function applyPostWeatherChangeAbAttrs( - attrType: Constructor, - pokemon: Pokemon, - weather: WeatherType, - simulated = false, - ...args: any[] -): void { - applyAbAttrsInternal( - attrType, - pokemon, - (attr, passive) => attr.applyPostWeatherChange(pokemon, passive, simulated, weather, args), - (attr, passive) => attr.canApplyPostWeatherChange(pokemon, passive, simulated, weather, args), - args, - simulated, - ); -} - -export function applyPostWeatherLapseAbAttrs( - attrType: Constructor, - pokemon: Pokemon, - weather: Weather | null, - simulated = false, - ...args: any[] -): void { - applyAbAttrsInternal( - attrType, - pokemon, - (attr, passive) => attr.applyPostWeatherLapse(pokemon, passive, simulated, weather, args), - (attr, passive) => attr.canApplyPostWeatherLapse(pokemon, passive, simulated, weather, args), - args, - simulated, - ); -} - -export function applyPostTerrainChangeAbAttrs( - attrType: Constructor, - pokemon: Pokemon, - terrain: TerrainType, - simulated = false, - ...args: any[] -): void { - applyAbAttrsInternal( - attrType, - pokemon, - (attr, passive) => attr.applyPostTerrainChange(pokemon, passive, simulated, terrain, args), - (attr, passive) => attr.canApplyPostTerrainChange(pokemon, passive, simulated, terrain, args), - args, - simulated, - ); -} - -export function applyCheckTrappedAbAttrs( - attrType: Constructor, - pokemon: Pokemon, - trapped: BooleanHolder, - otherPokemon: Pokemon, - messages: string[], - simulated = false, - ...args: any[] -): void { - applyAbAttrsInternal( - attrType, - pokemon, - (attr, passive) => attr.applyCheckTrapped(pokemon, passive, simulated, trapped, otherPokemon, args), - (attr, passive) => attr.canApplyCheckTrapped(pokemon, passive, simulated, trapped, otherPokemon, args), - args, - simulated, - messages, - ); -} - -export function applyPostBattleAbAttrs( - attrType: Constructor, - pokemon: Pokemon, - simulated = false, - ...args: any[] -): void { - applyAbAttrsInternal( - attrType, - pokemon, - (attr, passive) => attr.applyPostBattle(pokemon, passive, simulated, args), - (attr, passive) => attr.canApplyPostBattle(pokemon, passive, simulated, args), - args, - simulated, - ); -} - -export function applyPostFaintAbAttrs( - attrType: Constructor, - pokemon: Pokemon, - attacker?: Pokemon, - move?: Move, - hitResult?: HitResult, - simulated = false, - ...args: any[] -): void { - applyAbAttrsInternal( - attrType, - pokemon, - (attr, passive) => attr.applyPostFaint(pokemon, passive, simulated, attacker, move, hitResult, args), - (attr, passive) => attr.canApplyPostFaint(pokemon, passive, simulated, attacker, move, hitResult, args), - args, - simulated, - ); -} - -export function applyPostItemLostAbAttrs( - attrType: Constructor, - pokemon: Pokemon, - simulated = false, - ...args: any[] -): void { - applyAbAttrsInternal( - attrType, - pokemon, - (attr, _passive) => attr.applyPostItemLost(pokemon, simulated, args), - (attr, _passive) => attr.canApplyPostItemLost(pokemon, simulated, args), - args, - ); -} - -/** - * Applies abilities when they become active mid-turn (ability switch) - * - * Ignores passives as they don't change and shouldn't be reapplied when main abilities change - */ -export function applyOnGainAbAttrs(pokemon: Pokemon, passive = false, simulated = false, ...args: any[]): void { - applySingleAbAttrs( - pokemon, - passive, - PostSummonAbAttr, - (attr, passive) => attr.applyPostSummon(pokemon, passive, simulated, args), - (attr, passive) => attr.canApplyPostSummon(pokemon, passive, simulated, args), - args, - true, - simulated, - ); -} - -/** - * Applies ability attributes which activate when the ability is lost or suppressed (i.e. primal weather) - */ -export function applyOnLoseAbAttrs(pokemon: Pokemon, passive = false, simulated = false, ...args: any[]): void { - applySingleAbAttrs( - pokemon, - passive, - PreLeaveFieldAbAttr, - (attr, passive) => attr.applyPreLeaveField(pokemon, passive, simulated, [...args, true]), - (attr, passive) => attr.canApplyPreLeaveField(pokemon, passive, simulated, [...args, true]), - args, - true, - simulated, - ); - - applySingleAbAttrs( - pokemon, - passive, - IllusionBreakAbAttr, - (attr, passive) => attr.apply(pokemon, passive, simulated, null, args), - (attr, passive) => attr.canApply(pokemon, passive, simulated, args), - args, - true, - simulated, - ); -} +export type AbAttrConstructorMap = typeof AbilityAttrs; /** * Sets the ability of a Pokémon as revealed. @@ -8369,7 +8120,7 @@ export function initAbilities() { .conditionalAttr(p => globalScene.currentBattle.double && [ AbilityId.PLUS, AbilityId.MINUS ].some(a => (p.getAlly()?.hasAbility(a) ?? false)), StatMultiplierAbAttr, Stat.SPATK, 1.5), new Ability(AbilityId.MINUS, 3) .conditionalAttr(p => globalScene.currentBattle.double && [ AbilityId.PLUS, AbilityId.MINUS ].some(a => (p.getAlly()?.hasAbility(a) ?? false)), StatMultiplierAbAttr, Stat.SPATK, 1.5), - new Ability(AbilityId.FORECAST, 3) + new Ability(AbilityId.FORECAST, 3, -2) .uncopiable() .unreplaceable() .attr(NoFusionAbilityAbAttr) @@ -8503,7 +8254,7 @@ export function initAbilities() { .attr(StatusEffectImmunityAbAttr) .condition(getWeatherCondition(WeatherType.SUNNY, WeatherType.HARSH_SUN)) .ignorable(), - new Ability(AbilityId.KLUTZ, 4) + new Ability(AbilityId.KLUTZ, 4, 1) .unimplemented(), new Ability(AbilityId.MOLD_BREAKER, 4) .attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonMoldBreaker", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })) @@ -8555,7 +8306,7 @@ export function initAbilities() { .uncopiable() .unsuppressable() .unreplaceable(), - new Ability(AbilityId.FLOWER_GIFT, 4) + new Ability(AbilityId.FLOWER_GIFT, 4, -2) .conditionalAttr(getWeatherCondition(WeatherType.SUNNY || WeatherType.HARSH_SUN), StatMultiplierAbAttr, Stat.ATK, 1.5) .conditionalAttr(getWeatherCondition(WeatherType.SUNNY || WeatherType.HARSH_SUN), StatMultiplierAbAttr, Stat.SPDEF, 1.5) .conditionalAttr(getWeatherCondition(WeatherType.SUNNY || WeatherType.HARSH_SUN), AllyStatMultiplierAbAttr, Stat.ATK, 1.5) @@ -8577,7 +8328,7 @@ export function initAbilities() { new Ability(AbilityId.CONTRARY, 5) .attr(StatStageChangeMultiplierAbAttr, -1) .ignorable(), - new Ability(AbilityId.UNNERVE, 5) + new Ability(AbilityId.UNNERVE, 5, 1) .attr(PreventBerryUseAbAttr), new Ability(AbilityId.DEFIANT, 5) .attr(PostStatStageChangeStatStageChangeAbAttr, (_target, _statsChanged, stages) => stages < 0, [ Stat.ATK ], 2), @@ -8640,10 +8391,10 @@ export function initAbilities() { .attr(WonderSkinAbAttr) .ignorable(), new Ability(AbilityId.ANALYTIC, 5) - .attr(MovePowerBoostAbAttr, (user, _target, _move) => { - const movePhase = globalScene.phaseManager.findPhase((phase) => phase.is("MovePhase") && phase.pokemon.id !== user?.id); - return isNullOrUndefined(movePhase); - }, 1.3), + .attr(MovePowerBoostAbAttr, (user) => + // Boost power if all other Pokemon have already moved (no other moves are slated to execute) + !globalScene.phaseManager.findPhase((phase) => phase.is("MovePhase") && phase.pokemon.id !== user?.id), + 1.3), new Ability(AbilityId.ILLUSION, 5) // The Pokemon generate an illusion if it's available .attr(IllusionPreSummonAbAttr, false) @@ -8819,7 +8570,7 @@ export function initAbilities() { .attr(PostDefendStatStageChangeAbAttr, (_target, user, move) => user.getMoveType(move) === PokemonType.WATER && move.category !== MoveCategory.STATUS, Stat.DEF, 2), new Ability(AbilityId.MERCILESS, 7) .attr(ConditionalCritAbAttr, (_user, target, _move) => target?.status?.effect === StatusEffect.TOXIC || target?.status?.effect === StatusEffect.POISON), - new Ability(AbilityId.SHIELDS_DOWN, 7) + new Ability(AbilityId.SHIELDS_DOWN, 7, -1) .attr(PostBattleInitFormChangeAbAttr, () => 0) .attr(PostSummonFormChangeAbAttr, p => p.formIndex % 7 + (p.getHpRatio() <= 0.5 ? 7 : 0)) .attr(PostTurnFormChangeAbAttr, p => p.formIndex % 7 + (p.getHpRatio() <= 0.5 ? 7 : 0)) @@ -8857,7 +8608,7 @@ export function initAbilities() { .attr(MoveTypeChangeAbAttr, PokemonType.ELECTRIC, 1.2, (_user, _target, move) => move.type === PokemonType.NORMAL), new Ability(AbilityId.SURGE_SURFER, 7) .conditionalAttr(getTerrainCondition(TerrainType.ELECTRIC), StatMultiplierAbAttr, Stat.SPD, 2), - new Ability(AbilityId.SCHOOLING, 7) + new Ability(AbilityId.SCHOOLING, 7, -1) .attr(PostBattleInitFormChangeAbAttr, () => 0) .attr(PostSummonFormChangeAbAttr, p => p.level < 20 || p.getHpRatio() <= 0.25 ? 0 : 1) .attr(PostTurnFormChangeAbAttr, p => p.level < 20 || p.getHpRatio() <= 0.25 ? 0 : 1) @@ -8917,7 +8668,13 @@ export function initAbilities() { .attr(PostFaintHPDamageAbAttr) .bypassFaint(), 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) .attr(AllyMoveCategoryPowerBoostAbAttr, [ MoveCategory.SPECIAL ], 1.3), new Ability(AbilityId.FLUFFY, 7) @@ -9026,7 +8783,7 @@ export function initAbilities() { .ignorable(), new Ability(AbilityId.RIPEN, 8) .attr(DoubleBerryEffectAbAttr), - new Ability(AbilityId.ICE_FACE, 8) + new Ability(AbilityId.ICE_FACE, 8, -2) .attr(NoTransformAbilityAbAttr) .attr(NoFusionAbilityAbAttr) // Add BattlerTagType.ICE_FACE if the pokemon is in ice face form @@ -9046,7 +8803,7 @@ export function initAbilities() { .ignorable(), new Ability(AbilityId.POWER_SPOT, 8) .attr(AllyMoveCategoryPowerBoostAbAttr, [ MoveCategory.SPECIAL, MoveCategory.PHYSICAL ], 1.3), - new Ability(AbilityId.MIMICRY, 8) + new Ability(AbilityId.MIMICRY, 8, -1) .attr(TerrainEventTypeChangeAbAttr), new Ability(AbilityId.SCREEN_CLEANER, 8) .attr(PostSummonRemoveArenaTagAbAttr, [ ArenaTagType.AURORA_VEIL, ArenaTagType.LIGHT_SCREEN, ArenaTagType.REFLECT ]), @@ -9058,10 +8815,12 @@ export function initAbilities() { new Ability(AbilityId.WANDERING_SPIRIT, 8) .attr(PostDefendAbilitySwapAbAttr) .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) - .attr(GorillaTacticsAbAttr), - new Ability(AbilityId.NEUTRALIZING_GAS, 8) + .attr(GorillaTacticsAbAttr) + // TODO: Verify whether Gorilla Tactics increases struggle's power or not + .edgeCase(), + new Ability(AbilityId.NEUTRALIZING_GAS, 8, 2) .attr(PostSummonAddArenaTagAbAttr, true, ArenaTagType.NEUTRALIZING_GAS, 0) .attr(PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr) .uncopiable() @@ -9093,14 +8852,14 @@ export function initAbilities() { .attr(PostVictoryStatStageChangeAbAttr, Stat.ATK, 1), new Ability(AbilityId.GRIM_NEIGH, 8) .attr(PostVictoryStatStageChangeAbAttr, Stat.SPATK, 1), - new Ability(AbilityId.AS_ONE_GLASTRIER, 8) + new Ability(AbilityId.AS_ONE_GLASTRIER, 8, 1) .attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonAsOneGlastrier", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })) .attr(PreventBerryUseAbAttr) .attr(PostVictoryStatStageChangeAbAttr, Stat.ATK, 1) .uncopiable() .unreplaceable() .unsuppressable(), - new Ability(AbilityId.AS_ONE_SPECTRIER, 8) + new Ability(AbilityId.AS_ONE_SPECTRIER, 8, 1) .attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonAsOneSpectrier", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })) .attr(PreventBerryUseAbAttr) .attr(PostVictoryStatStageChangeAbAttr, Stat.SPATK, 1) @@ -9158,12 +8917,12 @@ export function initAbilities() { .edgeCase(), // Encore, Frenzy, and other non-`TURN_END` tags don't lapse correctly on the commanding Pokemon. new Ability(AbilityId.ELECTROMORPHOSIS, 9) .attr(PostDefendApplyBattlerTagAbAttr, (_target, _user, move) => move.category !== MoveCategory.STATUS, BattlerTagType.CHARGED), - new Ability(AbilityId.PROTOSYNTHESIS, 9) + new Ability(AbilityId.PROTOSYNTHESIS, 9, -2) .conditionalAttr(getWeatherCondition(WeatherType.SUNNY, WeatherType.HARSH_SUN), PostSummonAddBattlerTagAbAttr, BattlerTagType.PROTOSYNTHESIS, 0, true) .attr(PostWeatherChangeAddBattlerTagAttr, BattlerTagType.PROTOSYNTHESIS, 0, WeatherType.SUNNY, WeatherType.HARSH_SUN) .uncopiable() .attr(NoTransformAbilityAbAttr), - new Ability(AbilityId.QUARK_DRIVE, 9) + new Ability(AbilityId.QUARK_DRIVE, 9, -2) .conditionalAttr(getTerrainCondition(TerrainType.ELECTRIC), PostSummonAddBattlerTagAbAttr, BattlerTagType.QUARK_DRIVE, 0, true) .attr(PostTerrainChangeAddBattlerTagAttr, BattlerTagType.QUARK_DRIVE, 0, TerrainType.ELECTRIC) .uncopiable() @@ -9207,7 +8966,7 @@ export function initAbilities() { new Ability(AbilityId.SUPREME_OVERLORD, 9) .attr(VariableMovePowerBoostAbAttr, (user, _target, _move) => 1 + 0.1 * Math.min(user.isPlayer() ? globalScene.arena.playerFaints : globalScene.currentBattle.enemyFaints, 5)) .partial(), // Should only boost once, on summon - new Ability(AbilityId.COSTAR, 9) + new Ability(AbilityId.COSTAR, 9, -2) .attr(PostSummonCopyAllyStatsAbAttr), new Ability(AbilityId.TOXIC_DEBRIS, 9) .attr(PostDefendApplyArenaTrapTagAbAttr, (_target, _user, move) => move.category === MoveCategory.PHYSICAL, ArenaTagType.TOXIC_SPIKES) @@ -9229,7 +8988,7 @@ export function initAbilities() { .ignorable(), new Ability(AbilityId.SUPERSWEET_SYRUP, 9) .attr(PostSummonStatStageChangeAbAttr, [ Stat.EVA ], -1), - new Ability(AbilityId.HOSPITALITY, 9) + new Ability(AbilityId.HOSPITALITY, 9, -2) .attr(PostSummonAllyHealAbAttr, 4, true), new Ability(AbilityId.TOXIC_CHAIN, 9) .attr(PostAttackApplyStatusEffectAbAttr, false, 30, StatusEffect.TOXIC), @@ -9253,7 +9012,7 @@ export function initAbilities() { .uncopiable() .unreplaceable() .attr(NoTransformAbilityAbAttr), - new Ability(AbilityId.TERA_SHIFT, 9) + new Ability(AbilityId.TERA_SHIFT, 9, 2) .attr(PostSummonFormChangeAbAttr, p => p.getFormKey() ? 0 : 1) .uncopiable() .unreplaceable() diff --git a/src/data/abilities/apply-ab-attrs.ts b/src/data/abilities/apply-ab-attrs.ts new file mode 100644 index 00000000000..fdbd2652698 --- /dev/null +++ b/src/data/abilities/apply-ab-attrs.ts @@ -0,0 +1,832 @@ +import type { AbAttrApplyFunc, AbAttrMap, AbAttrString, AbAttrSuccessFunc } from "#app/@types/ability-types"; +import type Pokemon from "#app/field/pokemon"; +import { globalScene } from "#app/global-scene"; +import type { BooleanHolder, NumberHolder } from "#app/utils/common"; +import type { BattlerIndex } from "#enums/battler-index"; +import type { HitResult } from "#enums/hit-result"; +import type { BattleStat, Stat } from "#enums/stat"; +import type { StatusEffect } from "#enums/status-effect"; +import type { WeatherType } from "#enums/weather-type"; +import type { BattlerTag } from "../battler-tags"; +import type Move from "../moves/move"; +import type { PokemonMove } from "../moves/pokemon-move"; +import type { TerrainType } from "../terrain"; +import type { Weather } from "../weather"; +import type { + PostBattleInitAbAttr, + PreDefendAbAttr, + PostDefendAbAttr, + PostMoveUsedAbAttr, + StatMultiplierAbAttr, + AllyStatMultiplierAbAttr, + PostSetStatusAbAttr, + PostDamageAbAttr, + FieldMultiplyStatAbAttr, + PreAttackAbAttr, + ExecutedMoveAbAttr, + PostAttackAbAttr, + PostKnockOutAbAttr, + PostVictoryAbAttr, + PostSummonAbAttr, + PreSummonAbAttr, + PreSwitchOutAbAttr, + PreLeaveFieldAbAttr, + PreStatStageChangeAbAttr, + PostStatStageChangeAbAttr, + PreSetStatusAbAttr, + PreApplyBattlerTagAbAttr, + PreWeatherEffectAbAttr, + PreWeatherDamageAbAttr, + PostTurnAbAttr, + PostWeatherChangeAbAttr, + PostWeatherLapseAbAttr, + PostTerrainChangeAbAttr, + CheckTrappedAbAttr, + PostBattleAbAttr, + PostFaintAbAttr, + PostItemLostAbAttr, +} from "./ability"; + +function applySingleAbAttrs( + pokemon: Pokemon, + passive: boolean, + attrType: T, + applyFunc: AbAttrApplyFunc, + successFunc: AbAttrSuccessFunc, + args: any[], + gainedMidTurn = false, + simulated = false, + messages: string[] = [], +) { + if (!pokemon?.canApplyAbility(passive) || (passive && pokemon.getPassiveAbility().id === pokemon.getAbility().id)) { + return; + } + + const ability = passive ? pokemon.getPassiveAbility() : pokemon.getAbility(); + if ( + gainedMidTurn && + ability.getAttrs(attrType).some(attr => { + attr.is("PostSummonAbAttr") && !attr.shouldActivateOnGain(); + }) + ) { + return; + } + + for (const attr of ability.getAttrs(attrType)) { + const condition = attr.getCondition(); + let abShown = false; + if ((condition && !condition(pokemon)) || !successFunc(attr, passive)) { + continue; + } + + globalScene.phaseManager.setPhaseQueueSplice(); + + if (attr.showAbility && !simulated) { + globalScene.phaseManager.queueAbilityDisplay(pokemon, passive, true); + abShown = true; + } + const message = attr.getTriggerMessage(pokemon, ability.name, args); + if (message) { + if (!simulated) { + globalScene.phaseManager.queueMessage(message); + } + messages.push(message); + } + + applyFunc(attr, passive); + + if (abShown) { + globalScene.phaseManager.queueAbilityDisplay(pokemon, passive, false); + } + + if (!simulated) { + pokemon.waveData.abilitiesApplied.add(ability.id); + } + + globalScene.phaseManager.clearPhaseQueueSplice(); + } +} + +function applyAbAttrsInternal( + attrType: T, + pokemon: Pokemon | null, + applyFunc: AbAttrApplyFunc, + successFunc: AbAttrSuccessFunc, + args: any[], + simulated = false, + messages: string[] = [], + gainedMidTurn = false, +) { + for (const passive of [false, true]) { + if (pokemon) { + applySingleAbAttrs(pokemon, passive, attrType, applyFunc, successFunc, args, gainedMidTurn, simulated, messages); + globalScene.phaseManager.clearPhaseQueueSplice(); + } + } +} + +export function applyAbAttrs( + attrType: T, + pokemon: Pokemon, + cancelled: BooleanHolder | null, + simulated = false, + ...args: any[] +): void { + applyAbAttrsInternal( + attrType, + pokemon, + // @ts-expect-error: TODO: fix the error on `cancelled` + (attr, passive) => attr.apply(pokemon, passive, simulated, cancelled, args), + (attr, passive) => attr.canApply(pokemon, passive, simulated, args), + args, + simulated, + ); +} + +// TODO: Improve the type signatures of the following methods / refactor the apply methods + +export function applyPostBattleInitAbAttrs( + attrType: AbAttrMap[K] extends PostBattleInitAbAttr ? K : never, + pokemon: Pokemon, + simulated = false, + ...args: any[] +): void { + applyAbAttrsInternal( + attrType, + pokemon, + (attr, passive) => (attr as PostBattleInitAbAttr).applyPostBattleInit(pokemon, passive, simulated, args), + (attr, passive) => (attr as PostBattleInitAbAttr).canApplyPostBattleInit(pokemon, passive, simulated, args), + args, + simulated, + ); +} + +export function applyPreDefendAbAttrs( + attrType: AbAttrMap[K] extends PreDefendAbAttr ? K : never, + pokemon: Pokemon, + attacker: Pokemon, + move: Move | null, + cancelled: BooleanHolder | null, + simulated = false, + ...args: any[] +): void { + applyAbAttrsInternal( + attrType, + pokemon, + (attr, passive) => + (attr as PreDefendAbAttr).applyPreDefend(pokemon, passive, simulated, attacker, move, cancelled, args), + (attr, passive) => + (attr as PreDefendAbAttr).canApplyPreDefend(pokemon, passive, simulated, attacker, move, cancelled, args), + args, + simulated, + ); +} + +export function applyPostDefendAbAttrs( + attrType: AbAttrMap[K] extends PostDefendAbAttr ? K : never, + pokemon: Pokemon, + attacker: Pokemon, + move: Move, + hitResult: HitResult | null, + simulated = false, + ...args: any[] +): void { + applyAbAttrsInternal( + attrType, + pokemon, + (attr, passive) => + (attr as PostDefendAbAttr).applyPostDefend(pokemon, passive, simulated, attacker, move, hitResult, args), + (attr, passive) => + (attr as PostDefendAbAttr).canApplyPostDefend(pokemon, passive, simulated, attacker, move, hitResult, args), + args, + simulated, + ); +} + +export function applyPostMoveUsedAbAttrs( + attrType: AbAttrMap[K] extends PostMoveUsedAbAttr ? K : never, + pokemon: Pokemon, + move: PokemonMove, + source: Pokemon, + targets: BattlerIndex[], + simulated = false, + ...args: any[] +): void { + applyAbAttrsInternal( + attrType, + pokemon, + (attr, _passive) => (attr as PostMoveUsedAbAttr).applyPostMoveUsed(pokemon, move, source, targets, simulated, args), + (attr, _passive) => + (attr as PostMoveUsedAbAttr).canApplyPostMoveUsed(pokemon, move, source, targets, simulated, args), + args, + simulated, + ); +} + +export function applyStatMultiplierAbAttrs( + attrType: AbAttrMap[K] extends StatMultiplierAbAttr ? K : never, + pokemon: Pokemon, + stat: BattleStat, + statValue: NumberHolder, + simulated = false, + ...args: any[] +): void { + applyAbAttrsInternal( + attrType, + pokemon, + (attr, passive) => + (attr as StatMultiplierAbAttr).applyStatStage(pokemon, passive, simulated, stat, statValue, args), + (attr, passive) => + (attr as StatMultiplierAbAttr).canApplyStatStage(pokemon, passive, simulated, stat, statValue, args), + args, + ); +} + +/** + * Applies an ally's Stat multiplier attribute + * @param attrType - {@linkcode AllyStatMultiplierAbAttr} should always be AllyStatMultiplierAbAttr for the time being + * @param pokemon - The {@linkcode Pokemon} with the ability + * @param stat - The type of the checked {@linkcode Stat} + * @param statValue - {@linkcode NumberHolder} containing the value of the checked stat + * @param checkedPokemon - The {@linkcode Pokemon} with the checked stat + * @param ignoreAbility - Whether or not the ability should be ignored by the pokemon or its move. + * @param args - unused + */ +export function applyAllyStatMultiplierAbAttrs( + attrType: AbAttrMap[K] extends AllyStatMultiplierAbAttr ? K : never, + pokemon: Pokemon, + stat: BattleStat, + statValue: NumberHolder, + simulated = false, + checkedPokemon: Pokemon, + ignoreAbility: boolean, + ...args: any[] +): void { + applyAbAttrsInternal( + attrType, + pokemon, + (attr, passive) => + (attr as AllyStatMultiplierAbAttr).applyAllyStat( + pokemon, + passive, + simulated, + stat, + statValue, + checkedPokemon, + ignoreAbility, + args, + ), + (attr, passive) => + (attr as AllyStatMultiplierAbAttr).canApplyAllyStat( + pokemon, + passive, + simulated, + stat, + statValue, + checkedPokemon, + ignoreAbility, + args, + ), + args, + simulated, + ); +} + +export function applyPostSetStatusAbAttrs( + attrType: AbAttrMap[K] extends PostSetStatusAbAttr ? K : never, + pokemon: Pokemon, + effect: StatusEffect, + sourcePokemon?: Pokemon | null, + simulated = false, + ...args: any[] +): void { + applyAbAttrsInternal( + attrType, + pokemon, + (attr, passive) => + (attr as PostSetStatusAbAttr).applyPostSetStatus(pokemon, sourcePokemon, passive, effect, simulated, args), + (attr, passive) => + (attr as PostSetStatusAbAttr).canApplyPostSetStatus(pokemon, sourcePokemon, passive, effect, simulated, args), + args, + simulated, + ); +} + +export function applyPostDamageAbAttrs( + attrType: AbAttrMap[K] extends PostDamageAbAttr ? K : never, + pokemon: Pokemon, + damage: number, + _passive: boolean, + simulated = false, + args: any[], + source?: Pokemon, +): void { + applyAbAttrsInternal( + attrType, + pokemon, + (attr, passive) => (attr as PostDamageAbAttr).applyPostDamage(pokemon, damage, passive, simulated, args, source), + (attr, passive) => (attr as PostDamageAbAttr).canApplyPostDamage(pokemon, damage, passive, simulated, args, source), + args, + ); +} +/** + * Applies a field Stat multiplier attribute + * @param attrType {@linkcode FieldMultiplyStatAbAttr} should always be FieldMultiplyBattleStatAbAttr for the time being + * @param pokemon {@linkcode Pokemon} the Pokemon applying this ability + * @param stat {@linkcode Stat} the type of the checked stat + * @param statValue {@linkcode NumberHolder} the value of the checked stat + * @param checkedPokemon {@linkcode Pokemon} the Pokemon with the checked stat + * @param hasApplied {@linkcode BooleanHolder} whether or not a FieldMultiplyBattleStatAbAttr has already affected this stat + * @param args unused + */ + +export function applyFieldStatMultiplierAbAttrs( + attrType: AbAttrMap[K] extends FieldMultiplyStatAbAttr ? K : never, + pokemon: Pokemon, + stat: Stat, + statValue: NumberHolder, + checkedPokemon: Pokemon, + hasApplied: BooleanHolder, + simulated = false, + ...args: any[] +): void { + applyAbAttrsInternal( + attrType, + pokemon, + (attr, passive) => + (attr as FieldMultiplyStatAbAttr).applyFieldStat( + pokemon, + passive, + simulated, + stat, + statValue, + checkedPokemon, + hasApplied, + args, + ), + (attr, passive) => + (attr as FieldMultiplyStatAbAttr).canApplyFieldStat( + pokemon, + passive, + simulated, + stat, + statValue, + checkedPokemon, + hasApplied, + args, + ), + args, + ); +} + +export function applyPreAttackAbAttrs( + attrType: AbAttrMap[K] extends PreAttackAbAttr ? K : never, + pokemon: Pokemon, + defender: Pokemon | null, + move: Move, + simulated = false, + ...args: any[] +): void { + applyAbAttrsInternal( + attrType, + pokemon, + (attr, passive) => (attr as PreAttackAbAttr).applyPreAttack(pokemon, passive, simulated, defender, move, args), + (attr, passive) => (attr as PreAttackAbAttr).canApplyPreAttack(pokemon, passive, simulated, defender, move, args), + args, + simulated, + ); +} + +export function applyExecutedMoveAbAttrs( + attrType: AbAttrMap[K] extends ExecutedMoveAbAttr ? K : never, + pokemon: Pokemon, + simulated = false, + ...args: any[] +): void { + applyAbAttrsInternal( + attrType, + pokemon, + attr => (attr as ExecutedMoveAbAttr).applyExecutedMove(pokemon, simulated), + attr => (attr as ExecutedMoveAbAttr).canApplyExecutedMove(pokemon, simulated), + args, + simulated, + ); +} + +export function applyPostAttackAbAttrs( + attrType: AbAttrMap[K] extends PostAttackAbAttr ? K : never, + pokemon: Pokemon, + defender: Pokemon, + move: Move, + hitResult: HitResult | null, + simulated = false, + ...args: any[] +): void { + applyAbAttrsInternal( + attrType, + pokemon, + (attr, passive) => + (attr as PostAttackAbAttr).applyPostAttack(pokemon, passive, simulated, defender, move, hitResult, args), + (attr, passive) => + (attr as PostAttackAbAttr).canApplyPostAttack(pokemon, passive, simulated, defender, move, hitResult, args), + args, + simulated, + ); +} + +export function applyPostKnockOutAbAttrs( + attrType: AbAttrMap[K] extends PostKnockOutAbAttr ? K : never, + pokemon: Pokemon, + knockedOut: Pokemon, + simulated = false, + ...args: any[] +): void { + applyAbAttrsInternal( + attrType, + pokemon, + (attr, passive) => (attr as PostKnockOutAbAttr).applyPostKnockOut(pokemon, passive, simulated, knockedOut, args), + (attr, passive) => (attr as PostKnockOutAbAttr).canApplyPostKnockOut(pokemon, passive, simulated, knockedOut, args), + args, + simulated, + ); +} + +export function applyPostVictoryAbAttrs( + attrType: AbAttrMap[K] extends PostVictoryAbAttr ? K : never, + pokemon: Pokemon, + simulated = false, + ...args: any[] +): void { + applyAbAttrsInternal( + attrType, + pokemon, + (attr, passive) => (attr as PostVictoryAbAttr).applyPostVictory(pokemon, passive, simulated, args), + (attr, passive) => (attr as PostVictoryAbAttr).canApplyPostVictory(pokemon, passive, simulated, args), + args, + simulated, + ); +} + +export function applyPostSummonAbAttrs( + attrType: AbAttrMap[K] extends PostSummonAbAttr ? K : never, + pokemon: Pokemon, + passive = false, + simulated = false, + ...args: any[] +): void { + applySingleAbAttrs( + pokemon, + passive, + attrType, + (attr, passive) => (attr as PostSummonAbAttr).applyPostSummon(pokemon, passive, simulated, args), + (attr, passive) => (attr as PostSummonAbAttr).canApplyPostSummon(pokemon, passive, simulated, args), + args, + false, + simulated, + ); +} + +export function applyPreSummonAbAttrs( + attrType: AbAttrMap[K] extends PreSummonAbAttr ? K : never, + pokemon: Pokemon, + ...args: any[] +): void { + applyAbAttrsInternal( + attrType, + pokemon, + (attr, passive) => (attr as PreSummonAbAttr).applyPreSummon(pokemon, passive, args), + (attr, passive) => (attr as PreSummonAbAttr).canApplyPreSummon(pokemon, passive, args), + args, + ); +} + +export function applyPreSwitchOutAbAttrs( + attrType: AbAttrMap[K] extends PreSwitchOutAbAttr ? K : never, + pokemon: Pokemon, + simulated = false, + ...args: any[] +): void { + applyAbAttrsInternal( + attrType, + pokemon, + (attr, passive) => (attr as PreSwitchOutAbAttr).applyPreSwitchOut(pokemon, passive, simulated, args), + (attr, passive) => (attr as PreSwitchOutAbAttr).canApplyPreSwitchOut(pokemon, passive, simulated, args), + args, + simulated, + ); +} + +export function applyPreLeaveFieldAbAttrs( + attrType: AbAttrMap[K] extends PreLeaveFieldAbAttr ? K : never, + pokemon: Pokemon, + simulated = false, + ...args: any[] +): void { + applyAbAttrsInternal( + attrType, + pokemon, + (attr, passive) => (attr as PreLeaveFieldAbAttr).applyPreLeaveField(pokemon, passive, simulated, args), + (attr, passive) => (attr as PreLeaveFieldAbAttr).canApplyPreLeaveField(pokemon, passive, simulated, args), + args, + simulated, + ); +} + +export function applyPreStatStageChangeAbAttrs( + attrType: AbAttrMap[K] extends PreStatStageChangeAbAttr ? K : never, + pokemon: Pokemon | null, + stat: BattleStat, + cancelled: BooleanHolder, + simulated = false, + ...args: any[] +): void { + applyAbAttrsInternal( + attrType, + pokemon, + (attr, passive) => + (attr as PreStatStageChangeAbAttr).applyPreStatStageChange(pokemon, passive, simulated, stat, cancelled, args), + (attr, passive) => + (attr as PreStatStageChangeAbAttr).canApplyPreStatStageChange(pokemon, passive, simulated, stat, cancelled, args), + args, + simulated, + ); +} + +export function applyPostStatStageChangeAbAttrs( + attrType: AbAttrMap[K] extends PostStatStageChangeAbAttr ? K : never, + pokemon: Pokemon, + stats: BattleStat[], + stages: number, + selfTarget: boolean, + simulated = false, + ...args: any[] +): void { + applyAbAttrsInternal( + attrType, + pokemon, + (attr, _passive) => + (attr as PostStatStageChangeAbAttr).applyPostStatStageChange(pokemon, simulated, stats, stages, selfTarget, args), + (attr, _passive) => + (attr as PostStatStageChangeAbAttr).canApplyPostStatStageChange( + pokemon, + simulated, + stats, + stages, + selfTarget, + args, + ), + args, + simulated, + ); +} + +export function applyPreSetStatusAbAttrs( + attrType: AbAttrMap[K] extends PreSetStatusAbAttr ? K : never, + pokemon: Pokemon, + effect: StatusEffect | undefined, + cancelled: BooleanHolder, + simulated = false, + ...args: any[] +): void { + applyAbAttrsInternal( + attrType, + pokemon, + (attr, passive) => + (attr as PreSetStatusAbAttr).applyPreSetStatus(pokemon, passive, simulated, effect, cancelled, args), + (attr, passive) => + (attr as PreSetStatusAbAttr).canApplyPreSetStatus(pokemon, passive, simulated, effect, cancelled, args), + args, + simulated, + ); +} + +export function applyPreApplyBattlerTagAbAttrs( + attrType: AbAttrMap[K] extends PreApplyBattlerTagAbAttr ? K : never, + pokemon: Pokemon, + tag: BattlerTag, + cancelled: BooleanHolder, + simulated = false, + ...args: any[] +): void { + applyAbAttrsInternal( + attrType, + pokemon, + (attr, passive) => + (attr as PreApplyBattlerTagAbAttr).applyPreApplyBattlerTag(pokemon, passive, simulated, tag, cancelled, args), + (attr, passive) => + (attr as PreApplyBattlerTagAbAttr).canApplyPreApplyBattlerTag(pokemon, passive, simulated, tag, cancelled, args), + args, + simulated, + ); +} + +export function applyPreWeatherEffectAbAttrs( + attrType: AbAttrMap[K] extends PreWeatherEffectAbAttr ? K : never, + pokemon: Pokemon, + weather: Weather | null, + cancelled: BooleanHolder, + simulated = false, + ...args: any[] +): void { + applyAbAttrsInternal( + attrType, + pokemon, + (attr, passive) => + (attr as PreWeatherDamageAbAttr).applyPreWeatherEffect(pokemon, passive, simulated, weather, cancelled, args), + (attr, passive) => + (attr as PreWeatherDamageAbAttr).canApplyPreWeatherEffect(pokemon, passive, simulated, weather, cancelled, args), + args, + simulated, + ); +} + +export function applyPostTurnAbAttrs( + attrType: AbAttrMap[K] extends PostTurnAbAttr ? K : never, + pokemon: Pokemon, + simulated = false, + ...args: any[] +): void { + applyAbAttrsInternal( + attrType, + pokemon, + (attr, passive) => (attr as PostTurnAbAttr).applyPostTurn(pokemon, passive, simulated, args), + (attr, passive) => (attr as PostTurnAbAttr).canApplyPostTurn(pokemon, passive, simulated, args), + args, + simulated, + ); +} + +export function applyPostWeatherChangeAbAttrs( + attrType: AbAttrMap[K] extends PostWeatherChangeAbAttr ? K : never, + pokemon: Pokemon, + weather: WeatherType, + simulated = false, + ...args: any[] +): void { + applyAbAttrsInternal( + attrType, + pokemon, + (attr, passive) => + (attr as PostWeatherChangeAbAttr).applyPostWeatherChange(pokemon, passive, simulated, weather, args), + (attr, passive) => + (attr as PostWeatherChangeAbAttr).canApplyPostWeatherChange(pokemon, passive, simulated, weather, args), + args, + simulated, + ); +} + +export function applyPostWeatherLapseAbAttrs( + attrType: AbAttrMap[K] extends PostWeatherLapseAbAttr ? K : never, + pokemon: Pokemon, + weather: Weather | null, + simulated = false, + ...args: any[] +): void { + applyAbAttrsInternal( + attrType, + pokemon, + (attr, passive) => + (attr as PostWeatherLapseAbAttr).applyPostWeatherLapse(pokemon, passive, simulated, weather, args), + (attr, passive) => + (attr as PostWeatherLapseAbAttr).canApplyPostWeatherLapse(pokemon, passive, simulated, weather, args), + args, + simulated, + ); +} + +export function applyPostTerrainChangeAbAttrs( + attrType: AbAttrMap[K] extends PostTerrainChangeAbAttr ? K : never, + pokemon: Pokemon, + terrain: TerrainType, + simulated = false, + ...args: any[] +): void { + applyAbAttrsInternal( + attrType, + pokemon, + (attr, passive) => + (attr as PostTerrainChangeAbAttr).applyPostTerrainChange(pokemon, passive, simulated, terrain, args), + (attr, passive) => + (attr as PostTerrainChangeAbAttr).canApplyPostTerrainChange(pokemon, passive, simulated, terrain, args), + args, + simulated, + ); +} + +export function applyCheckTrappedAbAttrs( + attrType: AbAttrMap[K] extends CheckTrappedAbAttr ? K : never, + pokemon: Pokemon, + trapped: BooleanHolder, + otherPokemon: Pokemon, + messages: string[], + simulated = false, + ...args: any[] +): void { + applyAbAttrsInternal( + attrType, + pokemon, + (attr, passive) => + (attr as CheckTrappedAbAttr).applyCheckTrapped(pokemon, passive, simulated, trapped, otherPokemon, args), + (attr, passive) => + (attr as CheckTrappedAbAttr).canApplyCheckTrapped(pokemon, passive, simulated, trapped, otherPokemon, args), + args, + simulated, + messages, + ); +} + +export function applyPostBattleAbAttrs( + attrType: AbAttrMap[K] extends PostBattleAbAttr ? K : never, + pokemon: Pokemon, + simulated = false, + ...args: any[] +): void { + applyAbAttrsInternal( + attrType, + pokemon, + (attr, passive) => (attr as PostBattleAbAttr).applyPostBattle(pokemon, passive, simulated, args), + (attr, passive) => (attr as PostBattleAbAttr).canApplyPostBattle(pokemon, passive, simulated, args), + args, + simulated, + ); +} + +export function applyPostFaintAbAttrs( + attrType: AbAttrMap[K] extends PostFaintAbAttr ? K : never, + pokemon: Pokemon, + attacker?: Pokemon, + move?: Move, + hitResult?: HitResult, + simulated = false, + ...args: any[] +): void { + applyAbAttrsInternal( + attrType, + pokemon, + (attr, passive) => + (attr as PostFaintAbAttr).applyPostFaint(pokemon, passive, simulated, attacker, move, hitResult, args), + (attr, passive) => + (attr as PostFaintAbAttr).canApplyPostFaint(pokemon, passive, simulated, attacker, move, hitResult, args), + args, + simulated, + ); +} + +export function applyPostItemLostAbAttrs( + attrType: AbAttrMap[K] extends PostItemLostAbAttr ? K : never, + pokemon: Pokemon, + simulated = false, + ...args: any[] +): void { + applyAbAttrsInternal( + attrType, + pokemon, + (attr, _passive) => (attr as PostItemLostAbAttr).applyPostItemLost(pokemon, simulated, args), + (attr, _passive) => (attr as PostItemLostAbAttr).canApplyPostItemLost(pokemon, simulated, args), + args, + ); +} + +/** + * Applies abilities when they become active mid-turn (ability switch) + * + * Ignores passives as they don't change and shouldn't be reapplied when main abilities change + */ +export function applyOnGainAbAttrs(pokemon: Pokemon, passive = false, simulated = false, ...args: any[]): void { + applySingleAbAttrs( + pokemon, + passive, + "PostSummonAbAttr", + (attr, passive) => attr.applyPostSummon(pokemon, passive, simulated, args), + (attr, passive) => attr.canApplyPostSummon(pokemon, passive, simulated, args), + args, + true, + simulated, + ); +} +/** + * Applies ability attributes which activate when the ability is lost or suppressed (i.e. primal weather) + */ +export function applyOnLoseAbAttrs(pokemon: Pokemon, passive = false, simulated = false, ...args: any[]): void { + applySingleAbAttrs( + pokemon, + passive, + "PreLeaveFieldAbAttr", + (attr, passive) => attr.applyPreLeaveField(pokemon, passive, simulated, [...args, true]), + (attr, passive) => attr.canApplyPreLeaveField(pokemon, passive, simulated, [...args, true]), + args, + true, + simulated, + ); + + applySingleAbAttrs( + pokemon, + passive, + "IllusionBreakAbAttr", + (attr, passive) => attr.apply(pokemon, passive, simulated, null, args), + (attr, passive) => attr.canApply(pokemon, passive, simulated, args), + args, + true, + simulated, + ); +} diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index 325ce0b7500..92bcab7fddb 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -10,15 +10,7 @@ import type Pokemon from "#app/field/pokemon"; import { HitResult } from "#enums/hit-result"; import { StatusEffect } from "#enums/status-effect"; import type { BattlerIndex } from "#enums/battler-index"; -import { - BlockNonDirectDamageAbAttr, - InfiltratorAbAttr, - PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr, - ProtectStatAbAttr, - applyAbAttrs, - applyOnGainAbAttrs, - applyOnLoseAbAttrs, -} from "#app/data/abilities/ability"; +import { applyAbAttrs, applyOnGainAbAttrs, applyOnLoseAbAttrs } from "./abilities/apply-ab-attrs"; import { Stat } from "#enums/stat"; import { CommonBattleAnim } from "#app/data/battle-anims"; import { CommonAnim } from "#enums/move-anims-common"; @@ -28,6 +20,7 @@ import { ArenaTagType } from "#enums/arena-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type"; import { MoveId } from "#enums/move-id"; import { ArenaTagSide } from "#enums/arena-tag-side"; +import { MoveUseMode } from "#enums/move-use-mode"; export abstract class ArenaTag { constructor( @@ -144,7 +137,7 @@ export class MistTag extends ArenaTag { if (attacker) { const bypassed = new BooleanHolder(false); // TODO: Allow this to be simulated - applyAbAttrs(InfiltratorAbAttr, attacker, null, false, bypassed); + applyAbAttrs("InfiltratorAbAttr", attacker, null, false, bypassed); if (bypassed.value) { return false; } @@ -209,7 +202,7 @@ export class WeakenMoveScreenTag extends ArenaTag { ): boolean { if (this.weakenedCategories.includes(moveCategory)) { const bypassed = new BooleanHolder(false); - applyAbAttrs(InfiltratorAbAttr, attacker, null, false, bypassed); + applyAbAttrs("InfiltratorAbAttr", attacker, null, false, bypassed); if (bypassed.value) { return false; } @@ -765,7 +758,7 @@ class SpikesTag extends ArenaTrapTag { } const cancelled = new BooleanHolder(false); - applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled); + applyAbAttrs("BlockNonDirectDamageAbAttr", pokemon, cancelled); if (simulated || cancelled.value) { return !cancelled.value; } @@ -883,13 +876,13 @@ export class DelayedAttackTag extends ArenaTag { const ret = super.lapse(arena); if (!ret) { + // TODO: This should not add to move history (for Spite) globalScene.phaseManager.unshiftNew( "MoveEffectPhase", this.sourceId!, [this.targetIndex], allMoves[this.sourceMove!], - false, - true, + MoveUseMode.FOLLOW_UP, ); // TODO: are those bangs correct? } @@ -953,7 +946,7 @@ class StealthRockTag extends ArenaTrapTag { override activateTrap(pokemon: Pokemon, simulated: boolean): boolean { const cancelled = new BooleanHolder(false); - applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled); + applyAbAttrs("BlockNonDirectDamageAbAttr", pokemon, cancelled); if (cancelled.value) { return false; } @@ -1010,7 +1003,7 @@ class StickyWebTag extends ArenaTrapTag { override activateTrap(pokemon: Pokemon, simulated: boolean): boolean { if (pokemon.isGrounded()) { const cancelled = new BooleanHolder(false); - applyAbAttrs(ProtectStatAbAttr, pokemon, cancelled); + applyAbAttrs("ProtectStatAbAttr", pokemon, cancelled); if (simulated) { return !cancelled.value; @@ -1442,8 +1435,8 @@ export class SuppressAbilitiesTag extends ArenaTag { // Could have a custom message that plays when a specific pokemon's NG ends? This entire thing exists due to passives after all const setter = globalScene .getField() - .filter(p => p?.hasAbilityWithAttr(PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr, false))[0]; - applyOnGainAbAttrs(setter, setter.getAbility().hasAttr(PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr)); + .filter(p => p?.hasAbilityWithAttr("PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr", false))[0]; + applyOnGainAbAttrs(setter, setter.getAbility().hasAttr("PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr")); } } @@ -1455,7 +1448,7 @@ export class SuppressAbilitiesTag extends ArenaTag { for (const pokemon of globalScene.getField(true)) { // There is only one pokemon with this attr on the field on removal, so its abilities are already active - if (pokemon && !pokemon.hasAbilityWithAttr(PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr, false)) { + if (pokemon && !pokemon.hasAbilityWithAttr("PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr", false)) { [true, false].forEach(passive => applyOnGainAbAttrs(pokemon, passive)); } } diff --git a/src/data/balance/pokemon-evolutions.ts b/src/data/balance/pokemon-evolutions.ts index 298cf2d0719..5dda1912e44 100644 --- a/src/data/balance/pokemon-evolutions.ts +++ b/src/data/balance/pokemon-evolutions.ts @@ -1,9 +1,9 @@ import { globalScene } from "#app/global-scene"; -import { Gender } from "#app/data/gender"; +import { Gender, getGenderSymbol } from "#app/data/gender"; import { PokeballType } from "#enums/pokeball"; import type Pokemon from "#app/field/pokemon"; import { PokemonType } from "#enums/pokemon-type"; -import { randSeedInt } from "#app/utils/common"; +import { coerceArray, isNullOrUndefined, randSeedInt } from "#app/utils/common"; import { WeatherType } from "#enums/weather-type"; import { Nature } from "#enums/nature"; import { BiomeId } from "#enums/biome-id"; @@ -11,11 +11,11 @@ import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { SpeciesFormKey } from "#enums/species-form-key"; import { TimeOfDay } from "#enums/time-of-day"; -import { DamageMoneyRewardModifier, ExtraModifierModifier, MoneyMultiplierModifier, SpeciesStatBoosterModifier, TempExtraModifierModifier } from "#app/modifier/modifier"; -import type { SpeciesStatBoosterModifierType } from "#app/modifier/modifier-type"; +import type { SpeciesStatBoosterItem, SpeciesStatBoosterModifierType } from "#app/modifier/modifier-type"; import { speciesStarterCosts } from "./starters"; import i18next from "i18next"; -import { initI18n } from "#app/plugins/i18n"; +import { allMoves } from "#app/data/data-lists"; +import { getPokemonSpecies } from "#app/utils/pokemon-utils"; export enum SpeciesWildEvolutionDelay { NONE, @@ -75,6 +75,8 @@ export enum EvolutionItem { LEADERS_CREST } +type TyrogueMove = MoveId.LOW_SWEEP | MoveId.MACH_PUNCH | MoveId.RAPID_SPIN; + /** * Pokemon Evolution tuple type consisting of: * @property 0 {@linkcode SpeciesId} The species of the Pokemon. @@ -82,8 +84,132 @@ export enum EvolutionItem { */ export type EvolutionLevel = [species: SpeciesId, level: number]; -export type EvolutionConditionPredicate = (p: Pokemon) => boolean; -export type EvolutionConditionEnforceFunc = (p: Pokemon) => void; +const EvoCondKey = { + FRIENDSHIP: 1, + TIME: 2, + MOVE: 3, + MOVE_TYPE: 4, + PARTY_TYPE: 5, + WEATHER: 6, + BIOME: 7, + TYROGUE: 8, + SHEDINJA: 9, + EVO_TREASURE_TRACKER: 10, + RANDOM_FORM: 11, + SPECIES_CAUGHT: 12, + GENDER: 13, + NATURE: 14, + HELD_ITEM: 15, // Currently checks only for species stat booster items +} as const; + +type EvolutionConditionData = + {key: typeof EvoCondKey.FRIENDSHIP | typeof EvoCondKey.RANDOM_FORM | typeof EvoCondKey.EVO_TREASURE_TRACKER, value: number} | + {key: typeof EvoCondKey.MOVE, move: MoveId} | + {key: typeof EvoCondKey.TIME, time: TimeOfDay[]} | + {key: typeof EvoCondKey.BIOME, biome: BiomeId[]} | + {key: typeof EvoCondKey.GENDER, gender: Gender} | + {key: typeof EvoCondKey.MOVE_TYPE | typeof EvoCondKey.PARTY_TYPE, pkmnType: PokemonType} | + {key: typeof EvoCondKey.SPECIES_CAUGHT, speciesCaught: SpeciesId} | + {key: typeof EvoCondKey.HELD_ITEM, itemKey: SpeciesStatBoosterItem} | + {key: typeof EvoCondKey.NATURE, nature: Nature[]} | + {key: typeof EvoCondKey.WEATHER, weather: WeatherType[]} | + {key: typeof EvoCondKey.TYROGUE, move: TyrogueMove} | + {key: typeof EvoCondKey.SHEDINJA}; + +export class SpeciesEvolutionCondition { + public data: EvolutionConditionData[]; + private desc: string[]; + + constructor(...data: EvolutionConditionData[]) { + this.data = data; + } + + public get description(): string[] { + if (!isNullOrUndefined(this.desc)) { + return this.desc; + } + this.desc = this.data.map(cond => { + switch(cond.key) { + case EvoCondKey.FRIENDSHIP: + return i18next.t("pokemonEvolutions:friendship"); + case EvoCondKey.TIME: + return i18next.t(`pokemonEvolutions:timeOfDay.${TimeOfDay[cond.time[cond.time.length - 1]]}`); // For Day and Night evos, the key we want goes last + case EvoCondKey.MOVE_TYPE: + return i18next.t("pokemonEvolutions:moveType", {type: i18next.t(`pokemonInfo:Type.${PokemonType[cond.pkmnType]}`)}); + case EvoCondKey.PARTY_TYPE: + return i18next.t("pokemonEvolutions:partyType", {type: i18next.t(`pokemonInfo:Type.${PokemonType[cond.pkmnType]}`)}); + case EvoCondKey.GENDER: + return i18next.t("pokemonEvolutions:gender", {gender: getGenderSymbol(cond.gender)}); + case EvoCondKey.MOVE: + case EvoCondKey.TYROGUE: + return i18next.t("pokemonEvolutions:move", {move: allMoves[cond.move].name}); + case EvoCondKey.BIOME: + return i18next.t("pokemonEvolutions:biome"); + case EvoCondKey.NATURE: + return i18next.t("pokemonEvolutions:nature"); + case EvoCondKey.WEATHER: + return i18next.t("pokemonEvolutions:weather"); + case EvoCondKey.SHEDINJA: + return i18next.t("pokemonEvolutions:shedinja"); + case EvoCondKey.EVO_TREASURE_TRACKER: + return i18next.t("pokemonEvolutions:treasure"); + case EvoCondKey.SPECIES_CAUGHT: + return i18next.t("pokemonEvolutions:caught", {species: getPokemonSpecies(cond.speciesCaught).name}); + case EvoCondKey.HELD_ITEM: + return i18next.t(`pokemonEvolutions:heldItem.${cond.itemKey}`); + } + }).filter(s => !isNullOrUndefined(s)); // Filter out stringless conditions + return this.desc; + } + + public conditionsFulfilled(pokemon: Pokemon): boolean { + console.log(this.data); + return this.data.every(cond => { + switch (cond.key) { + case EvoCondKey.FRIENDSHIP: + return pokemon.friendship >= cond.value; + case EvoCondKey.TIME: + return cond.time.includes(globalScene.arena.getTimeOfDay()); + case EvoCondKey.MOVE: + return pokemon.moveset.some(m => m.moveId === cond.move); + case EvoCondKey.MOVE_TYPE: + return pokemon.moveset.some(m => m.getMove().type === cond.pkmnType); + case EvoCondKey.PARTY_TYPE: + return globalScene.getPlayerParty().some(p => p.getTypes(false, false, true).includes(cond.pkmnType)) + case EvoCondKey.EVO_TREASURE_TRACKER: + return pokemon.getHeldItems().some(m => + m.is("EvoTrackerModifier") && + m.getStackCount() + pokemon.getPersistentTreasureCount() >= cond.value + ); + case EvoCondKey.GENDER: + return pokemon.gender === cond.gender; + case EvoCondKey.SHEDINJA: // Shedinja cannot be evolved into directly + return false; + case EvoCondKey.BIOME: + return cond.biome.includes(globalScene.arena.biomeType); + case EvoCondKey.WEATHER: + return cond.weather.includes(globalScene.arena.getWeatherType()); + case EvoCondKey.TYROGUE: + return pokemon.getMoveset(true).find(m => m.moveId as TyrogueMove)?.moveId === cond.move; + case EvoCondKey.NATURE: + return cond.nature.includes(pokemon.getNature()); + case EvoCondKey.RANDOM_FORM: { + let ret = false; + globalScene.executeWithSeedOffset(() => ret = !randSeedInt(cond.value), pokemon.id); + return ret; + } + case EvoCondKey.SPECIES_CAUGHT: + return !!globalScene.gameData.dexData[cond.speciesCaught].caughtAttr; + case EvoCondKey.HELD_ITEM: + return pokemon.getHeldItems().some(m => m.is("SpeciesStatBoosterModifier") && (m.type as SpeciesStatBoosterModifierType).key === cond.itemKey) + } + }); + } +} + +export function validateShedinjaEvo(): boolean { + return globalScene.getPlayerParty().length < 6 && globalScene.pokeballCounts[PokeballType.POKEBALL] > 0; +} export class SpeciesFormEvolution { public speciesId: SpeciesId; @@ -93,41 +219,101 @@ export class SpeciesFormEvolution { public item: EvolutionItem | null; public condition: SpeciesEvolutionCondition | null; public wildDelay: SpeciesWildEvolutionDelay; - public description = ""; + public desc = ""; - constructor(speciesId: SpeciesId, preFormKey: string | null, evoFormKey: string | null, level: number, item: EvolutionItem | null, condition: SpeciesEvolutionCondition | null, wildDelay?: SpeciesWildEvolutionDelay) { - if (!i18next.isInitialized) { - initI18n(); - } + constructor(speciesId: SpeciesId, preFormKey: string | null, evoFormKey: string | null, level: number, item: EvolutionItem | null, condition: EvolutionConditionData | EvolutionConditionData[] | null, wildDelay?: SpeciesWildEvolutionDelay) { this.speciesId = speciesId; this.preFormKey = preFormKey; this.evoFormKey = evoFormKey; this.level = level; this.item = item || EvolutionItem.NONE; - this.condition = condition; + if (!isNullOrUndefined(condition)) { + this.condition = new SpeciesEvolutionCondition(...coerceArray(condition)); + } this.wildDelay = wildDelay ?? SpeciesWildEvolutionDelay.NONE; + } + + get description(): string { + if (this.desc.length > 0) { + return this.desc; + } const strings: string[] = []; + let len = 0; if (this.level > 1) { - strings.push(i18next.t("pokemonEvolutions:level") + ` ${this.level}`); + strings.push(i18next.t("pokemonEvolutions:atLevel", {lv: this.level})); } if (this.item) { const itemDescription = i18next.t(`modifierType:EvolutionItem.${EvolutionItem[this.item].toUpperCase()}`); const rarity = this.item > 50 ? i18next.t("pokemonEvolutions:ULTRA") : i18next.t("pokemonEvolutions:GREAT"); - strings.push(i18next.t("pokemonEvolutions:using") + itemDescription + ` (${rarity})`); + strings.push(i18next.t("pokemonEvolutions:using", {item: itemDescription, tier: rarity})); } if (this.condition) { - strings.push(this.condition.description); + if (strings.length === 0) { + strings.push(i18next.t("pokemonEvolutions:levelUp")); + } + strings.push(...this.condition.description); } - this.description = strings + this.desc = strings .filter(str => str !== "") - .map((str, index) => index > 0 ? str[0].toLowerCase() + str.slice(1) : str) - .join(i18next.t("pokemonEvolutions:connector")); + .map((str, index) => { + if (index === 0) { + len = str.length; + return str; + } + if (len + str.length > 60) { + len = str.length; + return "\n" + str[0].toLowerCase() + str.slice(1); + } + len += str.length; + return str[0].toLowerCase() + str.slice(1); + }) + .join(" ") + .replace(" \n", i18next.t("pokemonEvolutions:connector") + "\n"); + + return this.desc; + } + + /** + * Checks if a Pokemon fulfills the requirements of this evolution. + * @param pokemon {@linkcode Pokemon} who wants to evolve + * @param forFusion defaults to False. Whether this evolution is meant for the secondary fused mon. In that case, use their form key. + * @param item {@linkcode EvolutionItem} optional, check if the evolution uses a certain item + * @returns whether this evolution can apply to the Pokemon + */ + public validate(pokemon: Pokemon, forFusion = false, item?: EvolutionItem): boolean { + return ( + pokemon.level >= this.level && + // Check form key, using the fusion's form key if we're checking the fusion + (isNullOrUndefined(this.preFormKey) || (forFusion ? pokemon.getFusionFormKey() : pokemon.getFormKey()) === this.preFormKey) && + (isNullOrUndefined(this.condition) || this.condition.conditionsFulfilled(pokemon)) && + ((item ?? EvolutionItem.NONE) === (this.item ?? EvolutionItem.NONE)) + ); + } + + /** + * Checks if this evolution is item-based and any conditions for it are fulfilled + * @param pokemon {@linkcode Pokemon} who wants to evolve + * @param forFusion defaults to False. Whether this evolution is meant for the secondary fused mon. In that case, use their form key. + * @returns whether this evolution uses an item and can apply to the Pokemon + */ + public isValidItemEvolution(pokemon: Pokemon, forFusion = false): boolean { + return ( + !isNullOrUndefined(this.item) && + pokemon.level >= this.level && + // Check form key, using the fusion's form key if we're checking the fusion + (isNullOrUndefined(this.preFormKey) || (forFusion ? pokemon.getFusionFormKey() : pokemon.getFormKey()) === this.preFormKey) && + (isNullOrUndefined(this.condition) || this.condition.conditionsFulfilled(pokemon)) + ); + } + + public get evoItem(): EvolutionItem { + return this.item ?? EvolutionItem.NONE; } } export class SpeciesEvolution extends SpeciesFormEvolution { - constructor(speciesId: SpeciesId, level: number, item: EvolutionItem | null, condition: SpeciesEvolutionCondition | null, wildDelay?: SpeciesWildEvolutionDelay) { + constructor(speciesId: SpeciesId, level: number, item: EvolutionItem | null, condition: EvolutionConditionData | EvolutionConditionData[] | null, wildDelay?: SpeciesWildEvolutionDelay) { super(speciesId, null, null, level, item, condition, wildDelay); } } @@ -136,226 +322,12 @@ export class FusionSpeciesFormEvolution extends SpeciesFormEvolution { public primarySpeciesId: SpeciesId; constructor(primarySpeciesId: SpeciesId, evolution: SpeciesFormEvolution) { - super(evolution.speciesId, evolution.preFormKey, evolution.evoFormKey, evolution.level, evolution.item, evolution.condition, evolution.wildDelay); + super(evolution.speciesId, evolution.preFormKey, evolution.evoFormKey, evolution.level, evolution.item, evolution.condition?.data ?? null, evolution.wildDelay); this.primarySpeciesId = primarySpeciesId; } } -export class SpeciesEvolutionCondition { - public predicate: EvolutionConditionPredicate; - public enforceFunc?: EvolutionConditionEnforceFunc; - public description: string; - - constructor(predicate: EvolutionConditionPredicate, enforceFunc?: EvolutionConditionEnforceFunc) { - this.predicate = predicate; - this.enforceFunc = enforceFunc; - this.description = ""; - } -} - -class GenderEvolutionCondition extends SpeciesEvolutionCondition { - public gender: Gender; - constructor(gender: Gender) { - super(p => p.gender === gender, p => p.gender = gender); - this.gender = gender; - this.description = i18next.t("pokemonEvolutions:gender", { gender: i18next.t(`pokemonEvolutions:${Gender[gender]}`) }); - } -} - -class TimeOfDayEvolutionCondition extends SpeciesEvolutionCondition { - public timesOfDay: TimeOfDay[]; - constructor(tod: "day" | "night") { - if (tod === "day") { - super(() => globalScene.arena.getTimeOfDay() === TimeOfDay.DAWN || globalScene.arena.getTimeOfDay() === TimeOfDay.DAY); - this.timesOfDay = [ TimeOfDay.DAWN, TimeOfDay.DAY ]; - } else if (tod === "night") { - super(() => globalScene.arena.getTimeOfDay() === TimeOfDay.DUSK || globalScene.arena.getTimeOfDay() === TimeOfDay.NIGHT); - this.timesOfDay = [ TimeOfDay.DUSK, TimeOfDay.NIGHT ]; - } else { - super(() => false); - this.timesOfDay = []; - } - this.description = i18next.t("pokemonEvolutions:timeOfDay", { tod: i18next.t(`pokemonEvolutions:${tod}`) }); - } -} - -class MoveEvolutionCondition extends SpeciesEvolutionCondition { - public move: MoveId; - constructor(move: MoveId) { - super(p => p.moveset.filter(m => m.moveId === move).length > 0); - this.move = move; - const moveKey = MoveId[this.move].split("_").filter(f => f).map((f, i) => i ? `${f[0]}${f.slice(1).toLowerCase()}` : f.toLowerCase()).join(""); - this.description = i18next.t("pokemonEvolutions:move", { move: i18next.t(`move:${moveKey}.name`) }); - } -} - -class FriendshipEvolutionCondition extends SpeciesEvolutionCondition { - public amount: number; - constructor(amount: number) { - super(p => p.friendship >= amount); - this.amount = amount; - this.description = i18next.t("pokemonEvolutions:friendship"); - } -} - -class FriendshipTimeOfDayEvolutionCondition extends SpeciesEvolutionCondition { - public amount: number; - public timesOfDay: TimeOfDay[]; - constructor(amount: number, tod: "day" | "night") { - if (tod === "day") { - super(p => p.friendship >= amount && (globalScene.arena.getTimeOfDay() === TimeOfDay.DAWN || globalScene.arena.getTimeOfDay() === TimeOfDay.DAY)); - this.timesOfDay = [ TimeOfDay.DAWN, TimeOfDay.DAY ]; - } else if (tod === "night") { - super(p => p.friendship >= amount && (globalScene.arena.getTimeOfDay() === TimeOfDay.DUSK || globalScene.arena.getTimeOfDay() === TimeOfDay.NIGHT)); - this.timesOfDay = [ TimeOfDay.DUSK, TimeOfDay.NIGHT ]; - } else { - super(_p => false); - this.timesOfDay = []; - } - this.amount = amount; - this.description = i18next.t("pokemonEvolutions:friendshipTimeOfDay", { tod: i18next.t(`pokemonEvolutions:${tod}`) }); - } -} - -class FriendshipMoveTypeEvolutionCondition extends SpeciesEvolutionCondition { - public amount: number; - public type: PokemonType; - constructor(amount: number, type: PokemonType) { - super(p => p.friendship >= amount && !!p.getMoveset().find(m => m?.getMove().type === type)); - this.amount = amount; - this.type = type; - this.description = i18next.t("pokemonEvolutions:friendshipMoveType", { type: i18next.t(`pokemonInfo:Type.${PokemonType[this.type]}`) }); - } -} - -class ShedinjaEvolutionCondition extends SpeciesEvolutionCondition { - constructor() { - super(() => globalScene.getPlayerParty().length < 6 && globalScene.pokeballCounts[PokeballType.POKEBALL] > 0); - this.description = i18next.t("pokemonEvolutions:shedinja"); - } -} - -class PartyTypeEvolutionCondition extends SpeciesEvolutionCondition { - public type: PokemonType; - constructor(type: PokemonType) { - super(() => !!globalScene.getPlayerParty().find(p => p.getTypes(false, false, true).indexOf(type) > -1)); - this.type = type; - this.description = i18next.t("pokemonEvolutions:partyType", { type: i18next.t(`pokemonInfo:Type.${PokemonType[this.type]}`) }); - } -} - -class CaughtEvolutionCondition extends SpeciesEvolutionCondition { - public species: SpeciesId; - constructor(species: SpeciesId) { - super(() => !!globalScene.gameData.dexData[species].caughtAttr); - this.species = species; - this.description = i18next.t("pokemonEvolutions:caught", { species: i18next.t(`pokemon:${SpeciesId[this.species].toLowerCase()}`) }); - } -} - -class WeatherEvolutionCondition extends SpeciesEvolutionCondition { - public weatherTypes: WeatherType[]; - constructor(weatherTypes: WeatherType[]) { - super(() => weatherTypes.indexOf(globalScene.arena.weather?.weatherType || WeatherType.NONE) > -1); - this.weatherTypes = weatherTypes; - this.description = i18next.t("pokemonEvolutions:weather"); - } -} - -class MoveTypeEvolutionCondition extends SpeciesEvolutionCondition { - public type: PokemonType; - constructor(type: PokemonType) { - super(p => p.moveset.filter(m => m?.getMove().type === type).length > 0); - this.type = type; - this.description = i18next.t("pokemonEvolutions:moveType", { type: i18next.t(`pokemonInfo:Type.${PokemonType[this.type]}`) }); - } -} - -class TreasureEvolutionCondition extends SpeciesEvolutionCondition { - constructor() { - super(p => p.evoCounter - + p.getHeldItems().filter(m => m instanceof DamageMoneyRewardModifier).length - + globalScene.findModifiers(m => m instanceof MoneyMultiplierModifier - || m instanceof ExtraModifierModifier || m instanceof TempExtraModifierModifier).length > 9); - this.description = i18next.t("pokemonEvolutions:treasure"); - } -} - -class TyrogueEvolutionCondition extends SpeciesEvolutionCondition { - public move: MoveId; - constructor(move: MoveId) { - super(p => - p.getMoveset(true).find(m => m && [ MoveId.LOW_SWEEP, MoveId.MACH_PUNCH, MoveId.RAPID_SPIN ].includes(m.moveId))?.moveId === move); - this.move = move; - const moveKey = MoveId[this.move].split("_").filter(f => f).map((f, i) => i ? `${f[0]}${f.slice(1).toLowerCase()}` : f.toLowerCase()).join(""); - this.description = i18next.t("pokemonEvolutions:move", { move: i18next.t(`move:${moveKey}.name`) }); - } -} - -class NatureEvolutionCondition extends SpeciesEvolutionCondition { - public natures: Nature[]; - constructor(natures: Nature[]) { - super(p => natures.indexOf(p.getNature()) > -1); - this.natures = natures; - this.description = i18next.t("pokemonEvolutions:nature"); - } -} - -class MoveTimeOfDayEvolutionCondition extends SpeciesEvolutionCondition { - public move: MoveId; - public timesOfDay: TimeOfDay[]; - constructor(move: MoveId, tod: "day" | "night") { - if (tod === "day") { - super(p => p.moveset.filter(m => m.moveId === move).length > 0 && (globalScene.arena.getTimeOfDay() === TimeOfDay.DAWN || globalScene.arena.getTimeOfDay() === TimeOfDay.DAY)); - this.move = move; - this.timesOfDay = [ TimeOfDay.DAWN, TimeOfDay.DAY ]; - } else if (tod === "night") { - super(p => p.moveset.filter(m => m.moveId === move).length > 0 && (globalScene.arena.getTimeOfDay() === TimeOfDay.DUSK || globalScene.arena.getTimeOfDay() === TimeOfDay.NIGHT)); - this.move = move; - this.timesOfDay = [ TimeOfDay.DUSK, TimeOfDay.NIGHT ]; - } else { - super(() => false); - this.timesOfDay = []; - } - const moveKey = MoveId[this.move].split("_").filter(f => f).map((f, i) => i ? `${f[0]}${f.slice(1).toLowerCase()}` : f.toLowerCase()).join(""); - this.description = i18next.t("pokemonEvolutions:moveTimeOfDay", { move: i18next.t(`move:${moveKey}.name`), tod: i18next.t(`pokemonEvolutions:${tod}`) }); - } -} - -class BiomeEvolutionCondition extends SpeciesEvolutionCondition { - public biomes: BiomeId[]; - constructor(biomes: BiomeId[]) { - super(() => biomes.filter(b => b === globalScene.arena.biomeType).length > 0); - this.biomes = biomes; - this.description = i18next.t("pokemonEvolutions:biome"); - } -} - -class DunsparceEvolutionCondition extends SpeciesEvolutionCondition { - constructor() { - super(p => { - let ret = false; - if (p.moveset.filter(m => m.moveId === MoveId.HYPER_DRILL).length > 0) { - globalScene.executeWithSeedOffset(() => ret = !randSeedInt(4), p.id); - } - return ret; - }); - const moveKey = MoveId[MoveId.HYPER_DRILL].split("_").filter(f => f).map((f, i) => i ? `${f[0]}${f.slice(1).toLowerCase()}` : f.toLowerCase()).join(""); - this.description = i18next.t("pokemonEvolutions:move", { move: i18next.t(`move:${moveKey}.name`) }); - } -} - -class TandemausEvolutionCondition extends SpeciesEvolutionCondition { - constructor() { - super(p => { - let ret = false; - globalScene.executeWithSeedOffset(() => ret = !randSeedInt(4), p.id); - return ret; - }); - } -} - interface PokemonEvolutions { [key: string]: SpeciesFormEvolution[] } @@ -489,8 +461,8 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(SpeciesId.ELECTRODE, 30, null, null) ], [SpeciesId.CUBONE]: [ - new SpeciesEvolution(SpeciesId.ALOLA_MAROWAK, 28, null, new TimeOfDayEvolutionCondition("night")), - new SpeciesEvolution(SpeciesId.MAROWAK, 28, null, new TimeOfDayEvolutionCondition("day")) + new SpeciesEvolution(SpeciesId.ALOLA_MAROWAK, 28, null, {key: EvoCondKey.TIME, time: [TimeOfDay.DUSK, TimeOfDay.NIGHT]}), + new SpeciesEvolution(SpeciesId.MAROWAK, 28, null, {key: EvoCondKey.TIME, time: [TimeOfDay.DAWN, TimeOfDay.DAY]}) ], [SpeciesId.TYROGUE]: [ /** @@ -499,13 +471,13 @@ export const pokemonEvolutions: PokemonEvolutions = { * If Tyrogue knows multiple of these moves, its evolution is based on * the first qualifying move in its moveset. */ - new SpeciesEvolution(SpeciesId.HITMONLEE, 20, null, new TyrogueEvolutionCondition(MoveId.LOW_SWEEP)), - new SpeciesEvolution(SpeciesId.HITMONCHAN, 20, null, new TyrogueEvolutionCondition(MoveId.MACH_PUNCH)), - new SpeciesEvolution(SpeciesId.HITMONTOP, 20, null, new TyrogueEvolutionCondition(MoveId.RAPID_SPIN)), + new SpeciesEvolution(SpeciesId.HITMONLEE, 20, null, {key: EvoCondKey.TYROGUE, move: MoveId.LOW_SWEEP}), + new SpeciesEvolution(SpeciesId.HITMONCHAN, 20, null, {key: EvoCondKey.TYROGUE, move: MoveId.MACH_PUNCH}), + new SpeciesEvolution(SpeciesId.HITMONTOP, 20, null, {key: EvoCondKey.TYROGUE, move: MoveId.RAPID_SPIN}), ], [SpeciesId.KOFFING]: [ - new SpeciesEvolution(SpeciesId.GALAR_WEEZING, 35, null, new TimeOfDayEvolutionCondition("night")), - new SpeciesEvolution(SpeciesId.WEEZING, 35, null, new TimeOfDayEvolutionCondition("day")) + new SpeciesEvolution(SpeciesId.GALAR_WEEZING, 35, null, {key: EvoCondKey.TIME, time: [TimeOfDay.DUSK, TimeOfDay.NIGHT]}), + new SpeciesEvolution(SpeciesId.WEEZING, 35, null, {key: EvoCondKey.TIME, time: [TimeOfDay.DAWN, TimeOfDay.DAY]}) ], [SpeciesId.RHYHORN]: [ new SpeciesEvolution(SpeciesId.RHYDON, 42, null, null) @@ -550,8 +522,8 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(SpeciesId.QUILAVA, 14, null, null) ], [SpeciesId.QUILAVA]: [ - new SpeciesEvolution(SpeciesId.HISUI_TYPHLOSION, 36, null, new TimeOfDayEvolutionCondition("night")), - new SpeciesEvolution(SpeciesId.TYPHLOSION, 36, null, new TimeOfDayEvolutionCondition("day")) + new SpeciesEvolution(SpeciesId.HISUI_TYPHLOSION, 36, null, {key: EvoCondKey.TIME, time: [TimeOfDay.DUSK, TimeOfDay.NIGHT]}), + new SpeciesEvolution(SpeciesId.TYPHLOSION, 36, null, {key: EvoCondKey.TIME, time: [TimeOfDay.DAWN, TimeOfDay.DAY]}) ], [SpeciesId.TOTODILE]: [ new SpeciesEvolution(SpeciesId.CROCONAW, 18, null, null) @@ -653,8 +625,8 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(SpeciesId.LINOONE, 20, null, null) ], [SpeciesId.WURMPLE]: [ - new SpeciesEvolution(SpeciesId.SILCOON, 7, null, new TimeOfDayEvolutionCondition("day")), - new SpeciesEvolution(SpeciesId.CASCOON, 7, null, new TimeOfDayEvolutionCondition("night")) + new SpeciesEvolution(SpeciesId.SILCOON, 7, null, {key: EvoCondKey.TIME, time: [TimeOfDay.DAWN, TimeOfDay.DAY]}), + new SpeciesEvolution(SpeciesId.CASCOON, 7, null, {key: EvoCondKey.TIME, time: [TimeOfDay.DUSK, TimeOfDay.NIGHT]}) ], [SpeciesId.SILCOON]: [ new SpeciesEvolution(SpeciesId.BEAUTIFLY, 10, null, null) @@ -678,8 +650,8 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(SpeciesId.KIRLIA, 20, null, null) ], [SpeciesId.KIRLIA]: [ - new SpeciesEvolution(SpeciesId.GARDEVOIR, 30, null, new GenderEvolutionCondition(Gender.FEMALE)), - new SpeciesEvolution(SpeciesId.GALLADE, 30, null, new GenderEvolutionCondition(Gender.MALE)) + new SpeciesEvolution(SpeciesId.GARDEVOIR, 30, null, {key: EvoCondKey.GENDER, gender: Gender.FEMALE}), + new SpeciesEvolution(SpeciesId.GALLADE, 30, null, {key: EvoCondKey.GENDER, gender: Gender.MALE}) ], [SpeciesId.SURSKIT]: [ new SpeciesEvolution(SpeciesId.MASQUERAIN, 22, null, null) @@ -695,7 +667,7 @@ export const pokemonEvolutions: PokemonEvolutions = { ], [SpeciesId.NINCADA]: [ new SpeciesEvolution(SpeciesId.NINJASK, 20, null, null), - new SpeciesEvolution(SpeciesId.SHEDINJA, 20, null, new ShedinjaEvolutionCondition()) + new SpeciesEvolution(SpeciesId.SHEDINJA, 20, null, {key: EvoCondKey.SHEDINJA}) ], [SpeciesId.WHISMUR]: [ new SpeciesEvolution(SpeciesId.LOUDRED, 20, null, null) @@ -767,8 +739,8 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(SpeciesId.DUSCLOPS, 37, null, null) ], [SpeciesId.SNORUNT]: [ - new SpeciesEvolution(SpeciesId.GLALIE, 42, null, new GenderEvolutionCondition(Gender.MALE)), - new SpeciesEvolution(SpeciesId.FROSLASS, 42, null, new GenderEvolutionCondition(Gender.FEMALE)) + new SpeciesEvolution(SpeciesId.GLALIE, 42, null, {key: EvoCondKey.GENDER, gender: Gender.MALE}), + new SpeciesEvolution(SpeciesId.FROSLASS, 42, null, {key: EvoCondKey.GENDER, gender: Gender.FEMALE}) ], [SpeciesId.SPHEAL]: [ new SpeciesEvolution(SpeciesId.SEALEO, 32, null, null) @@ -831,11 +803,11 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(SpeciesId.BASTIODON, 30, null, null) ], [SpeciesId.BURMY]: [ - new SpeciesEvolution(SpeciesId.MOTHIM, 20, null, new GenderEvolutionCondition(Gender.MALE)), - new SpeciesEvolution(SpeciesId.WORMADAM, 20, null, new GenderEvolutionCondition(Gender.FEMALE)) + new SpeciesEvolution(SpeciesId.MOTHIM, 20, null, {key: EvoCondKey.GENDER, gender: Gender.MALE}), + new SpeciesEvolution(SpeciesId.WORMADAM, 20, null, {key: EvoCondKey.GENDER, gender: Gender.FEMALE}) ], [SpeciesId.COMBEE]: [ - new SpeciesEvolution(SpeciesId.VESPIQUEN, 21, null, new GenderEvolutionCondition(Gender.FEMALE)) + new SpeciesEvolution(SpeciesId.VESPIQUEN, 21, null, {key: EvoCondKey.GENDER, gender: Gender.FEMALE}) ], [SpeciesId.BUIZEL]: [ new SpeciesEvolution(SpeciesId.FLOATZEL, 26, null, null) @@ -877,7 +849,7 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(SpeciesId.LUMINEON, 31, null, null) ], [SpeciesId.MANTYKE]: [ - new SpeciesEvolution(SpeciesId.MANTINE, 32, null, new CaughtEvolutionCondition(SpeciesId.REMORAID), SpeciesWildEvolutionDelay.MEDIUM) + new SpeciesEvolution(SpeciesId.MANTINE, 32, null, {key: EvoCondKey.SPECIES_CAUGHT, speciesCaught: SpeciesId.REMORAID}, SpeciesWildEvolutionDelay.MEDIUM) ], [SpeciesId.SNOVER]: [ new SpeciesEvolution(SpeciesId.ABOMASNOW, 40, null, null) @@ -898,8 +870,8 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(SpeciesId.DEWOTT, 17, null, null) ], [SpeciesId.DEWOTT]: [ - new SpeciesEvolution(SpeciesId.HISUI_SAMUROTT, 36, null, new TimeOfDayEvolutionCondition("night")), - new SpeciesEvolution(SpeciesId.SAMUROTT, 36, null, new TimeOfDayEvolutionCondition("day")) + new SpeciesEvolution(SpeciesId.HISUI_SAMUROTT, 36, null, {key: EvoCondKey.TIME, time: [TimeOfDay.DUSK, TimeOfDay.NIGHT]}), + new SpeciesEvolution(SpeciesId.SAMUROTT, 36, null, {key: EvoCondKey.TIME, time: [TimeOfDay.DAWN, TimeOfDay.DAY]}) ], [SpeciesId.PATRAT]: [ new SpeciesEvolution(SpeciesId.WATCHOG, 20, null, null) @@ -1049,8 +1021,8 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(SpeciesId.KINGAMBIT, 1, EvolutionItem.LEADERS_CREST, null, SpeciesWildEvolutionDelay.VERY_LONG) ], [SpeciesId.RUFFLET]: [ - new SpeciesEvolution(SpeciesId.HISUI_BRAVIARY, 54, null, new TimeOfDayEvolutionCondition("night")), - new SpeciesEvolution(SpeciesId.BRAVIARY, 54, null, new TimeOfDayEvolutionCondition("day")) + new SpeciesEvolution(SpeciesId.HISUI_BRAVIARY, 54, null, {key: EvoCondKey.TIME, time: [TimeOfDay.DUSK, TimeOfDay.NIGHT]}), + new SpeciesEvolution(SpeciesId.BRAVIARY, 54, null, {key: EvoCondKey.TIME, time: [TimeOfDay.DAWN, TimeOfDay.DAY]}) ], [SpeciesId.VULLABY]: [ new SpeciesEvolution(SpeciesId.MANDIBUZZ, 54, null, null) @@ -1107,11 +1079,11 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(SpeciesId.GOGOAT, 32, null, null) ], [SpeciesId.PANCHAM]: [ - new SpeciesEvolution(SpeciesId.PANGORO, 32, null, new PartyTypeEvolutionCondition(PokemonType.DARK), SpeciesWildEvolutionDelay.MEDIUM) + new SpeciesEvolution(SpeciesId.PANGORO, 32, null, {key: EvoCondKey.PARTY_TYPE, pkmnType: PokemonType.DARK}, SpeciesWildEvolutionDelay.MEDIUM) ], [SpeciesId.ESPURR]: [ - new SpeciesFormEvolution(SpeciesId.MEOWSTIC, "", "female", 25, null, new GenderEvolutionCondition(Gender.FEMALE)), - new SpeciesFormEvolution(SpeciesId.MEOWSTIC, "", "", 25, null, new GenderEvolutionCondition(Gender.MALE)) + new SpeciesFormEvolution(SpeciesId.MEOWSTIC, "", "female", 25, null, {key: EvoCondKey.GENDER, gender: Gender.FEMALE}), + new SpeciesFormEvolution(SpeciesId.MEOWSTIC, "", "", 25, null, {key: EvoCondKey.GENDER, gender: Gender.MALE}) ], [SpeciesId.HONEDGE]: [ new SpeciesEvolution(SpeciesId.DOUBLADE, 35, null, null) @@ -1129,21 +1101,21 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(SpeciesId.CLAWITZER, 37, null, null) ], [SpeciesId.TYRUNT]: [ - new SpeciesEvolution(SpeciesId.TYRANTRUM, 39, null, new TimeOfDayEvolutionCondition("day")) + new SpeciesEvolution(SpeciesId.TYRANTRUM, 39, null, {key: EvoCondKey.TIME, time: [TimeOfDay.DAWN, TimeOfDay.DAY]}) ], [SpeciesId.AMAURA]: [ - new SpeciesEvolution(SpeciesId.AURORUS, 39, null, new TimeOfDayEvolutionCondition("night")) + new SpeciesEvolution(SpeciesId.AURORUS, 39, null, {key: EvoCondKey.TIME, time: [TimeOfDay.DUSK, TimeOfDay.NIGHT]}) ], [SpeciesId.GOOMY]: [ - new SpeciesEvolution(SpeciesId.HISUI_SLIGGOO, 40, null, new TimeOfDayEvolutionCondition("night")), - new SpeciesEvolution(SpeciesId.SLIGGOO, 40, null, new TimeOfDayEvolutionCondition("day")) + new SpeciesEvolution(SpeciesId.HISUI_SLIGGOO, 40, null, {key: EvoCondKey.TIME, time: [TimeOfDay.DUSK, TimeOfDay.NIGHT]}), + new SpeciesEvolution(SpeciesId.SLIGGOO, 40, null, {key: EvoCondKey.TIME, time: [TimeOfDay.DAWN, TimeOfDay.DAY]}) ], [SpeciesId.SLIGGOO]: [ - new SpeciesEvolution(SpeciesId.GOODRA, 50, null, new WeatherEvolutionCondition([ WeatherType.RAIN, WeatherType.FOG, WeatherType.HEAVY_RAIN ]), SpeciesWildEvolutionDelay.LONG) + new SpeciesEvolution(SpeciesId.GOODRA, 50, null, {key: EvoCondKey.WEATHER, weather: [ WeatherType.RAIN, WeatherType.FOG, WeatherType.HEAVY_RAIN ]}, SpeciesWildEvolutionDelay.LONG) ], [SpeciesId.BERGMITE]: [ - new SpeciesEvolution(SpeciesId.HISUI_AVALUGG, 37, null, new TimeOfDayEvolutionCondition("night")), - new SpeciesEvolution(SpeciesId.AVALUGG, 37, null, new TimeOfDayEvolutionCondition("day")) + new SpeciesEvolution(SpeciesId.HISUI_AVALUGG, 37, null, {key: EvoCondKey.TIME, time: [TimeOfDay.DUSK, TimeOfDay.NIGHT]}), + new SpeciesEvolution(SpeciesId.AVALUGG, 37, null, {key: EvoCondKey.TIME, time: [TimeOfDay.DAWN, TimeOfDay.DAY]}) ], [SpeciesId.NOIBAT]: [ new SpeciesEvolution(SpeciesId.NOIVERN, 48, null, null) @@ -1152,8 +1124,8 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(SpeciesId.DARTRIX, 17, null, null) ], [SpeciesId.DARTRIX]: [ - new SpeciesEvolution(SpeciesId.HISUI_DECIDUEYE, 36, null, new TimeOfDayEvolutionCondition("night")), - new SpeciesEvolution(SpeciesId.DECIDUEYE, 34, null, new TimeOfDayEvolutionCondition("day")) + new SpeciesEvolution(SpeciesId.HISUI_DECIDUEYE, 36, null, {key: EvoCondKey.TIME, time: [TimeOfDay.DUSK, TimeOfDay.NIGHT]}), + new SpeciesEvolution(SpeciesId.DECIDUEYE, 34, null, {key: EvoCondKey.TIME, time: [TimeOfDay.DAWN, TimeOfDay.DAY]}) ], [SpeciesId.LITTEN]: [ new SpeciesEvolution(SpeciesId.TORRACAT, 17, null, null) @@ -1174,7 +1146,7 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(SpeciesId.TOUCANNON, 28, null, null) ], [SpeciesId.YUNGOOS]: [ - new SpeciesEvolution(SpeciesId.GUMSHOOS, 20, null, new TimeOfDayEvolutionCondition("day")) + new SpeciesEvolution(SpeciesId.GUMSHOOS, 20, null, {key: EvoCondKey.TIME, time: [TimeOfDay.DAWN, TimeOfDay.DAY]}) ], [SpeciesId.GRUBBIN]: [ new SpeciesEvolution(SpeciesId.CHARJABUG, 20, null, null) @@ -1192,13 +1164,13 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(SpeciesId.ARAQUANID, 22, null, null) ], [SpeciesId.FOMANTIS]: [ - new SpeciesEvolution(SpeciesId.LURANTIS, 34, null, new TimeOfDayEvolutionCondition("day")) + new SpeciesEvolution(SpeciesId.LURANTIS, 34, null, {key: EvoCondKey.TIME, time: [TimeOfDay.DAWN, TimeOfDay.DAY]}) ], [SpeciesId.MORELULL]: [ new SpeciesEvolution(SpeciesId.SHIINOTIC, 24, null, null) ], [SpeciesId.SALANDIT]: [ - new SpeciesEvolution(SpeciesId.SALAZZLE, 33, null, new GenderEvolutionCondition(Gender.FEMALE)) + new SpeciesEvolution(SpeciesId.SALAZZLE, 33, null, {key: EvoCondKey.GENDER, gender: Gender.FEMALE}) ], [SpeciesId.STUFFUL]: [ new SpeciesEvolution(SpeciesId.BEWEAR, 27, null, null) @@ -1229,7 +1201,7 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(SpeciesId.MELMETAL, 48, null, null) ], [SpeciesId.ALOLA_RATTATA]: [ - new SpeciesEvolution(SpeciesId.ALOLA_RATICATE, 20, null, new TimeOfDayEvolutionCondition("night")) + new SpeciesEvolution(SpeciesId.ALOLA_RATICATE, 20, null, {key: EvoCondKey.TIME, time: [TimeOfDay.DUSK, TimeOfDay.NIGHT]}) ], [SpeciesId.ALOLA_DIGLETT]: [ new SpeciesEvolution(SpeciesId.ALOLA_DUGTRIO, 26, null, null) @@ -1302,7 +1274,7 @@ export const pokemonEvolutions: PokemonEvolutions = { ], [SpeciesId.TOXEL]: [ new SpeciesFormEvolution(SpeciesId.TOXTRICITY, "", "lowkey", 30, null, - new NatureEvolutionCondition([ Nature.LONELY, Nature.BOLD, Nature.RELAXED, Nature.TIMID, Nature.SERIOUS, Nature.MODEST, Nature.MILD, Nature.QUIET, Nature.BASHFUL, Nature.CALM, Nature.GENTLE, Nature.CAREFUL ]) + {key: EvoCondKey.NATURE, nature: [ Nature.LONELY, Nature.BOLD, Nature.RELAXED, Nature.TIMID, Nature.SERIOUS, Nature.MODEST, Nature.MILD, Nature.QUIET, Nature.BASHFUL, Nature.CALM, Nature.GENTLE, Nature.CAREFUL ]} ), new SpeciesFormEvolution(SpeciesId.TOXTRICITY, "", "amped", 30, null, null) ], @@ -1353,7 +1325,7 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(SpeciesId.GALAR_LINOONE, 20, null, null) ], [SpeciesId.GALAR_LINOONE]: [ - new SpeciesEvolution(SpeciesId.OBSTAGOON, 35, null, new TimeOfDayEvolutionCondition("night")) + new SpeciesEvolution(SpeciesId.OBSTAGOON, 35, null, {key: EvoCondKey.TIME, time: [TimeOfDay.DUSK, TimeOfDay.NIGHT]}) ], [SpeciesId.GALAR_YAMASK]: [ new SpeciesEvolution(SpeciesId.RUNERIGUS, 34, null, null) @@ -1362,7 +1334,7 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(SpeciesId.HISUI_ZOROARK, 30, null, null) ], [SpeciesId.HISUI_SLIGGOO]: [ - new SpeciesEvolution(SpeciesId.HISUI_GOODRA, 50, null, new WeatherEvolutionCondition([ WeatherType.RAIN, WeatherType.FOG, WeatherType.HEAVY_RAIN ]), SpeciesWildEvolutionDelay.LONG) + new SpeciesEvolution(SpeciesId.HISUI_GOODRA, 50, null, {key: EvoCondKey.WEATHER, weather: [ WeatherType.RAIN, WeatherType.FOG, WeatherType.HEAVY_RAIN ]}, SpeciesWildEvolutionDelay.LONG) ], [SpeciesId.SPRIGATITO]: [ new SpeciesEvolution(SpeciesId.FLORAGATO, 16, null, null) @@ -1383,8 +1355,8 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(SpeciesId.QUAQUAVAL, 36, null, null) ], [SpeciesId.LECHONK]: [ - new SpeciesFormEvolution(SpeciesId.OINKOLOGNE, "", "female", 18, null, new GenderEvolutionCondition(Gender.FEMALE)), - new SpeciesFormEvolution(SpeciesId.OINKOLOGNE, "", "", 18, null, new GenderEvolutionCondition(Gender.MALE)) + new SpeciesFormEvolution(SpeciesId.OINKOLOGNE, "", "female", 18, null, {key: EvoCondKey.GENDER, gender: Gender.FEMALE}), + new SpeciesFormEvolution(SpeciesId.OINKOLOGNE, "", "", 18, null, {key: EvoCondKey.GENDER, gender: Gender.MALE}) ], [SpeciesId.TAROUNTULA]: [ new SpeciesEvolution(SpeciesId.SPIDOPS, 15, null, null) @@ -1399,7 +1371,7 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(SpeciesId.PAWMOT, 32, null, null) ], [SpeciesId.TANDEMAUS]: [ - new SpeciesFormEvolution(SpeciesId.MAUSHOLD, "", "three", 25, null, new TandemausEvolutionCondition()), + new SpeciesFormEvolution(SpeciesId.MAUSHOLD, "", "three", 25, null, {key: EvoCondKey.RANDOM_FORM, value: 4}), new SpeciesFormEvolution(SpeciesId.MAUSHOLD, "", "four", 25, null, null) ], [SpeciesId.FIDOUGH]: [ @@ -1457,7 +1429,7 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(SpeciesId.GLIMMORA, 35, null, null) ], [SpeciesId.GREAVARD]: [ - new SpeciesEvolution(SpeciesId.HOUNDSTONE, 30, null, new TimeOfDayEvolutionCondition("night")) + new SpeciesEvolution(SpeciesId.HOUNDSTONE, 30, null, {key: EvoCondKey.TIME, time: [TimeOfDay.DUSK, TimeOfDay.NIGHT]}) ], [SpeciesId.FRIGIBAX]: [ new SpeciesEvolution(SpeciesId.ARCTIBAX, 35, null, null) @@ -1514,21 +1486,21 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(SpeciesId.EXEGGUTOR, 1, EvolutionItem.LEAF_STONE, null, SpeciesWildEvolutionDelay.LONG) ], [SpeciesId.TANGELA]: [ - new SpeciesEvolution(SpeciesId.TANGROWTH, 34, null, new MoveEvolutionCondition(MoveId.ANCIENT_POWER), SpeciesWildEvolutionDelay.LONG) + new SpeciesEvolution(SpeciesId.TANGROWTH, 34, null, {key: EvoCondKey.MOVE, move: MoveId.ANCIENT_POWER}, SpeciesWildEvolutionDelay.LONG) ], [SpeciesId.LICKITUNG]: [ - new SpeciesEvolution(SpeciesId.LICKILICKY, 32, null, new MoveEvolutionCondition(MoveId.ROLLOUT), SpeciesWildEvolutionDelay.LONG) + new SpeciesEvolution(SpeciesId.LICKILICKY, 32, null, {key: EvoCondKey.MOVE, move: MoveId.ROLLOUT}, SpeciesWildEvolutionDelay.LONG) ], [SpeciesId.STARYU]: [ new SpeciesEvolution(SpeciesId.STARMIE, 1, EvolutionItem.WATER_STONE, null, SpeciesWildEvolutionDelay.LONG) ], [SpeciesId.EEVEE]: [ - new SpeciesFormEvolution(SpeciesId.SYLVEON, "", "", 1, null, new FriendshipMoveTypeEvolutionCondition(120, PokemonType.FAIRY), SpeciesWildEvolutionDelay.LONG), - new SpeciesFormEvolution(SpeciesId.SYLVEON, "partner", "", 1, null, new FriendshipMoveTypeEvolutionCondition(120, PokemonType.FAIRY), SpeciesWildEvolutionDelay.LONG), - new SpeciesFormEvolution(SpeciesId.ESPEON, "", "", 1, null, new FriendshipTimeOfDayEvolutionCondition(120, "day"), SpeciesWildEvolutionDelay.LONG), - new SpeciesFormEvolution(SpeciesId.ESPEON, "partner", "", 1, null, new FriendshipTimeOfDayEvolutionCondition(120, "day"), SpeciesWildEvolutionDelay.LONG), - new SpeciesFormEvolution(SpeciesId.UMBREON, "", "", 1, null, new FriendshipTimeOfDayEvolutionCondition(120, "night"), SpeciesWildEvolutionDelay.LONG), - new SpeciesFormEvolution(SpeciesId.UMBREON, "partner", "", 1, null, new FriendshipTimeOfDayEvolutionCondition(120, "night"), SpeciesWildEvolutionDelay.LONG), + new SpeciesFormEvolution(SpeciesId.SYLVEON, "", "", 1, null, [{key: EvoCondKey.FRIENDSHIP, value: 120}, {key: EvoCondKey.MOVE_TYPE, pkmnType: PokemonType.FAIRY}], SpeciesWildEvolutionDelay.LONG), + new SpeciesFormEvolution(SpeciesId.SYLVEON, "partner", "", 1, null, [{key: EvoCondKey.FRIENDSHIP, value: 120}, {key: EvoCondKey.MOVE_TYPE, pkmnType: PokemonType.FAIRY}], SpeciesWildEvolutionDelay.LONG), + new SpeciesFormEvolution(SpeciesId.ESPEON, "", "", 1, null, [{key: EvoCondKey.FRIENDSHIP, value: 120}, {key: EvoCondKey.TIME, time: [TimeOfDay.DAWN, TimeOfDay.DAY]}], SpeciesWildEvolutionDelay.LONG), + new SpeciesFormEvolution(SpeciesId.ESPEON, "partner", "", 1, null, [{key: EvoCondKey.FRIENDSHIP, value: 120}, {key: EvoCondKey.TIME, time: [TimeOfDay.DAWN, TimeOfDay.DAY]}], SpeciesWildEvolutionDelay.LONG), + new SpeciesFormEvolution(SpeciesId.UMBREON, "", "", 1, null, [{key: EvoCondKey.FRIENDSHIP, value: 120}, {key: EvoCondKey.TIME, time: [TimeOfDay.DUSK, TimeOfDay.NIGHT]}], SpeciesWildEvolutionDelay.LONG), + new SpeciesFormEvolution(SpeciesId.UMBREON, "partner", "", 1, null, [{key: EvoCondKey.FRIENDSHIP, value: 120}, {key: EvoCondKey.TIME, time: [TimeOfDay.DUSK, TimeOfDay.NIGHT]}], SpeciesWildEvolutionDelay.LONG), new SpeciesFormEvolution(SpeciesId.VAPOREON, "", "", 1, EvolutionItem.WATER_STONE, null, SpeciesWildEvolutionDelay.LONG), new SpeciesFormEvolution(SpeciesId.VAPOREON, "partner", "", 1, EvolutionItem.WATER_STONE, null, SpeciesWildEvolutionDelay.LONG), new SpeciesFormEvolution(SpeciesId.JOLTEON, "", "", 1, EvolutionItem.THUNDER_STONE, null, SpeciesWildEvolutionDelay.LONG), @@ -1544,13 +1516,13 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(SpeciesId.TOGEKISS, 1, EvolutionItem.SHINY_STONE, null, SpeciesWildEvolutionDelay.VERY_LONG) ], [SpeciesId.AIPOM]: [ - new SpeciesEvolution(SpeciesId.AMBIPOM, 32, null, new MoveEvolutionCondition(MoveId.DOUBLE_HIT), SpeciesWildEvolutionDelay.LONG) + new SpeciesEvolution(SpeciesId.AMBIPOM, 32, null, {key: EvoCondKey.MOVE, move: MoveId.DOUBLE_HIT}, SpeciesWildEvolutionDelay.LONG) ], [SpeciesId.SUNKERN]: [ new SpeciesEvolution(SpeciesId.SUNFLORA, 1, EvolutionItem.SUN_STONE, null, SpeciesWildEvolutionDelay.LONG) ], [SpeciesId.YANMA]: [ - new SpeciesEvolution(SpeciesId.YANMEGA, 33, null, new MoveEvolutionCondition(MoveId.ANCIENT_POWER), SpeciesWildEvolutionDelay.LONG) + new SpeciesEvolution(SpeciesId.YANMEGA, 33, null, {key: EvoCondKey.MOVE, move: MoveId.ANCIENT_POWER}, SpeciesWildEvolutionDelay.LONG) ], [SpeciesId.MURKROW]: [ new SpeciesEvolution(SpeciesId.HONCHKROW, 1, EvolutionItem.DUSK_STONE, null, SpeciesWildEvolutionDelay.VERY_LONG) @@ -1559,26 +1531,26 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(SpeciesId.MISMAGIUS, 1, EvolutionItem.DUSK_STONE, null, SpeciesWildEvolutionDelay.VERY_LONG) ], [SpeciesId.GIRAFARIG]: [ - new SpeciesEvolution(SpeciesId.FARIGIRAF, 32, null, new MoveEvolutionCondition(MoveId.TWIN_BEAM), SpeciesWildEvolutionDelay.LONG) + new SpeciesEvolution(SpeciesId.FARIGIRAF, 32, null, {key: EvoCondKey.MOVE, move: MoveId.TWIN_BEAM}, SpeciesWildEvolutionDelay.LONG) ], [SpeciesId.DUNSPARCE]: [ - new SpeciesFormEvolution(SpeciesId.DUDUNSPARCE, "", "three-segment", 32, null, new DunsparceEvolutionCondition(), SpeciesWildEvolutionDelay.LONG), - new SpeciesFormEvolution(SpeciesId.DUDUNSPARCE, "", "two-segment", 32, null, new MoveEvolutionCondition(MoveId.HYPER_DRILL), SpeciesWildEvolutionDelay.LONG) + new SpeciesFormEvolution(SpeciesId.DUDUNSPARCE, "", "three-segment", 32, null, [{key: EvoCondKey.RANDOM_FORM, value: 4}, {key: EvoCondKey.MOVE, move: MoveId.HYPER_DRILL}], SpeciesWildEvolutionDelay.LONG), + new SpeciesFormEvolution(SpeciesId.DUDUNSPARCE, "", "two-segment", 32, null, {key: EvoCondKey.MOVE, move: MoveId.HYPER_DRILL}, SpeciesWildEvolutionDelay.LONG) ], [SpeciesId.GLIGAR]: [ - new SpeciesEvolution(SpeciesId.GLISCOR, 1, EvolutionItem.RAZOR_FANG, new TimeOfDayEvolutionCondition("night") /* Razor fang at night*/, SpeciesWildEvolutionDelay.VERY_LONG) + new SpeciesEvolution(SpeciesId.GLISCOR, 1, EvolutionItem.RAZOR_FANG, {key: EvoCondKey.TIME, time: [TimeOfDay.DUSK, TimeOfDay.NIGHT]} /* Razor fang at night*/, SpeciesWildEvolutionDelay.VERY_LONG) ], [SpeciesId.SNEASEL]: [ - new SpeciesEvolution(SpeciesId.WEAVILE, 1, EvolutionItem.RAZOR_CLAW, new TimeOfDayEvolutionCondition("night") /* Razor claw at night*/, SpeciesWildEvolutionDelay.VERY_LONG) + new SpeciesEvolution(SpeciesId.WEAVILE, 1, EvolutionItem.RAZOR_CLAW, {key: EvoCondKey.TIME, time: [TimeOfDay.DUSK, TimeOfDay.NIGHT]} /* Razor claw at night*/, SpeciesWildEvolutionDelay.VERY_LONG) ], [SpeciesId.URSARING]: [ new SpeciesEvolution(SpeciesId.URSALUNA, 1, EvolutionItem.PEAT_BLOCK, null, SpeciesWildEvolutionDelay.VERY_LONG) //Ursaring does not evolve into Bloodmoon Ursaluna ], [SpeciesId.PILOSWINE]: [ - new SpeciesEvolution(SpeciesId.MAMOSWINE, 1, null, new MoveEvolutionCondition(MoveId.ANCIENT_POWER), SpeciesWildEvolutionDelay.VERY_LONG) + new SpeciesEvolution(SpeciesId.MAMOSWINE, 1, null, {key: EvoCondKey.MOVE, move: MoveId.ANCIENT_POWER}, SpeciesWildEvolutionDelay.VERY_LONG) ], [SpeciesId.STANTLER]: [ - new SpeciesEvolution(SpeciesId.WYRDEER, 25, null, new MoveEvolutionCondition(MoveId.PSYSHIELD_BASH), SpeciesWildEvolutionDelay.VERY_LONG) + new SpeciesEvolution(SpeciesId.WYRDEER, 25, null, {key: EvoCondKey.MOVE, move: MoveId.PSYSHIELD_BASH}, SpeciesWildEvolutionDelay.VERY_LONG) ], [SpeciesId.LOMBRE]: [ new SpeciesEvolution(SpeciesId.LUDICOLO, 1, EvolutionItem.WATER_STONE, null, SpeciesWildEvolutionDelay.LONG) @@ -1596,11 +1568,11 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(SpeciesId.ROSERADE, 1, EvolutionItem.SHINY_STONE, null, SpeciesWildEvolutionDelay.VERY_LONG) ], [SpeciesId.BONSLY]: [ - new SpeciesEvolution(SpeciesId.SUDOWOODO, 1, null, new MoveEvolutionCondition(MoveId.MIMIC), SpeciesWildEvolutionDelay.MEDIUM) + new SpeciesEvolution(SpeciesId.SUDOWOODO, 1, null, {key: EvoCondKey.MOVE, move: MoveId.MIMIC}, SpeciesWildEvolutionDelay.MEDIUM) ], [SpeciesId.MIME_JR]: [ - new SpeciesEvolution(SpeciesId.GALAR_MR_MIME, 1, null, new MoveTimeOfDayEvolutionCondition(MoveId.MIMIC, "night"), SpeciesWildEvolutionDelay.MEDIUM), - new SpeciesEvolution(SpeciesId.MR_MIME, 1, null, new MoveTimeOfDayEvolutionCondition(MoveId.MIMIC, "day"), SpeciesWildEvolutionDelay.MEDIUM) + new SpeciesEvolution(SpeciesId.GALAR_MR_MIME, 1, null, [{key: EvoCondKey.MOVE, move: MoveId.MIMIC}, {key: EvoCondKey.TIME, time: [TimeOfDay.DUSK, TimeOfDay.NIGHT]}], SpeciesWildEvolutionDelay.MEDIUM), + new SpeciesEvolution(SpeciesId.MR_MIME, 1, null, [{key: EvoCondKey.MOVE, move: MoveId.MIMIC}, {key: EvoCondKey.TIME, time: [TimeOfDay.DAWN, TimeOfDay.DAY]}], SpeciesWildEvolutionDelay.MEDIUM) ], [SpeciesId.PANSAGE]: [ new SpeciesEvolution(SpeciesId.SIMISAGE, 1, EvolutionItem.LEAF_STONE, null, SpeciesWildEvolutionDelay.LONG) @@ -1622,8 +1594,8 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(SpeciesId.LILLIGANT, 1, EvolutionItem.SUN_STONE, null, SpeciesWildEvolutionDelay.LONG) ], [SpeciesId.BASCULIN]: [ - new SpeciesFormEvolution(SpeciesId.BASCULEGION, "white-striped", "female", 40, null, new GenderEvolutionCondition(Gender.FEMALE), SpeciesWildEvolutionDelay.VERY_LONG), - new SpeciesFormEvolution(SpeciesId.BASCULEGION, "white-striped", "male", 40, null, new GenderEvolutionCondition(Gender.MALE), SpeciesWildEvolutionDelay.VERY_LONG) + new SpeciesFormEvolution(SpeciesId.BASCULEGION, "white-striped", "female", 40, null, [{key: EvoCondKey.GENDER, gender: Gender.FEMALE}], SpeciesWildEvolutionDelay.VERY_LONG), + new SpeciesFormEvolution(SpeciesId.BASCULEGION, "white-striped", "male", 40, null, [{key: EvoCondKey.GENDER, gender: Gender.MALE}], SpeciesWildEvolutionDelay.VERY_LONG) ], [SpeciesId.MINCCINO]: [ new SpeciesEvolution(SpeciesId.CINCCINO, 1, EvolutionItem.SHINY_STONE, null, SpeciesWildEvolutionDelay.LONG) @@ -1651,14 +1623,14 @@ export const pokemonEvolutions: PokemonEvolutions = { ], [SpeciesId.ROCKRUFF]: [ new SpeciesFormEvolution(SpeciesId.LYCANROC, "own-tempo", "dusk", 25, null, null), - new SpeciesFormEvolution(SpeciesId.LYCANROC, "", "midday", 25, null, new TimeOfDayEvolutionCondition("day")), - new SpeciesFormEvolution(SpeciesId.LYCANROC, "", "midnight", 25, null, new TimeOfDayEvolutionCondition("night")) + new SpeciesFormEvolution(SpeciesId.LYCANROC, "", "midday", 25, null, {key: EvoCondKey.TIME, time: [TimeOfDay.DAWN, TimeOfDay.DAY]}), + new SpeciesFormEvolution(SpeciesId.LYCANROC, "", "midnight", 25, null, {key: EvoCondKey.TIME, time: [TimeOfDay.DUSK, TimeOfDay.NIGHT]}) ], [SpeciesId.STEENEE]: [ - new SpeciesEvolution(SpeciesId.TSAREENA, 28, null, new MoveEvolutionCondition(MoveId.STOMP), SpeciesWildEvolutionDelay.LONG) + new SpeciesEvolution(SpeciesId.TSAREENA, 28, null, {key: EvoCondKey.MOVE, move: MoveId.STOMP}, SpeciesWildEvolutionDelay.LONG) ], [SpeciesId.POIPOLE]: [ - new SpeciesEvolution(SpeciesId.NAGANADEL, 1, null, new MoveEvolutionCondition(MoveId.DRAGON_PULSE), SpeciesWildEvolutionDelay.LONG) + new SpeciesEvolution(SpeciesId.NAGANADEL, 1, null, {key: EvoCondKey.MOVE, move: MoveId.DRAGON_PULSE}, SpeciesWildEvolutionDelay.LONG) ], [SpeciesId.ALOLA_SANDSHREW]: [ new SpeciesEvolution(SpeciesId.ALOLA_SANDSLASH, 1, EvolutionItem.ICE_STONE, null, SpeciesWildEvolutionDelay.LONG) @@ -1672,7 +1644,7 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(SpeciesId.APPLETUN, 1, EvolutionItem.SWEET_APPLE, null, SpeciesWildEvolutionDelay.LONG) ], [SpeciesId.CLOBBOPUS]: [ - new SpeciesEvolution(SpeciesId.GRAPPLOCT, 35, null, new MoveEvolutionCondition(MoveId.TAUNT)/*Once Taunt is implemented, change evo level to 1 and delay to LONG*/) + new SpeciesEvolution(SpeciesId.GRAPPLOCT, 35, null, {key: EvoCondKey.MOVE, move: MoveId.TAUNT}/*Once Taunt is implemented, change evo level to 1 and delay to LONG*/) ], [SpeciesId.SINISTEA]: [ new SpeciesFormEvolution(SpeciesId.POLTEAGEIST, "phony", "phony", 1, EvolutionItem.CRACKED_POT, null, SpeciesWildEvolutionDelay.LONG), @@ -1680,31 +1652,31 @@ export const pokemonEvolutions: PokemonEvolutions = { ], [SpeciesId.MILCERY]: [ new SpeciesFormEvolution(SpeciesId.ALCREMIE, "", "vanilla-cream", 1, EvolutionItem.STRAWBERRY_SWEET, - new BiomeEvolutionCondition([ BiomeId.TOWN, BiomeId.PLAINS, BiomeId.GRASS, BiomeId.TALL_GRASS, BiomeId.METROPOLIS ]), + {key: EvoCondKey.BIOME, biome: [ BiomeId.TOWN, BiomeId.PLAINS, BiomeId.GRASS, BiomeId.TALL_GRASS, BiomeId.METROPOLIS ]}, SpeciesWildEvolutionDelay.LONG), new SpeciesFormEvolution(SpeciesId.ALCREMIE, "", "ruby-cream", 1, EvolutionItem.STRAWBERRY_SWEET, - new BiomeEvolutionCondition([ BiomeId.BADLANDS, BiomeId.VOLCANO, BiomeId.GRAVEYARD, BiomeId.FACTORY, BiomeId.SLUM ]), + {key: EvoCondKey.BIOME, biome: [ BiomeId.BADLANDS, BiomeId.VOLCANO, BiomeId.GRAVEYARD, BiomeId.FACTORY, BiomeId.SLUM ]}, SpeciesWildEvolutionDelay.LONG), new SpeciesFormEvolution(SpeciesId.ALCREMIE, "", "matcha-cream", 1, EvolutionItem.STRAWBERRY_SWEET, - new BiomeEvolutionCondition([ BiomeId.FOREST, BiomeId.SWAMP, BiomeId.MEADOW, BiomeId.JUNGLE ]), + {key: EvoCondKey.BIOME, biome: [ BiomeId.FOREST, BiomeId.SWAMP, BiomeId.MEADOW, BiomeId.JUNGLE ]}, SpeciesWildEvolutionDelay.LONG), new SpeciesFormEvolution(SpeciesId.ALCREMIE, "", "mint-cream", 1, EvolutionItem.STRAWBERRY_SWEET, - new BiomeEvolutionCondition([ BiomeId.SEA, BiomeId.BEACH, BiomeId.LAKE, BiomeId.SEABED ]), + {key: EvoCondKey.BIOME, biome: [ BiomeId.SEA, BiomeId.BEACH, BiomeId.LAKE, BiomeId.SEABED ]}, SpeciesWildEvolutionDelay.LONG), new SpeciesFormEvolution(SpeciesId.ALCREMIE, "", "lemon-cream", 1, EvolutionItem.STRAWBERRY_SWEET, - new BiomeEvolutionCondition([ BiomeId.DESERT, BiomeId.POWER_PLANT, BiomeId.DOJO, BiomeId.RUINS, BiomeId.CONSTRUCTION_SITE ]), + {key: EvoCondKey.BIOME, biome: [ BiomeId.DESERT, BiomeId.POWER_PLANT, BiomeId.DOJO, BiomeId.RUINS, BiomeId.CONSTRUCTION_SITE ]}, SpeciesWildEvolutionDelay.LONG), new SpeciesFormEvolution(SpeciesId.ALCREMIE, "", "salted-cream", 1, EvolutionItem.STRAWBERRY_SWEET, - new BiomeEvolutionCondition([ BiomeId.MOUNTAIN, BiomeId.CAVE, BiomeId.ICE_CAVE, BiomeId.FAIRY_CAVE, BiomeId.SNOWY_FOREST ]), + {key: EvoCondKey.BIOME, biome: [ BiomeId.MOUNTAIN, BiomeId.CAVE, BiomeId.ICE_CAVE, BiomeId.FAIRY_CAVE, BiomeId.SNOWY_FOREST ]}, SpeciesWildEvolutionDelay.LONG), new SpeciesFormEvolution(SpeciesId.ALCREMIE, "", "ruby-swirl", 1, EvolutionItem.STRAWBERRY_SWEET, - new BiomeEvolutionCondition([ BiomeId.WASTELAND, BiomeId.LABORATORY ]), + {key: EvoCondKey.BIOME, biome: [ BiomeId.WASTELAND, BiomeId.LABORATORY ]}, SpeciesWildEvolutionDelay.LONG), new SpeciesFormEvolution(SpeciesId.ALCREMIE, "", "caramel-swirl", 1, EvolutionItem.STRAWBERRY_SWEET, - new BiomeEvolutionCondition([ BiomeId.TEMPLE, BiomeId.ISLAND ]), + {key: EvoCondKey.BIOME, biome: [ BiomeId.TEMPLE, BiomeId.ISLAND ]}, SpeciesWildEvolutionDelay.LONG), new SpeciesFormEvolution(SpeciesId.ALCREMIE, "", "rainbow-swirl", 1, EvolutionItem.STRAWBERRY_SWEET, - new BiomeEvolutionCondition([ BiomeId.ABYSS, BiomeId.SPACE, BiomeId.END ]), + {key: EvoCondKey.BIOME, biome: [ BiomeId.ABYSS, BiomeId.SPACE, BiomeId.END ]}, SpeciesWildEvolutionDelay.LONG) ], [SpeciesId.DURALUDON]: [ @@ -1724,10 +1696,10 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(SpeciesId.HISUI_ELECTRODE, 1, EvolutionItem.LEAF_STONE, null, SpeciesWildEvolutionDelay.LONG) ], [SpeciesId.HISUI_QWILFISH]: [ - new SpeciesEvolution(SpeciesId.OVERQWIL, 28, null, new MoveEvolutionCondition(MoveId.BARB_BARRAGE), SpeciesWildEvolutionDelay.LONG) + new SpeciesEvolution(SpeciesId.OVERQWIL, 28, null, {key: EvoCondKey.MOVE, move: MoveId.BARB_BARRAGE}, SpeciesWildEvolutionDelay.LONG) ], [SpeciesId.HISUI_SNEASEL]: [ - new SpeciesEvolution(SpeciesId.SNEASLER, 1, EvolutionItem.RAZOR_CLAW, new TimeOfDayEvolutionCondition("day") /* Razor claw at day*/, SpeciesWildEvolutionDelay.VERY_LONG) + new SpeciesEvolution(SpeciesId.SNEASLER, 1, EvolutionItem.RAZOR_CLAW, {key: EvoCondKey.TIME, time: [TimeOfDay.DAWN, TimeOfDay.DAY]} /* Razor claw at day*/, SpeciesWildEvolutionDelay.VERY_LONG) ], [SpeciesId.CHARCADET]: [ new SpeciesEvolution(SpeciesId.ARMAROUGE, 1, EvolutionItem.AUSPICIOUS_ARMOR, null, SpeciesWildEvolutionDelay.LONG), @@ -1747,7 +1719,7 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesFormEvolution(SpeciesId.SINISTCHA, "artisan", "masterpiece", 1, EvolutionItem.MASTERPIECE_TEACUP, null, SpeciesWildEvolutionDelay.LONG) ], [SpeciesId.DIPPLIN]: [ - new SpeciesEvolution(SpeciesId.HYDRAPPLE, 1, null, new MoveEvolutionCondition(MoveId.DRAGON_CHEER), SpeciesWildEvolutionDelay.VERY_LONG) + new SpeciesEvolution(SpeciesId.HYDRAPPLE, 1, null, {key: EvoCondKey.MOVE, move: MoveId.DRAGON_CHEER}, SpeciesWildEvolutionDelay.VERY_LONG) ], [SpeciesId.KADABRA]: [ new SpeciesEvolution(SpeciesId.ALAKAZAM, 1, EvolutionItem.LINKING_CORD, null, SpeciesWildEvolutionDelay.VERY_LONG) @@ -1762,7 +1734,7 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(SpeciesId.GENGAR, 1, EvolutionItem.LINKING_CORD, null, SpeciesWildEvolutionDelay.VERY_LONG) ], [SpeciesId.ONIX]: [ - new SpeciesEvolution(SpeciesId.STEELIX, 1, EvolutionItem.LINKING_CORD, new MoveTypeEvolutionCondition(PokemonType.STEEL), SpeciesWildEvolutionDelay.VERY_LONG) + new SpeciesEvolution(SpeciesId.STEELIX, 1, EvolutionItem.LINKING_CORD, {key: EvoCondKey.MOVE_TYPE, pkmnType: PokemonType.STEEL}, SpeciesWildEvolutionDelay.VERY_LONG) ], [SpeciesId.RHYDON]: [ new SpeciesEvolution(SpeciesId.RHYPERIOR, 1, EvolutionItem.PROTECTOR, null, SpeciesWildEvolutionDelay.VERY_LONG) @@ -1771,7 +1743,7 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(SpeciesId.KINGDRA, 1, EvolutionItem.DRAGON_SCALE, null, SpeciesWildEvolutionDelay.VERY_LONG) ], [SpeciesId.SCYTHER]: [ - new SpeciesEvolution(SpeciesId.SCIZOR, 1, EvolutionItem.LINKING_CORD, new MoveTypeEvolutionCondition(PokemonType.STEEL), SpeciesWildEvolutionDelay.VERY_LONG), + new SpeciesEvolution(SpeciesId.SCIZOR, 1, EvolutionItem.LINKING_CORD, {key: EvoCondKey.MOVE_TYPE, pkmnType: PokemonType.STEEL}, SpeciesWildEvolutionDelay.VERY_LONG), new SpeciesEvolution(SpeciesId.KLEAVOR, 1, EvolutionItem.BLACK_AUGURITE, null, SpeciesWildEvolutionDelay.VERY_LONG) ], [SpeciesId.ELECTABUZZ]: [ @@ -1793,9 +1765,8 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(SpeciesId.DUSKNOIR, 1, EvolutionItem.REAPER_CLOTH, null, SpeciesWildEvolutionDelay.VERY_LONG) ], [SpeciesId.CLAMPERL]: [ - // TODO: Change the SpeciesEvolutionConditions here to use a bespoke HeldItemEvolutionCondition after the modifier rework - new SpeciesEvolution(SpeciesId.HUNTAIL, 1, EvolutionItem.LINKING_CORD, new SpeciesEvolutionCondition(p => p.getHeldItems().some(m => m instanceof SpeciesStatBoosterModifier && (m.type as SpeciesStatBoosterModifierType).key === "DEEP_SEA_TOOTH")), SpeciesWildEvolutionDelay.VERY_LONG), - new SpeciesEvolution(SpeciesId.GOREBYSS, 1, EvolutionItem.LINKING_CORD, new SpeciesEvolutionCondition(p => p.getHeldItems().some(m => m instanceof SpeciesStatBoosterModifier && (m.type as SpeciesStatBoosterModifierType).key === "DEEP_SEA_SCALE")), SpeciesWildEvolutionDelay.VERY_LONG) + new SpeciesEvolution(SpeciesId.HUNTAIL, 1, EvolutionItem.LINKING_CORD, {key: EvoCondKey.HELD_ITEM, itemKey: "DEEP_SEA_TOOTH"}, SpeciesWildEvolutionDelay.VERY_LONG), + new SpeciesEvolution(SpeciesId.GOREBYSS, 1, EvolutionItem.LINKING_CORD, {key: EvoCondKey.HELD_ITEM, itemKey: "DEEP_SEA_SCALE"}, SpeciesWildEvolutionDelay.VERY_LONG) ], [SpeciesId.BOLDORE]: [ new SpeciesEvolution(SpeciesId.GIGALITH, 1, EvolutionItem.LINKING_CORD, null, SpeciesWildEvolutionDelay.VERY_LONG) @@ -1804,10 +1775,10 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(SpeciesId.CONKELDURR, 1, EvolutionItem.LINKING_CORD, null, SpeciesWildEvolutionDelay.VERY_LONG) ], [SpeciesId.KARRABLAST]: [ - new SpeciesEvolution(SpeciesId.ESCAVALIER, 1, EvolutionItem.LINKING_CORD, new CaughtEvolutionCondition(SpeciesId.SHELMET), SpeciesWildEvolutionDelay.VERY_LONG) + new SpeciesEvolution(SpeciesId.ESCAVALIER, 1, EvolutionItem.LINKING_CORD, {key: EvoCondKey.SPECIES_CAUGHT, speciesCaught: SpeciesId.SHELMET}, SpeciesWildEvolutionDelay.VERY_LONG) ], [SpeciesId.SHELMET]: [ - new SpeciesEvolution(SpeciesId.ACCELGOR, 1, EvolutionItem.LINKING_CORD, new CaughtEvolutionCondition(SpeciesId.KARRABLAST), SpeciesWildEvolutionDelay.VERY_LONG) + new SpeciesEvolution(SpeciesId.ACCELGOR, 1, EvolutionItem.LINKING_CORD, {key: EvoCondKey.SPECIES_CAUGHT, speciesCaught: SpeciesId.KARRABLAST}, SpeciesWildEvolutionDelay.VERY_LONG) ], [SpeciesId.SPRITZEE]: [ new SpeciesEvolution(SpeciesId.AROMATISSE, 1, EvolutionItem.SACHET, null, SpeciesWildEvolutionDelay.VERY_LONG) @@ -1825,66 +1796,66 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(SpeciesId.ALOLA_GOLEM, 1, EvolutionItem.LINKING_CORD, null, SpeciesWildEvolutionDelay.VERY_LONG) ], [SpeciesId.PRIMEAPE]: [ - new SpeciesEvolution(SpeciesId.ANNIHILAPE, 35, null, new MoveEvolutionCondition(MoveId.RAGE_FIST), SpeciesWildEvolutionDelay.VERY_LONG) + new SpeciesEvolution(SpeciesId.ANNIHILAPE, 35, null, {key: EvoCondKey.MOVE, move: MoveId.RAGE_FIST}, SpeciesWildEvolutionDelay.VERY_LONG) ], [SpeciesId.GOLBAT]: [ - new SpeciesEvolution(SpeciesId.CROBAT, 1, null, new FriendshipEvolutionCondition(120), SpeciesWildEvolutionDelay.VERY_LONG) + new SpeciesEvolution(SpeciesId.CROBAT, 1, null, {key: EvoCondKey.FRIENDSHIP, value: 120}, SpeciesWildEvolutionDelay.VERY_LONG) ], [SpeciesId.CHANSEY]: [ - new SpeciesEvolution(SpeciesId.BLISSEY, 1, null, new FriendshipEvolutionCondition(200), SpeciesWildEvolutionDelay.LONG) + new SpeciesEvolution(SpeciesId.BLISSEY, 1, null, {key: EvoCondKey.FRIENDSHIP, value: 200}, SpeciesWildEvolutionDelay.LONG) ], [SpeciesId.PICHU]: [ - new SpeciesFormEvolution(SpeciesId.PIKACHU, "spiky", "partner", 1, null, new FriendshipEvolutionCondition(90), SpeciesWildEvolutionDelay.SHORT), - new SpeciesFormEvolution(SpeciesId.PIKACHU, "", "", 1, null, new FriendshipEvolutionCondition(90), SpeciesWildEvolutionDelay.SHORT), + new SpeciesFormEvolution(SpeciesId.PIKACHU, "spiky", "partner", 1, null, {key: EvoCondKey.FRIENDSHIP, value: 90}, SpeciesWildEvolutionDelay.SHORT), + new SpeciesFormEvolution(SpeciesId.PIKACHU, "", "", 1, null, {key: EvoCondKey.FRIENDSHIP, value: 90}, SpeciesWildEvolutionDelay.SHORT), ], [SpeciesId.CLEFFA]: [ - new SpeciesEvolution(SpeciesId.CLEFAIRY, 1, null, new FriendshipEvolutionCondition(160), SpeciesWildEvolutionDelay.SHORT) + new SpeciesEvolution(SpeciesId.CLEFAIRY, 1, null, {key: EvoCondKey.FRIENDSHIP, value: 160}, SpeciesWildEvolutionDelay.SHORT) ], [SpeciesId.IGGLYBUFF]: [ - new SpeciesEvolution(SpeciesId.JIGGLYPUFF, 1, null, new FriendshipEvolutionCondition(70), SpeciesWildEvolutionDelay.SHORT) + new SpeciesEvolution(SpeciesId.JIGGLYPUFF, 1, null, {key: EvoCondKey.FRIENDSHIP, value: 70}, SpeciesWildEvolutionDelay.SHORT) ], [SpeciesId.TOGEPI]: [ - new SpeciesEvolution(SpeciesId.TOGETIC, 1, null, new FriendshipEvolutionCondition(70), SpeciesWildEvolutionDelay.SHORT) + new SpeciesEvolution(SpeciesId.TOGETIC, 1, null, {key: EvoCondKey.FRIENDSHIP, value: 70}, SpeciesWildEvolutionDelay.SHORT) ], [SpeciesId.AZURILL]: [ - new SpeciesEvolution(SpeciesId.MARILL, 1, null, new FriendshipEvolutionCondition(70), SpeciesWildEvolutionDelay.SHORT) + new SpeciesEvolution(SpeciesId.MARILL, 1, null, {key: EvoCondKey.FRIENDSHIP, value: 70}, SpeciesWildEvolutionDelay.SHORT) ], [SpeciesId.BUDEW]: [ - new SpeciesEvolution(SpeciesId.ROSELIA, 1, null, new FriendshipTimeOfDayEvolutionCondition(70, "day"), SpeciesWildEvolutionDelay.SHORT) + new SpeciesEvolution(SpeciesId.ROSELIA, 1, null, [{key: EvoCondKey.FRIENDSHIP, value: 70}, {key: EvoCondKey.TIME, time: [TimeOfDay.DAWN, TimeOfDay.DAY]}], SpeciesWildEvolutionDelay.SHORT) ], [SpeciesId.BUNEARY]: [ - new SpeciesEvolution(SpeciesId.LOPUNNY, 1, null, new FriendshipEvolutionCondition(70), SpeciesWildEvolutionDelay.MEDIUM) + new SpeciesEvolution(SpeciesId.LOPUNNY, 1, null, {key: EvoCondKey.FRIENDSHIP, value: 70}, SpeciesWildEvolutionDelay.MEDIUM) ], [SpeciesId.CHINGLING]: [ - new SpeciesEvolution(SpeciesId.CHIMECHO, 1, null, new FriendshipTimeOfDayEvolutionCondition(90, "night"), SpeciesWildEvolutionDelay.MEDIUM) + new SpeciesEvolution(SpeciesId.CHIMECHO, 1, null, [{key: EvoCondKey.FRIENDSHIP, value: 90}, {key: EvoCondKey.TIME, time: [TimeOfDay.DUSK, TimeOfDay.NIGHT]}], SpeciesWildEvolutionDelay.MEDIUM) ], [SpeciesId.HAPPINY]: [ - new SpeciesEvolution(SpeciesId.CHANSEY, 1, null, new FriendshipEvolutionCondition(160), SpeciesWildEvolutionDelay.SHORT) + new SpeciesEvolution(SpeciesId.CHANSEY, 1, null, {key: EvoCondKey.FRIENDSHIP, value: 160}, SpeciesWildEvolutionDelay.SHORT) ], [SpeciesId.MUNCHLAX]: [ - new SpeciesEvolution(SpeciesId.SNORLAX, 1, null, new FriendshipEvolutionCondition(120), SpeciesWildEvolutionDelay.LONG) + new SpeciesEvolution(SpeciesId.SNORLAX, 1, null, {key: EvoCondKey.FRIENDSHIP, value: 120}, SpeciesWildEvolutionDelay.LONG) ], [SpeciesId.RIOLU]: [ - new SpeciesEvolution(SpeciesId.LUCARIO, 1, null, new FriendshipTimeOfDayEvolutionCondition(120, "day"), SpeciesWildEvolutionDelay.LONG) + new SpeciesEvolution(SpeciesId.LUCARIO, 1, null, [{key: EvoCondKey.FRIENDSHIP, value: 120}, {key: EvoCondKey.TIME, time: [TimeOfDay.DAWN, TimeOfDay.DAY]}], SpeciesWildEvolutionDelay.LONG) ], [SpeciesId.WOOBAT]: [ - new SpeciesEvolution(SpeciesId.SWOOBAT, 1, null, new FriendshipEvolutionCondition(90), SpeciesWildEvolutionDelay.MEDIUM) + new SpeciesEvolution(SpeciesId.SWOOBAT, 1, null, {key: EvoCondKey.FRIENDSHIP, value: 90}, SpeciesWildEvolutionDelay.MEDIUM) ], [SpeciesId.SWADLOON]: [ - new SpeciesEvolution(SpeciesId.LEAVANNY, 1, null, new FriendshipEvolutionCondition(120), SpeciesWildEvolutionDelay.LONG) + new SpeciesEvolution(SpeciesId.LEAVANNY, 1, null, {key: EvoCondKey.FRIENDSHIP, value: 120}, SpeciesWildEvolutionDelay.LONG) ], [SpeciesId.TYPE_NULL]: [ - new SpeciesEvolution(SpeciesId.SILVALLY, 1, null, new FriendshipEvolutionCondition(100), SpeciesWildEvolutionDelay.LONG) + new SpeciesEvolution(SpeciesId.SILVALLY, 1, null, {key: EvoCondKey.FRIENDSHIP, value: 100}, SpeciesWildEvolutionDelay.LONG) ], [SpeciesId.ALOLA_MEOWTH]: [ - new SpeciesEvolution(SpeciesId.ALOLA_PERSIAN, 1, null, new FriendshipEvolutionCondition(120), SpeciesWildEvolutionDelay.LONG) + new SpeciesEvolution(SpeciesId.ALOLA_PERSIAN, 1, null, {key: EvoCondKey.FRIENDSHIP, value: 120}, SpeciesWildEvolutionDelay.LONG) ], [SpeciesId.SNOM]: [ - new SpeciesEvolution(SpeciesId.FROSMOTH, 1, null, new FriendshipTimeOfDayEvolutionCondition(90, "night"), SpeciesWildEvolutionDelay.MEDIUM) + new SpeciesEvolution(SpeciesId.FROSMOTH, 1, null, [{key: EvoCondKey.FRIENDSHIP, value: 90}, {key: EvoCondKey.TIME, time: [TimeOfDay.DUSK, TimeOfDay.NIGHT]}], SpeciesWildEvolutionDelay.MEDIUM) ], [SpeciesId.GIMMIGHOUL]: [ - new SpeciesFormEvolution(SpeciesId.GHOLDENGO, "chest", "", 1, null, new TreasureEvolutionCondition(), SpeciesWildEvolutionDelay.VERY_LONG), - new SpeciesFormEvolution(SpeciesId.GHOLDENGO, "roaming", "", 1, null, new TreasureEvolutionCondition(), SpeciesWildEvolutionDelay.VERY_LONG) + new SpeciesFormEvolution(SpeciesId.GHOLDENGO, "chest", "", 1, null, {key: EvoCondKey.EVO_TREASURE_TRACKER, value: 10}, SpeciesWildEvolutionDelay.VERY_LONG), + new SpeciesFormEvolution(SpeciesId.GHOLDENGO, "roaming", "", 1, null, {key: EvoCondKey.EVO_TREASURE_TRACKER, value: 10}, SpeciesWildEvolutionDelay.VERY_LONG) ] }; diff --git a/src/data/balance/tms.ts b/src/data/balance/tms.ts index 2b0e8f5142d..e194dc4040c 100644 --- a/src/data/balance/tms.ts +++ b/src/data/balance/tms.ts @@ -1,4 +1,4 @@ -import { ModifierTier } from "#app/modifier/modifier-tier"; +import { ModifierTier } from "#enums/modifier-tier"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; diff --git a/src/data/battle-anims.ts b/src/data/battle-anims.ts index 0b40469b255..be060b57e9c 100644 --- a/src/data/battle-anims.ts +++ b/src/data/battle-anims.ts @@ -1,15 +1,22 @@ import { globalScene } from "#app/global-scene"; -import { allMoves } from "./data-lists"; +import { allMoves } from "#app/data/data-lists"; import { MoveFlags } from "#enums/MoveFlags"; -import type Pokemon from "../field/pokemon"; -import { type nil, getFrameMs, getEnumKeys, getEnumValues, animationFileName } from "../utils/common"; +import type Pokemon from "#app/field/pokemon"; +import { + type nil, + getFrameMs, + getEnumKeys, + getEnumValues, + animationFileName, + coerceArray, + isNullOrUndefined, +} from "#app/utils/common"; import type { BattlerIndex } from "#enums/battler-index"; import { MoveId } from "#enums/move-id"; -import { SubstituteTag } from "./battler-tags"; -import { isNullOrUndefined } from "../utils/common"; import Phaser from "phaser"; import { EncounterAnim } from "#enums/encounter-anims"; import { AnimBlendType, AnimFrameTarget, AnimFocus, ChargeAnim, CommonAnim } from "#enums/move-anims-common"; +import { BattlerTagType } from "#enums/battler-tag-type"; export class AnimConfig { public id: number; @@ -519,7 +526,7 @@ function logMissingMoveAnim(move: MoveId, ...optionalParams: any[]) { * @param encounterAnim one or more animations to fetch */ export async function initEncounterAnims(encounterAnim: EncounterAnim | EncounterAnim[]): Promise { - const anims = Array.isArray(encounterAnim) ? encounterAnim : [encounterAnim]; + const anims = coerceArray(encounterAnim); const encounterAnimNames = getEnumKeys(EncounterAnim); const encounterAnimFetches: Promise>[] = []; for (const anim of anims) { @@ -770,7 +777,7 @@ export abstract class BattleAnim { const user = !isOppAnim ? this.user : this.target; const target = !isOppAnim ? this.target : this.user; - const targetSubstitute = onSubstitute && user !== target ? target!.getTag(SubstituteTag) : null; + const targetSubstitute = onSubstitute && user !== target ? target!.getTag(BattlerTagType.SUBSTITUTE) : null; const userInitialX = user!.x; // TODO: is this bang correct? const userInitialY = user!.y; // TODO: is this bang correct? @@ -844,7 +851,7 @@ export abstract class BattleAnim { 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 targetSprite = targetSubstitute?.sprite ?? target.getSprite(); diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index d90964492e8..bde7bfcafdb 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -1,13 +1,6 @@ import { globalScene } from "#app/global-scene"; import Overrides from "#app/overrides"; -import { - applyAbAttrs, - BlockNonDirectDamageAbAttr, - FlinchEffectAbAttr, - ProtectStatAbAttr, - ConditionalUserFieldProtectStatAbAttr, - ReverseDrainAbAttr, -} from "#app/data/abilities/ability"; +import { applyAbAttrs } from "./abilities/apply-ab-attrs"; import { allAbilities } from "./data-lists"; import { CommonBattleAnim, MoveChargeAnim } from "#app/data/battle-anims"; import { ChargeAnim, CommonAnim } from "#enums/move-anims-common"; @@ -28,7 +21,7 @@ import type { MoveEffectPhase } from "#app/phases/move-effect-phase"; import type { MovePhase } from "#app/phases/move-phase"; import type { StatStageChangeCallback } from "#app/phases/stat-stage-change-phase"; import i18next from "#app/plugins/i18n"; -import { BooleanHolder, getFrameMs, NumberHolder, toDmgValue } from "#app/utils/common"; +import { BooleanHolder, coerceArray, getFrameMs, NumberHolder, toDmgValue } from "#app/utils/common"; import { AbilityId } from "#enums/ability-id"; import { BattlerTagType } from "#enums/battler-tag-type"; import { MoveId } from "#enums/move-id"; @@ -38,8 +31,15 @@ import { EFFECTIVE_STATS, getStatKey, Stat, type BattleStat, type EffectiveStat import { StatusEffect } from "#enums/status-effect"; import { WeatherType } from "#enums/weather-type"; 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"; +/** + * 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 { public tagType: BattlerTagType; public lapseTypes: BattlerTagLapseType[]; @@ -57,7 +57,7 @@ export class BattlerTag { isBatonPassable = false, ) { this.tagType = tagType; - this.lapseTypes = Array.isArray(lapseType) ? lapseType : [lapseType]; + this.lapseTypes = coerceArray(lapseType); this.turnCount = turnCount; this.sourceMove = sourceMove!; // TODO: is this bang correct? this.sourceId = sourceId; @@ -76,7 +76,7 @@ export class BattlerTag { /** * 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 { // TODO: Maybe flip this (return `true` if tag needs removal) @@ -132,16 +132,6 @@ export interface TerrainBattlerTag { * Players and enemies should not be allowed to select restricted moves. */ export abstract class MoveRestrictionBattlerTag extends BattlerTag { - constructor( - tagType: BattlerTagType, - lapseType: BattlerTagLapseType | BattlerTagLapseType[], - turnCount: number, - sourceMove?: MoveId, - sourceId?: number, - ) { - super(tagType, lapseType, turnCount, sourceMove, sourceId); - } - /** @override */ override lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { if (lapseType === BattlerTagLapseType.PRE_MOVE) { @@ -284,17 +274,18 @@ export class DisabledTag extends MoveRestrictionBattlerTag { /** * @override * - * Ensures that move history exists on `pokemon` and has a valid move. If so, sets the {@linkcode moveId} and shows a message. - * Otherwise the move ID will not get assigned and this tag will get removed next turn. + * Attempt to disable the target's last move by setting this tag's {@linkcode moveId} + * and showing a message. */ override onAdd(pokemon: Pokemon): void { - super.onAdd(pokemon); - - const move = pokemon.getLastXMoves(-1).find(m => !m.virtual); - if (isNullOrUndefined(move) || move.move === MoveId.STRUGGLE || move.move === MoveId.NONE) { + // 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.getLastNonVirtualMove(); + if (isNullOrUndefined(move) || move.move === MoveId.STRUGGLE) { return; } + super.onAdd(pokemon); this.moveId = move.move; globalScene.phaseManager.queueMessage( @@ -344,7 +335,6 @@ export class DisabledTag extends MoveRestrictionBattlerTag { /** * Tag used by Gorilla Tactics to restrict the user to using only one move. - * @extends MoveRestrictionBattlerTag */ export class GorillaTacticsTag extends MoveRestrictionBattlerTag { private moveId = MoveId.NONE; @@ -353,34 +343,30 @@ export class GorillaTacticsTag extends MoveRestrictionBattlerTag { super(BattlerTagType.GORILLA_TACTICS, BattlerTagLapseType.CUSTOM, 0); } - /** @override */ override isMoveRestricted(move: MoveId): boolean { return move !== this.moveId; } /** - * @override - * @param {Pokemon} pokemon the {@linkcode Pokemon} to check if the tag can be added - * @returns `true` if the pokemon has a valid move and no existing {@linkcode GorillaTacticsTag}; `false` otherwise + * Ensures that move history exists on {@linkcode Pokemon} and has a valid move to lock into. + * @param pokemon - The {@linkcode Pokemon} to add the tag to + * @returns `true` if the tag can be added */ 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. - * If so, sets the {@linkcode moveId} and increases the user's Attack by 50%. - * @override - * @param {Pokemon} pokemon the {@linkcode Pokemon} to add the tag to + * Sets this tag's {@linkcode moveId} and increases the user's Attack by 50%. + * @param pokemon - The {@linkcode Pokemon} to add the tag to */ override onAdd(pokemon: Pokemon): void { - const lastValidMove = this.getLastValidMove(pokemon); + super.onAdd(pokemon); - if (!lastValidMove) { - return; - } - - this.moveId = lastValidMove; + // Bang is justified as tag is not added if prior move doesn't exist + this.moveId = pokemon.getLastNonVirtualMove()!.move; pokemon.setStat(Stat.ATK, pokemon.getStat(Stat.ATK, false) * 1.5, false); } @@ -395,29 +381,16 @@ export class GorillaTacticsTag extends MoveRestrictionBattlerTag { } /** - * - * @override - * @param {Pokemon} pokemon n/a - * @param {MoveId} _move {@linkcode MoveId} ID of the move being denied - * @returns {string} text to display when the move is denied + * Return the text displayed when a move is restricted. + * @param pokemon - The {@linkcode Pokemon} with this tag. + * @returns A string containing the 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", { moveName: allMoves[this.moveId].name, 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; - } } /** @@ -431,8 +404,8 @@ export class RechargingTag extends BattlerTag { onAdd(pokemon: Pokemon): void { super.onAdd(pokemon); - // Queue a placeholder move for the Pokemon to "use" next turn - pokemon.getMoveQueue().push({ move: MoveId.NONE, targets: [] }); + // Queue a placeholder move for the Pokemon to "use" next turn. + pokemon.pushMoveQueue({ move: MoveId.NONE, targets: [], useMode: MoveUseMode.NORMAL }); } /** Cancels the source's move this turn and queues a "__ must recharge!" message */ @@ -648,7 +621,7 @@ export class FlinchedTag extends BattlerTag { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), }), ); - applyAbAttrs(FlinchEffectAbAttr, pokemon, null); + applyAbAttrs("FlinchEffectAbAttr", pokemon, null); return true; } @@ -681,6 +654,7 @@ export class InterruptedTag extends BattlerTag { move: MoveId.NONE, result: MoveResult.OTHER, targets: [], + useMode: MoveUseMode.NORMAL, }); } @@ -946,7 +920,7 @@ export class SeedTag extends BattlerTag { const source = pokemon.getOpponents().find(o => o.getBattlerIndex() === this.sourceIndex); if (source) { const cancelled = new BooleanHolder(false); - applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled); + applyAbAttrs("BlockNonDirectDamageAbAttr", pokemon, cancelled); if (!cancelled.value) { globalScene.phaseManager.unshiftNew( @@ -957,7 +931,7 @@ export class SeedTag extends BattlerTag { ); const damage = pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 8), { result: HitResult.INDIRECT }); - const reverseDrain = pokemon.hasAbilityWithAttr(ReverseDrainAbAttr, false); + const reverseDrain = pokemon.hasAbilityWithAttr("ReverseDrainAbAttr", false); globalScene.phaseManager.unshiftNew( "PokemonHealPhase", source.getBattlerIndex(), @@ -1006,42 +980,45 @@ export class PowderTag extends BattlerTag { } /** - * Applies Powder's effects before the tag owner uses a Fire-type move. - * Also causes the tag to expire at the end of turn. - * @param pokemon {@linkcode Pokemon} the owner of this tag - * @param lapseType {@linkcode BattlerTagLapseType} the type of lapse functionality to carry out - * @returns `true` if the tag should not expire after this lapse; `false` otherwise. + * Applies Powder's effects before the tag owner uses a Fire-type move, damaging and canceling its action. + * Lasts until the end of the turn. + * @param pokemon - The {@linkcode Pokemon} with this tag. + * @param lapseType - The {@linkcode BattlerTagLapseType} dictating how this tag is being activated + * @returns `true` if the tag should remain active. */ lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { - if (lapseType === BattlerTagLapseType.PRE_MOVE) { - const movePhase = globalScene.phaseManager.getCurrentPhase(); - if (movePhase?.is("MovePhase")) { - const move = movePhase.move.getMove(); - const weather = globalScene.arena.weather; - if ( - pokemon.getMoveType(move) === PokemonType.FIRE && - !(weather && weather.weatherType === WeatherType.HEAVY_RAIN && !weather.isEffectSuppressed()) - ) { - movePhase.fail(); - movePhase.showMoveText(); + const movePhase = globalScene.phaseManager.getCurrentPhase(); + if (lapseType !== BattlerTagLapseType.PRE_MOVE || !movePhase?.is("MovePhase")) { + return false; + } - const idx = pokemon.getBattlerIndex(); - - globalScene.phaseManager.unshiftNew("CommonAnimPhase", idx, idx, CommonAnim.POWDER); - - const cancelDamage = new BooleanHolder(false); - applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelDamage); - if (!cancelDamage.value) { - pokemon.damageAndUpdate(Math.floor(pokemon.getMaxHp() / 4), { result: HitResult.INDIRECT }); - } - - // "When the flame touched the powder\non the Pokémon, it exploded!" - globalScene.phaseManager.queueMessage(i18next.t("battlerTags:powderLapse", { moveName: move.name })); - } - } + const move = movePhase.move.getMove(); + const weather = globalScene.arena.weather; + if ( + pokemon.getMoveType(move) !== PokemonType.FIRE || + (weather?.weatherType === WeatherType.HEAVY_RAIN && !weather.isEffectSuppressed()) // Heavy rain takes priority over powder + ) { return true; } - return super.lapse(pokemon, lapseType); + + // Disable the target's fire type move and damage it (subject to Magic Guard) + movePhase.showMoveText(); + movePhase.fail(); + + const idx = pokemon.getBattlerIndex(); + + globalScene.phaseManager.unshiftNew("CommonAnimPhase", idx, idx, CommonAnim.POWDER); + + const cancelDamage = new BooleanHolder(false); + applyAbAttrs("BlockNonDirectDamageAbAttr", pokemon, cancelDamage); + if (!cancelDamage.value) { + pokemon.damageAndUpdate(Math.floor(pokemon.getMaxHp() / 4), { result: HitResult.INDIRECT }); + } + + // "When the flame touched the powder\non the Pokémon, it exploded!" + globalScene.phaseManager.queueMessage(i18next.t("battlerTags:powderLapse", { moveName: move.name })); + + return true; } } @@ -1083,7 +1060,7 @@ export class NightmareTag extends BattlerTag { phaseManager.unshiftNew("CommonAnimPhase", pokemon.getBattlerIndex(), undefined, CommonAnim.CURSE); // TODO: Update animation type const cancelled = new BooleanHolder(false); - applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled); + applyAbAttrs("BlockNonDirectDamageAbAttr", pokemon, cancelled); if (!cancelled.value) { pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 4), { result: HitResult.INDIRECT }); @@ -1136,34 +1113,22 @@ export class EncoreTag extends MoveRestrictionBattlerTag { } canAdd(pokemon: Pokemon): boolean { - const lastMoves = pokemon.getLastXMoves(1); - if (!lastMoves.length) { + const lastMove = pokemon.getLastNonVirtualMove(); + if (!lastMove) { return false; } - const repeatableMove = lastMoves[0]; - - if (!repeatableMove.move || repeatableMove.virtual) { + if (invalidEncoreMoves.has(lastMove.move)) { return false; } - switch (repeatableMove.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; + this.moveId = lastMove.move; return true; } onAdd(pokemon: Pokemon): void { + // TODO: shouldn't this be `onAdd`? super.onRemove(pokemon); globalScene.phaseManager.queueMessage( @@ -1179,7 +1144,13 @@ export class EncoreTag extends MoveRestrictionBattlerTag { const lastMove = pokemon.getLastXMoves(1)[0]; globalScene.phaseManager.tryReplacePhase( 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, + ), ); } } @@ -1442,7 +1413,7 @@ export abstract class DamagingTrapTag extends TrappedTag { phaseManager.unshiftNew("CommonAnimPhase", pokemon.getBattlerIndex(), undefined, this.commonAnim); const cancelled = new BooleanHolder(false); - applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled); + applyAbAttrs("BlockNonDirectDamageAbAttr", pokemon, cancelled); if (!cancelled.value) { pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 8), { result: HitResult.INDIRECT }); @@ -1481,16 +1452,6 @@ export class WrapTag extends DamagingTrapTag { } export abstract class VortexTrapTag extends DamagingTrapTag { - constructor( - tagType: BattlerTagType, - commonAnim: CommonAnim, - turnCount: number, - sourceMove: MoveId, - sourceId: number, - ) { - super(tagType, commonAnim, turnCount, sourceMove, sourceId); - } - getTrapMessage(pokemon: Pokemon): string { return i18next.t("battlerTags:vortexOnTrap", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), @@ -1685,7 +1646,7 @@ export class ContactDamageProtectedTag extends ContactProtectedTag { */ override onContact(attacker: Pokemon, user: Pokemon): void { const cancelled = new BooleanHolder(false); - applyAbAttrs(BlockNonDirectDamageAbAttr, user, cancelled); + applyAbAttrs("BlockNonDirectDamageAbAttr", user, cancelled); if (!cancelled.value) { attacker.damageAndUpdate(toDmgValue(attacker.getMaxHp() * (1 / this.damageRatio)), { result: HitResult.INDIRECT, @@ -1915,24 +1876,29 @@ export class TruantTag extends AbilityBattlerTag { lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { if (!pokemon.hasAbility(AbilityId.TRUANT)) { + // remove tag if mon lacks ability return super.lapse(pokemon, lapseType); } - const passive = pokemon.getAbility().id !== AbilityId.TRUANT; - const lastMove = pokemon.getLastXMoves().find(() => true); + const lastMove = pokemon.getLastXMoves()[0]; - if (lastMove && lastMove.move !== MoveId.NONE) { - (globalScene.phaseManager.getCurrentPhase() as MovePhase).cancel(); - // TODO: Ability displays should be handled by the ability - globalScene.phaseManager.queueAbilityDisplay(pokemon, passive, true); - globalScene.phaseManager.queueMessage( - i18next.t("battlerTags:truantLapse", { - pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), - }), - ); - globalScene.phaseManager.queueAbilityDisplay(pokemon, passive, false); + 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; + (globalScene.phaseManager.getCurrentPhase() as MovePhase).cancel(); + // TODO: Ability displays should be handled by the ability + globalScene.phaseManager.queueAbilityDisplay(pokemon, passive, true); + globalScene.phaseManager.queueMessage( + i18next.t("battlerTags:truantLapse", { + pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), + }), + ); + globalScene.phaseManager.queueAbilityDisplay(pokemon, passive, false); + return true; } } @@ -2281,7 +2247,7 @@ export class SaltCuredTag extends BattlerTag { ); const cancelled = new BooleanHolder(false); - applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled); + applyAbAttrs("BlockNonDirectDamageAbAttr", pokemon, cancelled); if (!cancelled.value) { const pokemonSteelOrWater = pokemon.isOfType(PokemonType.STEEL) || pokemon.isOfType(PokemonType.WATER); @@ -2335,7 +2301,7 @@ export class CursedTag extends BattlerTag { ); const cancelled = new BooleanHolder(false); - applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled); + applyAbAttrs("BlockNonDirectDamageAbAttr", pokemon, cancelled); if (!cancelled.value) { pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 4), { result: HitResult.INDIRECT }); @@ -2670,7 +2636,7 @@ export class GulpMissileTag extends BattlerTag { } const cancelled = new BooleanHolder(false); - applyAbAttrs(BlockNonDirectDamageAbAttr, attacker, cancelled); + applyAbAttrs("BlockNonDirectDamageAbAttr", attacker, cancelled); if (!cancelled.value) { attacker.damageAndUpdate(Math.max(1, Math.floor(attacker.getMaxHp() / 4)), { result: HitResult.INDIRECT }); @@ -3060,8 +3026,8 @@ export class MysteryEncounterPostSummonTag extends BattlerTag { if (lapseType === BattlerTagLapseType.CUSTOM) { const cancelled = new BooleanHolder(false); - applyAbAttrs(ProtectStatAbAttr, pokemon, cancelled); - applyAbAttrs(ConditionalUserFieldProtectStatAbAttr, pokemon, cancelled, false, pokemon); + applyAbAttrs("ProtectStatAbAttr", pokemon, cancelled); + applyAbAttrs("ConditionalUserFieldProtectStatAbAttr", pokemon, cancelled, false, pokemon); if (!cancelled.value) { if (pokemon.mysteryEncounterBattleEffects) { pokemon.mysteryEncounterBattleEffects(pokemon); diff --git a/src/data/berry.ts b/src/data/berry.ts index df500fa0609..7d1e62362a8 100644 --- a/src/data/berry.ts +++ b/src/data/berry.ts @@ -3,7 +3,7 @@ import type Pokemon from "../field/pokemon"; import { HitResult } from "#enums/hit-result"; import { getStatusEffectHealText } from "./status-effect"; import { NumberHolder, toDmgValue, randSeedInt } from "#app/utils/common"; -import { DoubleBerryEffectAbAttr, ReduceBerryUseThresholdAbAttr, applyAbAttrs } from "./abilities/ability"; +import { applyAbAttrs } from "./abilities/apply-ab-attrs"; import i18next from "i18next"; import { BattlerTagType } from "#enums/battler-tag-type"; import { BerryType } from "#enums/berry-type"; @@ -38,25 +38,25 @@ export function getBerryPredicate(berryType: BerryType): BerryPredicate { const threshold = new NumberHolder(0.25); // Offset BerryType such that LIECHI -> Stat.ATK = 1, GANLON -> Stat.DEF = 2, so on and so forth const stat: BattleStat = berryType - BerryType.ENIGMA; - applyAbAttrs(ReduceBerryUseThresholdAbAttr, pokemon, null, false, threshold); + applyAbAttrs("ReduceBerryUseThresholdAbAttr", pokemon, null, false, threshold); return pokemon.getHpRatio() < threshold.value && pokemon.getStatStage(stat) < 6; }; case BerryType.LANSAT: return (pokemon: Pokemon) => { const threshold = new NumberHolder(0.25); - applyAbAttrs(ReduceBerryUseThresholdAbAttr, pokemon, null, false, threshold); + applyAbAttrs("ReduceBerryUseThresholdAbAttr", pokemon, null, false, threshold); return pokemon.getHpRatio() < 0.25 && !pokemon.getTag(BattlerTagType.CRIT_BOOST); }; case BerryType.STARF: return (pokemon: Pokemon) => { const threshold = new NumberHolder(0.25); - applyAbAttrs(ReduceBerryUseThresholdAbAttr, pokemon, null, false, threshold); + applyAbAttrs("ReduceBerryUseThresholdAbAttr", pokemon, null, false, threshold); return pokemon.getHpRatio() < 0.25; }; case BerryType.LEPPA: return (pokemon: Pokemon) => { const threshold = new NumberHolder(0.25); - applyAbAttrs(ReduceBerryUseThresholdAbAttr, pokemon, null, false, threshold); + applyAbAttrs("ReduceBerryUseThresholdAbAttr", pokemon, null, false, threshold); return !!pokemon.getMoveset().find(m => !m.getPpRatio()); }; } @@ -72,7 +72,7 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc { case BerryType.ENIGMA: { const hpHealed = new NumberHolder(toDmgValue(consumer.getMaxHp() / 4)); - applyAbAttrs(DoubleBerryEffectAbAttr, consumer, null, false, hpHealed); + applyAbAttrs("DoubleBerryEffectAbAttr", consumer, null, false, hpHealed); globalScene.phaseManager.unshiftNew( "PokemonHealPhase", consumer.getBattlerIndex(), @@ -105,7 +105,7 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc { // Offset BerryType such that LIECHI --> Stat.ATK = 1, GANLON --> Stat.DEF = 2, etc etc. const stat: BattleStat = berryType - BerryType.ENIGMA; const statStages = new NumberHolder(1); - applyAbAttrs(DoubleBerryEffectAbAttr, consumer, null, false, statStages); + applyAbAttrs("DoubleBerryEffectAbAttr", consumer, null, false, statStages); globalScene.phaseManager.unshiftNew( "StatStageChangePhase", consumer.getBattlerIndex(), @@ -126,7 +126,7 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc { { const randStat = randSeedInt(Stat.SPD, Stat.ATK); const stages = new NumberHolder(2); - applyAbAttrs(DoubleBerryEffectAbAttr, consumer, null, false, stages); + applyAbAttrs("DoubleBerryEffectAbAttr", consumer, null, false, stages); globalScene.phaseManager.unshiftNew( "StatStageChangePhase", consumer.getBattlerIndex(), diff --git a/src/data/challenge.ts b/src/data/challenge.ts index 8bdccb6d5fd..3fdd83c185d 100644 --- a/src/data/challenge.ts +++ b/src/data/challenge.ts @@ -4,7 +4,8 @@ import i18next from "i18next"; import type { DexAttrProps, GameData } from "#app/system/game-data"; import { defaultStarterSpecies } from "#app/constants"; 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 type Pokemon from "#app/field/pokemon"; import { PokemonMove } from "./moves/pokemon-move"; @@ -21,7 +22,7 @@ import { TrainerType } from "#enums/trainer-type"; import { Nature } from "#enums/nature"; import type { MoveId } from "#enums/move-id"; import { TypeColor, TypeShadow } from "#enums/color"; -import { ModifierTier } from "#app/modifier/modifier-tier"; +import { ModifierTier } from "#enums/modifier-tier"; import { globalScene } from "#app/global-scene"; import { pokemonFormChanges } from "./pokemon-forms"; import { pokemonEvolutions } from "./balance/pokemon-evolutions"; diff --git a/src/data/daily-run.ts b/src/data/daily-run.ts index fc60e5795dc..e869ba7f28f 100644 --- a/src/data/daily-run.ts +++ b/src/data/daily-run.ts @@ -5,7 +5,8 @@ import { PlayerPokemon } from "#app/field/pokemon"; import type { Starter } from "#app/ui/starter-select-ui-handler"; import { randSeedGauss, randSeedInt, randSeedItem, getEnumValues } from "#app/utils/common"; 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 { pokerogueApi } from "#app/plugins/api/pokerogue-api"; import { BiomeId } from "#enums/biome-id"; diff --git a/src/data/data-lists.ts b/src/data/data-lists.ts index c763a001280..ed172846fe1 100644 --- a/src/data/data-lists.ts +++ b/src/data/data-lists.ts @@ -1,5 +1,11 @@ -import type { Ability } from "./abilities/ability-class"; +import type PokemonSpecies from "#app/data/pokemon-species"; +import type { ModifierTypes } from "#app/modifier/modifier-type"; +import type { Ability } from "./abilities/ability"; import type Move from "./moves/move"; export const allAbilities: Ability[] = []; export const allMoves: Move[] = []; +export const allSpecies: PokemonSpecies[] = []; + +// TODO: Figure out what this is used for and provide an appropriate tsdoc comment +export const modifierTypes = {} as ModifierTypes; diff --git a/src/data/dialogue.ts b/src/data/dialogue.ts index fa640e92b00..6bb96f0efb2 100644 --- a/src/data/dialogue.ts +++ b/src/data/dialogue.ts @@ -1723,49 +1723,6 @@ export const trainerTypeDialogue: TrainerTypeDialogue = { ], }; -export const doubleBattleDialogue = { - blue_red_double: { - encounter: ["doubleBattleDialogue:blue_red_double.encounter.1"], - victory: ["doubleBattleDialogue:blue_red_double.victory.1"], - }, - red_blue_double: { - encounter: ["doubleBattleDialogue:red_blue_double.encounter.1"], - victory: ["doubleBattleDialogue:red_blue_double.victory.1"], - }, - tate_liza_double: { - encounter: ["doubleBattleDialogue:tate_liza_double.encounter.1"], - victory: ["doubleBattleDialogue:tate_liza_double.victory.1"], - }, - liza_tate_double: { - encounter: ["doubleBattleDialogue:liza_tate_double.encounter.1"], - victory: ["doubleBattleDialogue:liza_tate_double.victory.1"], - }, - wallace_steven_double: { - encounter: ["doubleBattleDialogue:wallace_steven_double.encounter.1"], - victory: ["doubleBattleDialogue:wallace_steven_double.victory.1"], - }, - steven_wallace_double: { - encounter: ["doubleBattleDialogue:steven_wallace_double.encounter.1"], - victory: ["doubleBattleDialogue:steven_wallace_double.victory.1"], - }, - alder_iris_double: { - encounter: ["doubleBattleDialogue:alder_iris_double.encounter.1"], - victory: ["doubleBattleDialogue:alder_iris_double.victory.1"], - }, - iris_alder_double: { - encounter: ["doubleBattleDialogue:iris_alder_double.encounter.1"], - victory: ["doubleBattleDialogue:iris_alder_double.victory.1"], - }, - marnie_piers_double: { - encounter: ["doubleBattleDialogue:marnie_piers_double.encounter.1"], - victory: ["doubleBattleDialogue:marnie_piers_double.victory.1"], - }, - piers_marnie_double: { - encounter: ["doubleBattleDialogue:piers_marnie_double.encounter.1"], - victory: ["doubleBattleDialogue:piers_marnie_double.victory.1"], - }, -}; - export const battleSpecDialogue = { [BattleSpec.FINAL_BOSS]: { encounter: "battleSpecDialogue:encounter", diff --git a/src/data/double-battle-dialogue.ts b/src/data/double-battle-dialogue.ts new file mode 100644 index 00000000000..f15b74e4729 --- /dev/null +++ b/src/data/double-battle-dialogue.ts @@ -0,0 +1,44 @@ +// TODO: Move this back into `dialogue.ts` after finding a suitable way to remove the circular dependencies +// that caused this to be moved out in the first place +export const doubleBattleDialogue = { + blue_red_double: { + encounter: ["doubleBattleDialogue:blue_red_double.encounter.1"], + victory: ["doubleBattleDialogue:blue_red_double.victory.1"], + }, + red_blue_double: { + encounter: ["doubleBattleDialogue:red_blue_double.encounter.1"], + victory: ["doubleBattleDialogue:red_blue_double.victory.1"], + }, + tate_liza_double: { + encounter: ["doubleBattleDialogue:tate_liza_double.encounter.1"], + victory: ["doubleBattleDialogue:tate_liza_double.victory.1"], + }, + liza_tate_double: { + encounter: ["doubleBattleDialogue:liza_tate_double.encounter.1"], + victory: ["doubleBattleDialogue:liza_tate_double.victory.1"], + }, + wallace_steven_double: { + encounter: ["doubleBattleDialogue:wallace_steven_double.encounter.1"], + victory: ["doubleBattleDialogue:wallace_steven_double.victory.1"], + }, + steven_wallace_double: { + encounter: ["doubleBattleDialogue:steven_wallace_double.encounter.1"], + victory: ["doubleBattleDialogue:steven_wallace_double.victory.1"], + }, + alder_iris_double: { + encounter: ["doubleBattleDialogue:alder_iris_double.encounter.1"], + victory: ["doubleBattleDialogue:alder_iris_double.victory.1"], + }, + iris_alder_double: { + encounter: ["doubleBattleDialogue:iris_alder_double.encounter.1"], + victory: ["doubleBattleDialogue:iris_alder_double.victory.1"], + }, + marnie_piers_double: { + encounter: ["doubleBattleDialogue:marnie_piers_double.encounter.1"], + victory: ["doubleBattleDialogue:marnie_piers_double.victory.1"], + }, + piers_marnie_double: { + encounter: ["doubleBattleDialogue:piers_marnie_double.encounter.1"], + victory: ["doubleBattleDialogue:piers_marnie_double.victory.1"], + }, +}; diff --git a/src/data/egg.ts b/src/data/egg.ts index 67cdb7b1344..a6e2e04a5fe 100644 --- a/src/data/egg.ts +++ b/src/data/egg.ts @@ -1,7 +1,7 @@ import type BattleScene from "#app/battle-scene"; import { globalScene } from "#app/global-scene"; 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 { VariantTier } from "#enums/variant-tier"; import { randInt, randomString, randSeedInt, getIvsFromId } from "#app/utils/common"; diff --git a/src/data/moves/invalid-moves.ts b/src/data/moves/invalid-moves.ts index 559b679752d..6d97d8faf1e 100644 --- a/src/data/moves/invalid-moves.ts +++ b/src/data/moves/invalid-moves.ts @@ -1,6 +1,6 @@ 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 = new Set([ MoveId.AFTER_YOU, MoveId.ASSIST, @@ -255,3 +255,28 @@ export const noAbilityTypeOverrideMoves: ReadonlySet = new Set([ MoveId.TECHNO_BLAST, MoveId.HIDDEN_POWER, ]); + +/** Set of all moves that cannot be copied by {@linkcode Moves.SKETCH}. */ +export const invalidSketchMoves: ReadonlySet = 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 = new Set([ + MoveId.MIMIC, + MoveId.MIRROR_MOVE, + MoveId.TRANSFORM, + MoveId.STRUGGLE, + MoveId.SKETCH, + MoveId.SLEEP_TALK, + MoveId.ENCORE, +]); diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index ff92f5c34d8..662d886706c 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -33,38 +33,12 @@ import type { ArenaTrapTag } from "../arena-tag"; import { WeakenMoveTypeTag } from "../arena-tag"; import { ArenaTagSide } from "#enums/arena-tag-side"; import { - AllyMoveCategoryPowerBoostAbAttr, applyAbAttrs, applyPostAttackAbAttrs, applyPostItemLostAbAttrs, applyPreAttackAbAttrs, - applyPreDefendAbAttrs, - BlockItemTheftAbAttr, - BlockNonDirectDamageAbAttr, - BlockOneHitKOAbAttr, - BlockRecoilDamageAttr, - ChangeMovePriorityAbAttr, - ConfusionOnStatusEffectAbAttr, - FieldMoveTypePowerBoostAbAttr, - FieldPreventExplosiveMovesAbAttr, - ForceSwitchOutImmunityAbAttr, - HealFromBerryUseAbAttr, - IgnoreContactAbAttr, - IgnoreMoveEffectsAbAttr, - IgnoreProtectOnContactAbAttr, - InfiltratorAbAttr, - MaxMultiHitAbAttr, - MoveAbilityBypassAbAttr, - MoveEffectChanceMultiplierAbAttr, - MoveTypeChangeAbAttr, - PostDamageForceSwitchAbAttr, - PostItemLostAbAttr, - ReflectStatusMoveAbAttr, - ReverseDrainAbAttr, - UserFieldMoveTypePowerBoostAbAttr, - VariableMovePowerAbAttr, - WonderSkinAbAttr, -} from "../abilities/ability"; + applyPreDefendAbAttrs +} from "../abilities/apply-ab-attrs"; import { allAbilities, allMoves } from "../data-lists"; import { AttackTypeBoosterModifier, @@ -77,7 +51,7 @@ import { import type { BattlerIndex } from "#enums/battler-index"; import { BattleType } from "#enums/battle-type"; import { TerrainType } from "../terrain"; -import { ModifierPoolType } from "#app/modifier/modifier-type"; +import { ModifierPoolType } from "#enums/modifier-pool-type"; import { Command } from "#enums/command"; import i18next from "i18next"; import type { Localizable } from "#app/@types/locales"; @@ -96,13 +70,9 @@ import { getStatKey, Stat, } from "#app/enums/stat"; -import { BattleEndPhase } from "#app/phases/battle-end-phase"; import { MoveEndPhase } from "#app/phases/move-end-phase"; import { MovePhase } from "#app/phases/move-phase"; -import { NewBattlePhase } from "#app/phases/new-battle-phase"; import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase"; -import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; -import { SwitchPhase } from "#app/phases/switch-phase"; import { SwitchSummonPhase } from "#app/phases/switch-summon-phase"; import { SpeciesFormChangeRevertWeatherFormTrigger } from "../pokemon-forms/form-change-triggers"; import type { GameMode } from "#app/game-mode"; @@ -111,18 +81,14 @@ import { ChallengeType } from "#enums/challenge-type"; import { SwitchType } from "#enums/switch-type"; import { StatusEffect } from "#enums/status-effect"; 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 { MoveCategory } from "#enums/MoveCategory"; import { MoveTarget } from "#enums/MoveTarget"; import { MoveFlags } from "#enums/MoveFlags"; import { MoveEffectTrigger } from "#enums/MoveEffectTrigger"; import { MultiHitType } from "#enums/MultiHitType"; -import { invalidAssistMoves, invalidCopycatMoves, invalidMetronomeMoves, invalidMirrorMoveMoves, invalidSleepTalkMoves } from "./invalid-moves"; -import { SelectBiomePhase } from "#app/phases/select-biome-phase"; +import { invalidAssistMoves, invalidCopycatMoves, invalidMetronomeMoves, invalidMirrorMoveMoves, invalidSleepTalkMoves, invalidSketchMoves } from "./invalid-moves"; +import { isVirtual, MoveUseMode } from "#enums/move-use-mode"; import { ChargingMove, MoveAttrMap, MoveAttrString, MoveKindString, MoveClassMap } from "#app/@types/move-types"; import { applyMoveAttrs } from "./apply-attrs"; import { frenzyMissFunc, getMoveTargets } from "./move-utils"; @@ -377,7 +343,7 @@ export default abstract class Move implements Localizable { const bypassed = new BooleanHolder(false); // TODO: Allow this to be simulated - applyAbAttrs(InfiltratorAbAttr, user, null, false, bypassed); + applyAbAttrs("InfiltratorAbAttr", user, null, false, bypassed); return !bypassed.value && !this.hasFlag(MoveFlags.SOUND_BASED) @@ -668,14 +634,14 @@ export default abstract class Move implements Localizable { // special cases below, eg: if the move flag is MAKES_CONTACT, and the user pokemon has an ability that ignores contact (like "Long Reach"), then overrides and move does not make contact switch (flag) { case MoveFlags.MAKES_CONTACT: - if (user.hasAbilityWithAttr(IgnoreContactAbAttr) || this.hitsSubstitute(user, target)) { + if (user.hasAbilityWithAttr("IgnoreContactAbAttr") || this.hitsSubstitute(user, target)) { return false; } break; case MoveFlags.IGNORE_ABILITIES: - if (user.hasAbilityWithAttr(MoveAbilityBypassAbAttr)) { + if (user.hasAbilityWithAttr("MoveAbilityBypassAbAttr")) { const abilityEffectsIgnored = new BooleanHolder(false); - applyAbAttrs(MoveAbilityBypassAbAttr, user, abilityEffectsIgnored, false, this); + applyAbAttrs("MoveAbilityBypassAbAttr", user, abilityEffectsIgnored, false, this); if (abilityEffectsIgnored.value) { return true; } @@ -684,7 +650,7 @@ export default abstract class Move implements Localizable { } return this.hasFlag(MoveFlags.IGNORE_ABILITIES) && !isFollowUp; case MoveFlags.IGNORE_PROTECT: - if (user.hasAbilityWithAttr(IgnoreProtectOnContactAbAttr) + if (user.hasAbilityWithAttr("IgnoreProtectOnContactAbAttr") && this.doesFlagEffectApply({ flag: MoveFlags.MAKES_CONTACT, user })) { return true; } @@ -695,7 +661,7 @@ export default abstract class Move implements Localizable { target?.getTag(SemiInvulnerableTag) || !(target?.getTag(BattlerTagType.MAGIC_COAT) || (!this.doesFlagEffectApply({ flag: MoveFlags.IGNORE_ABILITIES, user, target }) && - target?.hasAbilityWithAttr(ReflectStatusMoveAbAttr))) + target?.hasAbilityWithAttr("ReflectStatusMoveAbAttr"))) ) { return false; } @@ -792,7 +758,7 @@ export default abstract class Move implements Localizable { const moveAccuracy = new NumberHolder(this.accuracy); applyMoveAttrs("VariableAccuracyAttr", user, target, this, moveAccuracy); - applyPreDefendAbAttrs(WonderSkinAbAttr, target, user, this, { value: false }, simulated, moveAccuracy); + applyPreDefendAbAttrs("WonderSkinAbAttr", target, user, this, { value: false }, simulated, moveAccuracy); if (moveAccuracy.value === -1) { return moveAccuracy.value; @@ -835,25 +801,25 @@ export default abstract class Move implements Localizable { const typeChangeMovePowerMultiplier = new NumberHolder(1); const typeChangeHolder = new NumberHolder(this.type); - applyPreAttackAbAttrs(MoveTypeChangeAbAttr, source, target, this, true, typeChangeHolder, typeChangeMovePowerMultiplier); + applyPreAttackAbAttrs("MoveTypeChangeAbAttr", source, target, this, true, typeChangeHolder, typeChangeMovePowerMultiplier); const sourceTeraType = source.getTeraType(); if (source.isTerastallized && sourceTeraType === this.type && power.value < 60 && this.priority <= 0 && !this.hasAttr("MultiHitAttr") && !globalScene.findModifier(m => m instanceof PokemonMultiHitModifier && m.pokemonId === source.id)) { power.value = 60; } - applyPreAttackAbAttrs(VariableMovePowerAbAttr, source, target, this, simulated, power); + applyPreAttackAbAttrs("VariableMovePowerAbAttr", source, target, this, simulated, power); const ally = source.getAlly(); if (!isNullOrUndefined(ally)) { - applyPreAttackAbAttrs(AllyMoveCategoryPowerBoostAbAttr, ally, target, this, simulated, power); + applyPreAttackAbAttrs("AllyMoveCategoryPowerBoostAbAttr", ally, target, this, simulated, power); } const fieldAuras = new Set( globalScene.getField(true) - .map((p) => p.getAbilityAttrs(FieldMoveTypePowerBoostAbAttr).filter(attr => { + .map((p) => p.getAbilityAttrs("FieldMoveTypePowerBoostAbAttr").filter(attr => { const condition = attr.getCondition(); return (!condition || condition(p)); - }) as FieldMoveTypePowerBoostAbAttr[]) + })) .flat(), ); for (const aura of fieldAuras) { @@ -861,7 +827,7 @@ export default abstract class Move implements Localizable { } const alliedField: Pokemon[] = source.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField(); - alliedField.forEach(p => applyPreAttackAbAttrs(UserFieldMoveTypePowerBoostAbAttr, p, target, this, simulated, power)); + alliedField.forEach(p => applyPreAttackAbAttrs("UserFieldMoveTypePowerBoostAbAttr", p, target, this, simulated, power)); power.value *= typeChangeMovePowerMultiplier.value; @@ -888,7 +854,7 @@ export default abstract class Move implements Localizable { const priority = new NumberHolder(this.priority); applyMoveAttrs("IncrementMovePriorityAttr", user, null, this, priority); - applyAbAttrs(ChangeMovePriorityAbAttr, user, null, simulated, this, priority); + applyAbAttrs("ChangeMovePriorityAbAttr", user, null, simulated, this, priority); return priority.value; } @@ -1340,7 +1306,7 @@ export class MoveEffectAttr extends MoveAttr { getMoveChance(user: Pokemon, target: Pokemon, move: Move, selfEffect?: Boolean, showAbility?: Boolean): number { const moveChance = new NumberHolder(this.effectChanceOverride ?? move.chance); - applyAbAttrs(MoveEffectChanceMultiplierAbAttr, user, null, !showAbility, moveChance, move); + applyAbAttrs("MoveEffectChanceMultiplierAbAttr", user, null, !showAbility, moveChance, move); if ((!move.hasAttr("FlinchAttr") || moveChance.value <= move.chance) && !move.hasAttr("SecretPowerAttr")) { const userSide = user.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; @@ -1348,7 +1314,7 @@ export class MoveEffectAttr extends MoveAttr { } if (!selfEffect) { - applyPreDefendAbAttrs(IgnoreMoveEffectsAbAttr, target, user, null, null, !showAbility, moveChance); + applyPreDefendAbAttrs("IgnoreMoveEffectsAbAttr", target, user, null, null, !showAbility, moveChance); } return moveChance.value; } @@ -1726,8 +1692,8 @@ export class RecoilAttr extends MoveEffectAttr { const cancelled = new BooleanHolder(false); if (!this.unblockable) { - applyAbAttrs(BlockRecoilDamageAttr, user, cancelled); - applyAbAttrs(BlockNonDirectDamageAbAttr, user, cancelled); + applyAbAttrs("BlockRecoilDamageAttr", user, cancelled); + applyAbAttrs("BlockNonDirectDamageAbAttr", user, cancelled); } if (cancelled.value) { @@ -1860,7 +1826,7 @@ export class HalfSacrificialAttr extends MoveEffectAttr { const cancelled = new BooleanHolder(false); // Check to see if the Pokemon has an ability that blocks non-direct damage - applyAbAttrs(BlockNonDirectDamageAbAttr, user, cancelled); + applyAbAttrs("BlockNonDirectDamageAbAttr", user, cancelled); if (!cancelled.value) { user.damageAndUpdate(toDmgValue(user.getMaxHp() / 2), { result: HitResult.INDIRECT, ignoreSegments: true }); globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:cutHpPowerUpMove", { pokemonName: getPokemonNameWithAffix(user) })); // Queue recoil message @@ -2059,7 +2025,7 @@ export class FlameBurstAttr extends MoveEffectAttr { const cancelled = new BooleanHolder(false); if (!isNullOrUndefined(targetAlly)) { - applyAbAttrs(BlockNonDirectDamageAbAttr, targetAlly, cancelled); + applyAbAttrs("BlockNonDirectDamageAbAttr", targetAlly, cancelled); } if (cancelled.value || !targetAlly || targetAlly.switchOutStatus) { @@ -2182,6 +2148,7 @@ export class PlantHealAttr extends WeatherHealAttr { case WeatherType.SANDSTORM: case WeatherType.HAIL: case WeatherType.SNOW: + case WeatherType.FOG: case WeatherType.HEAVY_RAIN: return 0.25; default: @@ -2289,7 +2256,7 @@ export class HitHealAttr extends MoveEffectAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { let healAmount = 0; let message = ""; - const reverseDrain = target.hasAbilityWithAttr(ReverseDrainAbAttr, false); + const reverseDrain = target.hasAbilityWithAttr("ReverseDrainAbAttr", false); if (this.healStat !== null) { // Strength Sap formula healAmount = target.getEffectiveStat(this.healStat); @@ -2300,7 +2267,7 @@ export class HitHealAttr extends MoveEffectAttr { message = i18next.t("battle:regainHealth", { pokemonName: getPokemonNameWithAffix(user) }); } if (reverseDrain) { - if (user.hasAbilityWithAttr(BlockNonDirectDamageAbAttr)) { + if (user.hasAbilityWithAttr("BlockNonDirectDamageAbAttr")) { healAmount = 0; message = ""; } else { @@ -2430,7 +2397,7 @@ export class MultiHitAttr extends MoveAttr { { const rand = user.randBattleSeedInt(20); const hitValue = new NumberHolder(rand); - applyAbAttrs(MaxMultiHitAbAttr, user, null, false, hitValue); + applyAbAttrs("MaxMultiHitAbAttr", user, null, false, hitValue); if (hitValue.value >= 13) { return 2; } else if (hitValue.value >= 6) { @@ -2538,7 +2505,7 @@ export class StatusEffectAttr extends MoveEffectAttr { } if (((!pokemon.status || this.overrideStatus) || (pokemon.status.effect === this.effect && moveChance < 0)) && pokemon.trySetStatus(this.effect, true, user, this.turnsRemaining, null, this.overrideStatus, quiet)) { - applyPostAttackAbAttrs(ConfusionOnStatusEffectAbAttr, user, target, move, null, false, this.effect); + applyPostAttackAbAttrs("ConfusionOnStatusEffectAbAttr", user, target, move, null, false, this.effect); return true; } } @@ -2694,7 +2661,7 @@ export class RemoveHeldItemAttr extends MoveEffectAttr { // Check for abilities that block item theft // TODO: This should not trigger if the target would faint beforehand const cancelled = new BooleanHolder(false); - applyAbAttrs(BlockItemTheftAbAttr, target, cancelled); + applyAbAttrs("BlockItemTheftAbAttr", target, cancelled); if (cancelled.value) { return false; @@ -2811,8 +2778,8 @@ export class EatBerryAttr extends MoveEffectAttr { protected eatBerry(consumer: Pokemon, berryOwner: Pokemon = consumer, updateHarvest = consumer === berryOwner) { // consumer eats berry, owner triggers unburden and similar effects getBerryEffectFunc(this.chosenBerry.berryType)(consumer); - applyPostItemLostAbAttrs(PostItemLostAbAttr, berryOwner, false); - applyAbAttrs(HealFromBerryUseAbAttr, consumer, new BooleanHolder(false)); + applyPostItemLostAbAttrs("PostItemLostAbAttr", berryOwner, false); + applyAbAttrs("HealFromBerryUseAbAttr", consumer, new BooleanHolder(false)); consumer.recordEatenBerry(this.chosenBerry.berryType, updateHarvest); } } @@ -2837,7 +2804,7 @@ export class StealEatBerryAttr extends EatBerryAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { // check for abilities that block item theft const cancelled = new BooleanHolder(false); - applyAbAttrs(BlockItemTheftAbAttr, target, cancelled); + applyAbAttrs("BlockItemTheftAbAttr", target, cancelled); if (cancelled.value === true) { return false; } @@ -2851,7 +2818,7 @@ export class StealEatBerryAttr extends EatBerryAttr { // pick a random berry and eat it this.chosenBerry = heldBerries[user.randBattleSeedInt(heldBerries.length)]; - applyPostItemLostAbAttrs(PostItemLostAbAttr, target, false); + applyPostItemLostAbAttrs("PostItemLostAbAttr", target, false); const message = i18next.t("battle:stealEatBerry", { pokemonName: user.name, targetName: target.name, berryName: this.chosenBerry.type.name }); globalScene.phaseManager.queueMessage(message); this.reduceBerryModifier(target); @@ -2892,7 +2859,7 @@ export class HealStatusEffectAttr extends MoveEffectAttr { // Special edge case for shield dust blocking Sparkling Aria curing burn const moveTargets = getMoveTargets(user, move.id); - if (target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && move.id === MoveId.SPARKLING_ARIA && moveTargets.targets.length === 1) { + if (target.hasAbilityWithAttr("IgnoreMoveEffectsAbAttr") && move.id === MoveId.SPARKLING_ARIA && moveTargets.targets.length === 1) { return false; } @@ -3042,7 +3009,7 @@ export class OneHitKOAttr extends MoveAttr { getCondition(): MoveConditionFunc { return (user, target, move) => { const cancelled = new BooleanHolder(false); - applyAbAttrs(BlockOneHitKOAbAttr, target, cancelled); + applyAbAttrs("BlockOneHitKOAbAttr", target, cancelled); return !cancelled.value && user.level >= target.level; }; } @@ -3149,7 +3116,7 @@ export class DelayedAttackAttr extends OverrideMoveEffectAttr { overridden.value = true; globalScene.phaseManager.unshiftNew("MoveAnimPhase", new MoveChargeAnim(this.chargeAnim, move.id, 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; globalScene.arena.addTag(this.tagType, 3, move.id, user.id, side, false, target.getBattlerIndex()); } else { @@ -4106,15 +4073,26 @@ export class OpponentHighHpPowerAttr extends VariablePowerAttr { } } +/** + * Attribute to double this move's power if the target hasn't acted yet in the current turn. + * Used by {@linkcode Moves.BOLT_BEAK} and {@linkcode Moves.FISHIOUS_REND} + */ export class FirstAttackDoublePowerAttr extends VariablePowerAttr { - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - console.log(target.getLastXMoves(1), globalScene.currentBattle.turn); - if (!target.getLastXMoves(1).find(m => m.turn === globalScene.currentBattle.turn)) { - (args[0] as NumberHolder).value *= 2; - return true; + /** + * Double this move's power if the user is acting before the target. + * @param user - Unused + * @param target - The {@linkcode Pokemon} being targeted by this move + * @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; } } @@ -4183,6 +4161,7 @@ export class AntiSunlightPowerDecreaseAttr extends VariablePowerAttr { case WeatherType.SANDSTORM: case WeatherType.HAIL: case WeatherType.SNOW: + case WeatherType.FOG: case WeatherType.HEAVY_RAIN: power.value *= 0.5; return true; @@ -5440,7 +5419,7 @@ export class NoEffectAttr extends MoveAttr { const crashDamageFunc = (user: Pokemon, move: Move) => { const cancelled = new BooleanHolder(false); - applyAbAttrs(BlockNonDirectDamageAbAttr, user, cancelled); + applyAbAttrs("BlockNonDirectDamageAbAttr", user, cancelled); if (cancelled.value) { return false; } @@ -5481,13 +5460,20 @@ export class FrenzyAttr extends MoveEffectAttr { return false; } - if (!user.getTag(BattlerTagType.FRENZY) && !user.getMoveQueue().length) { - const turnCount = user.randBattleSeedIntRange(1, 2); - new Array(turnCount).fill(null).map(() => user.getMoveQueue().push({ move: move.id, targets: [ target.getBattlerIndex() ], ignorePP: true })); + // TODO: Disable if used via dancer + // TODO: Add support for moves that don't add the frenzy tag (Uproar, Rollout, etc.) + + // 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); } else { 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; @@ -6254,6 +6240,7 @@ export class RevivalBlessingAttr extends MoveEffectAttr { 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 // (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(); if (user.fieldPosition === FieldPosition.CENTER) { user.setFieldPosition(FieldPosition.LEFT); @@ -6312,7 +6299,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { * Check if Wimp Out/Emergency Exit activates due to being hit by U-turn or Volt Switch * If it did, the user of U-turn or Volt Switch will not be switched out. */ - if (target.getAbility().hasAttr(PostDamageForceSwitchAbAttr) + if (target.getAbility().hasAttr("PostDamageForceSwitchAbAttr") && [ MoveId.U_TURN, MoveId.VOLT_SWITCH, MoveId.FLIP_TURN ].includes(move.id) ) { if (this.hpDroppedBelowHalf(target)) { @@ -6401,7 +6388,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { * Check if Wimp Out/Emergency Exit activates due to being hit by U-turn or Volt Switch * If it did, the user of U-turn or Volt Switch will not be switched out. */ - if (target.getAbility().hasAttr(PostDamageForceSwitchAbAttr) + if (target.getAbility().hasAttr("PostDamageForceSwitchAbAttr") && [ MoveId.U_TURN, MoveId.VOLT_SWITCH, MoveId.FLIP_TURN ].includes(move.id) ) { if (this.hpDroppedBelowHalf(target)) { @@ -6444,7 +6431,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { getFailedText(_user: Pokemon, target: Pokemon, _move: Move): string | undefined { const blockedByAbility = new BooleanHolder(false); - applyAbAttrs(ForceSwitchOutImmunityAbAttr, target, blockedByAbility); + applyAbAttrs("ForceSwitchOutImmunityAbAttr", target, blockedByAbility); if (blockedByAbility.value) { return i18next.t("moveTriggers:cannotBeSwitchedOut", { pokemonName: getPokemonNameWithAffix(target) }); } @@ -6484,7 +6471,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { } const blockedByAbility = new BooleanHolder(false); - applyAbAttrs(ForceSwitchOutImmunityAbAttr, target, blockedByAbility); + applyAbAttrs("ForceSwitchOutImmunityAbAttr", target, blockedByAbility); if (blockedByAbility.value) { return false; } @@ -6795,20 +6782,26 @@ export class FirstMoveTypeAttr extends MoveEffectAttr { class CallMoveAttr extends OverrideMoveEffectAttr { protected invalidMoves: ReadonlySet; protected hasTarget: 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 moveTargets = getMoveTargets(user, move.id, replaceMoveTarget); if (moveTargets.targets.length === 0) { globalScene.phaseManager.queueMessage(i18next.t("battle:attackFailed")); - console.log("CallMoveAttr failed due to no targets."); 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 ? moveTargets.targets - : [ this.hasTarget ? target.getBattlerIndex() : moveTargets.targets[user.randBattleSeedInt(moveTargets.targets.length)] ]; // account for Mirror Move having a target already - user.getMoveQueue().push({ move: move.id, targets: targets, virtual: true, ignorePP: true }); + : [this.hasTarget + ? target.getBattlerIndex() + : moveTargets.targets[user.randBattleSeedInt(moveTargets.targets.length)]]; + 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; } } @@ -6903,9 +6896,10 @@ export class RandomMovesetMoveAttr extends CallMoveAttr { } } +// TODO: extend CallMoveAttr export class NaturePowerAttr extends OverrideMoveEffectAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - let moveId; + let moveId = MoveId.NONE; switch (globalScene.arena.getTerrainType()) { // this allows terrains to 'override' the biome move case TerrainType.NONE: @@ -7030,14 +7024,14 @@ export class NaturePowerAttr extends OverrideMoveEffectAttr { moveId = MoveId.PSYCHIC; break; default: - // Just in case there's no match + // Just in case there's no match moveId = MoveId.TRI_ATTACK; 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("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; } } @@ -7056,64 +7050,63 @@ export class CopyMoveAttr extends CallMoveAttr { 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; - 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); } getCondition(): MoveConditionFunc { - return (user, target, move) => { - if (this.mirrorMove) { - const lastMove = target.getLastXMoves()[0]?.move; - return !!lastMove && !this.invalidMoves.has(lastMove); - } else { - const lastMove = globalScene.currentBattle.lastMove; - return lastMove !== undefined && !this.invalidMoves.has(lastMove); - } + return (_user, target, _move) => { + const lastMove = this.mirrorMove ? target.getLastNonVirtualMove(false, false)?.move : globalScene.currentBattle.lastMove; + return !isNullOrUndefined(lastMove) && !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)). */ export class RepeatMoveAttr extends MoveEffectAttr { + private movesetMove: PokemonMove; constructor() { super(false, { trigger: MoveEffectTrigger.POST_APPLY }); // needed to ensure correct protect interaction } /** - * Forces the target to re-use their last used move again - * - * @param user {@linkcode Pokemon} that used the attack - * @param target {@linkcode Pokemon} targeted by the attack - * @param move N/A - * @param args N/A + * Forces the target to re-use their last used move again. + * @param user - The {@linkcode Pokemon} using the attack + * @param target - The {@linkcode Pokemon} being targeted by the attack * @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 - const lastMove = target.getLastXMoves(-1).find(m => m.move !== MoveId.NONE)!; - const movesetMove = target.getMoveset().find(m => m.moveId === lastMove.move)!; - // If the last move used can hit more than one target or has variable targets, - // re-compute the targets for the attack - // (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; + // bangs are justified as Instruct fails if no prior move or moveset move exists + // TODO: How does instruct work when copying a move called via Copycat that the user itself knows? + const lastMove = target.getLastNonVirtualMove()!; + const movesetMove = target.getMoveset().find(m => m.moveId === lastMove?.move)! - /** 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 over when the enemy in question faints - (see `redirectPokemonMoves` in `battle-scene.ts`), - but since instruct adds a new move phase pre-emptively, we need to handle this interaction manually. - */ + // If the last move used can hit more than one target or has variable targets, + // re-compute the targets for the attack (mainly for alternating double/single battles) + // Rampaging moves (e.g. Outrage) are not included due to being incompatible with Instruct, + // 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]]; - 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(); - if (!isNullOrUndefined(ally) && ally.isActive()) { // ally exists, is not dead and can sponge the blast + if (!isNullOrUndefined(ally) && ally.isActive()) { moveTargets = [ ally.getBattlerIndex() ]; } } @@ -7122,15 +7115,15 @@ export class RepeatMoveAttr extends MoveEffectAttr { userPokemonName: getPokemonNameWithAffix(user), targetPokemonName: getPokemonNameWithAffix(target) })); - target.getMoveQueue().unshift({ move: lastMove.move, targets: moveTargets, ignorePP: false }); target.turnData.extraTurns++; - globalScene.phaseManager.appendNewToPhase("MoveEndPhase", "MovePhase", target, moveTargets, movesetMove); + globalScene.phaseManager.appendNewToPhase("MoveEndPhase", "MovePhase", target, moveTargets, movesetMove, MoveUseMode.NORMAL); return true; } getCondition(): MoveConditionFunc { - return (user, target, move) => { - const lastMove = target.getLastXMoves(-1).find(m => m.move !== MoveId.NONE); + return (_user, target, _move) => { + // 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 uninstructableMoves = [ // Locking/Continually Executed moves @@ -7140,6 +7133,7 @@ export class RepeatMoveAttr extends MoveEffectAttr { MoveId.PETAL_DANCE, MoveId.THRASH, MoveId.ICE_BALL, + MoveId.UPROAR, // Multi-turn Moves MoveId.BIDE, MoveId.SHELL_TRAP, @@ -7177,23 +7171,34 @@ export class RepeatMoveAttr extends MoveEffectAttr { MoveId.SOLAR_BEAM, MoveId.SOLAR_BLADE, 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, + // Misc moves MoveId.KINGS_SHIELD, MoveId.SKETCH, MoveId.TRANSFORM, MoveId.MIMIC, 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 || !movesetMove // called move not in target's moveset (forgetting the move, etc.) || 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 || uninstructableMoves.includes(lastMove.move)) { // called move is in the banlist return false; } + this.movesetMove = movesetMove; return true; }; } @@ -7224,53 +7229,48 @@ export class ReducePpMoveAttr extends MoveEffectAttr { /** * Reduces the PP of the target's last-used move by an amount based on this attribute instance's {@linkcode reduction}. * - * @param user {@linkcode Pokemon} that used the attack - * @param target {@linkcode Pokemon} targeted by the attack - * @param move N/A - * @param args N/A - * @returns `true` + * @param user - N/A + * @param target - The {@linkcode Pokemon} targeted by the attack + * @param move - N/A + * @param args - N/A + * @returns always `true` */ apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - // Null checks can be skipped due to condition function - const lastMove = target.getLastXMoves()[0]; - const movesetMove = target.getMoveset().find(m => m.moveId === lastMove.move)!; + /** The last move the target themselves used */ + const lastMove = target.getLastNonVirtualMove(); + const movesetMove = target.getMoveset().find(m => m.moveId === lastMove?.move)!; // bang is correct as condition prevents this from being nullish 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.phaseManager.queueMessage(message); + globalScene.phaseManager.queueMessage(i18next.t("battle:ppReduced", { targetName: getPokemonNameWithAffix(target), moveName: movesetMove.getName(), reduction: (movesetMove.ppUsed) - lastPpUsed })); return true; } getCondition(): MoveConditionFunc { return (user, target, move) => { - const lastMove = target.getLastXMoves()[0]; - if (lastMove) { - const movesetMove = target.getMoveset().find(m => m.moveId === lastMove.move); - return !!movesetMove?.getPpRatio(); - } - return false; + const lastMove = target.getLastNonVirtualMove(); + const movesetMove = target.getMoveset().find(m => m.moveId === lastMove?.move) + return !!movesetMove?.getPpRatio(); }; } getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { - const lastMove = target.getLastXMoves()[0]; - if (lastMove) { - const movesetMove = target.getMoveset().find(m => m.moveId === lastMove.move); - if (movesetMove) { - const maxPp = movesetMove.getMovePp(); - const ppLeft = maxPp - movesetMove.ppUsed; - const value = -(8 - Math.ceil(Math.min(maxPp, 30) / 5)); - if (ppLeft < 4) { - return (value / 4) * ppLeft; - } - return value; - } + const lastMove = target.getLastNonVirtualMove(); + const movesetMove = target.getMoveset().find(m => m.moveId === lastMove?.move) + if (!movesetMove) { + return 0; } - return 0; + const maxPp = movesetMove.getMovePp(); + const ppLeft = maxPp - movesetMove.ppUsed; + const value = -(8 - Math.ceil(Math.min(maxPp, 30) / 5)); + if (ppLeft < 4) { + return (value / 4) * ppLeft; + } + return value; + } } @@ -7286,40 +7286,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. * - * @param user {@linkcode Pokemon} that used the attack - * @param target {@linkcode Pokemon} targeted by the attack - * @param move {@linkcode Move} being used - * @param args N/A - * @returns {boolean} true + * @param user - The {@linkcode Pokemon} using the move + * @param target -The {@linkcode Pokemon} targeted by the attack + * @param move - The {@linkcode Move} being used + * @param args - N/A + * @returns - always `true` */ apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - const lastMove = target.getLastXMoves().find(() => true); - if (lastMove) { - const movesetMove = target.getMoveset().find(m => m.moveId === lastMove.move); - if (Boolean(movesetMove?.getPpRatio())) { - super.apply(user, target, move, args); - } + const lastMove = target.getLastNonVirtualMove(); + const movesetMove = target.getMoveset().find(m => m.moveId === lastMove?.move); + if (movesetMove?.getPpRatio()) { + super.apply(user, target, move, args); } return true; } - // Override condition function to always perform damage. Instead, perform pp-reduction condition check in apply function above - getCondition(): MoveConditionFunc { - return (user, target, move) => true; + /** + * Override condition function to always perform damage. + * 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 targetMoves = target.getMoveHistory().filter(m => !m.virtual); - if (!targetMoves.length) { - return false; - } - - const copiableMove = targetMoves[0]; - - if (!copiableMove.move) { + const copiableMove = target.getLastNonVirtualMove(); + if (!copiableMove?.move) { return false; } @@ -7332,14 +7328,18 @@ const targetMoveCopiableCondition: MoveConditionFunc = (user, target, move) => { return true; }; +/** + * Attribute to temporarily copy the last move in the target's moveset. + * Used by {@linkcode Moves.MIMIC}. + */ export class MovesetCopyMoveAttr extends OverrideMoveEffectAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - const targetMoves = target.getMoveHistory().filter(m => !m.virtual); - if (!targetMoves.length) { + const lastMove = target.getLastNonVirtualMove() + if (!lastMove?.move) { return false; } - const copiedMove = allMoves[targetMoves[0].move]; + const copiedMove = allMoves[lastMove.move]; const thisMoveIndex = user.getMoveset().findIndex(m => m.moveId === move.id); @@ -7347,8 +7347,9 @@ export class MovesetCopyMoveAttr extends OverrideMoveEffectAttr { 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[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 })); @@ -7386,9 +7387,9 @@ export class SketchAttr extends MoveEffectAttr { return false; } - const targetMove = target.getLastXMoves(-1) - .find(m => m.move !== MoveId.NONE && m.move !== MoveId.STRUGGLE && !m.virtual); + const targetMove = target.getLastNonVirtualMove() if (!targetMove) { + // failsafe for TS compiler return false; } @@ -7411,28 +7412,10 @@ export class SketchAttr extends MoveEffectAttr { return false; } - const targetMove = target.getMoveHistory().filter(m => !m.virtual).at(-1); - if (!targetMove) { - return false; - } - - 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); + const targetMove = target.getLastNonVirtualMove(); + return !isNullOrUndefined(targetMove) + && !invalidSketchMoves.has(targetMove.move) + && user.getMoveset().every(m => m.moveId !== targetMove.move) }; } } @@ -7872,19 +7855,19 @@ export class LastResortAttr extends MoveAttr { // TODO: Verify behavior as Bulbapedia page is _extremely_ poorly documented getCondition(): MoveConditionFunc { return (user: Pokemon, _target: Pokemon, move: Move) => { - const movesInMoveset = new Set(user.getMoveset().map(m => m.moveId)); - if (!movesInMoveset.delete(move.id) || !movesInMoveset.size) { + const otherMovesInMoveset = new Set(user.getMoveset().map(m => m.moveId)); + 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 } - const movesInHistory = new Set( + const movesInHistory = new Set( 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) ); - // Since `Set.intersection()` is only present in ESNext, we have to coerce it to an array to check inclusion - return [...movesInMoveset].every(m => movesInHistory.has(m)) + // Since `Set.intersection()` is only present in ESNext, we have to do this to check inclusion + return [...otherMovesInMoveset].every(m => movesInHistory.has(m)) }; } } @@ -7906,27 +7889,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 { /** - * Allows the target of this move to act right after the user. - * - * @param user {@linkcode Pokemon} that is using the move. - * @param target {@linkcode Pokemon} that will move right after this move is used. - * @param move {@linkcode Move} {@linkcode MoveId.AFTER_YOU} - * @param _args N/A - * @returns true + * Cause the target of this move to act right after the user. + * @param user - Unused + * @param target - The {@linkcode Pokemon} targeted by this move + * @param _move - Unused + * @param _args - Unused + * @returns `true` */ override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean { 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. - const nextAttackPhase = globalScene.phaseManager.findPhase((phase) => phase.pokemon === target); - if (nextAttackPhase && globalScene.phaseManager.tryRemovePhase((phase: MovePhase) => phase.pokemon === target)) { - globalScene.phaseManager.prependNewToPhase("MovePhase", "MovePhase", target, [ ...nextAttackPhase.targets ], nextAttackPhase.move); + // Will find next acting phase of the targeted pokémon, delete it and queue it right after us. + const targetNextPhase = globalScene.phaseManager.findPhase(phase => phase.pokemon === target); + if (targetNextPhase && globalScene.phaseManager.tryRemovePhase((phase: MovePhase) => phase.pokemon === target)) { + globalScene.phaseManager.prependToPhase(targetNextPhase, "MovePhase"); } return true; @@ -7951,6 +7933,7 @@ export class ForceLastAttr extends MoveEffectAttr { override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean { 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((phase) => 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 - @@ -7963,7 +7946,7 @@ export class ForceLastAttr extends MoveEffectAttr { globalScene.phaseManager.phaseQueue.splice( globalScene.phaseManager.phaseQueue.indexOf(prependPhase), 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) ); } } @@ -7971,7 +7954,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 => { let slower: boolean; // quashed pokemon still have speed ties @@ -7992,7 +7981,7 @@ const failIfSingleBattle: MoveConditionFunc = (user, target, move) => globalScen const failIfDampCondition: MoveConditionFunc = (user, target, move) => { const cancelled = new BooleanHolder(false); - globalScene.getField(true).map(p=>applyAbAttrs(FieldPreventExplosiveMovesAbAttr, p, cancelled)); + globalScene.getField(true).map(p=>applyAbAttrs("FieldPreventExplosiveMovesAbAttr", p, cancelled)); // Queue a message if an ability prevented usage of the move if (cancelled.value) { globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:cannotUseMove", { pokemonName: getPokemonNameWithAffix(user), moveName: move.name })); @@ -8067,8 +8056,7 @@ export class UpperHandCondition extends MoveCondition { super((user, target, move) => { const targetCommand = globalScene.currentBattle.turnCommands[target.getBattlerIndex()]; - return !!targetCommand - && targetCommand.command === Command.FIGHT + return targetCommand?.command === Command.FIGHT && !target.turnData.acted && !!targetCommand.move?.move && allMoves[targetCommand.move.move].category !== MoveCategory.STATUS @@ -8101,6 +8089,7 @@ export class ResistLastMoveTypeAttr extends MoveEffectAttr { constructor() { super(true); } + /** * 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 @@ -8114,7 +8103,8 @@ export class ResistLastMoveTypeAttr extends MoveEffectAttr { 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) { return false; } @@ -8156,9 +8146,9 @@ export class ResistLastMoveTypeAttr extends MoveEffectAttr { } getCondition(): MoveConditionFunc { + // TODO: Does this count dancer? return (user, target, move) => { - const moveHistory = target.getLastXMoves(); - return moveHistory.length !== 0; + return target.getLastXMoves(-1).some(tm => tm.move !== MoveId.NONE); }; } } @@ -8576,9 +8566,9 @@ export function initMoves() { .attr(FixedDamageAttr, 20), new StatusMove(MoveId.DISABLE, PokemonType.NORMAL, 100, 20, -1, 0, 1) .attr(AddBattlerTagAttr, BattlerTagType.DISABLED, false, true) - .condition((user, target, move) => { - const lastRealMove = target.getLastXMoves(-1).find(m => !m.virtual); - return !isNullOrUndefined(lastRealMove) && lastRealMove.move !== MoveId.NONE && lastRealMove.move !== MoveId.STRUGGLE; + .condition((_user, target, _move) => { + const lastNonVirtualMove = target.getLastNonVirtualMove(); + return !isNullOrUndefined(lastNonVirtualMove) && lastNonVirtualMove.move !== MoveId.STRUGGLE; }) .ignoresSubstitute() .reflectable(), @@ -9127,7 +9117,10 @@ export function initMoves() { .attr(AddBattlerTagAttr, BattlerTagType.ENCORE, false, true) .ignoresSubstitute() .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) .partial(), // No effect implemented new AttackMove(MoveId.RAPID_SPIN, PokemonType.NORMAL, MoveCategory.PHYSICAL, 50, 100, 40, 100, 0, 2) @@ -10010,7 +10003,14 @@ export function initMoves() { .chargeAttr(SemiInvulnerableAttr, BattlerTagType.FLYING) .condition(failOnGravityCondition) .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) .attr(StatStageChangeAttr, [ Stat.ATK ], 1, true) .attr(StatStageChangeAttr, [ Stat.SPD ], 2, true), @@ -10581,9 +10581,12 @@ export function initMoves() { new StatusMove(MoveId.INSTRUCT, PokemonType.PSYCHIC, -1, 15, -1, 0, 7) .ignoresSubstitute() .attr(RepeatMoveAttr) - // incorrect interactions with Gigaton Hammer, Blood Moon & Torment - // Also has incorrect interactions with Dancer due to the latter - // erroneously adding copied moves to move history. + /* + * Incorrect interactions with Gigaton Hammer, Blood Moon & Torment due to them _failing on use_, not merely being unselectable. + * 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(), new AttackMove(MoveId.BEAK_BLAST, PokemonType.FLYING, MoveCategory.PHYSICAL, 100, 100, 15, -1, -3, 7) .attr(BeakBlastHeaderAttr) @@ -10641,7 +10644,13 @@ export function initMoves() { .bitingMove() .attr(RemoveScreensAttr), 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) .attr(StatStageChangeAttr, [ Stat.DEF ], -1) .makesContact(false), diff --git a/src/data/moves/pokemon-move.ts b/src/data/moves/pokemon-move.ts index da46caa819f..daad199fbbd 100644 --- a/src/data/moves/pokemon-move.ts +++ b/src/data/moves/pokemon-move.ts @@ -21,7 +21,6 @@ export class PokemonMove { public moveId: MoveId; public ppUsed: 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). @@ -29,11 +28,10 @@ export class PokemonMove { */ 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.ppUsed = ppUsed; this.ppUp = ppUp; - this.virtual = virtual; 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`. */ 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)) { return false; } @@ -88,6 +87,6 @@ export class PokemonMove { * @returns A valid {@linkcode PokemonMove} object */ 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); } } diff --git a/src/data/mystery-encounters/encounters/a-trainers-test-encounter.ts b/src/data/mystery-encounters/encounters/a-trainers-test-encounter.ts index 11081892205..7a1c9821e89 100644 --- a/src/data/mystery-encounters/encounters/a-trainers-test-encounter.ts +++ b/src/data/mystery-encounters/encounters/a-trainers-test-encounter.ts @@ -19,8 +19,8 @@ import i18next from "i18next"; import type { IEggOptions } from "#app/data/egg"; import { EggSourceType } from "#enums/egg-source-types"; import { EggTier } from "#enums/egg-type"; -import { ModifierTier } from "#app/modifier/modifier-tier"; -import { modifierTypes } from "#app/modifier/modifier-type"; +import { ModifierTier } from "#enums/modifier-tier"; +import { modifierTypes } from "#app/data/data-lists"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; /** the i18n namespace for the encounter */ diff --git a/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts b/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts index 2213dc4afaa..bec75288837 100644 --- a/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts +++ b/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts @@ -10,7 +10,7 @@ import type Pokemon from "#app/field/pokemon"; import { EnemyPokemon } from "#app/field/pokemon"; import { PokemonMove } from "#app/data/moves/pokemon-move"; import type { BerryModifierType, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; -import { modifierTypes } from "#app/modifier/modifier-type"; +import { modifierTypes } from "#app/data/data-lists"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { SpeciesId } from "#enums/species-id"; import { globalScene } from "#app/global-scene"; @@ -22,7 +22,7 @@ import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encoun import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; 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 { BattlerTagType } from "#enums/battler-tag-type"; 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 { Stat } from "#enums/stat"; import i18next from "i18next"; +import { MoveUseMode } from "#enums/move-use-mode"; /** the i18n namespace for this encounter */ const namespace = "mysteryEncounters/absoluteAvarice"; @@ -307,7 +308,7 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = MysteryEncounterBuilde sourceBattlerIndex: BattlerIndex.ENEMY, targets: [BattlerIndex.ENEMY], move: new PokemonMove(MoveId.STUFF_CHEEKS), - ignorePp: true, + useMode: MoveUseMode.IGNORE_PP, }); await transitionMysteryEncounterIntroVisuals(true, true, 500); diff --git a/src/data/mystery-encounters/encounters/an-offer-you-cant-refuse-encounter.ts b/src/data/mystery-encounters/encounters/an-offer-you-cant-refuse-encounter.ts index cdb98c56ed1..f8a904cdb45 100644 --- a/src/data/mystery-encounters/encounters/an-offer-you-cant-refuse-encounter.ts +++ b/src/data/mystery-encounters/encounters/an-offer-you-cant-refuse-encounter.ts @@ -4,7 +4,7 @@ import { setEncounterExp, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; -import { modifierTypes } from "#app/modifier/modifier-type"; +import { modifierTypes } from "#app/data/data-lists"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { SpeciesId } from "#enums/species-id"; import { globalScene } from "#app/global-scene"; @@ -18,7 +18,7 @@ import { } from "#app/data/mystery-encounters/mystery-encounter-requirements"; 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 { getPokemonSpecies } from "#app/data/pokemon-species"; +import { getPokemonSpecies } from "#app/utils/pokemon-utils"; import { speciesStarterCosts } from "#app/data/balance/starters"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; diff --git a/src/data/mystery-encounters/encounters/berries-abound-encounter.ts b/src/data/mystery-encounters/encounters/berries-abound-encounter.ts index 65ae3ea6c4f..d86a8439804 100644 --- a/src/data/mystery-encounters/encounters/berries-abound-encounter.ts +++ b/src/data/mystery-encounters/encounters/berries-abound-encounter.ts @@ -12,7 +12,9 @@ import { import type { PlayerPokemon } from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon"; import type { BerryModifierType, ModifierTypeOption } from "#app/modifier/modifier-type"; -import { ModifierPoolType, modifierTypes, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type"; +import { regenerateModifierPoolThresholds } from "#app/modifier/modifier-type"; +import { modifierTypes } from "#app/data/data-lists"; +import { ModifierPoolType } from "#enums/modifier-pool-type"; import { randSeedInt } from "#app/utils/common"; import { BattlerTagType } from "#enums/battler-tag-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; diff --git a/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts b/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts index c318efc0cb3..df06f40c159 100644 --- a/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts +++ b/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts @@ -38,7 +38,7 @@ import { } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import { PokemonType } from "#enums/pokemon-type"; import type { AttackTypeBoosterModifierType, ModifierTypeOption } from "#app/modifier/modifier-type"; -import { modifierTypes } from "#app/modifier/modifier-type"; +import { modifierTypes } from "#app/data/data-lists"; import type { PokemonHeldItemModifier } from "#app/modifier/modifier"; import { AttackTypeBoosterModifier, @@ -50,7 +50,7 @@ import { import i18next from "i18next"; import MoveInfoOverlay from "#app/ui/move-info-overlay"; import { allMoves } from "#app/data/data-lists"; -import { ModifierTier } from "#app/modifier/modifier-tier"; +import { ModifierTier } from "#enums/modifier-tier"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { getSpriteKeysFromSpecies } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; diff --git a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts index a81dcc841b7..9bdaa603540 100644 --- a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts +++ b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts @@ -11,9 +11,9 @@ import { import { trainerConfigs } from "#app/data/trainers/trainer-config"; import { TrainerPartyCompoundTemplate } from "#app/data/trainers/TrainerPartyTemplate"; import { TrainerPartyTemplate } from "#app/data/trainers/TrainerPartyTemplate"; -import { ModifierTier } from "#app/modifier/modifier-tier"; +import { ModifierTier } from "#enums/modifier-tier"; import type { PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; -import { ModifierPoolType, modifierTypes } from "#app/modifier/modifier-type"; +import { ModifierPoolType } from "#enums/modifier-pool-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { PartyMemberStrength } from "#enums/party-member-strength"; import { globalScene } from "#app/global-scene"; @@ -22,7 +22,7 @@ import { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-en import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { SpeciesId } from "#enums/species-id"; 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 { applyAbilityOverrideToPokemon, @@ -38,7 +38,6 @@ import i18next from "i18next"; import type { OptionSelectConfig } from "#app/ui/abstact-option-select-ui-handler"; import type { PlayerPokemon } from "#app/field/pokemon"; import { PokemonMove } from "#app/data/moves/pokemon-move"; -import { Ability } from "#app/data/abilities/ability-class"; import { BerryModifier } from "#app/modifier/modifier"; import { BerryType } from "#enums/berry-type"; import { BattlerIndex } from "#enums/battler-index"; @@ -49,6 +48,8 @@ import { CustomPokemonData } from "#app/data/custom-pokemon-data"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { EncounterAnim } from "#enums/encounter-anims"; import { Challenges } from "#enums/challenges"; +import { MoveUseMode } from "#enums/move-use-mode"; +import { allAbilities, modifierTypes } from "#app/data/data-lists"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounters/clowningAround"; @@ -139,7 +140,7 @@ export const ClowningAroundEncounter: MysteryEncounter = MysteryEncounterBuilder // Generate random ability for Blacephalon from pool const ability = RANDOM_ABILITY_POOL[randSeedInt(RANDOM_ABILITY_POOL.length)]; - encounter.setDialogueToken("ability", new Ability(ability, 3).name); + encounter.setDialogueToken("ability", allAbilities[ability].name); encounter.misc = { ability }; // Decide the random types for Blacephalon. They should not be the same. @@ -209,19 +210,19 @@ export const ClowningAroundEncounter: MysteryEncounter = MysteryEncounterBuilder sourceBattlerIndex: BattlerIndex.ENEMY, targets: [BattlerIndex.ENEMY_2], move: new PokemonMove(MoveId.ROLE_PLAY), - ignorePp: true, + useMode: MoveUseMode.IGNORE_PP, }, { sourceBattlerIndex: BattlerIndex.ENEMY_2, targets: [BattlerIndex.PLAYER], move: new PokemonMove(MoveId.TAUNT), - ignorePp: true, + useMode: MoveUseMode.IGNORE_PP, }, { sourceBattlerIndex: BattlerIndex.ENEMY_2, targets: [BattlerIndex.PLAYER_2], move: new PokemonMove(MoveId.TAUNT), - ignorePp: true, + useMode: MoveUseMode.IGNORE_PP, }, ); diff --git a/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts b/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts index a74980919d6..c68e395b379 100644 --- a/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts +++ b/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts @@ -19,14 +19,14 @@ import { getEncounterPokemonLevelForWave, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER, } 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 type { PlayerPokemon } from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon"; import { EnemyPokemon } from "#app/field/pokemon"; import { PokemonMove } from "#app/data/moves/pokemon-move"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; -import { modifierTypes } from "#app/modifier/modifier-type"; +import { modifierTypes } from "#app/data/data-lists"; import PokemonData from "#app/system/pokemon-data"; import type { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; import { BattlerTagType } from "#enums/battler-tag-type"; @@ -40,6 +40,7 @@ import { PokeballType } from "#enums/pokeball"; import { SpeciesId } from "#enums/species-id"; import { Stat } from "#enums/stat"; import i18next from "i18next"; +import { MoveUseMode } from "#enums/move-use-mode"; /** the i18n namespace for this encounter */ const namespace = "mysteryEncounters/dancingLessons"; @@ -214,7 +215,7 @@ export const DancingLessonsEncounter: MysteryEncounter = MysteryEncounterBuilder sourceBattlerIndex: BattlerIndex.ENEMY, targets: [BattlerIndex.PLAYER], move: new PokemonMove(MoveId.REVELATION_DANCE), - ignorePp: true, + useMode: MoveUseMode.IGNORE_PP, }); await hideOricorioPokemon(); diff --git a/src/data/mystery-encounters/encounters/dark-deal-encounter.ts b/src/data/mystery-encounters/encounters/dark-deal-encounter.ts index 6474df3570e..4056ba3532e 100644 --- a/src/data/mystery-encounters/encounters/dark-deal-encounter.ts +++ b/src/data/mystery-encounters/encounters/dark-deal-encounter.ts @@ -3,8 +3,8 @@ import { isNullOrUndefined, randSeedInt } from "#app/utils/common"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { SpeciesId } from "#enums/species-id"; import { globalScene } from "#app/global-scene"; -import { modifierTypes } from "#app/modifier/modifier-type"; -import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { modifierTypes } from "#app/data/data-lists"; +import { getPokemonSpecies } from "#app/utils/pokemon-utils"; import type MysteryEncounter 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"; diff --git a/src/data/mystery-encounters/encounters/delibirdy-encounter.ts b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts index 40893d93930..842fc9d73bd 100644 --- a/src/data/mystery-encounters/encounters/delibirdy-encounter.ts +++ b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts @@ -15,7 +15,7 @@ import { updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-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 Pokemon from "#app/field/pokemon"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; @@ -28,7 +28,7 @@ import { PreserveBerryModifier, } from "#app/modifier/modifier"; import type { PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; -import { modifierTypes } from "#app/modifier/modifier-type"; +import { modifierTypes } from "#app/data/data-lists"; import i18next from "#app/plugins/i18n"; import type { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; import { randSeedItem } from "#app/utils/common"; diff --git a/src/data/mystery-encounters/encounters/department-store-sale-encounter.ts b/src/data/mystery-encounters/encounters/department-store-sale-encounter.ts index 2b6ac9b7cf3..46152a7dc41 100644 --- a/src/data/mystery-encounters/encounters/department-store-sale-encounter.ts +++ b/src/data/mystery-encounters/encounters/department-store-sale-encounter.ts @@ -2,8 +2,8 @@ import { leaveEncounterWithoutBattle, setEncounterRewards, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; -import type { ModifierTypeFunc } from "#app/modifier/modifier-type"; -import { modifierTypes } from "#app/modifier/modifier-type"; +import type { ModifierTypeFunc } from "#app/@types/modifier-types"; +import { modifierTypes } from "#app/data/data-lists"; import { randSeedInt } from "#app/utils/common"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { SpeciesId } from "#enums/species-id"; diff --git a/src/data/mystery-encounters/encounters/field-trip-encounter.ts b/src/data/mystery-encounters/encounters/field-trip-encounter.ts index 82fbfd51eec..6ab0f8a6a4b 100644 --- a/src/data/mystery-encounters/encounters/field-trip-encounter.ts +++ b/src/data/mystery-encounters/encounters/field-trip-encounter.ts @@ -9,7 +9,7 @@ import { } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import type { PlayerPokemon } from "#app/field/pokemon"; import type { PokemonMove } from "#app/data/moves/pokemon-move"; -import { modifierTypes } from "#app/modifier/modifier-type"; +import { modifierTypes } from "#app/data/data-lists"; import type { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { globalScene } from "#app/global-scene"; diff --git a/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts index d42778cb17c..e8900f8def4 100644 --- a/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts +++ b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts @@ -10,7 +10,6 @@ import { generateModifierType, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import type { AttackTypeBoosterModifierType } from "#app/modifier/modifier-type"; -import { modifierTypes } from "#app/modifier/modifier-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { globalScene } from "#app/global-scene"; import type MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; @@ -21,7 +20,7 @@ import { TypeRequirement, } from "#app/data/mystery-encounters/mystery-encounter-requirements"; 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 { PokemonType } from "#enums/pokemon-type"; import { BattlerIndex } from "#enums/battler-index"; @@ -45,8 +44,9 @@ import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { AbilityId } from "#enums/ability-id"; import { BattlerTagType } from "#enums/battler-tag-type"; import { Stat } from "#enums/stat"; -import { Ability } from "#app/data/abilities/ability-class"; import { FIRE_RESISTANT_ABILITIES } from "#app/data/mystery-encounters/requirements/requirement-groups"; +import { MoveUseMode } from "#enums/move-use-mode"; +import { allAbilities, modifierTypes } from "#app/data/data-lists"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounters/fieryFallout"; @@ -201,13 +201,13 @@ export const FieryFalloutEncounter: MysteryEncounter = MysteryEncounterBuilder.w sourceBattlerIndex: BattlerIndex.ENEMY, targets: [BattlerIndex.PLAYER], move: new PokemonMove(MoveId.FIRE_SPIN), - ignorePp: true, + useMode: MoveUseMode.IGNORE_PP, }, { sourceBattlerIndex: BattlerIndex.ENEMY_2, targets: [BattlerIndex.PLAYER_2], move: new PokemonMove(MoveId.FIRE_SPIN), - ignorePp: true, + useMode: MoveUseMode.IGNORE_PP, }, ); await initBattleWithEnemyConfig(globalScene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0]); @@ -246,7 +246,7 @@ export const FieryFalloutEncounter: MysteryEncounter = MysteryEncounterBuilder.w if (chosenPokemon.trySetStatus(StatusEffect.BURN)) { // Burn applied encounter.setDialogueToken("burnedPokemon", chosenPokemon.getNameToRender()); - encounter.setDialogueToken("abilityName", new Ability(AbilityId.HEATPROOF, 3).name); + encounter.setDialogueToken("abilityName", allAbilities[AbilityId.HEATPROOF].name); queueEncounterMessage(`${namespace}:option.2.target_burned`); // Also permanently change the burned Pokemon's ability to Heatproof diff --git a/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts b/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts index 83538e9e0e9..c53ff610c48 100644 --- a/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts +++ b/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts @@ -9,13 +9,10 @@ import { } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { STEALING_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups"; import type Pokemon from "#app/field/pokemon"; -import { ModifierTier } from "#app/modifier/modifier-tier"; +import { ModifierTier } from "#enums/modifier-tier"; import type { ModifierTypeOption } from "#app/modifier/modifier-type"; -import { - getPlayerModifierTypeOptions, - ModifierPoolType, - regenerateModifierPoolThresholds, -} from "#app/modifier/modifier-type"; +import { getPlayerModifierTypeOptions, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type"; +import { ModifierPoolType } from "#enums/modifier-pool-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { globalScene } from "#app/global-scene"; import type MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; diff --git a/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts b/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts index 456562ca70e..c57232402dd 100644 --- a/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts +++ b/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts @@ -14,7 +14,7 @@ import { TrainerSlot } from "#enums/trainer-slot"; import type { PlayerPokemon } from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon"; 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 { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; @@ -26,7 +26,7 @@ import { PlayerGender } from "#enums/player-gender"; import { getPokeballAtlasKey, getPokeballTintColor } from "#app/data/pokeball"; import { addPokeballOpenParticles } from "#app/field/anims"; import { SpeciesFormChangeActiveTrigger } from "#app/data/pokemon-forms/form-change-triggers"; -import { modifierTypes } from "#app/modifier/modifier-type"; +import { modifierTypes } from "#app/data/data-lists"; import { Nature } from "#enums/nature"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { isPokemonValidForEncounterOptionSelection } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; diff --git a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts index a84b0af30ed..6bbc1a68772 100644 --- a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts +++ b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts @@ -4,14 +4,11 @@ import { setEncounterRewards, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { TrainerSlot } from "#enums/trainer-slot"; -import { ModifierTier } from "#app/modifier/modifier-tier"; +import { ModifierTier } from "#enums/modifier-tier"; import { MusicPreference } from "#app/system/settings/settings"; import type { ModifierTypeOption } from "#app/modifier/modifier-type"; -import { - getPlayerModifierTypeOptions, - ModifierPoolType, - regenerateModifierPoolThresholds, -} from "#app/modifier/modifier-type"; +import { getPlayerModifierTypeOptions, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type"; +import { ModifierPoolType } from "#enums/modifier-pool-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { globalScene } from "#app/global-scene"; import type MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; @@ -19,7 +16,8 @@ import { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-en import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { SpeciesId } from "#enums/species-id"; 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 { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; diff --git a/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts b/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts index 8eea0623cd4..2b54b0a42f5 100644 --- a/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts +++ b/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts @@ -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 { SpeciesId } from "#enums/species-id"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; diff --git a/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts b/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts index 6907e18cfdc..010358ea3b2 100644 --- a/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts +++ b/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts @@ -7,8 +7,8 @@ import { trainerConfigs } from "#app/data/trainers/trainer-config"; import { trainerPartyTemplates } from "#app/data/trainers/TrainerPartyTemplate"; import { TrainerPartyCompoundTemplate } from "#app/data/trainers/TrainerPartyTemplate"; import { TrainerPartyTemplate } from "#app/data/trainers/TrainerPartyTemplate"; -import { ModifierTier } from "#app/modifier/modifier-tier"; -import { modifierTypes } from "#app/modifier/modifier-type"; +import { ModifierTier } from "#enums/modifier-tier"; +import { modifierTypes } from "#app/data/data-lists"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { PartyMemberStrength } from "#enums/party-member-strength"; import { globalScene } from "#app/global-scene"; diff --git a/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts b/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts index 62029eb1847..321e65d7008 100644 --- a/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts +++ b/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts @@ -14,9 +14,9 @@ import { getHighestLevelPlayerPokemon, koPlayerPokemon, } 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 { ModifierTier } from "#app/modifier/modifier-tier"; +import { ModifierTier } from "#enums/modifier-tier"; import { randSeedInt } from "#app/utils/common"; import { MoveId } from "#enums/move-id"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; diff --git a/src/data/mystery-encounters/encounters/safari-zone-encounter.ts b/src/data/mystery-encounters/encounters/safari-zone-encounter.ts index d324e9f9b6c..207e6ca400d 100644 --- a/src/data/mystery-encounters/encounters/safari-zone-encounter.ts +++ b/src/data/mystery-encounters/encounters/safari-zone-encounter.ts @@ -17,7 +17,7 @@ import { PokeballType } from "#enums/pokeball"; import { PlayerGender } from "#enums/player-gender"; import { NumberHolder, randSeedInt } from "#app/utils/common"; 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 { doPlayerFlee, diff --git a/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts b/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts index 1afc67e1d12..967c105c740 100644 --- a/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts +++ b/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts @@ -7,7 +7,7 @@ import { } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import type { PlayerPokemon } from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon"; -import { modifierTypes } from "#app/modifier/modifier-type"; +import { modifierTypes } from "#app/data/data-lists"; import { randSeedInt } from "#app/utils/common"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { SpeciesId } from "#enums/species-id"; diff --git a/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts b/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts index 1ecd73143fc..4169fd6d7c5 100644 --- a/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts +++ b/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts @@ -1,6 +1,6 @@ import { STEALING_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups"; import type { PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; -import { modifierTypes } from "#app/modifier/modifier-type"; +import { modifierTypes } from "#app/data/data-lists"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { SpeciesId } from "#enums/species-id"; import { globalScene } from "#app/global-scene"; @@ -24,13 +24,14 @@ import { MoveId } from "#enums/move-id"; import { BattlerIndex } from "#enums/battler-index"; import { PokemonMove } from "#app/data/moves/pokemon-move"; 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 { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { BerryType } from "#enums/berry-type"; import { Stat } from "#enums/stat"; import { CustomPokemonData } from "#app/data/custom-pokemon-data"; import { randSeedInt } from "#app/utils/common"; +import { MoveUseMode } from "#enums/move-use-mode"; /** i18n namespace for the encounter */ const namespace = "mysteryEncounters/slumberingSnorlax"; @@ -137,7 +138,7 @@ export const SlumberingSnorlaxEncounter: MysteryEncounter = MysteryEncounterBuil sourceBattlerIndex: BattlerIndex.ENEMY, targets: [BattlerIndex.PLAYER], move: new PokemonMove(MoveId.SNORE), - ignorePp: true, + useMode: MoveUseMode.IGNORE_PP, }); await initBattleWithEnemyConfig(encounter.enemyPartyConfigs[0]); }, diff --git a/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts b/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts index edc9cf13834..9c9232612c6 100644 --- a/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts +++ b/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts @@ -23,7 +23,8 @@ import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode import { BiomeId } from "#enums/biome-id"; import { getBiomeKey } from "#app/field/arena"; import { PokemonType } from "#enums/pokemon-type"; -import { getPartyLuckValue, modifierTypes } from "#app/modifier/modifier-type"; +import { getPartyLuckValue } from "#app/modifier/modifier-type"; +import { modifierTypes } from "#app/data/data-lists"; import { TrainerSlot } from "#enums/trainer-slot"; import { BattlerTagType } from "#enums/battler-tag-type"; import { getPokemonNameWithAffix } from "#app/messages"; diff --git a/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts b/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts index fdfcc8f2f13..03c09f6918e 100644 --- a/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts @@ -15,7 +15,7 @@ import { BiomeId } from "#enums/biome-id"; import { TrainerType } from "#enums/trainer-type"; import i18next from "i18next"; 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 { Nature } from "#enums/nature"; import { MoveId } from "#enums/move-id"; @@ -26,7 +26,7 @@ import { EggSourceType } from "#enums/egg-source-types"; import { EggTier } from "#enums/egg-type"; import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; -import { modifierTypes } from "#app/modifier/modifier-type"; +import { modifierTypes } from "#app/data/data-lists"; import { PokemonType } from "#enums/pokemon-type"; import { getPokeballTintColor } from "#app/data/pokeball"; diff --git a/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts b/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts index 0ad209ae4c6..c3bcf9ceb24 100644 --- a/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts @@ -15,7 +15,7 @@ import { getSpriteKeysFromPokemon, } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; 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 { SpeciesId } from "#enums/species-id"; import { PokeballType } from "#enums/pokeball"; diff --git a/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts b/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts index a3eb78f479e..37d16075543 100644 --- a/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts @@ -8,12 +8,12 @@ import { generateModifierType, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import type { PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; -import { modifierTypes } from "#app/modifier/modifier-type"; +import { modifierTypes } from "#app/data/data-lists"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { globalScene } from "#app/global-scene"; import type MysteryEncounter 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 { Nature } from "#enums/nature"; 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 { Stat } from "#enums/stat"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; +import { MoveUseMode } from "#enums/move-use-mode"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounters/theStrongStuff"; @@ -214,13 +215,13 @@ export const TheStrongStuffEncounter: MysteryEncounter = MysteryEncounterBuilder sourceBattlerIndex: BattlerIndex.ENEMY, targets: [BattlerIndex.PLAYER], move: new PokemonMove(MoveId.GASTRO_ACID), - ignorePp: true, + useMode: MoveUseMode.IGNORE_PP, }, { sourceBattlerIndex: BattlerIndex.ENEMY, targets: [BattlerIndex.PLAYER], move: new PokemonMove(MoveId.STEALTH_ROCK), - ignorePp: true, + useMode: MoveUseMode.IGNORE_PP, }, ); diff --git a/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts b/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts index 91754629821..eba8a6ba00e 100644 --- a/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts @@ -8,7 +8,7 @@ import { transitionMysteryEncounterIntroVisuals, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import type { PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; -import { modifierTypes } from "#app/modifier/modifier-type"; +import { modifierTypes } from "#app/data/data-lists"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { globalScene } from "#app/global-scene"; import type MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; @@ -17,18 +17,18 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { TrainerType } from "#enums/trainer-type"; import { SpeciesId } from "#enums/species-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 { Nature } from "#enums/nature"; import { PokemonType } from "#enums/pokemon-type"; import { BerryType } from "#enums/berry-type"; import { Stat } from "#enums/stat"; import { SpeciesFormChangeAbilityTrigger } from "#app/data/pokemon-forms/form-change-triggers"; -import { applyPostBattleInitAbAttrs, PostBattleInitAbAttr } from "#app/data/abilities/ability"; +import { applyPostBattleInitAbAttrs } from "#app/data/abilities/apply-ab-attrs"; import { showEncounterDialogue, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; import i18next from "i18next"; -import { ModifierTier } from "#app/modifier/modifier-tier"; +import { ModifierTier } from "#enums/modifier-tier"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { BattlerTagType } from "#enums/battler-tag-type"; @@ -221,7 +221,7 @@ function endTrainerBattleAndShowDialogue(): Promise { // Each trainer battle is supposed to be a new fight, so reset all per-battle activation effects pokemon.resetBattleAndWaveData(); - applyPostBattleInitAbAttrs(PostBattleInitAbAttr, pokemon); + applyPostBattleInitAbAttrs("PostBattleInitAbAttr", pokemon); } globalScene.phaseManager.unshiftNew("ShowTrainerPhase"); diff --git a/src/data/mystery-encounters/encounters/training-session-encounter.ts b/src/data/mystery-encounters/encounters/training-session-encounter.ts index 9ab91f439bf..04e64083602 100644 --- a/src/data/mystery-encounters/encounters/training-session-encounter.ts +++ b/src/data/mystery-encounters/encounters/training-session-encounter.ts @@ -1,4 +1,4 @@ -import type { Ability } from "#app/data/abilities/ability-class"; +import type { Ability } from "#app/data/abilities/ability"; import { allAbilities } from "#app/data/data-lists"; import type { EnemyPartyConfig } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { diff --git a/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts b/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts index 8bcd024de5c..e2a740c4900 100644 --- a/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts +++ b/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts @@ -8,7 +8,7 @@ import { transitionMysteryEncounterIntroVisuals, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import type { PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; -import { modifierTypes } from "#app/modifier/modifier-type"; +import { modifierTypes } from "#app/data/data-lists"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { globalScene } from "#app/global-scene"; import type MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; @@ -21,13 +21,14 @@ import { HitHealModifier, PokemonHeldItemModifier, TurnHealModifier } from "#app import { applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import i18next from "#app/plugins/i18n"; -import { ModifierTier } from "#app/modifier/modifier-tier"; -import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { ModifierTier } from "#enums/modifier-tier"; +import { getPokemonSpecies } from "#app/utils/pokemon-utils"; import { MoveId } from "#enums/move-id"; import { BattlerIndex } from "#enums/battler-index"; import { PokemonMove } from "#app/data/moves/pokemon-move"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { randSeedInt } from "#app/utils/common"; +import { MoveUseMode } from "#enums/move-use-mode"; /** the i18n namespace for this encounter */ const namespace = "mysteryEncounters/trashToTreasure"; @@ -207,13 +208,13 @@ export const TrashToTreasureEncounter: MysteryEncounter = MysteryEncounterBuilde sourceBattlerIndex: BattlerIndex.ENEMY, targets: [BattlerIndex.PLAYER], move: new PokemonMove(MoveId.TOXIC), - ignorePp: true, + useMode: MoveUseMode.IGNORE_PP, }, { sourceBattlerIndex: BattlerIndex.ENEMY, targets: [BattlerIndex.ENEMY], move: new PokemonMove(MoveId.STOCKPILE), - ignorePp: true, + useMode: MoveUseMode.IGNORE_PP, }, ); await initBattleWithEnemyConfig(encounter.enemyPartyConfigs[0]); diff --git a/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts b/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts index c48f93a9a9d..44e578540dd 100644 --- a/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts +++ b/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts @@ -36,6 +36,7 @@ import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encoun import { BerryModifier } from "#app/modifier/modifier"; import { Stat } from "#enums/stat"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; +import { MoveUseMode } from "#enums/move-use-mode"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounters/uncommonBreed"; @@ -180,7 +181,7 @@ export const UncommonBreedEncounter: MysteryEncounter = MysteryEncounterBuilder. sourceBattlerIndex: BattlerIndex.ENEMY, targets: [target], move: pokemonMove, - ignorePp: true, + useMode: MoveUseMode.IGNORE_PP, }); } diff --git a/src/data/mystery-encounters/encounters/weird-dream-encounter.ts b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts index fe1911c9007..1ba756c7f5d 100644 --- a/src/data/mystery-encounters/encounters/weird-dream-encounter.ts +++ b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts @@ -19,13 +19,14 @@ import type Pokemon from "#app/field/pokemon"; import { PokemonMove } from "#app/data/moves/pokemon-move"; import { NumberHolder, isNullOrUndefined, randSeedInt, randSeedShuffle } from "#app/utils/common"; 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 { HiddenAbilityRateBoosterModifier, PokemonFormChangeItemModifier } from "#app/modifier/modifier"; import { achvs } from "#app/system/achv"; import { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import type { PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; -import { modifierTypes } from "#app/modifier/modifier-type"; +import { modifierTypes } from "#app/data/data-lists"; import i18next from "#app/plugins/i18n"; import { doPokemonTransformationSequence, @@ -34,7 +35,7 @@ import { import { getLevelTotalExp } from "#app/data/exp"; import { Stat } from "#enums/stat"; import { Challenges } from "#enums/challenges"; -import { ModifierTier } from "#app/modifier/modifier-tier"; +import { ModifierTier } from "#enums/modifier-tier"; import { PlayerGender } from "#enums/player-gender"; import { TrainerType } from "#enums/trainer-type"; import PokemonData from "#app/system/pokemon-data"; diff --git a/src/data/mystery-encounters/mystery-encounter-requirements.ts b/src/data/mystery-encounters/mystery-encounter-requirements.ts index bca34be723b..07fd155b2b2 100644 --- a/src/data/mystery-encounters/mystery-encounter-requirements.ts +++ b/src/data/mystery-encounters/mystery-encounter-requirements.ts @@ -1,6 +1,5 @@ import { globalScene } from "#app/global-scene"; import { allAbilities } from "../data-lists"; -import { EvolutionItem, pokemonEvolutions } from "#app/data/balance/pokemon-evolutions"; import { Nature } from "#enums/nature"; import { pokemonFormChanges } from "#app/data/pokemon-forms"; import { SpeciesFormChangeItemTrigger } from "../pokemon-forms/form-change-triggers"; @@ -11,12 +10,11 @@ import { WeatherType } from "#enums/weather-type"; import type { PlayerPokemon } from "#app/field/pokemon"; import { AttackTypeBoosterModifier } from "#app/modifier/modifier"; import type { AttackTypeBoosterModifierType } from "#app/modifier/modifier-type"; -import { isNullOrUndefined } from "#app/utils/common"; +import { coerceArray, isNullOrUndefined } from "#app/utils/common"; import type { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import type { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { SpeciesId } from "#enums/species-id"; -import { SpeciesFormKey } from "#enums/species-form-key"; import { TimeOfDay } from "#enums/time-of-day"; export interface EncounterRequirement { @@ -272,7 +270,7 @@ export class TimeOfDayRequirement extends EncounterSceneRequirement { constructor(timeOfDay: TimeOfDay | TimeOfDay[]) { super(); - this.requiredTimeOfDay = Array.isArray(timeOfDay) ? timeOfDay : [timeOfDay]; + this.requiredTimeOfDay = coerceArray(timeOfDay); } override meetsRequirement(): boolean { @@ -294,7 +292,7 @@ export class WeatherRequirement extends EncounterSceneRequirement { constructor(weather: WeatherType | WeatherType[]) { super(); - this.requiredWeather = Array.isArray(weather) ? weather : [weather]; + this.requiredWeather = coerceArray(weather); } override meetsRequirement(): boolean { @@ -360,7 +358,7 @@ export class PersistentModifierRequirement extends EncounterSceneRequirement { constructor(heldItem: string | string[], minNumberOfItems = 1) { super(); this.minNumberOfItems = minNumberOfItems; - this.requiredHeldItemModifiers = Array.isArray(heldItem) ? heldItem : [heldItem]; + this.requiredHeldItemModifiers = coerceArray(heldItem); } override meetsRequirement(): boolean { @@ -426,7 +424,7 @@ export class SpeciesRequirement extends EncounterPokemonRequirement { super(); this.minNumberOfPokemon = minNumberOfPokemon; this.invertQuery = invertQuery; - this.requiredSpecies = Array.isArray(species) ? species : [species]; + this.requiredSpecies = coerceArray(species); } override meetsRequirement(): boolean { @@ -466,7 +464,7 @@ export class NatureRequirement extends EncounterPokemonRequirement { super(); this.minNumberOfPokemon = minNumberOfPokemon; this.invertQuery = invertQuery; - this.requiredNature = Array.isArray(nature) ? nature : [nature]; + this.requiredNature = coerceArray(nature); } override meetsRequirement(): boolean { @@ -504,7 +502,7 @@ export class TypeRequirement extends EncounterPokemonRequirement { this.excludeFainted = excludeFainted; this.minNumberOfPokemon = minNumberOfPokemon; this.invertQuery = invertQuery; - this.requiredType = Array.isArray(type) ? type : [type]; + this.requiredType = coerceArray(type); } override meetsRequirement(): boolean { @@ -558,7 +556,7 @@ export class MoveRequirement extends EncounterPokemonRequirement { this.excludeDisallowedPokemon = excludeDisallowedPokemon; this.minNumberOfPokemon = minNumberOfPokemon; this.invertQuery = invertQuery; - this.requiredMoves = Array.isArray(moves) ? moves : [moves]; + this.requiredMoves = coerceArray(moves); } override meetsRequirement(): boolean { @@ -609,7 +607,7 @@ export class CompatibleMoveRequirement extends EncounterPokemonRequirement { super(); this.minNumberOfPokemon = minNumberOfPokemon; this.invertQuery = invertQuery; - this.requiredMoves = Array.isArray(learnableMove) ? learnableMove : [learnableMove]; + this.requiredMoves = coerceArray(learnableMove); } override meetsRequirement(): boolean { @@ -665,7 +663,7 @@ export class AbilityRequirement extends EncounterPokemonRequirement { this.excludeDisallowedPokemon = excludeDisallowedPokemon; this.minNumberOfPokemon = minNumberOfPokemon; this.invertQuery = invertQuery; - this.requiredAbilities = Array.isArray(abilities) ? abilities : [abilities]; + this.requiredAbilities = coerceArray(abilities); } override meetsRequirement(): boolean { @@ -710,7 +708,7 @@ export class StatusEffectRequirement extends EncounterPokemonRequirement { super(); this.minNumberOfPokemon = minNumberOfPokemon; this.invertQuery = invertQuery; - this.requiredStatusEffect = Array.isArray(statusEffect) ? statusEffect : [statusEffect]; + this.requiredStatusEffect = coerceArray(statusEffect); } override meetsRequirement(): boolean { @@ -785,7 +783,7 @@ export class CanFormChangeWithItemRequirement extends EncounterPokemonRequiremen super(); this.minNumberOfPokemon = minNumberOfPokemon; this.invertQuery = invertQuery; - this.requiredFormChangeItem = Array.isArray(formChangeItem) ? formChangeItem : [formChangeItem]; + this.requiredFormChangeItem = coerceArray(formChangeItem); } override meetsRequirement(): boolean { @@ -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 = Array.isArray(evolutionItems) ? evolutionItems : [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 { requiredHeldItemModifiers: string[]; minNumberOfPokemon: number; @@ -908,7 +842,7 @@ export class HeldItemRequirement extends EncounterPokemonRequirement { super(); this.minNumberOfPokemon = minNumberOfPokemon; this.invertQuery = invertQuery; - this.requiredHeldItemModifiers = Array.isArray(heldItem) ? heldItem : [heldItem]; + this.requiredHeldItemModifiers = coerceArray(heldItem); this.requireTransferable = requireTransferable; } @@ -972,7 +906,7 @@ export class AttackTypeBoosterHeldItemTypeRequirement extends EncounterPokemonRe super(); this.minNumberOfPokemon = minNumberOfPokemon; this.invertQuery = invertQuery; - this.requiredHeldItemTypes = Array.isArray(heldItemTypes) ? heldItemTypes : [heldItemTypes]; + this.requiredHeldItemTypes = coerceArray(heldItemTypes); this.requireTransferable = requireTransferable; } diff --git a/src/data/mystery-encounters/mystery-encounter-save-data.ts b/src/data/mystery-encounters/mystery-encounter-save-data.ts index dd633390e46..20c10c7c5b9 100644 --- a/src/data/mystery-encounters/mystery-encounter-save-data.ts +++ b/src/data/mystery-encounters/mystery-encounter-save-data.ts @@ -1,5 +1,5 @@ import type { MysteryEncounterType } from "#enums/mystery-encounter-type"; -import { BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT } from "#app/data/mystery-encounters/mystery-encounters"; +import { BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT } from "#app/constants"; import { isNullOrUndefined } from "#app/utils/common"; import type { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; diff --git a/src/data/mystery-encounters/mystery-encounter.ts b/src/data/mystery-encounters/mystery-encounter.ts index 6510e32fe76..fa97a7f4d40 100644 --- a/src/data/mystery-encounters/mystery-encounter.ts +++ b/src/data/mystery-encounters/mystery-encounter.ts @@ -2,7 +2,7 @@ import type { EnemyPartyConfig } from "#app/data/mystery-encounters/utils/encoun import type { PlayerPokemon } from "#app/field/pokemon"; import type { PokemonMove } from "../moves/pokemon-move"; import type Pokemon from "#app/field/pokemon"; -import { capitalizeFirstLetter, isNullOrUndefined } from "#app/utils/common"; +import { capitalizeFirstLetter, coerceArray, isNullOrUndefined } from "#app/utils/common"; import type { MysteryEncounterType } from "#enums/mystery-encounter-type"; import type { MysteryEncounterSpriteConfig } from "#app/field/mystery-encounter-intro"; import MysteryEncounterIntroVisuals from "#app/field/mystery-encounter-intro"; @@ -29,14 +29,14 @@ import type { GameModes } from "#enums/game-modes"; import type { EncounterAnim } from "#enums/encounter-anims"; import type { Challenges } from "#enums/challenges"; import { globalScene } from "#app/global-scene"; +import type { MoveUseMode } from "#enums/move-use-mode"; export interface EncounterStartOfBattleEffect { sourcePokemon?: Pokemon; sourceBattlerIndex?: BattlerIndex; targets: BattlerIndex[]; move: PokemonMove; - ignorePp: boolean; - followUp?: boolean; + useMode: MoveUseMode; // TODO: This should always be ignore PP... } const DEFAULT_MAX_ALLOWED_ENCOUNTERS = 2; @@ -254,7 +254,7 @@ export default class MysteryEncounter implements IMysteryEncounter { */ 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[] = []; /** @@ -717,7 +717,7 @@ export class MysteryEncounterBuilder implements Partial { withAnimations( ...encounterAnimations: EncounterAnim[] ): this & Required> { - const animations = Array.isArray(encounterAnimations) ? encounterAnimations : [encounterAnimations]; + const animations = coerceArray(encounterAnimations); return Object.assign(this, { encounterAnimations: animations }); } @@ -729,7 +729,7 @@ export class MysteryEncounterBuilder implements Partial { withDisallowedGameModes( ...disallowedGameModes: GameModes[] ): this & Required> { - const gameModes = Array.isArray(disallowedGameModes) ? disallowedGameModes : [disallowedGameModes]; + const gameModes = coerceArray(disallowedGameModes); return Object.assign(this, { disallowedGameModes: gameModes }); } @@ -741,7 +741,7 @@ export class MysteryEncounterBuilder implements Partial { withDisallowedChallenges( ...disallowedChallenges: Challenges[] ): this & Required> { - const challenges = Array.isArray(disallowedChallenges) ? disallowedChallenges : [disallowedChallenges]; + const challenges = coerceArray(disallowedChallenges); return Object.assign(this, { disallowedChallenges: challenges }); } diff --git a/src/data/mystery-encounters/mystery-encounters.ts b/src/data/mystery-encounters/mystery-encounters.ts index 3a1d3760c83..5ee289a6c56 100644 --- a/src/data/mystery-encounters/mystery-encounters.ts +++ b/src/data/mystery-encounters/mystery-encounters.ts @@ -34,42 +34,6 @@ import { GlobalTradeSystemEncounter } from "#app/data/mystery-encounters/encount import { TheExpertPokemonBreederEncounter } from "#app/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter"; import { getBiomeName } from "#app/data/balance/biomes"; -/** - * Spawn chance: (BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT + WIGHT_INCREMENT_ON_SPAWN_MISS * ) / MYSTERY_ENCOUNTER_SPAWN_MAX_WEIGHT - */ -export const BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT = 3; -/** - * The divisor for determining ME spawns, defines the "maximum" weight required for a spawn - * If spawn_weight === MYSTERY_ENCOUNTER_SPAWN_MAX_WEIGHT, 100% chance to spawn a ME - */ -export const MYSTERY_ENCOUNTER_SPAWN_MAX_WEIGHT = 256; -/** - * When an ME spawn roll fails, WEIGHT_INCREMENT_ON_SPAWN_MISS is added to future rolls for ME spawn checks. - * These values are cleared whenever the next ME spawns, and spawn weight returns to BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT - */ -export const WEIGHT_INCREMENT_ON_SPAWN_MISS = 3; -/** - * Specifies the target average for total ME spawns in a single Classic run. - * Used by anti-variance mechanic to check whether a run is above or below the target on a given wave. - */ -export const AVERAGE_ENCOUNTERS_PER_RUN_TARGET = 12; -/** - * Will increase/decrease the chance of spawning a ME based on the current run's total MEs encountered vs AVERAGE_ENCOUNTERS_PER_RUN_TARGET - * Example: - * AVERAGE_ENCOUNTERS_PER_RUN_TARGET = 17 (expects avg 1 ME every 10 floors) - * ANTI_VARIANCE_WEIGHT_MODIFIER = 15 - * - * On wave 20, if 1 ME has been encountered, the difference from expected average is 0 MEs. - * So anti-variance adds 0/256 to the spawn weight check for ME spawn. - * - * On wave 20, if 0 MEs have been encountered, the difference from expected average is 1 ME. - * So anti-variance adds 15/256 to the spawn weight check for ME spawn. - * - * On wave 20, if 2 MEs have been encountered, the difference from expected average is -1 ME. - * So anti-variance adds -15/256 to the spawn weight check for ME spawn. - */ -export const ANTI_VARIANCE_WEIGHT_MODIFIER = 15; - export const EXTREME_ENCOUNTER_BIOMES = [ BiomeId.SEA, BiomeId.SEABED, diff --git a/src/data/mystery-encounters/requirements/can-learn-move-requirement.ts b/src/data/mystery-encounters/requirements/can-learn-move-requirement.ts index c0e45d5401c..0123ea7d6ba 100644 --- a/src/data/mystery-encounters/requirements/can-learn-move-requirement.ts +++ b/src/data/mystery-encounters/requirements/can-learn-move-requirement.ts @@ -1,7 +1,7 @@ import type { MoveId } from "#enums/move-id"; import type { PlayerPokemon } from "#app/field/pokemon"; import { PokemonMove } from "#app/data/moves/pokemon-move"; -import { isNullOrUndefined } from "#app/utils/common"; +import { coerceArray, isNullOrUndefined } from "#app/utils/common"; import { EncounterPokemonRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import { globalScene } from "#app/global-scene"; @@ -29,7 +29,7 @@ export class CanLearnMoveRequirement extends EncounterPokemonRequirement { constructor(requiredMoves: MoveId | MoveId[], options: CanLearnMoveRequirementOptions = {}) { super(); - this.requiredMoves = Array.isArray(requiredMoves) ? requiredMoves : [requiredMoves]; + this.requiredMoves = coerceArray(requiredMoves); this.excludeLevelMoves = options.excludeLevelMoves ?? false; this.excludeTmMoves = options.excludeTmMoves ?? false; diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts index 69984229681..bb74f11ce60 100644 --- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -1,12 +1,8 @@ import type Battle from "#app/battle"; -import { BattlerIndex } from "#enums/battler-index"; import { BattleType } from "#enums/battle-type"; import { biomeLinks, BiomePoolTier } from "#app/data/balance/biomes"; import type MysteryEncounterOption from "#app/data/mystery-encounters/mystery-encounter-option"; -import { - AVERAGE_ENCOUNTERS_PER_RUN_TARGET, - WEIGHT_INCREMENT_ON_SPAWN_MISS, -} from "#app/data/mystery-encounters/mystery-encounters"; +import { AVERAGE_ENCOUNTERS_PER_RUN_TARGET, WEIGHT_INCREMENT_ON_SPAWN_MISS } from "#app/constants"; import { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import type { PlayerPokemon } from "#app/field/pokemon"; import type { AiType } from "#enums/ai-type"; @@ -17,18 +13,18 @@ import { FieldPosition } from "#enums/field-position"; import type { CustomModifierSettings, ModifierType } from "#app/modifier/modifier-type"; import { getPartyLuckValue, - ModifierPoolType, ModifierTypeGenerator, ModifierTypeOption, - modifierTypes, regenerateModifierPoolThresholds, } from "#app/modifier/modifier-type"; +import { modifierTypes } from "#app/data/data-lists"; +import { ModifierPoolType } from "#enums/modifier-pool-type"; import type PokemonData from "#app/system/pokemon-data"; import type { OptionSelectConfig, OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; import type { PartyOption, PokemonSelectFilter } from "#app/ui/party-ui-handler"; import { PartyUiMode } from "#app/ui/party-ui-handler"; import { UiMode } from "#enums/ui-mode"; -import { isNullOrUndefined, randSeedInt, randomString, randSeedItem } from "#app/utils/common"; +import { isNullOrUndefined, randSeedInt, randomString, randSeedItem, coerceArray } from "#app/utils/common"; import type { BattlerTagType } from "#enums/battler-tag-type"; import { BiomeId } from "#enums/biome-id"; import type { TrainerType } from "#enums/trainer-type"; @@ -52,7 +48,7 @@ import type HeldModifierConfig from "#app/@types/held-modifier-config"; import type { Variant } from "#app/sprites/variant"; import { StatusEffect } from "#enums/status-effect"; 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 { getNatureName } from "#app/data/nature"; import { getPokemonNameWithAffix } from "#app/messages"; @@ -452,7 +448,7 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig): * @param moves */ export function loadCustomMovesForEncounter(moves: MoveId | MoveId[]) { - moves = Array.isArray(moves) ? moves : [moves]; + moves = coerceArray(moves); return Promise.all(moves.map(move => initMoveAnim(move))).then(() => loadMoveAnimAssets(moves)); } @@ -795,7 +791,7 @@ export function setEncounterRewards( * @param useWaveIndex - set to false when directly passing the the full exp value instead of baseExpValue */ export function setEncounterExp(participantId: number | number[], baseExpValue: number, useWaveIndex = true) { - const participantIds = Array.isArray(participantId) ? participantId : [participantId]; + const participantIds = coerceArray(participantId); globalScene.currentBattle.mysteryEncounter!.doEncounterExp = () => { globalScene.phaseManager.unshiftNew("PartyExpPhase", baseExpValue, useWaveIndex, new Set(participantIds)); @@ -977,33 +973,8 @@ export function handleMysteryEncounterBattleStartEffects() { ) { const effects = encounter.startOfBattleEffects; effects.forEach(effect => { - let source: EnemyPokemon | Pokemon; - if (effect.sourcePokemon) { - 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, - ); + const source = effect.sourcePokemon ?? globalScene.getField()[effect.sourceBattlerIndex ?? 0]; + globalScene.phaseManager.pushNew("MovePhase", source, effect.targets, effect.move, effect.useMode); }); // Pseudo turn end phase to reset flinch states, Endure, etc. diff --git a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts index e8a3db46cff..93abd432ef5 100644 --- a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts @@ -20,7 +20,7 @@ import { PartyUiMode } from "#app/ui/party-ui-handler"; import { SpeciesId } from "#enums/species-id"; import type { PokemonType } from "#enums/pokemon-type"; 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 { getEncounterText, @@ -29,7 +29,7 @@ import { } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { getPokemonNameWithAffix } from "#app/messages"; import type { PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; -import { modifierTypes } from "#app/modifier/modifier-type"; +import { modifierTypes } from "#app/data/data-lists"; import { Gender } from "#app/data/gender"; import type { PermanentStat } from "#enums/stat"; import { SummaryUiMode } from "#app/ui/summary-ui-handler"; diff --git a/src/data/phase-priority-queue.ts b/src/data/phase-priority-queue.ts new file mode 100644 index 00000000000..b815a6ac34f --- /dev/null +++ b/src/data/phase-priority-queue.ts @@ -0,0 +1,97 @@ +import { globalScene } from "#app/global-scene"; +import type { Phase } from "#app/phase"; +import { ActivatePriorityQueuePhase } from "#app/phases/activate-priority-queue-phase"; +import type { PostSummonPhase } from "#app/phases/post-summon-phase"; +import { PostSummonActivateAbilityPhase } from "#app/phases/post-summon-activate-ability-phase"; +import { Stat } from "#enums/stat"; +import { BooleanHolder } from "#app/utils/common"; +import { TrickRoomTag } from "#app/data/arena-tag"; +import { DynamicPhaseType } from "#enums/dynamic-phase-type"; + +/** + * Stores a list of {@linkcode Phase}s + * + * Dynamically updates ordering to always pop the highest "priority", based on implementation of {@linkcode reorder} + */ +export abstract class PhasePriorityQueue { + protected abstract queue: Phase[]; + + /** + * Sorts the elements in the queue + */ + public abstract reorder(): void; + + /** + * Calls {@linkcode reorder} and shifts the queue + * @returns The front element of the queue after sorting + */ + public pop(): Phase | undefined { + this.reorder(); + return this.queue.shift(); + } + + /** + * Adds a phase to the queue + * @param phase The phase to add + */ + public push(phase: Phase): void { + this.queue.push(phase); + } + + /** + * Removes all phases from the queue + */ + public clear(): void { + this.queue.splice(0, this.queue.length); + } +} + +/** + * Priority Queue for {@linkcode PostSummonPhase} and {@linkcode PostSummonActivateAbilityPhase} + * + * Orders phases first by ability priority, then by the {@linkcode Pokemon}'s effective speed + */ +export class PostSummonPhasePriorityQueue extends PhasePriorityQueue { + protected override queue: PostSummonPhase[] = []; + + public override reorder(): void { + this.queue.sort((phaseA: PostSummonPhase, phaseB: PostSummonPhase) => { + if (phaseA.getPriority() === phaseB.getPriority()) { + return ( + (phaseB.getPokemon().getEffectiveStat(Stat.SPD) - phaseA.getPokemon().getEffectiveStat(Stat.SPD)) * + (isTrickRoom() ? -1 : 1) + ); + } + + return phaseB.getPriority() - phaseA.getPriority(); + }); + } + + public override push(phase: PostSummonPhase): void { + super.push(phase); + this.queueAbilityPhase(phase); + } + + /** + * Queues all necessary {@linkcode PostSummonActivateAbilityPhase}s for each pushed {@linkcode PostSummonPhase} + * @param phase The {@linkcode PostSummonPhase} that was pushed onto the queue + */ + private queueAbilityPhase(phase: PostSummonPhase): void { + const phasePokemon = phase.getPokemon(); + + phasePokemon.getAbilityPriorities().forEach((priority, idx) => { + this.queue.push(new PostSummonActivateAbilityPhase(phasePokemon.getBattlerIndex(), priority, !!idx)); + globalScene.phaseManager.appendToPhase( + new ActivatePriorityQueuePhase(DynamicPhaseType.POST_SUMMON), + "ActivatePriorityQueuePhase", + (p: ActivatePriorityQueuePhase) => p.getType() === DynamicPhaseType.POST_SUMMON, + ); + }); + } +} + +function isTrickRoom(): boolean { + const speedReversed = new BooleanHolder(false); + globalScene.arena.applyTags(TrickRoomTag, false, speedReversed); + return speedReversed.value; +} diff --git a/src/data/pokeball.ts b/src/data/pokeball.ts index 7a44ebdda7c..a479fd8068a 100644 --- a/src/data/pokeball.ts +++ b/src/data/pokeball.ts @@ -1,5 +1,4 @@ import { globalScene } from "#app/global-scene"; -import { CriticalCatchChanceBoosterModifier } from "#app/modifier/modifier"; import { NumberHolder } from "#app/utils/common"; import { PokeballType } from "#enums/pokeball"; import i18next from "i18next"; @@ -94,7 +93,7 @@ export function getCriticalCaptureChance(modifiedCatchRate: number): number { } const dexCount = globalScene.gameData.getSpeciesCount(d => !!d.caughtAttr); const catchingCharmMultiplier = new NumberHolder(1); - globalScene.findModifier(m => m instanceof CriticalCatchChanceBoosterModifier)?.apply(catchingCharmMultiplier); + globalScene.findModifier(m => m.is("CriticalCatchChanceBoosterModifier"))?.apply(catchingCharmMultiplier); const dexMultiplier = globalScene.gameMode.isDaily || dexCount > 800 ? 2.5 diff --git a/src/data/pokemon-forms/form-change-triggers.ts b/src/data/pokemon-forms/form-change-triggers.ts index eb2c0a557c2..3726781d9e3 100644 --- a/src/data/pokemon-forms/form-change-triggers.ts +++ b/src/data/pokemon-forms/form-change-triggers.ts @@ -1,5 +1,5 @@ import i18next from "i18next"; -import type { Constructor } from "#app/utils/common"; +import { coerceArray, type Constructor } from "#app/utils/common"; import type { TimeOfDay } from "#enums/time-of-day"; import type Pokemon from "#app/field/pokemon"; import type { SpeciesFormChange } from "#app/data/pokemon-forms"; @@ -125,10 +125,7 @@ export class SpeciesFormChangeStatusEffectTrigger extends SpeciesFormChangeTrigg constructor(statusEffects: StatusEffect | StatusEffect[], invert = false) { super(); - if (!Array.isArray(statusEffects)) { - statusEffects = [statusEffects]; - } - this.statusEffects = statusEffects; + this.statusEffects = coerceArray(statusEffects); this.invert = invert; // this.description = i18next.t("pokemonEvolutions:Forms.statusEffect"); } diff --git a/src/data/pokemon-species.ts b/src/data/pokemon-species.ts index 56dc649afac..8e7e029fd56 100644 --- a/src/data/pokemon-species.ts +++ b/src/data/pokemon-species.ts @@ -42,6 +42,8 @@ import { starterPassiveAbilities } from "#app/data/balance/passives"; import { loadPokemonVariantAssets } from "#app/sprites/pokemon-sprite"; import { hasExpSprite } from "#app/sprites/sprite-utils"; import { Gender } from "./gender"; +import { allSpecies } from "#app/data/data-lists"; +import { getPokemonSpecies } from "#app/utils/pokemon-utils"; export enum Region { NORMAL, @@ -82,23 +84,6 @@ export const normalForm: SpeciesId[] = [ 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 { const retSpecies: PokemonSpecies = species >= 2000 @@ -1448,8 +1433,6 @@ export function getPokerusStarters(): PokemonSpecies[] { return pokerusStarters; } -export const allSpecies: PokemonSpecies[] = []; - // biome-ignore format: manually formatted export function initSpecies() { allSpecies.push( diff --git a/src/data/trainers/trainer-config.ts b/src/data/trainers/trainer-config.ts index 2f1f7ed07a8..6786aa00ef7 100644 --- a/src/data/trainers/trainer-config.ts +++ b/src/data/trainers/trainer-config.ts @@ -1,11 +1,18 @@ import { globalScene } from "#app/global-scene"; -import { modifierTypes } from "#app/modifier/modifier-type"; +import { modifierTypes } from "../data-lists"; import { PokemonMove } from "../moves/pokemon-move"; -import { toReadableString, isNullOrUndefined, randSeedItem, randSeedInt, randSeedIntRange } from "#app/utils/common"; +import { + toReadableString, + isNullOrUndefined, + randSeedItem, + randSeedInt, + coerceArray, + randSeedIntRange, +} from "#app/utils/common"; 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 { doubleBattleDialogue } from "#app/data/dialogue"; +import { doubleBattleDialogue } from "../double-battle-dialogue"; import { TrainerVariant } from "#enums/trainer-variant"; import { getIsInitialized, initI18n } from "#app/plugins/i18n"; import i18next from "i18next"; @@ -37,7 +44,7 @@ import { timedEventManager } from "#app/global-event-manager"; // Type imports import type { PokemonSpeciesFilter } from "#app/data/pokemon-species"; import type PokemonSpecies from "#app/data/pokemon-species"; -import type { ModifierTypeFunc } from "#app/modifier/modifier-type"; +import type { ModifierTypeFunc } from "#app/@types/modifier-types"; import type { EnemyPokemon } from "#app/field/pokemon"; import type { EvilTeam } from "./evil-admin-trainer-pools"; import type { @@ -554,10 +561,7 @@ export class TrainerConfig { this.speciesPools = evilAdminTrainerPools[poolName]; signatureSpecies.forEach((speciesPool, s) => { - if (!Array.isArray(speciesPool)) { - speciesPool = [speciesPool]; - } - this.setPartyMemberFunc(-(s + 1), getRandomPartyMemberFunc(speciesPool)); + this.setPartyMemberFunc(-(s + 1), getRandomPartyMemberFunc(coerceArray(speciesPool))); }); const nameForCall = this.name.toLowerCase().replace(/\s/g, "_"); @@ -620,10 +624,7 @@ export class TrainerConfig { this.setPartyTemplates(trainerPartyTemplates.RIVAL_5); } signatureSpecies.forEach((speciesPool, s) => { - if (!Array.isArray(speciesPool)) { - speciesPool = [speciesPool]; - } - this.setPartyMemberFunc(-(s + 1), getRandomPartyMemberFunc(speciesPool)); + this.setPartyMemberFunc(-(s + 1), getRandomPartyMemberFunc(coerceArray(speciesPool))); }); if (!isNullOrUndefined(specialtyType)) { this.setSpeciesFilter(p => p.isOfType(specialtyType)); @@ -668,12 +669,8 @@ export class TrainerConfig { // Set up party members with their corresponding species. signatureSpecies.forEach((speciesPool, s) => { - // Ensure speciesPool is an array. - if (!Array.isArray(speciesPool)) { - speciesPool = [speciesPool]; - } // Set a function to get a random party member from the species pool. - this.setPartyMemberFunc(-(s + 1), getRandomPartyMemberFunc(speciesPool)); + this.setPartyMemberFunc(-(s + 1), getRandomPartyMemberFunc(coerceArray(speciesPool))); }); // If specialty type is provided, set species filter and specialty type. @@ -729,12 +726,8 @@ export class TrainerConfig { // Set up party members with their corresponding species. signatureSpecies.forEach((speciesPool, s) => { - // Ensure speciesPool is an array. - if (!Array.isArray(speciesPool)) { - speciesPool = [speciesPool]; - } // Set a function to get a random party member from the species pool. - this.setPartyMemberFunc(-(s + 1), getRandomPartyMemberFunc(speciesPool)); + this.setPartyMemberFunc(-(s + 1), getRandomPartyMemberFunc(coerceArray(speciesPool))); }); // Set species filter and specialty type if provided, otherwise filter by base total. diff --git a/src/data/weather.ts b/src/data/weather.ts index 822e5aa8303..425e15b12a8 100644 --- a/src/data/weather.ts +++ b/src/data/weather.ts @@ -5,12 +5,12 @@ import type Pokemon from "../field/pokemon"; import { PokemonType } from "#enums/pokemon-type"; import type Move from "./moves/move"; import { randSeedInt } from "#app/utils/common"; -import { SuppressWeatherEffectAbAttr } from "./abilities/ability"; import { TerrainType, getTerrainName } from "./terrain"; import i18next from "i18next"; import { globalScene } from "#app/global-scene"; import type { Arena } from "#app/field/arena"; import { timedEventManager } from "#app/global-event-manager"; +import type { SuppressWeatherEffectAbAttr } from "./abilities/ability"; export class Weather { public weatherType: WeatherType; @@ -108,10 +108,10 @@ export class Weather { for (const pokemon of field) { let suppressWeatherEffectAbAttr: SuppressWeatherEffectAbAttr | null = pokemon .getAbility() - .getAttrs(SuppressWeatherEffectAbAttr)[0]; + .getAttrs("SuppressWeatherEffectAbAttr")[0]; if (!suppressWeatherEffectAbAttr) { suppressWeatherEffectAbAttr = pokemon.hasPassive() - ? pokemon.getPassiveAbility().getAttrs(SuppressWeatherEffectAbAttr)[0] + ? pokemon.getPassiveAbility().getAttrs("SuppressWeatherEffectAbAttr")[0] : null; } if (suppressWeatherEffectAbAttr && (!this.isImmutable() || suppressWeatherEffectAbAttr.affectsImmutable)) { diff --git a/src/enums/battler-tag-lapse-type.ts b/src/enums/battler-tag-lapse-type.ts index 355a084148b..4375e87e4e0 100644 --- a/src/enums/battler-tag-lapse-type.ts +++ b/src/enums/battler-tag-lapse-type.ts @@ -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 { + // TODO: This is unused... FAINT, + /** + * Tag activate before the holder uses a non-virtual move, possibly interrupting its action. + * @see MoveUseMode for more information + */ MOVE, + /** Tag activates before the holder uses **any** move, triggering effects or interrupting its action. */ PRE_MOVE, + /** Tag activates immediately after the holder's move finishes triggering (successful or not). */ AFTER_MOVE, + /** + * Tag activates before move effects are applied. + * TODO: Stop using this as a catch-all "semi-invulnerability" tag + */ MOVE_EFFECT, + /** Tag activates at the end of the turn. */ 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, - /** 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, - CUSTOM + /** The tag has some other custom activation or removal condition. */ + CUSTOM, } diff --git a/src/enums/dynamic-phase-type.ts b/src/enums/dynamic-phase-type.ts new file mode 100644 index 00000000000..a34ac371668 --- /dev/null +++ b/src/enums/dynamic-phase-type.ts @@ -0,0 +1,6 @@ +/** + * Enum representation of the phase types held by implementations of {@linkcode PhasePriorityQueue} + */ +export enum DynamicPhaseType { + POST_SUMMON +} diff --git a/src/enums/modifier-pool-type.ts b/src/enums/modifier-pool-type.ts new file mode 100644 index 00000000000..0d2b92ba80d --- /dev/null +++ b/src/enums/modifier-pool-type.ts @@ -0,0 +1,7 @@ +export enum ModifierPoolType { + PLAYER, + WILD, + TRAINER, + ENEMY_BUFF, + DAILY_STARTER +} diff --git a/src/modifier/modifier-tier.ts b/src/enums/modifier-tier.ts similarity index 100% rename from src/modifier/modifier-tier.ts rename to src/enums/modifier-tier.ts diff --git a/src/enums/move-use-mode.ts b/src/enums/move-use-mode.ts new file mode 100644 index 00000000000..31694ad4081 --- /dev/null +++ b/src/enums/move-use-mode.ts @@ -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; +} diff --git a/src/enums/unlockables.ts b/src/enums/unlockables.ts new file mode 100644 index 00000000000..77b39a17e90 --- /dev/null +++ b/src/enums/unlockables.ts @@ -0,0 +1,7 @@ + +export enum Unlockables { + ENDLESS_MODE, + MINI_BLACK_HOLE, + SPLICED_ENDLESS_MODE, + EVIOLITE +} diff --git a/src/field/arena.ts b/src/field/arena.ts index fffbf8dfa26..8d7e5037852 100644 --- a/src/field/arena.ts +++ b/src/field/arena.ts @@ -3,7 +3,7 @@ import type { BiomeTierTrainerPools, PokemonPools } from "#app/data/balance/biom import { biomePokemonPools, BiomePoolTier, biomeTrainerPools } from "#app/data/balance/biomes"; import { randSeedInt, NumberHolder, isNullOrUndefined, type Constructor } from "#app/utils/common"; import type PokemonSpecies from "#app/data/pokemon-species"; -import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { getPokemonSpecies } from "#app/utils/pokemon-utils"; import { getTerrainClearMessage, getTerrainStartMessage, @@ -24,10 +24,7 @@ import { applyAbAttrs, applyPostTerrainChangeAbAttrs, applyPostWeatherChangeAbAttrs, - PostTerrainChangeAbAttr, - PostWeatherChangeAbAttr, - TerrainEventTypeChangeAbAttr, -} from "#app/data/abilities/ability"; +} from "#app/data/abilities/apply-ab-attrs"; import type Pokemon from "#app/field/pokemon"; import Overrides from "#app/overrides"; import { TagAddedEvent, TagRemovedEvent, TerrainChangedEvent, WeatherChangedEvent } from "#app/events/arena"; @@ -265,7 +262,7 @@ export class Arena { return 5; } break; - case SpeciesId.LYCANROC: + case SpeciesId.LYCANROC: { const timeOfDay = this.getTimeOfDay(); switch (timeOfDay) { case TimeOfDay.DAY: @@ -277,6 +274,7 @@ export class Arena { return 1; } break; + } } return 0; @@ -374,7 +372,7 @@ export class Arena { pokemon.findAndRemoveTags( t => "weatherTypes" in t && !(t.weatherTypes as WeatherType[]).find(t => t === weather), ); - applyPostWeatherChangeAbAttrs(PostWeatherChangeAbAttr, pokemon, weather); + applyPostWeatherChangeAbAttrs("PostWeatherChangeAbAttr", pokemon, weather); }); return true; @@ -463,8 +461,8 @@ export class Arena { pokemon.findAndRemoveTags( t => "terrainTypes" in t && !(t.terrainTypes as TerrainType[]).find(t => t === terrain), ); - applyPostTerrainChangeAbAttrs(PostTerrainChangeAbAttr, pokemon, terrain); - applyAbAttrs(TerrainEventTypeChangeAbAttr, pokemon, null, false); + applyPostTerrainChangeAbAttrs("PostTerrainChangeAbAttr", pokemon, terrain); + applyAbAttrs("TerrainEventTypeChangeAbAttr", pokemon, null, false); }); return true; @@ -767,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 * @param tagPredicate a function mapping {@linkcode ArenaTag}s to `boolean`s diff --git a/src/field/pokemon-sprite-sparkle-handler.ts b/src/field/pokemon-sprite-sparkle-handler.ts index cceb0bd7717..bda9414bb92 100644 --- a/src/field/pokemon-sprite-sparkle-handler.ts +++ b/src/field/pokemon-sprite-sparkle-handler.ts @@ -1,6 +1,6 @@ import { globalScene } from "#app/global-scene"; import Pokemon from "./pokemon"; -import { fixedInt, randInt } from "#app/utils/common"; +import { fixedInt, coerceArray, randInt } from "#app/utils/common"; export default class PokemonSpriteSparkleHandler { private sprites: Set; @@ -57,9 +57,7 @@ export default class PokemonSpriteSparkleHandler { } add(sprites: Phaser.GameObjects.Sprite | Phaser.GameObjects.Sprite[]): void { - if (!Array.isArray(sprites)) { - sprites = [sprites]; - } + sprites = coerceArray(sprites); for (const s of sprites) { if (this.sprites.has(s)) { continue; @@ -69,9 +67,7 @@ export default class PokemonSpriteSparkleHandler { } remove(sprites: Phaser.GameObjects.Sprite | Phaser.GameObjects.Sprite[]): void { - if (!Array.isArray(sprites)) { - sprites = [sprites]; - } + sprites = coerceArray(sprites); for (const s of sprites) { this.sprites.delete(s); } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 6d522ebcf6b..c645af4abb8 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -15,12 +15,8 @@ import { allMoves } from "#app/data/data-lists"; import { MoveTarget } from "#enums/MoveTarget"; import { MoveCategory } from "#enums/MoveCategory"; import type { PokemonSpeciesForm } from "#app/data/pokemon-species"; -import { - default as PokemonSpecies, - getFusedSpeciesName, - getPokemonSpecies, - getPokemonSpeciesForm, -} from "#app/data/pokemon-species"; +import { default as PokemonSpecies, getFusedSpeciesName, getPokemonSpeciesForm } from "#app/data/pokemon-species"; +import { getPokemonSpecies } from "#app/utils/pokemon-utils"; import { getStarterValueFriendshipCap, speciesStarterCosts } from "#app/data/balance/starters"; import { NumberHolder, @@ -38,9 +34,9 @@ import { deltaRgb, isBetween, randSeedFloat, - type nil, type Constructor, randSeedIntRange, + coerceArray, } from "#app/utils/common"; import type { TypeDamageMultiplier } from "#app/data/type"; import { getTypeDamageMultiplier, getTypeRgb } from "#app/data/type"; @@ -79,11 +75,12 @@ import { import { PokeballType } from "#enums/pokeball"; import { Gender } from "#app/data/gender"; 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 { pokemonEvolutions, pokemonPrevolutions, FusionSpeciesFormEvolution, + validateShedinjaEvo, } from "#app/data/balance/pokemon-evolutions"; import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "#app/data/balance/tms"; import { @@ -111,61 +108,23 @@ import { WeatherType } from "#enums/weather-type"; import { NoCritTag, WeakenMoveScreenTag } from "#app/data/arena-tag"; import { ArenaTagSide } from "#enums/arena-tag-side"; import type { SuppressAbilitiesTag } from "#app/data/arena-tag"; -import type { Ability } from "#app/data/abilities/ability-class"; -import type { AbAttr } from "#app/data/abilities/ab-attrs/ab-attr"; +import type { Ability } from "#app/data/abilities/ability"; import { - StatMultiplierAbAttr, - BlockCritAbAttr, - BonusCritAbAttr, - BypassBurnDamageReductionAbAttr, - FieldPriorityMoveImmunityAbAttr, - IgnoreOpponentStatStagesAbAttr, - MoveImmunityAbAttr, - PreDefendFullHpEndureAbAttr, - ReceivedMoveDamageMultiplierAbAttr, - StabBoostAbAttr, - StatusEffectImmunityAbAttr, - TypeImmunityAbAttr, - WeightMultiplierAbAttr, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, - NoFusionAbilityAbAttr, - MultCritAbAttr, - IgnoreTypeImmunityAbAttr, - DamageBoostAbAttr, - IgnoreTypeStatusEffectImmunityAbAttr, - ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, - FieldMultiplyStatAbAttr, - AddSecondStrikeAbAttr, - UserFieldStatusEffectImmunityAbAttr, - UserFieldBattlerTagImmunityAbAttr, - BattlerTagImmunityAbAttr, - MoveTypeChangeAbAttr, - FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, - CheckTrappedAbAttr, - InfiltratorAbAttr, - AlliedFieldDamageReductionAbAttr, - PostDamageAbAttr, applyPostDamageAbAttrs, - CommanderAbAttr, applyPostItemLostAbAttrs, - PostItemLostAbAttr, applyOnGainAbAttrs, - PreLeaveFieldAbAttr, applyPreLeaveFieldAbAttrs, applyOnLoseAbAttrs, - PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr, applyAllyStatMultiplierAbAttrs, - AllyStatMultiplierAbAttr, - MoveAbilityBypassAbAttr, - PreSummonAbAttr, -} from "#app/data/abilities/ability"; +} from "#app/data/abilities/apply-ab-attrs"; import { allAbilities } from "#app/data/data-lists"; import type PokemonData from "#app/system/pokemon-data"; import { BattlerIndex } from "#enums/battler-index"; @@ -192,7 +151,7 @@ import type { TrainerSlot } from "#enums/trainer-slot"; import Overrides from "#app/overrides"; import i18next from "i18next"; import { speciesEggMoves } from "#app/data/balance/egg-moves"; -import { ModifierTier } from "#app/modifier/modifier-tier"; +import { ModifierTier } from "#enums/modifier-tier"; import { applyChallenges } from "#app/data/challenge"; import { ChallengeType } from "#enums/challenge-type"; import { AbilityId } from "#enums/ability-id"; @@ -223,12 +182,14 @@ import { doShinySparkleAnim } from "#app/field/anims"; import { MoveFlags } from "#enums/MoveFlags"; import { timedEventManager } from "#app/global-event-manager"; import { loadMoveAnimations } from "#app/sprites/pokemon-asset-loader"; +import { isVirtual, isIgnorePP, MoveUseMode } from "#enums/move-use-mode"; import { FieldPosition } from "#enums/field-position"; import { LearnMoveSituation } from "#enums/learn-move-situation"; import { HitResult } from "#enums/hit-result"; import { AiType } from "#enums/ai-type"; import type { MoveResult } from "#enums/move-result"; import { PokemonMove } from "#app/data/moves/pokemon-move"; +import type { AbAttrMap, AbAttrString } from "#app/@types/ability-types"; /** Base typeclass for damage parameter methods, used for DRY */ type damageParams = { @@ -360,7 +321,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { super(globalScene, x, y); 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; @@ -406,7 +367,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.metWave = dataSource.metWave ?? (this.metBiome === -1 ? -1 : 0); this.pauseEvolutions = dataSource.pauseEvolutions; this.pokerus = !!dataSource.pokerus; - this.evoCounter = dataSource.evoCounter ?? 0; this.fusionSpecies = dataSource.fusionSpecies instanceof PokemonSpecies ? dataSource.fusionSpecies @@ -1392,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 - * the given source + * Calculate the critical-hit stage of a move used **against** this pokemon by + * the given source. + * * @param source - The {@linkcode Pokemon} using the move * @param move - The {@linkcode Move} being used * @returns The final critical-hit stage value @@ -1403,14 +1364,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { applyMoveAttrs("HighCritAttr", source, this, move, critStage); globalScene.applyModifiers(CritBoosterModifier, source.isPlayer(), source, critStage); globalScene.applyModifiers(TempCritBoosterModifier, source.isPlayer(), critStage); - applyAbAttrs(BonusCritAbAttr, source, null, false, critStage); + applyAbAttrs("BonusCritAbAttr", source, null, false, critStage); const critBoostTag = source.getTag(CritBoostTag); if (critBoostTag) { - if (critBoostTag instanceof DragonCheerTag) { - critStage.value += critBoostTag.typesOnAdd.includes(PokemonType.DRAGON) ? 2 : 1; - } else { - critStage.value += 2; - } + // Dragon cheer only gives +1 crit stage to non-dragon types + critStage.value += + critBoostTag instanceof DragonCheerTag && !critBoostTag.typesOnAdd.includes(PokemonType.DRAGON) ? 1 : 2; } console.log(`crit stage: +${critStage.value}`); @@ -1464,19 +1423,27 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { // The Ruin abilities here are never ignored, but they reveal themselves on summon anyway const fieldApplied = new BooleanHolder(false); for (const pokemon of globalScene.getField(true)) { - applyFieldStatMultiplierAbAttrs(FieldMultiplyStatAbAttr, pokemon, stat, statValue, this, fieldApplied, simulated); + applyFieldStatMultiplierAbAttrs( + "FieldMultiplyStatAbAttr", + pokemon, + stat, + statValue, + this, + fieldApplied, + simulated, + ); if (fieldApplied.value) { break; } } if (!ignoreAbility) { - applyStatMultiplierAbAttrs(StatMultiplierAbAttr, this, stat, statValue, simulated); + applyStatMultiplierAbAttrs("StatMultiplierAbAttr", this, stat, statValue, simulated); } const ally = this.getAlly(); if (!isNullOrUndefined(ally)) { applyAllyStatMultiplierAbAttrs( - AllyStatMultiplierAbAttr, + "AllyStatMultiplierAbAttr", ally, stat, statValue, @@ -1803,9 +1770,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { let overrideArray: MoveId | Array = this.isPlayer() ? Overrides.MOVESET_OVERRIDE : Overrides.OPP_MOVESET_OVERRIDE; - if (!Array.isArray(overrideArray)) { - overrideArray = [overrideArray]; - } + overrideArray = coerceArray(overrideArray); if (overrideArray.length > 0) { if (!this.isPlayer()) { this.moveset = []; @@ -2059,15 +2024,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @param ignoreOverride - Whether to ignore ability changing effects; Default `false` * @returns An array of all the ability attributes on this ability. */ - public getAbilityAttrs( - attrType: { new (...args: any[]): T }, - canApply = true, - ignoreOverride = false, - ): T[] { - const abilityAttrs: T[] = []; + public getAbilityAttrs(attrType: T, canApply = true, ignoreOverride = false): AbAttrMap[T][] { + const abilityAttrs: AbAttrMap[T][] = []; if (!canApply || this.canApplyAbility()) { - abilityAttrs.push(...this.getAbility(ignoreOverride).getAttrs(attrType)); + abilityAttrs.push(...this.getAbility(ignoreOverride).getAttrs(attrType)); } if (!canApply || this.canApplyAbility(true)) { @@ -2152,7 +2113,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return false; } const ability = !passive ? this.getAbility() : this.getPassiveAbility(); - if (this.isFusion() && ability.hasAttr(NoFusionAbilityAbAttr)) { + if (this.isFusion() && ability.hasAttr("NoFusionAbilityAbAttr")) { return false; } const arena = globalScene?.arena; @@ -2163,10 +2124,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return false; } const suppressAbilitiesTag = arena.getTag(ArenaTagType.NEUTRALIZING_GAS) as SuppressAbilitiesTag; - const suppressOffField = ability.hasAttr(PreSummonAbAttr); + const suppressOffField = ability.hasAttr("PreSummonAbAttr"); if ((this.isOnField() || suppressOffField) && suppressAbilitiesTag && !suppressAbilitiesTag.isBeingRemoved()) { - const thisAbilitySuppressing = ability.hasAttr(PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr); - const hasSuppressingAbility = this.hasAbilityWithAttr(PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr, false); + const thisAbilitySuppressing = ability.hasAttr("PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr"); + const hasSuppressingAbility = this.hasAbilityWithAttr("PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr", false); // Neutralizing gas is up - suppress abilities unless they are unsuppressable or this pokemon is responsible for the gas // (Balance decided that the other ability of a neutralizing gas pokemon should not be neutralized) // If the ability itself is neutralizing gas, don't suppress it (handled through arena tag) @@ -2207,13 +2168,17 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @param ignoreOverride Whether to ignore ability changing effects; default `false` * @returns `true` if an ability with the given {@linkcode AbAttr} is present and active */ - public hasAbilityWithAttr(attrType: Constructor, canApply = true, ignoreOverride = false): boolean { + public hasAbilityWithAttr(attrType: AbAttrString, canApply = true, ignoreOverride = false): boolean { if ((!canApply || this.canApplyAbility()) && this.getAbility(ignoreOverride).hasAttr(attrType)) { return true; } return this.hasPassive() && (!canApply || this.canApplyAbility(true)) && this.getPassiveAbility().hasAttr(attrType); } + public getAbilityPriorities(): [number, number] { + return [this.getAbility().postSummonPriority, this.getPassiveAbility().postSummonPriority]; + } + /** * Gets the weight of the Pokemon with subtractive modifiers (Autotomize) happening first * and then multiplicative modifiers happening after (Heavy Metal and Light Metal) @@ -2229,7 +2194,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const weight = new NumberHolder(this.species.weight - weightRemoved); // This will trigger the ability overlay so only call this function when necessary - applyAbAttrs(WeightMultiplierAbAttr, this, null, false, weight); + applyAbAttrs("WeightMultiplierAbAttr", this, null, false, weight); return Math.max(minWeight, weight.value); } @@ -2316,7 +2281,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const opposingField = opposingFieldUnfiltered.filter(enemyPkm => enemyPkm.switchOutStatus === false); for (const opponent of opposingField) { - applyCheckTrappedAbAttrs(CheckTrappedAbAttr, opponent, trappedByAbility, this, trappedAbMessages, simulated); + applyCheckTrappedAbAttrs("CheckTrappedAbAttr", opponent, trappedByAbility, this, trappedAbMessages, simulated); } const side = this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; @@ -2338,7 +2303,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const moveTypeHolder = new NumberHolder(move.type); applyMoveAttrs("VariableMoveTypeAttr", this, null, move, moveTypeHolder); - applyPreAttackAbAttrs(MoveTypeChangeAbAttr, this, null, move, simulated, moveTypeHolder); + applyPreAttackAbAttrs("MoveTypeChangeAbAttr", this, null, move, simulated, moveTypeHolder); // If the user is terastallized and the move is tera blast, or tera starstorm that is stellar type, // then bypass the check for ion deluge and electrify @@ -2403,16 +2368,16 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const cancelledHolder = cancelled ?? new BooleanHolder(false); if (!ignoreAbility) { - applyPreDefendAbAttrs(TypeImmunityAbAttr, this, source, move, cancelledHolder, simulated, typeMultiplier); + applyPreDefendAbAttrs("TypeImmunityAbAttr", this, source, move, cancelledHolder, simulated, typeMultiplier); if (!cancelledHolder.value) { - applyPreDefendAbAttrs(MoveImmunityAbAttr, this, source, move, cancelledHolder, simulated, typeMultiplier); + applyPreDefendAbAttrs("MoveImmunityAbAttr", this, source, move, cancelledHolder, simulated, typeMultiplier); } if (!cancelledHolder.value) { const defendingSidePlayField = this.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField(); defendingSidePlayField.forEach(p => - applyPreDefendAbAttrs(FieldPriorityMoveImmunityAbAttr, p, source, move, cancelledHolder), + applyPreDefendAbAttrs("FieldPriorityMoveImmunityAbAttr", p, source, move, cancelledHolder), ); } } @@ -2427,7 +2392,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { // Apply Tera Shell's effect to attacks after all immunities are accounted for if (!ignoreAbility && move.category !== MoveCategory.STATUS) { - applyPreDefendAbAttrs(FullHpResistTypeAbAttr, this, source, move, cancelledHolder, simulated, typeMultiplier); + applyPreDefendAbAttrs("FullHpResistTypeAbAttr", this, source, move, cancelledHolder, simulated, typeMultiplier); } if (move.category === MoveCategory.STATUS && move.hitsSubstitute(source, this)) { @@ -2480,8 +2445,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } if (source) { const ignoreImmunity = new BooleanHolder(false); - if (source.isActive(true) && source.hasAbilityWithAttr(IgnoreTypeImmunityAbAttr)) { - applyAbAttrs(IgnoreTypeImmunityAbAttr, source, ignoreImmunity, simulated, moveType, defType); + if (source.isActive(true) && source.hasAbilityWithAttr("IgnoreTypeImmunityAbAttr")) { + applyAbAttrs("IgnoreTypeImmunityAbAttr", source, ignoreImmunity, simulated, moveType, defType); } if (ignoreImmunity.value) { if (multiplier.value === 0) { @@ -2566,14 +2531,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (pokemonEvolutions.hasOwnProperty(this.species.speciesId)) { const evolutions = pokemonEvolutions[this.species.speciesId]; for (const e of evolutions) { - if ( - !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; - } + if (e.validate(this)) { + return e; } } } @@ -2583,14 +2542,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { e => new FusionSpeciesFormEvolution(this.species.speciesId, e), ); for (const fe of fusionEvolutions) { - if ( - !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; - } + if (fe.validate(this)) { + return fe; } } } @@ -2832,17 +2785,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { */ public trySetShinySeed(thresholdOverride?: number, applyModifiersToOverride?: boolean): boolean { if (!this.shiny) { - const shinyThreshold = new NumberHolder(BASE_SHINY_CHANCE); - if (thresholdOverride === undefined || applyModifiersToOverride) { - if (thresholdOverride !== undefined && applyModifiersToOverride) { - shinyThreshold.value = thresholdOverride; - } + const shinyThreshold = new NumberHolder(thresholdOverride ?? BASE_SHINY_CHANCE); + if (applyModifiersToOverride) { if (timedEventManager.isEventActive()) { shinyThreshold.value *= timedEventManager.getShinyMultiplier(); } globalScene.applyModifiers(ShinyRateBoosterModifier, true, shinyThreshold); - } else { - shinyThreshold.value = thresholdOverride; } this.shiny = randSeedInt(65536) < shinyThreshold.value; @@ -2911,16 +2859,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (!this.species.abilityHidden) { return false; } - const haThreshold = new NumberHolder(BASE_HIDDEN_ABILITY_CHANCE); - if (thresholdOverride === undefined || applyModifiersToOverride) { - if (thresholdOverride !== undefined && applyModifiersToOverride) { - haThreshold.value = thresholdOverride; - } + const haThreshold = new NumberHolder(thresholdOverride ?? BASE_HIDDEN_ABILITY_CHANCE); + if (applyModifiersToOverride) { if (!this.hasTrainer()) { globalScene.applyModifiers(HiddenAbilityRateBoosterModifier, true, haThreshold); } - } else { - haThreshold.value = thresholdOverride; } if (randSeedInt(65536) < haThreshold.value) { @@ -3184,7 +3127,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { while (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) { @@ -3237,7 +3180,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { while (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 @@ -3432,7 +3375,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } } if (!ignoreOppAbility) { - applyAbAttrs(IgnoreOpponentStatStagesAbAttr, opponent, null, simulated, stat, ignoreStatStage); + applyAbAttrs("IgnoreOpponentStatStagesAbAttr", opponent, null, simulated, stat, ignoreStatStage); } if (move) { applyMoveAttrs("IgnoreOpponentStatStagesAttr", this, opponent, move, ignoreStatStage); @@ -3471,8 +3414,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const ignoreAccStatStage = new BooleanHolder(false); const ignoreEvaStatStage = new BooleanHolder(false); - applyAbAttrs(IgnoreOpponentStatStagesAbAttr, target, null, false, Stat.ACC, ignoreAccStatStage); - applyAbAttrs(IgnoreOpponentStatStagesAbAttr, this, null, false, Stat.EVA, ignoreEvaStatStage); + applyAbAttrs("IgnoreOpponentStatStagesAbAttr", target, null, false, Stat.ACC, ignoreAccStatStage); + applyAbAttrs("IgnoreOpponentStatStagesAbAttr", this, null, false, Stat.EVA, ignoreEvaStatStage); applyMoveAttrs("IgnoreOpponentStatStagesAttr", this, target, sourceMove, ignoreEvaStatStage); globalScene.applyModifiers(TempStatStageBoosterModifier, this.isPlayer(), Stat.ACC, userAccStage); @@ -3492,16 +3435,33 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { : 3 / (3 + Math.min(targetEvaStage.value - userAccStage.value, 6)); } - applyStatMultiplierAbAttrs(StatMultiplierAbAttr, this, Stat.ACC, accuracyMultiplier, false, sourceMove); + applyStatMultiplierAbAttrs("StatMultiplierAbAttr", this, Stat.ACC, accuracyMultiplier, false, sourceMove); const evasionMultiplier = new NumberHolder(1); - applyStatMultiplierAbAttrs(StatMultiplierAbAttr, target, Stat.EVA, evasionMultiplier); + applyStatMultiplierAbAttrs("StatMultiplierAbAttr", target, Stat.EVA, evasionMultiplier); const ally = this.getAlly(); if (!isNullOrUndefined(ally)) { - const ignore = this.hasAbilityWithAttr(MoveAbilityBypassAbAttr) || sourceMove.hasFlag(MoveFlags.IGNORE_ABILITIES); - applyAllyStatMultiplierAbAttrs(AllyStatMultiplierAbAttr, ally, Stat.ACC, accuracyMultiplier, false, this, ignore); - applyAllyStatMultiplierAbAttrs(AllyStatMultiplierAbAttr, ally, Stat.EVA, evasionMultiplier, false, this, ignore); + const ignore = + this.hasAbilityWithAttr("MoveAbilityBypassAbAttr") || sourceMove.hasFlag(MoveFlags.IGNORE_ABILITIES); + applyAllyStatMultiplierAbAttrs( + "AllyStatMultiplierAbAttr", + ally, + Stat.ACC, + accuracyMultiplier, + false, + this, + ignore, + ); + applyAllyStatMultiplierAbAttrs( + "AllyStatMultiplierAbAttr", + ally, + Stat.EVA, + evasionMultiplier, + false, + this, + ignore, + ); } return accuracyMultiplier.value / evasionMultiplier.value; @@ -3616,7 +3576,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { applyMoveAttrs("CombinedPledgeStabBoostAttr", source, this, move, stabMultiplier); if (!ignoreSourceAbility) { - applyAbAttrs(StabBoostAbAttr, source, null, simulated, stabMultiplier); + applyAbAttrs("StabBoostAbAttr", source, null, simulated, stabMultiplier); } if (source.isTerastallized && sourceTeraType === moveType && moveType !== PokemonType.STELLAR) { @@ -3765,7 +3725,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { ); if (!ignoreSourceAbility) { applyPreAttackAbAttrs( - AddSecondStrikeAbAttr, + "AddSecondStrikeAbAttr", source, this, move, @@ -3783,7 +3743,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { /** The damage multiplier when the given move critically hits */ const criticalMultiplier = new NumberHolder(isCritical ? 1.5 : 1); - applyAbAttrs(MultCritAbAttr, source, null, simulated, criticalMultiplier); + applyAbAttrs("MultCritAbAttr", source, null, simulated, criticalMultiplier); /** * A multiplier for random damage spread in the range [0.85, 1] @@ -3804,7 +3764,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { ) { const burnDamageReductionCancelled = new BooleanHolder(false); if (!ignoreSourceAbility) { - applyAbAttrs(BypassBurnDamageReductionAbAttr, source, burnDamageReductionCancelled, simulated); + applyAbAttrs("BypassBurnDamageReductionAbAttr", source, burnDamageReductionCancelled, simulated); } if (!burnDamageReductionCancelled.value) { burnMultiplier = 0.5; @@ -3868,7 +3828,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { /** Doubles damage if the attacker has Tinted Lens and is using a resisted move */ if (!ignoreSourceAbility) { - applyPreAttackAbAttrs(DamageBoostAbAttr, source, this, move, simulated, damage); + applyPreAttackAbAttrs("DamageBoostAbAttr", source, this, move, simulated, damage); } /** Apply the enemy's Damage and Resistance tokens */ @@ -3881,12 +3841,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { /** Apply this Pokemon's post-calc defensive modifiers (e.g. Fur Coat) */ if (!ignoreAbility) { - applyPreDefendAbAttrs(ReceivedMoveDamageMultiplierAbAttr, this, source, move, cancelled, simulated, damage); + applyPreDefendAbAttrs("ReceivedMoveDamageMultiplierAbAttr", this, source, move, cancelled, simulated, damage); const ally = this.getAlly(); /** Additionally apply friend guard damage reduction if ally has it. */ if (globalScene.currentBattle.double && !isNullOrUndefined(ally) && ally.isActive(true)) { - applyPreDefendAbAttrs(AlliedFieldDamageReductionAbAttr, ally, source, move, cancelled, simulated, damage); + applyPreDefendAbAttrs("AlliedFieldDamageReductionAbAttr", ally, source, move, cancelled, simulated, damage); } } @@ -3894,7 +3854,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { applyMoveAttrs("ModifiedDamageAttr", source, this, move, damage); if (this.isFullHp() && !ignoreAbility) { - applyPreDefendAbAttrs(PreDefendFullHpEndureAbAttr, this, source, move, cancelled, false, damage); + applyPreDefendAbAttrs("PreDefendFullHpEndureAbAttr", this, source, move, cancelled, false, damage); } // debug message for when damage is applied (i.e. not simulated) @@ -3918,33 +3878,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 move - The {@linkcode Move} being used - * @param simulated - If `true`, suppresses changes to game state during calculation (defaults to `true`) - * @returns whether the move critically hits the pokemon + * @returns Whether the move will critically hit the defender. */ - getCriticalHitResult(source: Pokemon, move: Move, simulated = true): boolean { - const defendingSide = this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; - const noCritTag = globalScene.arena.getTagOnSide(NoCritTag, defendingSide); - if (noCritTag || Overrides.NEVER_CRIT_OVERRIDE || move.hasAttr("FixedDamageAttr")) { + getCriticalHitResult(source: Pokemon, move: Move): boolean { + if (move.hasAttr("FixedDamageAttr")) { + // fixed damage moves (Dragon Rage, etc.) will nevet crit return false; } - const isCritical = new BooleanHolder(false); - if (source.getTag(BattlerTagType.ALWAYS_CRIT)) { - isCritical.value = true; - } - applyMoveAttrs("CritOnlyAttr", source, this, move, isCritical); - applyAbAttrs(ConditionalCritAbAttr, source, null, simulated, isCritical, this, move); - 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); - } + const alwaysCrit = new BooleanHolder(false); + applyMoveAttrs("CritOnlyAttr", source, this, move, alwaysCrit); + applyAbAttrs("ConditionalCritAbAttr", source, null, false, alwaysCrit, this, move); + const alwaysCritTag = !!source.getTag(BattlerTagType.ALWAYS_CRIT); + const critChance = [24, 8, 2, 1][Phaser.Math.Clamp(this.getCritStage(source, move), 0, 3)]; - 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; } /** @@ -4049,7 +4015,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * Multi-hits are handled in move-effect-phase.ts for PostDamageAbAttr */ if (!source || source.turnData.hitCount <= 1) { - applyPostDamageAbAttrs(PostDamageAbAttr, this, damage, this.hasPassive(), false, [], source); + applyPostDamageAbAttrs("PostDamageAbAttr", this, damage, this.hasPassive(), false, [], source); } return damage; } @@ -4097,11 +4063,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const stubTag = new BattlerTag(tagType, 0, 0); const cancelled = new BooleanHolder(false); - applyPreApplyBattlerTagAbAttrs(BattlerTagImmunityAbAttr, this, stubTag, cancelled, true); + applyPreApplyBattlerTagAbAttrs("BattlerTagImmunityAbAttr", this, stubTag, cancelled, true); const userField = this.getAlliedField(); userField.forEach(pokemon => - applyPreApplyBattlerTagAbAttrs(UserFieldBattlerTagImmunityAbAttr, pokemon, stubTag, cancelled, true, this), + applyPreApplyBattlerTagAbAttrs("UserFieldBattlerTagImmunityAbAttr", pokemon, stubTag, cancelled, true, this), ); return !cancelled.value; @@ -4117,13 +4083,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const newTag = getBattlerTag(tagType, turnCount, sourceMove!, sourceId!); // TODO: are the bangs correct? const cancelled = new BooleanHolder(false); - applyPreApplyBattlerTagAbAttrs(BattlerTagImmunityAbAttr, this, newTag, cancelled); + applyPreApplyBattlerTagAbAttrs("BattlerTagImmunityAbAttr", this, newTag, cancelled); if (cancelled.value) { return false; } for (const pokemon of this.getAlliedField()) { - applyPreApplyBattlerTagAbAttrs(UserFieldBattlerTagImmunityAbAttr, pokemon, newTag, cancelled, false, this); + applyPreApplyBattlerTagAbAttrs("UserFieldBattlerTagImmunityAbAttr", pokemon, newTag, cancelled, false, this); if (cancelled.value) { return false; } @@ -4139,7 +4105,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } /**@overload */ - getTag(tagType: BattlerTagType.GRUDGE): GrudgeTag | nil; + getTag(tagType: BattlerTagType.GRUDGE): GrudgeTag | undefined; + + /** @overload */ + getTag(tagType: BattlerTagType.SUBSTITUTE): SubstituteTag | undefined; /** @overload */ getTag(tagType: BattlerTagType): BattlerTag | undefined; @@ -4353,10 +4322,41 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { 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[] { 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 { return new Promise(resolve => { this.formIndex = Math.max( @@ -4412,14 +4412,16 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { // biome-ignore lint: there are a ton of issues.. faintCry(callback: Function): void { if (this.fusionSpecies && this.getSpeciesForm() !== this.getFusionSpeciesForm()) { - return this.fusionFaintCry(callback); + this.fusionFaintCry(callback); + return; } const key = this.species.getCryKey(this.formIndex); let rate = 0.85; const cry = globalScene.playSound(key, { rate: rate }) as AnySound; if (!cry || globalScene.fieldVolume === 0) { - return callback(); + callback(); + return; } const sprite = this.getSprite(); const tintSprite = this.getTintSprite(); @@ -4487,7 +4489,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { rate: rate, }) as AnySound; if (!cry || !fusionCry || globalScene.fieldVolume === 0) { - return callback(); + callback(); + return; } fusionCry.stop(); duration = Math.min(duration, fusionCry.totalDuration * 1000); @@ -4643,7 +4646,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { // Check if the source Pokemon has an ability that cancels the Poison/Toxic immunity const cancelImmunity = new BooleanHolder(false); if (sourcePokemon) { - applyAbAttrs(IgnoreTypeStatusEffectImmunityAbAttr, sourcePokemon, cancelImmunity, false, effect, defType); + applyAbAttrs("IgnoreTypeStatusEffectImmunityAbAttr", sourcePokemon, cancelImmunity, false, effect, defType); if (cancelImmunity.value) { return false; } @@ -4692,14 +4695,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } const cancelled = new BooleanHolder(false); - applyPreSetStatusAbAttrs(StatusEffectImmunityAbAttr, this, effect, cancelled, quiet); + applyPreSetStatusAbAttrs("StatusEffectImmunityAbAttr", this, effect, cancelled, quiet); if (cancelled.value) { return false; } for (const pokemon of this.getAlliedField()) { applyPreSetStatusAbAttrs( - UserFieldStatusEffectImmunityAbAttr, + "UserFieldStatusEffectImmunityAbAttr", pokemon, effect, cancelled, @@ -4856,7 +4859,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (globalScene.arena.getTagOnSide(ArenaTagType.SAFEGUARD, defendingSide)) { const bypassed = new BooleanHolder(false); if (attacker) { - applyAbAttrs(InfiltratorAbAttr, attacker, null, false, bypassed); + applyAbAttrs("InfiltratorAbAttr", attacker, null, false, bypassed); } return !bypassed.value; } @@ -4880,7 +4883,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { // If this Pokemon has Commander and Dondozo as an active ally, hide this Pokemon's sprite. if ( - this.hasAbilityWithAttr(CommanderAbAttr) && + this.hasAbilityWithAttr("CommanderAbAttr") && globalScene.currentBattle.double && this.getAlly()?.species.speciesId === SpeciesId.DONDOZO ) { @@ -5405,7 +5408,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.hideInfo(); } // Trigger abilities that activate upon leaving the field - applyPreLeaveFieldAbAttrs(PreLeaveFieldAbAttr, this); + applyPreLeaveFieldAbAttrs("PreLeaveFieldAbAttr", this); this.setSwitchOutStatus(true); globalScene.triggerPokemonFormChange(this, SpeciesFormChangeActiveTrigger, true); globalScene.field.remove(this, destroy); @@ -5465,7 +5468,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { globalScene.removeModifier(heldItem, this.isEnemy()); } if (forBattle) { - applyPostItemLostAbAttrs(PostItemLostAbAttr, this, false); + applyPostItemLostAbAttrs("PostItemLostAbAttr", this, false); } return true; @@ -5485,6 +5488,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } 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 { @@ -5823,7 +5833,7 @@ export class PlayerPokemon extends Pokemon { if (evoSpecies?.speciesId === SpeciesId.NINCADA && evolution.speciesId === SpeciesId.NINJASK) { const newEvolution = pokemonEvolutions[evoSpecies.speciesId][1]; - if (newEvolution.condition?.predicate(this)) { + if (validateShedinjaEvo()) { const newPokemon = globalScene.addPlayerPokemon( this.species, this.level, @@ -5853,7 +5863,6 @@ export class PlayerPokemon extends Pokemon { newPokemon.fusionLuck = this.fusionLuck; newPokemon.fusionTeraType = this.fusionTeraType; newPokemon.usedTMs = this.usedTMs; - newPokemon.evoCounter = this.evoCounter; globalScene.getPlayerParty().push(newPokemon); newPokemon.evolve(!isFusion ? newEvolution : new FusionSpeciesFormEvolution(this.id, newEvolution), evoSpecies); @@ -5942,7 +5951,6 @@ export class PlayerPokemon extends Pokemon { this.fusionGender = pokemon.gender; this.fusionLuck = pokemon.luck; this.fusionCustomPokemonData = pokemon.customPokemonData; - this.evoCounter = Math.max(pokemon.evoCounter, this.evoCounter); if (pokemon.pauseEvolutions || this.pauseEvolutions) { this.pauseEvolutions = true; } @@ -6011,7 +6019,7 @@ export class PlayerPokemon extends Pokemon { copyMoveset(): PokemonMove[] { const newMoveset: PokemonMove[] = []; 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; @@ -6098,18 +6106,6 @@ export class EnemyPokemon extends Pokemon { 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) { const { waveIndex } = globalScene.currentBattle; const ivs: number[] = []; @@ -6191,33 +6187,39 @@ export class EnemyPokemon extends Pokemon { * the Pokemon the move will target. * @returns this Pokemon's next move in the format {move, moveTargets} */ + // TODO: split this up and move it elsewhere 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(); - if (moveQueue.length !== 0) { - const queuedMove = moveQueue[0]; - if (queuedMove) { - const moveIndex = this.getMoveset().findIndex(m => m.moveId === queuedMove.move); - if ( - (moveIndex > -1 && this.getMoveset()[moveIndex].isUsable(this, queuedMove.ignorePP)) || - queuedMove.virtual - ) { - return queuedMove; - } - this.getMoveQueue().shift(); - return this.getNextMove(); + for (const [i, queuedMove] of moveQueue.entries()) { + const movesetMove = this.getMoveset().find(m => m.moveId === queuedMove.move); + // If the queued move was called indirectly, ignore all PP and usability checks. + // Otherwise, ensure that the move being used is actually usable & in our moveset. + // TODO: What should happen if a pokemon forgets a charging move mid-use? + if (isVirtual(queuedMove.useMode) || movesetMove?.isUsable(this, isIgnorePP(queuedMove.useMode))) { + moveQueue.splice(0, i); // TODO: This should not be done here + return queuedMove; } } + // We went through the entire queue without a match; clear the entire thing. + this.summonData.moveQueue = []; + // Filter out any moves this Pokemon cannot use let movePool = this.getMoveset().filter(m => m.isUsable(this)); // If no moves are left, use Struggle. Otherwise, continue with move selection if (movePool.length) { // If there's only 1 move in the move pool, use it. 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. + // Said moves are executed normally const encoreTag = this.getTag(EncoreTag) as EncoreTag; if (encoreTag) { const encoreMove = movePool.find(m => m.moveId === encoreTag.moveId); @@ -6225,6 +6227,7 @@ export class EnemyPokemon extends Pokemon { return { move: encoreMove.moveId, targets: this.getNextTargets(encoreMove.moveId), + useMode: MoveUseMode.NORMAL, }; } } @@ -6232,7 +6235,7 @@ export class EnemyPokemon extends Pokemon { // No enemy should spawn with this AI type in-game case AiType.RANDOM: { 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: { @@ -6401,14 +6404,20 @@ export class EnemyPokemon extends Pokemon { r, 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 { move: MoveId.STRUGGLE, targets: this.getNextTargets(MoveId.STRUGGLE), + useMode: MoveUseMode.IGNORE_PP, }; } @@ -6754,10 +6763,9 @@ interface IllusionData { export interface TurnMove { move: MoveId; targets: BattlerIndex[]; + useMode: MoveUseMode; result?: MoveResult; - virtual?: boolean; turn?: number; - ignorePP?: boolean; } export interface AttackMoveResult { @@ -6776,6 +6784,12 @@ export interface AttackMoveResult { export class PokemonSummonData { /** [Atk, Def, SpAtk, SpDef, Spd, Acc, Eva] */ 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 tags: BattlerTag[] = []; public abilitySuppressed = false; @@ -6895,7 +6909,6 @@ export class PokemonWaveData { * Resets at the start of a new turn, as well as on switch. */ export class PokemonTurnData { - public flinched = false; public acted = false; /** How many times the current move should hit the target(s) */ public hitCount = 0; @@ -6917,8 +6930,9 @@ export class PokemonTurnData { public failedRunAway = 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 - * 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; /** diff --git a/src/field/trainer.ts b/src/field/trainer.ts index 8d950b08507..b64821d259a 100644 --- a/src/field/trainer.ts +++ b/src/field/trainer.ts @@ -1,7 +1,7 @@ import { globalScene } from "#app/global-scene"; import { pokemonPrevolutions } from "#app/data/balance/pokemon-evolutions"; 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 { TrainerPartyTemplate } from "#app/data/trainers/TrainerPartyTemplate"; import { trainerConfigs } from "#app/data/trainers/trainer-config"; diff --git a/src/game-mode.ts b/src/game-mode.ts index a6fc8195175..da6ef62e33c 100644 --- a/src/game-mode.ts +++ b/src/game-mode.ts @@ -5,7 +5,7 @@ import type { Challenge } from "./data/challenge"; import { allChallenges, applyChallenges, copyChallenge } from "./data/challenge"; import { ChallengeType } from "#enums/challenge-type"; 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 Overrides from "#app/overrides"; 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: - * - override from overrides.ts + * - starting level override from overrides.ts * - 20 for Daily Runs * - 5 for all other modes */ getStartingLevel(): number { - if (Overrides.STARTING_LEVEL_OVERRIDE) { + if (Overrides.STARTING_LEVEL_OVERRIDE > 0) { return Overrides.STARTING_LEVEL_OVERRIDE; } switch (this.modeId) { diff --git a/src/loading-scene.ts b/src/loading-scene.ts index 67ca9a28bc5..f67d19e1027 100644 --- a/src/loading-scene.ts +++ b/src/loading-scene.ts @@ -21,6 +21,8 @@ import { initVouchers } from "#app/system/voucher"; import { BiomeId } from "#enums/biome-id"; import { initMysteryEncounters } from "#app/data/mystery-encounters/mystery-encounters"; import { timedEventManager } from "./global-event-manager"; +import { initModifierPools } from "./modifier/init-modifier-pools"; +import { initModifierTypes } from "./modifier/modifier-type"; export class LoadingScene extends SceneBase { public static readonly KEY = "loading"; @@ -363,6 +365,9 @@ export class LoadingScene extends SceneBase { this.loadLoadingScreen(); + initModifierTypes(); + initModifierPools(); + initAchievements(); initVouchers(); initStatsKeys(); diff --git a/src/modifier/init-modifier-pools.ts b/src/modifier/init-modifier-pools.ts new file mode 100644 index 00000000000..60697333600 --- /dev/null +++ b/src/modifier/init-modifier-pools.ts @@ -0,0 +1,854 @@ +import type Pokemon from "#app/field/pokemon"; +import { + dailyStarterModifierPool, + enemyBuffModifierPool, + modifierPool, + trainerModifierPool, + wildModifierPool, +} from "#app/modifier/modifier-pools"; +import { globalScene } from "#app/global-scene"; +import { DoubleBattleChanceBoosterModifier, SpeciesCritBoosterModifier, TurnStatusEffectModifier } from "./modifier"; +import { WeightedModifierType } from "./modifier-type"; +import { ModifierTier } from "../enums/modifier-tier"; +import type { WeightedModifierTypeWeightFunc } from "#app/@types/modifier-types"; +import { modifierTypes } from "#app/data/data-lists"; +import { PokeballType } from "#enums/pokeball"; +import { BerryModifier } from "./modifier"; +import { BerryType } from "#enums/berry-type"; +import { SpeciesId } from "#enums/species-id"; +import { timedEventManager } from "#app/global-event-manager"; +import { pokemonEvolutions } from "#app/data/balance/pokemon-evolutions"; +import { Unlockables } from "#enums/unlockables"; +import { isNullOrUndefined } from "#app/utils/common"; +import { MoveId } from "#enums/move-id"; +import { StatusEffect } from "#enums/status-effect"; +import { AbilityId } from "#enums/ability-id"; +import { MAX_PER_TYPE_POKEBALLS } from "#app/data/pokeball"; +// biome-ignore lint/correctness/noUnusedImports: This is used in a tsdoc comment +import type { initModifierTypes } from "./modifier-type"; + +/** + * Initialize the wild modifier pool + */ +function initWildModifierPool() { + wildModifierPool[ModifierTier.COMMON] = [new WeightedModifierType(modifierTypes.BERRY, 1)].map(m => { + m.setTier(ModifierTier.COMMON); + return m; + }); + wildModifierPool[ModifierTier.GREAT] = [new WeightedModifierType(modifierTypes.BASE_STAT_BOOSTER, 1)].map(m => { + m.setTier(ModifierTier.GREAT); + return m; + }); + wildModifierPool[ModifierTier.ULTRA] = [ + new WeightedModifierType(modifierTypes.ATTACK_TYPE_BOOSTER, 10), + new WeightedModifierType(modifierTypes.WHITE_HERB, 0), + ].map(m => { + m.setTier(ModifierTier.ULTRA); + return m; + }); + wildModifierPool[ModifierTier.ROGUE] = [new WeightedModifierType(modifierTypes.LUCKY_EGG, 4)].map(m => { + m.setTier(ModifierTier.ROGUE); + return m; + }); + wildModifierPool[ModifierTier.MASTER] = [new WeightedModifierType(modifierTypes.GOLDEN_EGG, 1)].map(m => { + m.setTier(ModifierTier.MASTER); + return m; + }); +} + +/** + * Initialize the common modifier pool + */ +function initCommonModifierPool() { + modifierPool[ModifierTier.COMMON] = [ + new WeightedModifierType(modifierTypes.POKEBALL, () => (hasMaximumBalls(PokeballType.POKEBALL) ? 0 : 6), 6), + new WeightedModifierType(modifierTypes.RARE_CANDY, 2), + new WeightedModifierType( + modifierTypes.POTION, + (party: Pokemon[]) => { + const thresholdPartyMemberCount = Math.min( + party.filter(p => p.getInverseHp() >= 10 && p.getHpRatio() <= 0.875 && !p.isFainted()).length, + 3, + ); + return thresholdPartyMemberCount * 3; + }, + 9, + ), + new WeightedModifierType( + modifierTypes.SUPER_POTION, + (party: Pokemon[]) => { + const thresholdPartyMemberCount = Math.min( + party.filter(p => p.getInverseHp() >= 25 && p.getHpRatio() <= 0.75 && !p.isFainted()).length, + 3, + ); + return thresholdPartyMemberCount; + }, + 3, + ), + new WeightedModifierType( + modifierTypes.ETHER, + (party: Pokemon[]) => { + const thresholdPartyMemberCount = Math.min( + party.filter( + p => + p.hp && + !p.getHeldItems().some(m => m instanceof BerryModifier && m.berryType === BerryType.LEPPA) && + p + .getMoveset() + .filter(m => m.ppUsed && m.getMovePp() - m.ppUsed <= 5 && m.ppUsed > Math.floor(m.getMovePp() / 2)) + .length, + ).length, + 3, + ); + return thresholdPartyMemberCount * 3; + }, + 9, + ), + new WeightedModifierType( + modifierTypes.MAX_ETHER, + (party: Pokemon[]) => { + const thresholdPartyMemberCount = Math.min( + party.filter( + p => + p.hp && + !p.getHeldItems().some(m => m instanceof BerryModifier && m.berryType === BerryType.LEPPA) && + p + .getMoveset() + .filter(m => m.ppUsed && m.getMovePp() - m.ppUsed <= 5 && m.ppUsed > Math.floor(m.getMovePp() / 2)) + .length, + ).length, + 3, + ); + return thresholdPartyMemberCount; + }, + 3, + ), + new WeightedModifierType(modifierTypes.LURE, lureWeightFunc(10, 2)), + new WeightedModifierType(modifierTypes.TEMP_STAT_STAGE_BOOSTER, 4), + new WeightedModifierType(modifierTypes.BERRY, 2), + new WeightedModifierType(modifierTypes.TM_COMMON, 2), + ].map(m => { + m.setTier(ModifierTier.COMMON); + return m; + }); +} + +/** + * Initialize the Great modifier pool + */ +function initGreatModifierPool() { + modifierPool[ModifierTier.GREAT] = [ + new WeightedModifierType(modifierTypes.GREAT_BALL, () => (hasMaximumBalls(PokeballType.GREAT_BALL) ? 0 : 6), 6), + new WeightedModifierType(modifierTypes.PP_UP, 2), + new WeightedModifierType( + modifierTypes.FULL_HEAL, + (party: Pokemon[]) => { + const statusEffectPartyMemberCount = Math.min( + party.filter( + p => + p.hp && + !!p.status && + !p.getHeldItems().some(i => { + if (i instanceof TurnStatusEffectModifier) { + return (i as TurnStatusEffectModifier).getStatusEffect() === p.status?.effect; + } + return false; + }), + ).length, + 3, + ); + return statusEffectPartyMemberCount * 6; + }, + 18, + ), + new WeightedModifierType( + modifierTypes.REVIVE, + (party: Pokemon[]) => { + const faintedPartyMemberCount = Math.min(party.filter(p => p.isFainted()).length, 3); + return faintedPartyMemberCount * 9; + }, + 27, + ), + new WeightedModifierType( + modifierTypes.MAX_REVIVE, + (party: Pokemon[]) => { + const faintedPartyMemberCount = Math.min(party.filter(p => p.isFainted()).length, 3); + return faintedPartyMemberCount * 3; + }, + 9, + ), + new WeightedModifierType( + modifierTypes.SACRED_ASH, + (party: Pokemon[]) => { + return party.filter(p => p.isFainted()).length >= Math.ceil(party.length / 2) ? 1 : 0; + }, + 1, + ), + new WeightedModifierType( + modifierTypes.HYPER_POTION, + (party: Pokemon[]) => { + const thresholdPartyMemberCount = Math.min( + party.filter(p => p.getInverseHp() >= 100 && p.getHpRatio() <= 0.625 && !p.isFainted()).length, + 3, + ); + return thresholdPartyMemberCount * 3; + }, + 9, + ), + new WeightedModifierType( + modifierTypes.MAX_POTION, + (party: Pokemon[]) => { + const thresholdPartyMemberCount = Math.min( + party.filter(p => p.getInverseHp() >= 100 && p.getHpRatio() <= 0.5 && !p.isFainted()).length, + 3, + ); + return thresholdPartyMemberCount; + }, + 3, + ), + new WeightedModifierType( + modifierTypes.FULL_RESTORE, + (party: Pokemon[]) => { + const statusEffectPartyMemberCount = Math.min( + party.filter( + p => + p.hp && + !!p.status && + !p.getHeldItems().some(i => { + if (i instanceof TurnStatusEffectModifier) { + return (i as TurnStatusEffectModifier).getStatusEffect() === p.status?.effect; + } + return false; + }), + ).length, + 3, + ); + const thresholdPartyMemberCount = Math.floor( + (Math.min(party.filter(p => p.getInverseHp() >= 100 && p.getHpRatio() <= 0.5 && !p.isFainted()).length, 3) + + statusEffectPartyMemberCount) / + 2, + ); + return thresholdPartyMemberCount; + }, + 3, + ), + new WeightedModifierType( + modifierTypes.ELIXIR, + (party: Pokemon[]) => { + const thresholdPartyMemberCount = Math.min( + party.filter( + p => + p.hp && + !p.getHeldItems().some(m => m instanceof BerryModifier && m.berryType === BerryType.LEPPA) && + p + .getMoveset() + .filter(m => m.ppUsed && m.getMovePp() - m.ppUsed <= 5 && m.ppUsed > Math.floor(m.getMovePp() / 2)) + .length, + ).length, + 3, + ); + return thresholdPartyMemberCount * 3; + }, + 9, + ), + new WeightedModifierType( + modifierTypes.MAX_ELIXIR, + (party: Pokemon[]) => { + const thresholdPartyMemberCount = Math.min( + party.filter( + p => + p.hp && + !p.getHeldItems().some(m => m instanceof BerryModifier && m.berryType === BerryType.LEPPA) && + p + .getMoveset() + .filter(m => m.ppUsed && m.getMovePp() - m.ppUsed <= 5 && m.ppUsed > Math.floor(m.getMovePp() / 2)) + .length, + ).length, + 3, + ); + return thresholdPartyMemberCount; + }, + 3, + ), + new WeightedModifierType(modifierTypes.DIRE_HIT, 4), + new WeightedModifierType(modifierTypes.SUPER_LURE, lureWeightFunc(15, 4)), + new WeightedModifierType(modifierTypes.NUGGET, skipInLastClassicWaveOrDefault(5)), + new WeightedModifierType(modifierTypes.SPECIES_STAT_BOOSTER, 4), + new WeightedModifierType( + modifierTypes.EVOLUTION_ITEM, + () => { + return Math.min(Math.ceil(globalScene.currentBattle.waveIndex / 15), 8); + }, + 8, + ), + new WeightedModifierType( + modifierTypes.MAP, + () => (globalScene.gameMode.isClassic && globalScene.currentBattle.waveIndex < 180 ? 2 : 0), + 2, + ), + new WeightedModifierType(modifierTypes.SOOTHE_BELL, 2), + new WeightedModifierType(modifierTypes.TM_GREAT, 3), + new WeightedModifierType( + modifierTypes.MEMORY_MUSHROOM, + (party: Pokemon[]) => { + if (!party.find(p => p.getLearnableLevelMoves().length)) { + return 0; + } + const highestPartyLevel = party + .map(p => p.level) + .reduce((highestLevel: number, level: number) => Math.max(highestLevel, level), 1); + return Math.min(Math.ceil(highestPartyLevel / 20), 4); + }, + 4, + ), + new WeightedModifierType(modifierTypes.BASE_STAT_BOOSTER, 3), + new WeightedModifierType(modifierTypes.TERA_SHARD, (party: Pokemon[]) => + party.filter( + p => + !(p.hasSpecies(SpeciesId.TERAPAGOS) || p.hasSpecies(SpeciesId.OGERPON) || p.hasSpecies(SpeciesId.SHEDINJA)), + ).length > 0 + ? 1 + : 0, + ), + new WeightedModifierType( + modifierTypes.DNA_SPLICERS, + (party: Pokemon[]) => { + if (party.filter(p => !p.fusionSpecies).length > 1) { + if (globalScene.gameMode.isSplicedOnly) { + return 4; + } + if (globalScene.gameMode.isClassic && timedEventManager.areFusionsBoosted()) { + return 2; + } + } + return 0; + }, + 4, + ), + new WeightedModifierType( + modifierTypes.VOUCHER, + (_party: Pokemon[], rerollCount: number) => (!globalScene.gameMode.isDaily ? Math.max(1 - rerollCount, 0) : 0), + 1, + ), + ].map(m => { + m.setTier(ModifierTier.GREAT); + return m; + }); +} + +/** + * Initialize the Ultra modifier pool + */ +function initUltraModifierPool() { + modifierPool[ModifierTier.ULTRA] = [ + new WeightedModifierType(modifierTypes.ULTRA_BALL, () => (hasMaximumBalls(PokeballType.ULTRA_BALL) ? 0 : 15), 15), + new WeightedModifierType(modifierTypes.MAX_LURE, lureWeightFunc(30, 4)), + new WeightedModifierType(modifierTypes.BIG_NUGGET, skipInLastClassicWaveOrDefault(12)), + new WeightedModifierType(modifierTypes.PP_MAX, 3), + new WeightedModifierType(modifierTypes.MINT, 4), + new WeightedModifierType( + modifierTypes.RARE_EVOLUTION_ITEM, + () => Math.min(Math.ceil(globalScene.currentBattle.waveIndex / 15) * 4, 32), + 32, + ), + new WeightedModifierType( + modifierTypes.FORM_CHANGE_ITEM, + () => Math.min(Math.ceil(globalScene.currentBattle.waveIndex / 50), 4) * 6, + 24, + ), + new WeightedModifierType(modifierTypes.AMULET_COIN, skipInLastClassicWaveOrDefault(3)), + new WeightedModifierType(modifierTypes.EVIOLITE, (party: Pokemon[]) => { + const { gameMode, gameData } = globalScene; + if (gameMode.isDaily || (!gameMode.isFreshStartChallenge() && gameData.isUnlocked(Unlockables.EVIOLITE))) { + return party.some(p => { + // Check if Pokemon's species (or fusion species, if applicable) can evolve or if they're G-Max'd + if ( + !p.isMax() && + (p.getSpeciesForm(true).speciesId in pokemonEvolutions || + (p.isFusion() && p.getFusionSpeciesForm(true).speciesId in pokemonEvolutions)) + ) { + // Check if Pokemon is already holding an Eviolite + return !p.getHeldItems().some(i => i.type.id === "EVIOLITE"); + } + return false; + }) + ? 10 + : 0; + } + return 0; + }), + new WeightedModifierType(modifierTypes.RARE_SPECIES_STAT_BOOSTER, 12), + new WeightedModifierType( + modifierTypes.LEEK, + (party: Pokemon[]) => { + const checkedSpecies = [SpeciesId.FARFETCHD, SpeciesId.GALAR_FARFETCHD, SpeciesId.SIRFETCHD]; + // If a party member doesn't already have a Leek and is one of the relevant species, Leek can appear + return party.some( + p => + !p.getHeldItems().some(i => i instanceof SpeciesCritBoosterModifier) && + (checkedSpecies.includes(p.getSpeciesForm(true).speciesId) || + (p.isFusion() && checkedSpecies.includes(p.getFusionSpeciesForm(true).speciesId))), + ) + ? 12 + : 0; + }, + 12, + ), + new WeightedModifierType( + modifierTypes.TOXIC_ORB, + (party: Pokemon[]) => { + return party.some(p => { + const isHoldingOrb = p.getHeldItems().some(i => i.type.id === "FLAME_ORB" || i.type.id === "TOXIC_ORB"); + + if (!isHoldingOrb) { + const moveset = p + .getMoveset(true) + .filter(m => !isNullOrUndefined(m)) + .map(m => m.moveId); + const canSetStatus = p.canSetStatus(StatusEffect.TOXIC, true, true, null, true); + + // Moves that take advantage of obtaining the actual status effect + const hasStatusMoves = [MoveId.FACADE, MoveId.PSYCHO_SHIFT].some(m => moveset.includes(m)); + // Moves that take advantage of being able to give the target a status orb + // TODO: Take moves (Trick, Fling, Switcheroo) from comment when they are implemented + const hasItemMoves = [ + /* MoveId.TRICK, MoveId.FLING, MoveId.SWITCHEROO */ + ].some(m => moveset.includes(m)); + + if (canSetStatus) { + // Abilities that take advantage of obtaining the actual status effect, separated based on specificity to the orb + const hasGeneralAbility = [ + AbilityId.QUICK_FEET, + AbilityId.GUTS, + AbilityId.MARVEL_SCALE, + AbilityId.MAGIC_GUARD, + ].some(a => p.hasAbility(a, false, true)); + const hasSpecificAbility = [AbilityId.TOXIC_BOOST, AbilityId.POISON_HEAL].some(a => + p.hasAbility(a, false, true), + ); + const hasOppositeAbility = [AbilityId.FLARE_BOOST].some(a => p.hasAbility(a, false, true)); + + return hasSpecificAbility || (hasGeneralAbility && !hasOppositeAbility) || hasStatusMoves; + } + return hasItemMoves; + } + + return false; + }) + ? 10 + : 0; + }, + 10, + ), + new WeightedModifierType( + modifierTypes.FLAME_ORB, + (party: Pokemon[]) => { + return party.some(p => { + const isHoldingOrb = p.getHeldItems().some(i => i.type.id === "FLAME_ORB" || i.type.id === "TOXIC_ORB"); + + if (!isHoldingOrb) { + const moveset = p + .getMoveset(true) + .filter(m => !isNullOrUndefined(m)) + .map(m => m.moveId); + const canSetStatus = p.canSetStatus(StatusEffect.BURN, true, true, null, true); + + // Moves that take advantage of obtaining the actual status effect + const hasStatusMoves = [MoveId.FACADE, MoveId.PSYCHO_SHIFT].some(m => moveset.includes(m)); + // Moves that take advantage of being able to give the target a status orb + // TODO: Take moves (Trick, Fling, Switcheroo) from comment when they are implemented + const hasItemMoves = [ + /* MoveId.TRICK, MoveId.FLING, MoveId.SWITCHEROO */ + ].some(m => moveset.includes(m)); + + if (canSetStatus) { + // Abilities that take advantage of obtaining the actual status effect, separated based on specificity to the orb + const hasGeneralAbility = [ + AbilityId.QUICK_FEET, + AbilityId.GUTS, + AbilityId.MARVEL_SCALE, + AbilityId.MAGIC_GUARD, + ].some(a => p.hasAbility(a, false, true)); + const hasSpecificAbility = [AbilityId.FLARE_BOOST].some(a => p.hasAbility(a, false, true)); + const hasOppositeAbility = [AbilityId.TOXIC_BOOST, AbilityId.POISON_HEAL].some(a => + p.hasAbility(a, false, true), + ); + + return hasSpecificAbility || (hasGeneralAbility && !hasOppositeAbility) || hasStatusMoves; + } + return hasItemMoves; + } + + return false; + }) + ? 10 + : 0; + }, + 10, + ), + new WeightedModifierType( + modifierTypes.MYSTICAL_ROCK, + (party: Pokemon[]) => { + return party.some(p => { + let isHoldingMax = false; + for (const i of p.getHeldItems()) { + if (i.type.id === "MYSTICAL_ROCK") { + isHoldingMax = i.getStackCount() === i.getMaxStackCount(); + break; + } + } + + if (!isHoldingMax) { + const moveset = p.getMoveset(true).map(m => m.moveId); + + const hasAbility = [ + AbilityId.DROUGHT, + AbilityId.ORICHALCUM_PULSE, + AbilityId.DRIZZLE, + AbilityId.SAND_STREAM, + AbilityId.SAND_SPIT, + AbilityId.SNOW_WARNING, + AbilityId.ELECTRIC_SURGE, + AbilityId.HADRON_ENGINE, + AbilityId.PSYCHIC_SURGE, + AbilityId.GRASSY_SURGE, + AbilityId.SEED_SOWER, + AbilityId.MISTY_SURGE, + ].some(a => p.hasAbility(a, false, true)); + + const hasMoves = [ + MoveId.SUNNY_DAY, + MoveId.RAIN_DANCE, + MoveId.SANDSTORM, + MoveId.SNOWSCAPE, + MoveId.HAIL, + MoveId.CHILLY_RECEPTION, + MoveId.ELECTRIC_TERRAIN, + MoveId.PSYCHIC_TERRAIN, + MoveId.GRASSY_TERRAIN, + MoveId.MISTY_TERRAIN, + ].some(m => moveset.includes(m)); + + return hasAbility || hasMoves; + } + return false; + }) + ? 10 + : 0; + }, + 10, + ), + new WeightedModifierType(modifierTypes.REVIVER_SEED, 4), + new WeightedModifierType(modifierTypes.CANDY_JAR, skipInLastClassicWaveOrDefault(5)), + new WeightedModifierType(modifierTypes.ATTACK_TYPE_BOOSTER, 9), + new WeightedModifierType(modifierTypes.TM_ULTRA, 11), + new WeightedModifierType(modifierTypes.RARER_CANDY, 4), + new WeightedModifierType(modifierTypes.GOLDEN_PUNCH, skipInLastClassicWaveOrDefault(2)), + new WeightedModifierType(modifierTypes.IV_SCANNER, skipInLastClassicWaveOrDefault(4)), + new WeightedModifierType(modifierTypes.EXP_CHARM, skipInLastClassicWaveOrDefault(8)), + new WeightedModifierType(modifierTypes.EXP_SHARE, skipInLastClassicWaveOrDefault(10)), + new WeightedModifierType( + modifierTypes.TERA_ORB, + () => + !globalScene.gameMode.isClassic + ? Math.min(Math.max(Math.floor(globalScene.currentBattle.waveIndex / 50) * 2, 1), 4) + : 0, + 4, + ), + new WeightedModifierType(modifierTypes.QUICK_CLAW, 3), + new WeightedModifierType(modifierTypes.WIDE_LENS, 7), + ].map(m => { + m.setTier(ModifierTier.ULTRA); + return m; + }); +} + +function initRogueModifierPool() { + modifierPool[ModifierTier.ROGUE] = [ + new WeightedModifierType(modifierTypes.ROGUE_BALL, () => (hasMaximumBalls(PokeballType.ROGUE_BALL) ? 0 : 16), 16), + new WeightedModifierType(modifierTypes.RELIC_GOLD, skipInLastClassicWaveOrDefault(2)), + new WeightedModifierType(modifierTypes.LEFTOVERS, 3), + new WeightedModifierType(modifierTypes.SHELL_BELL, 3), + new WeightedModifierType(modifierTypes.BERRY_POUCH, 4), + new WeightedModifierType(modifierTypes.GRIP_CLAW, 5), + new WeightedModifierType(modifierTypes.SCOPE_LENS, 4), + new WeightedModifierType(modifierTypes.BATON, 2), + new WeightedModifierType(modifierTypes.SOUL_DEW, 7), + new WeightedModifierType(modifierTypes.CATCHING_CHARM, () => (!globalScene.gameMode.isClassic ? 4 : 0), 4), + new WeightedModifierType(modifierTypes.ABILITY_CHARM, skipInClassicAfterWave(189, 6)), + new WeightedModifierType(modifierTypes.FOCUS_BAND, 5), + new WeightedModifierType(modifierTypes.KINGS_ROCK, 3), + new WeightedModifierType(modifierTypes.LOCK_CAPSULE, () => (globalScene.gameMode.isClassic ? 0 : 3)), + new WeightedModifierType(modifierTypes.SUPER_EXP_CHARM, skipInLastClassicWaveOrDefault(8)), + new WeightedModifierType( + modifierTypes.RARE_FORM_CHANGE_ITEM, + () => Math.min(Math.ceil(globalScene.currentBattle.waveIndex / 50), 4) * 6, + 24, + ), + new WeightedModifierType( + modifierTypes.MEGA_BRACELET, + () => Math.min(Math.ceil(globalScene.currentBattle.waveIndex / 50), 4) * 9, + 36, + ), + new WeightedModifierType( + modifierTypes.DYNAMAX_BAND, + () => Math.min(Math.ceil(globalScene.currentBattle.waveIndex / 50), 4) * 9, + 36, + ), + new WeightedModifierType( + modifierTypes.VOUCHER_PLUS, + (_party: Pokemon[], rerollCount: number) => + !globalScene.gameMode.isDaily ? Math.max(3 - rerollCount * 1, 0) : 0, + 3, + ), + ].map(m => { + m.setTier(ModifierTier.ROGUE); + return m; + }); +} + +/** + * Initialize the Master modifier pool + */ +function initMasterModifierPool() { + modifierPool[ModifierTier.MASTER] = [ + new WeightedModifierType(modifierTypes.MASTER_BALL, () => (hasMaximumBalls(PokeballType.MASTER_BALL) ? 0 : 24), 24), + new WeightedModifierType(modifierTypes.SHINY_CHARM, 14), + new WeightedModifierType(modifierTypes.HEALING_CHARM, 18), + new WeightedModifierType(modifierTypes.MULTI_LENS, 18), + new WeightedModifierType( + modifierTypes.VOUCHER_PREMIUM, + (_party: Pokemon[], rerollCount: number) => + !globalScene.gameMode.isDaily && !globalScene.gameMode.isEndless && !globalScene.gameMode.isSplicedOnly + ? Math.max(5 - rerollCount * 2, 0) + : 0, + 5, + ), + new WeightedModifierType( + modifierTypes.DNA_SPLICERS, + (party: Pokemon[]) => + !(globalScene.gameMode.isClassic && timedEventManager.areFusionsBoosted()) && + !globalScene.gameMode.isSplicedOnly && + party.filter(p => !p.fusionSpecies).length > 1 + ? 24 + : 0, + 24, + ), + new WeightedModifierType( + modifierTypes.MINI_BLACK_HOLE, + () => + globalScene.gameMode.isDaily || + (!globalScene.gameMode.isFreshStartChallenge() && globalScene.gameData.isUnlocked(Unlockables.MINI_BLACK_HOLE)) + ? 1 + : 0, + 1, + ), + ].map(m => { + m.setTier(ModifierTier.MASTER); + return m; + }); +} + +function initTrainerModifierPool() { + trainerModifierPool[ModifierTier.COMMON] = [ + new WeightedModifierType(modifierTypes.BERRY, 8), + new WeightedModifierType(modifierTypes.BASE_STAT_BOOSTER, 3), + ].map(m => { + m.setTier(ModifierTier.COMMON); + return m; + }); + trainerModifierPool[ModifierTier.GREAT] = [new WeightedModifierType(modifierTypes.BASE_STAT_BOOSTER, 3)].map(m => { + m.setTier(ModifierTier.GREAT); + return m; + }); + trainerModifierPool[ModifierTier.ULTRA] = [ + new WeightedModifierType(modifierTypes.ATTACK_TYPE_BOOSTER, 10), + new WeightedModifierType(modifierTypes.WHITE_HERB, 0), + ].map(m => { + m.setTier(ModifierTier.ULTRA); + return m; + }); + trainerModifierPool[ModifierTier.ROGUE] = [ + new WeightedModifierType(modifierTypes.FOCUS_BAND, 2), + new WeightedModifierType(modifierTypes.LUCKY_EGG, 4), + new WeightedModifierType(modifierTypes.QUICK_CLAW, 1), + new WeightedModifierType(modifierTypes.GRIP_CLAW, 1), + new WeightedModifierType(modifierTypes.WIDE_LENS, 1), + ].map(m => { + m.setTier(ModifierTier.ROGUE); + return m; + }); + trainerModifierPool[ModifierTier.MASTER] = [ + new WeightedModifierType(modifierTypes.KINGS_ROCK, 1), + new WeightedModifierType(modifierTypes.LEFTOVERS, 1), + new WeightedModifierType(modifierTypes.SHELL_BELL, 1), + new WeightedModifierType(modifierTypes.SCOPE_LENS, 1), + ].map(m => { + m.setTier(ModifierTier.MASTER); + return m; + }); +} + +/** + * Initialize the enemy buff modifier pool + */ +function initEnemyBuffModifierPool() { + enemyBuffModifierPool[ModifierTier.COMMON] = [ + new WeightedModifierType(modifierTypes.ENEMY_DAMAGE_BOOSTER, 9), + new WeightedModifierType(modifierTypes.ENEMY_DAMAGE_REDUCTION, 9), + new WeightedModifierType(modifierTypes.ENEMY_ATTACK_POISON_CHANCE, 3), + new WeightedModifierType(modifierTypes.ENEMY_ATTACK_PARALYZE_CHANCE, 3), + new WeightedModifierType(modifierTypes.ENEMY_ATTACK_BURN_CHANCE, 3), + new WeightedModifierType(modifierTypes.ENEMY_STATUS_EFFECT_HEAL_CHANCE, 9), + new WeightedModifierType(modifierTypes.ENEMY_ENDURE_CHANCE, 4), + new WeightedModifierType(modifierTypes.ENEMY_FUSED_CHANCE, 1), + ].map(m => { + m.setTier(ModifierTier.COMMON); + return m; + }); + enemyBuffModifierPool[ModifierTier.GREAT] = [ + new WeightedModifierType(modifierTypes.ENEMY_DAMAGE_BOOSTER, 5), + new WeightedModifierType(modifierTypes.ENEMY_DAMAGE_REDUCTION, 5), + new WeightedModifierType(modifierTypes.ENEMY_STATUS_EFFECT_HEAL_CHANCE, 5), + new WeightedModifierType(modifierTypes.ENEMY_ENDURE_CHANCE, 5), + new WeightedModifierType(modifierTypes.ENEMY_FUSED_CHANCE, 1), + ].map(m => { + m.setTier(ModifierTier.GREAT); + return m; + }); + enemyBuffModifierPool[ModifierTier.ULTRA] = [ + new WeightedModifierType(modifierTypes.ENEMY_DAMAGE_BOOSTER, 10), + new WeightedModifierType(modifierTypes.ENEMY_DAMAGE_REDUCTION, 10), + new WeightedModifierType(modifierTypes.ENEMY_HEAL, 10), + new WeightedModifierType(modifierTypes.ENEMY_STATUS_EFFECT_HEAL_CHANCE, 10), + new WeightedModifierType(modifierTypes.ENEMY_ENDURE_CHANCE, 10), + new WeightedModifierType(modifierTypes.ENEMY_FUSED_CHANCE, 5), + ].map(m => { + m.setTier(ModifierTier.ULTRA); + return m; + }); + enemyBuffModifierPool[ModifierTier.ROGUE] = [].map((m: WeightedModifierType) => { + m.setTier(ModifierTier.ROGUE); + return m; + }); + enemyBuffModifierPool[ModifierTier.MASTER] = [].map((m: WeightedModifierType) => { + m.setTier(ModifierTier.MASTER); + return m; + }); +} + +/** + * Initialize the daily starter modifier pool + */ +function initDailyStarterModifierPool() { + dailyStarterModifierPool[ModifierTier.COMMON] = [ + new WeightedModifierType(modifierTypes.BASE_STAT_BOOSTER, 1), + new WeightedModifierType(modifierTypes.BERRY, 3), + ].map(m => { + m.setTier(ModifierTier.COMMON); + return m; + }); + dailyStarterModifierPool[ModifierTier.GREAT] = [new WeightedModifierType(modifierTypes.ATTACK_TYPE_BOOSTER, 5)].map( + m => { + m.setTier(ModifierTier.GREAT); + return m; + }, + ); + dailyStarterModifierPool[ModifierTier.ULTRA] = [ + new WeightedModifierType(modifierTypes.REVIVER_SEED, 4), + new WeightedModifierType(modifierTypes.SOOTHE_BELL, 1), + new WeightedModifierType(modifierTypes.SOUL_DEW, 1), + new WeightedModifierType(modifierTypes.GOLDEN_PUNCH, 1), + ].map(m => { + m.setTier(ModifierTier.ULTRA); + return m; + }); + dailyStarterModifierPool[ModifierTier.ROGUE] = [ + new WeightedModifierType(modifierTypes.GRIP_CLAW, 5), + new WeightedModifierType(modifierTypes.BATON, 2), + new WeightedModifierType(modifierTypes.FOCUS_BAND, 5), + new WeightedModifierType(modifierTypes.QUICK_CLAW, 3), + new WeightedModifierType(modifierTypes.KINGS_ROCK, 3), + ].map(m => { + m.setTier(ModifierTier.ROGUE); + return m; + }); + dailyStarterModifierPool[ModifierTier.MASTER] = [ + new WeightedModifierType(modifierTypes.LEFTOVERS, 1), + new WeightedModifierType(modifierTypes.SHELL_BELL, 1), + ].map(m => { + m.setTier(ModifierTier.MASTER); + return m; + }); +} + +/** + * Initialize {@linkcode modifierPool} with the initial set of modifier types. + * {@linkcode initModifierTypes} MUST be called before this function. + */ +export function initModifierPools() { + // The modifier pools the player chooses from during modifier selection + initCommonModifierPool(); + initGreatModifierPool(); + initUltraModifierPool(); + initRogueModifierPool(); + initMasterModifierPool(); + + // Modifier pools for specific scenarios + initWildModifierPool(); + initTrainerModifierPool(); + initEnemyBuffModifierPool(); + initDailyStarterModifierPool(); +} + +/** + * High order function that returns a WeightedModifierTypeWeightFunc that will only be applied on + * classic and skip an ModifierType if current wave is greater or equal to the one passed down + * @param wave - Wave where we should stop showing the modifier + * @param defaultWeight - ModifierType default weight + * @returns A WeightedModifierTypeWeightFunc + */ +function skipInClassicAfterWave(wave: number, defaultWeight: number): WeightedModifierTypeWeightFunc { + return () => { + const gameMode = globalScene.gameMode; + const currentWave = globalScene.currentBattle.waveIndex; + return gameMode.isClassic && currentWave >= wave ? 0 : defaultWeight; + }; +} + +/** + * High order function that returns a WeightedModifierTypeWeightFunc that will only be applied on + * classic and it will skip a ModifierType if it is the last wave pull. + * @param defaultWeight ModifierType default weight + * @returns A WeightedModifierTypeWeightFunc + */ +function skipInLastClassicWaveOrDefault(defaultWeight: number): WeightedModifierTypeWeightFunc { + return skipInClassicAfterWave(199, defaultWeight); +} + +/** + * High order function that returns a WeightedModifierTypeWeightFunc to ensure Lures don't spawn on Classic 199 + * or if the lure still has over 60% of its duration left + * @param maxBattles The max battles the lure type in question lasts. 10 for green, 15 for Super, 30 for Max + * @param weight The desired weight for the lure when it does spawn + * @returns A WeightedModifierTypeWeightFunc + */ +function lureWeightFunc(maxBattles: number, weight: number): WeightedModifierTypeWeightFunc { + return () => { + const lures = globalScene.getModifiers(DoubleBattleChanceBoosterModifier); + return !(globalScene.gameMode.isClassic && globalScene.currentBattle.waveIndex === 199) && + (lures.length === 0 || + lures.filter(m => m.getMaxBattles() === maxBattles && m.getBattleCount() >= maxBattles * 0.6).length === 0) + ? weight + : 0; + }; +} + +/** + * Used to check if the player has max of a given ball type in Classic + * @param ballType The {@linkcode PokeballType} being checked + * @returns boolean: true if the player has the maximum of a given ball type + */ +function hasMaximumBalls(ballType: PokeballType): boolean { + return globalScene.gameMode.isClassic && globalScene.pokeballCounts[ballType] >= MAX_PER_TYPE_POKEBALLS; +} diff --git a/src/modifier/modifier-pools.ts b/src/modifier/modifier-pools.ts new file mode 100644 index 00000000000..3396dca1f93 --- /dev/null +++ b/src/modifier/modifier-pools.ts @@ -0,0 +1,16 @@ +/** + * Contains modifier pools for different contexts in the game. + * Can be safely imported without worrying about circular dependencies. + */ + +import type { ModifierPool } from "#app/@types/modifier-types"; + +export const modifierPool: ModifierPool = {}; + +export const wildModifierPool: ModifierPool = {}; + +export const trainerModifierPool: ModifierPool = {}; + +export const enemyBuffModifierPool: ModifierPool = {}; + +export const dailyStarterModifierPool: ModifierPool = {}; diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index b37ed7dea3b..a04a5e2be47 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -2,9 +2,9 @@ import { globalScene } from "#app/global-scene"; import { EvolutionItem, pokemonEvolutions } from "#app/data/balance/pokemon-evolutions"; import { tmPoolTiers, tmSpecies } from "#app/data/balance/tms"; import { getBerryEffectDescription, getBerryName } from "#app/data/berry"; -import { allMoves } from "#app/data/data-lists"; +import { allMoves, modifierTypes } from "#app/data/data-lists"; import { getNatureName, getNatureStatMultiplier } from "#app/data/nature"; -import { getPokeballCatchMultiplier, getPokeballName, MAX_PER_TYPE_POKEBALLS } from "#app/data/pokeball"; +import { getPokeballCatchMultiplier, getPokeballName } from "#app/data/pokeball"; import { pokemonFormChanges, SpeciesFormChangeCondition } from "#app/data/pokemon-forms"; import { SpeciesFormChangeItemTrigger } from "#app/data/pokemon-forms/form-change-triggers"; import { FormChangeItem } from "#enums/form-change-item"; @@ -97,9 +97,8 @@ import { CriticalCatchChanceBoosterModifier, FieldEffectModifier, } from "#app/modifier/modifier"; -import { ModifierTier } from "#app/modifier/modifier-tier"; +import { ModifierTier } from "#enums/modifier-tier"; import Overrides from "#app/overrides"; -import { Unlockables } from "#app/system/unlockables"; import { getVoucherTypeIcon, getVoucherTypeName, VoucherType } from "#app/system/voucher"; import type { PokemonMoveSelectFilter, PokemonSelectFilter } from "#app/ui/party-ui-handler"; import PartyUiHandler from "#app/ui/party-ui-handler"; @@ -113,7 +112,6 @@ import { padInt, randSeedInt, } from "#app/utils/common"; -import { AbilityId } from "#enums/ability-id"; import { BattlerTagType } from "#enums/battler-tag-type"; import { BerryType } from "#enums/berry-type"; import { MoveId } from "#enums/move-id"; @@ -127,18 +125,13 @@ import { StatusEffect } from "#enums/status-effect"; import i18next from "i18next"; import { timedEventManager } from "#app/global-event-manager"; import { TYPE_BOOST_ITEM_BOOST_PERCENT } from "#app/constants"; +import { ModifierPoolType } from "#enums/modifier-pool-type"; +import { getModifierPoolForType, getModifierType } from "#app/utils/modifier-utils"; +import type { ModifierTypeFunc, WeightedModifierTypeWeightFunc } from "#app/@types/modifier-types"; const outputModifierData = false; const useMaxWeightForOutput = false; -export enum ModifierPoolType { - PLAYER, - WILD, - TRAINER, - ENEMY_BUFF, - DAILY_STARTER, -} - type NewModifierFunc = (type: ModifierType, args: any[]) => Modifier; export class ModifierType { @@ -150,6 +143,19 @@ export class ModifierType { public tier: ModifierTier; protected newModifierFunc: NewModifierFunc | null; + /** + * Checks if the modifier type is of a specific type + * @param modifierType - The type to check against + * @return Whether the modifier type is of the specified type + */ + public is(modifierType: K): this is ModifierTypeInstanceMap[K] { + const targetType = ModifierTypeConstructorMap[modifierType]; + if (!targetType) { + return false; + } + return this instanceof targetType; + } + constructor( localeKey: string | null, iconImage: string | null, @@ -218,7 +224,7 @@ export class ModifierType { * @param func */ withIdFromFunc(func: ModifierTypeFunc): ModifierType { - this.id = Object.keys(modifierTypes).find(k => modifierTypes[k] === func)!; // TODO: is this bang correct? + this.id = Object.keys(modifierTypeInitObj).find(k => modifierTypeInitObj[k] === func)!; // TODO: is this bang correct? return this; } @@ -299,7 +305,7 @@ export interface GeneratedPersistentModifierType { getPregenArgs(): any[]; } -class AddPokeballModifierType extends ModifierType { +export class AddPokeballModifierType extends ModifierType { private pokeballType: PokeballType; private count: number; @@ -329,7 +335,7 @@ class AddPokeballModifierType extends ModifierType { } } -class AddVoucherModifierType extends ModifierType { +export class AddVoucherModifierType extends ModifierType { private voucherType: VoucherType; private count: number; @@ -1212,12 +1218,8 @@ export class EvolutionItemModifierType extends PokemonModifierType implements Ge (pokemon: PlayerPokemon) => { if ( pokemonEvolutions.hasOwnProperty(pokemon.species.speciesId) && - pokemonEvolutions[pokemon.species.speciesId].filter( - e => - e.item === this.evolutionItem && - (!e.condition || e.condition.predicate(pokemon)) && - (e.preFormKey === null || e.preFormKey === pokemon.getFormKey()), - ).length && + pokemonEvolutions[pokemon.species.speciesId].filter(e => e.validate(pokemon, false, this.evolutionItem)) + .length && pokemon.getFormKey() !== SpeciesFormKey.GIGANTAMAX ) { return null; @@ -1226,12 +1228,8 @@ export class EvolutionItemModifierType extends PokemonModifierType implements Ge pokemon.isFusion() && pokemon.fusionSpecies && pokemonEvolutions.hasOwnProperty(pokemon.fusionSpecies.speciesId) && - pokemonEvolutions[pokemon.fusionSpecies.speciesId].filter( - e => - e.item === this.evolutionItem && - (!e.condition || e.condition.predicate(pokemon)) && - (e.preFormKey === null || e.preFormKey === pokemon.getFusionFormKey()), - ).length && + pokemonEvolutions[pokemon.fusionSpecies.speciesId].filter(e => e.validate(pokemon, true, this.evolutionItem)) + .length && pokemon.getFusionFormKey() !== SpeciesFormKey.GIGANTAMAX ) { return null; @@ -1591,12 +1589,7 @@ class EvolutionItemModifierTypeGenerator extends ModifierTypeGenerator { ) .flatMap(p => { const evolutions = pokemonEvolutions[p.species.speciesId]; - return evolutions.filter( - e => - e.item !== EvolutionItem.NONE && - (e.evoFormKey === null || (e.preFormKey || "") === p.getFormKey()) && - (!e.condition || e.condition.predicate(p)), - ); + return evolutions.filter(e => e.isValidItemEvolution(p)); }), party .filter( @@ -1610,17 +1603,12 @@ class EvolutionItemModifierTypeGenerator extends ModifierTypeGenerator { ) .flatMap(p => { const evolutions = pokemonEvolutions[p.fusionSpecies!.speciesId]; - return evolutions.filter( - e => - e.item !== EvolutionItem.NONE && - (e.evoFormKey === null || (e.preFormKey || "") === p.getFusionFormKey()) && - (!e.condition || e.condition.predicate(p)), - ); + return evolutions.filter(e => e.isValidItemEvolution(p, true)); }), ] .flat() - .flatMap(e => e.item) - .filter(i => (!!i && i > 50) === rare); + .flatMap(e => e.evoItem) + .filter(i => !!i && i > 50 === rare); if (!evolutionItemPool.length) { return null; @@ -1631,7 +1619,7 @@ class EvolutionItemModifierTypeGenerator extends ModifierTypeGenerator { } } -class FormChangeItemModifierTypeGenerator extends ModifierTypeGenerator { +export class FormChangeItemModifierTypeGenerator extends ModifierTypeGenerator { constructor(isRareFormChangeItem: boolean) { super((party: Pokemon[], pregenArgs?: any[]) => { if (pregenArgs && pregenArgs.length === 1 && pregenArgs[0] in FormChangeItem) { @@ -1794,52 +1782,7 @@ export class EnemyEndureChanceModifierType extends ModifierType { } } -export type ModifierTypeFunc = () => ModifierType; -type WeightedModifierTypeWeightFunc = (party: Pokemon[], rerollCount?: number) => number; - -/** - * High order function that returns a WeightedModifierTypeWeightFunc that will only be applied on - * classic and skip an ModifierType if current wave is greater or equal to the one passed down - * @param wave - Wave where we should stop showing the modifier - * @param defaultWeight - ModifierType default weight - * @returns A WeightedModifierTypeWeightFunc - */ -function skipInClassicAfterWave(wave: number, defaultWeight: number): WeightedModifierTypeWeightFunc { - return () => { - const gameMode = globalScene.gameMode; - const currentWave = globalScene.currentBattle.waveIndex; - return gameMode.isClassic && currentWave >= wave ? 0 : defaultWeight; - }; -} - -/** - * High order function that returns a WeightedModifierTypeWeightFunc that will only be applied on - * classic and it will skip a ModifierType if it is the last wave pull. - * @param defaultWeight ModifierType default weight - * @returns A WeightedModifierTypeWeightFunc - */ -function skipInLastClassicWaveOrDefault(defaultWeight: number): WeightedModifierTypeWeightFunc { - return skipInClassicAfterWave(199, defaultWeight); -} - -/** - * High order function that returns a WeightedModifierTypeWeightFunc to ensure Lures don't spawn on Classic 199 - * or if the lure still has over 60% of its duration left - * @param maxBattles The max battles the lure type in question lasts. 10 for green, 15 for Super, 30 for Max - * @param weight The desired weight for the lure when it does spawn - * @returns A WeightedModifierTypeWeightFunc - */ -function lureWeightFunc(maxBattles: number, weight: number): WeightedModifierTypeWeightFunc { - return () => { - const lures = globalScene.getModifiers(DoubleBattleChanceBoosterModifier); - return !(globalScene.gameMode.isClassic && globalScene.currentBattle.waveIndex === 199) && - (lures.length === 0 || - lures.filter(m => m.getMaxBattles() === maxBattles && m.getBattleCount() >= maxBattles * 0.6).length === 0) - ? weight - : 0; - }; -} -class WeightedModifierType { +export class WeightedModifierType { public modifierType: ModifierType; public weight: number | WeightedModifierTypeWeightFunc; public maxWeight: number | WeightedModifierTypeWeightFunc; @@ -1850,7 +1793,7 @@ class WeightedModifierType { maxWeight?: number | WeightedModifierTypeWeightFunc, ) { this.modifierType = modifierTypeFunc(); - this.modifierType.id = Object.keys(modifierTypes).find(k => modifierTypes[k] === modifierTypeFunc)!; // TODO: is this bang correct? + this.modifierType.id = Object.keys(modifierTypeInitObj).find(k => modifierTypeInitObj[k] === modifierTypeFunc)!; // TODO: is this bang correct? this.weight = weight; this.maxWeight = maxWeight || (!(weight instanceof Function) ? weight : 0); } @@ -1870,39 +1813,39 @@ export type GeneratorModifierOverride = { count?: number; } & ( | { - name: keyof Pick; + name: keyof Pick; type?: SpeciesStatBoosterItem; } | { - name: keyof Pick; + name: keyof Pick; type?: TempBattleStat; } | { - name: keyof Pick; + name: keyof Pick; type?: Stat; } | { - name: keyof Pick; + name: keyof Pick; type?: Nature; } | { - name: keyof Pick; + name: keyof Pick; type?: PokemonType; } | { - name: keyof Pick; + name: keyof Pick; type?: BerryType; } | { - name: keyof Pick; + name: keyof Pick; type?: EvolutionItem; } | { - name: keyof Pick; + name: keyof Pick; type?: FormChangeItem; } | { - name: keyof Pick; + name: keyof Pick; type?: MoveId; } ); @@ -1910,9 +1853,9 @@ export type GeneratorModifierOverride = { /** Type used to construct modifiers and held items for overriding purposes. */ export type ModifierOverride = GeneratorModifierOverride | BaseModifierOverride; -export type ModifierTypeKeys = keyof typeof modifierTypes; +export type ModifierTypeKeys = keyof typeof modifierTypeInitObj; -export const modifierTypes = { +const modifierTypeInitObj = Object.freeze({ POKEBALL: () => new AddPokeballModifierType("pb", PokeballType.POKEBALL, 5), GREAT_BALL: () => new AddPokeballModifierType("gb", PokeballType.GREAT_BALL, 5), ULTRA_BALL: () => new AddPokeballModifierType("ub", PokeballType.ULTRA_BALL, 5), @@ -1931,7 +1874,8 @@ export const modifierTypes = { new PokemonHeldItemModifierType( "modifierType:ModifierType.EVOLUTION_TRACKER_GIMMIGHOUL", "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: () => @@ -2421,747 +2365,15 @@ export const modifierTypes = { "golden_net", (type, _args) => new BoostBugSpawnModifier(type), ), -}; - -interface ModifierPool { - [tier: string]: WeightedModifierType[]; -} +}); /** - * Used to check if the player has max of a given ball type in Classic - * @param ballType The {@linkcode PokeballType} being checked - * @returns boolean: true if the player has the maximum of a given ball type + * The initial set of modifier types, used to generate the modifier pool. */ -function hasMaximumBalls(ballType: PokeballType): boolean { - return globalScene.gameMode.isClassic && globalScene.pokeballCounts[ballType] >= MAX_PER_TYPE_POKEBALLS; -} +export type ModifierTypes = typeof modifierTypeInitObj; -const modifierPool: ModifierPool = { - [ModifierTier.COMMON]: [ - new WeightedModifierType(modifierTypes.POKEBALL, () => (hasMaximumBalls(PokeballType.POKEBALL) ? 0 : 6), 6), - new WeightedModifierType(modifierTypes.RARE_CANDY, 2), - new WeightedModifierType( - modifierTypes.POTION, - (party: Pokemon[]) => { - const thresholdPartyMemberCount = Math.min( - party.filter(p => p.getInverseHp() >= 10 && p.getHpRatio() <= 0.875 && !p.isFainted()).length, - 3, - ); - return thresholdPartyMemberCount * 3; - }, - 9, - ), - new WeightedModifierType( - modifierTypes.SUPER_POTION, - (party: Pokemon[]) => { - const thresholdPartyMemberCount = Math.min( - party.filter(p => p.getInverseHp() >= 25 && p.getHpRatio() <= 0.75 && !p.isFainted()).length, - 3, - ); - return thresholdPartyMemberCount; - }, - 3, - ), - new WeightedModifierType( - modifierTypes.ETHER, - (party: Pokemon[]) => { - const thresholdPartyMemberCount = Math.min( - party.filter( - p => - p.hp && - !p.getHeldItems().some(m => m instanceof BerryModifier && m.berryType === BerryType.LEPPA) && - p - .getMoveset() - .filter(m => m.ppUsed && m.getMovePp() - m.ppUsed <= 5 && m.ppUsed > Math.floor(m.getMovePp() / 2)) - .length, - ).length, - 3, - ); - return thresholdPartyMemberCount * 3; - }, - 9, - ), - new WeightedModifierType( - modifierTypes.MAX_ETHER, - (party: Pokemon[]) => { - const thresholdPartyMemberCount = Math.min( - party.filter( - p => - p.hp && - !p.getHeldItems().some(m => m instanceof BerryModifier && m.berryType === BerryType.LEPPA) && - p - .getMoveset() - .filter(m => m.ppUsed && m.getMovePp() - m.ppUsed <= 5 && m.ppUsed > Math.floor(m.getMovePp() / 2)) - .length, - ).length, - 3, - ); - return thresholdPartyMemberCount; - }, - 3, - ), - new WeightedModifierType(modifierTypes.LURE, lureWeightFunc(10, 2)), - new WeightedModifierType(modifierTypes.TEMP_STAT_STAGE_BOOSTER, 4), - new WeightedModifierType(modifierTypes.BERRY, 2), - new WeightedModifierType(modifierTypes.TM_COMMON, 2), - ].map(m => { - m.setTier(ModifierTier.COMMON); - return m; - }), - [ModifierTier.GREAT]: [ - new WeightedModifierType(modifierTypes.GREAT_BALL, () => (hasMaximumBalls(PokeballType.GREAT_BALL) ? 0 : 6), 6), - new WeightedModifierType(modifierTypes.PP_UP, 2), - new WeightedModifierType( - modifierTypes.FULL_HEAL, - (party: Pokemon[]) => { - const statusEffectPartyMemberCount = Math.min( - party.filter( - p => - p.hp && - !!p.status && - !p.getHeldItems().some(i => { - if (i instanceof TurnStatusEffectModifier) { - return (i as TurnStatusEffectModifier).getStatusEffect() === p.status?.effect; - } - return false; - }), - ).length, - 3, - ); - return statusEffectPartyMemberCount * 6; - }, - 18, - ), - new WeightedModifierType( - modifierTypes.REVIVE, - (party: Pokemon[]) => { - const faintedPartyMemberCount = Math.min(party.filter(p => p.isFainted()).length, 3); - return faintedPartyMemberCount * 9; - }, - 27, - ), - new WeightedModifierType( - modifierTypes.MAX_REVIVE, - (party: Pokemon[]) => { - const faintedPartyMemberCount = Math.min(party.filter(p => p.isFainted()).length, 3); - return faintedPartyMemberCount * 3; - }, - 9, - ), - new WeightedModifierType( - modifierTypes.SACRED_ASH, - (party: Pokemon[]) => { - return party.filter(p => p.isFainted()).length >= Math.ceil(party.length / 2) ? 1 : 0; - }, - 1, - ), - new WeightedModifierType( - modifierTypes.HYPER_POTION, - (party: Pokemon[]) => { - const thresholdPartyMemberCount = Math.min( - party.filter(p => p.getInverseHp() >= 100 && p.getHpRatio() <= 0.625 && !p.isFainted()).length, - 3, - ); - return thresholdPartyMemberCount * 3; - }, - 9, - ), - new WeightedModifierType( - modifierTypes.MAX_POTION, - (party: Pokemon[]) => { - const thresholdPartyMemberCount = Math.min( - party.filter(p => p.getInverseHp() >= 100 && p.getHpRatio() <= 0.5 && !p.isFainted()).length, - 3, - ); - return thresholdPartyMemberCount; - }, - 3, - ), - new WeightedModifierType( - modifierTypes.FULL_RESTORE, - (party: Pokemon[]) => { - const statusEffectPartyMemberCount = Math.min( - party.filter( - p => - p.hp && - !!p.status && - !p.getHeldItems().some(i => { - if (i instanceof TurnStatusEffectModifier) { - return (i as TurnStatusEffectModifier).getStatusEffect() === p.status?.effect; - } - return false; - }), - ).length, - 3, - ); - const thresholdPartyMemberCount = Math.floor( - (Math.min(party.filter(p => p.getInverseHp() >= 100 && p.getHpRatio() <= 0.5 && !p.isFainted()).length, 3) + - statusEffectPartyMemberCount) / - 2, - ); - return thresholdPartyMemberCount; - }, - 3, - ), - new WeightedModifierType( - modifierTypes.ELIXIR, - (party: Pokemon[]) => { - const thresholdPartyMemberCount = Math.min( - party.filter( - p => - p.hp && - !p.getHeldItems().some(m => m instanceof BerryModifier && m.berryType === BerryType.LEPPA) && - p - .getMoveset() - .filter(m => m.ppUsed && m.getMovePp() - m.ppUsed <= 5 && m.ppUsed > Math.floor(m.getMovePp() / 2)) - .length, - ).length, - 3, - ); - return thresholdPartyMemberCount * 3; - }, - 9, - ), - new WeightedModifierType( - modifierTypes.MAX_ELIXIR, - (party: Pokemon[]) => { - const thresholdPartyMemberCount = Math.min( - party.filter( - p => - p.hp && - !p.getHeldItems().some(m => m instanceof BerryModifier && m.berryType === BerryType.LEPPA) && - p - .getMoveset() - .filter(m => m.ppUsed && m.getMovePp() - m.ppUsed <= 5 && m.ppUsed > Math.floor(m.getMovePp() / 2)) - .length, - ).length, - 3, - ); - return thresholdPartyMemberCount; - }, - 3, - ), - new WeightedModifierType(modifierTypes.DIRE_HIT, 4), - new WeightedModifierType(modifierTypes.SUPER_LURE, lureWeightFunc(15, 4)), - new WeightedModifierType(modifierTypes.NUGGET, skipInLastClassicWaveOrDefault(5)), - new WeightedModifierType(modifierTypes.SPECIES_STAT_BOOSTER, 4), - new WeightedModifierType( - modifierTypes.EVOLUTION_ITEM, - () => { - return Math.min(Math.ceil(globalScene.currentBattle.waveIndex / 15), 8); - }, - 8, - ), - new WeightedModifierType( - modifierTypes.MAP, - () => (globalScene.gameMode.isClassic && globalScene.currentBattle.waveIndex < 180 ? 2 : 0), - 2, - ), - new WeightedModifierType(modifierTypes.SOOTHE_BELL, 2), - new WeightedModifierType(modifierTypes.TM_GREAT, 3), - new WeightedModifierType( - modifierTypes.MEMORY_MUSHROOM, - (party: Pokemon[]) => { - if (!party.find(p => p.getLearnableLevelMoves().length)) { - return 0; - } - const highestPartyLevel = party - .map(p => p.level) - .reduce((highestLevel: number, level: number) => Math.max(highestLevel, level), 1); - return Math.min(Math.ceil(highestPartyLevel / 20), 4); - }, - 4, - ), - new WeightedModifierType(modifierTypes.BASE_STAT_BOOSTER, 3), - new WeightedModifierType(modifierTypes.TERA_SHARD, (party: Pokemon[]) => - party.filter( - p => - !(p.hasSpecies(SpeciesId.TERAPAGOS) || p.hasSpecies(SpeciesId.OGERPON) || p.hasSpecies(SpeciesId.SHEDINJA)), - ).length > 0 - ? 1 - : 0, - ), - new WeightedModifierType( - modifierTypes.DNA_SPLICERS, - (party: Pokemon[]) => { - if (party.filter(p => !p.fusionSpecies).length > 1) { - if (globalScene.gameMode.isSplicedOnly) { - return 4; - } - if (globalScene.gameMode.isClassic && timedEventManager.areFusionsBoosted()) { - return 2; - } - } - return 0; - }, - 4, - ), - new WeightedModifierType( - modifierTypes.VOUCHER, - (_party: Pokemon[], rerollCount: number) => (!globalScene.gameMode.isDaily ? Math.max(1 - rerollCount, 0) : 0), - 1, - ), - ].map(m => { - m.setTier(ModifierTier.GREAT); - return m; - }), - [ModifierTier.ULTRA]: [ - new WeightedModifierType(modifierTypes.ULTRA_BALL, () => (hasMaximumBalls(PokeballType.ULTRA_BALL) ? 0 : 15), 15), - new WeightedModifierType(modifierTypes.MAX_LURE, lureWeightFunc(30, 4)), - new WeightedModifierType(modifierTypes.BIG_NUGGET, skipInLastClassicWaveOrDefault(12)), - new WeightedModifierType(modifierTypes.PP_MAX, 3), - new WeightedModifierType(modifierTypes.MINT, 4), - new WeightedModifierType( - modifierTypes.RARE_EVOLUTION_ITEM, - () => Math.min(Math.ceil(globalScene.currentBattle.waveIndex / 15) * 4, 32), - 32, - ), - new WeightedModifierType( - modifierTypes.FORM_CHANGE_ITEM, - () => Math.min(Math.ceil(globalScene.currentBattle.waveIndex / 50), 4) * 6, - 24, - ), - new WeightedModifierType(modifierTypes.AMULET_COIN, skipInLastClassicWaveOrDefault(3)), - new WeightedModifierType(modifierTypes.EVIOLITE, (party: Pokemon[]) => { - const { gameMode, gameData } = globalScene; - if (gameMode.isDaily || (!gameMode.isFreshStartChallenge() && gameData.isUnlocked(Unlockables.EVIOLITE))) { - return party.some(p => { - // Check if Pokemon's species (or fusion species, if applicable) can evolve or if they're G-Max'd - if ( - !p.isMax() && - (p.getSpeciesForm(true).speciesId in pokemonEvolutions || - (p.isFusion() && p.getFusionSpeciesForm(true).speciesId in pokemonEvolutions)) - ) { - // Check if Pokemon is already holding an Eviolite - return !p.getHeldItems().some(i => i.type.id === "EVIOLITE"); - } - return false; - }) - ? 10 - : 0; - } - return 0; - }), - new WeightedModifierType(modifierTypes.RARE_SPECIES_STAT_BOOSTER, 12), - new WeightedModifierType( - modifierTypes.LEEK, - (party: Pokemon[]) => { - const checkedSpecies = [SpeciesId.FARFETCHD, SpeciesId.GALAR_FARFETCHD, SpeciesId.SIRFETCHD]; - // If a party member doesn't already have a Leek and is one of the relevant species, Leek can appear - return party.some( - p => - !p.getHeldItems().some(i => i instanceof SpeciesCritBoosterModifier) && - (checkedSpecies.includes(p.getSpeciesForm(true).speciesId) || - (p.isFusion() && checkedSpecies.includes(p.getFusionSpeciesForm(true).speciesId))), - ) - ? 12 - : 0; - }, - 12, - ), - new WeightedModifierType( - modifierTypes.TOXIC_ORB, - (party: Pokemon[]) => { - return party.some(p => { - const isHoldingOrb = p.getHeldItems().some(i => i.type.id === "FLAME_ORB" || i.type.id === "TOXIC_ORB"); - - if (!isHoldingOrb) { - const moveset = p - .getMoveset(true) - .filter(m => !isNullOrUndefined(m)) - .map(m => m.moveId); - const canSetStatus = p.canSetStatus(StatusEffect.TOXIC, true, true, null, true); - - // Moves that take advantage of obtaining the actual status effect - const hasStatusMoves = [MoveId.FACADE, MoveId.PSYCHO_SHIFT].some(m => moveset.includes(m)); - // Moves that take advantage of being able to give the target a status orb - // TODO: Take moves (Trick, Fling, Switcheroo) from comment when they are implemented - const hasItemMoves = [ - /* MoveId.TRICK, MoveId.FLING, MoveId.SWITCHEROO */ - ].some(m => moveset.includes(m)); - - if (canSetStatus) { - // Abilities that take advantage of obtaining the actual status effect, separated based on specificity to the orb - const hasGeneralAbility = [ - AbilityId.QUICK_FEET, - AbilityId.GUTS, - AbilityId.MARVEL_SCALE, - AbilityId.MAGIC_GUARD, - ].some(a => p.hasAbility(a, false, true)); - const hasSpecificAbility = [AbilityId.TOXIC_BOOST, AbilityId.POISON_HEAL].some(a => - p.hasAbility(a, false, true), - ); - const hasOppositeAbility = [AbilityId.FLARE_BOOST].some(a => p.hasAbility(a, false, true)); - - return hasSpecificAbility || (hasGeneralAbility && !hasOppositeAbility) || hasStatusMoves; - } - return hasItemMoves; - } - - return false; - }) - ? 10 - : 0; - }, - 10, - ), - new WeightedModifierType( - modifierTypes.FLAME_ORB, - (party: Pokemon[]) => { - return party.some(p => { - const isHoldingOrb = p.getHeldItems().some(i => i.type.id === "FLAME_ORB" || i.type.id === "TOXIC_ORB"); - - if (!isHoldingOrb) { - const moveset = p - .getMoveset(true) - .filter(m => !isNullOrUndefined(m)) - .map(m => m.moveId); - const canSetStatus = p.canSetStatus(StatusEffect.BURN, true, true, null, true); - - // Moves that take advantage of obtaining the actual status effect - const hasStatusMoves = [MoveId.FACADE, MoveId.PSYCHO_SHIFT].some(m => moveset.includes(m)); - // Moves that take advantage of being able to give the target a status orb - // TODO: Take moves (Trick, Fling, Switcheroo) from comment when they are implemented - const hasItemMoves = [ - /* MoveId.TRICK, MoveId.FLING, MoveId.SWITCHEROO */ - ].some(m => moveset.includes(m)); - - if (canSetStatus) { - // Abilities that take advantage of obtaining the actual status effect, separated based on specificity to the orb - const hasGeneralAbility = [ - AbilityId.QUICK_FEET, - AbilityId.GUTS, - AbilityId.MARVEL_SCALE, - AbilityId.MAGIC_GUARD, - ].some(a => p.hasAbility(a, false, true)); - const hasSpecificAbility = [AbilityId.FLARE_BOOST].some(a => p.hasAbility(a, false, true)); - const hasOppositeAbility = [AbilityId.TOXIC_BOOST, AbilityId.POISON_HEAL].some(a => - p.hasAbility(a, false, true), - ); - - return hasSpecificAbility || (hasGeneralAbility && !hasOppositeAbility) || hasStatusMoves; - } - return hasItemMoves; - } - - return false; - }) - ? 10 - : 0; - }, - 10, - ), - new WeightedModifierType( - modifierTypes.MYSTICAL_ROCK, - (party: Pokemon[]) => { - return party.some(p => { - let isHoldingMax = false; - for (const i of p.getHeldItems()) { - if (i.type.id === "MYSTICAL_ROCK") { - isHoldingMax = i.getStackCount() === i.getMaxStackCount(); - break; - } - } - - if (!isHoldingMax) { - const moveset = p.getMoveset(true).map(m => m.moveId); - - const hasAbility = [ - AbilityId.DROUGHT, - AbilityId.ORICHALCUM_PULSE, - AbilityId.DRIZZLE, - AbilityId.SAND_STREAM, - AbilityId.SAND_SPIT, - AbilityId.SNOW_WARNING, - AbilityId.ELECTRIC_SURGE, - AbilityId.HADRON_ENGINE, - AbilityId.PSYCHIC_SURGE, - AbilityId.GRASSY_SURGE, - AbilityId.SEED_SOWER, - AbilityId.MISTY_SURGE, - ].some(a => p.hasAbility(a, false, true)); - - const hasMoves = [ - MoveId.SUNNY_DAY, - MoveId.RAIN_DANCE, - MoveId.SANDSTORM, - MoveId.SNOWSCAPE, - MoveId.HAIL, - MoveId.CHILLY_RECEPTION, - MoveId.ELECTRIC_TERRAIN, - MoveId.PSYCHIC_TERRAIN, - MoveId.GRASSY_TERRAIN, - MoveId.MISTY_TERRAIN, - ].some(m => moveset.includes(m)); - - return hasAbility || hasMoves; - } - return false; - }) - ? 10 - : 0; - }, - 10, - ), - new WeightedModifierType(modifierTypes.REVIVER_SEED, 4), - new WeightedModifierType(modifierTypes.CANDY_JAR, skipInLastClassicWaveOrDefault(5)), - new WeightedModifierType(modifierTypes.ATTACK_TYPE_BOOSTER, 9), - new WeightedModifierType(modifierTypes.TM_ULTRA, 11), - new WeightedModifierType(modifierTypes.RARER_CANDY, 4), - new WeightedModifierType(modifierTypes.GOLDEN_PUNCH, skipInLastClassicWaveOrDefault(2)), - new WeightedModifierType(modifierTypes.IV_SCANNER, skipInLastClassicWaveOrDefault(4)), - new WeightedModifierType(modifierTypes.EXP_CHARM, skipInLastClassicWaveOrDefault(8)), - new WeightedModifierType(modifierTypes.EXP_SHARE, skipInLastClassicWaveOrDefault(10)), - new WeightedModifierType( - modifierTypes.TERA_ORB, - () => - !globalScene.gameMode.isClassic - ? Math.min(Math.max(Math.floor(globalScene.currentBattle.waveIndex / 50) * 2, 1), 4) - : 0, - 4, - ), - new WeightedModifierType(modifierTypes.QUICK_CLAW, 3), - new WeightedModifierType(modifierTypes.WIDE_LENS, 7), - ].map(m => { - m.setTier(ModifierTier.ULTRA); - return m; - }), - [ModifierTier.ROGUE]: [ - new WeightedModifierType(modifierTypes.ROGUE_BALL, () => (hasMaximumBalls(PokeballType.ROGUE_BALL) ? 0 : 16), 16), - new WeightedModifierType(modifierTypes.RELIC_GOLD, skipInLastClassicWaveOrDefault(2)), - new WeightedModifierType(modifierTypes.LEFTOVERS, 3), - new WeightedModifierType(modifierTypes.SHELL_BELL, 3), - new WeightedModifierType(modifierTypes.BERRY_POUCH, 4), - new WeightedModifierType(modifierTypes.GRIP_CLAW, 5), - new WeightedModifierType(modifierTypes.SCOPE_LENS, 4), - new WeightedModifierType(modifierTypes.BATON, 2), - new WeightedModifierType(modifierTypes.SOUL_DEW, 7), - new WeightedModifierType(modifierTypes.CATCHING_CHARM, () => (!globalScene.gameMode.isClassic ? 4 : 0), 4), - new WeightedModifierType(modifierTypes.ABILITY_CHARM, skipInClassicAfterWave(189, 6)), - new WeightedModifierType(modifierTypes.FOCUS_BAND, 5), - new WeightedModifierType(modifierTypes.KINGS_ROCK, 3), - new WeightedModifierType(modifierTypes.LOCK_CAPSULE, () => (globalScene.gameMode.isClassic ? 0 : 3)), - new WeightedModifierType(modifierTypes.SUPER_EXP_CHARM, skipInLastClassicWaveOrDefault(8)), - new WeightedModifierType( - modifierTypes.RARE_FORM_CHANGE_ITEM, - () => Math.min(Math.ceil(globalScene.currentBattle.waveIndex / 50), 4) * 6, - 24, - ), - new WeightedModifierType( - modifierTypes.MEGA_BRACELET, - () => Math.min(Math.ceil(globalScene.currentBattle.waveIndex / 50), 4) * 9, - 36, - ), - new WeightedModifierType( - modifierTypes.DYNAMAX_BAND, - () => Math.min(Math.ceil(globalScene.currentBattle.waveIndex / 50), 4) * 9, - 36, - ), - new WeightedModifierType( - modifierTypes.VOUCHER_PLUS, - (_party: Pokemon[], rerollCount: number) => - !globalScene.gameMode.isDaily ? Math.max(3 - rerollCount * 1, 0) : 0, - 3, - ), - ].map(m => { - m.setTier(ModifierTier.ROGUE); - return m; - }), - [ModifierTier.MASTER]: [ - new WeightedModifierType(modifierTypes.MASTER_BALL, () => (hasMaximumBalls(PokeballType.MASTER_BALL) ? 0 : 24), 24), - new WeightedModifierType(modifierTypes.SHINY_CHARM, 14), - new WeightedModifierType(modifierTypes.HEALING_CHARM, 18), - new WeightedModifierType(modifierTypes.MULTI_LENS, 18), - new WeightedModifierType( - modifierTypes.VOUCHER_PREMIUM, - (_party: Pokemon[], rerollCount: number) => - !globalScene.gameMode.isDaily && !globalScene.gameMode.isEndless && !globalScene.gameMode.isSplicedOnly - ? Math.max(5 - rerollCount * 2, 0) - : 0, - 5, - ), - new WeightedModifierType( - modifierTypes.DNA_SPLICERS, - (party: Pokemon[]) => - !(globalScene.gameMode.isClassic && timedEventManager.areFusionsBoosted()) && - !globalScene.gameMode.isSplicedOnly && - party.filter(p => !p.fusionSpecies).length > 1 - ? 24 - : 0, - 24, - ), - new WeightedModifierType( - modifierTypes.MINI_BLACK_HOLE, - () => - globalScene.gameMode.isDaily || - (!globalScene.gameMode.isFreshStartChallenge() && globalScene.gameData.isUnlocked(Unlockables.MINI_BLACK_HOLE)) - ? 1 - : 0, - 1, - ), - ].map(m => { - m.setTier(ModifierTier.MASTER); - return m; - }), -}; - -const wildModifierPool: ModifierPool = { - [ModifierTier.COMMON]: [new WeightedModifierType(modifierTypes.BERRY, 1)].map(m => { - m.setTier(ModifierTier.COMMON); - return m; - }), - [ModifierTier.GREAT]: [new WeightedModifierType(modifierTypes.BASE_STAT_BOOSTER, 1)].map(m => { - m.setTier(ModifierTier.GREAT); - return m; - }), - [ModifierTier.ULTRA]: [ - new WeightedModifierType(modifierTypes.ATTACK_TYPE_BOOSTER, 10), - new WeightedModifierType(modifierTypes.WHITE_HERB, 0), - ].map(m => { - m.setTier(ModifierTier.ULTRA); - return m; - }), - [ModifierTier.ROGUE]: [new WeightedModifierType(modifierTypes.LUCKY_EGG, 4)].map(m => { - m.setTier(ModifierTier.ROGUE); - return m; - }), - [ModifierTier.MASTER]: [new WeightedModifierType(modifierTypes.GOLDEN_EGG, 1)].map(m => { - m.setTier(ModifierTier.MASTER); - return m; - }), -}; - -const trainerModifierPool: ModifierPool = { - [ModifierTier.COMMON]: [ - new WeightedModifierType(modifierTypes.BERRY, 8), - new WeightedModifierType(modifierTypes.BASE_STAT_BOOSTER, 3), - ].map(m => { - m.setTier(ModifierTier.COMMON); - return m; - }), - [ModifierTier.GREAT]: [new WeightedModifierType(modifierTypes.BASE_STAT_BOOSTER, 3)].map(m => { - m.setTier(ModifierTier.GREAT); - return m; - }), - [ModifierTier.ULTRA]: [ - new WeightedModifierType(modifierTypes.ATTACK_TYPE_BOOSTER, 10), - new WeightedModifierType(modifierTypes.WHITE_HERB, 0), - ].map(m => { - m.setTier(ModifierTier.ULTRA); - return m; - }), - [ModifierTier.ROGUE]: [ - new WeightedModifierType(modifierTypes.FOCUS_BAND, 2), - new WeightedModifierType(modifierTypes.LUCKY_EGG, 4), - new WeightedModifierType(modifierTypes.QUICK_CLAW, 1), - new WeightedModifierType(modifierTypes.GRIP_CLAW, 1), - new WeightedModifierType(modifierTypes.WIDE_LENS, 1), - ].map(m => { - m.setTier(ModifierTier.ROGUE); - return m; - }), - [ModifierTier.MASTER]: [ - new WeightedModifierType(modifierTypes.KINGS_ROCK, 1), - new WeightedModifierType(modifierTypes.LEFTOVERS, 1), - new WeightedModifierType(modifierTypes.SHELL_BELL, 1), - new WeightedModifierType(modifierTypes.SCOPE_LENS, 1), - ].map(m => { - m.setTier(ModifierTier.MASTER); - return m; - }), -}; - -const enemyBuffModifierPool: ModifierPool = { - [ModifierTier.COMMON]: [ - new WeightedModifierType(modifierTypes.ENEMY_DAMAGE_BOOSTER, 9), - new WeightedModifierType(modifierTypes.ENEMY_DAMAGE_REDUCTION, 9), - new WeightedModifierType(modifierTypes.ENEMY_ATTACK_POISON_CHANCE, 3), - new WeightedModifierType(modifierTypes.ENEMY_ATTACK_PARALYZE_CHANCE, 3), - new WeightedModifierType(modifierTypes.ENEMY_ATTACK_BURN_CHANCE, 3), - new WeightedModifierType(modifierTypes.ENEMY_STATUS_EFFECT_HEAL_CHANCE, 9), - new WeightedModifierType(modifierTypes.ENEMY_ENDURE_CHANCE, 4), - new WeightedModifierType(modifierTypes.ENEMY_FUSED_CHANCE, 1), - ].map(m => { - m.setTier(ModifierTier.COMMON); - return m; - }), - [ModifierTier.GREAT]: [ - new WeightedModifierType(modifierTypes.ENEMY_DAMAGE_BOOSTER, 5), - new WeightedModifierType(modifierTypes.ENEMY_DAMAGE_REDUCTION, 5), - new WeightedModifierType(modifierTypes.ENEMY_STATUS_EFFECT_HEAL_CHANCE, 5), - new WeightedModifierType(modifierTypes.ENEMY_ENDURE_CHANCE, 5), - new WeightedModifierType(modifierTypes.ENEMY_FUSED_CHANCE, 1), - ].map(m => { - m.setTier(ModifierTier.GREAT); - return m; - }), - [ModifierTier.ULTRA]: [ - new WeightedModifierType(modifierTypes.ENEMY_DAMAGE_BOOSTER, 10), - new WeightedModifierType(modifierTypes.ENEMY_DAMAGE_REDUCTION, 10), - new WeightedModifierType(modifierTypes.ENEMY_HEAL, 10), - new WeightedModifierType(modifierTypes.ENEMY_STATUS_EFFECT_HEAL_CHANCE, 10), - new WeightedModifierType(modifierTypes.ENEMY_ENDURE_CHANCE, 10), - new WeightedModifierType(modifierTypes.ENEMY_FUSED_CHANCE, 5), - ].map(m => { - m.setTier(ModifierTier.ULTRA); - return m; - }), - [ModifierTier.ROGUE]: [].map((m: WeightedModifierType) => { - m.setTier(ModifierTier.ROGUE); - return m; - }), - [ModifierTier.MASTER]: [].map((m: WeightedModifierType) => { - m.setTier(ModifierTier.MASTER); - return m; - }), -}; - -const dailyStarterModifierPool: ModifierPool = { - [ModifierTier.COMMON]: [ - new WeightedModifierType(modifierTypes.BASE_STAT_BOOSTER, 1), - new WeightedModifierType(modifierTypes.BERRY, 3), - ].map(m => { - m.setTier(ModifierTier.COMMON); - return m; - }), - [ModifierTier.GREAT]: [new WeightedModifierType(modifierTypes.ATTACK_TYPE_BOOSTER, 5)].map(m => { - m.setTier(ModifierTier.GREAT); - return m; - }), - [ModifierTier.ULTRA]: [ - new WeightedModifierType(modifierTypes.REVIVER_SEED, 4), - new WeightedModifierType(modifierTypes.SOOTHE_BELL, 1), - new WeightedModifierType(modifierTypes.SOUL_DEW, 1), - new WeightedModifierType(modifierTypes.GOLDEN_PUNCH, 1), - ].map(m => { - m.setTier(ModifierTier.ULTRA); - return m; - }), - [ModifierTier.ROGUE]: [ - new WeightedModifierType(modifierTypes.GRIP_CLAW, 5), - new WeightedModifierType(modifierTypes.BATON, 2), - new WeightedModifierType(modifierTypes.FOCUS_BAND, 5), - new WeightedModifierType(modifierTypes.QUICK_CLAW, 3), - new WeightedModifierType(modifierTypes.KINGS_ROCK, 3), - ].map(m => { - m.setTier(ModifierTier.ROGUE); - return m; - }), - [ModifierTier.MASTER]: [ - new WeightedModifierType(modifierTypes.LEFTOVERS, 1), - new WeightedModifierType(modifierTypes.SHELL_BELL, 1), - ].map(m => { - m.setTier(ModifierTier.MASTER); - return m; - }), -}; - -export function getModifierType(modifierTypeFunc: ModifierTypeFunc): ModifierType { - const modifierType = modifierTypeFunc(); - if (!modifierType.id) { - modifierType.id = Object.keys(modifierTypes).find(k => modifierTypes[k] === modifierTypeFunc)!; // TODO: is this bang correct? - } - return modifierType; +export interface ModifierPool { + [tier: string]: WeightedModifierType[]; } let modifierPoolThresholds = {}; @@ -3179,28 +2391,6 @@ let enemyBuffModifierPoolThresholds = {}; // biome-ignore lint/correctness/noUnusedVariables: TODO explain why this is marked as OK let enemyBuffIgnoredPoolIndexes = {}; -export function getModifierPoolForType(poolType: ModifierPoolType): ModifierPool { - let pool: ModifierPool; - switch (poolType) { - case ModifierPoolType.PLAYER: - pool = modifierPool; - break; - case ModifierPoolType.WILD: - pool = wildModifierPool; - break; - case ModifierPoolType.TRAINER: - pool = trainerModifierPool; - break; - case ModifierPoolType.ENEMY_BUFF: - pool = enemyBuffModifierPool; - break; - case ModifierPoolType.DAILY_STARTER: - pool = dailyStarterModifierPool; - break; - } - return pool; -} - const tierWeights = [768 / 1024, 195 / 1024, 48 / 1024, 12 / 1024, 1 / 1024]; /** * Allows a unit test to check if an item exists in the Modifier Pool. Checks the pool directly, rather than attempting to reroll for the item. @@ -3314,7 +2504,7 @@ export interface CustomModifierSettings { } export function getModifierTypeFuncById(id: string): ModifierTypeFunc { - return modifierTypes[id]; + return modifierTypeInitObj[id]; } /** @@ -3367,12 +2557,12 @@ export function getPlayerModifierTypeOptions( customModifierSettings.guaranteedModifierTypeFuncs.length > 0 ) { customModifierSettings.guaranteedModifierTypeFuncs!.forEach((mod, _i) => { - const modifierId = Object.keys(modifierTypes).find(k => modifierTypes[k] === mod) as string; - let guaranteedMod: ModifierType = modifierTypes[modifierId]?.(); + const modifierId = Object.keys(modifierTypeInitObj).find(k => modifierTypeInitObj[k] === mod) as string; + let guaranteedMod: ModifierType = modifierTypeInitObj[modifierId]?.(); // Populates item id and tier guaranteedMod = guaranteedMod - .withIdFromFunc(modifierTypes[modifierId]) + .withIdFromFunc(modifierTypeInitObj[modifierId]) .withTierFromPool(ModifierPoolType.PLAYER, party); const modType = @@ -3451,7 +2641,7 @@ export function overridePlayerModifierTypeOptions(options: ModifierTypeOption[], const minLength = Math.min(options.length, Overrides.ITEM_REWARD_OVERRIDE.length); for (let i = 0; i < minLength; i++) { const override: ModifierOverride = Overrides.ITEM_REWARD_OVERRIDE[i]; - const modifierFunc = modifierTypes[override.name]; + const modifierFunc = modifierTypeInitObj[override.name]; let modifierType: ModifierType | null = modifierFunc(); if (modifierType instanceof ModifierTypeGenerator) { @@ -3472,29 +2662,29 @@ export function getPlayerShopModifierTypeOptionsForWave(waveIndex: number, baseC const options = [ [ - new ModifierTypeOption(modifierTypes.POTION(), 0, baseCost * 0.2), - new ModifierTypeOption(modifierTypes.ETHER(), 0, baseCost * 0.4), - new ModifierTypeOption(modifierTypes.REVIVE(), 0, baseCost * 2), + new ModifierTypeOption(modifierTypeInitObj.POTION(), 0, baseCost * 0.2), + new ModifierTypeOption(modifierTypeInitObj.ETHER(), 0, baseCost * 0.4), + new ModifierTypeOption(modifierTypeInitObj.REVIVE(), 0, baseCost * 2), ], [ - new ModifierTypeOption(modifierTypes.SUPER_POTION(), 0, baseCost * 0.45), - new ModifierTypeOption(modifierTypes.FULL_HEAL(), 0, baseCost), + new ModifierTypeOption(modifierTypeInitObj.SUPER_POTION(), 0, baseCost * 0.45), + new ModifierTypeOption(modifierTypeInitObj.FULL_HEAL(), 0, baseCost), ], [ - new ModifierTypeOption(modifierTypes.ELIXIR(), 0, baseCost), - new ModifierTypeOption(modifierTypes.MAX_ETHER(), 0, baseCost), + new ModifierTypeOption(modifierTypeInitObj.ELIXIR(), 0, baseCost), + new ModifierTypeOption(modifierTypeInitObj.MAX_ETHER(), 0, baseCost), ], [ - new ModifierTypeOption(modifierTypes.HYPER_POTION(), 0, baseCost * 0.8), - new ModifierTypeOption(modifierTypes.MAX_REVIVE(), 0, baseCost * 2.75), - new ModifierTypeOption(modifierTypes.MEMORY_MUSHROOM(), 0, baseCost * 4), + new ModifierTypeOption(modifierTypeInitObj.HYPER_POTION(), 0, baseCost * 0.8), + new ModifierTypeOption(modifierTypeInitObj.MAX_REVIVE(), 0, baseCost * 2.75), + new ModifierTypeOption(modifierTypeInitObj.MEMORY_MUSHROOM(), 0, baseCost * 4), ], [ - new ModifierTypeOption(modifierTypes.MAX_POTION(), 0, baseCost * 1.5), - new ModifierTypeOption(modifierTypes.MAX_ELIXIR(), 0, baseCost * 2.5), + new ModifierTypeOption(modifierTypeInitObj.MAX_POTION(), 0, baseCost * 1.5), + new ModifierTypeOption(modifierTypeInitObj.MAX_ELIXIR(), 0, baseCost * 2.5), ], - [new ModifierTypeOption(modifierTypes.FULL_RESTORE(), 0, baseCost * 2.25)], - [new ModifierTypeOption(modifierTypes.SACRED_ASH(), 0, baseCost * 10)], + [new ModifierTypeOption(modifierTypeInitObj.FULL_RESTORE(), 0, baseCost * 2.25)], + [new ModifierTypeOption(modifierTypeInitObj.SACRED_ASH(), 0, baseCost * 10)], ]; return options.slice(0, Math.ceil(Math.max(waveIndex + 10, 0) / 30)).flat(); } @@ -3549,7 +2739,7 @@ export function getEnemyModifierTypesForWave( ?.type as PokemonHeldItemModifierType, ); if (!(waveIndex % 1000)) { - ret.push(getModifierType(modifierTypes.MINI_BLACK_HOLE) as PokemonHeldItemModifierType); + ret.push(getModifierType(modifierTypeInitObj.MINI_BLACK_HOLE) as PokemonHeldItemModifierType); } return ret; } @@ -3650,7 +2840,7 @@ function getNewModifierTypeOption( } tier += upgradeCount; - while (tier && (!modifierPool.hasOwnProperty(tier) || !modifierPool[tier].length)) { + while (tier && (!pool.hasOwnProperty(tier) || !pool[tier].length)) { tier--; if (upgradeCount) { upgradeCount--; @@ -3661,7 +2851,7 @@ function getNewModifierTypeOption( if (tier < ModifierTier.MASTER && allowLuckUpgrades) { const partyLuckValue = getPartyLuckValue(party); const upgradeOdds = Math.floor(128 / ((partyLuckValue + 4) / 4)); - while (modifierPool.hasOwnProperty(tier + upgradeCount + 1) && modifierPool[tier + upgradeCount + 1].length) { + while (pool.hasOwnProperty(tier + upgradeCount + 1) && pool[tier + upgradeCount + 1].length) { if (randSeedInt(upgradeOdds) < 4) { upgradeCount++; } else { @@ -3711,6 +2901,7 @@ function getNewModifierTypeOption( } export function getDefaultModifierTypeForTier(tier: ModifierTier): ModifierType { + const modifierPool = getModifierPoolForType(ModifierPoolType.PLAYER); let modifierType: ModifierType | WeightedModifierType = modifierPool[tier || ModifierTier.COMMON][0]; if (modifierType instanceof WeightedModifierType) { modifierType = (modifierType as WeightedModifierType).modifierType; @@ -3779,3 +2970,30 @@ export function getLuckTextTint(luckValue: number): number { } return getModifierTierTextTint(modifierTier); } + +export function initModifierTypes() { + for (const [key, value] of Object.entries(modifierTypeInitObj)) { + modifierTypes[key] = value; + } +} + +// TODO: If necessary, add the rest of the modifier types here. +// For now, doing the minimal work until the modifier rework lands. +const ModifierTypeConstructorMap = Object.freeze({ + ModifierTypeGenerator, + PokemonHeldItemModifierType, +}); + +/** + * Map of of modifier type strings to their constructor type + */ +export type ModifierTypeConstructorMap = typeof ModifierTypeConstructorMap; + +/** + * Map of modifier type strings to their instance type + */ +export type ModifierTypeInstanceMap = { + [K in keyof ModifierTypeConstructorMap]: InstanceType; +}; + +export type ModifierTypeString = keyof ModifierTypeConstructorMap; diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index bfea061b1e8..54b7323569a 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -1,12 +1,13 @@ import { FusionSpeciesFormEvolution, pokemonEvolutions } from "#app/data/balance/pokemon-evolutions"; import { getBerryEffectFunc, getBerryPredicate } from "#app/data/berry"; import { getLevelTotalExp } from "#app/data/exp"; -import { allMoves } from "#app/data/data-lists"; +import { allMoves, modifierTypes } from "#app/data/data-lists"; import { MAX_PER_TYPE_POKEBALLS } from "#app/data/pokeball"; import { SpeciesFormChangeItemTrigger } from "#app/data/pokemon-forms/form-change-triggers"; import type { FormChangeItem } from "#enums/form-change-item"; import { getStatusEffectHealText } from "#app/data/status-effect"; -import Pokemon, { type PlayerPokemon } from "#app/field/pokemon"; +import type { PlayerPokemon } from "#app/field/pokemon"; +import type Pokemon from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; import Overrides from "#app/overrides"; import { LearnMoveType } from "#enums/learn-move-type"; @@ -24,33 +25,26 @@ import { type PermanentStat, type TempBattleStat, BATTLE_STATS, Stat, TEMP_BATTL import { StatusEffect } from "#enums/status-effect"; import type { PokemonType } from "#enums/pokemon-type"; import i18next from "i18next"; -import { - type DoubleBattleChanceBoosterModifierType, - type EvolutionItemModifierType, - type FormChangeItemModifierType, - type ModifierOverride, - type ModifierType, - type PokemonBaseStatTotalModifierType, - type PokemonExpBoosterModifierType, - type PokemonFriendshipBoosterModifierType, - type PokemonMoveAccuracyBoosterModifierType, - type PokemonMultiHitModifierType, - type TerastallizeModifierType, - type TmModifierType, - getModifierType, - ModifierTypeGenerator, - modifierTypes, - PokemonHeldItemModifierType, +import type { + DoubleBattleChanceBoosterModifierType, + EvolutionItemModifierType, + FormChangeItemModifierType, + ModifierOverride, + ModifierType, + PokemonBaseStatTotalModifierType, + PokemonExpBoosterModifierType, + PokemonFriendshipBoosterModifierType, + PokemonMoveAccuracyBoosterModifierType, + PokemonMultiHitModifierType, + TerastallizeModifierType, + TmModifierType, } from "./modifier-type"; +import { getModifierType } from "#app/utils/modifier-utils"; import { Color, ShadowColor } from "#enums/color"; import { FRIENDSHIP_GAIN_FROM_RARE_CANDY } from "#app/data/balance/starters"; -import { - applyAbAttrs, - applyPostItemLostAbAttrs, - CommanderAbAttr, - PostItemLostAbAttr, -} from "#app/data/abilities/ability"; +import { applyAbAttrs, applyPostItemLostAbAttrs } from "#app/data/abilities/apply-ab-attrs"; import { globalScene } from "#app/global-scene"; +import type { ModifierInstanceMap, ModifierString } from "#app/@types/modifier-types"; export type ModifierPredicate = (modifier: Modifier) => boolean; @@ -164,6 +158,23 @@ export abstract class Modifier { this.type = type; } + /** + * Return whether this modifier is of the given class + * + * @remarks + * Used to avoid requiring the caller to have imported the specific modifier class, avoiding circular dependencies. + * + * @param modifier - The modifier to check against + * @returns Whether the modiifer is an instance of the given type + */ + public is(modifier: T): this is ModifierInstanceMap[T] { + const targetModifier = ModifierClassMap[modifier]; + if (!targetModifier) { + return false; + } + return this instanceof targetModifier; + } + match(_modifier: Modifier): boolean { return false; } @@ -188,6 +199,11 @@ export abstract class PersistentModifier extends Modifier { public stackCount: number; public virtualStackCount: number; + /** This field does not exist at runtime and must not be used. + * Its sole purpose is to ensure that typescript is able to properly narrow when the `is` method is called. + */ + private declare _: never; + constructor(type: ModifierType, stackCount = 1) { super(type); this.stackCount = stackCount; @@ -756,6 +772,10 @@ export abstract class PokemonHeldItemModifier extends PersistentModifier { return this.getMaxHeldItemCount(pokemon); } + getSpecies(): SpeciesId | null { + return null; + } + abstract getMaxHeldItemCount(pokemon?: Pokemon): number; } @@ -902,27 +922,14 @@ export class EvoTrackerModifier extends PokemonHeldItemModifier { return true; } - getIconStackText(virtual?: boolean): Phaser.GameObjects.BitmapText | null { - if (this.getMaxStackCount() === 1 || (virtual && !this.virtualStackCount)) { - return null; - } + getIconStackText(_virtual?: boolean): Phaser.GameObjects.BitmapText | null { + const pokemon = this.getPokemon(); - const pokemon = globalScene.getPokemonById(this.pokemonId); + const count = (pokemon?.getPersistentTreasureCount() || 0) + this.getStackCount(); - this.stackCount = pokemon - ? 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); + const text = globalScene.add.bitmapText(10, 15, "item-count", count.toString(), 11); text.letterSpacing = -0.5; - if (this.getStackCount() >= this.required) { + if (count >= this.required) { text.setTint(0xf89890); } text.setOrigin(0, 0); @@ -930,18 +937,13 @@ export class EvoTrackerModifier extends PokemonHeldItemModifier { return text; } - 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; + getMaxHeldItemCount(_pokemon: Pokemon): number { return 999; } + + override getSpecies(): SpeciesId { + return this.species; + } } /** @@ -1593,7 +1595,7 @@ export class BypassSpeedChanceModifier extends PokemonHeldItemModifier { doBypassSpeed.value = true; const isCommandFight = globalScene.currentBattle.turnCommands[pokemon.getBattlerIndex()]?.command === Command.FIGHT; - const hasQuickClaw = this.type instanceof PokemonHeldItemModifierType && this.type.id === "QUICK_CLAW"; + const hasQuickClaw = this.type.is("PokemonHeldItemModifierType") && this.type.id === "QUICK_CLAW"; if (isCommandFight && hasQuickClaw) { globalScene.phaseManager.queueMessage( @@ -1877,7 +1879,7 @@ export class BerryModifier extends PokemonHeldItemModifier { // munch the berry and trigger unburden-like effects getBerryEffectFunc(this.berryType)(pokemon); - applyPostItemLostAbAttrs(PostItemLostAbAttr, pokemon, false); + applyPostItemLostAbAttrs("PostItemLostAbAttr", pokemon, false); // Update berry eaten trackers for Belch, Harvest, Cud Chew, etc. // Don't recover it if we proc berry pouch (no item duplication) @@ -1965,7 +1967,7 @@ export class PokemonInstantReviveModifier extends PokemonHeldItemModifier { // Reapply Commander on the Pokemon's side of the field, if applicable const field = pokemon.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField(); for (const p of field) { - applyAbAttrs(CommanderAbAttr, p, null, false); + applyAbAttrs("CommanderAbAttr", p, null, false); } return true; } @@ -2386,19 +2388,13 @@ export class EvolutionItemModifier extends ConsumablePokemonModifier { override apply(playerPokemon: PlayerPokemon): boolean { let matchingEvolution = pokemonEvolutions.hasOwnProperty(playerPokemon.species.speciesId) ? pokemonEvolutions[playerPokemon.species.speciesId].find( - e => - e.item === this.type.evolutionItem && - (e.evoFormKey === null || (e.preFormKey || "") === playerPokemon.getFormKey()) && - (!e.condition || e.condition.predicate(playerPokemon)), + e => e.evoItem === this.type.evolutionItem && e.validate(playerPokemon, false, e.item!), ) : null; if (!matchingEvolution && playerPokemon.isFusion()) { matchingEvolution = pokemonEvolutions[playerPokemon.fusionSpecies!.speciesId].find( - e => - e.item === this.type.evolutionItem && // TODO: is the bang correct? - (e.evoFormKey === null || (e.preFormKey || "") === playerPokemon.getFusionFormKey()) && - (!e.condition || e.condition.predicate(playerPokemon)), + e => e.evoItem === this.type.evolutionItem && e.validate(playerPokemon, true, e.item!), ); if (matchingEvolution) { matchingEvolution = new FusionSpeciesFormEvolution(playerPokemon.species.speciesId, matchingEvolution); @@ -2918,11 +2914,10 @@ export class MoneyRewardModifier extends ConsumableModifier { globalScene.getPlayerParty().map(p => { if (p.species?.speciesId === SpeciesId.GIMMIGHOUL || p.fusionSpecies?.speciesId === SpeciesId.GIMMIGHOUL) { - p.evoCounter - ? (p.evoCounter += Math.min(Math.floor(this.moneyMultiplier), 3)) - : (p.evoCounter = Math.min(Math.floor(this.moneyMultiplier), 3)); + const factor = Math.min(Math.floor(this.moneyMultiplier), 3); const modifier = getModifierType(modifierTypes.EVOLUTION_TRACKER_GIMMIGHOUL).newModifier( p, + factor, ) as EvoTrackerModifier; globalScene.addModifier(modifier); } @@ -3215,7 +3210,7 @@ export abstract class HeldItemTransferModifier extends PokemonHeldItemModifier { * @returns the opponents of the source {@linkcode Pokemon} */ getTargets(pokemon?: Pokemon, ..._args: unknown[]): Pokemon[] { - return pokemon instanceof Pokemon ? pokemon.getOpponents() : []; + return pokemon?.getOpponents?.() ?? []; } /** @@ -3787,7 +3782,7 @@ export function overrideModifiers(isPlayer = true): void { const modifierFunc = modifierTypes[item.name]; let modifierType: ModifierType | null = modifierFunc(); - if (modifierType instanceof ModifierTypeGenerator) { + if (modifierType.is("ModifierTypeGenerator")) { const pregenArgs = "type" in item && item.type !== null ? [item.type] : undefined; modifierType = modifierType.generateType([], pregenArgs); } @@ -3829,7 +3824,7 @@ export function overrideHeldItems(pokemon: Pokemon, isPlayer = true): void { let modifierType: ModifierType | null = modifierFunc(); const qty = item.count || 1; - if (modifierType instanceof ModifierTypeGenerator) { + if (modifierType.is("ModifierTypeGenerator")) { const pregenArgs = "type" in item && item.type !== null ? [item.type] : undefined; modifierType = modifierType.generateType([], pregenArgs); } @@ -3847,3 +3842,102 @@ export function overrideHeldItems(pokemon: Pokemon, isPlayer = true): void { } } } + +/** + * Private map from modifier strings to their constructors. + * + * @remarks + * Used for {@linkcode Modifier.is} to check if a modifier is of a certain type without + * requiring modifier types to be imported in every file. + */ +const ModifierClassMap = Object.freeze({ + PersistentModifier, + ConsumableModifier, + AddPokeballModifier, + AddVoucherModifier, + LapsingPersistentModifier, + DoubleBattleChanceBoosterModifier, + TempStatStageBoosterModifier, + TempCritBoosterModifier, + MapModifier, + MegaEvolutionAccessModifier, + GigantamaxAccessModifier, + TerastallizeAccessModifier, + PokemonHeldItemModifier, + LapsingPokemonHeldItemModifier, + BaseStatModifier, + EvoTrackerModifier, + PokemonBaseStatTotalModifier, + PokemonBaseStatFlatModifier, + PokemonIncrementingStatModifier, + StatBoosterModifier, + SpeciesStatBoosterModifier, + CritBoosterModifier, + SpeciesCritBoosterModifier, + AttackTypeBoosterModifier, + SurviveDamageModifier, + BypassSpeedChanceModifier, + FlinchChanceModifier, + TurnHealModifier, + TurnStatusEffectModifier, + HitHealModifier, + LevelIncrementBoosterModifier, + BerryModifier, + PreserveBerryModifier, + PokemonInstantReviveModifier, + ResetNegativeStatStageModifier, + FieldEffectModifier, + ConsumablePokemonModifier, + TerrastalizeModifier, + PokemonHpRestoreModifier, + PokemonStatusHealModifier, + ConsumablePokemonMoveModifier, + PokemonPpRestoreModifier, + PokemonAllMovePpRestoreModifier, + PokemonPpUpModifier, + PokemonNatureChangeModifier, + PokemonLevelIncrementModifier, + TmModifier, + RememberMoveModifier, + EvolutionItemModifier, + FusePokemonModifier, + MultipleParticipantExpBonusModifier, + HealingBoosterModifier, + ExpBoosterModifier, + PokemonExpBoosterModifier, + ExpShareModifier, + ExpBalanceModifier, + PokemonFriendshipBoosterModifier, + PokemonNatureWeightModifier, + PokemonMoveAccuracyBoosterModifier, + PokemonMultiHitModifier, + PokemonFormChangeItemModifier, + MoneyRewardModifier, + DamageMoneyRewardModifier, + MoneyInterestModifier, + HiddenAbilityRateBoosterModifier, + ShinyRateBoosterModifier, + CriticalCatchChanceBoosterModifier, + LockModifierTiersModifier, + HealShopCostModifier, + BoostBugSpawnModifier, + SwitchEffectTransferModifier, + HeldItemTransferModifier, + TurnHeldItemTransferModifier, + ContactHeldItemTransferChanceModifier, + IvScannerModifier, + ExtraModifierModifier, + TempExtraModifierModifier, + EnemyPersistentModifier, + EnemyDamageMultiplierModifier, + EnemyDamageBoosterModifier, + EnemyDamageReducerModifier, + EnemyTurnHealModifier, + EnemyAttackStatusEffectChanceModifier, + EnemyStatusEffectHealChanceModifier, + EnemyEndureChanceModifier, + EnemyFusionChanceModifier, + MoneyMultiplierModifier, +}); + +export type ModifierConstructorMap = typeof ModifierClassMap; diff --git a/src/overrides.ts b/src/overrides.ts index 1fbca8e8de1..b390b9fa70f 100644 --- a/src/overrides.ts +++ b/src/overrides.ts @@ -4,7 +4,7 @@ import { Gender } from "#app/data/gender"; import { FormChangeItem } from "#enums/form-change-item"; import { type ModifierOverride } from "#app/modifier/modifier-type"; import { Variant } from "#app/sprites/variant"; -import { Unlockables } from "#app/system/unlockables"; +import { Unlockables } from "#enums/unlockables"; import { AbilityId } from "#enums/ability-id"; import { BattleType } from "#enums/battle-type"; import { BerryType } from "#enums/berry-type"; @@ -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, or any negative number to set it to 9 quadrillion (effectively disabling it). */ 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 */ readonly STARTING_MONEY_OVERRIDE: number = 0; /** Sets all shop item prices to 0 */ @@ -272,7 +276,7 @@ class DefaultOverrides { /** * Set all non-scripted waves to use the selected battle type. - * + * * Ignored if set to {@linkcode BattleType.TRAINER} and `DISABLE_STANDARD_TRAINERS_OVERRIDE` is `true`. */ readonly BATTLE_TYPE_OVERRIDE: Exclude | null = null; @@ -285,17 +289,17 @@ export const defaultOverrides = new DefaultOverrides(); export default { ...defaultOverrides, - ...overrides + ...overrides, } satisfies InstanceType; export type BattleStyle = "double" | "single" | "even-doubles" | "odd-doubles"; export type RandomTrainerOverride = { /** The Type of trainer to force */ - trainerType: Exclude, + trainerType: Exclude; /* 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 */ -export type OverridesType = typeof DefaultOverrides; \ No newline at end of file +export type OverridesType = typeof DefaultOverrides; diff --git a/src/phase-manager.ts b/src/phase-manager.ts index 230e0331caf..9390e6dd75d 100644 --- a/src/phase-manager.ts +++ b/src/phase-manager.ts @@ -2,6 +2,7 @@ import type { Phase } from "#app/phase"; import type { default as Pokemon } from "#app/field/pokemon"; import type { PhaseMap, PhaseString } from "./@types/phase-types"; import { globalScene } from "#app/global-scene"; +import { ActivatePriorityQueuePhase } from "#app/phases/activate-priority-queue-phase"; import { AddEnemyBuffModifierPhase } from "#app/phases/add-enemy-buff-modifier-phase"; import { AttemptCapturePhase } from "#app/phases/attempt-capture-phase"; import { AttemptRunPhase } from "#app/phases/attempt-run-phase"; @@ -11,7 +12,9 @@ import { CheckStatusEffectPhase } from "#app/phases/check-status-effect-phase"; import { CheckSwitchPhase } from "#app/phases/check-switch-phase"; import { CommandPhase } from "#app/phases/command-phase"; import { CommonAnimPhase } from "#app/phases/common-anim-phase"; +import { coerceArray, type Constructor } from "#app/utils/common"; import { DamageAnimPhase } from "#app/phases/damage-anim-phase"; +import type { DynamicPhaseType } from "#enums/dynamic-phase-type"; import { EggHatchPhase } from "#app/phases/egg-hatch-phase"; import { EggLapsePhase } from "#app/phases/egg-lapse-phase"; import { EggSummaryPhase } from "#app/phases/egg-summary-phase"; @@ -55,6 +58,7 @@ import { NextEncounterPhase } from "#app/phases/next-encounter-phase"; import { ObtainStatusEffectPhase } from "#app/phases/obtain-status-effect-phase"; import { PartyExpPhase } from "#app/phases/party-exp-phase"; import { PartyHealPhase } from "#app/phases/party-heal-phase"; +import { type PhasePriorityQueue, PostSummonPhasePriorityQueue } from "#app/data/phase-priority-queue"; import { PokemonAnimPhase } from "#app/phases/pokemon-anim-phase"; import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase"; import { PokemonTransformPhase } from "#app/phases/pokemon-transform-phase"; @@ -111,6 +115,7 @@ import { WeatherEffectPhase } from "#app/phases/weather-effect-phase"; * This allows for easy creation of new phases without needing to import each phase individually. */ const PHASES = Object.freeze({ + ActivatePriorityQueuePhase, AddEnemyBuffModifierPhase, AttemptCapturePhase, AttemptRunPhase, @@ -222,9 +227,19 @@ export class PhaseManager { private phaseQueuePrependSpliceIndex = -1; private nextCommandPhaseQueue: Phase[] = []; + /** Storage for {@linkcode PhasePriorityQueue}s which hold phases whose order dynamically changes */ + private dynamicPhaseQueues: PhasePriorityQueue[]; + /** Parallel array to {@linkcode dynamicPhaseQueues} - matches phase types to their queues */ + private dynamicPhaseTypes: Constructor[]; + private currentPhase: Phase | null = null; private standbyPhase: Phase | null = null; + constructor() { + this.dynamicPhaseQueues = [new PostSummonPhasePriorityQueue()]; + this.dynamicPhaseTypes = [PostSummonPhase]; + } + /* Phase Functions */ getCurrentPhase(): Phase | null { return this.currentPhase; @@ -254,7 +269,11 @@ export class PhaseManager { * @param defer boolean on which queue to add to, defaults to false, and adds to phaseQueue */ pushPhase(phase: Phase, defer = false): void { - (!defer ? this.phaseQueue : this.nextCommandPhaseQueue).push(phase); + if (this.getDynamicPhaseType(phase) !== undefined) { + this.pushDynamicPhase(phase); + } else { + (!defer ? this.phaseQueue : this.nextCommandPhaseQueue).push(phase); + } } /** @@ -283,6 +302,7 @@ export class PhaseManager { for (const queue of [this.phaseQueue, this.phaseQueuePrepend, this.conditionalQueue, this.nextCommandPhaseQueue]) { queue.splice(0, queue.length); } + this.dynamicPhaseQueues.forEach(queue => queue.clear()); this.currentPhase = null; this.standbyPhase = null; this.clearPhaseQueueSplice(); @@ -333,8 +353,9 @@ export class PhaseManager { this.currentPhase = this.phaseQueue.shift() ?? null; + const unactivatedConditionalPhases: [() => boolean, Phase][] = []; // Check if there are any conditional phases queued - if (this.conditionalQueue?.length) { + while (this.conditionalQueue?.length) { // Retrieve the first conditional phase from the queue const conditionalPhase = this.conditionalQueue.shift(); // Evaluate the condition associated with the phase @@ -343,11 +364,12 @@ export class PhaseManager { this.pushPhase(conditionalPhase[1]); } else if (conditionalPhase) { // If the condition is not met, re-add the phase back to the front of the conditional queue - this.conditionalQueue.unshift(conditionalPhase); + unactivatedConditionalPhases.push(conditionalPhase); } else { console.warn("condition phase is undefined/null!", conditionalPhase); } } + this.conditionalQueue.push(...unactivatedConditionalPhases); if (this.currentPhase) { console.log(`%cStart Phase ${this.currentPhase.constructor.name}`, "color:green;"); @@ -375,7 +397,7 @@ export class PhaseManager { * @returns the found phase or undefined if none found */ findPhase

(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 { @@ -416,9 +438,7 @@ export class PhaseManager { * @returns boolean if a targetPhase was found and added */ prependToPhase(phase: Phase | Phase[], targetPhase: PhaseString): boolean { - if (!Array.isArray(phase)) { - phase = [phase]; - } + phase = coerceArray(phase); const target = PHASES[targetPhase]; const targetIndex = this.phaseQueue.findIndex(ph => ph instanceof target); @@ -431,17 +451,16 @@ export class PhaseManager { } /** - * Attempt to add the input phase(s) to index after target phase in the {@linkcode phaseQueue}, else simply calls {@linkcode unshiftPhase()} - * @param phase - The phase(s) to be added - * @param targetPhase - The phase to search for in phaseQueue + * Tries to add the input phase(s) to index after target phase in the {@linkcode phaseQueue}, else simply calls {@linkcode unshiftPhase()} + * @param phase {@linkcode Phase} the phase(s) to be added + * @param targetPhase {@linkcode Phase} the type of phase to search for in {@linkcode phaseQueue} + * @param condition Condition the target phase must meet to be appended to * @returns `true` if a `targetPhase` was found to append to */ - appendToPhase(phase: Phase | Phase[], targetPhase: PhaseString): boolean { - if (!Array.isArray(phase)) { - phase = [phase]; - } + appendToPhase(phase: Phase | Phase[], targetPhase: PhaseString, condition?: (p: Phase) => boolean): boolean { + phase = coerceArray(phase); const target = PHASES[targetPhase]; - const targetIndex = this.phaseQueue.findIndex(ph => ph instanceof target); + const targetIndex = this.phaseQueue.findIndex(ph => ph instanceof target && (!condition || condition(ph))); if (targetIndex !== -1 && this.phaseQueue.length > targetIndex) { this.phaseQueue.splice(targetIndex + 1, 0, ...phase); @@ -451,6 +470,68 @@ export class PhaseManager { return false; } + /** + * Checks a phase and returns the matching {@linkcode DynamicPhaseType}, or undefined if it does not match one + * @param phase The phase to check + * @returns The corresponding {@linkcode DynamicPhaseType} or `undefined` + */ + public getDynamicPhaseType(phase: Phase | null): DynamicPhaseType | undefined { + let phaseType: DynamicPhaseType | undefined; + this.dynamicPhaseTypes.forEach((cls, index) => { + if (phase instanceof cls) { + phaseType = index; + } + }); + + return phaseType; + } + + /** + * Pushes a phase onto its corresponding dynamic queue and marks the activation point in {@linkcode phaseQueue} + * + * The {@linkcode ActivatePriorityQueuePhase} will run the top phase in the dynamic queue (not necessarily {@linkcode phase}) + * @param phase The phase to push + */ + public pushDynamicPhase(phase: Phase): void { + const type = this.getDynamicPhaseType(phase); + if (type === undefined) { + return; + } + + this.pushPhase(new ActivatePriorityQueuePhase(type)); + this.dynamicPhaseQueues[type].push(phase); + } + + /** + * Unshifts the top phase from the corresponding dynamic queue onto {@linkcode phaseQueue} + * @param type {@linkcode DynamicPhaseType} The type of dynamic phase to start + */ + public startDynamicPhaseType(type: DynamicPhaseType): void { + const phase = this.dynamicPhaseQueues[type].pop(); + if (phase) { + this.unshiftPhase(phase); + } + } + + /** + * Unshifts an {@linkcode ActivatePriorityQueuePhase} for {@linkcode phase}, then pushes {@linkcode phase} to its dynamic queue + * + * This is the same as {@linkcode pushDynamicPhase}, except the activation phase is unshifted + * + * {@linkcode phase} is not guaranteed to be the next phase from the queue to run (if the queue is not empty) + * @param phase The phase to add + * @returns + */ + public startDynamicPhase(phase: Phase): void { + const type = this.getDynamicPhaseType(phase); + if (type === undefined) { + return; + } + + this.unshiftPhase(new ActivatePriorityQueuePhase(type)); + this.dynamicPhaseQueues[type].push(phase); + } + /** * Adds a MessagePhase, either to PhaseQueuePrepend or nextCommandPhaseQueue * @param message - string for MessagePhase @@ -578,4 +659,11 @@ export class PhaseManager { ): boolean { return this.appendToPhase(this.create(phase, ...args), targetPhase); } + + public startNewDynamicPhase( + phase: T, + ...args: ConstructorParameters + ): void { + this.startDynamicPhase(this.create(phase, ...args)); + } } diff --git a/src/phases/activate-priority-queue-phase.ts b/src/phases/activate-priority-queue-phase.ts new file mode 100644 index 00000000000..df42c491676 --- /dev/null +++ b/src/phases/activate-priority-queue-phase.ts @@ -0,0 +1,23 @@ +import type { DynamicPhaseType } from "#enums/dynamic-phase-type"; +import { globalScene } from "#app/global-scene"; +import { Phase } from "#app/phase"; + +export class ActivatePriorityQueuePhase extends Phase { + public readonly phaseName = "ActivatePriorityQueuePhase"; + private type: DynamicPhaseType; + + constructor(type: DynamicPhaseType) { + super(); + this.type = type; + } + + override start() { + super.start(); + globalScene.phaseManager.startDynamicPhaseType(this.type); + this.end(); + } + + public getType(): DynamicPhaseType { + return this.type; + } +} diff --git a/src/phases/add-enemy-buff-modifier-phase.ts b/src/phases/add-enemy-buff-modifier-phase.ts index 28eaf0dc4df..218a3a653ff 100644 --- a/src/phases/add-enemy-buff-modifier-phase.ts +++ b/src/phases/add-enemy-buff-modifier-phase.ts @@ -1,9 +1,6 @@ -import { ModifierTier } from "#app/modifier/modifier-tier"; -import { - regenerateModifierPoolThresholds, - ModifierPoolType, - getEnemyBuffModifierForWave, -} from "#app/modifier/modifier-type"; +import { ModifierTier } from "#enums/modifier-tier"; +import { regenerateModifierPoolThresholds, getEnemyBuffModifierForWave } from "#app/modifier/modifier-type"; +import { ModifierPoolType } from "#enums/modifier-pool-type"; import { EnemyPersistentModifier } from "#app/modifier/modifier"; import { Phase } from "#app/phase"; import { globalScene } from "#app/global-scene"; diff --git a/src/phases/attempt-run-phase.ts b/src/phases/attempt-run-phase.ts index 525be8c21ab..5e24f3474a6 100644 --- a/src/phases/attempt-run-phase.ts +++ b/src/phases/attempt-run-phase.ts @@ -1,9 +1,4 @@ -import { - applyAbAttrs, - applyPreLeaveFieldAbAttrs, - PreLeaveFieldAbAttr, - RunSuccessAbAttr, -} from "#app/data/abilities/ability"; +import { applyAbAttrs, applyPreLeaveFieldAbAttrs } from "#app/data/abilities/apply-ab-attrs"; import { Stat } from "#enums/stat"; import { StatusEffect } from "#enums/status-effect"; import type { PlayerPokemon, EnemyPokemon } from "#app/field/pokemon"; @@ -30,10 +25,10 @@ export class AttemptRunPhase extends PokemonPhase { this.attemptRunAway(playerField, enemyField, escapeChance); - applyAbAttrs(RunSuccessAbAttr, playerPokemon, null, false, escapeChance); + applyAbAttrs("RunSuccessAbAttr", playerPokemon, null, false, escapeChance); if (playerPokemon.randBattleSeedInt(100) < escapeChance.value && !this.forceFailEscape) { - enemyField.forEach(enemyPokemon => applyPreLeaveFieldAbAttrs(PreLeaveFieldAbAttr, enemyPokemon)); + enemyField.forEach(enemyPokemon => applyPreLeaveFieldAbAttrs("PreLeaveFieldAbAttr", enemyPokemon)); globalScene.playSound("se/flee"); globalScene.phaseManager.queueMessage(i18next.t("battle:runAwaySuccess"), null, true, 500); diff --git a/src/phases/battle-end-phase.ts b/src/phases/battle-end-phase.ts index e169de58cb3..e1bf4c2296c 100644 --- a/src/phases/battle-end-phase.ts +++ b/src/phases/battle-end-phase.ts @@ -1,5 +1,5 @@ import { globalScene } from "#app/global-scene"; -import { applyPostBattleAbAttrs, PostBattleAbAttr } from "#app/data/abilities/ability"; +import { applyPostBattleAbAttrs } from "#app/data/abilities/apply-ab-attrs"; import { LapsingPersistentModifier, LapsingPokemonHeldItemModifier } from "#app/modifier/modifier"; import { BattlePhase } from "./battle-phase"; @@ -65,7 +65,7 @@ export class BattleEndPhase extends BattlePhase { } for (const pokemon of globalScene.getPokemonAllowedInBattle()) { - applyPostBattleAbAttrs(PostBattleAbAttr, pokemon, false, this.isVictory); + applyPostBattleAbAttrs("PostBattleAbAttr", pokemon, false, this.isVictory); } if (globalScene.currentBattle.moneyScattered) { diff --git a/src/phases/berry-phase.ts b/src/phases/berry-phase.ts index cc990d1e2ae..c126f3306b9 100644 --- a/src/phases/berry-phase.ts +++ b/src/phases/berry-phase.ts @@ -1,9 +1,4 @@ -import { - applyAbAttrs, - PreventBerryUseAbAttr, - HealFromBerryUseAbAttr, - RepeatBerryNextTurnAbAttr, -} from "#app/data/abilities/ability"; +import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs"; import { CommonAnim } from "#enums/move-anims-common"; import { BerryUsedEvent } from "#app/events/battle-scene"; import { getPokemonNameWithAffix } from "#app/messages"; @@ -25,7 +20,7 @@ export class BerryPhase extends FieldPhase { this.executeForAll(pokemon => { this.eatBerries(pokemon); - applyAbAttrs(RepeatBerryNextTurnAbAttr, pokemon, null); + applyAbAttrs("RepeatBerryNextTurnAbAttr", pokemon, null); }); this.end(); @@ -47,7 +42,7 @@ export class BerryPhase extends FieldPhase { // TODO: If both opponents on field have unnerve, which one displays its message? const cancelled = new BooleanHolder(false); - pokemon.getOpponents().forEach(opp => applyAbAttrs(PreventBerryUseAbAttr, opp, cancelled)); + pokemon.getOpponents().forEach(opp => applyAbAttrs("PreventBerryUseAbAttr", opp, cancelled)); if (cancelled.value) { globalScene.phaseManager.queueMessage( i18next.t("abilityTriggers:preventBerryUse", { @@ -75,6 +70,6 @@ export class BerryPhase extends FieldPhase { globalScene.updateModifiers(pokemon.isPlayer()); // AbilityId.CHEEK_POUCH only works once per round of nom noms - applyAbAttrs(HealFromBerryUseAbAttr, pokemon, new BooleanHolder(false)); + applyAbAttrs("HealFromBerryUseAbAttr", pokemon, new BooleanHolder(false)); } } diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index d7264b4aff2..754d54de70a 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -22,6 +22,7 @@ import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; import { isNullOrUndefined } from "#app/utils/common"; import { ArenaTagSide } from "#enums/arena-tag-side"; import { ArenaTagType } from "#app/enums/arena-tag-type"; +import { isVirtual, isIgnorePP, MoveUseMode } from "#enums/move-use-mode"; export class CommandPhase extends FieldPhase { public readonly phaseName = "CommandPhase"; @@ -80,7 +81,7 @@ export class CommandPhase extends FieldPhase { ) { globalScene.currentBattle.turnCommands[this.fieldIndex] = { command: Command.FIGHT, - move: { move: MoveId.NONE, targets: [] }, + move: { move: MoveId.NONE, targets: [], useMode: MoveUseMode.NORMAL }, skip: true, }; } @@ -103,29 +104,31 @@ export class CommandPhase extends FieldPhase { moveQueue.length && moveQueue[0] && moveQueue[0].move && - !moveQueue[0].virtual && + !isVirtual(moveQueue[0].useMode) && (!playerPokemon.getMoveset().find(m => m.moveId === moveQueue[0].move) || !playerPokemon .getMoveset() [playerPokemon.getMoveset().findIndex(m => m.moveId === moveQueue[0].move)].isUsable( playerPokemon, - moveQueue[0].ignorePP, + isIgnorePP(moveQueue[0].useMode), )) ) { moveQueue.shift(); } + // TODO: Refactor this. I did a few simple find/replace matches but this is just ABHORRENTLY structured if (moveQueue.length > 0) { const queuedMove = moveQueue[0]; if (!queuedMove.move) { - this.handleCommand(Command.FIGHT, -1); + this.handleCommand(Command.FIGHT, -1, MoveUseMode.NORMAL); } else { const moveIndex = playerPokemon.getMoveset().findIndex(m => m.moveId === queuedMove.move); if ( - (moveIndex > -1 && playerPokemon.getMoveset()[moveIndex].isUsable(playerPokemon, queuedMove.ignorePP)) || - queuedMove.virtual + (moveIndex > -1 && + 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 { 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 { const playerPokemon = globalScene.getPlayerField()[this.fieldIndex]; let success = false; switch (command) { + // TODO: We don't need 2 args for this - moveUseMode is carried over from queuedMove case Command.TERA: - case Command.FIGHT: + case Command.FIGHT: { let useStruggle = false; const turnMove: TurnMove | undefined = args.length === 2 ? (args[1] as TurnMove) : undefined; if ( 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) ) { let moveId: MoveId; @@ -171,7 +179,7 @@ export class CommandPhase extends FieldPhase { const turnCommand: TurnCommand = { command: Command.FIGHT, cursor: cursor, - move: { move: moveId, targets: [], ignorePP: args[0] }, + move: { move: moveId, targets: [], useMode: args[0] }, args: args, }; const preTurnCommand: TurnCommand = { @@ -233,7 +241,8 @@ export class CommandPhase extends FieldPhase { ); } break; - case Command.BALL: + } + case Command.BALL: { const notInDex = globalScene .getEnemyField() @@ -337,8 +346,9 @@ export class CommandPhase extends FieldPhase { } } break; + } case Command.POKEMON: - case Command.RUN: + case Command.RUN: { const isSwitch = command === Command.POKEMON; const { currentBattle, arena } = globalScene; const mysteryEncounterFleeAllowed = currentBattle.mysteryEncounter?.fleeAllowed; @@ -445,6 +455,7 @@ export class CommandPhase extends FieldPhase { } } break; + } } if (success) { diff --git a/src/phases/encounter-phase.ts b/src/phases/encounter-phase.ts index 74623f947ee..f2c23384627 100644 --- a/src/phases/encounter-phase.ts +++ b/src/phases/encounter-phase.ts @@ -2,12 +2,7 @@ import { BattlerIndex } from "#enums/battler-index"; import { BattleType } from "#enums/battle-type"; import { globalScene } from "#app/global-scene"; import { PLAYER_PARTY_MAX_SIZE } from "#app/constants"; -import { - applyAbAttrs, - SyncEncounterNatureAbAttr, - applyPreSummonAbAttrs, - PreSummonAbAttr, -} from "#app/data/abilities/ability"; +import { applyAbAttrs, applyPreSummonAbAttrs } from "#app/data/abilities/apply-ab-attrs"; import { initEncounterAnims, loadEncounterAnimAssets } from "#app/data/battle-anims"; import { getCharVariantFromDialogue } from "#app/data/dialogue"; import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; @@ -20,7 +15,8 @@ import type Pokemon from "#app/field/pokemon"; import { FieldPosition } from "#enums/field-position"; import { getPokemonNameWithAffix } from "#app/messages"; import { BoostBugSpawnModifier, IvScannerModifier, TurnHeldItemTransferModifier } from "#app/modifier/modifier"; -import { ModifierPoolType, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type"; +import { regenerateModifierPoolThresholds } from "#app/modifier/modifier-type"; +import { ModifierPoolType } from "#enums/modifier-pool-type"; import Overrides from "#app/overrides"; import { BattlePhase } from "#app/phases/battle-phase"; import { achvs } from "#app/system/achv"; @@ -34,7 +30,7 @@ import { PlayerGender } from "#enums/player-gender"; import { SpeciesId } from "#enums/species-id"; import { overrideHeldItems, overrideModifiers } from "#app/modifier/modifier"; import i18next from "i18next"; -import { WEIGHT_INCREMENT_ON_SPAWN_MISS } from "#app/data/mystery-encounters/mystery-encounters"; +import { WEIGHT_INCREMENT_ON_SPAWN_MISS } from "#app/constants"; import { getNatureName } from "#app/data/nature"; export class EncounterPhase extends BattlePhase { @@ -132,7 +128,7 @@ export class EncounterPhase extends BattlePhase { .slice(0, !battle.double ? 1 : 2) .reverse() .forEach(playerPokemon => { - applyAbAttrs(SyncEncounterNatureAbAttr, playerPokemon, null, false, battle.enemyParty[e]); + applyAbAttrs("SyncEncounterNatureAbAttr", playerPokemon, null, false, battle.enemyParty[e]); }); } } @@ -253,7 +249,7 @@ export class EncounterPhase extends BattlePhase { if (e < (battle.double ? 2 : 1)) { if (battle.battleType === BattleType.WILD) { for (const pokemon of globalScene.getField()) { - applyPreSummonAbAttrs(PreSummonAbAttr, pokemon, []); + applyPreSummonAbAttrs("PreSummonAbAttr", pokemon, []); } globalScene.field.add(enemyPokemon); battle.seenEnemyPartyMemberIds.add(enemyPokemon.id); diff --git a/src/phases/faint-phase.ts b/src/phases/faint-phase.ts index ca23b20be12..675a198d096 100644 --- a/src/phases/faint-phase.ts +++ b/src/phases/faint-phase.ts @@ -5,10 +5,7 @@ import { applyPostFaintAbAttrs, applyPostKnockOutAbAttrs, applyPostVictoryAbAttrs, - PostFaintAbAttr, - PostKnockOutAbAttr, - PostVictoryAbAttr, -} from "#app/data/abilities/ability"; +} from "#app/data/abilities/apply-ab-attrs"; import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type"; import { battleSpecDialogue } from "#app/data/dialogue"; import { allMoves } from "#app/data/data-lists"; @@ -123,7 +120,7 @@ export class FaintPhase extends PokemonPhase { if (pokemon.turnData.attacksReceived?.length) { const lastAttack = pokemon.turnData.attacksReceived[0]; applyPostFaintAbAttrs( - PostFaintAbAttr, + "PostFaintAbAttr", pokemon, globalScene.getPokemonById(lastAttack.sourceId)!, new PokemonMove(lastAttack.move).getMove(), @@ -131,18 +128,18 @@ export class FaintPhase extends PokemonPhase { ); // TODO: is this bang correct? } else { //If killed by indirect damage, apply post-faint abilities without providing a last move - applyPostFaintAbAttrs(PostFaintAbAttr, pokemon); + applyPostFaintAbAttrs("PostFaintAbAttr", pokemon); } const alivePlayField = globalScene.getField(true); for (const p of alivePlayField) { - applyPostKnockOutAbAttrs(PostKnockOutAbAttr, p, pokemon); + applyPostKnockOutAbAttrs("PostKnockOutAbAttr", p, pokemon); } if (pokemon.turnData.attacksReceived?.length) { const defeatSource = this.source; if (defeatSource?.isOnField()) { - applyPostVictoryAbAttrs(PostVictoryAbAttr, defeatSource); + applyPostVictoryAbAttrs("PostVictoryAbAttr", defeatSource); const pvmove = allMoves[pokemon.turnData.attacksReceived[0].move]; const pvattrs = pvmove.getAttrs("PostVictoryStatStageChangeAttr"); if (pvattrs.length) { diff --git a/src/phases/game-over-phase.ts b/src/phases/game-over-phase.ts index 5fabc5cee81..eaf1e4f58d7 100644 --- a/src/phases/game-over-phase.ts +++ b/src/phases/game-over-phase.ts @@ -4,14 +4,14 @@ import { globalScene } from "#app/global-scene"; import { pokemonEvolutions } from "#app/data/balance/pokemon-evolutions"; import { getCharVariantFromDialogue } from "#app/data/dialogue"; 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 type Pokemon from "#app/field/pokemon"; -import { modifierTypes } from "#app/modifier/modifier-type"; +import { modifierTypes } from "#app/data/data-lists"; import { BattlePhase } from "#app/phases/battle-phase"; import type { EndCardPhase } from "#app/phases/end-card-phase"; import { achvs, ChallengeAchv } from "#app/system/achv"; -import { Unlockables } from "#app/system/unlockables"; +import { Unlockables } from "#enums/unlockables"; import { UiMode } from "#enums/ui-mode"; import { isLocal, isLocalServerConnected } from "#app/utils/common"; import { PlayerGender } from "#enums/player-gender"; diff --git a/src/phases/modifier-reward-phase.ts b/src/phases/modifier-reward-phase.ts index 83bd8704f59..29bbe215fc0 100644 --- a/src/phases/modifier-reward-phase.ts +++ b/src/phases/modifier-reward-phase.ts @@ -1,6 +1,7 @@ import { globalScene } from "#app/global-scene"; -import type { ModifierType, ModifierTypeFunc } from "#app/modifier/modifier-type"; -import { getModifierType } from "#app/modifier/modifier-type"; +import type { ModifierType } from "#app/modifier/modifier-type"; +import type { ModifierTypeFunc } from "#app/@types/modifier-types"; +import { getModifierType } from "#app/utils/modifier-utils"; import i18next from "i18next"; import { BattlePhase } from "./battle-phase"; diff --git a/src/phases/move-charge-phase.ts b/src/phases/move-charge-phase.ts index 15a98ebabd2..51b8fe96ff6 100644 --- a/src/phases/move-charge-phase.ts +++ b/src/phases/move-charge-phase.ts @@ -8,10 +8,11 @@ import { MoveResult } from "#enums/move-result"; import { BooleanHolder } from "#app/utils/common"; import { PokemonPhase } from "#app/phases/pokemon-phase"; 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). - * @extends {@linkcode PokemonPhase} */ export class MoveChargePhase extends PokemonPhase { 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) */ 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); this.move = move; this.targetIndex = targetIndex; + this.useMode = useMode; } public override start() { @@ -37,7 +49,8 @@ export class MoveChargePhase extends PokemonPhase { // immediately end this phase. if (!target || !move.isChargingMove()) { console.warn("Invalid parameters for MoveChargePhase"); - return super.end(); + super.end(); + return; } new MoveChargeAnim(move.chargeAnim, move.id, user).play(false, () => { @@ -52,29 +65,30 @@ export class MoveChargePhase extends PokemonPhase { /** Checks the move's instant charge conditions, then ends this phase. */ public override end() { 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 (instantCharge.value) { - // this MoveEndPhase will be duplicated by the queued MovePhase if not removed - 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, false); - } else { - user.getMoveQueue().push({ move: move.id, targets: [this.targetIndex] }); - } - - // Add this move's charging phase to the user's move history - user.pushMoveHistory({ - move: this.move.moveId, - targets: [this.targetIndex], - result: MoveResult.OTHER, - }); + // 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) { + globalScene.phaseManager.tryRemovePhase(phase => phase.is("MoveEndPhase") && phase.getPokemon() === user); + globalScene.phaseManager.unshiftNew("MovePhase", user, [this.targetIndex], this.move, this.useMode); + } else { + user.pushMoveQueue({ move: move.id, targets: [this.targetIndex], useMode: this.useMode }); } + + // Add this move's charging phase to the user's move history + user.pushMoveHistory({ + move: this.move.moveId, + targets: [this.targetIndex], + result: MoveResult.OTHER, + useMode: this.useMode, + }); + super.end(); } diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index 68c96ddce1e..d7da1ab996c 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -1,21 +1,12 @@ import { BattlerIndex } from "#enums/battler-index"; import { globalScene } from "#app/global-scene"; import { - AddSecondStrikeAbAttr, - AlwaysHitAbAttr, applyExecutedMoveAbAttrs, applyPostAttackAbAttrs, applyPostDamageAbAttrs, applyPostDefendAbAttrs, applyPreAttackAbAttrs, - ExecutedMoveAbAttr, - IgnoreMoveEffectsAbAttr, - MaxMultiHitAbAttr, - PostAttackAbAttr, - PostDamageAbAttr, - PostDefendAbAttr, - ReflectStatusMoveAbAttr, -} from "#app/data/abilities/ability"; +} from "#app/data/abilities/apply-ab-attrs"; import { ConditionalProtectTag } from "#app/data/arena-tag"; import { ArenaTagSide } from "#enums/arena-tag-side"; import { MoveAnim } from "#app/data/battle-anims"; @@ -63,20 +54,25 @@ import { HitCheckResult } from "#enums/hit-check-result"; import type Move from "#app/data/moves/move"; import { isFieldTargeted } from "#app/data/moves/move-utils"; 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 { public readonly phaseName = "MoveEffectPhase"; public move: Move; - private virtual = false; protected targets: BattlerIndex[]; - protected reflected = false; + protected useMode: MoveUseMode; /** The result of the hit check against each target */ 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; /** Is this the first strike of a move? */ @@ -84,19 +80,20 @@ export class MoveEffectPhase extends PokemonPhase { /** Is this the last strike of a move? */ 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[] = []; /** - * @param reflected Indicates that the move was reflected by the user due to magic coat or magic bounce - * @param virtual Indicates that the move is a virtual move (i.e. called by metronome) + * @param useMode - The {@linkcode MoveUseMode} corresponding to how this move was used. */ - constructor(battlerIndex: BattlerIndex, targets: BattlerIndex[], move: Move, reflected = false, virtual = false) { + constructor(battlerIndex: BattlerIndex, targets: BattlerIndex[], move: Move, useMode: MoveUseMode) { super(battlerIndex); 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 * with no party members available to switch in, then the right Pokemon takes the index @@ -167,7 +164,7 @@ export class MoveEffectPhase extends PokemonPhase { * 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 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 { const newTargets = this.move.isMultiTarget() @@ -179,7 +176,7 @@ export class MoveEffectPhase extends PokemonPhase { globalScene.phaseManager.create( "ShowAbilityPhase", target.getBattlerIndex(), - target.getPassiveAbility().hasAttr(ReflectStatusMoveAbAttr), + target.getPassiveAbility().hasAttr("ReflectStatusMoveAbAttr"), ), ); this.queuedPhases.push(globalScene.phaseManager.create("HideAbilityPhase")); @@ -190,10 +187,8 @@ export class MoveEffectPhase extends PokemonPhase { "MovePhase", target, newTargets, - new PokemonMove(this.move.id, 0, 0, true), - true, - true, - true, + new PokemonMove(this.move.id), + MoveUseMode.REFLECTED, ), ); } @@ -287,8 +282,18 @@ export class MoveEffectPhase extends PokemonPhase { const overridden = new BooleanHolder(false); const move = this.move; - // Assume single target for override - applyMoveAttrs("OverrideMoveEffectAttr", user, this.getFirstTarget() ?? null, move, overridden, this.virtual); + // Apply effects to override a move effect. + // 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 (overridden.value) { @@ -299,8 +304,8 @@ export class MoveEffectPhase extends PokemonPhase { // Lapse `MOVE_EFFECT` effects (i.e. semi-invulnerability) when applicable user.lapseTags(BattlerTagLapseType.MOVE_EFFECT); - // If the user is acting again (such as due to Instruct), reset hitsLeft/hitCount so that - // the move executes correctly (ensures all hits of a multi-hit are properly calculated) + // If the user is acting again (such as due to Instruct or Dancer), reset hitsLeft/hitCount and + // recalculate hit count for multi-hit moves. if (user.turnData.hitsLeft === 0 && user.turnData.hitCount > 0 && user.turnData.extraTurns > 0) { user.turnData.hitsLeft = -1; user.turnData.hitCount = 0; @@ -317,7 +322,7 @@ export class MoveEffectPhase extends PokemonPhase { // Assume single target for multi hit applyMoveAttrs("MultiHitAttr", user, this.getFirstTarget() ?? null, move, hitCount); // If Parental Bond is applicable, add another hit - applyPreAttackAbAttrs(AddSecondStrikeAbAttr, user, null, move, false, hitCount, null); + applyPreAttackAbAttrs("AddSecondStrikeAbAttr", user, null, move, false, hitCount, null); // If Multi-Lens is applicable, add hits equal to the number of held Multi-Lenses globalScene.applyModifiers(PokemonMultiHitModifier, user.isPlayer(), user, move.id, hitCount); // Set the user's relevant turnData fields to reflect the final hit count @@ -325,16 +330,11 @@ export class MoveEffectPhase extends PokemonPhase { 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 = { move: this.move.id, targets: this.targets, result: MoveResult.PENDING, - virtual: this.virtual, + useMode: this.useMode, }; const fieldMove = isFieldTargeted(move); @@ -370,7 +370,7 @@ export class MoveEffectPhase extends PokemonPhase { // Add to the move history entry if (this.firstHit) { user.pushMoveHistory(this.moveHistoryEntry); - applyExecutedMoveAbAttrs(ExecutedMoveAbAttr, user); + applyExecutedMoveAbAttrs("ExecutedMoveAbAttr", user); } try { @@ -399,29 +399,35 @@ export class MoveEffectPhase extends PokemonPhase { public override end(): void { const user = this.getUserPokemon(); - /** - * If this phase isn't for the invoked move's last strike, - * unshift another MoveEffectPhase for the next strike. - * 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 (!user) { + super.end(); + return; } + /** + * 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(); } @@ -431,10 +437,9 @@ export class MoveEffectPhase extends PokemonPhase { * @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 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 { - applyPostDefendAbAttrs(PostDefendAbAttr, target, user, this.move, hitResult); + applyPostDefendAbAttrs("PostDefendAbAttr", target, user, this.move, hitResult); target.lapseTags(BattlerTagLapseType.AFTER_HIT); } @@ -443,14 +448,17 @@ export class MoveEffectPhase extends PokemonPhase { * @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 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 { if (this.move.hasAttr("FlinchAttr")) { return; } - if (dealsDamage && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && !this.move.hitsSubstitute(user, target)) { + if ( + dealsDamage && + !target.hasAbilityWithAttr("IgnoreMoveEffectsAbAttr") && + !this.move.hitsSubstitute(user, target) + ) { const flinched = new BooleanHolder(false); globalScene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched); if (flinched.value) { @@ -463,8 +471,9 @@ export class MoveEffectPhase extends PokemonPhase { * @param user - The {@linkcode Pokemon} using this phase's invoked move * @param target - {@linkcode Pokemon} the target to check for protection * @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 */ const targetSide = target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; /** Has the invoked move been cancelled by conditional protection (e.g Quick Guard)? */ @@ -485,14 +494,15 @@ export class MoveEffectPhase extends PokemonPhase { ); } + // TODO: Break up this chunky boolean to make it more palatable return ( ![MoveTarget.ENEMY_SIDE, MoveTarget.BOTH_SIDES].includes(this.move.moveTarget) && (bypassIgnoreProtect.value || !this.move.doesFlagEffectApply({ flag: MoveFlags.IGNORE_PROTECT, user, target })) && (hasConditionalProtectApplied.value || (!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 && - target.findTags(t => t instanceof DamageProtectedTag).find(t => target.lapseTag(t.tagType)))) + target.findTags(t => t instanceof DamageProtectedTag).some(t => target.lapseTag(t.tagType)))) ); } @@ -552,7 +562,8 @@ export class MoveEffectPhase extends PokemonPhase { 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]; } @@ -580,7 +591,7 @@ export class MoveEffectPhase extends PokemonPhase { // Strikes after the first in a multi-strike move are guaranteed to hit, // unless the move is flagged to check all hits and the user does not have Skill Link. if (user.turnData.hitsLeft < user.turnData.hitCount) { - if (!move.hasFlag(MoveFlags.CHECK_ALL_HITS) || user.hasAbilityWithAttr(MaxMultiHitAbAttr)) { + if (!move.hasFlag(MoveFlags.CHECK_ALL_HITS) || user.hasAbilityWithAttr("MaxMultiHitAbAttr")) { return [HitCheckResult.HIT, effectiveness]; } } @@ -626,7 +637,7 @@ export class MoveEffectPhase extends PokemonPhase { if (!user) { return false; } - if (user.hasAbilityWithAttr(AlwaysHitAbAttr) || target.hasAbilityWithAttr(AlwaysHitAbAttr)) { + if (user.hasAbilityWithAttr("AlwaysHitAbAttr") || target.hasAbilityWithAttr("AlwaysHitAbAttr")) { return true; } if (this.move.hasAttr("ToxicAccuracyAttr") && user.isOfType(PokemonType.POISON)) { @@ -665,12 +676,17 @@ export class MoveEffectPhase extends PokemonPhase { 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[] { 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 { return this.getTargets()[0]; } @@ -710,9 +726,12 @@ export class MoveEffectPhase extends PokemonPhase { } } - /** @returns A new `MoveEffectPhase` with the same properties as this phase */ - protected getNewHitPhase(): MoveEffectPhase { - return new MoveEffectPhase(this.battlerIndex, this.targets, this.move, this.reflected, this.virtual); + /** + * Unshifts a new `MoveEffectPhase` with the same properties as this phase. + * 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 */ @@ -734,7 +753,6 @@ export class MoveEffectPhase extends PokemonPhase { * @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 * effects (if set to `true`) or targeted effects (if set to `false`). - * @returns a `Promise` applying the relevant move effects. */ protected triggerMoveEffects( triggerType: MoveEffectTrigger, @@ -780,6 +798,7 @@ export class MoveEffectPhase extends PokemonPhase { 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); if (!this.move.hitsSubstitute(user, target)) { this.applyOnTargetEffects(user, target, hitResult, firstTarget); @@ -789,7 +808,7 @@ export class MoveEffectPhase extends PokemonPhase { // Multi-hit check for Wimp Out/Emergency Exit if (user.turnData.hitCount > 1) { - applyPostDamageAbAttrs(PostDamageAbAttr, target, 0, target.hasPassive(), false, [], user); + applyPostDamageAbAttrs("PostDamageAbAttr", target, 0, target.hasPassive(), false, [], user); } } } @@ -802,7 +821,7 @@ export class MoveEffectPhase extends PokemonPhase { * @param effectiveness - The effectiveness of the move against the target */ 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} @@ -983,7 +1002,7 @@ export class MoveEffectPhase extends PokemonPhase { this.triggerMoveEffects(MoveEffectTrigger.POST_APPLY, user, target, firstTarget, false); this.applyHeldItemFlinchCheck(user, target, dealsDamage); this.applyOnGetHitAbEffects(user, target, hitResult); - applyPostAttackAbAttrs(PostAttackAbAttr, user, target, this.move, hitResult); + applyPostAttackAbAttrs("PostAttackAbAttr", user, target, this.move, hitResult); // We assume only enemy Pokemon are able to have the EnemyAttackStatusEffectChanceModifier from tokens if (!user.isPlayer() && this.move.is("AttackMove")) { diff --git a/src/phases/move-end-phase.ts b/src/phases/move-end-phase.ts index e5f87089fae..8c8f2ac5239 100644 --- a/src/phases/move-end-phase.ts +++ b/src/phases/move-end-phase.ts @@ -2,7 +2,7 @@ import { globalScene } from "#app/global-scene"; import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type"; import { PokemonPhase } from "./pokemon-phase"; import type { BattlerIndex } from "#enums/battler-index"; -import { applyPostSummonAbAttrs, PostSummonRemoveEffectAbAttr } from "#app/data/abilities/ability"; +import { applyPostSummonAbAttrs } from "#app/data/abilities/apply-ab-attrs"; import type Pokemon from "#app/field/pokemon"; export class MoveEndPhase extends PokemonPhase { @@ -25,12 +25,12 @@ export class MoveEndPhase extends PokemonPhase { if (!this.wasFollowUp && pokemon?.isActive(true)) { 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) + globalScene.arena.setIgnoreAbilities(false); for (const target of this.targets) { if (target) { - applyPostSummonAbAttrs(PostSummonRemoveEffectAbAttr, target); + applyPostSummonAbAttrs("PostSummonRemoveEffectAbAttr", target); } } diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index ca66ca745e7..41a1042387b 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -1,21 +1,10 @@ import { BattlerIndex } from "#enums/battler-index"; import { globalScene } from "#app/global-scene"; -import { - applyAbAttrs, - applyPostMoveUsedAbAttrs, - applyPreAttackAbAttrs, - BlockRedirectAbAttr, - IncreasePpAbAttr, - PokemonTypeChangeAbAttr, - PostMoveUsedAbAttr, - RedirectMoveAbAttr, - ReduceStatusEffectDurationAbAttr, -} from "#app/data/abilities/ability"; +import { applyAbAttrs, applyPostMoveUsedAbAttrs, applyPreAttackAbAttrs } from "#app/data/abilities/apply-ab-attrs"; import type { DelayedAttackTag } from "#app/data/arena-tag"; import { CommonAnim } from "#enums/move-anims-common"; import { CenterOfAttentionTag } from "#app/data/battler-tags"; 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 { allMoves } from "#app/data/data-lists"; import { MoveFlags } from "#enums/MoveFlags"; @@ -30,13 +19,14 @@ import { MoveResult } from "#enums/move-result"; import { getPokemonNameWithAffix } from "#app/messages"; import Overrides from "#app/overrides"; 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 { ArenaTagType } from "#enums/arena-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type"; import { MoveId } from "#enums/move-id"; import { StatusEffect } from "#enums/status-effect"; import i18next from "i18next"; +import { isVirtual, isIgnorePP, isReflected, MoveUseMode, isIgnoreStatus } from "#enums/move-use-mode"; import { frenzyMissFunc } from "#app/data/moves/move-utils"; export class MovePhase extends BattlePhase { @@ -44,17 +34,19 @@ export class MovePhase extends BattlePhase { protected _pokemon: Pokemon; protected _move: PokemonMove; protected _targets: BattlerIndex[]; - protected followUp: boolean; - protected ignorePp: boolean; + public readonly useMode: MoveUseMode; // Made public for quash protected forcedLast: boolean; + + /** Whether the current move should fail but still use PP */ protected failed = false; + /** Whether the current move should cancel and retain PP */ protected cancelled = false; - protected reflected = false; public get pokemon(): Pokemon { return this._pokemon; } + // TODO: Do we need public getters but only protected setters? protected set pokemon(pokemon: Pokemon) { this._pokemon = pokemon; } @@ -76,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. - * Follow-ups bypass a few failure conditions, including flinches, sleep/paralysis/freeze and volatile status checks, etc. - * @param reflected Indicates that the move was reflected by Magic Coat or Magic Bounce. - * Reflected moves cannot be reflected again and will not trigger Dancer. + * Create a new MovePhase for using moves. + * @param pokemon - The {@linkcode Pokemon} using the move + * @param move - The {@linkcode PokemonMove} to use + * @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, - followUp = false, - ignorePp = false, - reflected = false, - forcedLast = false, - ) { + constructor(pokemon: Pokemon, targets: BattlerIndex[], move: PokemonMove, useMode: MoveUseMode, forcedLast = false) { super(); this.pokemon = pokemon; this.targets = targets; this.move = move; - this.followUp = followUp; - this.ignorePp = ignorePp; - this.reflected = reflected; + this.useMode = useMode; 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 * @returns `true` if all the checks pass */ public canMove(ignoreDisableTags = false): boolean { return ( this.pokemon.isActive(true) && - this.move.isUsable(this.pokemon, this.ignorePp, ignoreDisableTags) && - !!this.targets.length + this.move.isUsable(this.pokemon, isIgnorePP(this.useMode), ignoreDisableTags) && + 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 { this.failed = true; } - /**Signifies the current move should cancel and retain PP */ + /** Signifies the current move should cancel and retain PP */ public cancel(): void { this.cancelled = true; } @@ -128,7 +111,7 @@ export class MovePhase extends BattlePhase { /** * Shows whether the current move has been forced to the end of the turn * Needed for speed order, see {@linkcode MoveId.QUASH} - * */ + */ public isForcedLast(): boolean { return this.forcedLast; } @@ -136,35 +119,37 @@ export class MovePhase extends BattlePhase { public start(): void { 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.pokemon.isActive(true)) { this.fail(); this.showMoveText(); this.showFailedText(); } - return this.end(); + this.end(); + return; } this.pokemon.turnData.acted = true; // 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.hitCount = 0; } // Check move to see if arena.ignoreAbilities should be true. - if (!this.followUp || this.reflected) { - if ( - this.move - .getMove() - .doesFlagEffectApply({ flag: MoveFlags.IGNORE_ABILITIES, user: this.pokemon, isFollowUp: this.followUp }) - ) { - globalScene.arena.setIgnoreAbilities(true, this.pokemon.getBattlerIndex()); - } + if ( + this.move.getMove().doesFlagEffectApply({ + flag: MoveFlags.IGNORE_ABILITIES, + user: this.pokemon, + isFollowUp: isVirtual(this.useMode), // Sunsteel strike and co. don't work when called indirectly + }) + ) { + globalScene.arena.setIgnoreAbilities(true, this.pokemon.getBattlerIndex()); } this.resolveRedirectTarget(); @@ -197,7 +182,7 @@ export class MovePhase extends BattlePhase { if ( (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.showFailedText(); @@ -210,83 +195,98 @@ export class MovePhase extends BattlePhase { } /** - * Handles {@link StatusEffect.SLEEP Sleep}/{@link StatusEffect.PARALYSIS Paralysis}/{@link StatusEffect.FREEZE Freeze} rolls and side effects. + * Handles {@link StatusEffect.SLEEP | Sleep}/{@link StatusEffect.PARALYSIS | Paralysis}/{@link StatusEffect.FREEZE | Freeze} rolls and side effects. */ protected resolvePreMoveStatusEffects(): void { - if (!this.followUp && this.pokemon.status && !this.pokemon.status.isPostTurn()) { - this.pokemon.status.incrementTurn(); - let activated = false; - let healed = false; + // 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; + } - switch (this.pokemon.status.effect) { - case StatusEffect.PARALYSIS: - activated = - (!this.pokemon.randBattleSeedInt(4) || Overrides.STATUS_ACTIVATION_OVERRIDE === true) && - Overrides.STATUS_ACTIVATION_OVERRIDE !== false; - break; - case StatusEffect.SLEEP: { - applyMoveAttrs("BypassSleepAttr", this.pokemon, null, this.move.getMove()); - const turnsRemaining = new NumberHolder(this.pokemon.status.sleepTurnsRemaining ?? 0); - applyAbAttrs( - ReduceStatusEffectDurationAbAttr, - this.pokemon, - null, - false, - this.pokemon.status.effect, - turnsRemaining, - ); - this.pokemon.status.sleepTurnsRemaining = turnsRemaining.value; - healed = this.pokemon.status.sleepTurnsRemaining <= 0; - activated = !healed && !this.pokemon.getTag(BattlerTagType.BYPASS_SLEEP); - break; - } - case StatusEffect.FREEZE: - healed = - !!this.move - .getMove() - .findAttr( - attr => - attr.is("HealStatusEffectAttr") && - attr.selfTarget && - (attr as unknown as HealStatusEffectAttr).isOfEffect(StatusEffect.FREEZE), - ) || - (!this.pokemon.randBattleSeedInt(5) && Overrides.STATUS_ACTIVATION_OVERRIDE !== true) || - Overrides.STATUS_ACTIVATION_OVERRIDE === false; + 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; + } - activated = !healed; - break; + this.pokemon.status.incrementTurn(); + + /** Whether to prevent us from using the move */ + let activated = false; + /** Whether to cure the status */ + let healed = false; + + switch (this.pokemon.status.effect) { + case StatusEffect.PARALYSIS: + activated = + (this.pokemon.randBattleSeedInt(4) === 0 || Overrides.STATUS_ACTIVATION_OVERRIDE === true) && + Overrides.STATUS_ACTIVATION_OVERRIDE !== false; + break; + case StatusEffect.SLEEP: { + applyMoveAttrs("BypassSleepAttr", this.pokemon, null, this.move.getMove()); + const turnsRemaining = new NumberHolder(this.pokemon.status.sleepTurnsRemaining ?? 0); + applyAbAttrs( + "ReduceStatusEffectDurationAbAttr", + this.pokemon, + null, + false, + this.pokemon.status.effect, + turnsRemaining, + ); + this.pokemon.status.sleepTurnsRemaining = turnsRemaining.value; + healed = this.pokemon.status.sleepTurnsRemaining <= 0; + activated = !healed && !this.pokemon.getTag(BattlerTagType.BYPASS_SLEEP); + break; } + case StatusEffect.FREEZE: + healed = + !!this.move + .getMove() + .findAttr( + attr => attr.is("HealStatusEffectAttr") && attr.selfTarget && attr.isOfEffect(StatusEffect.FREEZE), + ) || + (!this.pokemon.randBattleSeedInt(5) && Overrides.STATUS_ACTIVATION_OVERRIDE !== true) || + Overrides.STATUS_ACTIVATION_OVERRIDE === false; - if (activated) { - this.cancel(); - globalScene.phaseManager.queueMessage( - getStatusEffectActivationText(this.pokemon.status.effect, getPokemonNameWithAffix(this.pokemon)), - ); - globalScene.phaseManager.unshiftNew( - "CommonAnimPhase", - this.pokemon.getBattlerIndex(), - undefined, - CommonAnim.POISON + (this.pokemon.status.effect - 1), - ); - } else if (healed) { - globalScene.phaseManager.queueMessage( - getStatusEffectHealText(this.pokemon.status.effect, getPokemonNameWithAffix(this.pokemon)), - ); - this.pokemon.resetStatus(); - this.pokemon.updateInfo(); - } + activated = !healed; + break; + } + + if (activated) { + // Cancel move activation and play effect + this.cancel(); + globalScene.phaseManager.queueMessage( + getStatusEffectActivationText(this.pokemon.status.effect, getPokemonNameWithAffix(this.pokemon)), + ); + globalScene.phaseManager.unshiftNew( + "CommonAnimPhase", + this.pokemon.getBattlerIndex(), + undefined, + CommonAnim.POISON + (this.pokemon.status.effect - 1), // offset anim # by effect # + ); + } else if (healed) { + // cure status and play effect + globalScene.phaseManager.queueMessage( + getStatusEffectHealText(this.pokemon.status.effect, getPokemonNameWithAffix(this.pokemon)), + ); + this.pokemon.resetStatus(); + this.pokemon.updateInfo(); } } /** - * Lapse {@linkcode BattlerTagLapseType.PRE_MOVE PRE_MOVE} tags that trigger before a move is used, regardless of whether or not it failed. - * Also lapse {@linkcode BattlerTagLapseType.MOVE MOVE} tags if the move should be successful. + * 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 is successful and not called indirectly. */ protected lapsePreMoveAndMoveTags(): void { this.pokemon.lapseTags(BattlerTagLapseType.PRE_MOVE); // 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); } } @@ -294,11 +294,12 @@ export class MovePhase extends BattlePhase { protected useMove(): void { const targets = this.getActiveTargetPokemon(); const moveQueue = this.pokemon.getMoveQueue(); + const move = this.move.getMove(); // form changes happen even before we know that the move wll execute. globalScene.triggerPokemonFormChange(this.pokemon, SpeciesFormChangePreMoveTrigger); - const isDelayedAttack = this.move.getMove().hasAttr("DelayedAttackAttr"); + const isDelayedAttack = move.hasAttr("DelayedAttackAttr"); if (isDelayedAttack) { // Check the player side arena if future sight is active const futureSightTags = globalScene.arena.findTags(t => t.tagType === ArenaTagType.FUTURE_SIGHT); @@ -320,7 +321,8 @@ export class MovePhase extends BattlePhase { if (fail) { this.showMoveText(); this.showFailedText(); - return this.end(); + this.end(); + return; } } @@ -337,21 +339,21 @@ export class MovePhase extends BattlePhase { this.showMoveText(); } - if (moveQueue.length > 0) { - // Using .shift here clears out two turn moves once they've been used - this.ignorePp = moveQueue.shift()?.ignorePP ?? false; - } - + // Clear out any two turn moves once they've been used. + // TODO: Refactor move queues and remove this assignment; + // 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) { this.pokemon.lapseTag(BattlerTagType.CHARGING); } - // "commit" to using the move, deducting PP. - if (!this.ignorePp) { + if (!isIgnorePP(this.useMode)) { + // "commit" to using the move, deducting PP. const ppUsed = 1 + this.getPpIncreaseFromPressure(targets); 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)); } /** @@ -365,8 +367,6 @@ export class MovePhase extends BattlePhase { * 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 * TODO: is this sustainable? @@ -396,25 +396,24 @@ export class MovePhase extends BattlePhase { */ if (success) { const move = this.move.getMove(); - applyPreAttackAbAttrs(PokemonTypeChangeAbAttr, this.pokemon, null, move); + applyPreAttackAbAttrs("PokemonTypeChangeAbAttr", this.pokemon, null, move); globalScene.phaseManager.unshiftNew( "MoveEffectPhase", this.pokemon.getBattlerIndex(), this.targets, move, - this.reflected, - this.move.virtual, + this.useMode, ); } else { if ([MoveId.ROAR, MoveId.WHIRLWIND, MoveId.TRICK_OR_TREAT, MoveId.FORESTS_CURSE].includes(this.move.moveId)) { - applyPreAttackAbAttrs(PokemonTypeChangeAbAttr, this.pokemon, null, this.move.getMove()); + applyPreAttackAbAttrs("PokemonTypeChangeAbAttr", this.pokemon, null, this.move.getMove()); } this.pokemon.pushMoveHistory({ move: this.move.moveId, targets: this.targets, result: MoveResult.FAIL, - virtual: this.move.virtual, + useMode: this.useMode, }); const failureMessage = move.getFailedText(this.pokemon, targets[0], move); @@ -434,10 +433,12 @@ export class MovePhase extends BattlePhase { } // 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. - if (this.move.getMove().hasFlag(MoveFlags.DANCE_MOVE) && !this.followUp) { + // Note the MoveUseMode check here prevents an infinite Dancer loop. + 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 => { - applyPostMoveUsedAbAttrs(PostMoveUsedAbAttr, pokemon, this.move, this.pokemon, this.targets); + applyPostMoveUsedAbAttrs("PostMoveUsedAbAttr", pokemon, this.move, this.pokemon, this.targets); }); } } @@ -447,23 +448,16 @@ export class MovePhase extends BattlePhase { const move = this.move.getMove(); 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", - this.pokemon.getBattlerIndex(), - this.targets[0], - this.move, - ); - } else { + // Conditions currently assume single target + // TODO: Is this sustainable? + if (!move.applyConditions(this.pokemon, targets[0], move)) { this.pokemon.pushMoveHistory({ move: this.move.moveId, targets: this.targets, result: MoveResult.FAIL, - virtual: this.move.virtual, + useMode: this.useMode, }); const failureMessage = move.getFailedText(this.pokemon, targets[0], move); @@ -472,7 +466,19 @@ export class MovePhase extends BattlePhase { // Remove the user from its semi-invulnerable state (if applicable) 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, + ); } /** @@ -483,7 +489,7 @@ export class MovePhase extends BattlePhase { "MoveEndPhase", this.pokemon.getBattlerIndex(), this.getActiveTargetPokemon(), - this.followUp, + isVirtual(this.useMode), ); super.end(); @@ -498,7 +504,7 @@ export class MovePhase extends BattlePhase { public getPpIncreaseFromPressure(targets: Pokemon[]): number { const foesWithPressure = this.pokemon .getOpponents() - .filter(o => targets.includes(o) && o.isActive(true) && o.hasAbilityWithAttr(IncreasePpAbAttr)); + .filter(o => targets.includes(o) && o.isActive(true) && o.hasAbilityWithAttr("IncreasePpAbAttr")); return foesWithPressure.length; } @@ -516,7 +522,9 @@ export class MovePhase extends BattlePhase { globalScene .getField(true) .filter(p => p !== this.pokemon) - .forEach(p => applyAbAttrs(RedirectMoveAbAttr, p, null, false, this.move.moveId, redirectTarget, this.pokemon)); + .forEach(p => + applyAbAttrs("RedirectMoveAbAttr", p, null, false, this.move.moveId, redirectTarget, this.pokemon), + ); /** `true` if an Ability is responsible for redirecting the move to another target; `false` otherwise */ let redirectedByAbility = currentTarget !== redirectTarget.value; @@ -545,17 +553,17 @@ export class MovePhase extends BattlePhase { } }); - if (this.pokemon.hasAbilityWithAttr(BlockRedirectAbAttr)) { + if (this.pokemon.hasAbilityWithAttr("BlockRedirectAbAttr")) { redirectTarget.value = currentTarget; // TODO: Ability displays should be handled by the ability globalScene.phaseManager.queueAbilityDisplay( this.pokemon, - this.pokemon.getPassiveAbility().hasAttr(BlockRedirectAbAttr), + this.pokemon.getPassiveAbility().hasAttr("BlockRedirectAbAttr"), true, ); globalScene.phaseManager.queueAbilityDisplay( this.pokemon, - this.pokemon.getPassiveAbility().hasAttr(BlockRedirectAbAttr), + this.pokemon.getPassiveAbility().hasAttr("BlockRedirectAbAttr"), false, ); } @@ -613,7 +621,7 @@ export class MovePhase extends BattlePhase { protected handlePreMoveFailures(): void { if (this.cancelled || this.failed) { if (this.failed) { - const ppUsed = this.ignorePp ? 0 : 1; + const ppUsed = isIgnorePP(this.useMode) ? 0 : 1; if (ppUsed) { this.move.usePp(); @@ -630,6 +638,7 @@ export class MovePhase extends BattlePhase { move: MoveId.NONE, result: MoveResult.FAIL, targets: this.targets, + useMode: this.useMode, }); this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT); @@ -653,7 +662,7 @@ export class MovePhase extends BattlePhase { } globalScene.phaseManager.queueMessage( - i18next.t(this.reflected ? "battle:magicCoatActivated" : "battle:useMove", { + i18next.t(isReflected(this.useMode) ? "battle:magicCoatActivated" : "battle:useMove", { pokemonNameWithAffix: getPokemonNameWithAffix(this.pokemon), moveName: this.move.getName(), }), diff --git a/src/phases/new-biome-encounter-phase.ts b/src/phases/new-biome-encounter-phase.ts index 29ba67cb797..5aad607764f 100644 --- a/src/phases/new-biome-encounter-phase.ts +++ b/src/phases/new-biome-encounter-phase.ts @@ -1,5 +1,5 @@ import { globalScene } from "#app/global-scene"; -import { applyAbAttrs, PostBiomeChangeAbAttr } from "#app/data/abilities/ability"; +import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs"; import { getRandomWeatherType } from "#app/data/weather"; import { NextEncounterPhase } from "./next-encounter-phase"; @@ -14,7 +14,7 @@ export class NewBiomeEncounterPhase extends NextEncounterPhase { if (pokemon) { pokemon.resetBattleAndWaveData(); if (pokemon.isOnField()) { - applyAbAttrs(PostBiomeChangeAbAttr, pokemon, null); + applyAbAttrs("PostBiomeChangeAbAttr", pokemon, null); } } } diff --git a/src/phases/obtain-status-effect-phase.ts b/src/phases/obtain-status-effect-phase.ts index bf172269d5f..dc26d070029 100644 --- a/src/phases/obtain-status-effect-phase.ts +++ b/src/phases/obtain-status-effect-phase.ts @@ -8,7 +8,7 @@ import type Pokemon from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; import { PokemonPhase } from "./pokemon-phase"; import { SpeciesFormChangeStatusEffectTrigger } from "#app/data/pokemon-forms/form-change-triggers"; -import { applyPostSetStatusAbAttrs, PostSetStatusAbAttr } from "#app/data/abilities/ability"; +import { applyPostSetStatusAbAttrs } from "#app/data/abilities/apply-ab-attrs"; import { isNullOrUndefined } from "#app/utils/common"; export class ObtainStatusEffectPhase extends PokemonPhase { @@ -53,7 +53,7 @@ export class ObtainStatusEffectPhase extends PokemonPhase { globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeStatusEffectTrigger, true); // If mold breaker etc was used to set this status, it shouldn't apply to abilities activated afterwards globalScene.arena.setIgnoreAbilities(false); - applyPostSetStatusAbAttrs(PostSetStatusAbAttr, pokemon, this.statusEffect, this.sourcePokemon); + applyPostSetStatusAbAttrs("PostSetStatusAbAttr", pokemon, this.statusEffect, this.sourcePokemon); } this.end(); }); diff --git a/src/phases/pokemon-anim-phase.ts b/src/phases/pokemon-anim-phase.ts index b1a21446996..314bddb981d 100644 --- a/src/phases/pokemon-anim-phase.ts +++ b/src/phases/pokemon-anim-phase.ts @@ -53,7 +53,8 @@ export class PokemonAnimPhase extends BattlePhase { private doSubstituteAddAnim(): void { const substitute = this.pokemon.getTag(SubstituteTag); if (isNullOrUndefined(substitute)) { - return this.end(); + this.end(); + return; } const getSprite = () => { @@ -116,12 +117,14 @@ export class PokemonAnimPhase extends BattlePhase { private doSubstitutePreMoveAnim(): void { if (this.fieldAssets.length !== 1) { - return this.end(); + this.end(); + return; } const subSprite = this.fieldAssets[0]; if (subSprite === undefined) { - return this.end(); + this.end(); + return; } globalScene.tweens.add({ @@ -145,12 +148,14 @@ export class PokemonAnimPhase extends BattlePhase { private doSubstitutePostMoveAnim(): void { if (this.fieldAssets.length !== 1) { - return this.end(); + this.end(); + return; } const subSprite = this.fieldAssets[0]; if (subSprite === undefined) { - return this.end(); + this.end(); + return; } globalScene.tweens.add({ @@ -174,12 +179,14 @@ export class PokemonAnimPhase extends BattlePhase { private doSubstituteRemoveAnim(): void { if (this.fieldAssets.length !== 1) { - return this.end(); + this.end(); + return; } const subSprite = this.fieldAssets[0]; if (subSprite === undefined) { - return this.end(); + this.end(); + return; } const getSprite = () => { @@ -244,12 +251,14 @@ export class PokemonAnimPhase extends BattlePhase { private doCommanderApplyAnim(): void { if (!globalScene.currentBattle?.double) { - return this.end(); + this.end(); + return; } const dondozo = this.pokemon.getAlly(); if (dondozo?.species?.speciesId !== SpeciesId.DONDOZO) { - return this.end(); + this.end(); + return; } const tatsugiriX = this.pokemon.x + this.pokemon.getSprite().x; @@ -329,7 +338,8 @@ export class PokemonAnimPhase extends BattlePhase { const tatsugiri = this.pokemon.getAlly(); if (isNullOrUndefined(tatsugiri)) { console.warn("Aborting COMMANDER_REMOVE anim: Tatsugiri is undefined"); - return this.end(); + this.end(); + return; } const tatsuSprite = globalScene.addPokemonSprite( diff --git a/src/phases/pokemon-phase.ts b/src/phases/pokemon-phase.ts index d7fe58d0b80..cfa30f0d06e 100644 --- a/src/phases/pokemon-phase.ts +++ b/src/phases/pokemon-phase.ts @@ -4,6 +4,10 @@ import type Pokemon from "#app/field/pokemon"; import { FieldPhase } from "./field-phase"; 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; public player: boolean; public fieldIndex: number; @@ -15,10 +19,12 @@ export abstract class PokemonPhase extends FieldPhase { battlerIndex ?? globalScene .getField() - .find(p => p?.isActive())! // TODO: is the bang correct here? - .getBattlerIndex(); + .find(p => p?.isActive()) + ?.getBattlerIndex(); 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; diff --git a/src/phases/pokemon-transform-phase.ts b/src/phases/pokemon-transform-phase.ts index ab0949c42b9..938915309d9 100644 --- a/src/phases/pokemon-transform-phase.ts +++ b/src/phases/pokemon-transform-phase.ts @@ -29,7 +29,8 @@ export class PokemonTransformPhase extends PokemonPhase { const target = globalScene.getField(true).find(p => p.getBattlerIndex() === this.targetIndex); if (!target) { - return this.end(); + this.end(); + return; } user.summonData.speciesForm = target.getSpeciesForm(); @@ -52,7 +53,7 @@ export class PokemonTransformPhase extends PokemonPhase { user.summonData.moveset = target.getMoveset().map(m => { if (m) { // 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!`); return new PokemonMove(MoveId.NONE); diff --git a/src/phases/post-summon-activate-ability-phase.ts b/src/phases/post-summon-activate-ability-phase.ts new file mode 100644 index 00000000000..ba6c80d4ee0 --- /dev/null +++ b/src/phases/post-summon-activate-ability-phase.ts @@ -0,0 +1,27 @@ +import { applyPostSummonAbAttrs } from "#app/data/abilities/apply-ab-attrs"; +import { PostSummonPhase } from "#app/phases/post-summon-phase"; +import type { BattlerIndex } from "#enums/battler-index"; + +/** + * Helper to {@linkcode PostSummonPhase} which applies abilities + */ +export class PostSummonActivateAbilityPhase extends PostSummonPhase { + private priority: number; + private passive: boolean; + + constructor(battlerIndex: BattlerIndex, priority: number, passive: boolean) { + super(battlerIndex); + this.priority = priority; + this.passive = passive; + } + + start() { + applyPostSummonAbAttrs("PostSummonAbAttr", this.getPokemon(), this.passive, false); + + this.end(); + } + + public override getPriority() { + return this.priority; + } +} diff --git a/src/phases/post-summon-phase.ts b/src/phases/post-summon-phase.ts index a7faf614292..26fffd1b024 100644 --- a/src/phases/post-summon-phase.ts +++ b/src/phases/post-summon-phase.ts @@ -1,10 +1,10 @@ import { globalScene } from "#app/global-scene"; -import { applyAbAttrs, applyPostSummonAbAttrs, CommanderAbAttr, PostSummonAbAttr } from "#app/data/abilities/ability"; import { ArenaTrapTag } from "#app/data/arena-tag"; import { StatusEffect } from "#app/enums/status-effect"; import { PokemonPhase } from "./pokemon-phase"; import { MysteryEncounterPostSummonTag } from "#app/data/battler-tags"; import { BattlerTagType } from "#enums/battler-tag-type"; +import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs"; export class PostSummonPhase extends PokemonPhase { public readonly phaseName = "PostSummonPhase"; @@ -26,12 +26,15 @@ export class PostSummonPhase extends PokemonPhase { pokemon.lapseTag(BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON); } - applyPostSummonAbAttrs(PostSummonAbAttr, pokemon); const field = pokemon.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField(); for (const p of field) { - applyAbAttrs(CommanderAbAttr, p, null, false); + applyAbAttrs("CommanderAbAttr", p, null, false); } this.end(); } + + public getPriority() { + return 0; + } } diff --git a/src/phases/post-turn-status-effect-phase.ts b/src/phases/post-turn-status-effect-phase.ts index c868b963f39..e0a3bb5c00b 100644 --- a/src/phases/post-turn-status-effect-phase.ts +++ b/src/phases/post-turn-status-effect-phase.ts @@ -1,13 +1,6 @@ import { globalScene } from "#app/global-scene"; import type { BattlerIndex } from "#enums/battler-index"; -import { - applyAbAttrs, - applyPostDamageAbAttrs, - BlockNonDirectDamageAbAttr, - BlockStatusDamageAbAttr, - PostDamageAbAttr, - ReduceBurnDamageAbAttr, -} from "#app/data/abilities/ability"; +import { applyAbAttrs, applyPostDamageAbAttrs } from "#app/data/abilities/apply-ab-attrs"; import { CommonBattleAnim } from "#app/data/battle-anims"; import { CommonAnim } from "#enums/move-anims-common"; import { getStatusEffectActivationText } from "#app/data/status-effect"; @@ -29,8 +22,8 @@ export class PostTurnStatusEffectPhase extends PokemonPhase { if (pokemon?.isActive(true) && pokemon.status && pokemon.status.isPostTurn() && !pokemon.switchOutStatus) { pokemon.status.incrementTurn(); const cancelled = new BooleanHolder(false); - applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled); - applyAbAttrs(BlockStatusDamageAbAttr, pokemon, cancelled); + applyAbAttrs("BlockNonDirectDamageAbAttr", pokemon, cancelled); + applyAbAttrs("BlockStatusDamageAbAttr", pokemon, cancelled); if (!cancelled.value) { globalScene.phaseManager.queueMessage( @@ -46,14 +39,14 @@ export class PostTurnStatusEffectPhase extends PokemonPhase { break; case StatusEffect.BURN: damage.value = Math.max(pokemon.getMaxHp() >> 4, 1); - applyAbAttrs(ReduceBurnDamageAbAttr, pokemon, null, false, damage); + applyAbAttrs("ReduceBurnDamageAbAttr", pokemon, null, false, damage); break; } if (damage.value) { // Set preventEndure flag to avoid pokemon surviving thanks to focus band, sturdy, endure ... globalScene.damageNumberHandler.add(this.getPokemon(), pokemon.damage(damage.value, false, true)); pokemon.updateInfo(); - applyPostDamageAbAttrs(PostDamageAbAttr, pokemon, damage.value, pokemon.hasPassive(), false, []); + applyPostDamageAbAttrs("PostDamageAbAttr", pokemon, damage.value, pokemon.hasPassive(), false, []); } new CommonBattleAnim(CommonAnim.POISON + (pokemon.status.effect - 1), pokemon).play(false, () => this.end()); } else { diff --git a/src/phases/quiet-form-change-phase.ts b/src/phases/quiet-form-change-phase.ts index cfd9c521e2b..41b691844bf 100644 --- a/src/phases/quiet-form-change-phase.ts +++ b/src/phases/quiet-form-change-phase.ts @@ -12,12 +12,7 @@ import type Pokemon from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; import { BattlePhase } from "./battle-phase"; import type { MovePhase } from "./move-phase"; -import { - applyAbAttrs, - ClearTerrainAbAttr, - ClearWeatherAbAttr, - PostTeraFormChangeStatChangeAbAttr, -} from "#app/data/abilities/ability"; +import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs"; export class QuietFormChangePhase extends BattlePhase { public readonly phaseName = "QuietFormChangePhase"; @@ -34,7 +29,8 @@ export class QuietFormChangePhase extends BattlePhase { super.start(); if (this.pokemon.formIndex === this.pokemon.species.forms.findIndex(f => f.formKey === this.formChange.formKey)) { - return this.end(); + this.end(); + return; } const preName = getPokemonNameWithAffix(this.pokemon); @@ -185,9 +181,9 @@ export class QuietFormChangePhase extends BattlePhase { } } if (this.formChange.trigger instanceof SpeciesFormChangeTeraTrigger) { - applyAbAttrs(PostTeraFormChangeStatChangeAbAttr, this.pokemon, null); - applyAbAttrs(ClearWeatherAbAttr, this.pokemon, null); - applyAbAttrs(ClearTerrainAbAttr, this.pokemon, null); + applyAbAttrs("PostTeraFormChangeStatChangeAbAttr", this.pokemon, null); + applyAbAttrs("ClearWeatherAbAttr", this.pokemon, null); + applyAbAttrs("ClearTerrainAbAttr", this.pokemon, null); } super.end(); diff --git a/src/phases/ribbon-modifier-reward-phase.ts b/src/phases/ribbon-modifier-reward-phase.ts index 949f7af0302..10d63ba707f 100644 --- a/src/phases/ribbon-modifier-reward-phase.ts +++ b/src/phases/ribbon-modifier-reward-phase.ts @@ -1,6 +1,6 @@ import { globalScene } from "#app/global-scene"; import type PokemonSpecies from "#app/data/pokemon-species"; -import type { ModifierTypeFunc } from "#app/modifier/modifier-type"; +import type { ModifierTypeFunc } from "#app/@types/modifier-types"; import { UiMode } from "#enums/ui-mode"; import i18next from "i18next"; import { ModifierRewardPhase } from "./modifier-reward-phase"; diff --git a/src/phases/select-modifier-phase.ts b/src/phases/select-modifier-phase.ts index f99c921412f..34a905f64c6 100644 --- a/src/phases/select-modifier-phase.ts +++ b/src/phases/select-modifier-phase.ts @@ -1,5 +1,5 @@ import { globalScene } from "#app/global-scene"; -import type { ModifierTier } from "#app/modifier/modifier-tier"; +import type { ModifierTier } from "#enums/modifier-tier"; import type { ModifierTypeOption, ModifierType } from "#app/modifier/modifier-type"; import { regenerateModifierPoolThresholds, @@ -11,9 +11,9 @@ import { RememberMoveModifierType, PokemonPpRestoreModifierType, PokemonPpUpModifierType, - ModifierPoolType, getPlayerModifierTypeOptions, } from "#app/modifier/modifier-type"; +import { ModifierPoolType } from "#enums/modifier-pool-type"; import type { Modifier } from "#app/modifier/modifier"; import { ExtraModifierModifier, diff --git a/src/phases/select-starter-phase.ts b/src/phases/select-starter-phase.ts index e7e87f5a25f..76247c14ce0 100644 --- a/src/phases/select-starter-phase.ts +++ b/src/phases/select-starter-phase.ts @@ -3,7 +3,7 @@ import { applyChallenges } from "#app/data/challenge"; import { ChallengeType } from "#enums/challenge-type"; import { Gender } from "#app/data/gender"; 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 Overrides from "#app/overrides"; import { Phase } from "#app/phase"; diff --git a/src/phases/stat-stage-change-phase.ts b/src/phases/stat-stage-change-phase.ts index 9c351096180..e73f72f7a63 100644 --- a/src/phases/stat-stage-change-phase.ts +++ b/src/phases/stat-stage-change-phase.ts @@ -4,13 +4,7 @@ import { applyAbAttrs, applyPostStatStageChangeAbAttrs, applyPreStatStageChangeAbAttrs, - ConditionalUserFieldProtectStatAbAttr, - PostStatStageChangeAbAttr, - ProtectStatAbAttr, - ReflectStatStageChangeAbAttr, - StatStageChangeCopyAbAttr, - StatStageChangeMultiplierAbAttr, -} from "#app/data/abilities/ability"; +} from "#app/data/abilities/apply-ab-attrs"; import { MistTag } from "#app/data/arena-tag"; import { ArenaTagSide } from "#enums/arena-tag-side"; import type { ArenaTag } from "#app/data/arena-tag"; @@ -132,7 +126,7 @@ export class StatStageChangePhase extends PokemonPhase { const stages = new NumberHolder(this.stages); if (!this.ignoreAbilities) { - applyAbAttrs(StatStageChangeMultiplierAbAttr, pokemon, null, false, stages); + applyAbAttrs("StatStageChangeMultiplierAbAttr", pokemon, null, false, stages); } let simulate = false; @@ -152,9 +146,9 @@ export class StatStageChangePhase extends PokemonPhase { } if (!cancelled.value && !this.selfTarget && stages.value < 0) { - applyPreStatStageChangeAbAttrs(ProtectStatAbAttr, pokemon, stat, cancelled, simulate); + applyPreStatStageChangeAbAttrs("ProtectStatAbAttr", pokemon, stat, cancelled, simulate); applyPreStatStageChangeAbAttrs( - ConditionalUserFieldProtectStatAbAttr, + "ConditionalUserFieldProtectStatAbAttr", pokemon, stat, cancelled, @@ -164,7 +158,7 @@ export class StatStageChangePhase extends PokemonPhase { const ally = pokemon.getAlly(); if (!isNullOrUndefined(ally)) { applyPreStatStageChangeAbAttrs( - ConditionalUserFieldProtectStatAbAttr, + "ConditionalUserFieldProtectStatAbAttr", ally, stat, cancelled, @@ -180,7 +174,7 @@ export class StatStageChangePhase extends PokemonPhase { !this.comingFromMirrorArmorUser ) { applyPreStatStageChangeAbAttrs( - ReflectStatStageChangeAbAttr, + "ReflectStatStageChangeAbAttr", pokemon, stat, cancelled, @@ -228,11 +222,17 @@ export class StatStageChangePhase extends PokemonPhase { if (stages.value > 0 && this.canBeCopied) { for (const opponent of pokemon.getOpponents()) { - applyAbAttrs(StatStageChangeCopyAbAttr, opponent, null, false, this.stats, stages.value); + applyAbAttrs("StatStageChangeCopyAbAttr", opponent, null, false, this.stats, stages.value); } } - applyPostStatStageChangeAbAttrs(PostStatStageChangeAbAttr, pokemon, filteredStats, this.stages, this.selfTarget); + applyPostStatStageChangeAbAttrs( + "PostStatStageChangeAbAttr", + pokemon, + filteredStats, + this.stages, + this.selfTarget, + ); // Look for any other stat change phases; if this is the last one, do White Herb check const existingPhase = globalScene.phaseManager.findPhase( diff --git a/src/phases/summon-phase.ts b/src/phases/summon-phase.ts index e902fd0183e..ad93452331f 100644 --- a/src/phases/summon-phase.ts +++ b/src/phases/summon-phase.ts @@ -10,7 +10,7 @@ import { getPokemonNameWithAffix } from "#app/messages"; import i18next from "i18next"; import { PartyMemberPokemonPhase } from "./party-member-pokemon-phase"; import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; -import { applyPreSummonAbAttrs, PreSummonAbAttr } from "#app/data/abilities/ability"; +import { applyPreSummonAbAttrs } from "#app/data/abilities/apply-ab-attrs"; import { globalScene } from "#app/global-scene"; export class SummonPhase extends PartyMemberPokemonPhase { @@ -27,7 +27,7 @@ export class SummonPhase extends PartyMemberPokemonPhase { start() { super.start(); - applyPreSummonAbAttrs(PreSummonAbAttr, this.getPokemon()); + applyPreSummonAbAttrs("PreSummonAbAttr", this.getPokemon()); this.preSummon(); } diff --git a/src/phases/switch-summon-phase.ts b/src/phases/switch-summon-phase.ts index f6395397920..af03cc42b54 100644 --- a/src/phases/switch-summon-phase.ts +++ b/src/phases/switch-summon-phase.ts @@ -1,11 +1,5 @@ import { globalScene } from "#app/global-scene"; -import { - applyPreSummonAbAttrs, - applyPreSwitchOutAbAttrs, - PostDamageForceSwitchAbAttr, - PreSummonAbAttr, - PreSwitchOutAbAttr, -} from "#app/data/abilities/ability"; +import { applyPreSummonAbAttrs, applyPreSwitchOutAbAttrs } from "#app/data/abilities/apply-ab-attrs"; import { allMoves } from "#app/data/data-lists"; import { getPokeballTintColor } from "#app/data/pokeball"; import { SpeciesFormChangeActiveTrigger } from "#app/data/pokemon-forms/form-change-triggers"; @@ -130,8 +124,8 @@ export class SwitchSummonPhase extends SummonPhase { switchedInPokemon.resetSummonData(); switchedInPokemon.loadAssets(true); - applyPreSummonAbAttrs(PreSummonAbAttr, switchedInPokemon); - applyPreSwitchOutAbAttrs(PreSwitchOutAbAttr, this.lastPokemon); + applyPreSummonAbAttrs("PreSummonAbAttr", switchedInPokemon); + applyPreSwitchOutAbAttrs("PreSwitchOutAbAttr", this.lastPokemon); if (!switchedInPokemon) { this.end(); return; @@ -174,19 +168,7 @@ export class SwitchSummonPhase extends SummonPhase { party[this.slotIndex] = this.lastPokemon; party[this.fieldIndex] = switchedInPokemon; const showTextAndSummon = () => { - globalScene.ui.showText( - this.player - ? i18next.t("battle:playerGo", { - pokemonName: getPokemonNameWithAffix(switchedInPokemon), - }) - : i18next.t("battle:trainerGo", { - trainerName: globalScene.currentBattle.trainer?.getName( - !(this.fieldIndex % 2) ? TrainerSlot.TRAINER : TrainerSlot.TRAINER_PARTNER, - ), - pokemonName: this.getPokemon().getNameToRender(), - }), - ); - + globalScene.ui.showText(this.getSendOutText(switchedInPokemon)); /** * If this switch is passing a Substitute, make the switched Pokemon matches the returned Pokemon's state as it left. * Otherwise, clear any persisting tags on the returned Pokemon. @@ -227,7 +209,7 @@ export class SwitchSummonPhase extends SummonPhase { const lastPokemonIsForceSwitchedAndNotFainted = lastUsedMove?.hasAttr("ForceSwitchOutAttr") && !this.lastPokemon.isFainted(); const lastPokemonHasForceSwitchAbAttr = - this.lastPokemon.hasAbilityWithAttr(PostDamageForceSwitchAbAttr) && !this.lastPokemon.isFainted(); + this.lastPokemon.hasAbilityWithAttr("PostDamageForceSwitchAbAttr") && !this.lastPokemon.isFainted(); // Compensate for turn spent summoning/forced switch if switched out pokemon is not fainted. // Needed as we increment turn counters in `TurnEndPhase`. @@ -263,6 +245,34 @@ export class SwitchSummonPhase extends SummonPhase { } queuePostSummon(): void { - globalScene.phaseManager.unshiftNew("PostSummonPhase", this.getPokemon().getBattlerIndex()); + globalScene.phaseManager.startNewDynamicPhase("PostSummonPhase", this.getPokemon().getBattlerIndex()); + } + + /** + * Get the text to be displayed when a pokemon is forced to switch and leave the field. + * @param switchedInPokemon - The Pokemon having newly been sent in. + * @returns The text to display. + */ + private getSendOutText(switchedInPokemon: Pokemon): string { + if (this.switchType === SwitchType.FORCE_SWITCH) { + // "XYZ was dragged out!" + return i18next.t("battle:pokemonDraggedOut", { + pokemonName: getPokemonNameWithAffix(switchedInPokemon), + }); + } + if (this.player) { + // "Go! XYZ!" + return i18next.t("battle:playerGo", { + pokemonName: getPokemonNameWithAffix(switchedInPokemon), + }); + } + + // "Trainer sent out XYZ!" + return i18next.t("battle:trainerGo", { + trainerName: globalScene.currentBattle.trainer?.getName( + !(this.fieldIndex % 2) ? TrainerSlot.TRAINER : TrainerSlot.TRAINER_PARTNER, + ), + pokemonName: this.getPokemon().getNameToRender(), + }); } } diff --git a/src/phases/title-phase.ts b/src/phases/title-phase.ts index 26311d52ab8..5e36081b899 100644 --- a/src/phases/title-phase.ts +++ b/src/phases/title-phase.ts @@ -6,15 +6,12 @@ import { getBiomeKey } from "#app/field/arena"; import { GameMode, getGameMode } from "#app/game-mode"; import { GameModes } from "#enums/game-modes"; import type { Modifier } from "#app/modifier/modifier"; -import { - getDailyRunStarterModifiers, - ModifierPoolType, - modifierTypes, - regenerateModifierPoolThresholds, -} from "#app/modifier/modifier-type"; +import { getDailyRunStarterModifiers, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type"; +import { modifierTypes } from "#app/data/data-lists"; +import { ModifierPoolType } from "#enums/modifier-pool-type"; import { Phase } from "#app/phase"; import type { SessionSaveData } from "#app/system/game-data"; -import { Unlockables } from "#app/system/unlockables"; +import { Unlockables } from "#enums/unlockables"; import { vouchers } from "#app/system/voucher"; import type { OptionSelectConfig, OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; import { SaveSlotUiMode } from "#app/ui/save-slot-select-ui-handler"; diff --git a/src/phases/trainer-victory-phase.ts b/src/phases/trainer-victory-phase.ts index 5d35dd5d375..554b8109f02 100644 --- a/src/phases/trainer-victory-phase.ts +++ b/src/phases/trainer-victory-phase.ts @@ -1,6 +1,6 @@ import { getCharVariantFromDialogue } from "#app/data/dialogue"; import { TrainerType } from "#app/enums/trainer-type"; -import { modifierTypes } from "#app/modifier/modifier-type"; +import { modifierTypes } from "#app/data/data-lists"; import { vouchers } from "#app/system/voucher"; import i18next from "i18next"; import { randSeedItem } from "#app/utils/common"; diff --git a/src/phases/turn-end-phase.ts b/src/phases/turn-end-phase.ts index a539b234a18..ab46292c1d2 100644 --- a/src/phases/turn-end-phase.ts +++ b/src/phases/turn-end-phase.ts @@ -1,4 +1,4 @@ -import { applyPostTurnAbAttrs, PostTurnAbAttr } from "#app/data/abilities/ability"; +import { applyPostTurnAbAttrs } from "#app/data/abilities/apply-ab-attrs"; import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type"; import { TerrainType } from "#app/data/terrain"; import { WeatherType } from "#app/enums/weather-type"; @@ -49,7 +49,7 @@ export class TurnEndPhase extends FieldPhase { globalScene.applyModifier(EnemyStatusEffectHealChanceModifier, false, pokemon); } - applyPostTurnAbAttrs(PostTurnAbAttr, pokemon); + applyPostTurnAbAttrs("PostTurnAbAttr", pokemon); } globalScene.applyModifiers(TurnStatusEffectModifier, pokemon.isPlayer(), pokemon); diff --git a/src/phases/turn-start-phase.ts b/src/phases/turn-start-phase.ts index e9a8a82afdc..6219907fb68 100644 --- a/src/phases/turn-start-phase.ts +++ b/src/phases/turn-start-phase.ts @@ -1,4 +1,4 @@ -import { applyAbAttrs, BypassSpeedChanceAbAttr, PreventBypassSpeedChanceAbAttr } from "#app/data/abilities/ability"; +import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs"; import { allMoves } from "#app/data/data-lists"; import { AbilityId } from "#enums/ability-id"; import { Stat } from "#app/enums/stat"; @@ -63,11 +63,11 @@ export class TurnStartPhase extends FieldPhase { // This occurs before the main loop because of battles with more than two Pokemon const battlerBypassSpeed = {}; - globalScene.getField(true).map(p => { + globalScene.getField(true).forEach(p => { const bypassSpeed = new BooleanHolder(false); const canCheckHeldItems = new BooleanHolder(true); - applyAbAttrs(BypassSpeedChanceAbAttr, p, null, false, bypassSpeed); - applyAbAttrs(PreventBypassSpeedChanceAbAttr, p, null, false, bypassSpeed, canCheckHeldItems); + applyAbAttrs("BypassSpeedChanceAbAttr", p, null, false, bypassSpeed); + applyAbAttrs("PreventBypassSpeedChanceAbAttr", p, null, false, bypassSpeed, canCheckHeldItems); if (canCheckHeldItems.value) { globalScene.applyModifiers(BypassSpeedChanceModifier, p.isPlayer(), p, bypassSpeed); } @@ -124,6 +124,8 @@ export class TurnStartPhase extends FieldPhase { return moveOrder; } + // TODO: Refactor this alongside `CommandPhase.handleCommand` to use SEPARATE METHODS + // Also need a clearer distinction between "turn command" and queued moves start() { super.start(); @@ -157,44 +159,38 @@ export class TurnStartPhase extends FieldPhase { } switch (turnCommand?.command) { - case Command.FIGHT: - { - const queuedMove = turnCommand.move; - pokemon.turnData.order = orderIndex++; - if (!queuedMove) { - continue; - } - const move = - pokemon.getMoveset().find(m => m.moveId === queuedMove.move && m.ppUsed < m.getMovePp()) || - new PokemonMove(queuedMove.move); - if (move.getMove().hasAttr("MoveHeaderAttr")) { - phaseManager.unshiftNew("MoveHeaderPhase", pokemon, move); - } - if (pokemon.isPlayer()) { - if (turnCommand.cursor === -1) { - phaseManager.pushNew("MovePhase", pokemon, turnCommand.targets || turnCommand.move!.targets, move); - } else { - phaseManager.pushNew( - "MovePhase", - pokemon, - turnCommand.targets || turnCommand.move!.targets, // TODO: is the bang correct here? - move, - false, - queuedMove.ignorePP, - ); - } - } else { - phaseManager.pushNew( - "MovePhase", - pokemon, - turnCommand.targets || turnCommand.move!.targets, - move, - false, - queuedMove.ignorePP, - ); - } + case Command.FIGHT: { + const queuedMove = turnCommand.move; + pokemon.turnData.order = orderIndex++; + if (!queuedMove) { + continue; + } + const move = + pokemon.getMoveset().find(m => m.moveId === queuedMove.move && m.ppUsed < m.getMovePp()) ?? + new PokemonMove(queuedMove.move); + if (move.getMove().hasAttr("MoveHeaderAttr")) { + phaseManager.unshiftNew("MoveHeaderPhase", pokemon, move); + } + + if (pokemon.isPlayer() && turnCommand.cursor === -1) { + phaseManager.pushNew( + "MovePhase", + pokemon, + turnCommand.targets || turnCommand.move!.targets, + move, + turnCommand.move!.useMode, + ); //TODO: is the bang correct here? + } else { + phaseManager.pushNew( + "MovePhase", + pokemon, + turnCommand.targets || turnCommand.move!.targets, + move, + queuedMove.useMode, + ); // TODO: is the bang correct here? } break; + } case Command.BALL: phaseManager.unshiftNew("AttemptCapturePhase", turnCommand.targets![0] % 2, turnCommand.cursor!); //TODO: is the bang correct here? break; diff --git a/src/phases/unlock-phase.ts b/src/phases/unlock-phase.ts index 839ac31dc5d..76719847f92 100644 --- a/src/phases/unlock-phase.ts +++ b/src/phases/unlock-phase.ts @@ -1,6 +1,6 @@ import { globalScene } from "#app/global-scene"; import { Phase } from "#app/phase"; -import type { Unlockables } from "#app/system/unlockables"; +import type { Unlockables } from "#enums/unlockables"; import { getUnlockableName } from "#app/system/unlockables"; import { UiMode } from "#enums/ui-mode"; import i18next from "i18next"; diff --git a/src/phases/victory-phase.ts b/src/phases/victory-phase.ts index bf6ea6d4a43..ae5b727c2a6 100644 --- a/src/phases/victory-phase.ts +++ b/src/phases/victory-phase.ts @@ -2,7 +2,7 @@ import type { BattlerIndex } from "#enums/battler-index"; import { ClassicFixedBossWaves } from "#enums/fixed-boss-waves"; import { BattleType } from "#enums/battle-type"; import type { CustomModifierSettings } from "#app/modifier/modifier-type"; -import { modifierTypes } from "#app/modifier/modifier-type"; +import { modifierTypes } from "#app/data/data-lists"; import { PokemonPhase } from "./pokemon-phase"; import { handleMysteryEncounterVictory } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { globalScene } from "#app/global-scene"; diff --git a/src/phases/weather-effect-phase.ts b/src/phases/weather-effect-phase.ts index 0873283652e..d9239220376 100644 --- a/src/phases/weather-effect-phase.ts +++ b/src/phases/weather-effect-phase.ts @@ -1,13 +1,9 @@ import { globalScene } from "#app/global-scene"; import { applyPreWeatherEffectAbAttrs, - SuppressWeatherEffectAbAttr, - PreWeatherDamageAbAttr, applyAbAttrs, - BlockNonDirectDamageAbAttr, applyPostWeatherLapseAbAttrs, - PostWeatherLapseAbAttr, -} from "#app/data/abilities/ability"; +} from "#app/data/abilities/apply-ab-attrs"; import { CommonAnim } from "#enums/move-anims-common"; import type { Weather } from "#app/data/weather"; import { getWeatherDamageMessage, getWeatherLapseMessage } from "#app/data/weather"; @@ -45,15 +41,15 @@ export class WeatherEffectPhase extends CommonAnimPhase { const cancelled = new BooleanHolder(false); this.executeForAll((pokemon: Pokemon) => - applyPreWeatherEffectAbAttrs(SuppressWeatherEffectAbAttr, pokemon, this.weather, cancelled), + applyPreWeatherEffectAbAttrs("SuppressWeatherEffectAbAttr", pokemon, this.weather, cancelled), ); if (!cancelled.value) { const inflictDamage = (pokemon: Pokemon) => { const cancelled = new BooleanHolder(false); - applyPreWeatherEffectAbAttrs(PreWeatherDamageAbAttr, pokemon, this.weather, cancelled); - applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled); + applyPreWeatherEffectAbAttrs("PreWeatherDamageAbAttr", pokemon, this.weather, cancelled); + applyAbAttrs("BlockNonDirectDamageAbAttr", pokemon, cancelled); if ( cancelled.value || @@ -84,7 +80,7 @@ export class WeatherEffectPhase extends CommonAnimPhase { globalScene.ui.showText(getWeatherLapseMessage(this.weather.weatherType) ?? "", null, () => { this.executeForAll((pokemon: Pokemon) => { if (!pokemon.switchOutStatus) { - applyPostWeatherLapseAbAttrs(PostWeatherLapseAbAttr, pokemon, this.weather); + applyPostWeatherLapseAbAttrs("PostWeatherLapseAbAttr", pokemon, this.weather); } }); diff --git a/src/pipelines/glsl/fieldSpriteFragShader.frag b/src/pipelines/glsl/fieldSpriteFragShader.frag index e79dea86fe9..0eb95ece5e3 100644 --- a/src/pipelines/glsl/fieldSpriteFragShader.frag +++ b/src/pipelines/glsl/fieldSpriteFragShader.frag @@ -51,7 +51,7 @@ float hue2rgb(float f1, float f2, float hue) { vec3 rgb2hsl(vec3 color) { vec3 hsl; - + float fmin = min(min(color.r, color.g), color.b); float fmax = max(max(color.r, color.g), color.b); float delta = fmax - fmin; @@ -66,7 +66,7 @@ vec3 rgb2hsl(vec3 color) { hsl.y = delta / (fmax + fmin); else hsl.y = delta / (2.0 - fmax - fmin); - + float deltaR = (((fmax - color.r) / 6.0) + (delta / 2.0)) / delta; float deltaG = (((fmax - color.g) / 6.0) + (delta / 2.0)) / delta; float deltaB = (((fmax - color.b) / 6.0) + (delta / 2.0)) / delta; @@ -89,24 +89,24 @@ vec3 rgb2hsl(vec3 color) { vec3 hsl2rgb(vec3 hsl) { vec3 rgb; - + if (hsl.y == 0.0) rgb = vec3(hsl.z); else { float f2; - + if (hsl.z < 0.5) f2 = hsl.z * (1.0 + hsl.y); else f2 = (hsl.z + hsl.y) - (hsl.y * hsl.z); - + float f1 = 2.0 * hsl.z - f2; - + rgb.r = hue2rgb(f1, f2, hsl.x + (1.0/3.0)); rgb.g = hue2rgb(f1, f2, hsl.x); rgb.b = hue2rgb(f1, f2, hsl.x - (1.0/3.0)); } - + return rgb; } diff --git a/src/pipelines/glsl/spriteFragShader.frag b/src/pipelines/glsl/spriteFragShader.frag index 03f8c8c27bc..9328cc4d96d 100644 --- a/src/pipelines/glsl/spriteFragShader.frag +++ b/src/pipelines/glsl/spriteFragShader.frag @@ -83,7 +83,7 @@ vec3 rgb2hsl(vec3 color) { hsl.y = delta / (fmax + fmin); else hsl.y = delta / (2.0 - fmax - fmin); - + float deltaR = (((fmax - color.r) / 6.0) + (delta / 2.0)) / delta; float deltaG = (((fmax - color.g) / 6.0) + (delta / 2.0)) / delta; float deltaB = (((fmax - color.b) / 6.0) + (delta / 2.0)) / delta; @@ -106,24 +106,24 @@ vec3 rgb2hsl(vec3 color) { vec3 hsl2rgb(vec3 hsl) { vec3 rgb; - + if (hsl.y == 0.0) rgb = vec3(hsl.z); else { float f2; - + if (hsl.z < 0.5) f2 = hsl.z * (1.0 + hsl.y); else f2 = (hsl.z + hsl.y) - (hsl.y * hsl.z); - + float f1 = 2.0 * hsl.z - f2; - + rgb.r = hue2rgb(f1, f2, hsl.x + (1.0/3.0)); rgb.g = hue2rgb(f1, f2, hsl.x); rgb.b= hue2rgb(f1, f2, hsl.x - (1.0/3.0)); } - + return rgb; } diff --git a/src/plugins/cache-busted-loader-plugin.ts b/src/plugins/cache-busted-loader-plugin.ts index e5b1abb5903..4ae9b352ae3 100644 --- a/src/plugins/cache-busted-loader-plugin.ts +++ b/src/plugins/cache-busted-loader-plugin.ts @@ -1,10 +1,8 @@ +import { coerceArray } from "#app/utils/common"; + let manifest: object; export default class CacheBustedLoaderPlugin extends Phaser.Loader.LoaderPlugin { - constructor(scene: Phaser.Scene) { - super(scene); - } - get manifest() { return manifest; } @@ -14,9 +12,7 @@ export default class CacheBustedLoaderPlugin extends Phaser.Loader.LoaderPlugin } addFile(file): void { - if (!Array.isArray(file)) { - file = [file]; - } + file = coerceArray(file); file.forEach(item => { if (manifest) { diff --git a/src/plugins/i18n.ts b/src/plugins/i18n.ts index 515d9aec528..eab427e7b4a 100644 --- a/src/plugins/i18n.ts +++ b/src/plugins/i18n.ts @@ -101,6 +101,7 @@ const namespaceMap = { doubleBattleDialogue: "dialogue-double-battle", splashMessages: "splash-texts", mysteryEncounterMessages: "mystery-encounter-texts", + biome: "biomes", }; //#region Functions @@ -174,7 +175,24 @@ export async function initI18n(): Promise { "es-MX": ["es-ES", "en"], default: ["en"], }, - supportedLngs: ["en", "es-ES", "es-MX", "fr", "it", "de", "zh-CN", "zh-TW", "pt-BR", "ko", "ja", "ca", "da", "tr", "ro", "ru"], + supportedLngs: [ + "en", + "es-ES", + "es-MX", + "fr", + "it", + "de", + "zh-CN", + "zh-TW", + "pt-BR", + "ko", + "ja", + "ca", + "da", + "tr", + "ro", + "ru", + ], backend: { loadPath(lng: string, [ns]: string[]) { let fileName: string; diff --git a/src/plugins/vite/vite-minify-json-plugin.ts b/src/plugins/vite/vite-minify-json-plugin.ts index f14fdf7042d..38f299eea50 100644 --- a/src/plugins/vite/vite-minify-json-plugin.ts +++ b/src/plugins/vite/vite-minify-json-plugin.ts @@ -41,9 +41,9 @@ export function minifyJsonPlugin(basePath: string | string[], recursive?: boolea }, async closeBundle() { console.log("Minifying JSON files..."); - const basePathes = Array.isArray(basePath) ? basePath : [basePath]; + const basePaths = Array.isArray(basePath) ? basePath : [basePath]; - basePathes.forEach(basePath => { + basePaths.forEach(basePath => { const baseDir = path.resolve(buildDir, basePath); if (fs.existsSync(baseDir)) { applyToDir(baseDir, recursive); diff --git a/src/scene-base.ts b/src/scene-base.ts index 430a9bc8aac..ccea373fca0 100644 --- a/src/scene-base.ts +++ b/src/scene-base.ts @@ -1,3 +1,5 @@ +import { coerceArray } from "#app/utils/common"; + export const legacyCompatibleImages: string[] = []; export class SceneBase extends Phaser.Scene { @@ -88,9 +90,7 @@ export class SceneBase extends Phaser.Scene { } else { folder += "/"; } - if (!Array.isArray(filenames)) { - filenames = [filenames]; - } + filenames = coerceArray(filenames); for (const f of filenames as string[]) { this.load.audio(folder + key, this.getCachedUrl(`audio/${folder}${f}`)); } diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 31c7ad7e41c..e933c5704f9 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -6,14 +6,15 @@ import type { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon"; import { pokemonPrevolutions } from "#app/data/balance/pokemon-evolutions"; 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 { randInt, getEnumKeys, isLocal, executeIf, fixedInt, randSeedItem, NumberHolder } from "#app/utils/common"; import Overrides from "#app/overrides"; import PokemonData from "#app/system/pokemon-data"; import PersistentModifierData from "#app/system/modifier-data"; import ArenaData from "#app/system/arena-data"; -import { Unlockables } from "#app/system/unlockables"; +import { Unlockables } from "#enums/unlockables"; import { getGameMode } from "#app/game-mode"; import { GameModes } from "#enums/game-modes"; import { BattleType } from "#enums/battle-type"; diff --git a/src/system/pokemon-data.ts b/src/system/pokemon-data.ts index 7571f0cc82f..050da57e0be 100644 --- a/src/system/pokemon-data.ts +++ b/src/system/pokemon-data.ts @@ -3,7 +3,8 @@ import { globalScene } from "#app/global-scene"; import type { Gender } from "../data/gender"; import { Nature } from "#enums/nature"; 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 Pokemon, { EnemyPokemon, PokemonBattleData, PokemonSummonData } from "../field/pokemon"; import { PokemonMove } from "#app/data/moves/pokemon-move"; @@ -45,7 +46,6 @@ export default class PokemonData { public pauseEvolutions: boolean; public pokerus: boolean; public usedTMs: MoveId[]; - public evoCounter: number; public teraType: PokemonType; public isTerastallized: boolean; public stellarTypesBoosted: PokemonType[]; @@ -118,7 +118,6 @@ export default class PokemonData { this.pauseEvolutions = !!source.pauseEvolutions; this.pokerus = !!source.pokerus; this.usedTMs = source.usedTMs ?? []; - this.evoCounter = source.evoCounter ?? 0; this.teraType = source.teraType as PokemonType; this.isTerastallized = !!source.isTerastallized; this.stellarTypesBoosted = source.stellarTypesBoosted ?? []; diff --git a/src/system/settings/settings-gamepad.ts b/src/system/settings/settings-gamepad.ts index d39d5cf5a41..8a28e9fbf14 100644 --- a/src/system/settings/settings-gamepad.ts +++ b/src/system/settings/settings-gamepad.ts @@ -32,7 +32,10 @@ const pressAction = i18next.t("settings:pressActionToAssign"); export const settingGamepadOptions = { [SettingGamepad.Controller]: [i18next.t("settings:controllerDefault"), i18next.t("settings:controllerChange")], - [SettingGamepad.Gamepad_Support]: [i18next.t("settings:gamepadSupportAuto"), i18next.t("settings:gamepadSupportDisabled")], + [SettingGamepad.Gamepad_Support]: [ + i18next.t("settings:gamepadSupportAuto"), + i18next.t("settings:gamepadSupportDisabled"), + ], [SettingGamepad.Button_Up]: [`KEY ${Button.UP.toString()}`, pressAction], [SettingGamepad.Button_Down]: [`KEY ${Button.DOWN.toString()}`, pressAction], [SettingGamepad.Button_Left]: [`KEY ${Button.LEFT.toString()}`, pressAction], diff --git a/src/system/settings/settings.ts b/src/system/settings/settings.ts index 69abc669870..ca5395a5af7 100644 --- a/src/system/settings/settings.ts +++ b/src/system/settings/settings.ts @@ -959,7 +959,7 @@ export function setSetting(setting: string, value: number): boolean { }, { label: "Türkçe (Needs Help)", - handler: () => changeLocaleHandler("tr") + handler: () => changeLocaleHandler("tr"), }, { label: "Русский (Needs Help)", @@ -967,11 +967,11 @@ export function setSetting(setting: string, value: number): boolean { }, { label: "Dansk (Needs Help)", - handler: () => changeLocaleHandler("da") + handler: () => changeLocaleHandler("da"), }, { label: "Română (Needs Help)", - handler: () => changeLocaleHandler("ro") + handler: () => changeLocaleHandler("ro"), }, { label: i18next.t("settings:back"), diff --git a/src/system/unlockables.ts b/src/system/unlockables.ts index f64d4d33966..72588858eae 100644 --- a/src/system/unlockables.ts +++ b/src/system/unlockables.ts @@ -1,13 +1,7 @@ import i18next from "i18next"; import { GameMode } from "../game-mode"; import { GameModes } from "#enums/game-modes"; - -export enum Unlockables { - ENDLESS_MODE, - MINI_BLACK_HOLE, - SPLICED_ENDLESS_MODE, - EVIOLITE, -} +import { Unlockables } from "#enums/unlockables"; export function getUnlockableName(unlockable: Unlockables) { switch (unlockable) { diff --git a/src/system/version_migration/version_converter.ts b/src/system/version_migration/version_converter.ts index 798115e0395..32e105aec66 100644 --- a/src/system/version_migration/version_converter.ts +++ b/src/system/version_migration/version_converter.ts @@ -63,6 +63,10 @@ import * as v1_8_3 from "./versions/v1_8_3"; // biome-ignore lint/style/noNamespaceImport: Convenience 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 */ const LATEST_VERSION = version; @@ -85,6 +89,7 @@ const sessionMigrators: SessionSaveMigrator[] = []; sessionMigrators.push(...v1_0_4.sessionMigrators); sessionMigrators.push(...v1_7_0.sessionMigrators); sessionMigrators.push(...v1_9_0.sessionMigrators); +sessionMigrators.push(...v1_10_0.sessionMigrators); /** All settings migrators */ const settingsMigrators: SettingsSaveMigrator[] = []; diff --git a/src/system/version_migration/versions/v1_0_4.ts b/src/system/version_migration/versions/v1_0_4.ts index fbbde49bc08..a08327a3b70 100644 --- a/src/system/version_migration/versions/v1_0_4.ts +++ b/src/system/version_migration/versions/v1_0_4.ts @@ -3,7 +3,7 @@ import type { SystemSaveData, SessionSaveData } from "#app/system/game-data"; import { defaultStarterSpecies } from "#app/constants"; import { AbilityAttr } from "#enums/ability-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 { isNullOrUndefined } from "#app/utils/common"; import type { SystemSaveMigrator } from "#app/@types/SystemSaveMigrator"; diff --git a/src/system/version_migration/versions/v1_10_0.ts b/src/system/version_migration/versions/v1_10_0.ts new file mode 100644 index 00000000000..4d1dedf701e --- /dev/null +++ b/src/system/version_migration/versions/v1_10_0.ts @@ -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 = [fixMoveHistory] as const; diff --git a/src/system/version_migration/versions/v1_7_0.ts b/src/system/version_migration/versions/v1_7_0.ts index e309959317e..e3c599bf77b 100644 --- a/src/system/version_migration/versions/v1_7_0.ts +++ b/src/system/version_migration/versions/v1_7_0.ts @@ -1,6 +1,7 @@ import type { SessionSaveMigrator } from "#app/@types/SessionSaveMigrator"; 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 type { SessionSaveData, SystemSaveData } from "#app/system/game-data"; import { DexAttr } from "#enums/dex-attr"; diff --git a/src/system/version_migration/versions/v1_8_3.ts b/src/system/version_migration/versions/v1_8_3.ts index bd963290800..81430659e0b 100644 --- a/src/system/version_migration/versions/v1_8_3.ts +++ b/src/system/version_migration/versions/v1_8_3.ts @@ -1,5 +1,5 @@ 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 { DexAttr } from "#enums/dex-attr"; import { SpeciesId } from "#enums/species-id"; diff --git a/src/ui/abstact-option-select-ui-handler.ts b/src/ui/abstact-option-select-ui-handler.ts index 07609648a4e..e1263bc088c 100644 --- a/src/ui/abstact-option-select-ui-handler.ts +++ b/src/ui/abstact-option-select-ui-handler.ts @@ -56,10 +56,6 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler { protected defaultTextStyle: TextStyle = TextStyle.WINDOW; protected textContent: string; - constructor(mode: UiMode | null) { - super(mode); - } - abstract getWindowWidth(): number; getWindowHeight(): number { diff --git a/src/ui/admin-ui-handler.ts b/src/ui/admin-ui-handler.ts index 67ae3118863..c8c8e43802b 100644 --- a/src/ui/admin-ui-handler.ts +++ b/src/ui/admin-ui-handler.ts @@ -69,7 +69,7 @@ export default class AdminUiHandler extends FormModalUiHandler { case AdminMode.SEARCH: inputFieldConfigs.push({ label: "Username" }); break; - case AdminMode.ADMIN: + case AdminMode.ADMIN: { const adminResult = this.adminResult ?? { username: "", discordId: "", @@ -90,6 +90,7 @@ export default class AdminUiHandler extends FormModalUiHandler { inputFieldConfigs.push({ label: "Last played", isReadOnly: true }); inputFieldConfigs.push({ label: "Registered", isReadOnly: true }); break; + } } return inputFieldConfigs; } diff --git a/src/ui/daily-run-scoreboard.ts b/src/ui/daily-run-scoreboard.ts index 076a782908b..e19882b09cc 100644 --- a/src/ui/daily-run-scoreboard.ts +++ b/src/ui/daily-run-scoreboard.ts @@ -169,12 +169,13 @@ export class DailyRunScoreboard extends Phaser.GameObjects.Container { entryContainer.add(scoreLabel); switch (this.category) { - case ScoreboardCategory.DAILY: + case ScoreboardCategory.DAILY: { const waveLabel = addTextObject(68, 0, wave, TextStyle.WINDOW, { fontSize: "54px", }); entryContainer.add(waveLabel); break; + } case ScoreboardCategory.WEEKLY: scoreLabel.x -= 16; break; diff --git a/src/ui/egg-gacha-ui-handler.ts b/src/ui/egg-gacha-ui-handler.ts index d6b1b630a05..0c8d90fa138 100644 --- a/src/ui/egg-gacha-ui-handler.ts +++ b/src/ui/egg-gacha-ui-handler.ts @@ -5,7 +5,7 @@ import { getEnumValues, getEnumKeys, fixedInt, randSeedShuffle } from "#app/util import type { IEggOptions } from "../data/egg"; import { Egg, getLegendaryGachaSpeciesForTimestamp } from "../data/egg"; 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 { Tutorial, handleTutorial } from "../tutorial"; import { Button } from "#enums/buttons"; @@ -131,7 +131,7 @@ export default class EggGachaUiHandler extends MessageUiHandler { gachaInfoContainer.add(gachaUpLabel); switch (gachaType as GachaType) { - case GachaType.LEGENDARY: + case GachaType.LEGENDARY: { if (["de", "es-ES"].includes(currentLanguage)) { gachaUpLabel.setAlign("center"); gachaUpLabel.setY(0); @@ -152,6 +152,7 @@ export default class EggGachaUiHandler extends MessageUiHandler { gachaInfoContainer.add(pokemonIcon); break; + } case GachaType.MOVE: if (["de", "es-ES", "fr", "pt-BR", "ru"].includes(currentLanguage)) { gachaUpLabel.setAlign("center"); @@ -623,11 +624,12 @@ export default class EggGachaUiHandler extends MessageUiHandler { updateGachaInfo(gachaType: GachaType): void { const infoContainer = this.gachaInfoContainers[gachaType]; switch (gachaType as GachaType) { - case GachaType.LEGENDARY: + case GachaType.LEGENDARY: { const species = getPokemonSpecies(getLegendaryGachaSpeciesForTimestamp(new Date().getTime())); const pokemonIcon = infoContainer.getAt(1) as Phaser.GameObjects.Sprite; pokemonIcon.setTexture(species.getIconAtlasKey(), species.getIconId(false)); break; + } } } diff --git a/src/ui/fight-ui-handler.ts b/src/ui/fight-ui-handler.ts index f30c7a4935c..14cd10d0d6f 100644 --- a/src/ui/fight-ui-handler.ts +++ b/src/ui/fight-ui-handler.ts @@ -16,6 +16,7 @@ import type Pokemon from "#app/field/pokemon"; import type { CommandPhase } from "#app/phases/command-phase"; import MoveInfoOverlay from "./move-info-overlay"; import { BattleType } from "#enums/battle-type"; +import { MoveUseMode } from "#enums/move-use-mode"; export default class FightUiHandler extends UiHandler implements InfoToggle { public static readonly MOVES_CONTAINER_NAME = "moves"; @@ -139,53 +140,63 @@ export default class FightUiHandler extends UiHandler implements InfoToggle { 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 { const ui = this.getUi(); - + const cursor = this.getCursor(); let success = false; - const cursor = this.getCursor(); - - if (button === Button.CANCEL || button === Button.ACTION) { - if (button === Button.ACTION) { + switch (button) { + case Button.CANCEL: + { + // Attempts to back out of the move selection pane are blocked in certain MEs + // TODO: Should we allow showing the summary menu at least? + const { battleType, mysteryEncounter } = globalScene.currentBattle; + if (battleType !== BattleType.MYSTERY_ENCOUNTER || !mysteryEncounter?.skipToFightInput) { + ui.setMode(UiMode.COMMAND, this.fieldIndex); + success = true; + } + } + break; + case Button.ACTION: if ( - (globalScene.phaseManager.getCurrentPhase() as CommandPhase).handleCommand(this.fromCommand, cursor, false) + (globalScene.phaseManager.getCurrentPhase() as CommandPhase).handleCommand( + this.fromCommand, + cursor, + MoveUseMode.NORMAL, + ) ) { success = true; } else { ui.playError(); } - } else { - // Cannot back out of fight menu if skipToFightInput is enabled - const { battleType, mysteryEncounter } = globalScene.currentBattle; - if (battleType !== BattleType.MYSTERY_ENCOUNTER || !mysteryEncounter?.skipToFightInput) { - ui.setMode(UiMode.COMMAND, this.fieldIndex); - success = true; + break; + case Button.UP: + if (cursor >= 2) { + success = this.setCursor(cursor - 2); } - } - } else { - switch (button) { - case Button.UP: - if (cursor >= 2) { - success = this.setCursor(cursor - 2); - } - break; - case Button.DOWN: - if (cursor < 2) { - success = this.setCursor(cursor + 2); - } - break; - case Button.LEFT: - if (cursor % 2 === 1) { - success = this.setCursor(cursor - 1); - } - break; - case Button.RIGHT: - if (cursor % 2 === 0) { - success = this.setCursor(cursor + 1); - } - break; - } + break; + case Button.DOWN: + if (cursor < 2) { + success = this.setCursor(cursor + 2); + } + break; + case Button.LEFT: + if (cursor % 2 === 1) { + success = this.setCursor(cursor - 1); + } + break; + case Button.RIGHT: + if (cursor % 2 === 0) { + success = this.setCursor(cursor + 1); + } + break; + default: + // other inputs do nothing while in fight menu } if (success) { diff --git a/src/ui/menu-ui-handler.ts b/src/ui/menu-ui-handler.ts index e68cc706aba..5ab1d4f9e96 100644 --- a/src/ui/menu-ui-handler.ts +++ b/src/ui/menu-ui-handler.ts @@ -686,7 +686,7 @@ export default class MenuUiHandler extends MessageUiHandler { error = true; } break; - case MenuOptions.LOG_OUT: + case MenuOptions.LOG_OUT: { success = true; const doLogout = () => { ui.setMode(UiMode.LOADING, { @@ -718,6 +718,7 @@ export default class MenuUiHandler extends MessageUiHandler { doLogout(); } break; + } } } else if (button === Button.CANCEL) { success = true; diff --git a/src/ui/party-ui-handler.ts b/src/ui/party-ui-handler.ts index f60bfa808ca..cf6333f4580 100644 --- a/src/ui/party-ui-handler.ts +++ b/src/ui/party-ui-handler.ts @@ -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 Pokemon from "#app/field/pokemon"; import { MoveResult } from "#enums/move-result"; @@ -7,11 +7,7 @@ import { Command } from "#enums/command"; import MessageUiHandler from "#app/ui/message-ui-handler"; import { UiMode } from "#enums/ui-mode"; import { BooleanHolder, toReadableString, randInt, getLocalizedSpriteKey } from "#app/utils/common"; -import { - PokemonFormChangeItemModifier, - PokemonHeldItemModifier, - SwitchEffectTransferModifier, -} from "#app/modifier/modifier"; +import type { PokemonFormChangeItemModifier, PokemonHeldItemModifier } from "#app/modifier/modifier"; import { allMoves } from "#app/data/data-lists"; import { Gender, getGenderColor, getGenderSymbol } from "#app/data/gender"; import { StatusEffect } from "#enums/status-effect"; @@ -226,7 +222,7 @@ export default class PartyUiHandler extends MessageUiHandler { public static FilterItemMaxStacks = (pokemon: PlayerPokemon, modifier: PokemonHeldItemModifier) => { const matchingModifier = globalScene.findModifier( - m => m instanceof PokemonHeldItemModifier && m.pokemonId === pokemon.id && m.matchType(modifier), + m => m.is("PokemonHeldItemModifier") && m.pokemonId === pokemon.id && m.matchType(modifier), ) as PokemonHeldItemModifier; if (matchingModifier && matchingModifier.stackCount === matchingModifier.getMaxStackCount()) { return i18next.t("partyUiHandler:tooManyItems", { pokemonName: getPokemonNameWithAffix(pokemon, false) }); @@ -554,7 +550,7 @@ export default class PartyUiHandler extends MessageUiHandler { // this returns `undefined` if the new pokemon doesn't have the item at all, otherwise it returns the `pokemonHeldItemModifier` for that item const matchingModifier = globalScene.findModifier( m => - m instanceof PokemonHeldItemModifier && + m.is("PokemonHeldItemModifier") && m.pokemonId === newPokemon.id && m.matchType(this.getTransferrableItemsFromPokemon(pokemon)[this.transferOptionCursor]), ) as PokemonHeldItemModifier; @@ -687,7 +683,7 @@ export default class PartyUiHandler extends MessageUiHandler { private getTransferrableItemsFromPokemon(pokemon: PlayerPokemon) { return globalScene.findModifiers( - m => m instanceof PokemonHeldItemModifier && m.isTransferable && m.pokemonId === pokemon.id, + m => m.is("PokemonHeldItemModifier") && m.isTransferable && m.pokemonId === pokemon.id, ) as PokemonHeldItemModifier[]; } @@ -928,7 +924,7 @@ export default class PartyUiHandler extends MessageUiHandler { /** Initialize item quantities for the selected Pokemon */ const itemModifiers = globalScene.findModifiers( m => - m instanceof PokemonHeldItemModifier && + m.is("PokemonHeldItemModifier") && m.isTransferable && m.pokemonId === globalScene.getPlayerParty()[this.cursor].id, ) as PokemonHeldItemModifier[]; @@ -1165,27 +1161,26 @@ export default class PartyUiHandler extends MessageUiHandler { return !!( this.partyUiMode !== PartyUiMode.FAINT_SWITCH && globalScene.findModifier( - m => - m instanceof SwitchEffectTransferModifier && m.pokemonId === globalScene.getPlayerField()[this.fieldIndex].id, + m => m.is("SwitchEffectTransferModifier") && m.pokemonId === globalScene.getPlayerField()[this.fieldIndex].id, ) ); } // 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 { - const moveHistory = globalScene.getPlayerField()[this.fieldIndex].getMoveHistory(); - return !!( + const lastMove: TurnMove | undefined = globalScene.getPlayerField()[this.fieldIndex].getLastXMoves()[0]; + return ( this.partyUiMode === PartyUiMode.FAINT_SWITCH && - moveHistory.length && - allMoves[moveHistory[moveHistory.length - 1].move].getAttrs("ForceSwitchOutAttr")[0]?.isBatonPass() && - moveHistory[moveHistory.length - 1].result === MoveResult.SUCCESS + lastMove?.result === MoveResult.SUCCESS && + allMoves[lastMove.move].getAttrs("ForceSwitchOutAttr")[0]?.isBatonPass() ); } private getItemModifiers(pokemon: Pokemon): PokemonHeldItemModifier[] { return ( (globalScene.findModifiers( - m => m instanceof PokemonHeldItemModifier && m.isTransferable && m.pokemonId === pokemon.id, + m => m.is("PokemonHeldItemModifier") && m.isTransferable && m.pokemonId === pokemon.id, ) as PokemonHeldItemModifier[]) ?? [] ); } @@ -1390,7 +1385,7 @@ export default class PartyUiHandler extends MessageUiHandler { case PartyOption.MOVE_1: case PartyOption.MOVE_2: case PartyOption.MOVE_3: - case PartyOption.MOVE_4: + case PartyOption.MOVE_4: { const move = pokemon.moveset[option - PartyOption.MOVE_1]; if (this.showMovePp) { const maxPP = move.getMovePp(); @@ -1400,7 +1395,8 @@ export default class PartyUiHandler extends MessageUiHandler { optionName = move.getName(); } break; - default: + } + default: { const formChangeItemModifiers = this.getFormChangeItemsModifiers(pokemon); if (formChangeItemModifiers && option >= PartyOption.FORM_CHANGE_ITEM) { const modifier = formChangeItemModifiers[option - PartyOption.FORM_CHANGE_ITEM]; @@ -1415,6 +1411,7 @@ export default class PartyUiHandler extends MessageUiHandler { } } break; + } } } else if (this.partyUiMode === PartyUiMode.REMEMBER_MOVE_MODIFIER) { const learnableLevelMoves = pokemon.getLearnableLevelMoves(); @@ -1565,7 +1562,7 @@ export default class PartyUiHandler extends MessageUiHandler { getFormChangeItemsModifiers(pokemon: Pokemon) { let formChangeItemModifiers = globalScene.findModifiers( - m => m instanceof PokemonFormChangeItemModifier && m.pokemonId === pokemon.id, + m => m.is("PokemonFormChangeItemModifier") && m.pokemonId === pokemon.id, ) as PokemonFormChangeItemModifier[]; const ultraNecrozmaModifiers = formChangeItemModifiers.filter( m => m.active && m.formChangeItem === FormChangeItem.ULTRANECROZIUM_Z, diff --git a/src/ui/pokedex-page-ui-handler.ts b/src/ui/pokedex-page-ui-handler.ts index 7ef4f8f920b..50c15336e36 100644 --- a/src/ui/pokedex-page-ui-handler.ts +++ b/src/ui/pokedex-page-ui-handler.ts @@ -16,7 +16,9 @@ import { pokemonFormChanges } from "#app/data/pokemon-forms"; import type { LevelMoves } 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 { 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 { starterPassiveAbilities } from "#app/data/balance/passives"; import { PokemonType } from "#enums/pokemon-type"; diff --git a/src/ui/pokedex-scan-ui-handler.ts b/src/ui/pokedex-scan-ui-handler.ts index df3e7cbc8c4..2bffc464793 100644 --- a/src/ui/pokedex-scan-ui-handler.ts +++ b/src/ui/pokedex-scan-ui-handler.ts @@ -8,7 +8,7 @@ import { UiMode } from "#enums/ui-mode"; import { FilterTextRow } from "./filter-text"; import { allAbilities } 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"; export default class PokedexScanUiHandler extends FormModalUiHandler { diff --git a/src/ui/pokedex-ui-handler.ts b/src/ui/pokedex-ui-handler.ts index 5b292e7232f..1732bb005d3 100644 --- a/src/ui/pokedex-ui-handler.ts +++ b/src/ui/pokedex-ui-handler.ts @@ -7,7 +7,8 @@ import { speciesEggMoves } from "#app/data/balance/egg-moves"; import { pokemonFormLevelMoves, pokemonSpeciesLevelMoves } from "#app/data/balance/pokemon-level-moves"; import type { PokemonForm } 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 { catchableSpecies } from "#app/data/balance/biomes"; import { PokemonType } from "#enums/pokemon-type"; diff --git a/src/ui/pokemon-icon-anim-handler.ts b/src/ui/pokemon-icon-anim-handler.ts index 253ccbe3623..8a206167a94 100644 --- a/src/ui/pokemon-icon-anim-handler.ts +++ b/src/ui/pokemon-icon-anim-handler.ts @@ -1,5 +1,5 @@ import { globalScene } from "#app/global-scene"; -import { fixedInt } from "#app/utils/common"; +import { fixedInt, coerceArray } from "#app/utils/common"; export enum PokemonIconAnimMode { NONE, @@ -49,9 +49,7 @@ export default class PokemonIconAnimHandler { } addOrUpdate(icons: PokemonIcon | PokemonIcon[], mode: PokemonIconAnimMode): void { - if (!Array.isArray(icons)) { - icons = [icons]; - } + icons = coerceArray(icons); for (const i of icons) { if (this.icons.has(i) && this.icons.get(i) === mode) { continue; @@ -66,9 +64,7 @@ export default class PokemonIconAnimHandler { } remove(icons: PokemonIcon | PokemonIcon[]): void { - if (!Array.isArray(icons)) { - icons = [icons]; - } + icons = coerceArray(icons); for (const i of icons) { if (this.toggled) { const icon = this.icons.get(i); diff --git a/src/ui/run-info-ui-handler.ts b/src/ui/run-info-ui-handler.ts index a4de2215fa4..78ab4a40407 100644 --- a/src/ui/run-info-ui-handler.ts +++ b/src/ui/run-info-ui-handler.ts @@ -567,7 +567,7 @@ export default class RunInfoUiHandler extends UiHandler { case GameModes.SPLICED_ENDLESS: modeText.appendText(`${i18next.t("gameMode:endlessSpliced")}`, false); break; - case GameModes.CHALLENGE: + case GameModes.CHALLENGE: { modeText.appendText(`${i18next.t("gameMode:challenge")}`, false); modeText.appendText(`${i18next.t("runHistory:challengeRules")}: `); modeText.setWrapMode(1); // wrap by word @@ -582,6 +582,7 @@ export default class RunInfoUiHandler extends UiHandler { } } break; + } case GameModes.ENDLESS: modeText.appendText(`${i18next.t("gameMode:endless")}`, false); break; @@ -687,7 +688,7 @@ export default class RunInfoUiHandler extends UiHandler { case Challenges.SINGLE_GENERATION: rules.push(i18next.t(`runHistory:challengeMonoGen${this.runInfo.challenges[i].value}`)); break; - case Challenges.SINGLE_TYPE: + case Challenges.SINGLE_TYPE: { const typeRule = PokemonType[this.runInfo.challenges[i].value - 1]; const typeTextColor = `[color=${TypeColor[typeRule]}]`; const typeShadowColor = `[shadow=${TypeShadow[typeRule]}]`; @@ -695,16 +696,18 @@ export default class RunInfoUiHandler extends UiHandler { typeTextColor + typeShadowColor + i18next.t(`pokemonInfo:Type.${typeRule}`)! + "[/color]" + "[/shadow]"; rules.push(typeText); break; + } case Challenges.INVERSE_BATTLE: rules.push(i18next.t("challenges:inverseBattle.shortName")); break; - default: + default: { const localisationKey = Challenges[this.runInfo.challenges[i].id] .split("_") .map((f, i) => (i ? `${f[0]}${f.slice(1).toLowerCase()}` : f.toLowerCase())) .join(""); rules.push(i18next.t(`challenges:${localisationKey}.name`)); break; + } } } } diff --git a/src/ui/settings/abstract-control-settings-ui-handler.ts b/src/ui/settings/abstract-control-settings-ui-handler.ts index 495a0f68540..e3631c062df 100644 --- a/src/ui/settings/abstract-control-settings-ui-handler.ts +++ b/src/ui/settings/abstract-control-settings-ui-handler.ts @@ -126,6 +126,11 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler ); this.actionsBg.setOrigin(0, 0); + /* + * If there isn't enough space to fit all the icons and texts, there will be an overlap + * This currently doesn't happen, but it's something to keep in mind. + */ + const iconAction = globalScene.add.sprite(0, 0, "keyboard"); iconAction.setOrigin(0, -0.1); iconAction.setPositionRelative(this.actionsBg, this.navigationContainer.width - 32, 4); @@ -137,7 +142,7 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler const iconCancel = globalScene.add.sprite(0, 0, "keyboard"); iconCancel.setOrigin(0, -0.1); - iconCancel.setPositionRelative(this.actionsBg, this.navigationContainer.width - 100, 4); + iconCancel.setPositionRelative(this.actionsBg, actionText.x - 28, 4); this.navigationIcons["BUTTON_CANCEL"] = iconCancel; const cancelText = addTextObject(0, 0, i18next.t("settings:back"), TextStyle.SETTINGS_LABEL); @@ -146,7 +151,7 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler const iconReset = globalScene.add.sprite(0, 0, "keyboard"); iconReset.setOrigin(0, -0.1); - iconReset.setPositionRelative(this.actionsBg, this.navigationContainer.width - 180, 4); + iconReset.setPositionRelative(this.actionsBg, cancelText.x - 28, 4); this.navigationIcons["BUTTON_HOME"] = iconReset; const resetText = addTextObject(0, 0, i18next.t("settings:reset"), TextStyle.SETTINGS_LABEL); diff --git a/src/ui/settings/abstract-settings-ui-handler.ts b/src/ui/settings/abstract-settings-ui-handler.ts index 1fb4b6d34dc..6db9840a818 100644 --- a/src/ui/settings/abstract-settings-ui-handler.ts +++ b/src/ui/settings/abstract-settings-ui-handler.ts @@ -94,7 +94,7 @@ export default class AbstractSettingsUiHandler extends MessageUiHandler { const iconCancel = globalScene.add.sprite(0, 0, "keyboard"); iconCancel.setOrigin(0, -0.1); - iconCancel.setPositionRelative(actionsBg, this.navigationContainer.width - 100, 4); + iconCancel.setPositionRelative(actionsBg, actionText.x - 28, 4); this.navigationIcons["BUTTON_CANCEL"] = iconCancel; const cancelText = addTextObject(0, 0, i18next.t("settings:back"), TextStyle.SETTINGS_LABEL); @@ -332,12 +332,13 @@ export default class AbstractSettingsUiHandler extends MessageUiHandler { case Button.CYCLE_SHINY: success = this.navigationContainer.navigate(button); break; - case Button.ACTION: + case Button.ACTION: { const setting: Setting = this.settings[cursor]; if (setting?.activatable) { success = this.activateSetting(setting); } break; + } } } diff --git a/src/ui/settings/move-touch-controls-handler.ts b/src/ui/settings/move-touch-controls-handler.ts index 44377c8c2ab..f684d27f748 100644 --- a/src/ui/settings/move-touch-controls-handler.ts +++ b/src/ui/settings/move-touch-controls-handler.ts @@ -98,7 +98,7 @@ export default class MoveTouchControlsHandler {

${i18next.t("settings:touchCancel")}
-
+
${i18next.t("settings:orientation")} ${this.isLandscapeMode ? i18next.t("settings:landscape") : i18next.t("settings:portrait")} diff --git a/src/ui/starter-select-ui-handler.ts b/src/ui/starter-select-ui-handler.ts index 0b1e690a918..20f613fb694 100644 --- a/src/ui/starter-select-ui-handler.ts +++ b/src/ui/starter-select-ui-handler.ts @@ -8,7 +8,7 @@ import i18next from "i18next"; import type BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext"; import { starterColors } from "#app/global-vars/starter-colors"; import { globalScene } from "#app/global-scene"; -import type { Ability } from "#app/data/abilities/ability-class"; +import type { Ability } from "#app/data/abilities/ability"; import { allAbilities } from "#app/data/data-lists"; import { speciesEggMoves } from "#app/data/balance/egg-moves"; import { GrowthRate, getGrowthRateColor } from "#app/data/exp"; @@ -19,7 +19,8 @@ import { pokemonFormChanges } from "#app/data/pokemon-forms"; import type { LevelMoves } 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 { 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 { PokemonType } from "#enums/pokemon-type"; import { GameModes } from "#enums/game-modes"; @@ -1763,7 +1764,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { } } else if (this.randomCursorObj.visible) { switch (button) { - case Button.ACTION: + case Button.ACTION: { if (this.starterSpecies.length >= 6) { error = true; break; @@ -1815,6 +1816,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { } }); break; + } case Button.UP: this.randomCursorObj.setVisible(false); this.filterBarCursor = this.filterBar.numFilters - 1; diff --git a/src/ui/summary-ui-handler.ts b/src/ui/summary-ui-handler.ts index 05b0ea2097f..d30322de293 100644 --- a/src/ui/summary-ui-handler.ts +++ b/src/ui/summary-ui-handler.ts @@ -33,7 +33,7 @@ import { loggedInUser } from "#app/account"; import type { Variant } from "#app/sprites/variant"; import { getVariantTint } from "#app/sprites/variant"; import { Button } from "#enums/buttons"; -import type { Ability } from "#app/data/abilities/ability-class"; +import type { Ability } from "#app/data/abilities/ability"; import i18next from "i18next"; import { modifierSortFunc } from "#app/modifier/modifier"; import { PlayerGender } from "#enums/player-gender"; diff --git a/src/ui/test-dialogue-ui-handler.ts b/src/ui/test-dialogue-ui-handler.ts index 9ecf1641e7b..b1e5047955a 100644 --- a/src/ui/test-dialogue-ui-handler.ts +++ b/src/ui/test-dialogue-ui-handler.ts @@ -10,10 +10,6 @@ import { UiMode } from "#enums/ui-mode"; export default class TestDialogueUiHandler extends FormModalUiHandler { keys: string[]; - constructor(mode) { - super(mode); - } - setup() { super.setup(); diff --git a/src/ui/text.ts b/src/ui/text.ts index d3afdef666f..8812d8ee4a8 100644 --- a/src/ui/text.ts +++ b/src/ui/text.ts @@ -4,7 +4,7 @@ import type Phaser from "phaser"; import BBCodeText from "phaser3-rex-plugins/plugins/gameobjects/tagtext/bbcodetext/BBCodeText"; import InputText from "phaser3-rex-plugins/plugins/inputtext"; import { globalScene } from "#app/global-scene"; -import { ModifierTier } from "../modifier/modifier-tier"; +import { ModifierTier } from "../enums/modifier-tier"; import i18next from "#app/plugins/i18n"; export enum TextStyle { diff --git a/src/ui/title-ui-handler.ts b/src/ui/title-ui-handler.ts index 29a354dbe01..50e77bbdd14 100644 --- a/src/ui/title-ui-handler.ts +++ b/src/ui/title-ui-handler.ts @@ -9,7 +9,7 @@ import { version } from "../../package.json"; import { pokerogueApi } from "#app/plugins/api/pokerogue-api"; import { globalScene } from "#app/global-scene"; 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 { timedEventManager } from "#app/global-event-manager"; diff --git a/src/utils/common.ts b/src/utils/common.ts index 56fa3b5c698..e19e5976507 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -611,3 +611,34 @@ export function getShinyDescriptor(variant: Variant): string { return i18next.t("common:commonShiny"); } } + +/** + * If the input isn't already an array, turns it into one. + * @returns An array with the same type as the type of the input + */ +export function coerceArray(input: T): T extends any[] ? T : [T]; +export function coerceArray(input: T): T | [T] { + 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>(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}`); +} diff --git a/src/utils/modifier-utils.ts b/src/utils/modifier-utils.ts new file mode 100644 index 00000000000..3be4af3730c --- /dev/null +++ b/src/utils/modifier-utils.ts @@ -0,0 +1,35 @@ +import { ModifierPoolType } from "#enums/modifier-pool-type"; +import { + dailyStarterModifierPool, + enemyBuffModifierPool, + modifierPool, + trainerModifierPool, + wildModifierPool, +} from "#app/modifier/modifier-pools"; +import type { ModifierPool, ModifierTypeFunc } from "#app/@types/modifier-types"; +import { modifierTypes } from "#app/data/data-lists"; +import type { ModifierType } from "#app/modifier/modifier-type"; + +export function getModifierPoolForType(poolType: ModifierPoolType): ModifierPool { + switch (poolType) { + case ModifierPoolType.PLAYER: + return modifierPool; + case ModifierPoolType.WILD: + return wildModifierPool; + case ModifierPoolType.TRAINER: + return trainerModifierPool; + case ModifierPoolType.ENEMY_BUFF: + return enemyBuffModifierPool; + case ModifierPoolType.DAILY_STARTER: + return dailyStarterModifierPool; + } +} + +// TODO: document this +export function getModifierType(modifierTypeFunc: ModifierTypeFunc): ModifierType { + const modifierType = modifierTypeFunc(); + if (!modifierType.id) { + modifierType.id = Object.keys(modifierTypes).find(k => modifierTypes[k] === modifierTypeFunc)!; // TODO: is this bang correct? + } + return modifierType; +} diff --git a/src/utils/pokemon-utils.ts b/src/utils/pokemon-utils.ts new file mode 100644 index 00000000000..9f87c36b050 --- /dev/null +++ b/src/utils/pokemon-utils.ts @@ -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]; +} diff --git a/test/abilities/ability_activation_order.test.ts b/test/abilities/ability_activation_order.test.ts new file mode 100644 index 00000000000..0b8e354cc7d --- /dev/null +++ b/test/abilities/ability_activation_order.test.ts @@ -0,0 +1,95 @@ +import { AbilityId } from "#enums/ability-id"; +import { MoveId } from "#enums/move-id"; +import { SpeciesId } from "#enums/species-id"; +import { Stat } from "#enums/stat"; +import { WeatherType } from "#enums/weather-type"; +import GameManager from "#test/testUtils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Ability Activation Order", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .moveset([MoveId.SPLASH]) + .ability(AbilityId.BALL_FETCH) + .battleStyle("single") + .criticalHits(false) + .enemySpecies(SpeciesId.MAGIKARP) + .enemyAbility(AbilityId.BALL_FETCH) + .enemyMoveset(MoveId.SPLASH); + }); + + it("should activate the ability of the faster Pokemon first", async () => { + game.override.enemyLevel(100).ability(AbilityId.DRIZZLE).enemyAbility(AbilityId.DROUGHT); + await game.classicMode.startBattle([SpeciesId.SLOWPOKE]); + + // Enemy's ability should activate first, so sun ends up replaced with rain + expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.RAIN); + }); + + it("should consider base stat boosting items in determining order", async () => { + game.override + .startingLevel(25) + .enemyLevel(50) + .enemySpecies(SpeciesId.MAGIKARP) + .enemyAbility(AbilityId.DROUGHT) + .ability(AbilityId.DRIZZLE) + .startingHeldItems([{ name: "BASE_STAT_BOOSTER", type: Stat.SPD, count: 100 }]); + + await game.classicMode.startBattle([SpeciesId.MAGIKARP]); + expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SUNNY); + }); + + it("should consider stat boosting items in determining order", async () => { + game.override + .startingLevel(35) + .enemyLevel(50) + .enemySpecies(SpeciesId.DITTO) + .enemyAbility(AbilityId.DROUGHT) + .ability(AbilityId.DRIZZLE) + .startingHeldItems([{ name: "SPECIES_STAT_BOOSTER", type: "QUICK_POWDER" }]); + + await game.classicMode.startBattle([SpeciesId.DITTO]); + expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SUNNY); + }); + + it("should activate priority abilities first", async () => { + game.override + .startingLevel(1) + .enemyLevel(100) + .enemySpecies(SpeciesId.ACCELGOR) + .enemyAbility(AbilityId.DROUGHT) + .ability(AbilityId.NEUTRALIZING_GAS); + + await game.classicMode.startBattle([SpeciesId.SLOWPOKE]); + expect(game.scene.arena.weather).toBeUndefined(); + }); + + it("should update dynamically based on speed order", async () => { + game.override + .startingLevel(35) + .enemyLevel(50) + .enemySpecies(SpeciesId.MAGIKARP) + .enemyAbility(AbilityId.SLOW_START) + .enemyPassiveAbility(AbilityId.DROUGHT) + .ability(AbilityId.DRIZZLE); + + await game.classicMode.startBattle([SpeciesId.MAGIKARP]); + // Slow start activates and makes enemy slower, so drought activates after drizzle + expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SUNNY); + }); +}); diff --git a/test/abilities/ability_timing.test.ts b/test/abilities/ability_timing.test.ts index 2ba1e821f8a..124591597be 100644 --- a/test/abilities/ability_timing.test.ts +++ b/test/abilities/ability_timing.test.ts @@ -50,5 +50,5 @@ describe("Ability Timing", () => { await game.phaseInterceptor.to("MessagePhase"); expect(i18next.t).toHaveBeenCalledWith("battle:statFell", expect.objectContaining({ count: 1 })); - }, 5000); + }); }); diff --git a/test/abilities/analytic.test.ts b/test/abilities/analytic.test.ts index 2cfea64d20e..5dbf4365186 100644 --- a/test/abilities/analytic.test.ts +++ b/test/abilities/analytic.test.ts @@ -27,7 +27,7 @@ describe("Abilities - Analytic", () => { .moveset([MoveId.SPLASH, MoveId.TACKLE]) .ability(AbilityId.ANALYTIC) .battleStyle("single") - .disableCrits() + .criticalHits(false) .startingLevel(200) .enemyLevel(200) .enemySpecies(SpeciesId.SNORLAX) diff --git a/test/abilities/battery.test.ts b/test/abilities/battery.test.ts index 980eaa5b381..db129e72b0b 100644 --- a/test/abilities/battery.test.ts +++ b/test/abilities/battery.test.ts @@ -26,11 +26,12 @@ describe("Abilities - Battery", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleStyle("double"); - game.override.enemySpecies(SpeciesId.SHUCKLE); - game.override.enemyAbility(AbilityId.BALL_FETCH); - game.override.moveset([MoveId.TACKLE, MoveId.BREAKING_SWIPE, MoveId.SPLASH, MoveId.DAZZLING_GLEAM]); - game.override.enemyMoveset(MoveId.SPLASH); + game.override + .battleStyle("double") + .enemySpecies(SpeciesId.SHUCKLE) + .enemyAbility(AbilityId.BALL_FETCH) + .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 () => { diff --git a/test/abilities/beast_boost.test.ts b/test/abilities/beast_boost.test.ts index 1d9eb5c5f2e..8be9156ce07 100644 --- a/test/abilities/beast_boost.test.ts +++ b/test/abilities/beast_boost.test.ts @@ -47,7 +47,7 @@ describe("Abilities - Beast Boost", () => { await game.phaseInterceptor.to("VictoryPhase"); 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 () => { game.override.enemyMoveset([MoveId.GUARD_SPLIT]); @@ -66,7 +66,7 @@ describe("Abilities - Beast Boost", () => { await game.phaseInterceptor.to("VictoryPhase"); expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(1); - }, 20000); + }); it("should have order preference in case of stat ties", async () => { // Order preference follows the order of EFFECTIVE_STAT @@ -84,5 +84,5 @@ describe("Abilities - Beast Boost", () => { await game.phaseInterceptor.to("VictoryPhase"); expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(1); - }, 20000); + }); }); diff --git a/test/abilities/commander.test.ts b/test/abilities/commander.test.ts index 39d9d0f4d7f..99512d7a733 100644 --- a/test/abilities/commander.test.ts +++ b/test/abilities/commander.test.ts @@ -35,7 +35,7 @@ describe("Abilities - Commander", () => { .moveset([MoveId.LIQUIDATION, MoveId.MEMENTO, MoveId.SPLASH, MoveId.FLIP_TURN]) .ability(AbilityId.COMMANDER) .battleStyle("double") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.SNORLAX) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.TACKLE); diff --git a/test/abilities/contrary.test.ts b/test/abilities/contrary.test.ts index 0e47862f3e8..6962abb0fec 100644 --- a/test/abilities/contrary.test.ts +++ b/test/abilities/contrary.test.ts @@ -36,7 +36,7 @@ describe("Abilities - Contrary", () => { const enemyPokemon = game.scene.getEnemyPokemon()!; expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1); - }, 20000); + }); describe("With Clear Body", () => { it("should apply positive effects", async () => { diff --git a/test/abilities/corrosion.test.ts b/test/abilities/corrosion.test.ts index 81b04ef340c..2a0464d1146 100644 --- a/test/abilities/corrosion.test.ts +++ b/test/abilities/corrosion.test.ts @@ -24,7 +24,7 @@ describe("Abilities - Corrosion", () => { game.override .moveset([MoveId.SPLASH]) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.GRIMER) .enemyAbility(AbilityId.CORROSION) .enemyMoveset(MoveId.TOXIC); diff --git a/test/abilities/costar.test.ts b/test/abilities/costar.test.ts index ae09aeea096..a40d84e72f5 100644 --- a/test/abilities/costar.test.ts +++ b/test/abilities/costar.test.ts @@ -24,10 +24,11 @@ describe("Abilities - COSTAR", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleStyle("double"); - game.override.ability(AbilityId.COSTAR); - game.override.moveset([MoveId.SPLASH, MoveId.NASTY_PLOT]); - game.override.enemyMoveset(MoveId.SPLASH); + game.override + .battleStyle("double") + .ability(AbilityId.COSTAR) + .moveset([MoveId.SPLASH, MoveId.NASTY_PLOT]) + .enemyMoveset(MoveId.SPLASH); }); test("ability copies positive stat stages", async () => { diff --git a/test/abilities/cud_chew.test.ts b/test/abilities/cud_chew.test.ts index 8a80c6625cc..70c282bf8a8 100644 --- a/test/abilities/cud_chew.test.ts +++ b/test/abilities/cud_chew.test.ts @@ -33,7 +33,7 @@ describe("Abilities - Cud Chew", () => { .startingHeldItems([{ name: "BERRY", type: BerryType.SITRUS, count: 1 }]) .ability(AbilityId.CUD_CHEW) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH); diff --git a/test/abilities/disguise.test.ts b/test/abilities/disguise.test.ts index 64e0715774d..4ccd825e574 100644 --- a/test/abilities/disguise.test.ts +++ b/test/abilities/disguise.test.ts @@ -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 () => { - game.override.moveset([MoveId.SURGING_STRIKES]); - game.override.enemyLevel(5); + game.override.moveset([MoveId.SURGING_STRIKES]).enemyLevel(5); await game.classicMode.startBattle(); const mimikyu = game.scene.getEnemyPokemon()!; @@ -106,8 +105,7 @@ describe("Abilities - Disguise", () => { }); it("persists form change when switched out", async () => { - game.override.enemyMoveset([MoveId.SHADOW_SNEAK]); - game.override.starterSpecies(0); + game.override.enemyMoveset([MoveId.SHADOW_SNEAK]).starterSpecies(0); 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 () => { - game.override.starterSpecies(0); - game.override.starterForms({ + game.override.starterSpecies(0).starterForms({ [SpeciesId.MIMIKYU]: bustedForm, }); await game.classicMode.startBattle([SpeciesId.FURRET, SpeciesId.MIMIKYU]); @@ -148,11 +145,12 @@ describe("Abilities - Disguise", () => { }); it("reverts to Disguised form on arena reset", async () => { - game.override.startingWave(4); - game.override.starterSpecies(SpeciesId.MIMIKYU); - game.override.starterForms({ - [SpeciesId.MIMIKYU]: bustedForm, - }); + game.override + .startingWave(4) + .starterSpecies(SpeciesId.MIMIKYU) + .starterForms({ + [SpeciesId.MIMIKYU]: bustedForm, + }); await game.classicMode.startBattle(); @@ -168,11 +166,12 @@ describe("Abilities - Disguise", () => { }); it("reverts to Disguised form on biome change when fainted", async () => { - game.override.startingWave(10); - game.override.starterSpecies(0); - game.override.starterForms({ - [SpeciesId.MIMIKYU]: bustedForm, - }); + game.override + .startingWave(10) + .starterSpecies(0) + .starterForms({ + [SpeciesId.MIMIKYU]: bustedForm, + }); await game.classicMode.startBattle([SpeciesId.MIMIKYU, SpeciesId.FURRET]); @@ -206,8 +205,7 @@ describe("Abilities - Disguise", () => { }); it("activates when Aerilate circumvents immunity to the move's base type", async () => { - game.override.ability(AbilityId.AERILATE); - game.override.moveset([MoveId.TACKLE]); + game.override.ability(AbilityId.AERILATE).moveset([MoveId.TACKLE]); await game.classicMode.startBattle(); diff --git a/test/abilities/dry_skin.test.ts b/test/abilities/dry_skin.test.ts index 549bb45ba9a..31620beb9bd 100644 --- a/test/abilities/dry_skin.test.ts +++ b/test/abilities/dry_skin.test.ts @@ -23,7 +23,7 @@ describe("Abilities - Dry Skin", () => { game = new GameManager(phaserGame); game.override .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemyAbility(AbilityId.DRY_SKIN) .enemyMoveset(MoveId.SPLASH) .enemySpecies(SpeciesId.CHARMANDER) diff --git a/test/abilities/early_bird.test.ts b/test/abilities/early_bird.test.ts index e19f2653b57..dcdfd8e4956 100644 --- a/test/abilities/early_bird.test.ts +++ b/test/abilities/early_bird.test.ts @@ -28,7 +28,7 @@ describe("Abilities - Early Bird", () => { .moveset([MoveId.REST, MoveId.BELLY_DRUM, MoveId.SPLASH]) .ability(AbilityId.EARLY_BIRD) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH); diff --git a/test/abilities/flash_fire.test.ts b/test/abilities/flash_fire.test.ts index 92bd7b52e57..3be156a1578 100644 --- a/test/abilities/flash_fire.test.ts +++ b/test/abilities/flash_fire.test.ts @@ -32,7 +32,7 @@ describe("Abilities - Flash Fire", () => { .enemyAbility(AbilityId.BALL_FETCH) .startingLevel(20) .enemyLevel(20) - .disableCrits(); + .criticalHits(false); }); it("immune to Fire-type moves", async () => { @@ -44,7 +44,7 @@ describe("Abilities - Flash Fire", () => { game.move.select(MoveId.SPLASH); await game.phaseInterceptor.to(TurnEndPhase); expect(blissey.hp).toBe(blissey.getMaxHp()); - }, 20000); + }); it("not activate if the Pokémon is protected from the Fire-type move", async () => { game.override.enemyMoveset([MoveId.EMBER]).moveset([MoveId.PROTECT]); @@ -55,7 +55,7 @@ describe("Abilities - Flash Fire", () => { game.move.select(MoveId.PROTECT); await game.phaseInterceptor.to(TurnEndPhase); expect(blissey!.getTag(BattlerTagType.FIRE_BOOST)).toBeUndefined(); - }, 20000); + }); it("activated by Will-O-Wisp", async () => { game.override.enemyMoveset([MoveId.WILL_O_WISP]).moveset(MoveId.SPLASH); @@ -70,11 +70,10 @@ describe("Abilities - Flash Fire", () => { await game.phaseInterceptor.to(TurnEndPhase); expect(blissey!.getTag(BattlerTagType.FIRE_BOOST)).toBeDefined(); - }, 20000); + }); it("activated after being frozen", async () => { - game.override.enemyMoveset([MoveId.EMBER]).moveset(MoveId.SPLASH); - game.override.statusEffect(StatusEffect.FREEZE); + game.override.enemyMoveset([MoveId.EMBER]).moveset(MoveId.SPLASH).statusEffect(StatusEffect.FREEZE); await game.classicMode.startBattle([SpeciesId.BLISSEY]); const blissey = game.scene.getPlayerPokemon()!; @@ -83,7 +82,7 @@ describe("Abilities - Flash Fire", () => { await game.phaseInterceptor.to(TurnEndPhase); expect(blissey!.getTag(BattlerTagType.FIRE_BOOST)).toBeDefined(); - }, 20000); + }); it("not passing with baton pass", async () => { game.override.enemyMoveset([MoveId.EMBER]).moveset([MoveId.BATON_PASS]); @@ -99,11 +98,14 @@ describe("Abilities - Flash Fire", () => { const chansey = game.scene.getPlayerPokemon()!; expect(game.scene.getPlayerPokemon()!.species.speciesId).toBe(SpeciesId.CHANSEY); expect(chansey!.getTag(BattlerTagType.FIRE_BOOST)).toBeUndefined(); - }, 20000); + }); it("boosts Fire-type move when the ability is activated", async () => { - game.override.enemyMoveset([MoveId.FIRE_PLEDGE]).moveset([MoveId.EMBER, MoveId.SPLASH]); - game.override.enemyAbility(AbilityId.FLASH_FIRE).ability(AbilityId.NONE); + game.override + .enemyMoveset([MoveId.FIRE_PLEDGE]) + .moveset([MoveId.EMBER, MoveId.SPLASH]) + .enemyAbility(AbilityId.FLASH_FIRE) + .ability(AbilityId.NONE); await game.classicMode.startBattle([SpeciesId.BLISSEY]); const blissey = game.scene.getPlayerPokemon()!; const initialHP = 1000; @@ -124,12 +126,15 @@ describe("Abilities - Flash Fire", () => { const flashFireDmg = initialHP - blissey.hp; expect(flashFireDmg).toBeGreaterThan(originalDmg); - }, 20000); + }); it("still activates regardless of accuracy check", async () => { - game.override.moveset(MoveId.FIRE_PLEDGE).enemyMoveset(MoveId.EMBER); - game.override.enemyAbility(AbilityId.NONE).ability(AbilityId.FLASH_FIRE); - game.override.enemySpecies(SpeciesId.BLISSEY); + game.override + .moveset(MoveId.FIRE_PLEDGE) + .enemyMoveset(MoveId.EMBER) + .enemyAbility(AbilityId.NONE) + .ability(AbilityId.FLASH_FIRE) + .enemySpecies(SpeciesId.BLISSEY); await game.classicMode.startBattle([SpeciesId.RATTATA]); const blissey = game.scene.getEnemyPokemon()!; @@ -153,5 +158,5 @@ describe("Abilities - Flash Fire", () => { const flashFireDmg = initialHP - blissey.hp; expect(flashFireDmg).toBeGreaterThan(originalDmg); - }, 20000); + }); }); diff --git a/test/abilities/flower_gift.test.ts b/test/abilities/flower_gift.test.ts index 26180492cbd..f072afe7f34 100644 --- a/test/abilities/flower_gift.test.ts +++ b/test/abilities/flower_gift.test.ts @@ -47,9 +47,10 @@ describe("Abilities - Flower Gift", () => { allyAbility = AbilityId.BALL_FETCH, enemyAbility = AbilityId.BALL_FETCH, ): Promise<[number, number]> => { - game.override.battleStyle("double"); - game.override.moveset([MoveId.SPLASH, MoveId.SUNNY_DAY, move, MoveId.HEAL_PULSE]); - game.override.enemyMoveset([MoveId.SPLASH, MoveId.HEAL_PULSE]); + game.override + .battleStyle("double") + .moveset([MoveId.SPLASH, MoveId.SUNNY_DAY, move, MoveId.HEAL_PULSE]) + .enemyMoveset([MoveId.SPLASH, MoveId.HEAL_PULSE]); const target_index = allyAttacker ? BattlerIndex.ENEMY : BattlerIndex.PLAYER_2; const attacker_index = allyAttacker ? BattlerIndex.PLAYER_2 : BattlerIndex.ENEMY; const ally_move = allyAttacker ? move : MoveId.SPLASH; diff --git a/test/abilities/flower_veil.test.ts b/test/abilities/flower_veil.test.ts index c59d8c6eb29..7cabfa23e96 100644 --- a/test/abilities/flower_veil.test.ts +++ b/test/abilities/flower_veil.test.ts @@ -32,7 +32,7 @@ describe("Abilities - Flower Veil", () => { .enemySpecies(SpeciesId.BULBASAUR) .ability(AbilityId.FLOWER_VEIL) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH); @@ -89,7 +89,6 @@ describe("Abilities - Flower Veil", () => { await game.move.selectEnemyMove(MoveId.THUNDER_WAVE); await game.toNextTurn(); expect(game.scene.getPlayerPokemon()!.status).toBeUndefined(); - vi.spyOn(allMoves[MoveId.THUNDER_WAVE], "accuracy", "get").mockClear(); }); it("should not prevent status conditions for a non-grass user and its non-grass allies", async () => { diff --git a/test/abilities/forecast.test.ts b/test/abilities/forecast.test.ts index 8d3a679c917..9111766ebdf 100644 --- a/test/abilities/forecast.test.ts +++ b/test/abilities/forecast.test.ts @@ -21,26 +21,10 @@ describe("Abilities - Forecast", () => { const RAINY_FORM = 2; const SNOWY_FORM = 3; - /** - * Tests form changes based on weather changes - * @param {GameManager} game The game manager instance - * @param {WeatherType} weather The active weather to set - * @param form The expected form based on the active weather - * @param initialForm The initial form pre form change - */ - const testWeatherFormChange = async (game: GameManager, weather: WeatherType, form: number, initialForm?: number) => { - game.override.weather(weather).starterForms({ [SpeciesId.CASTFORM]: initialForm }); - await game.classicMode.startBattle([SpeciesId.CASTFORM]); - - game.move.select(MoveId.SPLASH); - - expect(game.scene.getPlayerPokemon()?.formIndex).toBe(form); - }; - /** * Tests reverting to normal form when Cloud Nine/Air Lock is active on the field - * @param {GameManager} game The game manager instance - * @param {AbilityId} ability The ability that is active on the field + * @param game - The game manager instance + * @param ability - The ability that is active on the field */ const testRevertFormAgainstAbility = async (game: GameManager, ability: AbilityId) => { game.override.starterForms({ [SpeciesId.CASTFORM]: SUNNY_FORM }).enemyAbility(ability); @@ -191,10 +175,6 @@ describe("Abilities - Forecast", () => { 30 * 1000, ); - it("reverts to Normal Form during Clear weather", async () => { - await testWeatherFormChange(game, WeatherType.NONE, NORMAL_FORM, SUNNY_FORM); - }); - it("reverts to Normal Form if a Pokémon on the field has Air Lock", async () => { await testRevertFormAgainstAbility(game, AbilityId.AIR_LOCK); }); @@ -277,4 +257,20 @@ describe("Abilities - Forecast", () => { expect(castform.formIndex).toBe(NORMAL_FORM); }); + + // NOTE: The following pairs of tests are intentionally testing the same scenario, switching the player and enemy pokemon + // as this is a regression test where the order of player and enemy mattered. + it("should trigger player's form change when summoned at the same time as an enemy with a weather changing ability", async () => { + game.override.enemyAbility(AbilityId.DROUGHT); + await game.classicMode.startBattle([SpeciesId.CASTFORM, SpeciesId.MAGIKARP]); + const castform = game.scene.getPlayerPokemon()!; + expect(castform.formIndex).toBe(SUNNY_FORM); + }); + + it("should trigger enemy's form change when summoned at the same time as a player with a weather changing ability", async () => { + game.override.ability(AbilityId.DROUGHT).enemySpecies(SpeciesId.CASTFORM).enemyAbility(AbilityId.FORECAST); + await game.classicMode.startBattle([SpeciesId.MAGIKARP]); + const castform = game.scene.getEnemyPokemon()!; + expect(castform.formIndex).toBe(SUNNY_FORM); + }); }); diff --git a/test/abilities/good_as_gold.test.ts b/test/abilities/good_as_gold.test.ts index 85fadd472dc..89e354b1f34 100644 --- a/test/abilities/good_as_gold.test.ts +++ b/test/abilities/good_as_gold.test.ts @@ -33,7 +33,7 @@ describe("Abilities - Good As Gold", () => { .moveset([MoveId.SPLASH]) .ability(AbilityId.GOOD_AS_GOLD) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH); @@ -63,9 +63,10 @@ describe("Abilities - Good As Gold", () => { }); it("should not block any status moves that target the field, one side, or all pokemon", async () => { - game.override.battleStyle("double"); - game.override.enemyMoveset([MoveId.STEALTH_ROCK, MoveId.HAZE]); - game.override.moveset([MoveId.SWORDS_DANCE, MoveId.SAFEGUARD]); + game.override + .battleStyle("double") + .enemyMoveset([MoveId.STEALTH_ROCK, MoveId.HAZE]) + .moveset([MoveId.SWORDS_DANCE, MoveId.SAFEGUARD]); await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.FEEBAS]); const [good_as_gold, ball_fetch] = game.scene.getPlayerField(); @@ -85,8 +86,7 @@ describe("Abilities - Good As Gold", () => { }); it("should not block field targeted effects in singles", async () => { - game.override.battleStyle("single"); - game.override.enemyMoveset([MoveId.SPIKES]); + game.override.battleStyle("single").enemyMoveset([MoveId.SPIKES]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); game.move.select(MoveId.SPLASH, 0); @@ -96,8 +96,7 @@ describe("Abilities - Good As Gold", () => { }); it("should block the ally's helping hand", async () => { - game.override.battleStyle("double"); - game.override.moveset([MoveId.HELPING_HAND, MoveId.TACKLE]); + game.override.battleStyle("double").moveset([MoveId.HELPING_HAND, MoveId.TACKLE]); await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.FEEBAS]); game.move.select(MoveId.HELPING_HAND, 0); @@ -129,8 +128,7 @@ describe("Abilities - Good As Gold", () => { }); it("should not block field targeted effects like rain dance", async () => { - game.override.battleStyle("single"); - game.override.enemyMoveset([MoveId.RAIN_DANCE]); + game.override.battleStyle("single").enemyMoveset([MoveId.RAIN_DANCE]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); game.move.use(MoveId.SPLASH, 0); diff --git a/test/abilities/gorilla_tactics.test.ts b/test/abilities/gorilla_tactics.test.ts index 1759267f16d..a8b09461ea0 100644 --- a/test/abilities/gorilla_tactics.test.ts +++ b/test/abilities/gorilla_tactics.test.ts @@ -1,15 +1,19 @@ import { BattlerIndex } from "#enums/battler-index"; +import { RandomMoveAttr } from "#app/data/moves/move"; import { MoveId } from "#enums/move-id"; +import { AbilityId } from "#enums/ability-id"; import { SpeciesId } from "#enums/species-id"; import { Stat } from "#app/enums/stat"; -import { AbilityId } from "#enums/ability-id"; import GameManager from "#test/testUtils/gameManager"; import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { MoveResult } from "#enums/move-result"; +import { MoveUseMode } from "#enums/move-use-mode"; describe("Abilities - Gorilla Tactics", () => { let phaserGame: Phaser.Game; let game: GameManager; + beforeAll(() => { phaserGame = new Phaser.Game({ type: Phaser.HEADLESS, @@ -25,10 +29,9 @@ describe("Abilities - Gorilla Tactics", () => { game.override .battleStyle("single") .enemyAbility(AbilityId.BALL_FETCH) - .enemyMoveset([MoveId.SPLASH, MoveId.DISABLE]) .enemySpecies(SpeciesId.MAGIKARP) .enemyLevel(30) - .moveset([MoveId.SPLASH, MoveId.TACKLE, MoveId.GROWL]) + .moveset([MoveId.SPLASH, MoveId.TACKLE, MoveId.GROWL, MoveId.METRONOME]) .ability(AbilityId.GORILLA_TACTICS); }); @@ -39,9 +42,8 @@ describe("Abilities - Gorilla Tactics", () => { const initialAtkStat = darmanitan.getStat(Stat.ATK); game.move.select(MoveId.SPLASH); - await game.move.selectEnemyMove(MoveId.SPLASH); - - await game.phaseInterceptor.to("TurnEndPhase"); + await game.move.forceEnemyMove(MoveId.SPLASH); + await game.toEndOfTurn(); expect(darmanitan.getStat(Stat.ATK, false)).toBeCloseTo(initialAtkStat * 1.5); // Other moves should be restricted @@ -52,32 +54,50 @@ describe("Abilities - Gorilla Tactics", () => { it("should struggle if the only usable move is disabled", async () => { await game.classicMode.startBattle([SpeciesId.GALAR_DARMANITAN]); - const darmanitan = game.scene.getPlayerPokemon()!; - const enemy = game.scene.getEnemyPokemon()!; + const darmanitan = game.field.getPlayerPokemon(); + const enemy = game.field.getEnemyPokemon(); // First turn, lock move to Growl game.move.select(MoveId.GROWL); - await game.move.selectEnemyMove(MoveId.SPLASH); - - // Second turn, Growl is interrupted by Disable + await game.move.forceEnemyMove(MoveId.SPLASH); await game.toNextTurn(); + // Second turn, Growl is interrupted by Disable game.move.select(MoveId.GROWL); - await game.move.selectEnemyMove(MoveId.DISABLE); + await game.move.forceEnemyMove(MoveId.DISABLE); await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + await game.toNextTurn(); - await game.phaseInterceptor.to("TurnEndPhase"); expect(enemy.getStatStage(Stat.ATK)).toBe(-1); // Only the effect of the first Growl should be applied // Third turn, Struggle is used - await game.toNextTurn(); - game.move.select(MoveId.TACKLE); await game.move.forceEnemyMove(MoveId.SPLASH); //prevent protect from being used by the enemy await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); - await game.phaseInterceptor.to("MoveEndPhase"); + expect(darmanitan.hp).toBeLessThan(darmanitan.getMaxHp()); + + await game.toNextTurn(); + expect(darmanitan.getLastXMoves()[0].move).toBe(MoveId.STRUGGLE); + }); + + it("should lock into calling moves, even if also in moveset", async () => { + vi.spyOn(RandomMoveAttr.prototype, "getMoveOverride").mockReturnValue(MoveId.TACKLE); + await game.classicMode.startBattle([SpeciesId.GALAR_DARMANITAN]); + + const darmanitan = game.scene.getPlayerPokemon()!; + + game.move.select(MoveId.METRONOME); + await game.phaseInterceptor.to("TurnEndPhase"); + + // Gorilla Tactics should bypass dancer and instruct + expect(darmanitan.isMoveRestricted(MoveId.TACKLE)).toBe(true); + expect(darmanitan.isMoveRestricted(MoveId.METRONOME)).toBe(false); + expect(darmanitan.getLastXMoves(-1)).toEqual([ + expect.objectContaining({ move: MoveId.TACKLE, result: MoveResult.SUCCESS, useMode: MoveUseMode.FOLLOW_UP }), + expect.objectContaining({ move: MoveId.METRONOME, result: MoveResult.SUCCESS, useMode: MoveUseMode.NORMAL }), + ]); }); it("should activate when the opponenet protects", async () => { diff --git a/test/abilities/gulp_missile.test.ts b/test/abilities/gulp_missile.test.ts index b41eea9ce18..e47b22f0c06 100644 --- a/test/abilities/gulp_missile.test.ts +++ b/test/abilities/gulp_missile.test.ts @@ -41,7 +41,7 @@ describe("Abilities - Gulp Missile", () => { beforeEach(() => { game = new GameManager(phaserGame); game.override - .disableCrits() + .criticalHits(false) .battleStyle("single") .moveset([MoveId.SURF, MoveId.DIVE, MoveId.SPLASH, MoveId.SUBSTITUTE]) .enemySpecies(SpeciesId.SNORLAX) diff --git a/test/abilities/harvest.test.ts b/test/abilities/harvest.test.ts index dcfcf08bd74..c04af8899b8 100644 --- a/test/abilities/harvest.test.ts +++ b/test/abilities/harvest.test.ts @@ -46,7 +46,7 @@ describe("Abilities - Harvest", () => { .ability(AbilityId.HARVEST) .startingLevel(100) .battleStyle("single") - .disableCrits() + .criticalHits(false) .statusActivation(false) // Since we're using nuzzle to proc both enigma and sitrus berries .weather(WeatherType.SUNNY) // guaranteed recovery .enemyLevel(1) diff --git a/test/abilities/healer.test.ts b/test/abilities/healer.test.ts index 7f71be7b86e..b37c9effeb0 100644 --- a/test/abilities/healer.test.ts +++ b/test/abilities/healer.test.ts @@ -4,17 +4,15 @@ import { SpeciesId } from "#enums/species-id"; import { StatusEffect } from "#enums/status-effect"; import GameManager from "#test/testUtils/gameManager"; import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it, vi, type MockInstance } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { isNullOrUndefined } from "#app/utils/common"; -import { PostTurnResetStatusAbAttr } from "#app/data/abilities/ability"; import { allAbilities } from "#app/data/data-lists"; import type Pokemon from "#app/field/pokemon"; +import { PostTurnResetStatusAbAttr } from "#app/data/abilities/ability"; describe("Abilities - Healer", () => { let phaserGame: Phaser.Game; let game: GameManager; - let healerAttrSpy: MockInstance; - let healerAttr: PostTurnResetStatusAbAttr; beforeAll(() => { phaserGame = new Phaser.Game({ @@ -24,7 +22,6 @@ describe("Abilities - Healer", () => { afterEach(() => { game.phaseInterceptor.restoreOg(); - healerAttrSpy.mockRestore(); }); beforeEach(() => { @@ -33,35 +30,33 @@ describe("Abilities - Healer", () => { .moveset([MoveId.SPLASH]) .ability(AbilityId.BALL_FETCH) .battleStyle("double") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH); - healerAttr = allAbilities[AbilityId.HEALER].getAttrs(PostTurnResetStatusAbAttr)[0]; - healerAttrSpy = vi - .spyOn(healerAttr, "getCondition") - .mockReturnValue((pokemon: Pokemon) => !isNullOrUndefined(pokemon.getAlly())); + // Mock healer to have a 100% chance of healing its ally + vi.spyOn(allAbilities[AbilityId.HEALER].getAttrs("PostTurnResetStatusAbAttr")[0], "getCondition").mockReturnValue( + (pokemon: Pokemon) => !isNullOrUndefined(pokemon.getAlly()), + ); }); it("should not queue a message phase for healing if the ally has fainted", async () => { + const abSpy = vi.spyOn(PostTurnResetStatusAbAttr.prototype, "canApplyPostTurn"); game.override.moveset([MoveId.SPLASH, MoveId.LUNAR_DANCE]); await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MAGIKARP]); + const user = game.scene.getPlayerPokemon()!; - // Only want one magikarp to have the ability. + // Only want one magikarp to have the ability vi.spyOn(user, "getAbility").mockReturnValue(allAbilities[AbilityId.HEALER]); game.move.select(MoveId.SPLASH); // faint the ally game.move.select(MoveId.LUNAR_DANCE, 1); - const abSpy = vi.spyOn(healerAttr, "canApplyPostTurn"); await game.phaseInterceptor.to("TurnEndPhase"); // It's not enough to just test that the ally still has its status. // We need to ensure that the ability failed to meet its condition expect(abSpy).toHaveReturnedWith(false); - - // Explicitly restore the mock to ensure pollution doesn't happen - abSpy.mockRestore(); }); it("should heal the status of an ally if the ally has a status", async () => { diff --git a/test/abilities/heatproof.test.ts b/test/abilities/heatproof.test.ts index f705e8bf785..39c9dff8289 100644 --- a/test/abilities/heatproof.test.ts +++ b/test/abilities/heatproof.test.ts @@ -26,7 +26,7 @@ describe("Abilities - Heatproof", () => { game = new GameManager(phaserGame); game.override .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.CHARMANDER) .enemyAbility(AbilityId.HEATPROOF) .enemyMoveset(MoveId.SPLASH) diff --git a/test/abilities/honey_gather.test.ts b/test/abilities/honey_gather.test.ts index 06d351b2a61..f8700f3e6f7 100644 --- a/test/abilities/honey_gather.test.ts +++ b/test/abilities/honey_gather.test.ts @@ -29,7 +29,7 @@ describe("Abilities - Honey Gather", () => { .ability(AbilityId.HONEY_GATHER) .passiveAbility(AbilityId.RUN_AWAY) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH); diff --git a/test/abilities/hustle.test.ts b/test/abilities/hustle.test.ts index 58aa01bb39a..57a1a2e1d86 100644 --- a/test/abilities/hustle.test.ts +++ b/test/abilities/hustle.test.ts @@ -26,7 +26,7 @@ describe("Abilities - Hustle", () => { game.override .ability(AbilityId.HUSTLE) .moveset([MoveId.TACKLE, MoveId.GIGA_DRAIN, MoveId.FISSURE]) - .disableCrits() + .criticalHits(false) .battleStyle("single") .enemyMoveset(MoveId.SPLASH) .enemySpecies(SpeciesId.SHUCKLE) @@ -75,8 +75,7 @@ describe("Abilities - Hustle", () => { }); it("does not affect OHKO moves", async () => { - game.override.startingLevel(100); - game.override.enemyLevel(30); + game.override.startingLevel(100).enemyLevel(30); await game.classicMode.startBattle([SpeciesId.PIKACHU]); const pikachu = game.scene.getPlayerPokemon()!; diff --git a/test/abilities/ice_face.test.ts b/test/abilities/ice_face.test.ts index 2899120db5a..c42713d7e6c 100644 --- a/test/abilities/ice_face.test.ts +++ b/test/abilities/ice_face.test.ts @@ -30,10 +30,11 @@ describe("Abilities - Ice Face", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleStyle("single"); - game.override.enemySpecies(SpeciesId.EISCUE); - game.override.enemyAbility(AbilityId.ICE_FACE); - game.override.moveset([MoveId.TACKLE, MoveId.ICE_BEAM, MoveId.TOXIC_THREAD, MoveId.HAIL]); + game.override + .battleStyle("single") + .enemySpecies(SpeciesId.EISCUE) + .enemyAbility(AbilityId.ICE_FACE) + .moveset([MoveId.TACKLE, MoveId.ICE_BEAM, MoveId.TOXIC_THREAD, MoveId.HAIL]); }); it("takes no damage from physical move and transforms to Noice", async () => { @@ -51,8 +52,7 @@ describe("Abilities - Ice Face", () => { }); it("takes no damage from the first hit of multihit physical move and transforms to Noice", async () => { - game.override.moveset([MoveId.SURGING_STRIKES]); - game.override.enemyLevel(1); + game.override.moveset([MoveId.SURGING_STRIKES]).enemyLevel(1); await game.classicMode.startBattle([SpeciesId.HITMONLEE]); game.move.select(MoveId.SURGING_STRIKES); @@ -196,12 +196,13 @@ describe("Abilities - Ice Face", () => { }); it("reverts to Ice Face on arena reset", async () => { - game.override.startingWave(4); - game.override.startingLevel(4); - game.override.enemySpecies(SpeciesId.MAGIKARP); - game.override.starterForms({ - [SpeciesId.EISCUE]: noiceForm, - }); + game.override + .startingWave(4) + .startingLevel(4) + .enemySpecies(SpeciesId.MAGIKARP) + .starterForms({ + [SpeciesId.EISCUE]: noiceForm, + }); await game.classicMode.startBattle([SpeciesId.EISCUE]); diff --git a/test/abilities/illusion.test.ts b/test/abilities/illusion.test.ts index 37e2ca59c6a..1c2809ad813 100644 --- a/test/abilities/illusion.test.ts +++ b/test/abilities/illusion.test.ts @@ -116,26 +116,23 @@ describe("Abilities - Illusion", () => { expect(psychicEffectiveness).above(flameThrowerEffectiveness); }); - it("does not break from indirect damage", async () => { - game.override.enemySpecies(SpeciesId.GIGALITH); - game.override.enemyAbility(AbilityId.SAND_STREAM); - game.override.enemyMoveset(MoveId.WILL_O_WISP); - game.override.moveset([MoveId.FLARE_BLITZ]); + it("should not break from indirect damage from status, weather or recoil", async () => { + game.override.enemySpecies(SpeciesId.GIGALITH).enemyAbility(AbilityId.SAND_STREAM); await game.classicMode.startBattle([SpeciesId.ZOROARK, SpeciesId.AZUMARILL]); - game.move.select(MoveId.FLARE_BLITZ); - - await game.phaseInterceptor.to("TurnEndPhase"); + game.move.use(MoveId.FLARE_BLITZ); + await game.move.forceEnemyMove(MoveId.WILL_O_WISP); + await game.toEndOfTurn(); const zoroark = game.scene.getPlayerPokemon()!; - expect(!!zoroark.summonData.illusion).equals(true); }); it("copies the the name, nickname, gender, shininess, and pokeball from the illusion source", async () => { game.override.enemyMoveset(MoveId.SPLASH); await game.classicMode.startBattle([SpeciesId.ABRA, SpeciesId.ZOROARK, SpeciesId.AXEW]); + const axew = game.scene.getPlayerParty().at(2)!; axew.shiny = true; axew.nickname = btoa(unescape(encodeURIComponent("axew nickname"))); diff --git a/test/abilities/immunity.test.ts b/test/abilities/immunity.test.ts index b6ca34bfaa3..063ab57f11a 100644 --- a/test/abilities/immunity.test.ts +++ b/test/abilities/immunity.test.ts @@ -26,7 +26,7 @@ describe("Abilities - Immunity", () => { .moveset([MoveId.SPLASH]) .ability(AbilityId.BALL_FETCH) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH); diff --git a/test/abilities/imposter.test.ts b/test/abilities/imposter.test.ts index e9ae6bd64bb..30491139877 100644 --- a/test/abilities/imposter.test.ts +++ b/test/abilities/imposter.test.ts @@ -162,8 +162,7 @@ describe("Abilities - Imposter", () => { }); it("should stay transformed with the correct form after reload", async () => { - game.override.moveset([MoveId.ABSORB]); - game.override.enemySpecies(SpeciesId.UNOWN); + game.override.moveset([MoveId.ABSORB]).enemySpecies(SpeciesId.UNOWN); await game.classicMode.startBattle([SpeciesId.DITTO]); const enemy = game.scene.getEnemyPokemon()!; diff --git a/test/abilities/infiltrator.test.ts b/test/abilities/infiltrator.test.ts index 03b55e925cd..a5de035a136 100644 --- a/test/abilities/infiltrator.test.ts +++ b/test/abilities/infiltrator.test.ts @@ -31,7 +31,7 @@ describe("Abilities - Infiltrator", () => { .moveset([MoveId.TACKLE, MoveId.WATER_GUN, MoveId.SPORE, MoveId.BABY_DOLL_EYES]) .ability(AbilityId.INFILTRATOR) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.SNORLAX) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH) diff --git a/test/abilities/insomnia.test.ts b/test/abilities/insomnia.test.ts index 418e0ed1345..800663823c3 100644 --- a/test/abilities/insomnia.test.ts +++ b/test/abilities/insomnia.test.ts @@ -26,7 +26,7 @@ describe("Abilities - Insomnia", () => { .moveset([MoveId.SPLASH]) .ability(AbilityId.BALL_FETCH) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH); diff --git a/test/abilities/intimidate.test.ts b/test/abilities/intimidate.test.ts index e72b9669b6e..3dcd9bcd129 100644 --- a/test/abilities/intimidate.test.ts +++ b/test/abilities/intimidate.test.ts @@ -62,7 +62,7 @@ describe("Abilities - Intimidate", () => { expect(playerPokemon.species.speciesId).toBe(SpeciesId.POOCHYENA); expect(playerPokemon.getStatStage(Stat.ATK)).toBe(0); expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-2); - }, 20000); + }); it("should lower ATK stat stage by 1 for every enemy Pokemon in a double battle on entry", async () => { game.override.battleStyle("double").startingWave(3); @@ -85,11 +85,10 @@ describe("Abilities - Intimidate", () => { expect(enemyField[1].getStatStage(Stat.ATK)).toBe(-2); expect(playerField[0].getStatStage(Stat.ATK)).toBe(-2); expect(playerField[1].getStatStage(Stat.ATK)).toBe(-2); - }, 20000); + }); it("should not activate again if there is no switch or new entry", async () => { - game.override.startingWave(2); - game.override.moveset([MoveId.SPLASH]); + game.override.startingWave(2).moveset([MoveId.SPLASH]); await game.classicMode.startBattle([SpeciesId.MIGHTYENA, SpeciesId.POOCHYENA]); const playerPokemon = game.scene.getPlayerPokemon()!; @@ -103,7 +102,7 @@ describe("Abilities - Intimidate", () => { expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-1); expect(playerPokemon.getStatStage(Stat.ATK)).toBe(-1); - }, 20000); + }); it("should lower ATK stat stage by 1 for every switch", async () => { game.override.moveset([MoveId.SPLASH]).enemyMoveset([MoveId.VOLT_SWITCH]).startingWave(5); @@ -130,5 +129,5 @@ describe("Abilities - Intimidate", () => { expect(playerPokemon.getStatStage(Stat.ATK)).toBe(-3); expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0); - }, 200000); + }); }); diff --git a/test/abilities/intrepid_sword.test.ts b/test/abilities/intrepid_sword.test.ts index 7cfa9581bb9..d9b81e9552e 100644 --- a/test/abilities/intrepid_sword.test.ts +++ b/test/abilities/intrepid_sword.test.ts @@ -22,10 +22,11 @@ describe("Abilities - Intrepid Sword", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleStyle("single"); - game.override.enemySpecies(SpeciesId.ZACIAN); - game.override.enemyAbility(AbilityId.INTREPID_SWORD); - game.override.ability(AbilityId.INTREPID_SWORD); + game.override + .battleStyle("single") + .enemySpecies(SpeciesId.ZACIAN) + .enemyAbility(AbilityId.INTREPID_SWORD) + .ability(AbilityId.INTREPID_SWORD); }); it("should raise ATK stat stage by 1 on entry", async () => { @@ -38,5 +39,5 @@ describe("Abilities - Intrepid Sword", () => { expect(playerPokemon.getStatStage(Stat.ATK)).toBe(1); expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1); - }, 20000); + }); }); diff --git a/test/abilities/libero.test.ts b/test/abilities/libero.test.ts index 6cae83219fd..eaa2630e90d 100644 --- a/test/abilities/libero.test.ts +++ b/test/abilities/libero.test.ts @@ -108,8 +108,7 @@ describe("Abilities - Libero", () => { }); test("ability applies correctly even if the type has changed by another ability", async () => { - game.override.moveset([MoveId.TACKLE]); - game.override.passiveAbility(AbilityId.REFRIGERATE); + game.override.moveset([MoveId.TACKLE]).passiveAbility(AbilityId.REFRIGERATE); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); @@ -156,8 +155,7 @@ describe("Abilities - Libero", () => { }); test("ability applies correctly even if the pokemon's move misses", async () => { - game.override.moveset([MoveId.TACKLE]); - game.override.enemyMoveset(MoveId.SPLASH); + game.override.moveset([MoveId.TACKLE]).enemyMoveset(MoveId.SPLASH); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); @@ -188,8 +186,7 @@ describe("Abilities - Libero", () => { }); test("ability applies correctly even if the pokemon's move fails because of type immunity", async () => { - game.override.moveset([MoveId.TACKLE]); - game.override.enemySpecies(SpeciesId.GASTLY); + game.override.moveset([MoveId.TACKLE]).enemySpecies(SpeciesId.GASTLY); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); @@ -262,8 +259,7 @@ describe("Abilities - Libero", () => { }); test("ability applies correctly even if the pokemon's Trick-or-Treat fails", async () => { - game.override.moveset([MoveId.TRICK_OR_TREAT]); - game.override.enemySpecies(SpeciesId.GASTLY); + game.override.moveset([MoveId.TRICK_OR_TREAT]).enemySpecies(SpeciesId.GASTLY); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); diff --git a/test/abilities/lightningrod.test.ts b/test/abilities/lightningrod.test.ts index a69f5e94067..2dc29500454 100644 --- a/test/abilities/lightningrod.test.ts +++ b/test/abilities/lightningrod.test.ts @@ -27,7 +27,7 @@ describe("Abilities - Lightningrod", () => { .moveset([MoveId.SPLASH, MoveId.SHOCK_WAVE]) .ability(AbilityId.BALL_FETCH) .battleStyle("double") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH); diff --git a/test/abilities/limber.test.ts b/test/abilities/limber.test.ts index 2ca469dcaa1..77acd8b3dab 100644 --- a/test/abilities/limber.test.ts +++ b/test/abilities/limber.test.ts @@ -26,7 +26,7 @@ describe("Abilities - Limber", () => { .moveset([MoveId.SPLASH]) .ability(AbilityId.BALL_FETCH) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH); diff --git a/test/abilities/magic_bounce.test.ts b/test/abilities/magic_bounce.test.ts index 3c4ff2ad909..04bfcbbfe8f 100644 --- a/test/abilities/magic_bounce.test.ts +++ b/test/abilities/magic_bounce.test.ts @@ -32,7 +32,7 @@ describe("Abilities - Magic Bounce", () => { .ability(AbilityId.BALL_FETCH) .battleStyle("single") .moveset([MoveId.GROWL, MoveId.SPLASH]) - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.MAGIC_BOUNCE) .enemyMoveset(MoveId.SPLASH); @@ -41,18 +41,16 @@ describe("Abilities - Magic Bounce", () => { it("should reflect basic status moves", async () => { await game.classicMode.startBattle([SpeciesId.MAGIKARP]); - game.move.select(MoveId.GROWL); + game.move.use(MoveId.GROWL); await game.phaseInterceptor.to("BerryPhase"); expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(-1); }); it("should not bounce moves while the target is in the semi-invulnerable state", async () => { await game.classicMode.startBattle([SpeciesId.MAGIKARP]); - game.override.moveset([MoveId.GROWL]); - game.override.enemyMoveset([MoveId.FLY]); - game.move.select(MoveId.GROWL); - await game.move.selectEnemyMove(MoveId.FLY); + game.move.use(MoveId.GROWL); + await game.move.forceEnemyMove(MoveId.FLY); await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.phaseInterceptor.to("BerryPhase"); @@ -61,11 +59,10 @@ describe("Abilities - Magic Bounce", () => { it("should individually bounce back multi-target moves", async () => { game.override.battleStyle("double"); - game.override.moveset([MoveId.GROWL, MoveId.SPLASH]); await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MAGIKARP]); - game.move.select(MoveId.GROWL, 0); - game.move.select(MoveId.SPLASH, 1); + game.move.use(MoveId.GROWL, 0); + game.move.use(MoveId.SPLASH, 1); await game.phaseInterceptor.to("BerryPhase"); const user = game.scene.getPlayerField()[0]; @@ -75,9 +72,8 @@ describe("Abilities - Magic Bounce", () => { it("should still bounce back a move that would otherwise fail", async () => { await game.classicMode.startBattle([SpeciesId.MAGIKARP]); game.scene.getEnemyPokemon()?.setStatStage(Stat.ATK, -6); - game.override.moveset([MoveId.GROWL]); - game.move.select(MoveId.GROWL); + game.move.use(MoveId.GROWL); await game.phaseInterceptor.to("BerryPhase"); expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(-1); @@ -107,29 +103,26 @@ describe("Abilities - Magic Bounce", () => { game.override.ability(AbilityId.MOLD_BREAKER); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); - game.move.select(MoveId.GROWL); + game.move.use(MoveId.GROWL); await game.phaseInterceptor.to("BerryPhase"); expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.ATK)).toBe(-1); }); it("should bounce back a spread status move against both pokemon", async () => { - game.override.battleStyle("double"); - game.override.moveset([MoveId.GROWL, MoveId.SPLASH]); - game.override.enemyMoveset([MoveId.SPLASH]); + game.override.battleStyle("double").enemyMoveset([MoveId.SPLASH]); await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MAGIKARP]); - game.move.select(MoveId.GROWL, 0); - game.move.select(MoveId.SPLASH, 1); + game.move.use(MoveId.GROWL, 0); + game.move.use(MoveId.SPLASH, 1); await game.phaseInterceptor.to("BerryPhase"); expect(game.scene.getPlayerField().every(p => p.getStatStage(Stat.ATK) === -2)).toBeTruthy(); }); it("should only bounce spikes back once in doubles when both targets have magic bounce", async () => { - game.override.battleStyle("double"); + game.override.battleStyle("double").moveset([MoveId.SPIKES]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); - game.override.moveset([MoveId.SPIKES]); game.move.select(MoveId.SPIKES); await game.phaseInterceptor.to("BerryPhase"); @@ -139,8 +132,7 @@ describe("Abilities - Magic Bounce", () => { }); it("should bounce spikes even when the target is protected", async () => { - game.override.moveset([MoveId.SPIKES]); - game.override.enemyMoveset([MoveId.PROTECT]); + game.override.moveset([MoveId.SPIKES]).enemyMoveset([MoveId.PROTECT]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); game.move.select(MoveId.SPIKES); @@ -149,8 +141,7 @@ describe("Abilities - Magic Bounce", () => { }); it("should not bounce spikes when the target is in the semi-invulnerable state", async () => { - game.override.moveset([MoveId.SPIKES]); - game.override.enemyMoveset([MoveId.FLY]); + game.override.moveset([MoveId.SPIKES]).enemyMoveset([MoveId.FLY]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); game.move.select(MoveId.SPIKES); @@ -160,9 +151,8 @@ describe("Abilities - Magic Bounce", () => { }); it("should not bounce back curse", async () => { - game.override.starterSpecies(SpeciesId.GASTLY); - await game.classicMode.startBattle([SpeciesId.GASTLY]); game.override.moveset([MoveId.CURSE]); + await game.classicMode.startBattle([SpeciesId.GASTLY]); game.move.select(MoveId.CURSE); await game.phaseInterceptor.to("BerryPhase"); @@ -171,8 +161,7 @@ describe("Abilities - Magic Bounce", () => { }); it("should not cause encore to be interrupted after bouncing", async () => { - game.override.moveset([MoveId.SPLASH, MoveId.GROWL, MoveId.ENCORE]); - game.override.enemyMoveset([MoveId.TACKLE, MoveId.GROWL]); + game.override.moveset([MoveId.SPLASH, MoveId.GROWL, MoveId.ENCORE]).enemyMoveset([MoveId.TACKLE, MoveId.GROWL]); // game.override.ability(AbilityId.MOLD_BREAKER); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); const playerPokemon = game.scene.getPlayerPokemon()!; @@ -199,9 +188,10 @@ describe("Abilities - Magic Bounce", () => { // TODO: encore is failing if the last move was virtual. it.todo("should not cause the bounced move to count for encore", async () => { - game.override.moveset([MoveId.SPLASH, MoveId.GROWL, MoveId.ENCORE]); - game.override.enemyMoveset([MoveId.GROWL, MoveId.TACKLE]); - game.override.enemyAbility(AbilityId.MAGIC_BOUNCE); + game.override + .moveset([MoveId.SPLASH, MoveId.GROWL, MoveId.ENCORE]) + .enemyMoveset([MoveId.GROWL, MoveId.TACKLE]) + .enemyAbility(AbilityId.MAGIC_BOUNCE); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); const playerPokemon = game.scene.getPlayerPokemon()!; @@ -227,9 +217,8 @@ describe("Abilities - Magic Bounce", () => { // TODO: stomping tantrum should consider moves that were bounced. it.todo("should cause stomping tantrum to double in power when the last move was bounced", async () => { - game.override.battleStyle("single"); + game.override.battleStyle("single").moveset([MoveId.STOMPING_TANTRUM, MoveId.CHARM]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); - game.override.moveset([MoveId.STOMPING_TANTRUM, MoveId.CHARM]); const stomping_tantrum = allMoves[MoveId.STOMPING_TANTRUM]; vi.spyOn(stomping_tantrum, "calculateBattlePower"); @@ -242,36 +231,30 @@ describe("Abilities - Magic Bounce", () => { expect(stomping_tantrum.calculateBattlePower).toHaveReturnedWith(150); }); - // TODO: stomping tantrum should consider moves that were bounced. - it.todo( - "should properly cause the enemy's stomping tantrum to be doubled in power after bouncing and failing", - async () => { - game.override.enemyMoveset([MoveId.STOMPING_TANTRUM, MoveId.SPLASH, MoveId.CHARM]); - await game.classicMode.startBattle([SpeciesId.BULBASAUR]); + // TODO: stomping tantrum should consider moves that were bounced + it.todo("should boost enemy's stomping tantrum after failed bounce", async () => { + game.override.enemyMoveset([MoveId.STOMPING_TANTRUM, MoveId.SPLASH, MoveId.CHARM]); + await game.classicMode.startBattle([SpeciesId.BULBASAUR]); - const stomping_tantrum = allMoves[MoveId.STOMPING_TANTRUM]; - const enemy = game.scene.getEnemyPokemon()!; - vi.spyOn(stomping_tantrum, "calculateBattlePower"); + const stomping_tantrum = allMoves[MoveId.STOMPING_TANTRUM]; + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(stomping_tantrum, "calculateBattlePower"); - game.move.select(MoveId.SPORE); - await game.move.selectEnemyMove(MoveId.CHARM); - await game.phaseInterceptor.to("TurnEndPhase"); - expect(enemy.getLastXMoves(1)[0].result).toBe("success"); + // Spore gets reflected back onto us + game.move.select(MoveId.SPORE); + await game.move.selectEnemyMove(MoveId.CHARM); + await game.toNextTurn(); + expect(enemy.getLastXMoves(1)[0].result).toBe("success"); - await game.phaseInterceptor.to("BerryPhase"); - expect(stomping_tantrum.calculateBattlePower).toHaveReturnedWith(75); - - await game.toNextTurn(); - game.move.select(MoveId.GROWL); - await game.phaseInterceptor.to("BerryPhase"); - expect(stomping_tantrum.calculateBattlePower).toHaveReturnedWith(75); - }, - ); + game.move.select(MoveId.SPORE); + await game.move.selectEnemyMove(MoveId.STOMPING_TANTRUM); + await game.toNextTurn(); + expect(stomping_tantrum.calculateBattlePower).toHaveReturnedWith(150); + }); it("should respect immunities when bouncing a move", async () => { vi.spyOn(allMoves[MoveId.THUNDER_WAVE], "accuracy", "get").mockReturnValue(100); - game.override.moveset([MoveId.THUNDER_WAVE, MoveId.GROWL]); - game.override.ability(AbilityId.SOUNDPROOF); + game.override.moveset([MoveId.THUNDER_WAVE, MoveId.GROWL]).ability(AbilityId.SOUNDPROOF); await game.classicMode.startBattle([SpeciesId.PHANPY]); // Turn 1 - thunder wave immunity test @@ -309,8 +292,7 @@ describe("Abilities - Magic Bounce", () => { }); it("should always apply the leftmost available target's magic bounce when bouncing moves like sticky webs in doubles", async () => { - game.override.battleStyle("double"); - game.override.moveset([MoveId.STICKY_WEB, MoveId.SPLASH, MoveId.TRICK_ROOM]); + game.override.battleStyle("double").moveset([MoveId.STICKY_WEB, MoveId.SPLASH, MoveId.TRICK_ROOM]); await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MAGIKARP]); const [enemy_1, enemy_2] = game.scene.getEnemyField(); @@ -345,6 +327,7 @@ describe("Abilities - Magic Bounce", () => { it("should not bounce back status moves that hit through semi-invulnerable states", async () => { game.override.moveset([MoveId.TOXIC, MoveId.CHARM]); await game.classicMode.startBattle([SpeciesId.BULBASAUR]); + game.move.select(MoveId.TOXIC); await game.move.selectEnemyMove(MoveId.FLY); await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); diff --git a/test/abilities/magic_guard.test.ts b/test/abilities/magic_guard.test.ts index 40bb3203673..f135a761bba 100644 --- a/test/abilities/magic_guard.test.ts +++ b/test/abilities/magic_guard.test.ts @@ -30,16 +30,16 @@ describe("Abilities - Magic Guard", () => { beforeEach(() => { game = new GameManager(phaserGame); - /** Player Pokemon overrides */ - game.override.ability(AbilityId.MAGIC_GUARD); - game.override.moveset([MoveId.SPLASH]); - game.override.startingLevel(100); - - /** Enemy Pokemon overrides */ - game.override.enemySpecies(SpeciesId.SNORLAX); - game.override.enemyAbility(AbilityId.INSOMNIA); - game.override.enemyMoveset(MoveId.SPLASH); - game.override.enemyLevel(100); + game.override + /** Player Pokemon overrides */ + .ability(AbilityId.MAGIC_GUARD) + .moveset([MoveId.SPLASH]) + .startingLevel(100) + /** Enemy Pokemon overrides */ + .enemySpecies(SpeciesId.SNORLAX) + .enemyAbility(AbilityId.INSOMNIA) + .enemyMoveset(MoveId.SPLASH) + .enemyLevel(100); }); //Bulbapedia Reference: https://bulbapedia.bulbagarden.net/wiki/Magic_Guard_(Ability) @@ -89,8 +89,7 @@ describe("Abilities - Magic Guard", () => { }); it("ability effect should not persist when the ability is replaced", async () => { - game.override.enemyMoveset([MoveId.WORRY_SEED, MoveId.WORRY_SEED, MoveId.WORRY_SEED, MoveId.WORRY_SEED]); - game.override.statusEffect(StatusEffect.POISON); + game.override.enemyMoveset(MoveId.WORRY_SEED).statusEffect(StatusEffect.POISON); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); @@ -108,8 +107,7 @@ describe("Abilities - Magic Guard", () => { }); it("Magic Guard prevents damage caused by burn but other non-damaging effects are still applied", async () => { - game.override.enemyStatusEffect(StatusEffect.BURN); - game.override.enemyAbility(AbilityId.MAGIC_GUARD); + game.override.enemyStatusEffect(StatusEffect.BURN).enemyAbility(AbilityId.MAGIC_GUARD); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); @@ -130,8 +128,7 @@ describe("Abilities - Magic Guard", () => { }); it("Magic Guard prevents damage caused by toxic but other non-damaging effects are still applied", async () => { - game.override.enemyStatusEffect(StatusEffect.TOXIC); - game.override.enemyAbility(AbilityId.MAGIC_GUARD); + game.override.enemyStatusEffect(StatusEffect.TOXIC).enemyAbility(AbilityId.MAGIC_GUARD); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); @@ -208,8 +205,7 @@ describe("Abilities - Magic Guard", () => { it("Magic Guard prevents against damage from volatile status effects", async () => { await game.classicMode.startBattle([SpeciesId.DUSKULL]); - game.override.moveset([MoveId.CURSE]); - game.override.enemyAbility(AbilityId.MAGIC_GUARD); + game.override.moveset([MoveId.CURSE]).enemyAbility(AbilityId.MAGIC_GUARD); const leadPokemon = game.scene.getPlayerPokemon()!; @@ -331,8 +327,9 @@ describe("Abilities - Magic Guard", () => { //Tests the ability Bad Dreams game.override.statusEffect(StatusEffect.SLEEP); //enemy pokemon is given Spore just in case player pokemon somehow awakens during test - game.override.enemyMoveset([MoveId.SPORE, MoveId.SPORE, MoveId.SPORE, MoveId.SPORE]); - game.override.enemyAbility(AbilityId.BAD_DREAMS); + game.override + .enemyMoveset([MoveId.SPORE, MoveId.SPORE, MoveId.SPORE, MoveId.SPORE]) + .enemyAbility(AbilityId.BAD_DREAMS); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); @@ -353,8 +350,7 @@ describe("Abilities - Magic Guard", () => { it("Magic Guard prevents damage from abilities with PostFaintContactDamageAbAttr", async () => { //Tests the abilities Innards Out/Aftermath - game.override.moveset([MoveId.TACKLE]); - game.override.enemyAbility(AbilityId.AFTERMATH); + game.override.moveset([MoveId.TACKLE]).enemyAbility(AbilityId.AFTERMATH); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); @@ -377,8 +373,7 @@ describe("Abilities - Magic Guard", () => { it("Magic Guard prevents damage from abilities with PostDefendContactDamageAbAttr", async () => { //Tests the abilities Iron Barbs/Rough Skin - game.override.moveset([MoveId.TACKLE]); - game.override.enemyAbility(AbilityId.IRON_BARBS); + game.override.moveset([MoveId.TACKLE]).enemyAbility(AbilityId.IRON_BARBS); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); @@ -400,8 +395,7 @@ describe("Abilities - Magic Guard", () => { it("Magic Guard prevents damage from abilities with ReverseDrainAbAttr", async () => { //Tests the ability Liquid Ooze - game.override.moveset([MoveId.ABSORB]); - game.override.enemyAbility(AbilityId.LIQUID_OOZE); + game.override.moveset([MoveId.ABSORB]).enemyAbility(AbilityId.LIQUID_OOZE); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); @@ -422,9 +416,7 @@ describe("Abilities - Magic Guard", () => { }); it("Magic Guard prevents HP loss from abilities with PostWeatherLapseDamageAbAttr", async () => { - //Tests the abilities Solar Power/Dry Skin - game.override.passiveAbility(AbilityId.SOLAR_POWER); - game.override.weather(WeatherType.SUNNY); + game.override.passiveAbility(AbilityId.SOLAR_POWER).weather(WeatherType.SUNNY); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); const leadPokemon = game.scene.getPlayerPokemon()!; diff --git a/test/abilities/magma_armor.test.ts b/test/abilities/magma_armor.test.ts index 74493fac365..0a8f3024291 100644 --- a/test/abilities/magma_armor.test.ts +++ b/test/abilities/magma_armor.test.ts @@ -26,7 +26,7 @@ describe("Abilities - Magma Armor", () => { .moveset([MoveId.SPLASH]) .ability(AbilityId.BALL_FETCH) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH); diff --git a/test/abilities/mimicry.test.ts b/test/abilities/mimicry.test.ts index df6facc3755..7ac7aeb599f 100644 --- a/test/abilities/mimicry.test.ts +++ b/test/abilities/mimicry.test.ts @@ -26,7 +26,7 @@ describe("Abilities - Mimicry", () => { .moveset([MoveId.SPLASH]) .ability(AbilityId.MIMICRY) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyMoveset(MoveId.SPLASH); }); diff --git a/test/abilities/mirror_armor.test.ts b/test/abilities/mirror_armor.test.ts index 540a3b5d426..71b072c7d06 100644 --- a/test/abilities/mirror_armor.test.ts +++ b/test/abilities/mirror_armor.test.ts @@ -37,8 +37,7 @@ describe("Ability - Mirror Armor", () => { }); it("Player side + single battle Intimidate - opponent loses stats", async () => { - game.override.ability(AbilityId.MIRROR_ARMOR); - game.override.enemyAbility(AbilityId.INTIMIDATE); + game.override.ability(AbilityId.MIRROR_ARMOR).enemyAbility(AbilityId.INTIMIDATE); await game.classicMode.startBattle([SpeciesId.BULBASAUR]); const enemyPokemon = game.scene.getEnemyPokemon()!; @@ -54,8 +53,7 @@ describe("Ability - Mirror Armor", () => { }); it("Enemy side + single battle Intimidate - player loses stats", async () => { - game.override.enemyAbility(AbilityId.MIRROR_ARMOR); - game.override.ability(AbilityId.INTIMIDATE); + game.override.enemyAbility(AbilityId.MIRROR_ARMOR).ability(AbilityId.INTIMIDATE); await game.classicMode.startBattle([SpeciesId.BULBASAUR]); const enemyPokemon = game.scene.getEnemyPokemon()!; @@ -71,9 +69,7 @@ describe("Ability - Mirror Armor", () => { }); it("Player side + double battle Intimidate - opponents each lose -2 atk", async () => { - game.override.battleStyle("double"); - game.override.ability(AbilityId.MIRROR_ARMOR); - game.override.enemyAbility(AbilityId.INTIMIDATE); + game.override.battleStyle("double").ability(AbilityId.MIRROR_ARMOR).enemyAbility(AbilityId.INTIMIDATE); await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER]); const [enemy1, enemy2] = game.scene.getEnemyField(); @@ -93,9 +89,7 @@ describe("Ability - Mirror Armor", () => { }); it("Enemy side + double battle Intimidate - players each lose -2 atk", async () => { - game.override.battleStyle("double"); - game.override.enemyAbility(AbilityId.MIRROR_ARMOR); - game.override.ability(AbilityId.INTIMIDATE); + game.override.battleStyle("double").enemyAbility(AbilityId.MIRROR_ARMOR).ability(AbilityId.INTIMIDATE); await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER]); const [enemy1, enemy2] = game.scene.getEnemyField(); @@ -115,8 +109,7 @@ describe("Ability - Mirror Armor", () => { }); it("Player side + single battle Intimidate + Tickle - opponent loses stats", async () => { - game.override.ability(AbilityId.MIRROR_ARMOR); - game.override.enemyAbility(AbilityId.INTIMIDATE); + game.override.ability(AbilityId.MIRROR_ARMOR).enemyAbility(AbilityId.INTIMIDATE); await game.classicMode.startBattle([SpeciesId.BULBASAUR]); const enemyPokemon = game.scene.getEnemyPokemon()!; @@ -134,9 +127,7 @@ describe("Ability - Mirror Armor", () => { }); it("Player side + double battle Intimidate + Tickle - opponents each lose -3 atk, -1 def", async () => { - game.override.battleStyle("double"); - game.override.ability(AbilityId.MIRROR_ARMOR); - game.override.enemyAbility(AbilityId.INTIMIDATE); + game.override.battleStyle("double").ability(AbilityId.MIRROR_ARMOR).enemyAbility(AbilityId.INTIMIDATE); await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER]); const [enemy1, enemy2] = game.scene.getEnemyField(); @@ -159,8 +150,7 @@ describe("Ability - Mirror Armor", () => { }); it("Enemy side + single battle Intimidate + Tickle - player loses stats", async () => { - game.override.enemyAbility(AbilityId.MIRROR_ARMOR); - game.override.ability(AbilityId.INTIMIDATE); + game.override.enemyAbility(AbilityId.MIRROR_ARMOR).ability(AbilityId.INTIMIDATE); await game.classicMode.startBattle([SpeciesId.BULBASAUR]); const enemyPokemon = game.scene.getEnemyPokemon()!; @@ -178,8 +168,7 @@ describe("Ability - Mirror Armor", () => { }); it("Player side + single battle Intimidate + oppoenent has white smoke - no one loses stats", async () => { - game.override.enemyAbility(AbilityId.WHITE_SMOKE); - game.override.ability(AbilityId.MIRROR_ARMOR); + game.override.enemyAbility(AbilityId.WHITE_SMOKE).ability(AbilityId.MIRROR_ARMOR); await game.classicMode.startBattle([SpeciesId.BULBASAUR]); const enemyPokemon = game.scene.getEnemyPokemon()!; @@ -197,8 +186,7 @@ describe("Ability - Mirror Armor", () => { }); it("Enemy side + single battle Intimidate + player has white smoke - no one loses stats", async () => { - game.override.ability(AbilityId.WHITE_SMOKE); - game.override.enemyAbility(AbilityId.MIRROR_ARMOR); + game.override.ability(AbilityId.WHITE_SMOKE).enemyAbility(AbilityId.MIRROR_ARMOR); await game.classicMode.startBattle([SpeciesId.BULBASAUR]); const enemyPokemon = game.scene.getEnemyPokemon()!; @@ -252,9 +240,7 @@ describe("Ability - Mirror Armor", () => { }); it("Both sides have mirror armor - does not loop, player loses attack", async () => { - game.override.enemyAbility(AbilityId.MIRROR_ARMOR); - game.override.ability(AbilityId.MIRROR_ARMOR); - game.override.ability(AbilityId.INTIMIDATE); + game.override.enemyAbility(AbilityId.MIRROR_ARMOR).ability(AbilityId.MIRROR_ARMOR).ability(AbilityId.INTIMIDATE); await game.classicMode.startBattle([SpeciesId.BULBASAUR]); const enemyPokemon = game.scene.getEnemyPokemon()!; @@ -288,8 +274,7 @@ describe("Ability - Mirror Armor", () => { }); it("Double battle + sticky web applied player side - player switches out and enemy 1 should lose -1 speed", async () => { - game.override.battleStyle("double"); - game.override.ability(AbilityId.MIRROR_ARMOR); + game.override.battleStyle("double").ability(AbilityId.MIRROR_ARMOR); await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER, SpeciesId.SQUIRTLE]); const [enemy1, enemy2] = game.scene.getEnemyField(); diff --git a/test/abilities/mold_breaker.test.ts b/test/abilities/mold_breaker.test.ts index e2e8a73da9d..28a077e8908 100644 --- a/test/abilities/mold_breaker.test.ts +++ b/test/abilities/mold_breaker.test.ts @@ -27,7 +27,7 @@ describe("Abilities - Mold Breaker", () => { .moveset([MoveId.SPLASH]) .ability(AbilityId.MOLD_BREAKER) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH); diff --git a/test/abilities/moxie.test.ts b/test/abilities/moxie.test.ts index 8b658f60bf6..a85ed081448 100644 --- a/test/abilities/moxie.test.ts +++ b/test/abilities/moxie.test.ts @@ -27,13 +27,14 @@ describe("Abilities - Moxie", () => { beforeEach(() => { game = new GameManager(phaserGame); const moveToUse = MoveId.AERIAL_ACE; - game.override.battleStyle("single"); - game.override.enemySpecies(SpeciesId.RATTATA); - game.override.enemyAbility(AbilityId.MOXIE); - game.override.ability(AbilityId.MOXIE); - game.override.startingLevel(2000); - game.override.moveset([moveToUse]); - game.override.enemyMoveset(MoveId.SPLASH); + game.override + .battleStyle("single") + .enemySpecies(SpeciesId.RATTATA) + .enemyAbility(AbilityId.MOXIE) + .ability(AbilityId.MOXIE) + .startingLevel(2000) + .moveset([moveToUse]) + .enemyMoveset(MoveId.SPLASH); }); it("should raise ATK stat stage by 1 when winning a battle", async () => { @@ -48,7 +49,7 @@ describe("Abilities - Moxie", () => { await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(VictoryPhase); expect(playerPokemon.getStatStage(Stat.ATK)).toBe(1); - }, 20000); + }); // TODO: Activate this test when MOXIE is corrected to work on faint and not on battle victory it.todo( diff --git a/test/abilities/mummy.test.ts b/test/abilities/mummy.test.ts index 2b35e801677..d6c40541d25 100644 --- a/test/abilities/mummy.test.ts +++ b/test/abilities/mummy.test.ts @@ -25,7 +25,7 @@ describe("Abilities - Mummy", () => { .moveset([MoveId.SPLASH]) .ability(AbilityId.MUMMY) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.TACKLE); diff --git a/test/abilities/mycelium_might.test.ts b/test/abilities/mycelium_might.test.ts index 1f236f2c2fe..2cdda4353b5 100644 --- a/test/abilities/mycelium_might.test.ts +++ b/test/abilities/mycelium_might.test.ts @@ -26,7 +26,7 @@ describe("Abilities - Mycelium Might", () => { game = new GameManager(phaserGame); game.override .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.SHUCKLE) .enemyAbility(AbilityId.CLEAR_BODY) @@ -63,7 +63,7 @@ describe("Abilities - Mycelium Might", () => { // Despite the opponent's ability (Clear Body), its ATK stat stage is still reduced. expect(enemyPokemon?.getStatStage(Stat.ATK)).toBe(-1); - }, 20000); + }); it("will still go first if a status move that is in a higher priority bracket than the opponent's move is used", async () => { game.override.enemyMoveset(MoveId.TACKLE); @@ -86,7 +86,7 @@ describe("Abilities - Mycelium Might", () => { await game.phaseInterceptor.to(TurnEndPhase); // Despite the opponent's ability (Clear Body), its ATK stat stage is still reduced. expect(enemyPokemon?.getStatStage(Stat.ATK)).toBe(-1); - }, 20000); + }); it("will not affect non-status moves", async () => { await game.classicMode.startBattle([SpeciesId.REGIELEKI]); @@ -105,5 +105,5 @@ describe("Abilities - Mycelium Might", () => { // This means that the commandOrder should be identical to the speedOrder expect(speedOrder).toEqual([playerIndex, enemyIndex]); expect(commandOrder).toEqual([playerIndex, enemyIndex]); - }, 20000); + }); }); diff --git a/test/abilities/neutralizing_gas.test.ts b/test/abilities/neutralizing_gas.test.ts index 067d164e835..2408a78f11d 100644 --- a/test/abilities/neutralizing_gas.test.ts +++ b/test/abilities/neutralizing_gas.test.ts @@ -1,7 +1,6 @@ import { BattlerIndex } from "#enums/battler-index"; import type { CommandPhase } from "#app/phases/command-phase"; import { Command } from "#enums/command"; -import { PostSummonWeatherChangeAbAttr } from "#app/data/abilities/ability"; import { AbilityId } from "#enums/ability-id"; import { ArenaTagType } from "#enums/arena-tag-type"; import { MoveId } from "#enums/move-id"; @@ -32,7 +31,7 @@ describe("Abilities - Neutralizing Gas", () => { .moveset([MoveId.SPLASH]) .ability(AbilityId.NEUTRALIZING_GAS) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH); @@ -178,7 +177,7 @@ describe("Abilities - Neutralizing Gas", () => { await game.classicMode.startBattle([SpeciesId.MAGIKARP]); const enemy = game.scene.getEnemyPokemon()!; - const weatherChangeAttr = enemy.getAbilityAttrs(PostSummonWeatherChangeAbAttr, false)[0]; + const weatherChangeAttr = enemy.getAbilityAttrs("PostSummonWeatherChangeAbAttr", false)[0]; vi.spyOn(weatherChangeAttr, "applyPostSummon"); expect(game.scene.arena.getTag(ArenaTagType.NEUTRALIZING_GAS)).toBeDefined(); diff --git a/test/abilities/normal-move-type-change.test.ts b/test/abilities/normal-move-type-change.test.ts index 03fb5b2e7be..d9e39b32a7c 100644 --- a/test/abilities/normal-move-type-change.test.ts +++ b/test/abilities/normal-move-type-change.test.ts @@ -9,7 +9,6 @@ import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { TYPE_BOOST_ITEM_BOOST_PERCENT } from "#app/constants"; import { allAbilities } from "#app/data/data-lists"; -import { MoveTypeChangeAbAttr } from "#app/data/abilities/ability"; import { toDmgValue } from "#app/utils/common"; /** @@ -160,7 +159,7 @@ describe.each([ // get the power boost from the ability so we can compare it to the item // @ts-expect-error power multiplier is private - const boost = allAbilities[ab]?.getAttrs(MoveTypeChangeAbAttr)[0]?.powerMultiplier; + const boost = allAbilities[ab]?.getAttrs("MoveTypeChangeAbAttr")[0]?.powerMultiplier; expect(boost, "power boost should be defined").toBeDefined(); const powerSpy = vi.spyOn(testMoveInstance, "calculateBattlePower"); @@ -177,7 +176,7 @@ describe.each([ // get the power boost from the ability so we can compare it to the item // @ts-expect-error power multiplier is private - const boost = allAbilities[ab]?.getAttrs(MoveTypeChangeAbAttr)[0]?.powerMultiplier; + const boost = allAbilities[ab]?.getAttrs("MoveTypeChangeAbAttr")[0]?.powerMultiplier; expect(boost, "power boost should be defined").toBeDefined(); const tackle = allMoves[MoveId.TACKLE]; diff --git a/test/abilities/normalize.test.ts b/test/abilities/normalize.test.ts index 821ce9589a1..1a4b93f3f32 100644 --- a/test/abilities/normalize.test.ts +++ b/test/abilities/normalize.test.ts @@ -29,7 +29,7 @@ describe("Abilities - Normalize", () => { .moveset([MoveId.TACKLE]) .ability(AbilityId.NORMALIZE) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH); @@ -45,8 +45,9 @@ describe("Abilities - Normalize", () => { }); it("should not apply the old type boost item after changing a move's type", async () => { - game.override.startingHeldItems([{ name: "ATTACK_TYPE_BOOSTER", count: 1, type: PokemonType.GRASS }]); - game.override.moveset([MoveId.LEAFAGE]); + game.override + .startingHeldItems([{ name: "ATTACK_TYPE_BOOSTER", count: 1, type: PokemonType.GRASS }]) + .moveset([MoveId.LEAFAGE]); const powerSpy = vi.spyOn(allMoves[MoveId.LEAFAGE], "calculateBattlePower"); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); @@ -58,8 +59,9 @@ describe("Abilities - Normalize", () => { }); it("should apply silk scarf's power boost after changing a move's type", async () => { - game.override.startingHeldItems([{ name: "ATTACK_TYPE_BOOSTER", count: 1, type: PokemonType.NORMAL }]); - game.override.moveset([MoveId.LEAFAGE]); + game.override + .startingHeldItems([{ name: "ATTACK_TYPE_BOOSTER", count: 1, type: PokemonType.NORMAL }]) + .moveset([MoveId.LEAFAGE]); const powerSpy = vi.spyOn(allMoves[MoveId.LEAFAGE], "calculateBattlePower"); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); diff --git a/test/abilities/oblivious.test.ts b/test/abilities/oblivious.test.ts index e340d3c867d..dab8394edd0 100644 --- a/test/abilities/oblivious.test.ts +++ b/test/abilities/oblivious.test.ts @@ -26,7 +26,7 @@ describe("Abilities - Oblivious", () => { .moveset([MoveId.SPLASH]) .ability(AbilityId.BALL_FETCH) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH); diff --git a/test/abilities/own_tempo.test.ts b/test/abilities/own_tempo.test.ts index 1d6d8aa76e5..869953b4e93 100644 --- a/test/abilities/own_tempo.test.ts +++ b/test/abilities/own_tempo.test.ts @@ -26,7 +26,7 @@ describe("Abilities - Own Tempo", () => { .moveset([MoveId.SPLASH]) .ability(AbilityId.BALL_FETCH) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH); diff --git a/test/abilities/parental_bond.test.ts b/test/abilities/parental_bond.test.ts index 73efd0f57e2..fd914b86100 100644 --- a/test/abilities/parental_bond.test.ts +++ b/test/abilities/parental_bond.test.ts @@ -26,14 +26,15 @@ describe("Abilities - Parental Bond", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleStyle("single"); - game.override.disableCrits(); - game.override.ability(AbilityId.PARENTAL_BOND); - game.override.enemySpecies(SpeciesId.SNORLAX); - game.override.enemyAbility(AbilityId.FUR_COAT); - game.override.enemyMoveset(MoveId.SPLASH); - game.override.startingLevel(100); - game.override.enemyLevel(100); + game.override + .battleStyle("single") + .criticalHits(false) + .ability(AbilityId.PARENTAL_BOND) + .enemySpecies(SpeciesId.SNORLAX) + .enemyAbility(AbilityId.FUR_COAT) + .enemyMoveset(MoveId.SPLASH) + .startingLevel(100) + .enemyLevel(100); }); it("should add second strike to attack move", async () => { @@ -61,8 +62,7 @@ describe("Abilities - Parental Bond", () => { }); it("should apply secondary effects to both strikes", async () => { - game.override.moveset([MoveId.POWER_UP_PUNCH]); - game.override.enemySpecies(SpeciesId.AMOONGUSS); + game.override.moveset([MoveId.POWER_UP_PUNCH]).enemySpecies(SpeciesId.AMOONGUSS); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); @@ -148,8 +148,7 @@ describe("Abilities - Parental Bond", () => { }); it("should not apply multiplier to counter moves", async () => { - game.override.moveset([MoveId.COUNTER]); - game.override.enemyMoveset([MoveId.TACKLE]); + game.override.moveset([MoveId.COUNTER]).enemyMoveset([MoveId.TACKLE]); await game.classicMode.startBattle([SpeciesId.SHUCKLE]); @@ -167,9 +166,7 @@ describe("Abilities - Parental Bond", () => { }); it("should not apply to multi-target moves", async () => { - game.override.battleStyle("double"); - game.override.moveset([MoveId.EARTHQUAKE]); - game.override.passiveAbility(AbilityId.LEVITATE); + game.override.battleStyle("double").moveset([MoveId.EARTHQUAKE]).passiveAbility(AbilityId.LEVITATE); await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.FEEBAS]); @@ -237,8 +234,7 @@ describe("Abilities - Parental Bond", () => { }); it("Moves boosted by this ability and Multi-Lens should strike 3 times", async () => { - game.override.moveset([MoveId.TACKLE]); - game.override.startingHeldItems([{ name: "MULTI_LENS", count: 1 }]); + game.override.moveset([MoveId.TACKLE]).startingHeldItems([{ name: "MULTI_LENS", count: 1 }]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); @@ -252,8 +248,7 @@ describe("Abilities - Parental Bond", () => { }); it("Seismic Toss boosted by this ability and Multi-Lens should strike 3 times", async () => { - game.override.moveset([MoveId.SEISMIC_TOSS]); - game.override.startingHeldItems([{ name: "MULTI_LENS", count: 1 }]); + game.override.moveset([MoveId.SEISMIC_TOSS]).startingHeldItems([{ name: "MULTI_LENS", count: 1 }]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); @@ -378,8 +373,7 @@ describe("Abilities - Parental Bond", () => { }); it("should not cause user to hit into King's Shield more than once", async () => { - game.override.moveset([MoveId.TACKLE]); - game.override.enemyMoveset([MoveId.KINGS_SHIELD]); + game.override.moveset([MoveId.TACKLE]).enemyMoveset([MoveId.KINGS_SHIELD]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); @@ -393,8 +387,7 @@ describe("Abilities - Parental Bond", () => { }); it("should not cause user to hit into Storm Drain more than once", async () => { - game.override.moveset([MoveId.WATER_GUN]); - game.override.enemyAbility(AbilityId.STORM_DRAIN); + game.override.moveset([MoveId.WATER_GUN]).enemyAbility(AbilityId.STORM_DRAIN); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); diff --git a/test/abilities/perish_body.test.ts b/test/abilities/perish_body.test.ts index 9668bf349a8..4852533816b 100644 --- a/test/abilities/perish_body.test.ts +++ b/test/abilities/perish_body.test.ts @@ -21,19 +21,18 @@ describe("Abilities - Perish Song", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleStyle("single"); - game.override.disableCrits(); - - game.override.enemySpecies(SpeciesId.MAGIKARP); - game.override.enemyAbility(AbilityId.BALL_FETCH); - - game.override.starterSpecies(SpeciesId.CURSOLA); - game.override.ability(AbilityId.PERISH_BODY); - game.override.moveset([MoveId.SPLASH]); + game.override + .battleStyle("single") + .criticalHits(false) + .enemySpecies(SpeciesId.MAGIKARP) + .enemyAbility(AbilityId.BALL_FETCH) + .starterSpecies(SpeciesId.CURSOLA) + .ability(AbilityId.PERISH_BODY) + .moveset(MoveId.SPLASH) + .enemyMoveset(MoveId.AQUA_JET); }); it("should trigger when hit with damaging move", async () => { - game.override.enemyMoveset([MoveId.AQUA_JET]); await game.classicMode.startBattle(); const cursola = game.scene.getPlayerPokemon(); const magikarp = game.scene.getEnemyPokemon(); @@ -46,7 +45,7 @@ describe("Abilities - Perish Song", () => { }); it("should trigger even when fainting", async () => { - game.override.enemyMoveset([MoveId.AQUA_JET]).enemyLevel(100).startingLevel(1); + game.override.enemyLevel(100).startingLevel(1); await game.classicMode.startBattle([SpeciesId.CURSOLA, SpeciesId.FEEBAS]); const magikarp = game.scene.getEnemyPokemon(); @@ -87,9 +86,10 @@ describe("Abilities - Perish Song", () => { }); it("should activate if cursola already has perish song, but not reset its counter", async () => { - game.override.enemyMoveset([MoveId.PERISH_SONG, MoveId.AQUA_JET, MoveId.SPLASH]); - game.override.moveset([MoveId.WHIRLWIND, MoveId.SPLASH]); - game.override.startingWave(5); + game.override + .enemyMoveset([MoveId.PERISH_SONG, MoveId.AQUA_JET, MoveId.SPLASH]) + .moveset([MoveId.WHIRLWIND, MoveId.SPLASH]) + .startingWave(5); await game.classicMode.startBattle([SpeciesId.CURSOLA]); const cursola = game.scene.getPlayerPokemon(); diff --git a/test/abilities/power_construct.test.ts b/test/abilities/power_construct.test.ts index f8e3de802e6..2ea14597ef9 100644 --- a/test/abilities/power_construct.test.ts +++ b/test/abilities/power_construct.test.ts @@ -35,8 +35,7 @@ describe("Abilities - POWER CONSTRUCT", () => { test("check if fainted 50% Power Construct Pokemon switches to base form on arena reset", async () => { const baseForm = 2, completeForm = 4; - game.override.startingWave(4); - game.override.starterForms({ + game.override.startingWave(4).starterForms({ [SpeciesId.ZYGARDE]: completeForm, }); @@ -62,8 +61,7 @@ describe("Abilities - POWER CONSTRUCT", () => { test("check if fainted 10% Power Construct Pokemon switches to base form on arena reset", async () => { const baseForm = 3, completeForm = 5; - game.override.startingWave(4); - game.override.starterForms({ + game.override.startingWave(4).starterForms({ [SpeciesId.ZYGARDE]: completeForm, }); diff --git a/test/abilities/power_spot.test.ts b/test/abilities/power_spot.test.ts index 5e6cbce7742..11f75588386 100644 --- a/test/abilities/power_spot.test.ts +++ b/test/abilities/power_spot.test.ts @@ -26,11 +26,12 @@ describe("Abilities - Power Spot", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleStyle("double"); - game.override.moveset([MoveId.TACKLE, MoveId.BREAKING_SWIPE, MoveId.SPLASH, MoveId.DAZZLING_GLEAM]); - game.override.enemyMoveset(MoveId.SPLASH); - game.override.enemySpecies(SpeciesId.SHUCKLE); - game.override.enemyAbility(AbilityId.BALL_FETCH); + game.override + .battleStyle("double") + .moveset([MoveId.TACKLE, MoveId.BREAKING_SWIPE, MoveId.SPLASH, MoveId.DAZZLING_GLEAM]) + .enemyMoveset(MoveId.SPLASH) + .enemySpecies(SpeciesId.SHUCKLE) + .enemyAbility(AbilityId.BALL_FETCH); }); it("raises the power of allies' special moves by 30%", async () => { diff --git a/test/abilities/protean.test.ts b/test/abilities/protean.test.ts index a0b04fa0be5..09c9addbc35 100644 --- a/test/abilities/protean.test.ts +++ b/test/abilities/protean.test.ts @@ -108,8 +108,7 @@ describe("Abilities - Protean", () => { }); test("ability applies correctly even if the type has changed by another ability", async () => { - game.override.moveset([MoveId.TACKLE]); - game.override.passiveAbility(AbilityId.REFRIGERATE); + game.override.moveset([MoveId.TACKLE]).passiveAbility(AbilityId.REFRIGERATE); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); @@ -156,8 +155,7 @@ describe("Abilities - Protean", () => { }); test("ability applies correctly even if the pokemon's move misses", async () => { - game.override.moveset([MoveId.TACKLE]); - game.override.enemyMoveset(MoveId.SPLASH); + game.override.moveset([MoveId.TACKLE]).enemyMoveset(MoveId.SPLASH); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); @@ -188,8 +186,7 @@ describe("Abilities - Protean", () => { }); test("ability applies correctly even if the pokemon's move fails because of type immunity", async () => { - game.override.moveset([MoveId.TACKLE]); - game.override.enemySpecies(SpeciesId.GASTLY); + game.override.moveset([MoveId.TACKLE]).enemySpecies(SpeciesId.GASTLY); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); @@ -262,8 +259,7 @@ describe("Abilities - Protean", () => { }); test("ability applies correctly even if the pokemon's Trick-or-Treat fails", async () => { - game.override.moveset([MoveId.TRICK_OR_TREAT]); - game.override.enemySpecies(SpeciesId.GASTLY); + game.override.moveset([MoveId.TRICK_OR_TREAT]).enemySpecies(SpeciesId.GASTLY); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); diff --git a/test/abilities/protosynthesis.test.ts b/test/abilities/protosynthesis.test.ts index ba7bb5d084b..ef860fc0cd5 100644 --- a/test/abilities/protosynthesis.test.ts +++ b/test/abilities/protosynthesis.test.ts @@ -28,7 +28,7 @@ describe("Abilities - Protosynthesis", () => { .moveset([MoveId.SPLASH, MoveId.TACKLE]) .ability(AbilityId.PROTOSYNTHESIS) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH); diff --git a/test/abilities/quick_draw.test.ts b/test/abilities/quick_draw.test.ts index 70b8637aa37..11418f31375 100644 --- a/test/abilities/quick_draw.test.ts +++ b/test/abilities/quick_draw.test.ts @@ -1,4 +1,3 @@ -import { BypassSpeedChanceAbAttr } from "#app/data/abilities/ability"; import { allAbilities } from "#app/data/data-lists"; import { FaintPhase } from "#app/phases/faint-phase"; import { AbilityId } from "#enums/ability-id"; @@ -24,20 +23,21 @@ describe("Abilities - Quick Draw", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleStyle("single"); + game.override + .battleStyle("single") + .starterSpecies(SpeciesId.MAGIKARP) + .ability(AbilityId.QUICK_DRAW) + .moveset([MoveId.TACKLE, MoveId.TAIL_WHIP]) + .enemyLevel(100) + .enemySpecies(SpeciesId.MAGIKARP) + .enemyAbility(AbilityId.BALL_FETCH) + .enemyMoveset([MoveId.TACKLE]); - game.override.starterSpecies(SpeciesId.MAGIKARP); - game.override.ability(AbilityId.QUICK_DRAW); - game.override.moveset([MoveId.TACKLE, MoveId.TAIL_WHIP]); - - game.override.enemyLevel(100); - game.override.enemySpecies(SpeciesId.MAGIKARP); - game.override.enemyAbility(AbilityId.BALL_FETCH); - game.override.enemyMoveset([MoveId.TACKLE]); - - vi.spyOn(allAbilities[AbilityId.QUICK_DRAW].getAttrs(BypassSpeedChanceAbAttr)[0], "chance", "get").mockReturnValue( - 100, - ); + vi.spyOn( + allAbilities[AbilityId.QUICK_DRAW].getAttrs("BypassSpeedChanceAbAttr")[0], + "chance", + "get", + ).mockReturnValue(100); }); test("makes pokemon going first in its priority bracket", async () => { @@ -54,8 +54,8 @@ describe("Abilities - Quick Draw", () => { expect(pokemon.isFainted()).toBe(false); expect(enemy.isFainted()).toBe(true); - expect(pokemon.waveData.abilitiesApplied).contain(AbilityId.QUICK_DRAW); - }, 20000); + expect(pokemon.waveData.abilitiesApplied).toContain(AbilityId.QUICK_DRAW); + }); test( "does not triggered by non damage moves", @@ -96,6 +96,6 @@ describe("Abilities - Quick Draw", () => { expect(pokemon.isFainted()).toBe(true); expect(enemy.isFainted()).toBe(false); - expect(pokemon.waveData.abilitiesApplied).contain(AbilityId.QUICK_DRAW); - }, 20000); + expect(pokemon.waveData.abilitiesApplied).toContain(AbilityId.QUICK_DRAW); + }); }); diff --git a/test/abilities/sand_spit.test.ts b/test/abilities/sand_spit.test.ts index 96e6380ae88..2b519745649 100644 --- a/test/abilities/sand_spit.test.ts +++ b/test/abilities/sand_spit.test.ts @@ -22,15 +22,14 @@ describe("Abilities - Sand Spit", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleStyle("single"); - game.override.disableCrits(); - - game.override.enemySpecies(SpeciesId.MAGIKARP); - game.override.enemyAbility(AbilityId.BALL_FETCH); - - game.override.starterSpecies(SpeciesId.SILICOBRA); - game.override.ability(AbilityId.SAND_SPIT); - game.override.moveset([MoveId.SPLASH, MoveId.COIL]); + game.override + .battleStyle("single") + .criticalHits(false) + .enemySpecies(SpeciesId.MAGIKARP) + .enemyAbility(AbilityId.BALL_FETCH) + .starterSpecies(SpeciesId.SILICOBRA) + .ability(AbilityId.SAND_SPIT) + .moveset([MoveId.SPLASH, MoveId.COIL]); }); it("should trigger when hit with damaging move", async () => { @@ -41,7 +40,7 @@ describe("Abilities - Sand Spit", () => { await game.toNextTurn(); expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SANDSTORM); - }, 20000); + }); it("should trigger even when fainting", async () => { game.override.enemyMoveset([MoveId.TACKLE]).enemyLevel(100).startingLevel(1); @@ -62,5 +61,5 @@ describe("Abilities - Sand Spit", () => { await game.toNextTurn(); expect(game.scene.arena.weather?.weatherType).not.toBe(WeatherType.SANDSTORM); - }, 20000); + }); }); diff --git a/test/abilities/sand_veil.test.ts b/test/abilities/sand_veil.test.ts index f4b322dc2e9..35a0a3347ff 100644 --- a/test/abilities/sand_veil.test.ts +++ b/test/abilities/sand_veil.test.ts @@ -1,4 +1,3 @@ -import { StatMultiplierAbAttr } from "#app/data/abilities/ability"; import { allAbilities } from "#app/data/data-lists"; import { CommandPhase } from "#app/phases/command-phase"; import { MoveEffectPhase } from "#app/phases/move-effect-phase"; @@ -46,7 +45,7 @@ describe("Abilities - Sand Veil", () => { vi.spyOn(leadPokemon[0], "getAbility").mockReturnValue(allAbilities[AbilityId.SAND_VEIL]); - const sandVeilAttr = allAbilities[AbilityId.SAND_VEIL].getAttrs(StatMultiplierAbAttr)[0]; + const sandVeilAttr = allAbilities[AbilityId.SAND_VEIL].getAttrs("StatMultiplierAbAttr")[0]; vi.spyOn(sandVeilAttr, "applyStatStage").mockImplementation( (_pokemon, _passive, _simulated, stat, statValue, _args) => { if (stat === Stat.EVA && game.scene.arena.weather?.weatherType === WeatherType.SANDSTORM) { diff --git a/test/abilities/sap_sipper.test.ts b/test/abilities/sap_sipper.test.ts index 16559fb563f..c70730f0711 100644 --- a/test/abilities/sap_sipper.test.ts +++ b/test/abilities/sap_sipper.test.ts @@ -31,7 +31,7 @@ describe("Abilities - Sap Sipper", () => { game = new GameManager(phaserGame); game.override .battleStyle("single") - .disableCrits() + .criticalHits(false) .ability(AbilityId.SAP_SIPPER) .enemySpecies(SpeciesId.RATTATA) .enemyAbility(AbilityId.SAP_SIPPER) diff --git a/test/abilities/schooling.test.ts b/test/abilities/schooling.test.ts index 728e9bb7024..646beafb80f 100644 --- a/test/abilities/schooling.test.ts +++ b/test/abilities/schooling.test.ts @@ -31,8 +31,7 @@ describe("Abilities - SCHOOLING", () => { test("check if fainted pokemon switches to base form on arena reset", async () => { const soloForm = 0, schoolForm = 1; - game.override.startingWave(4); - game.override.starterForms({ + game.override.startingWave(4).starterForms({ [SpeciesId.WISHIWASHI]: schoolForm, }); diff --git a/test/abilities/screen_cleaner.test.ts b/test/abilities/screen_cleaner.test.ts index 619cc08bccf..e896d2e51f9 100644 --- a/test/abilities/screen_cleaner.test.ts +++ b/test/abilities/screen_cleaner.test.ts @@ -24,9 +24,7 @@ describe("Abilities - Screen Cleaner", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleStyle("single"); - game.override.ability(AbilityId.SCREEN_CLEANER); - game.override.enemySpecies(SpeciesId.SHUCKLE); + game.override.battleStyle("single").ability(AbilityId.SCREEN_CLEANER).enemySpecies(SpeciesId.SHUCKLE); }); it("removes Aurora Veil", async () => { diff --git a/test/abilities/seed_sower.test.ts b/test/abilities/seed_sower.test.ts index 761342bfb27..7a898f5bcf9 100644 --- a/test/abilities/seed_sower.test.ts +++ b/test/abilities/seed_sower.test.ts @@ -22,15 +22,14 @@ describe("Abilities - Seed Sower", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleStyle("single"); - game.override.disableCrits(); - - game.override.enemySpecies(SpeciesId.MAGIKARP); - game.override.enemyAbility(AbilityId.BALL_FETCH); - - game.override.starterSpecies(SpeciesId.ARBOLIVA); - game.override.ability(AbilityId.SEED_SOWER); - game.override.moveset([MoveId.SPLASH]); + game.override + .battleStyle("single") + .criticalHits(false) + .enemySpecies(SpeciesId.MAGIKARP) + .enemyAbility(AbilityId.BALL_FETCH) + .starterSpecies(SpeciesId.ARBOLIVA) + .ability(AbilityId.SEED_SOWER) + .moveset(MoveId.SPLASH); }); it("should trigger when hit with damaging move", async () => { diff --git a/test/abilities/serene_grace.test.ts b/test/abilities/serene_grace.test.ts index c5c12fe217e..2acf2981537 100644 --- a/test/abilities/serene_grace.test.ts +++ b/test/abilities/serene_grace.test.ts @@ -24,7 +24,7 @@ describe("Abilities - Serene Grace", () => { beforeEach(() => { game = new GameManager(phaserGame); game.override - .disableCrits() + .criticalHits(false) .battleStyle("single") .ability(AbilityId.SERENE_GRACE) .moveset([MoveId.AIR_SLASH]) diff --git a/test/abilities/sheer_force.test.ts b/test/abilities/sheer_force.test.ts index 3d249b2235f..30ae9aef4a6 100644 --- a/test/abilities/sheer_force.test.ts +++ b/test/abilities/sheer_force.test.ts @@ -29,7 +29,7 @@ describe("Abilities - Sheer Force", () => { .enemySpecies(SpeciesId.ONIX) .enemyAbility(AbilityId.NO_GUARD) .enemyMoveset(MoveId.SPLASH) - .disableCrits(); + .criticalHits(false); }); const SHEER_FORCE_MULT = 1.3; diff --git a/test/abilities/shell-armor.test.ts b/test/abilities/shell-armor.test.ts new file mode 100644 index 00000000000..ea0ed4a95dd --- /dev/null +++ b/test/abilities/shell-armor.test.ts @@ -0,0 +1,69 @@ +import type Move from "#app/data/moves/move"; +import Pokemon from "#app/field/pokemon"; +import { AbilityId } from "#enums/ability-id"; +import { MoveId } from "#enums/move-id"; +import { SpeciesId } from "#enums/species-id"; +import { StatusEffect } from "#enums/status-effect"; +import GameManager from "#test/testUtils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi, type MockInstance } from "vitest"; + +describe("Abilities - Shell Armor", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let critSpy: MockInstance<(source: Pokemon, move: Move, simulated?: boolean) => boolean>; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .ability(AbilityId.SHELL_ARMOR) + .battleStyle("single") + .enemySpecies(SpeciesId.MAGIKARP) + .enemyAbility(AbilityId.BALL_FETCH) + .statusEffect(StatusEffect.POISON); + + critSpy = vi.spyOn(Pokemon.prototype, "getCriticalHitResult"); + }); + + it("should prevent natural crit rolls from suceeding", async () => { + game.override.criticalHits(true); // force random crit rolls to always succeed + await game.classicMode.startBattle([SpeciesId.ABOMASNOW]); + + game.move.use(MoveId.SPLASH); + await game.move.forceEnemyMove(MoveId.TACKLE); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(critSpy).toHaveReturnedWith(false); + }); + + it("should prevent guaranteed-crit moves from critting", async () => { + await game.classicMode.startBattle([SpeciesId.ABOMASNOW]); + + game.move.use(MoveId.SPLASH); + await game.move.forceEnemyMove(MoveId.FLOWER_TRICK); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(critSpy).toHaveReturnedWith(false); + }); + + it("should block Merciless guaranteed crits", async () => { + game.override.enemyAbility(AbilityId.MERCILESS); + await game.classicMode.startBattle([SpeciesId.ABOMASNOW]); + + game.move.use(MoveId.SPLASH); + await game.move.forceEnemyMove(MoveId.TACKLE); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(critSpy).toHaveReturnedWith(false); + }); +}); diff --git a/test/abilities/shield_dust.test.ts b/test/abilities/shield_dust.test.ts index e071cf7a245..6bb63fd16a5 100644 --- a/test/abilities/shield_dust.test.ts +++ b/test/abilities/shield_dust.test.ts @@ -1,10 +1,5 @@ import { BattlerIndex } from "#enums/battler-index"; -import { - applyAbAttrs, - applyPreDefendAbAttrs, - IgnoreMoveEffectsAbAttr, - MoveEffectChanceMultiplierAbAttr, -} from "#app/data/abilities/ability"; +import { applyAbAttrs, applyPreDefendAbAttrs } from "#app/data/abilities/apply-ab-attrs"; import { MoveEffectPhase } from "#app/phases/move-effect-phase"; import { NumberHolder } from "#app/utils/common"; import { AbilityId } from "#enums/ability-id"; @@ -31,12 +26,13 @@ describe("Abilities - Shield Dust", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleStyle("single"); - game.override.enemySpecies(SpeciesId.ONIX); - game.override.enemyAbility(AbilityId.SHIELD_DUST); - game.override.startingLevel(100); - game.override.moveset(MoveId.AIR_SLASH); - game.override.enemyMoveset(MoveId.TACKLE); + game.override + .battleStyle("single") + .enemySpecies(SpeciesId.ONIX) + .enemyAbility(AbilityId.SHIELD_DUST) + .startingLevel(100) + .moveset(MoveId.AIR_SLASH) + .enemyMoveset(MoveId.TACKLE); }); it("Shield Dust", async () => { @@ -57,7 +53,7 @@ describe("Abilities - Shield Dust", () => { const chance = new NumberHolder(move.chance); await applyAbAttrs( - MoveEffectChanceMultiplierAbAttr, + "MoveEffectChanceMultiplierAbAttr", phase.getUserPokemon()!, null, false, @@ -67,7 +63,7 @@ describe("Abilities - Shield Dust", () => { false, ); await applyPreDefendAbAttrs( - IgnoreMoveEffectsAbAttr, + "IgnoreMoveEffectsAbAttr", phase.getFirstTarget()!, phase.getUserPokemon()!, null, diff --git a/test/abilities/shields_down.test.ts b/test/abilities/shields_down.test.ts index 0a7270dff31..7b4803915f1 100644 --- a/test/abilities/shields_down.test.ts +++ b/test/abilities/shields_down.test.ts @@ -26,17 +26,17 @@ describe("Abilities - SHIELDS DOWN", () => { beforeEach(() => { game = new GameManager(phaserGame); const moveToUse = MoveId.SPLASH; - game.override.battleStyle("single"); - game.override.ability(AbilityId.SHIELDS_DOWN); - game.override.moveset([moveToUse]); - game.override.enemyMoveset([MoveId.TACKLE]); + game.override + .battleStyle("single") + .ability(AbilityId.SHIELDS_DOWN) + .moveset([moveToUse]) + .enemyMoveset([MoveId.TACKLE]); }); test("check if fainted pokemon switched to base form on arena reset", async () => { const meteorForm = 0, coreForm = 7; - game.override.startingWave(4); - game.override.starterForms({ + game.override.startingWave(4).starterForms({ [SpeciesId.MINIOR]: coreForm, }); @@ -70,8 +70,7 @@ describe("Abilities - SHIELDS DOWN", () => { }); test("should still ignore non-volatile status moves used by a pokemon with mold breaker", async () => { - game.override.enemyAbility(AbilityId.MOLD_BREAKER); - game.override.enemyMoveset([MoveId.SPORE]); + game.override.enemyAbility(AbilityId.MOLD_BREAKER).enemyMoveset([MoveId.SPORE]); await game.classicMode.startBattle([SpeciesId.MINIOR]); @@ -94,8 +93,7 @@ describe("Abilities - SHIELDS DOWN", () => { }); test("should ignore status moves even through mold breaker", async () => { - game.override.enemyMoveset([MoveId.SPORE]); - game.override.enemyAbility(AbilityId.MOLD_BREAKER); + game.override.enemyMoveset([MoveId.SPORE]).enemyAbility(AbilityId.MOLD_BREAKER); await game.classicMode.startBattle([SpeciesId.MINIOR]); @@ -108,8 +106,9 @@ describe("Abilities - SHIELDS DOWN", () => { // toxic spikes currently does not poison flying types when gravity is in effect test.todo("should become poisoned by toxic spikes when grounded", async () => { - game.override.enemyMoveset([MoveId.GRAVITY, MoveId.TOXIC_SPIKES, MoveId.SPLASH]); - game.override.moveset([MoveId.GRAVITY, MoveId.SPLASH]); + game.override + .enemyMoveset([MoveId.GRAVITY, MoveId.TOXIC_SPIKES, MoveId.SPLASH]) + .moveset([MoveId.GRAVITY, MoveId.SPLASH]); await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MINIOR]); @@ -155,9 +154,7 @@ describe("Abilities - SHIELDS DOWN", () => { // the `NoTransformAbilityAbAttr` attribute is not checked anywhere, so this test cannot pass. test.todo("ditto should not be immune to status after transforming", async () => { - game.override.enemySpecies(SpeciesId.DITTO); - game.override.enemyAbility(AbilityId.IMPOSTER); - game.override.moveset([MoveId.SPLASH, MoveId.SPORE]); + game.override.enemySpecies(SpeciesId.DITTO).enemyAbility(AbilityId.IMPOSTER).moveset([MoveId.SPLASH, MoveId.SPORE]); await game.classicMode.startBattle([SpeciesId.MINIOR]); @@ -169,11 +166,12 @@ describe("Abilities - SHIELDS DOWN", () => { }); test("should not prevent minior from receiving the fainted status effect in trainer battles", async () => { - game.override.enemyMoveset([MoveId.TACKLE]); - game.override.moveset([MoveId.THUNDERBOLT]); - game.override.startingLevel(100); - game.override.startingWave(5); - game.override.enemySpecies(SpeciesId.MINIOR); + game.override + .enemyMoveset([MoveId.TACKLE]) + .moveset([MoveId.THUNDERBOLT]) + .startingLevel(100) + .startingWave(5) + .enemySpecies(SpeciesId.MINIOR); await game.classicMode.startBattle([SpeciesId.REGIELEKI]); const minior = game.scene.getEnemyPokemon()!; diff --git a/test/abilities/simple.test.ts b/test/abilities/simple.test.ts index 9df70848f70..703f8fcffac 100644 --- a/test/abilities/simple.test.ts +++ b/test/abilities/simple.test.ts @@ -36,5 +36,5 @@ describe("Abilities - Simple", () => { const enemyPokemon = game.scene.getEnemyPokemon()!; expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-2); - }, 20000); + }); }); diff --git a/test/abilities/stakeout.test.ts b/test/abilities/stakeout.test.ts index ba4325e0295..b3844bf17ca 100644 --- a/test/abilities/stakeout.test.ts +++ b/test/abilities/stakeout.test.ts @@ -27,7 +27,7 @@ describe("Abilities - Stakeout", () => { .moveset([MoveId.SPLASH, MoveId.SURF]) .ability(AbilityId.STAKEOUT) .battleStyle("single") - .disableCrits() + .criticalHits(false) .startingLevel(100) .enemyLevel(100) .enemySpecies(SpeciesId.SNORLAX) diff --git a/test/abilities/stall.test.ts b/test/abilities/stall.test.ts index df40bed3e90..e8ee23fb972 100644 --- a/test/abilities/stall.test.ts +++ b/test/abilities/stall.test.ts @@ -24,7 +24,7 @@ describe("Abilities - Stall", () => { game = new GameManager(phaserGame); game.override .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.REGIELEKI) .enemyAbility(AbilityId.STALL) .enemyMoveset(MoveId.QUICK_ATTACK) @@ -53,7 +53,7 @@ describe("Abilities - Stall", () => { // The opponent Pokemon (with Stall) goes last despite having higher speed than the player Pokemon. expect(speedOrder).toEqual([enemyIndex, playerIndex]); expect(commandOrder).toEqual([playerIndex, enemyIndex]); - }, 20000); + }); it("Pokemon with Stall will go first if a move that is in a higher priority bracket than the opponent's move is used", async () => { await game.classicMode.startBattle([SpeciesId.SHUCKLE]); @@ -71,7 +71,7 @@ describe("Abilities - Stall", () => { // The player Pokemon goes second because its move is in a lower priority bracket. expect(speedOrder).toEqual([enemyIndex, playerIndex]); expect(commandOrder).toEqual([enemyIndex, playerIndex]); - }, 20000); + }); it("If both Pokemon have stall and use the same move, speed is used to determine who goes first.", async () => { game.override.ability(AbilityId.STALL); @@ -91,5 +91,5 @@ describe("Abilities - Stall", () => { // The player Pokemon (with Stall) goes second because its speed is lower. expect(speedOrder).toEqual([enemyIndex, playerIndex]); expect(commandOrder).toEqual([enemyIndex, playerIndex]); - }, 20000); + }); }); diff --git a/test/abilities/steely_spirit.test.ts b/test/abilities/steely_spirit.test.ts index 481e037fb4b..7a2bd98b6b1 100644 --- a/test/abilities/steely_spirit.test.ts +++ b/test/abilities/steely_spirit.test.ts @@ -28,11 +28,12 @@ describe("Abilities - Steely Spirit", () => { beforeEach(() => { ironHeadPower = allMoves[moveToCheck].power; game = new GameManager(phaserGame); - game.override.battleStyle("double"); - game.override.enemySpecies(SpeciesId.SHUCKLE); - game.override.enemyAbility(AbilityId.BALL_FETCH); - game.override.moveset([MoveId.IRON_HEAD, MoveId.SPLASH]); - game.override.enemyMoveset(MoveId.SPLASH); + game.override + .battleStyle("double") + .enemySpecies(SpeciesId.SHUCKLE) + .enemyAbility(AbilityId.BALL_FETCH) + .moveset([MoveId.IRON_HEAD, MoveId.SPLASH]) + .enemyMoveset(MoveId.SPLASH); vi.spyOn(allMoves[moveToCheck], "calculateBattlePower"); }); diff --git a/test/abilities/storm_drain.test.ts b/test/abilities/storm_drain.test.ts index 3a6c0aba100..8eedf0c7ce8 100644 --- a/test/abilities/storm_drain.test.ts +++ b/test/abilities/storm_drain.test.ts @@ -27,7 +27,7 @@ describe("Abilities - Storm Drain", () => { .moveset([MoveId.SPLASH, MoveId.WATER_GUN]) .ability(AbilityId.BALL_FETCH) .battleStyle("double") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH); diff --git a/test/abilities/sturdy.test.ts b/test/abilities/sturdy.test.ts index a50e2075fea..e7b2c05040d 100644 --- a/test/abilities/sturdy.test.ts +++ b/test/abilities/sturdy.test.ts @@ -24,15 +24,14 @@ describe("Abilities - Sturdy", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleStyle("single"); - - game.override.starterSpecies(SpeciesId.LUCARIO); - game.override.startingLevel(100); - game.override.moveset([MoveId.CLOSE_COMBAT, MoveId.FISSURE]); - - game.override.enemySpecies(SpeciesId.ARON); - game.override.enemyLevel(5); - game.override.enemyAbility(AbilityId.STURDY); + game.override + .battleStyle("single") + .starterSpecies(SpeciesId.LUCARIO) + .startingLevel(100) + .moveset([MoveId.CLOSE_COMBAT, MoveId.FISSURE]) + .enemySpecies(SpeciesId.ARON) + .enemyLevel(5) + .enemyAbility(AbilityId.STURDY); }); test("Sturdy activates when user is at full HP", async () => { diff --git a/test/abilities/super_luck.test.ts b/test/abilities/super_luck.test.ts index e94a4cf21f0..60ed2534fe9 100644 --- a/test/abilities/super_luck.test.ts +++ b/test/abilities/super_luck.test.ts @@ -30,13 +30,13 @@ describe("Abilities - Super Luck", () => { .enemyMoveset(MoveId.SPLASH); }); - it("should increase the crit stage of a user by 1", async () => { + it("should increase the user's crit stage by 1", async () => { await game.classicMode.startBattle([SpeciesId.MAGIKARP]); const enemy = game.scene.getEnemyPokemon()!; - const fn = vi.spyOn(enemy, "getCritStage"); + const critSpy = vi.spyOn(enemy, "getCritStage"); // crit stage is called on enemy + game.move.select(MoveId.TACKLE); await game.phaseInterceptor.to("BerryPhase"); - expect(fn).toHaveReturnedWith(1); - fn.mockRestore(); + expect(critSpy).toHaveReturnedWith(1); }); }); diff --git a/test/abilities/tera_shell.test.ts b/test/abilities/tera_shell.test.ts index 26babca240d..a52a01862ff 100644 --- a/test/abilities/tera_shell.test.ts +++ b/test/abilities/tera_shell.test.ts @@ -93,8 +93,6 @@ describe("Abilities - Tera Shell", () => { await game.phaseInterceptor.to("BerryPhase", false); expect(spy).toHaveLastReturnedWith(1); expect(playerPokemon.hp).toBe(playerPokemon.getMaxHp() - 40); - - spy.mockRestore(); }); it("should change the effectiveness of all strikes of a multi-strike move", async () => { @@ -114,6 +112,5 @@ describe("Abilities - Tera Shell", () => { expect(spy).toHaveLastReturnedWith(0.5); } expect(spy).toHaveReturnedTimes(2); - spy.mockRestore(); }); }); diff --git a/test/abilities/thermal_exchange.test.ts b/test/abilities/thermal_exchange.test.ts index f27e6da1d3b..70af864d413 100644 --- a/test/abilities/thermal_exchange.test.ts +++ b/test/abilities/thermal_exchange.test.ts @@ -26,7 +26,7 @@ describe("Abilities - Thermal Exchange", () => { .moveset([MoveId.SPLASH]) .ability(AbilityId.BALL_FETCH) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH); diff --git a/test/abilities/trace.test.ts b/test/abilities/trace.test.ts index 9bfd2f021c8..f17bec159ab 100644 --- a/test/abilities/trace.test.ts +++ b/test/abilities/trace.test.ts @@ -26,7 +26,7 @@ describe("Abilities - Trace", () => { .moveset([MoveId.SPLASH]) .ability(AbilityId.TRACE) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH); diff --git a/test/abilities/unburden.test.ts b/test/abilities/unburden.test.ts index 9748b6340f0..6e24e10d168 100644 --- a/test/abilities/unburden.test.ts +++ b/test/abilities/unburden.test.ts @@ -1,5 +1,4 @@ import { BattlerIndex } from "#enums/battler-index"; -import { PostItemLostAbAttr } from "#app/data/abilities/ability"; import { StealHeldItemChanceAttr } from "#app/data/moves/move"; import { allMoves } from "#app/data/data-lists"; import type Pokemon from "#app/field/pokemon"; @@ -277,7 +276,7 @@ describe("Abilities - Unburden", () => { const [treecko, purrloin] = game.scene.getPlayerParty(); const initialTreeckoSpeed = treecko.getStat(Stat.SPD); const initialPurrloinSpeed = purrloin.getStat(Stat.SPD); - const unburdenAttr = treecko.getAbilityAttrs(PostItemLostAbAttr)[0]; + const unburdenAttr = treecko.getAbilityAttrs("PostItemLostAbAttr")[0]; vi.spyOn(unburdenAttr, "applyPostItemLost"); // Player uses Baton Pass, which also passes the Baton item diff --git a/test/abilities/unseen_fist.test.ts b/test/abilities/unseen_fist.test.ts index 56408c6cbc3..26de77d4643 100644 --- a/test/abilities/unseen_fist.test.ts +++ b/test/abilities/unseen_fist.test.ts @@ -51,8 +51,7 @@ describe("Abilities - Unseen Fist", () => { await testUnseenFistHitResult(game, MoveId.BULLDOZE, MoveId.WIDE_GUARD, false)); it("should cause a contact move to ignore Protect, but not Substitute", async () => { - game.override.enemyLevel(1); - game.override.moveset([MoveId.TACKLE]); + game.override.enemyLevel(1).moveset([MoveId.TACKLE]); await game.classicMode.startBattle(); diff --git a/test/abilities/victory_star.test.ts b/test/abilities/victory_star.test.ts index a15beac8b2c..4742dd96aa6 100644 --- a/test/abilities/victory_star.test.ts +++ b/test/abilities/victory_star.test.ts @@ -26,7 +26,7 @@ describe("Abilities - Victory Star", () => { game.override .moveset([MoveId.TACKLE, MoveId.SPLASH]) .battleStyle("double") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH); diff --git a/test/abilities/vital_spirit.test.ts b/test/abilities/vital_spirit.test.ts index c32454e9d31..18764f94a6b 100644 --- a/test/abilities/vital_spirit.test.ts +++ b/test/abilities/vital_spirit.test.ts @@ -26,7 +26,7 @@ describe("Abilities - Vital Spirit", () => { .moveset([MoveId.SPLASH]) .ability(AbilityId.BALL_FETCH) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH); diff --git a/test/abilities/volt_absorb.test.ts b/test/abilities/volt_absorb.test.ts index 34dbbbd4783..6bea70ee2a4 100644 --- a/test/abilities/volt_absorb.test.ts +++ b/test/abilities/volt_absorb.test.ts @@ -3,11 +3,11 @@ import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { AbilityId } from "#enums/ability-id"; import { BattlerTagType } from "#enums/battler-tag-type"; import { MoveId } from "#enums/move-id"; -import { SpeciesId } from "#enums/species-id"; import GameManager from "#test/testUtils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { BattlerIndex } from "#enums/battler-index"; +import { SpeciesId } from "#enums/species-id"; // See also: TypeImmunityAbAttr describe("Abilities - Volt Absorb", () => { @@ -26,19 +26,19 @@ describe("Abilities - Volt Absorb", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleStyle("single"); - game.override.disableCrits(); + game.override.battleStyle("single").criticalHits(false); }); it("does not activate when CHARGE is used", async () => { const moveToUse = MoveId.CHARGE; const ability = AbilityId.VOLT_ABSORB; - game.override.moveset([moveToUse]); - game.override.ability(ability); - game.override.enemyMoveset([MoveId.SPLASH, MoveId.NONE, MoveId.NONE, MoveId.NONE]); - game.override.enemySpecies(SpeciesId.DUSKULL); - game.override.enemyAbility(AbilityId.BALL_FETCH); + game.override + .moveset([moveToUse]) + .ability(ability) + .enemyMoveset([MoveId.SPLASH]) + .enemySpecies(SpeciesId.DUSKULL) + .enemyAbility(AbilityId.BALL_FETCH); await game.classicMode.startBattle(); @@ -54,10 +54,11 @@ describe("Abilities - Volt Absorb", () => { }); it("should activate regardless of accuracy checks", async () => { - game.override.moveset(MoveId.THUNDERBOLT); - game.override.enemyMoveset(MoveId.SPLASH); - game.override.enemySpecies(SpeciesId.MAGIKARP); - game.override.enemyAbility(AbilityId.VOLT_ABSORB); + game.override + .moveset(MoveId.THUNDERBOLT) + .enemyMoveset(MoveId.SPLASH) + .enemySpecies(SpeciesId.MAGIKARP) + .enemyAbility(AbilityId.VOLT_ABSORB); await game.classicMode.startBattle(); @@ -74,10 +75,11 @@ describe("Abilities - Volt Absorb", () => { }); it("regardless of accuracy should not trigger on pokemon in semi invulnerable state", async () => { - game.override.moveset(MoveId.THUNDERBOLT); - game.override.enemyMoveset(MoveId.DIVE); - game.override.enemySpecies(SpeciesId.MAGIKARP); - game.override.enemyAbility(AbilityId.VOLT_ABSORB); + game.override + .moveset(MoveId.THUNDERBOLT) + .enemyMoveset(MoveId.DIVE) + .enemySpecies(SpeciesId.MAGIKARP) + .enemyAbility(AbilityId.VOLT_ABSORB); await game.classicMode.startBattle(); diff --git a/test/abilities/wandering_spirit.test.ts b/test/abilities/wandering_spirit.test.ts index 360eedda4c9..950cec6d27c 100644 --- a/test/abilities/wandering_spirit.test.ts +++ b/test/abilities/wandering_spirit.test.ts @@ -26,7 +26,7 @@ describe("Abilities - Wandering Spirit", () => { .moveset([MoveId.SPLASH]) .ability(AbilityId.WANDERING_SPIRIT) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.TACKLE); diff --git a/test/abilities/water_bubble.test.ts b/test/abilities/water_bubble.test.ts index 412c4a25035..455a2e368c4 100644 --- a/test/abilities/water_bubble.test.ts +++ b/test/abilities/water_bubble.test.ts @@ -26,7 +26,7 @@ describe("Abilities - Water Bubble", () => { .moveset([MoveId.SPLASH]) .ability(AbilityId.BALL_FETCH) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH); diff --git a/test/abilities/water_veil.test.ts b/test/abilities/water_veil.test.ts index e67287d250f..2df06ec1a21 100644 --- a/test/abilities/water_veil.test.ts +++ b/test/abilities/water_veil.test.ts @@ -26,7 +26,7 @@ describe("Abilities - Water Veil", () => { .moveset([MoveId.SPLASH]) .ability(AbilityId.BALL_FETCH) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH); diff --git a/test/abilities/wimp_out.test.ts b/test/abilities/wimp_out.test.ts index 05c848a75c0..1db0b80fcd0 100644 --- a/test/abilities/wimp_out.test.ts +++ b/test/abilities/wimp_out.test.ts @@ -39,7 +39,7 @@ describe("Abilities - Wimp Out", () => { .enemyLevel(70) .moveset([MoveId.SPLASH, MoveId.FALSE_SWIPE, MoveId.ENDURE]) .enemyMoveset(MoveId.FALSE_SWIPE) - .disableCrits(); + .criticalHits(false); }); function confirmSwitch(): void { diff --git a/test/abilities/wind_power.test.ts b/test/abilities/wind_power.test.ts index a7b4d525a56..8e657997008 100644 --- a/test/abilities/wind_power.test.ts +++ b/test/abilities/wind_power.test.ts @@ -23,14 +23,15 @@ describe("Abilities - Wind Power", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleStyle("single"); - game.override.enemySpecies(SpeciesId.SHIFTRY); - game.override.enemyAbility(AbilityId.WIND_POWER); - game.override.moveset([MoveId.TAILWIND, MoveId.SPLASH, MoveId.PETAL_BLIZZARD, MoveId.SANDSTORM]); - game.override.enemyMoveset(MoveId.SPLASH); + game.override + .battleStyle("single") + .enemySpecies(SpeciesId.SHIFTRY) + .enemyAbility(AbilityId.WIND_POWER) + .moveset([MoveId.TAILWIND, MoveId.SPLASH, MoveId.PETAL_BLIZZARD, MoveId.SANDSTORM]) + .enemyMoveset(MoveId.SPLASH); }); - it("it becomes charged when hit by wind moves", async () => { + it("becomes charged when hit by wind moves", async () => { await game.classicMode.startBattle([SpeciesId.MAGIKARP]); const shiftry = game.scene.getEnemyPokemon()!; @@ -42,9 +43,8 @@ describe("Abilities - Wind Power", () => { expect(shiftry.getTag(BattlerTagType.CHARGED)).toBeDefined(); }); - it("it becomes charged when Tailwind takes effect on its side", async () => { - game.override.ability(AbilityId.WIND_POWER); - game.override.enemySpecies(SpeciesId.MAGIKARP); + it("becomes charged when Tailwind takes effect on its side", async () => { + game.override.ability(AbilityId.WIND_POWER).enemySpecies(SpeciesId.MAGIKARP); await game.classicMode.startBattle([SpeciesId.SHIFTRY]); const shiftry = game.scene.getPlayerPokemon()!; @@ -58,8 +58,7 @@ describe("Abilities - Wind Power", () => { }); it("does not become charged when Tailwind takes effect on opposing side", async () => { - game.override.enemySpecies(SpeciesId.MAGIKARP); - game.override.ability(AbilityId.WIND_POWER); + game.override.enemySpecies(SpeciesId.MAGIKARP).ability(AbilityId.WIND_POWER); await game.classicMode.startBattle([SpeciesId.SHIFTRY]); const magikarp = game.scene.getEnemyPokemon()!; diff --git a/test/abilities/wonder_skin.test.ts b/test/abilities/wonder_skin.test.ts index 933d3653580..886882ab6fd 100644 --- a/test/abilities/wonder_skin.test.ts +++ b/test/abilities/wonder_skin.test.ts @@ -23,12 +23,13 @@ describe("Abilities - Wonder Skin", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleStyle("single"); - game.override.moveset([MoveId.TACKLE, MoveId.CHARM]); - game.override.ability(AbilityId.BALL_FETCH); - game.override.enemySpecies(SpeciesId.SHUCKLE); - game.override.enemyAbility(AbilityId.WONDER_SKIN); - game.override.enemyMoveset(MoveId.SPLASH); + game.override + .battleStyle("single") + .moveset([MoveId.TACKLE, MoveId.CHARM]) + .ability(AbilityId.BALL_FETCH) + .enemySpecies(SpeciesId.SHUCKLE) + .enemyAbility(AbilityId.WONDER_SKIN) + .enemyMoveset(MoveId.SPLASH); }); it("lowers accuracy of status moves to 50%", async () => { diff --git a/test/abilities/zen_mode.test.ts b/test/abilities/zen_mode.test.ts index 163df7b1853..5df2b3a6bc7 100644 --- a/test/abilities/zen_mode.test.ts +++ b/test/abilities/zen_mode.test.ts @@ -27,7 +27,7 @@ describe("Abilities - ZEN MODE", () => { game = new GameManager(phaserGame); game.override .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyLevel(5) @@ -85,8 +85,7 @@ describe("Abilities - ZEN MODE", () => { }); it("should switch to base form on arena reset", async () => { - game.override.startingWave(4); - game.override.starterForms({ + game.override.startingWave(4).starterForms({ [SpeciesId.DARMANITAN]: zenForm, }); diff --git a/test/abilities/zero_to_hero.test.ts b/test/abilities/zero_to_hero.test.ts index 90e10c89ca1..7d0128a4dbc 100644 --- a/test/abilities/zero_to_hero.test.ts +++ b/test/abilities/zero_to_hero.test.ts @@ -34,8 +34,7 @@ describe("Abilities - ZERO TO HERO", () => { }); it("should swap to base form on arena reset", async () => { - game.override.startingWave(4); - game.override.starterForms({ + game.override.startingWave(4).starterForms({ [SpeciesId.PALAFIN]: heroForm, }); diff --git a/test/achievements/achievement.test.ts b/test/achievements/achievement.test.ts index 0b49c4d23ab..5a74c77fe6a 100644 --- a/test/achievements/achievement.test.ts +++ b/test/achievements/achievement.test.ts @@ -130,12 +130,6 @@ describe("RibbonAchv", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.moveset([]); - game.override.startingLevel(0); - game.override.starterSpecies(0); - game.override.enemyMoveset([]); - game.override.enemySpecies(0); - game.override.startingWave(0); scene = game.scene; }); diff --git a/test/arena/terrain.test.ts b/test/arena/terrain.test.ts index f90e903976b..11c1a8e48b8 100644 --- a/test/arena/terrain.test.ts +++ b/test/arena/terrain.test.ts @@ -39,7 +39,7 @@ describe("Terrain -", () => { game = new GameManager(phaserGame); game.override .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.SNOM) .enemyAbility(AbilityId.STURDY) .ability(ability) @@ -92,7 +92,7 @@ describe("Terrain -", () => { game = new GameManager(phaserGame); game.override .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemyLevel(100) .enemySpecies(SpeciesId.SHUCKLE) .enemyAbility(AbilityId.STURDY) @@ -183,7 +183,7 @@ describe("Terrain -", () => { game = new GameManager(phaserGame); game.override .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemyLevel(100) .enemySpecies(SpeciesId.SHUCKLE) .enemyAbility(AbilityId.ELECTRIC_SURGE) @@ -247,7 +247,7 @@ describe("Terrain -", () => { game = new GameManager(phaserGame); game.override .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemyLevel(100) .enemySpecies(SpeciesId.SHUCKLE) .enemyAbility(AbilityId.MISTY_SURGE) @@ -325,7 +325,7 @@ describe("Terrain -", () => { game = new GameManager(phaserGame); game.override .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemyLevel(100) .enemySpecies(SpeciesId.SHUCKLE) .enemyAbility(AbilityId.PSYCHIC_SURGE) diff --git a/test/arena/weather_fog.test.ts b/test/arena/weather_fog.test.ts index 24c07d26cba..e6984e13bd0 100644 --- a/test/arena/weather_fog.test.ts +++ b/test/arena/weather_fog.test.ts @@ -24,12 +24,14 @@ describe("Weather - Fog", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.weather(WeatherType.FOG).battleStyle("single"); - game.override.moveset([MoveId.TACKLE]); - game.override.ability(AbilityId.BALL_FETCH); - game.override.enemyAbility(AbilityId.BALL_FETCH); - game.override.enemySpecies(SpeciesId.MAGIKARP); - game.override.enemyMoveset([MoveId.SPLASH]); + game.override + .weather(WeatherType.FOG) + .battleStyle("single") + .moveset([MoveId.TACKLE]) + .ability(AbilityId.BALL_FETCH) + .enemyAbility(AbilityId.BALL_FETCH) + .enemySpecies(SpeciesId.MAGIKARP) + .enemyMoveset([MoveId.SPLASH]); }); it("move accuracy is multiplied by 90%", async () => { diff --git a/test/arena/weather_strong_winds.test.ts b/test/arena/weather_strong_winds.test.ts index 8b3c2562e6f..d0d256816eb 100644 --- a/test/arena/weather_strong_winds.test.ts +++ b/test/arena/weather_strong_winds.test.ts @@ -24,11 +24,12 @@ describe("Weather - Strong Winds", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleStyle("single"); - game.override.startingLevel(10); - game.override.enemySpecies(SpeciesId.TAILLOW); - game.override.enemyAbility(AbilityId.DELTA_STREAM); - game.override.moveset([MoveId.THUNDERBOLT, MoveId.ICE_BEAM, MoveId.ROCK_SLIDE]); + game.override + .battleStyle("single") + .startingLevel(10) + .enemySpecies(SpeciesId.TAILLOW) + .enemyAbility(AbilityId.DELTA_STREAM) + .moveset([MoveId.THUNDERBOLT, MoveId.ICE_BEAM, MoveId.ROCK_SLIDE]); }); it("electric type move is not very effective on Rayquaza", async () => { diff --git a/test/battle/ability_swap.test.ts b/test/battle/ability_swap.test.ts index 64446d703ac..3dd92576e3b 100644 --- a/test/battle/ability_swap.test.ts +++ b/test/battle/ability_swap.test.ts @@ -27,7 +27,7 @@ describe("Test Ability Swapping", () => { .moveset([MoveId.SPLASH]) .ability(AbilityId.BALL_FETCH) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH); diff --git a/test/battle/battle-order.test.ts b/test/battle/battle-order.test.ts index c12760a7f30..114592c8c8e 100644 --- a/test/battle/battle-order.test.ts +++ b/test/battle/battle-order.test.ts @@ -24,11 +24,12 @@ describe("Battle order", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleStyle("single"); - game.override.enemySpecies(SpeciesId.MEWTWO); - game.override.enemyAbility(AbilityId.INSOMNIA); - game.override.ability(AbilityId.INSOMNIA); - game.override.moveset([MoveId.TACKLE]); + game.override + .battleStyle("single") + .enemySpecies(SpeciesId.MEWTWO) + .enemyAbility(AbilityId.INSOMNIA) + .ability(AbilityId.INSOMNIA) + .moveset([MoveId.TACKLE]); }); it("opponent faster than player 50 vs 150", async () => { @@ -48,7 +49,7 @@ describe("Battle order", () => { const order = phase.getCommandOrder(); expect(order[0]).toBe(enemyPokemonIndex); expect(order[1]).toBe(playerPokemonIndex); - }, 20000); + }); it("Player faster than opponent 150 vs 50", async () => { await game.classicMode.startBattle([SpeciesId.BULBASAUR]); @@ -67,7 +68,7 @@ describe("Battle order", () => { const order = phase.getCommandOrder(); expect(order[0]).toBe(playerPokemonIndex); expect(order[1]).toBe(enemyPokemonIndex); - }, 20000); + }); it("double - both opponents faster than player 50/50 vs 150/150", async () => { game.override.battleStyle("double"); @@ -91,7 +92,7 @@ describe("Battle order", () => { expect(order.slice(0, 2).includes(enemyIndices[1])).toBe(true); expect(order.slice(2, 4).includes(playerIndices[0])).toBe(true); expect(order.slice(2, 4).includes(playerIndices[1])).toBe(true); - }, 20000); + }); it("double - speed tie except 1 - 100/100 vs 100/150", async () => { game.override.battleStyle("double"); @@ -111,11 +112,10 @@ describe("Battle order", () => { const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase; const order = phase.getCommandOrder(); + // enemy 2 should be first, followed by some other assortment of the other 3 pokemon expect(order[0]).toBe(enemyIndices[1]); - expect(order.slice(1, 4).includes(enemyIndices[0])).toBe(true); - expect(order.slice(1, 4).includes(playerIndices[0])).toBe(true); - expect(order.slice(1, 4).includes(playerIndices[1])).toBe(true); - }, 20000); + expect(order.slice(1, 4)).toEqual(expect.arrayContaining([enemyIndices[0], ...playerIndices])); + }); it("double - speed tie 100/150 vs 100/150", async () => { game.override.battleStyle("double"); @@ -136,9 +136,8 @@ describe("Battle order", () => { const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase; const order = phase.getCommandOrder(); - expect(order.slice(0, 2).includes(playerIndices[1])).toBe(true); - expect(order.slice(0, 2).includes(enemyIndices[1])).toBe(true); - expect(order.slice(2, 4).includes(playerIndices[0])).toBe(true); - expect(order.slice(2, 4).includes(enemyIndices[0])).toBe(true); - }, 20000); + // P2/E2 should be randomly first/second, then P1/E1 randomly 3rd/4th + expect(order.slice(0, 2)).toStrictEqual(expect.arrayContaining([playerIndices[1], enemyIndices[1]])); + expect(order.slice(2, 4)).toStrictEqual(expect.arrayContaining([playerIndices[0], enemyIndices[0]])); + }); }); diff --git a/test/battle/battle.test.ts b/test/battle/battle.test.ts index 3eab06e2f48..bf2c3968aa6 100644 --- a/test/battle/battle.test.ts +++ b/test/battle/battle.test.ts @@ -1,4 +1,4 @@ -import { allSpecies } from "#app/data/pokemon-species"; +import { allSpecies } from "#app/data/data-lists"; import { Stat } from "#enums/stat"; import { getGameMode } from "#app/game-mode"; import { GameModes } from "#enums/game-modes"; @@ -10,13 +10,11 @@ import { EnemyCommandPhase } from "#app/phases/enemy-command-phase"; import { LoginPhase } from "#app/phases/login-phase"; import { NextEncounterPhase } from "#app/phases/next-encounter-phase"; import { SelectGenderPhase } from "#app/phases/select-gender-phase"; -import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; import { SelectStarterPhase } from "#app/phases/select-starter-phase"; import { SummonPhase } from "#app/phases/summon-phase"; import { SwitchPhase } from "#app/phases/switch-phase"; import { TitlePhase } from "#app/phases/title-phase"; import { TurnInitPhase } from "#app/phases/turn-init-phase"; -import { VictoryPhase } from "#app/phases/victory-phase"; import GameManager from "#test/testUtils/gameManager"; import { generateStarter } from "#test/testUtils/gameManagerUtils"; import { UiMode } from "#enums/ui-mode"; @@ -62,7 +60,7 @@ describe("Test Battle Phase", () => { expect(game.scene.ui?.getMode()).toBe(UiMode.TITLE); expect(game.scene.gameData.gender).toBe(PlayerGender.MALE); - }, 20000); + }); it("test phase interceptor with prompt with preparation for a future prompt", async () => { await game.phaseInterceptor.run(LoginPhase); @@ -83,40 +81,34 @@ describe("Test Battle Phase", () => { expect(game.scene.ui?.getMode()).toBe(UiMode.TITLE); expect(game.scene.gameData.gender).toBe(PlayerGender.MALE); - }, 20000); + }); it("newGame one-liner", async () => { await game.classicMode.startBattle(); expect(game.scene.ui?.getMode()).toBe(UiMode.COMMAND); - expect(game.scene.phaseManager.getCurrentPhase()!.constructor.name).toBe(CommandPhase.name); - }, 20000); + expect(game.scene.phaseManager.getCurrentPhase()?.phaseName).toBe("CommandPhase"); + }); it("do attack wave 3 - single battle - regular - OHKO", async () => { - game.override.starterSpecies(SpeciesId.MEWTWO); - game.override.enemySpecies(SpeciesId.RATTATA); - game.override.startingLevel(2000); - game.override.startingWave(3).battleStyle("single"); - game.override.moveset([MoveId.TACKLE]); - game.override.enemyAbility(AbilityId.HYDRATION); - game.override.enemyMoveset([MoveId.TACKLE, MoveId.TACKLE, MoveId.TACKLE, MoveId.TACKLE]); - await game.classicMode.startBattle(); - game.move.select(MoveId.TACKLE); - await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(SelectModifierPhase, false); - }, 20000); + game.override.enemySpecies(SpeciesId.RATTATA).startingLevel(2000).battleStyle("single").startingWave(3); + await game.classicMode.startBattle([SpeciesId.MEWTWO]); + game.move.use(MoveId.TACKLE); + await game.phaseInterceptor.to("SelectModifierPhase"); + }); it("do attack wave 3 - single battle - regular - NO OHKO with opponent using non damage attack", async () => { - game.override.starterSpecies(SpeciesId.MEWTWO); - game.override.enemySpecies(SpeciesId.RATTATA); - game.override.startingLevel(5); - game.override.startingWave(3); - game.override.moveset([MoveId.TACKLE]); - game.override.enemyAbility(AbilityId.HYDRATION); - game.override.enemyMoveset([MoveId.TAIL_WHIP, MoveId.TAIL_WHIP, MoveId.TAIL_WHIP, MoveId.TAIL_WHIP]); - game.override.battleStyle("single"); - await game.classicMode.startBattle(); + game.override + .enemySpecies(SpeciesId.RATTATA) + .startingLevel(5) + .startingWave(3) + .moveset([MoveId.TACKLE]) + .enemyAbility(AbilityId.HYDRATION) + .enemyMoveset([MoveId.TAIL_WHIP, MoveId.TAIL_WHIP, MoveId.TAIL_WHIP, MoveId.TAIL_WHIP]) + .battleStyle("single"); + await game.classicMode.startBattle([SpeciesId.MEWTWO]); game.move.select(MoveId.TACKLE); await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(TurnInitPhase, false); - }, 20000); + }); it("load 100% data file", async () => { await game.importData("./test/testUtils/saves/everything.prsv"); @@ -125,14 +117,16 @@ describe("Test Battle Phase", () => { return species.caughtAttr !== 0n; }).length; expect(caughtCount).toBe(Object.keys(allSpecies).length); - }, 20000); + }); it("start battle with selected team", async () => { await game.classicMode.startBattle([SpeciesId.CHARIZARD, SpeciesId.CHANSEY, SpeciesId.MEW]); - expect(game.scene.getPlayerParty()[0].species.speciesId).toBe(SpeciesId.CHARIZARD); - expect(game.scene.getPlayerParty()[1].species.speciesId).toBe(SpeciesId.CHANSEY); - expect(game.scene.getPlayerParty()[2].species.speciesId).toBe(SpeciesId.MEW); - }, 20000); + expect(game.scene.getPlayerParty().map(p => p.species.speciesId)).toEqual([ + SpeciesId.CHARIZARD, + SpeciesId.CHANSEY, + SpeciesId.MEW, + ]); + }); it("test remove random battle seed int", async () => { for (let i = 0; i < 10; i++) { @@ -146,12 +140,12 @@ describe("Test Battle Phase", () => { await game.phaseInterceptor.run(LoginPhase).catch(e => { expect(e).toBe("Wrong phase: this is SelectGenderPhase and not LoginPhase"); }); - }, 20000); + }); it("wrong phase but skip", async () => { await game.phaseInterceptor.run(LoginPhase); await game.phaseInterceptor.run(LoginPhase, () => game.isCurrentPhase(SelectGenderPhase)); - }, 20000); + }); it("good run", async () => { await game.phaseInterceptor.run(LoginPhase); @@ -166,7 +160,7 @@ describe("Test Battle Phase", () => { ); await game.phaseInterceptor.run(SelectGenderPhase, () => game.isCurrentPhase(TitlePhase)); await game.phaseInterceptor.run(TitlePhase); - }, 20000); + }); it("good run from select gender to title", async () => { await game.phaseInterceptor.run(LoginPhase); @@ -180,7 +174,7 @@ describe("Test Battle Phase", () => { () => game.isCurrentPhase(TitlePhase), ); await game.phaseInterceptor.runFrom(SelectGenderPhase).to(TitlePhase); - }, 20000); + }); it("good run to SummonPhase phase", async () => { await game.phaseInterceptor.run(LoginPhase); @@ -201,7 +195,7 @@ describe("Test Battle Phase", () => { selectStarterPhase.initBattle(starters); }); await game.phaseInterceptor.runFrom(SelectGenderPhase).to(SummonPhase); - }, 20000); + }); it("2vs1", async () => { game.override.battleStyle("single"); @@ -247,41 +241,42 @@ describe("Test Battle Phase", () => { it("kill opponent pokemon", async () => { const moveToUse = MoveId.SPLASH; - game.override.battleStyle("single"); - game.override.starterSpecies(SpeciesId.MEWTWO); - game.override.enemySpecies(SpeciesId.RATTATA); - game.override.enemyAbility(AbilityId.HYDRATION); - game.override.ability(AbilityId.ZEN_MODE); - game.override.startingLevel(2000); - game.override.startingWave(3); - game.override.moveset([moveToUse]); - game.override.enemyMoveset([MoveId.TACKLE, MoveId.TACKLE, MoveId.TACKLE, MoveId.TACKLE]); + game.override + .battleStyle("single") + .starterSpecies(SpeciesId.MEWTWO) + .enemySpecies(SpeciesId.RATTATA) + .enemyAbility(AbilityId.HYDRATION) + .ability(AbilityId.ZEN_MODE) + .startingLevel(2000) + .startingWave(3) + .moveset([moveToUse]) + .enemyMoveset([MoveId.TACKLE, MoveId.TACKLE, MoveId.TACKLE, MoveId.TACKLE]); await game.classicMode.startBattle([SpeciesId.DARMANITAN, SpeciesId.CHARIZARD]); game.move.select(moveToUse); await game.phaseInterceptor.to(DamageAnimPhase, false); await game.killPokemon(game.scene.currentBattle.enemyParty[0]); expect(game.scene.currentBattle.enemyParty[0].isFainted()).toBe(true); - await game.phaseInterceptor.to(VictoryPhase, false); - }, 200000); + await game.phaseInterceptor.to("VictoryPhase"); + }); it("to next turn", async () => { const moveToUse = MoveId.SPLASH; - game.override.battleStyle("single"); - game.override.starterSpecies(SpeciesId.MEWTWO); - game.override.enemySpecies(SpeciesId.RATTATA); - game.override.enemyAbility(AbilityId.HYDRATION); - game.override.ability(AbilityId.ZEN_MODE); - game.override.startingLevel(2000); - game.override.startingWave(3); - game.override.moveset([moveToUse]); - game.override.enemyMoveset([MoveId.TACKLE, MoveId.TACKLE, MoveId.TACKLE, MoveId.TACKLE]); - await game.classicMode.startBattle(); + game.override + .battleStyle("single") + .enemySpecies(SpeciesId.RATTATA) + .enemyAbility(AbilityId.HYDRATION) + .ability(AbilityId.ZEN_MODE) + .startingLevel(2000) + .startingWave(3) + .moveset([moveToUse]) + .enemyMoveset([MoveId.TACKLE, MoveId.TACKLE, MoveId.TACKLE, MoveId.TACKLE]); + await game.classicMode.startBattle([SpeciesId.MEWTWO]); const turn = game.scene.currentBattle.turn; game.move.select(moveToUse); await game.toNextTurn(); expect(game.scene.currentBattle.turn).toBeGreaterThan(turn); - }, 20000); + }); it("does not set new weather if staying in same biome", async () => { const moveToUse = MoveId.SPLASH; @@ -294,8 +289,8 @@ describe("Test Battle Phase", () => { .startingLevel(2000) .startingWave(3) .startingBiome(BiomeId.LAKE) - .moveset([moveToUse]); - game.override.enemyMoveset([MoveId.TACKLE, MoveId.TACKLE, MoveId.TACKLE, MoveId.TACKLE]); + .moveset([moveToUse]) + .enemyMoveset([MoveId.TACKLE, MoveId.TACKLE, MoveId.TACKLE, MoveId.TACKLE]); await game.classicMode.startBattle(); const waveIndex = game.scene.currentBattle.waveIndex; game.move.select(moveToUse); @@ -305,7 +300,7 @@ describe("Test Battle Phase", () => { await game.toNextWave(); expect(game.scene.arena.trySetWeather).not.toHaveBeenCalled(); expect(game.scene.currentBattle.waveIndex).toBeGreaterThan(waveIndex); - }, 20000); + }); it("does not force switch if active pokemon faints at same time as enemy mon and is revived in post-battle", async () => { const moveToUse = MoveId.TAKE_DOWN; @@ -336,5 +331,5 @@ describe("Test Battle Phase", () => { () => game.isCurrentPhase(NextEncounterPhase), ); await game.phaseInterceptor.to(SwitchPhase); - }, 20000); + }); }); diff --git a/test/battle/damage_calculation.test.ts b/test/battle/damage_calculation.test.ts index a054ad0f468..19cdf6b9237 100644 --- a/test/battle/damage_calculation.test.ts +++ b/test/battle/damage_calculation.test.ts @@ -1,6 +1,6 @@ import { allMoves } from "#app/data/data-lists"; import type { EnemyPersistentModifier } from "#app/modifier/modifier"; -import { modifierTypes } from "#app/modifier/modifier-type"; +import { modifierTypes } from "#app/data/data-lists"; import { AbilityId } from "#enums/ability-id"; import { ArenaTagType } from "#enums/arena-tag-type"; import { MoveId } from "#enums/move-id"; @@ -32,7 +32,7 @@ describe("Battle Mechanics - Damage Calculation", () => { .enemyMoveset(MoveId.SPLASH) .startingLevel(100) .enemyLevel(100) - .disableCrits() + .criticalHits(false) .moveset([MoveId.TACKLE, MoveId.DRAGON_RAGE, MoveId.FISSURE, MoveId.JUMP_KICK]); }); diff --git a/test/battle/double_battle.test.ts b/test/battle/double_battle.test.ts index 54c573813d7..8e606a99ae0 100644 --- a/test/battle/double_battle.test.ts +++ b/test/battle/double_battle.test.ts @@ -56,7 +56,7 @@ describe("Double Battles", () => { await game.phaseInterceptor.to(TurnInitPhase); expect(game.scene.getPlayerField().filter(p => !p.isFainted())).toHaveLength(2); - }, 20000); + }); it("randomly chooses between single and double battles if there is no battle type override", async () => { let rngSweepProgress = 0; // Will simulate RNG rolls by slowly increasing from 0 to 1 diff --git a/test/battle/special_battle.test.ts b/test/battle/special_battle.test.ts index f0c77668093..4988e526b1e 100644 --- a/test/battle/special_battle.test.ts +++ b/test/battle/special_battle.test.ts @@ -1,4 +1,3 @@ -import { CommandPhase } from "#app/phases/command-phase"; import { UiMode } from "#enums/ui-mode"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; @@ -32,66 +31,67 @@ describe("Test Battle Phase", () => { .enemyMoveset(MoveId.TACKLE); }); + // TODO: Make these into `it.each`es it("startBattle 2vs1 boss", async () => { game.override.battleStyle("single").startingWave(10); await game.classicMode.startBattle([SpeciesId.BLASTOISE, SpeciesId.CHARIZARD]); expect(game.scene.ui?.getMode()).toBe(UiMode.COMMAND); - expect(game.scene.phaseManager.getCurrentPhase()!.constructor.name).toBe(CommandPhase.name); - }, 20000); + expect(game.scene.phaseManager.getCurrentPhase()?.phaseName).toBe("CommandPhase"); + }); it("startBattle 2vs2 boss", async () => { game.override.battleStyle("double").startingWave(10); await game.classicMode.startBattle([SpeciesId.BLASTOISE, SpeciesId.CHARIZARD]); expect(game.scene.ui?.getMode()).toBe(UiMode.COMMAND); - expect(game.scene.phaseManager.getCurrentPhase()!.constructor.name).toBe(CommandPhase.name); - }, 20000); + expect(game.scene.phaseManager.getCurrentPhase()?.phaseName).toBe("CommandPhase"); + }); it("startBattle 2vs2 trainer", async () => { game.override.battleStyle("double").startingWave(5); await game.classicMode.startBattle([SpeciesId.BLASTOISE, SpeciesId.CHARIZARD]); expect(game.scene.ui?.getMode()).toBe(UiMode.COMMAND); - expect(game.scene.phaseManager.getCurrentPhase()!.constructor.name).toBe(CommandPhase.name); - }, 20000); + expect(game.scene.phaseManager.getCurrentPhase()?.phaseName).toBe("CommandPhase"); + }); it("startBattle 2vs1 trainer", async () => { game.override.battleStyle("single").startingWave(5); await game.classicMode.startBattle([SpeciesId.BLASTOISE, SpeciesId.CHARIZARD]); expect(game.scene.ui?.getMode()).toBe(UiMode.COMMAND); - expect(game.scene.phaseManager.getCurrentPhase()!.constructor.name).toBe(CommandPhase.name); - }, 20000); + expect(game.scene.phaseManager.getCurrentPhase()?.phaseName).toBe("CommandPhase"); + }); it("startBattle 2vs1 rival", async () => { game.override.battleStyle("single").startingWave(8); await game.classicMode.startBattle([SpeciesId.BLASTOISE, SpeciesId.CHARIZARD]); expect(game.scene.ui?.getMode()).toBe(UiMode.COMMAND); - expect(game.scene.phaseManager.getCurrentPhase()!.constructor.name).toBe(CommandPhase.name); - }, 20000); + expect(game.scene.phaseManager.getCurrentPhase()?.phaseName).toBe("CommandPhase"); + }); it("startBattle 2vs2 rival", async () => { game.override.battleStyle("double").startingWave(8); await game.classicMode.startBattle([SpeciesId.BLASTOISE, SpeciesId.CHARIZARD]); expect(game.scene.ui?.getMode()).toBe(UiMode.COMMAND); - expect(game.scene.phaseManager.getCurrentPhase()!.constructor.name).toBe(CommandPhase.name); - }, 20000); + expect(game.scene.phaseManager.getCurrentPhase()?.phaseName).toBe("CommandPhase"); + }); it("startBattle 1vs1 trainer", async () => { game.override.battleStyle("single").startingWave(5); await game.classicMode.startBattle([SpeciesId.BLASTOISE]); expect(game.scene.ui?.getMode()).toBe(UiMode.COMMAND); - expect(game.scene.phaseManager.getCurrentPhase()!.constructor.name).toBe(CommandPhase.name); - }, 20000); + expect(game.scene.phaseManager.getCurrentPhase()?.phaseName).toBe("CommandPhase"); + }); it("startBattle 2vs2 trainer", async () => { game.override.battleStyle("double").startingWave(5); await game.classicMode.startBattle([SpeciesId.BLASTOISE, SpeciesId.CHARIZARD]); expect(game.scene.ui?.getMode()).toBe(UiMode.COMMAND); - expect(game.scene.phaseManager.getCurrentPhase()!.constructor.name).toBe(CommandPhase.name); - }, 20000); + expect(game.scene.phaseManager.getCurrentPhase()?.phaseName).toBe("CommandPhase"); + }); it("startBattle 4vs2 trainer", async () => { game.override.battleStyle("double").startingWave(5); await game.classicMode.startBattle([SpeciesId.BLASTOISE, SpeciesId.CHARIZARD, SpeciesId.DARKRAI, SpeciesId.GABITE]); expect(game.scene.ui?.getMode()).toBe(UiMode.COMMAND); - expect(game.scene.phaseManager.getCurrentPhase()!.constructor.name).toBe(CommandPhase.name); - }, 20000); + expect(game.scene.phaseManager.getCurrentPhase()?.phaseName).toBe("CommandPhase"); + }); }); diff --git a/test/boss-pokemon.test.ts b/test/boss-pokemon.test.ts index fdfca9249f7..6eb6d262c56 100644 --- a/test/boss-pokemon.test.ts +++ b/test/boss-pokemon.test.ts @@ -1,7 +1,7 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import GameManager from "#test/testUtils/gameManager"; import { SpeciesId } from "#enums/species-id"; -import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { getPokemonSpecies } from "#app/utils/pokemon-utils"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { EFFECTIVE_STATS } from "#app/enums/stat"; @@ -28,7 +28,7 @@ describe("Boss Pokemon / Shields", () => { game.override .battleStyle("single") .disableTrainerWaves() - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.RATTATA) .enemyMoveset(MoveId.SPLASH) .enemyHeldItems([]) diff --git a/test/data/status_effect.test.ts b/test/data/status_effect.test.ts index 06e11dfeb76..e697ee3f1c7 100644 --- a/test/data/status_effect.test.ts +++ b/test/data/status_effect.test.ts @@ -13,11 +13,12 @@ import { SpeciesId } from "#enums/species-id"; import { StatusEffect } from "#enums/status-effect"; import GameManager from "#test/testUtils/gameManager"; import { mockI18next } from "#test/testUtils/testUtils"; -import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; const pokemonName = "PKM"; const sourceText = "SOURCE"; +// TODO: Make this a giant it.each describe("Status Effect Messages", () => { describe("NONE", () => { const statusEffect = StatusEffect.NONE; @@ -294,10 +295,6 @@ describe("Status Effect Messages", () => { expect(text).toBe("statusEffect:burn.overlap"); }); }); - - afterEach(() => { - vi.resetAllMocks(); - }); }); describe("Status Effects", () => { @@ -359,7 +356,7 @@ describe("Status Effects", () => { .moveset([MoveId.SPLASH]) .ability(AbilityId.BALL_FETCH) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH); @@ -390,7 +387,7 @@ describe("Status Effects", () => { game.move.select(MoveId.SPLASH); await game.toNextTurn(); - expect(player.status?.effect).toBeUndefined(); + expect(player.status).toBeFalsy(); expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS); }); }); @@ -415,7 +412,7 @@ describe("Status Effects", () => { .moveset([MoveId.SPLASH]) .ability(AbilityId.BALL_FETCH) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.NUZZLE) diff --git a/test/eggs/egg.test.ts b/test/eggs/egg.test.ts index e37a7c00aa9..0d5d09c5179 100644 --- a/test/eggs/egg.test.ts +++ b/test/eggs/egg.test.ts @@ -1,6 +1,6 @@ import { speciesEggTiers } from "#app/data/balance/species-egg-tiers"; import { Egg, getLegendaryGachaSpeciesForTimestamp, getValidLegendaryGachaSpecies } from "#app/data/egg"; -import { allSpecies } from "#app/data/pokemon-species"; +import { allSpecies } from "#app/data/data-lists"; import { EggSourceType } from "#app/enums/egg-source-types"; import { EggTier } from "#app/enums/egg-type"; import { VariantTier } from "#app/enums/variant-tier"; @@ -25,7 +25,6 @@ describe("Egg Generation Tests", () => { afterEach(() => { game.phaseInterceptor.restoreOg(); - vi.restoreAllMocks(); }); beforeEach(async () => { diff --git a/test/eggs/manaphy-egg.test.ts b/test/eggs/manaphy-egg.test.ts index 4dc38ef10b6..a58dfa9beba 100644 --- a/test/eggs/manaphy-egg.test.ts +++ b/test/eggs/manaphy-egg.test.ts @@ -21,7 +21,6 @@ describe("Manaphy Eggs", () => { afterEach(() => { game.phaseInterceptor.restoreOg(); - vi.restoreAllMocks(); }); beforeEach(async () => { diff --git a/test/endless_boss.test.ts b/test/endless_boss.test.ts index 6814cf5e12a..d219cacf317 100644 --- a/test/endless_boss.test.ts +++ b/test/endless_boss.test.ts @@ -21,7 +21,7 @@ describe("Endless Boss", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.startingBiome(BiomeId.END).disableCrits(); + game.override.startingBiome(BiomeId.END).criticalHits(false); }); afterEach(() => { diff --git a/test/escape-calculations.test.ts b/test/escape-calculations.test.ts index ca51f40a509..2cc0934f7c1 100644 --- a/test/escape-calculations.test.ts +++ b/test/escape-calculations.test.ts @@ -94,7 +94,7 @@ describe("Escape chance calculations", () => { phase.attemptRunAway(playerPokemon, enemyField, escapePercentage); expect(escapePercentage.value).toBe(escapeChances[i].expectedEscapeChance); } - }, 20000); + }); it("double non-boss opponent", async () => { game.override.battleStyle("double"); @@ -180,7 +180,7 @@ describe("Escape chance calculations", () => { escapeChances[i].pokemonSpeedRatio * totalEnemySpeed, ); } - }, 20000); + }); it("single boss opponent", async () => { game.override.startingWave(10); @@ -259,11 +259,10 @@ describe("Escape chance calculations", () => { phase.attemptRunAway(playerPokemon, enemyField, escapePercentage); expect(escapePercentage.value).toBe(escapeChances[i].expectedEscapeChance); } - }, 20000); + }); it("double boss opponent", async () => { - game.override.battleStyle("double"); - game.override.startingWave(10); + game.override.battleStyle("double").startingWave(10); await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.ABOMASNOW]); const playerPokemon = game.scene.getPlayerField(); @@ -358,5 +357,5 @@ describe("Escape chance calculations", () => { escapeChances[i].pokemonSpeedRatio * totalEnemySpeed, ); } - }, 20000); + }); }); diff --git a/test/evolution.test.ts b/test/evolution.test.ts index b2e7a9243d0..c4bcca785c1 100644 --- a/test/evolution.test.ts +++ b/test/evolution.test.ts @@ -28,12 +28,11 @@ describe("Evolution", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleStyle("single"); - - game.override.enemySpecies(SpeciesId.MAGIKARP); - game.override.enemyAbility(AbilityId.BALL_FETCH); - - game.override.startingLevel(60); + game.override + .battleStyle("single") + .enemySpecies(SpeciesId.MAGIKARP) + .enemyAbility(AbilityId.BALL_FETCH) + .startingLevel(60); }); it("should keep hidden ability after evolving", async () => { diff --git a/test/field/pokemon.test.ts b/test/field/pokemon.test.ts index 74ad973b51d..774d46b18fe 100644 --- a/test/field/pokemon.test.ts +++ b/test/field/pokemon.test.ts @@ -88,9 +88,7 @@ describe("Spec - Pokemon", () => { let scene: BattleScene; beforeEach(async () => { - game.override.enemySpecies(SpeciesId.ZUBAT); - game.override.starterSpecies(SpeciesId.ABRA); - game.override.enableStarterFusion(); + game.override.enemySpecies(SpeciesId.ZUBAT).starterSpecies(SpeciesId.ABRA).enableStarterFusion(); scene = game.scene; }); @@ -146,8 +144,7 @@ describe("Spec - Pokemon", () => { }); it("Fusing mons with one and two types", async () => { - game.override.starterSpecies(SpeciesId.CHARMANDER); - game.override.starterFusionSpecies(SpeciesId.HOUNDOUR); + game.override.starterSpecies(SpeciesId.CHARMANDER).starterFusionSpecies(SpeciesId.HOUNDOUR); await game.classicMode.startBattle(); const pokemon = scene.getPlayerParty()[0]; @@ -157,8 +154,7 @@ describe("Spec - Pokemon", () => { }); it("Fusing mons with two and one types", async () => { - game.override.starterSpecies(SpeciesId.NUMEL); - game.override.starterFusionSpecies(SpeciesId.CHARMANDER); + game.override.starterSpecies(SpeciesId.NUMEL).starterFusionSpecies(SpeciesId.CHARMANDER); await game.classicMode.startBattle(); const pokemon = scene.getPlayerParty()[0]; @@ -168,8 +164,7 @@ describe("Spec - Pokemon", () => { }); it("Fusing two mons with two types", async () => { - game.override.starterSpecies(SpeciesId.NATU); - game.override.starterFusionSpecies(SpeciesId.HOUNDOUR); + game.override.starterSpecies(SpeciesId.NATU).starterFusionSpecies(SpeciesId.HOUNDOUR); await game.classicMode.startBattle(); const pokemon = scene.getPlayerParty()[0]; diff --git a/test/final_boss.test.ts b/test/final_boss.test.ts index d3fcd3214bb..071f83285e7 100644 --- a/test/final_boss.test.ts +++ b/test/final_boss.test.ts @@ -27,7 +27,7 @@ describe("Final Boss", () => { game.override .startingWave(FinalWave.Classic) .startingBiome(BiomeId.END) - .disableCrits() + .criticalHits(false) .enemyMoveset(MoveId.SPLASH) .moveset([MoveId.SPLASH, MoveId.WILL_O_WISP, MoveId.DRAGON_PULSE]) .startingLevel(10000); diff --git a/test/game-mode.test.ts b/test/game-mode.test.ts index 4a53739c45e..c5ce1e02852 100644 --- a/test/game-mode.test.ts +++ b/test/game-mode.test.ts @@ -15,8 +15,6 @@ describe("game-mode", () => { }); afterEach(() => { game.phaseInterceptor.restoreOg(); - vi.clearAllMocks(); - vi.resetAllMocks(); }); beforeEach(() => { game = new GameManager(phaserGame); diff --git a/test/items/dire_hit.test.ts b/test/items/dire_hit.test.ts index f60adebac36..25fe9c8b876 100644 --- a/test/items/dire_hit.test.ts +++ b/test/items/dire_hit.test.ts @@ -37,7 +37,7 @@ describe("Items - Dire Hit", () => { .moveset([MoveId.POUND]) .startingHeldItems([{ name: "DIRE_HIT" }]) .battleStyle("single"); - }, 20000); + }); it("should raise CRIT stage by 1", async () => { await game.classicMode.startBattle([SpeciesId.GASTLY]); @@ -51,7 +51,7 @@ describe("Items - Dire Hit", () => { await game.phaseInterceptor.to(TurnEndPhase); expect(enemyPokemon.getCritStage).toHaveReturnedWith(1); - }, 20000); + }); it("should renew how many battles are left of existing DIRE_HIT when picking up new DIRE_HIT", async () => { game.override.itemRewards([{ name: "DIRE_HIT" }]); @@ -93,5 +93,5 @@ describe("Items - Dire Hit", () => { } } expect(count).toBe(1); - }, 20000); + }); }); diff --git a/test/items/exp_booster.test.ts b/test/items/exp_booster.test.ts index f5273a78a55..44d7721aba2 100644 --- a/test/items/exp_booster.test.ts +++ b/test/items/exp_booster.test.ts @@ -22,9 +22,7 @@ describe("EXP Modifier Items", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.enemyAbility(AbilityId.BALL_FETCH); - game.override.ability(AbilityId.BALL_FETCH); - game.override.battleStyle("single"); + game.override.enemyAbility(AbilityId.BALL_FETCH).ability(AbilityId.BALL_FETCH).battleStyle("single"); }); it("EXP booster items stack multiplicatively", async () => { @@ -36,5 +34,5 @@ describe("EXP Modifier Items", () => { const expHolder = new NumberHolder(partyMember.exp); game.scene.applyModifiers(PokemonExpBoosterModifier, true, partyMember, expHolder); expect(expHolder.value).toBe(440); - }, 20000); + }); }); diff --git a/test/items/leek.test.ts b/test/items/leek.test.ts index 05ee77c991a..eedb6667b9b 100644 --- a/test/items/leek.test.ts +++ b/test/items/leek.test.ts @@ -43,7 +43,7 @@ describe("Items - Leek", () => { await game.phaseInterceptor.to(TurnEndPhase); expect(enemyMember.getCritStage).toHaveReturnedWith(2); - }, 20000); + }); it("should raise CRIT stage by 2 when held by GALAR_FARFETCHD", async () => { await game.classicMode.startBattle([SpeciesId.GALAR_FARFETCHD]); @@ -57,7 +57,7 @@ describe("Items - Leek", () => { await game.phaseInterceptor.to(TurnEndPhase); expect(enemyMember.getCritStage).toHaveReturnedWith(2); - }, 20000); + }); it("should raise CRIT stage by 2 when held by SIRFETCHD", async () => { await game.classicMode.startBattle([SpeciesId.SIRFETCHD]); @@ -71,7 +71,7 @@ describe("Items - Leek", () => { await game.phaseInterceptor.to(TurnEndPhase); expect(enemyMember.getCritStage).toHaveReturnedWith(2); - }, 20000); + }); it("should raise CRIT stage by 2 when held by FARFETCHD line fused with Pokemon", async () => { // Randomly choose from the Farfetch'd line @@ -99,7 +99,7 @@ describe("Items - Leek", () => { await game.phaseInterceptor.to(TurnEndPhase); expect(enemyMember.getCritStage).toHaveReturnedWith(2); - }, 20000); + }); it("should raise CRIT stage by 2 when held by Pokemon fused with FARFETCHD line", async () => { // Randomly choose from the Farfetch'd line @@ -127,7 +127,7 @@ describe("Items - Leek", () => { await game.phaseInterceptor.to(TurnEndPhase); expect(enemyMember.getCritStage).toHaveReturnedWith(2); - }, 20000); + }); it("should not raise CRIT stage when held by a Pokemon outside of FARFETCHD line", async () => { await game.classicMode.startBattle([SpeciesId.PIKACHU]); @@ -141,5 +141,5 @@ describe("Items - Leek", () => { await game.phaseInterceptor.to(TurnEndPhase); expect(enemyMember.getCritStage).toHaveReturnedWith(0); - }, 20000); + }); }); diff --git a/test/items/leftovers.test.ts b/test/items/leftovers.test.ts index 1f42dbcf537..21319d2c9d7 100644 --- a/test/items/leftovers.test.ts +++ b/test/items/leftovers.test.ts @@ -56,5 +56,5 @@ describe("Items - Leftovers", () => { // Check if leftovers heal us await game.phaseInterceptor.to(TurnEndPhase); expect(leadPokemon.hp).toBeGreaterThan(leadHpAfterDamage); - }, 20000); + }); }); diff --git a/test/items/light_ball.test.ts b/test/items/light_ball.test.ts index 84a1689260f..6dfed3389b9 100644 --- a/test/items/light_ball.test.ts +++ b/test/items/light_ball.test.ts @@ -1,6 +1,6 @@ import { Stat } from "#enums/stat"; import { SpeciesStatBoosterModifier } from "#app/modifier/modifier"; -import { modifierTypes } from "#app/modifier/modifier-type"; +import { modifierTypes } from "#app/data/data-lists"; import i18next from "#app/plugins/i18n"; import { NumberHolder } from "#app/utils/common"; import { SpeciesId } from "#enums/species-id"; @@ -108,7 +108,7 @@ describe("Items - Light Ball", () => { expect(atkValue.value / atkStat).toBe(2); expect(spAtkValue.value / spAtkStat).toBe(2); - }, 20000); + }); it("LIGHT_BALL held by fused PIKACHU (base)", async () => { await game.classicMode.startBattle([SpeciesId.PIKACHU, SpeciesId.MAROWAK]); @@ -147,7 +147,7 @@ describe("Items - Light Ball", () => { expect(atkValue.value / atkStat).toBe(2); expect(spAtkValue.value / spAtkStat).toBe(2); - }, 20000); + }); it("LIGHT_BALL held by fused PIKACHU (part)", async () => { await game.classicMode.startBattle([SpeciesId.MAROWAK, SpeciesId.PIKACHU]); @@ -186,7 +186,7 @@ describe("Items - Light Ball", () => { expect(atkValue.value / atkStat).toBe(2); expect(spAtkValue.value / spAtkStat).toBe(2); - }, 20000); + }); it("LIGHT_BALL not held by PIKACHU", async () => { await game.classicMode.startBattle([SpeciesId.MAROWAK]); @@ -215,5 +215,5 @@ describe("Items - Light Ball", () => { expect(atkValue.value / atkStat).toBe(1); expect(spAtkValue.value / spAtkStat).toBe(1); - }, 20000); + }); }); diff --git a/test/items/lock_capsule.test.ts b/test/items/lock_capsule.test.ts index 292031a2bf0..beacc3a3907 100644 --- a/test/items/lock_capsule.test.ts +++ b/test/items/lock_capsule.test.ts @@ -1,6 +1,6 @@ import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; -import { ModifierTier } from "#app/modifier/modifier-tier"; +import { ModifierTier } from "#enums/modifier-tier"; import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; import { UiMode } from "#enums/ui-mode"; import GameManager from "#test/testUtils/gameManager"; @@ -49,5 +49,5 @@ describe("Items - Lock Capsule", () => { game.doSelectModifier(); await game.phaseInterceptor.to("SelectModifierPhase"); - }, 20000); + }); }); diff --git a/test/items/metal_powder.test.ts b/test/items/metal_powder.test.ts index 20b0b90a766..e731f6e7295 100644 --- a/test/items/metal_powder.test.ts +++ b/test/items/metal_powder.test.ts @@ -1,6 +1,6 @@ import { Stat } from "#enums/stat"; import { SpeciesStatBoosterModifier } from "#app/modifier/modifier"; -import { modifierTypes } from "#app/modifier/modifier-type"; +import { modifierTypes } from "#app/data/data-lists"; import i18next from "#app/plugins/i18n"; import { NumberHolder } from "#app/utils/common"; import { SpeciesId } from "#enums/species-id"; @@ -102,7 +102,7 @@ describe("Items - Metal Powder", () => { game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, defValue); expect(defValue.value / defStat).toBe(2); - }, 20000); + }); it("METAL_POWDER held by fused DITTO (base)", async () => { await game.classicMode.startBattle([SpeciesId.DITTO, SpeciesId.MAROWAK]); @@ -135,7 +135,7 @@ describe("Items - Metal Powder", () => { game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, defValue); expect(defValue.value / defStat).toBe(2); - }, 20000); + }); it("METAL_POWDER held by fused DITTO (part)", async () => { await game.classicMode.startBattle([SpeciesId.MAROWAK, SpeciesId.DITTO]); @@ -168,7 +168,7 @@ describe("Items - Metal Powder", () => { game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, defValue); expect(defValue.value / defStat).toBe(2); - }, 20000); + }); it("METAL_POWDER not held by DITTO", async () => { await game.classicMode.startBattle([SpeciesId.MAROWAK]); @@ -191,5 +191,5 @@ describe("Items - Metal Powder", () => { game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, defValue); expect(defValue.value / defStat).toBe(1); - }, 20000); + }); }); diff --git a/test/items/multi_lens.test.ts b/test/items/multi_lens.test.ts index 11e4c04ec21..8a3161970c0 100644 --- a/test/items/multi_lens.test.ts +++ b/test/items/multi_lens.test.ts @@ -28,7 +28,7 @@ describe("Items - Multi Lens", () => { .ability(AbilityId.BALL_FETCH) .startingHeldItems([{ name: "MULTI_LENS" }]) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.SNORLAX) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH) diff --git a/test/items/quick_powder.test.ts b/test/items/quick_powder.test.ts index 0192dec4635..af99f51273d 100644 --- a/test/items/quick_powder.test.ts +++ b/test/items/quick_powder.test.ts @@ -1,6 +1,6 @@ import { Stat } from "#enums/stat"; import { SpeciesStatBoosterModifier } from "#app/modifier/modifier"; -import { modifierTypes } from "#app/modifier/modifier-type"; +import { modifierTypes } from "#app/data/data-lists"; import i18next from "#app/plugins/i18n"; import { NumberHolder } from "#app/utils/common"; import { SpeciesId } from "#enums/species-id"; diff --git a/test/items/reviver_seed.test.ts b/test/items/reviver_seed.test.ts index 54927130869..f444a6eac66 100644 --- a/test/items/reviver_seed.test.ts +++ b/test/items/reviver_seed.test.ts @@ -29,7 +29,7 @@ describe("Items - Reviver Seed", () => { .moveset([MoveId.SPLASH, MoveId.TACKLE, MoveId.ENDURE]) .ability(AbilityId.BALL_FETCH) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .startingHeldItems([{ name: "REVIVER_SEED" }]) diff --git a/test/items/scope_lens.test.ts b/test/items/scope_lens.test.ts index 2ec5260d092..16be8aab930 100644 --- a/test/items/scope_lens.test.ts +++ b/test/items/scope_lens.test.ts @@ -28,7 +28,7 @@ describe("Items - Scope Lens", () => { .moveset([MoveId.POUND]) .startingHeldItems([{ name: "SCOPE_LENS" }]) .battleStyle("single"); - }, 20000); + }); it("should raise CRIT stage by 1", async () => { await game.classicMode.startBattle([SpeciesId.GASTLY]); @@ -42,5 +42,5 @@ describe("Items - Scope Lens", () => { await game.phaseInterceptor.to(TurnEndPhase); expect(enemyPokemon.getCritStage).toHaveReturnedWith(1); - }, 20000); + }); }); diff --git a/test/items/temp_stat_stage_booster.test.ts b/test/items/temp_stat_stage_booster.test.ts index a58c2d611c9..b8cd0cde4eb 100644 --- a/test/items/temp_stat_stage_booster.test.ts +++ b/test/items/temp_stat_stage_booster.test.ts @@ -50,7 +50,7 @@ describe("Items - Temporary Stat Stage Boosters", () => { await game.phaseInterceptor.runFrom("EnemyCommandPhase").to(TurnEndPhase); expect(partyMember.getStatStageMultiplier).toHaveReturnedWith(1.3); - }, 20000); + }); it("should increase existing ACC stat stage by 1 for X_ACCURACY only", async () => { game.override.startingModifier([{ name: "TEMP_STAT_STAGE_BOOSTER", type: Stat.ACC }]).ability(AbilityId.SIMPLE); @@ -72,7 +72,7 @@ describe("Items - Temporary Stat Stage Boosters", () => { // ACC at +3 stat stages yields a x2 multiplier expect(partyMember.getAccuracyMultiplier).toHaveReturnedWith(2); - }, 20000); + }); it("should increase existing stat stage multiplier by 3/10 for the rest of the boosters", async () => { await game.classicMode.startBattle([SpeciesId.PIKACHU]); @@ -92,7 +92,7 @@ describe("Items - Temporary Stat Stage Boosters", () => { // ATK at +1 stat stage yields a x1.5 multiplier, add 0.3 from X_ATTACK expect(partyMember.getStatStageMultiplier).toHaveReturnedWith(1.8); - }, 20000); + }); it("should not increase past maximum stat stage multiplier", async () => { game.override.startingModifier([ @@ -116,7 +116,7 @@ describe("Items - Temporary Stat Stage Boosters", () => { expect(partyMember.getAccuracyMultiplier).toHaveReturnedWith(3); expect(partyMember.getStatStageMultiplier).toHaveReturnedWith(4); - }, 20000); + }); it("should renew how many battles are left of existing booster when picking up new booster of same type", async () => { game.override.startingLevel(200).itemRewards([{ name: "TEMP_STAT_STAGE_BOOSTER", type: Stat.ATK }]); @@ -161,5 +161,5 @@ describe("Items - Temporary Stat Stage Boosters", () => { } } expect(count).toBe(1); - }, 20000); + }); }); diff --git a/test/items/thick_club.test.ts b/test/items/thick_club.test.ts index cff080d0e42..bc019ee99f8 100644 --- a/test/items/thick_club.test.ts +++ b/test/items/thick_club.test.ts @@ -1,6 +1,6 @@ import { Stat } from "#enums/stat"; import { SpeciesStatBoosterModifier } from "#app/modifier/modifier"; -import { modifierTypes } from "#app/modifier/modifier-type"; +import { modifierTypes } from "#app/data/data-lists"; import i18next from "#app/plugins/i18n"; import { NumberHolder, randInt } from "#app/utils/common"; import { SpeciesId } from "#enums/species-id"; diff --git a/test/moves/after_you.test.ts b/test/moves/after_you.test.ts index 78372de3fb6..37186dcc7a5 100644 --- a/test/moves/after_you.test.ts +++ b/test/moves/after_you.test.ts @@ -2,6 +2,7 @@ import { BattlerIndex } from "#enums/battler-index"; import { AbilityId } from "#enums/ability-id"; import { MoveResult } from "#enums/move-result"; import { MovePhase } from "#app/phases/move-phase"; +import { MoveUseMode } from "#enums/move-use-mode"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import GameManager from "#test/testUtils/gameManager"; @@ -60,4 +61,37 @@ describe("Moves - After You", () => { expect(game.scene.getPlayerField()[1].getLastXMoves(1)[0].result).toBe(MoveResult.FAIL); }); + + // TODO: Enable once rampaging moves and move queue are fixed. + // Currently does literally nothing because `MoveUseMode` is overridden from move queue + // within `MovePhase`, but should be enabled once that jank is removed + it.todo("should maintain PP ignore status of rampaging moves", async () => { + game.override.moveset([]); + await game.classicMode.startBattle([SpeciesId.ACCELGOR, SpeciesId.RATTATA]); + + const [accelgor, rattata] = game.scene.getPlayerField(); + expect(accelgor).toBeDefined(); + expect(rattata).toBeDefined(); + + game.move.changeMoveset(accelgor, [MoveId.SPLASH, MoveId.AFTER_YOU]); + game.move.changeMoveset(rattata, MoveId.OUTRAGE); + + game.move.select(MoveId.SPLASH, BattlerIndex.PLAYER); + game.move.select(MoveId.OUTRAGE, BattlerIndex.PLAYER_2); + await game.phaseInterceptor.to("TurnEndPhase"); + + const outrageMove = rattata.getMoveset().find(m => m.moveId === MoveId.OUTRAGE); + expect(outrageMove?.ppUsed).toBe(1); + + game.move.select(MoveId.AFTER_YOU, BattlerIndex.PLAYER, BattlerIndex.PLAYER_2); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(accelgor.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); + expect(outrageMove?.ppUsed).toBe(1); + expect(rattata.getLastXMoves()[0]).toMatchObject({ + move: MoveId.OUTRAGE, + result: MoveResult.SUCCESS, + useMode: MoveUseMode.IGNORE_PP, + }); + }); }); diff --git a/test/moves/alluring_voice.test.ts b/test/moves/alluring_voice.test.ts index 132f83cd4c1..ba096391f1b 100644 --- a/test/moves/alluring_voice.test.ts +++ b/test/moves/alluring_voice.test.ts @@ -26,7 +26,7 @@ describe("Moves - Alluring Voice", () => { game = new GameManager(phaserGame); game.override .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.ICE_SCALES) .enemyMoveset(MoveId.HOWL) diff --git a/test/moves/assist.test.ts b/test/moves/assist.test.ts index bc51f8bd06a..27aa5528f17 100644 --- a/test/moves/assist.test.ts +++ b/test/moves/assist.test.ts @@ -30,7 +30,7 @@ describe("Moves - Assist", () => { game.override .ability(AbilityId.BALL_FETCH) .battleStyle("double") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyLevel(100) .enemyAbility(AbilityId.BALL_FETCH) diff --git a/test/moves/aurora_veil.test.ts b/test/moves/aurora_veil.test.ts index 98b490b0e32..b9ae79e4155 100644 --- a/test/moves/aurora_veil.test.ts +++ b/test/moves/aurora_veil.test.ts @@ -42,7 +42,7 @@ describe("Moves - Aurora Veil", () => { .enemyLevel(100) .enemySpecies(SpeciesId.MAGIKARP) .enemyMoveset(MoveId.AURORA_VEIL) - .disableCrits() + .criticalHits(false) .weather(WeatherType.HAIL); }); diff --git a/test/moves/baneful_bunker.test.ts b/test/moves/baneful_bunker.test.ts index eddfa87ead4..c9cc2025a58 100644 --- a/test/moves/baneful_bunker.test.ts +++ b/test/moves/baneful_bunker.test.ts @@ -24,16 +24,14 @@ describe("Moves - Baneful Bunker", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleStyle("single"); - - game.override.moveset(MoveId.SLASH); - - game.override.enemySpecies(SpeciesId.SNORLAX); - game.override.enemyAbility(AbilityId.INSOMNIA); - game.override.enemyMoveset(MoveId.BANEFUL_BUNKER); - - game.override.startingLevel(100); - game.override.enemyLevel(100); + game.override + .battleStyle("single") + .moveset(MoveId.SLASH) + .enemySpecies(SpeciesId.SNORLAX) + .enemyAbility(AbilityId.INSOMNIA) + .enemyMoveset(MoveId.BANEFUL_BUNKER) + .startingLevel(100) + .enemyLevel(100); }); test("should protect the user and poison attackers that make contact", async () => { await game.classicMode.startBattle([SpeciesId.CHARIZARD]); diff --git a/test/moves/baton_pass.test.ts b/test/moves/baton_pass.test.ts index f6256e95ce8..1b1b0620133 100644 --- a/test/moves/baton_pass.test.ts +++ b/test/moves/baton_pass.test.ts @@ -31,7 +31,7 @@ describe("Moves - Baton Pass", () => { .moveset([MoveId.BATON_PASS, MoveId.NASTY_PLOT, MoveId.SPLASH]) .ability(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH) - .disableCrits(); + .criticalHits(false); }); it("transfers all stat stages when player uses it", async () => { @@ -55,39 +55,34 @@ describe("Moves - Baton Pass", () => { playerPokemon = game.scene.getPlayerPokemon()!; expect(playerPokemon.species.speciesId).toEqual(SpeciesId.SHUCKLE); expect(playerPokemon.getStatStage(Stat.SPATK)).toEqual(2); - }, 20000); + }); it("passes stat stage buffs when AI uses it", async () => { // arrange - game.override.startingWave(5).enemyMoveset(new Array(4).fill([MoveId.NASTY_PLOT])); + game.override.startingWave(5).enemyMoveset([MoveId.NASTY_PLOT, MoveId.BATON_PASS]); await game.classicMode.startBattle([SpeciesId.RAICHU, SpeciesId.SHUCKLE]); // round 1 - ai buffs game.move.select(MoveId.SPLASH); + await game.move.forceEnemyMove(MoveId.NASTY_PLOT); await game.toNextTurn(); // round 2 - baton pass - game.scene.getEnemyPokemon()!.hp = 100; - game.override.enemyMoveset([MoveId.BATON_PASS]); - // Force moveset to update mid-battle - // TODO: replace with enemy ai control function when it's added - game.scene.getEnemyParty()[0].getMoveset(); game.move.select(MoveId.SPLASH); + await game.move.forceEnemyMove(MoveId.BATON_PASS); await game.phaseInterceptor.to("PostSummonPhase", false); - // assert // check buffs are still there - expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.SPATK)).toEqual(2); + expect(game.scene.getEnemyPokemon()?.getStatStage(Stat.SPATK)).toEqual(2); // confirm that a switch actually happened. can't use species because I // can't find a way to override trainer parties with more than 1 pokemon species - expect(game.scene.getEnemyPokemon()!.hp).not.toEqual(100); expect(game.phaseInterceptor.log.slice(-4)).toEqual([ "MoveEffectPhase", "SwitchSummonPhase", "SummonPhase", "PostSummonPhase", ]); - }, 20000); + }); it("doesn't transfer effects that aren't transferrable", async () => { game.override.enemyMoveset([MoveId.SALT_CURE]); @@ -103,7 +98,7 @@ describe("Moves - Baton Pass", () => { await game.toNextTurn(); expect(player2.findTag(t => t.tagType === BattlerTagType.SALT_CURED)).toBeUndefined(); - }, 20000); + }); it("doesn't allow binding effects from the user to persist", async () => { game.override.moveset([MoveId.FIRE_SPIN, MoveId.BATON_PASS]); diff --git a/test/moves/beak_blast.test.ts b/test/moves/beak_blast.test.ts index ad2959e5101..2cb9f9bdd6f 100644 --- a/test/moves/beak_blast.test.ts +++ b/test/moves/beak_blast.test.ts @@ -129,8 +129,7 @@ describe("Moves - Beak Blast", () => { }); it("should not burn a long reach enemy that hits the user with a contact move", async () => { - game.override.enemyAbility(AbilityId.LONG_REACH); - game.override.enemyMoveset([MoveId.FALSE_SWIPE]).enemyLevel(100); + game.override.enemyAbility(AbilityId.LONG_REACH).enemyMoveset([MoveId.FALSE_SWIPE]).enemyLevel(100); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); game.move.select(MoveId.BEAK_BLAST); await game.phaseInterceptor.to("BerryPhase", false); diff --git a/test/moves/beat_up.test.ts b/test/moves/beat_up.test.ts index 184204a91aa..7c4686ab4fa 100644 --- a/test/moves/beat_up.test.ts +++ b/test/moves/beat_up.test.ts @@ -23,15 +23,14 @@ describe("Moves - Beat Up", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleStyle("single"); - - game.override.enemySpecies(SpeciesId.SNORLAX); - game.override.enemyLevel(100); - game.override.enemyMoveset([MoveId.SPLASH]); - game.override.enemyAbility(AbilityId.INSOMNIA); - - game.override.startingLevel(100); - game.override.moveset([MoveId.BEAT_UP]); + game.override + .battleStyle("single") + .enemySpecies(SpeciesId.SNORLAX) + .enemyLevel(100) + .enemyMoveset([MoveId.SPLASH]) + .enemyAbility(AbilityId.INSOMNIA) + .startingLevel(100) + .moveset([MoveId.BEAT_UP]); }); it("should hit once for each healthy player Pokemon", async () => { diff --git a/test/moves/burning_jealousy.test.ts b/test/moves/burning_jealousy.test.ts index 9e1898d9cd1..ce0a1ce57c9 100644 --- a/test/moves/burning_jealousy.test.ts +++ b/test/moves/burning_jealousy.test.ts @@ -26,7 +26,7 @@ describe("Moves - Burning Jealousy", () => { game = new GameManager(phaserGame); game.override .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.ICE_SCALES) .enemyMoveset([MoveId.HOWL]) diff --git a/test/moves/camouflage.test.ts b/test/moves/camouflage.test.ts index 03364265179..6f28493b1e5 100644 --- a/test/moves/camouflage.test.ts +++ b/test/moves/camouflage.test.ts @@ -28,7 +28,7 @@ describe("Moves - Camouflage", () => { .moveset([MoveId.CAMOUFLAGE]) .ability(AbilityId.BALL_FETCH) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.REGIELEKI) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.PSYCHIC_TERRAIN); diff --git a/test/moves/ceaseless_edge.test.ts b/test/moves/ceaseless_edge.test.ts index fcaddfb0d76..1dec98fe3a8 100644 --- a/test/moves/ceaseless_edge.test.ts +++ b/test/moves/ceaseless_edge.test.ts @@ -27,14 +27,15 @@ describe("Moves - Ceaseless Edge", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleStyle("single"); - game.override.enemySpecies(SpeciesId.RATTATA); - game.override.enemyAbility(AbilityId.RUN_AWAY); - game.override.enemyPassiveAbility(AbilityId.RUN_AWAY); - game.override.startingLevel(100); - game.override.enemyLevel(100); - game.override.moveset([MoveId.CEASELESS_EDGE, MoveId.SPLASH, MoveId.ROAR]); - game.override.enemyMoveset(MoveId.SPLASH); + game.override + .battleStyle("single") + .enemySpecies(SpeciesId.RATTATA) + .enemyAbility(AbilityId.RUN_AWAY) + .enemyPassiveAbility(AbilityId.RUN_AWAY) + .startingLevel(100) + .enemyLevel(100) + .moveset([MoveId.CEASELESS_EDGE, MoveId.SPLASH, MoveId.ROAR]) + .enemyMoveset(MoveId.SPLASH); vi.spyOn(allMoves[MoveId.CEASELESS_EDGE], "accuracy", "get").mockReturnValue(100); }); @@ -82,8 +83,7 @@ describe("Moves - Ceaseless Edge", () => { }); test("trainer - move should hit twice, apply two layers of spikes, force switch opponent - opponent takes damage", async () => { - game.override.startingHeldItems([{ name: "MULTI_LENS" }]); - game.override.startingWave(25); + game.override.startingHeldItems([{ name: "MULTI_LENS" }]).startingWave(25); await game.classicMode.startBattle([SpeciesId.ILLUMISE]); diff --git a/test/moves/chloroblast.test.ts b/test/moves/chloroblast.test.ts index cf5e791d0b9..6c790bac150 100644 --- a/test/moves/chloroblast.test.ts +++ b/test/moves/chloroblast.test.ts @@ -25,7 +25,7 @@ describe("Moves - Chloroblast", () => { game.override .ability(AbilityId.BALL_FETCH) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH); }); diff --git a/test/moves/clangorous_soul.test.ts b/test/moves/clangorous_soul.test.ts index 6f4c18852b0..f08503acdc6 100644 --- a/test/moves/clangorous_soul.test.ts +++ b/test/moves/clangorous_soul.test.ts @@ -27,12 +27,13 @@ describe("Moves - Clangorous Soul", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.starterSpecies(SpeciesId.MAGIKARP); - game.override.enemySpecies(SpeciesId.SNORLAX); - game.override.startingLevel(100); - game.override.enemyLevel(100); - game.override.moveset([MoveId.CLANGOROUS_SOUL]); - game.override.enemyMoveset(MoveId.SPLASH); + game.override + .starterSpecies(SpeciesId.MAGIKARP) + .enemySpecies(SpeciesId.SNORLAX) + .startingLevel(100) + .enemyLevel(100) + .moveset([MoveId.CLANGOROUS_SOUL]) + .enemyMoveset(MoveId.SPLASH); }); //Bulbapedia Reference: https://bulbapedia.bulbagarden.net/wiki/Clangorous_Soul_(move) diff --git a/test/moves/copycat.test.ts b/test/moves/copycat.test.ts index 1691cc1478c..8d135d5e2c7 100644 --- a/test/moves/copycat.test.ts +++ b/test/moves/copycat.test.ts @@ -1,9 +1,9 @@ import { BattlerIndex } from "#enums/battler-index"; -import type { RandomMoveAttr } from "#app/data/moves/move"; -import { allMoves } from "#app/data/data-lists"; +import { RandomMoveAttr } from "#app/data/moves/move"; import { Stat } from "#app/enums/stat"; import { MoveResult } from "#enums/move-result"; import { AbilityId } from "#enums/ability-id"; +import { MoveUseMode } from "#enums/move-use-mode"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import GameManager from "#test/testUtils/gameManager"; @@ -14,8 +14,6 @@ describe("Moves - Copycat", () => { let phaserGame: Phaser.Game; let game: GameManager; - let randomMoveAttr: RandomMoveAttr; - beforeAll(() => { phaserGame = new Phaser.Game({ type: Phaser.HEADLESS, @@ -27,14 +25,12 @@ describe("Moves - Copycat", () => { }); beforeEach(() => { - randomMoveAttr = allMoves[MoveId.METRONOME].getAttrs("RandomMoveAttr")[0]; game = new GameManager(phaserGame); game.override .moveset([MoveId.COPYCAT, MoveId.SPIKY_SHIELD, MoveId.SWORDS_DANCE, MoveId.SPLASH]) .ability(AbilityId.BALL_FETCH) .battleStyle("single") - .disableCrits() - .starterSpecies(SpeciesId.FEEBAS) + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH); @@ -42,7 +38,7 @@ describe("Moves - Copycat", () => { it("should copy the last move successfully executed", async () => { game.override.enemyMoveset(MoveId.SUCKER_PUNCH); - await game.classicMode.startBattle(); + await game.classicMode.startBattle([SpeciesId.FEEBAS]); game.move.select(MoveId.SWORDS_DANCE); await game.toNextTurn(); @@ -55,7 +51,7 @@ describe("Moves - Copycat", () => { it("should fail when the last move used is not a valid Copycat move", async () => { game.override.enemyMoveset(MoveId.PROTECT); // Protect is not a valid move for Copycat to copy - await game.classicMode.startBattle(); + await game.classicMode.startBattle([SpeciesId.FEEBAS]); game.move.select(MoveId.SPIKY_SHIELD); // Spiky Shield is not a valid move for Copycat to copy await game.toNextTurn(); @@ -68,19 +64,25 @@ describe("Moves - Copycat", () => { it("should copy the called move when the last move successfully calls another", async () => { game.override.moveset([MoveId.SPLASH, MoveId.METRONOME]).enemyMoveset(MoveId.COPYCAT); - await game.classicMode.startBattle(); - vi.spyOn(randomMoveAttr, "getMoveOverride").mockReturnValue(MoveId.SWORDS_DANCE); + await game.classicMode.startBattle([SpeciesId.DRAMPA]); + vi.spyOn(RandomMoveAttr.prototype, "getMoveOverride").mockReturnValue(MoveId.SWORDS_DANCE); game.move.select(MoveId.METRONOME); - await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); // Player moves first, so enemy can copy Swords Dance + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); // Player moves first so enemy can copy Swords Dance await game.toNextTurn(); - expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.ATK)).toBe(2); + const enemy = game.scene.getEnemyPokemon()!; + expect(enemy.getLastXMoves()[0]).toMatchObject({ + move: MoveId.SWORDS_DANCE, + result: MoveResult.SUCCESS, + useMode: MoveUseMode.FOLLOW_UP, + }); + expect(enemy.getStatStage(Stat.ATK)).toBe(2); }); - it("should apply secondary effects of a move", async () => { + it("should apply move secondary effects", async () => { game.override.enemyMoveset(MoveId.ACID_SPRAY); // Secondary effect lowers SpDef by 2 stages - await game.classicMode.startBattle(); + await game.classicMode.startBattle([SpeciesId.FEEBAS]); game.move.select(MoveId.COPYCAT); await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); diff --git a/test/moves/crafty_shield.test.ts b/test/moves/crafty_shield.test.ts index 01ab0416e5a..12a607a2a1b 100644 --- a/test/moves/crafty_shield.test.ts +++ b/test/moves/crafty_shield.test.ts @@ -26,16 +26,14 @@ describe("Moves - Crafty Shield", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleStyle("double"); - - game.override.moveset([MoveId.CRAFTY_SHIELD, MoveId.SPLASH, MoveId.SWORDS_DANCE]); - - game.override.enemySpecies(SpeciesId.SNORLAX); - game.override.enemyMoveset([MoveId.GROWL]); - game.override.enemyAbility(AbilityId.INSOMNIA); - - game.override.startingLevel(100); - game.override.enemyLevel(100); + game.override + .battleStyle("double") + .moveset([MoveId.CRAFTY_SHIELD, MoveId.SPLASH, MoveId.SWORDS_DANCE]) + .enemySpecies(SpeciesId.SNORLAX) + .enemyMoveset([MoveId.GROWL]) + .enemyAbility(AbilityId.INSOMNIA) + .startingLevel(100) + .enemyLevel(100); }); test("should protect the user and allies from status moves", async () => { @@ -73,8 +71,7 @@ describe("Moves - Crafty Shield", () => { }); test("should protect the user and allies from moves that ignore other protection", async () => { - game.override.enemySpecies(SpeciesId.DUSCLOPS); - game.override.enemyMoveset([MoveId.CURSE]); + game.override.enemySpecies(SpeciesId.DUSCLOPS).enemyMoveset([MoveId.CURSE]); await game.classicMode.startBattle([SpeciesId.CHARIZARD, SpeciesId.BLASTOISE]); diff --git a/test/moves/defog.test.ts b/test/moves/defog.test.ts index d42682c7f3f..17dc0613e9b 100644 --- a/test/moves/defog.test.ts +++ b/test/moves/defog.test.ts @@ -26,7 +26,7 @@ describe("Moves - Defog", () => { .moveset([MoveId.MIST, MoveId.SAFEGUARD, MoveId.SPLASH]) .ability(AbilityId.BALL_FETCH) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.SHUCKLE) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset([MoveId.DEFOG, MoveId.GROWL]); diff --git a/test/moves/dig.test.ts b/test/moves/dig.test.ts index 73540c6ed48..a80ac0405fe 100644 --- a/test/moves/dig.test.ts +++ b/test/moves/dig.test.ts @@ -42,8 +42,8 @@ describe("Moves - Dig", () => { const enemyPokemon = game.scene.getEnemyPokemon()!; game.move.select(MoveId.DIG); - await game.phaseInterceptor.to("TurnEndPhase"); + expect(playerPokemon.getTag(BattlerTagType.UNDERGROUND)).toBeDefined(); expect(enemyPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.MISS); expect(playerPokemon.hp).toBe(playerPokemon.getMaxHp()); @@ -53,9 +53,25 @@ describe("Moves - Dig", () => { await game.phaseInterceptor.to("TurnEndPhase"); expect(playerPokemon.getTag(BattlerTagType.UNDERGROUND)).toBeUndefined(); expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp()); + expect(playerPokemon.getMoveQueue()).toHaveLength(0); expect(playerPokemon.getMoveHistory()).toHaveLength(2); + }); - const playerDig = playerPokemon.getMoveset().find(mv => mv && mv.moveId === MoveId.DIG); + // TODO: Verify this on cartridge double battles + it.todo("should deduct PP only on the 2nd turn of the move", async () => { + game.override.moveset([]); + await game.classicMode.startBattle([SpeciesId.MAGIKARP]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + game.move.changeMoveset(playerPokemon, MoveId.DIG); + + game.move.select(MoveId.DIG); + await game.phaseInterceptor.to("TurnEndPhase"); + + const playerDig = playerPokemon.getMoveset().find(mv => mv?.moveId === MoveId.DIG); + expect(playerDig?.ppUsed).toBe(0); + + await game.phaseInterceptor.to("TurnEndPhase"); expect(playerDig?.ppUsed).toBe(1); }); diff --git a/test/moves/disable.test.ts b/test/moves/disable.test.ts index a269a8177aa..b113acb9525 100644 --- a/test/moves/disable.test.ts +++ b/test/moves/disable.test.ts @@ -1,10 +1,13 @@ import { BattlerIndex } from "#enums/battler-index"; import { MoveResult } from "#enums/move-result"; import { AbilityId } from "#enums/ability-id"; +import { MoveUseMode } from "#enums/move-use-mode"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; +import { Stat } from "#enums/stat"; import GameManager from "#test/testUtils/gameManager"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { RandomMoveAttr } from "#app/data/moves/move"; describe("Moves - Disable", () => { let phaserGame: Phaser.Game; @@ -28,44 +31,49 @@ describe("Moves - Disable", () => { .enemyAbility(AbilityId.BALL_FETCH) .moveset([MoveId.DISABLE, MoveId.SPLASH]) .enemyMoveset(MoveId.SPLASH) - .starterSpecies(SpeciesId.PIKACHU) .enemySpecies(SpeciesId.SHUCKLE); }); - it("restricts moves", async () => { - await game.classicMode.startBattle(); + it("should restrict the last move used", async () => { + await game.classicMode.startBattle([SpeciesId.PIKACHU]); - const enemyMon = game.scene.getEnemyPokemon()!; + const enemyMon = game.field.getEnemyPokemon(); + + game.move.select(MoveId.SPLASH); + await game.move.forceEnemyMove(MoveId.GROWL); + await game.toNextTurn(); game.move.select(MoveId.DISABLE); + await game.move.forceEnemyMove(MoveId.SPLASH); await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.toNextTurn(); - expect(enemyMon.getMoveHistory()).toHaveLength(1); + expect(enemyMon.getLastXMoves(-1)).toHaveLength(2); expect(enemyMon.isMoveRestricted(MoveId.SPLASH)).toBe(true); + expect(enemyMon.isMoveRestricted(MoveId.GROWL)).toBe(false); }); - it("fails if enemy has no move history", async () => { - await game.classicMode.startBattle(); + it("should fail if enemy has no move history", async () => { + await game.classicMode.startBattle([SpeciesId.PIKACHU]); - const playerMon = game.scene.getPlayerPokemon()!; - const enemyMon = game.scene.getEnemyPokemon()!; + const playerMon = game.field.getPlayerPokemon(); + const enemyMon = game.field.getEnemyPokemon(); game.move.select(MoveId.DISABLE); await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.toNextTurn(); - expect(playerMon.getMoveHistory()[0]).toMatchObject({ + expect(playerMon.getLastXMoves()[0]).toMatchObject({ move: MoveId.DISABLE, result: MoveResult.FAIL, }); expect(enemyMon.isMoveRestricted(MoveId.SPLASH)).toBe(false); - }, 20000); + }); it("causes STRUGGLE if all usable moves are disabled", async () => { - await game.classicMode.startBattle(); + await game.classicMode.startBattle([SpeciesId.PIKACHU]); - const enemyMon = game.scene.getEnemyPokemon()!; + const enemyMon = game.field.getEnemyPokemon(); game.move.select(MoveId.DISABLE); await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); @@ -74,81 +82,98 @@ describe("Moves - Disable", () => { game.move.select(MoveId.SPLASH); await game.toNextTurn(); - const enemyHistory = enemyMon.getMoveHistory(); + const enemyHistory = enemyMon.getLastXMoves(-1); expect(enemyHistory).toHaveLength(2); - expect(enemyHistory[0].move).toBe(MoveId.SPLASH); - expect(enemyHistory[1].move).toBe(MoveId.STRUGGLE); - }, 20000); + expect(enemyHistory.map(m => m.move)).toEqual([MoveId.STRUGGLE, MoveId.SPLASH]); + }); - it("cannot disable STRUGGLE", async () => { - game.override.enemyMoveset([MoveId.STRUGGLE]); - await game.classicMode.startBattle(); + it("should fail if it would otherwise disable struggle", async () => { + await game.classicMode.startBattle([SpeciesId.PIKACHU]); - const playerMon = game.scene.getPlayerPokemon()!; - const enemyMon = game.scene.getEnemyPokemon()!; + const playerMon = game.field.getPlayerPokemon(); + const enemyMon = game.field.getEnemyPokemon(); game.move.select(MoveId.DISABLE); + await game.move.forceEnemyMove(MoveId.STRUGGLE); await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.toNextTurn(); expect(playerMon.getLastXMoves()[0].result).toBe(MoveResult.FAIL); expect(enemyMon.getLastXMoves()[0].move).toBe(MoveId.STRUGGLE); expect(enemyMon.isMoveRestricted(MoveId.STRUGGLE)).toBe(false); - }, 20000); + }); - it("interrupts target's move when target moves after", async () => { - await game.classicMode.startBattle(); + it("should interrupt target's move if used first", async () => { + await game.classicMode.startBattle([SpeciesId.PIKACHU]); - const enemyMon = game.scene.getEnemyPokemon()!; + const enemyMon = game.field.getEnemyPokemon(); + // add splash to enemy move history + enemyMon.pushMoveHistory({ + move: MoveId.SPLASH, + targets: [BattlerIndex.ENEMY], + useMode: MoveUseMode.NORMAL, + }); - game.move.select(MoveId.SPLASH); - await game.toNextTurn(); - - // Both mons just used Splash last turn; now have player use Disable. game.move.select(MoveId.DISABLE); await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.toNextTurn(); - const enemyHistory = enemyMon.getMoveHistory(); + const enemyHistory = enemyMon.getLastXMoves(-1); expect(enemyHistory).toHaveLength(2); - expect(enemyHistory[0]).toMatchObject({ - move: MoveId.SPLASH, - result: MoveResult.SUCCESS, - }); - expect(enemyHistory[1].result).toBe(MoveResult.FAIL); - }, 20000); + expect(enemyHistory[0].result).toBe(MoveResult.FAIL); + }); - it("disables NATURE POWER, not the move invoked by it", async () => { - game.override.enemyMoveset([MoveId.NATURE_POWER]); - await game.classicMode.startBattle(); + it.each([ + { name: "Nature Power", moveId: MoveId.NATURE_POWER }, + { name: "Mirror Move", moveId: MoveId.MIRROR_MOVE }, + { name: "Copycat", moveId: MoveId.COPYCAT }, + { name: "Metronome", moveId: MoveId.METRONOME }, + ])("should ignore virtual moves called by $name", async ({ moveId }) => { + vi.spyOn(RandomMoveAttr.prototype, "getMoveOverride").mockReturnValue(MoveId.ABSORB); + await game.classicMode.startBattle([SpeciesId.PIKACHU]); - const enemyMon = game.scene.getEnemyPokemon()!; + const playerMon = game.scene.getPlayerPokemon()!; + playerMon.pushMoveHistory({ move: MoveId.SPLASH, targets: [BattlerIndex.ENEMY], useMode: MoveUseMode.NORMAL }); + game.scene.currentBattle.lastMove = MoveId.SPLASH; game.move.select(MoveId.DISABLE); + await game.move.forceEnemyMove(moveId); await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.toNextTurn(); - expect(enemyMon.isMoveRestricted(MoveId.NATURE_POWER)).toBe(true); - expect(enemyMon.isMoveRestricted(enemyMon.getLastXMoves(2)[0].move)).toBe(false); - }, 20000); - - it("disables most recent move", async () => { - game.override.enemyMoveset([MoveId.SPLASH, MoveId.TACKLE]); - await game.classicMode.startBattle(); - const enemyMon = game.scene.getEnemyPokemon()!; + expect(enemyMon.isMoveRestricted(moveId), `calling move ${MoveId[moveId]} was not disabled`).toBe(true); + expect(enemyMon.getLastXMoves(-1)).toHaveLength(2); + const calledMove = enemyMon.getLastXMoves()[0].move; + expect( + enemyMon.isMoveRestricted(calledMove), + `called move ${MoveId[calledMove]} (from ${MoveId[moveId]}) was incorrectly disabled`, + ).toBe(false); + }); - game.move.select(MoveId.SPLASH); - await game.move.selectEnemyMove(MoveId.SPLASH, BattlerIndex.PLAYER); + it("should ignore dancer copied moves, even if also in moveset", async () => { + game.override + .enemyAbility(AbilityId.DANCER) + .moveset([MoveId.DISABLE, MoveId.SWORDS_DANCE]) + .enemyMoveset([MoveId.SPLASH, MoveId.SWORDS_DANCE]); + await game.classicMode.startBattle([SpeciesId.PIKACHU]); + + game.move.select(MoveId.SWORDS_DANCE); + await game.move.selectEnemyMove(MoveId.SPLASH); await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.toNextTurn(); game.move.select(MoveId.DISABLE); - await game.move.selectEnemyMove(MoveId.TACKLE, BattlerIndex.PLAYER); - await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + await game.move.selectEnemyMove(MoveId.SWORDS_DANCE); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.toNextTurn(); - expect(enemyMon.isMoveRestricted(MoveId.TACKLE)).toBe(true); - expect(enemyMon.isMoveRestricted(MoveId.SPLASH)).toBe(false); - }, 20000); + // Dancer-induced Swords Dance was ignored in favor of splash, + // leaving the subsequent _normal_ swords dance free to work as normal + const shuckle = game.field.getEnemyPokemon(); + expect.soft(shuckle.isMoveRestricted(MoveId.SPLASH)).toBe(true); + expect.soft(shuckle.isMoveRestricted(MoveId.SWORDS_DANCE)).toBe(false); + expect(shuckle.getLastXMoves()[0]).toMatchObject({ move: MoveId.SWORDS_DANCE, result: MoveResult.SUCCESS }); + expect(shuckle.getStatStage(Stat.ATK)).toBe(4); + }); }); diff --git a/test/moves/doodle.test.ts b/test/moves/doodle.test.ts index d4d09b3a195..2dbd5c08c8f 100644 --- a/test/moves/doodle.test.ts +++ b/test/moves/doodle.test.ts @@ -27,7 +27,7 @@ describe("Moves - Doodle", () => { .moveset([MoveId.SPLASH, MoveId.DOODLE]) .ability(AbilityId.ADAPTABILITY) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH); diff --git a/test/moves/double_team.test.ts b/test/moves/double_team.test.ts index 9a17a542f47..d1f6c5be3e4 100644 --- a/test/moves/double_team.test.ts +++ b/test/moves/double_team.test.ts @@ -26,7 +26,7 @@ describe("Moves - Double Team", () => { game.override .battleStyle("single") .moveset([MoveId.DOUBLE_TEAM]) - .disableCrits() + .criticalHits(false) .ability(AbilityId.BALL_FETCH) .enemySpecies(SpeciesId.SHUCKLE) .enemyAbility(AbilityId.BALL_FETCH) diff --git a/test/moves/dragon_rage.test.ts b/test/moves/dragon_rage.test.ts index c2a0a1bc979..c5bed3377fa 100644 --- a/test/moves/dragon_rage.test.ts +++ b/test/moves/dragon_rage.test.ts @@ -1,6 +1,5 @@ import { Stat } from "#enums/stat"; import { PokemonType } from "#enums/pokemon-type"; -import { SpeciesId } from "#enums/species-id"; import type { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { AbilityId } from "#enums/ability-id"; @@ -9,6 +8,7 @@ import { MoveId } from "#enums/move-id"; import GameManager from "#test/testUtils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { SpeciesId } from "#enums/species-id"; describe("Moves - Dragon Rage", () => { let phaserGame: Phaser.Game; @@ -31,19 +31,18 @@ describe("Moves - Dragon Rage", () => { beforeEach(async () => { game = new GameManager(phaserGame); - game.override.battleStyle("single"); - - game.override.starterSpecies(SpeciesId.SNORLAX); - game.override.moveset([MoveId.DRAGON_RAGE]); - game.override.ability(AbilityId.BALL_FETCH); - game.override.passiveAbility(AbilityId.BALL_FETCH); - game.override.startingLevel(100); - - game.override.enemySpecies(SpeciesId.SNORLAX); - game.override.enemyMoveset(MoveId.SPLASH); - game.override.enemyAbility(AbilityId.BALL_FETCH); - game.override.enemyPassiveAbility(AbilityId.BALL_FETCH); - game.override.enemyLevel(100); + game.override + .battleStyle("single") + .starterSpecies(SpeciesId.SNORLAX) + .moveset([MoveId.DRAGON_RAGE]) + .ability(AbilityId.BALL_FETCH) + .passiveAbility(AbilityId.BALL_FETCH) + .startingLevel(100) + .enemySpecies(SpeciesId.SNORLAX) + .enemyMoveset(MoveId.SPLASH) + .enemyAbility(AbilityId.BALL_FETCH) + .enemyPassiveAbility(AbilityId.BALL_FETCH) + .enemyLevel(100); await game.classicMode.startBattle(); @@ -52,7 +51,7 @@ describe("Moves - Dragon Rage", () => { }); it("ignores weaknesses", async () => { - game.override.disableCrits(); + game.override.criticalHits(false); vi.spyOn(enemyPokemon, "getTypes").mockReturnValue([PokemonType.DRAGON]); game.move.select(MoveId.DRAGON_RAGE); @@ -62,7 +61,7 @@ describe("Moves - Dragon Rage", () => { }); it("ignores resistances", async () => { - game.override.disableCrits(); + game.override.criticalHits(false); vi.spyOn(enemyPokemon, "getTypes").mockReturnValue([PokemonType.STEEL]); game.move.select(MoveId.DRAGON_RAGE); @@ -72,7 +71,7 @@ describe("Moves - Dragon Rage", () => { }); it("ignores SPATK stat stages", async () => { - game.override.disableCrits(); + game.override.criticalHits(false); partyPokemon.setStatStage(Stat.SPATK, 2); game.move.select(MoveId.DRAGON_RAGE); @@ -82,7 +81,7 @@ describe("Moves - Dragon Rage", () => { }); it("ignores stab", async () => { - game.override.disableCrits(); + game.override.criticalHits(false); vi.spyOn(partyPokemon, "getTypes").mockReturnValue([PokemonType.DRAGON]); game.move.select(MoveId.DRAGON_RAGE); @@ -101,8 +100,7 @@ describe("Moves - Dragon Rage", () => { }); it("ignores damage modification from abilities, for example ICE_SCALES", async () => { - game.override.disableCrits(); - game.override.enemyAbility(AbilityId.ICE_SCALES); + game.override.criticalHits(false).enemyAbility(AbilityId.ICE_SCALES); game.move.select(MoveId.DRAGON_RAGE); await game.phaseInterceptor.to(TurnEndPhase); diff --git a/test/moves/dynamax_cannon.test.ts b/test/moves/dynamax_cannon.test.ts index 2cdba48c0ea..3febf918de8 100644 --- a/test/moves/dynamax_cannon.test.ts +++ b/test/moves/dynamax_cannon.test.ts @@ -32,20 +32,12 @@ describe("Moves - Dynamax Cannon", () => { game.override .moveset(MoveId.DYNAMAX_CANNON) .startingLevel(200) - .levelCap(10) + .levelCap(100) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyMoveset(MoveId.SPLASH); - // Note that, for Waves 1-10, the level cap is 10 - game.override.startingWave(1); - game.override.battleStyle("single"); - game.override.disableCrits(); - - game.override.enemySpecies(SpeciesId.MAGIKARP); - game.override.enemyMoveset([MoveId.SPLASH, MoveId.SPLASH, MoveId.SPLASH, MoveId.SPLASH]); - vi.spyOn(dynamaxCannon, "calculateBattlePower"); }); @@ -59,10 +51,10 @@ describe("Moves - Dynamax Cannon", () => { expect((game.scene.phaseManager.getCurrentPhase() as MoveEffectPhase).move.id).toBe(dynamaxCannon.id); await game.phaseInterceptor.to(DamageAnimPhase, false); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(100); - }, 20000); + }); it("should return 100 power against an enemy at level cap", async () => { - game.override.enemyLevel(10); + game.override.enemyLevel(100); await game.classicMode.startBattle([SpeciesId.ETERNATUS]); game.move.select(dynamaxCannon.id); @@ -71,7 +63,7 @@ describe("Moves - Dynamax Cannon", () => { expect((game.scene.phaseManager.getCurrentPhase() as MoveEffectPhase).move.id).toBe(dynamaxCannon.id); await game.phaseInterceptor.to(DamageAnimPhase, false); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(100); - }, 20000); + }); it("should return 120 power against an enemy 1% above level cap", async () => { game.override.enemyLevel(101); @@ -86,7 +78,7 @@ describe("Moves - Dynamax Cannon", () => { vi.spyOn(game.scene, "getMaxExpLevel").mockReturnValue(100); await game.phaseInterceptor.to(DamageAnimPhase, false); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(120); - }, 20000); + }); it("should return 140 power against an enemy 2% above level capp", async () => { game.override.enemyLevel(102); @@ -101,7 +93,7 @@ describe("Moves - Dynamax Cannon", () => { vi.spyOn(game.scene, "getMaxExpLevel").mockReturnValue(100); await game.phaseInterceptor.to(DamageAnimPhase, false); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(140); - }, 20000); + }); it("should return 160 power against an enemy 3% above level cap", async () => { game.override.enemyLevel(103); @@ -116,7 +108,7 @@ describe("Moves - Dynamax Cannon", () => { vi.spyOn(game.scene, "getMaxExpLevel").mockReturnValue(100); await game.phaseInterceptor.to(DamageAnimPhase, false); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(160); - }, 20000); + }); it("should return 180 power against an enemy 4% above level cap", async () => { game.override.enemyLevel(104); @@ -131,7 +123,7 @@ describe("Moves - Dynamax Cannon", () => { vi.spyOn(game.scene, "getMaxExpLevel").mockReturnValue(100); await game.phaseInterceptor.to(DamageAnimPhase, false); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(180); - }, 20000); + }); it("should return 200 power against an enemy 5% above level cap", async () => { game.override.enemyLevel(105); @@ -146,7 +138,7 @@ describe("Moves - Dynamax Cannon", () => { vi.spyOn(game.scene, "getMaxExpLevel").mockReturnValue(100); await game.phaseInterceptor.to(DamageAnimPhase, false); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(200); - }, 20000); + }); it("should return 200 power against an enemy way above level cap", async () => { game.override.enemyLevel(999); @@ -159,5 +151,5 @@ describe("Moves - Dynamax Cannon", () => { expect((game.scene.phaseManager.getCurrentPhase() as MoveEffectPhase).move.id).toBe(dynamaxCannon.id); await game.phaseInterceptor.to(DamageAnimPhase, false); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(200); - }, 20000); + }); }); diff --git a/test/moves/effectiveness.test.ts b/test/moves/effectiveness.test.ts index b906e00e1a0..58b2d07b1b6 100644 --- a/test/moves/effectiveness.test.ts +++ b/test/moves/effectiveness.test.ts @@ -1,5 +1,5 @@ import { allMoves } from "#app/data/data-lists"; -import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { getPokemonSpecies } from "#app/utils/pokemon-utils"; import { TrainerSlot } from "#enums/trainer-slot"; import { PokemonType } from "#enums/pokemon-type"; import { AbilityId } from "#enums/ability-id"; diff --git a/test/moves/electro_shot.test.ts b/test/moves/electro_shot.test.ts index 3f751687c59..1d69e9c2fa7 100644 --- a/test/moves/electro_shot.test.ts +++ b/test/moves/electro_shot.test.ts @@ -80,7 +80,7 @@ describe("Moves - Electro Shot", () => { expect(playerPokemon.getTag(BattlerTagType.CHARGING)).toBeUndefined(); expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp()); expect(playerPokemon.getMoveHistory()).toHaveLength(2); - expect(playerPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS); + expect(playerPokemon.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); const playerElectroShot = playerPokemon.getMoveset().find(mv => mv && mv.moveId === MoveId.ELECTRO_SHOT); expect(playerElectroShot?.ppUsed).toBe(1); diff --git a/test/moves/encore.test.ts b/test/moves/encore.test.ts index 120d065d528..772b7c96060 100644 --- a/test/moves/encore.test.ts +++ b/test/moves/encore.test.ts @@ -28,7 +28,7 @@ describe("Moves - Encore", () => { .moveset([MoveId.SPLASH, MoveId.ENCORE]) .ability(AbilityId.BALL_FETCH) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset([MoveId.SPLASH, MoveId.TACKLE]) diff --git a/test/moves/endure.test.ts b/test/moves/endure.test.ts index 799be22ba45..7738f7426fe 100644 --- a/test/moves/endure.test.ts +++ b/test/moves/endure.test.ts @@ -26,7 +26,7 @@ describe("Moves - Endure", () => { .ability(AbilityId.SKILL_LINK) .startingLevel(100) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.NO_GUARD) .enemyMoveset(MoveId.ENDURE); diff --git a/test/moves/entrainment.test.ts b/test/moves/entrainment.test.ts index 0a0cbd3b5f9..65586149492 100644 --- a/test/moves/entrainment.test.ts +++ b/test/moves/entrainment.test.ts @@ -26,7 +26,7 @@ describe("Moves - Entrainment", () => { .moveset([MoveId.SPLASH, MoveId.ENTRAINMENT]) .ability(AbilityId.ADAPTABILITY) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH); diff --git a/test/moves/fairy_lock.test.ts b/test/moves/fairy_lock.test.ts index 74524d67b38..b9f854cdc93 100644 --- a/test/moves/fairy_lock.test.ts +++ b/test/moves/fairy_lock.test.ts @@ -27,7 +27,7 @@ describe("Moves - Fairy Lock", () => { .moveset([MoveId.FAIRY_LOCK, MoveId.SPLASH]) .ability(AbilityId.BALL_FETCH) .battleStyle("double") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset([MoveId.SPLASH, MoveId.U_TURN]); diff --git a/test/moves/fake_out.test.ts b/test/moves/fake_out.test.ts index 95f69fe792e..4fc83e5aa07 100644 --- a/test/moves/fake_out.test.ts +++ b/test/moves/fake_out.test.ts @@ -27,7 +27,7 @@ describe("Moves - Fake Out", () => { .enemyMoveset(MoveId.SPLASH) .enemyLevel(10) .startingLevel(1) // prevent LevelUpPhase from happening - .disableCrits(); + .criticalHits(false); }); it("should only work the first turn a pokemon is sent out in a battle", async () => { diff --git a/test/moves/false_swipe.test.ts b/test/moves/false_swipe.test.ts index c98b76f1ef1..bf9c8307c22 100644 --- a/test/moves/false_swipe.test.ts +++ b/test/moves/false_swipe.test.ts @@ -27,7 +27,7 @@ describe("Moves - False Swipe", () => { .ability(AbilityId.BALL_FETCH) .startingLevel(1000) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH); diff --git a/test/moves/fell_stinger.test.ts b/test/moves/fell_stinger.test.ts index d5081a3ba30..0737db9105f 100644 --- a/test/moves/fell_stinger.test.ts +++ b/test/moves/fell_stinger.test.ts @@ -30,7 +30,7 @@ describe("Moves - Fell Stinger", () => { .battleStyle("single") .moveset([MoveId.FELL_STINGER, MoveId.SALT_CURE, MoveId.BIND, MoveId.LEECH_SEED]) .startingLevel(50) - .disableCrits() + .criticalHits(false) .enemyAbility(AbilityId.STURDY) .enemySpecies(SpeciesId.HYPNO) .enemyMoveset(MoveId.SPLASH) diff --git a/test/moves/fillet_away.test.ts b/test/moves/fillet_away.test.ts index a24b5c4ae04..1e00f2ee02d 100644 --- a/test/moves/fillet_away.test.ts +++ b/test/moves/fillet_away.test.ts @@ -28,12 +28,13 @@ describe("Moves - FILLET AWAY", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.starterSpecies(SpeciesId.MAGIKARP); - game.override.enemySpecies(SpeciesId.SNORLAX); - game.override.startingLevel(100); - game.override.enemyLevel(100); - game.override.moveset([MoveId.FILLET_AWAY]); - game.override.enemyMoveset(MoveId.SPLASH); + game.override + .starterSpecies(SpeciesId.MAGIKARP) + .enemySpecies(SpeciesId.SNORLAX) + .startingLevel(100) + .enemyLevel(100) + .moveset([MoveId.FILLET_AWAY]) + .enemyMoveset(MoveId.SPLASH); }); //Bulbapedia Reference: https://bulbapedia.bulbagarden.net/wiki/fillet_away_(move) diff --git a/test/moves/fissure.test.ts b/test/moves/fissure.test.ts index 53327b505c4..27031a7736d 100644 --- a/test/moves/fissure.test.ts +++ b/test/moves/fissure.test.ts @@ -1,5 +1,4 @@ import { Stat } from "#enums/stat"; -import { SpeciesId } from "#enums/species-id"; import type { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon"; import { DamageAnimPhase } from "#app/phases/damage-anim-phase"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; @@ -8,6 +7,7 @@ import { MoveId } from "#enums/move-id"; import GameManager from "#test/testUtils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { SpeciesId } from "#enums/species-id"; describe("Moves - Fissure", () => { let phaserGame: Phaser.Game; @@ -28,18 +28,17 @@ describe("Moves - Fissure", () => { beforeEach(async () => { game = new GameManager(phaserGame); - game.override.battleStyle("single"); - game.override.disableCrits(); - - game.override.starterSpecies(SpeciesId.SNORLAX); - game.override.moveset([MoveId.FISSURE]); - game.override.passiveAbility(AbilityId.BALL_FETCH); - game.override.startingLevel(100); - - game.override.enemySpecies(SpeciesId.SNORLAX); - game.override.enemyMoveset(MoveId.SPLASH); - game.override.enemyPassiveAbility(AbilityId.BALL_FETCH); - game.override.enemyLevel(100); + game.override + .battleStyle("single") + .criticalHits(false) + .starterSpecies(SpeciesId.SNORLAX) + .moveset(MoveId.FISSURE) + .passiveAbility(AbilityId.BALL_FETCH) + .startingLevel(100) + .enemySpecies(SpeciesId.SNORLAX) + .enemyMoveset(MoveId.SPLASH) + .enemyPassiveAbility(AbilityId.BALL_FETCH) + .enemyLevel(100); await game.classicMode.startBattle(); @@ -48,8 +47,7 @@ describe("Moves - Fissure", () => { }); it("ignores damage modification from abilities, for example FUR_COAT", async () => { - game.override.ability(AbilityId.NO_GUARD); - game.override.enemyAbility(AbilityId.FUR_COAT); + game.override.ability(AbilityId.NO_GUARD).enemyAbility(AbilityId.FUR_COAT); game.move.select(MoveId.FISSURE); await game.phaseInterceptor.to(DamageAnimPhase, true); diff --git a/test/moves/flame_burst.test.ts b/test/moves/flame_burst.test.ts index 1d61daaacb5..0a378df1077 100644 --- a/test/moves/flame_burst.test.ts +++ b/test/moves/flame_burst.test.ts @@ -35,14 +35,15 @@ describe("Moves - Flame Burst", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleStyle("double"); - game.override.moveset([MoveId.FLAME_BURST, MoveId.SPLASH]); - game.override.disableCrits(); - game.override.ability(AbilityId.UNNERVE); - game.override.startingWave(4); - game.override.enemySpecies(SpeciesId.SHUCKLE); - game.override.enemyAbility(AbilityId.BALL_FETCH); - game.override.enemyMoveset([MoveId.SPLASH]); + game.override + .battleStyle("double") + .moveset([MoveId.FLAME_BURST, MoveId.SPLASH]) + .criticalHits(false) + .ability(AbilityId.UNNERVE) + .startingWave(4) + .enemySpecies(SpeciesId.SHUCKLE) + .enemyAbility(AbilityId.BALL_FETCH) + .enemyMoveset(MoveId.SPLASH); }); it("inflicts damage to the target's ally equal to 1/16 of its max HP", async () => { diff --git a/test/moves/flower_shield.test.ts b/test/moves/flower_shield.test.ts index 3561d396ef9..c3c5e5cf870 100644 --- a/test/moves/flower_shield.test.ts +++ b/test/moves/flower_shield.test.ts @@ -26,11 +26,12 @@ describe("Moves - Flower Shield", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.ability(AbilityId.NONE); - game.override.enemyAbility(AbilityId.NONE); - game.override.battleStyle("single"); - game.override.moveset([MoveId.FLOWER_SHIELD, MoveId.SPLASH]); - game.override.enemyMoveset(MoveId.SPLASH); + game.override + .ability(AbilityId.NONE) + .enemyAbility(AbilityId.NONE) + .battleStyle("single") + .moveset([MoveId.FLOWER_SHIELD, MoveId.SPLASH]) + .enemyMoveset(MoveId.SPLASH); }); it("raises DEF stat stage by 1 for all Grass-type Pokemon on the field by one stage - single battle", async () => { diff --git a/test/moves/follow_me.test.ts b/test/moves/follow_me.test.ts index 8279e7b325a..a99ac0b6c00 100644 --- a/test/moves/follow_me.test.ts +++ b/test/moves/follow_me.test.ts @@ -24,14 +24,15 @@ describe("Moves - Follow Me", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleStyle("double"); - game.override.starterSpecies(SpeciesId.AMOONGUSS); - game.override.ability(AbilityId.BALL_FETCH); - game.override.enemySpecies(SpeciesId.SNORLAX); - game.override.startingLevel(100); - game.override.enemyLevel(100); - game.override.moveset([MoveId.FOLLOW_ME, MoveId.RAGE_POWDER, MoveId.SPOTLIGHT, MoveId.QUICK_ATTACK]); - game.override.enemyMoveset([MoveId.TACKLE, MoveId.FOLLOW_ME, MoveId.SPLASH]); + game.override + .battleStyle("double") + .starterSpecies(SpeciesId.AMOONGUSS) + .ability(AbilityId.BALL_FETCH) + .enemySpecies(SpeciesId.SNORLAX) + .startingLevel(100) + .enemyLevel(100) + .moveset([MoveId.FOLLOW_ME, MoveId.RAGE_POWDER, MoveId.SPOTLIGHT, MoveId.QUICK_ATTACK]) + .enemyMoveset([MoveId.TACKLE, MoveId.FOLLOW_ME, MoveId.SPLASH]); }); test("move should redirect enemy attacks to the user", async () => { @@ -73,8 +74,7 @@ describe("Moves - Follow Me", () => { }); test("move effect should be bypassed by Stalwart", async () => { - game.override.ability(AbilityId.STALWART); - game.override.moveset([MoveId.QUICK_ATTACK]); + game.override.ability(AbilityId.STALWART).moveset([MoveId.QUICK_ATTACK]); await game.classicMode.startBattle([SpeciesId.AMOONGUSS, SpeciesId.CHARIZARD]); diff --git a/test/moves/foresight.test.ts b/test/moves/foresight.test.ts index a30b4d6a444..96a341582f9 100644 --- a/test/moves/foresight.test.ts +++ b/test/moves/foresight.test.ts @@ -22,7 +22,7 @@ describe("Moves - Foresight", () => { beforeEach(() => { game = new GameManager(phaserGame); game.override - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.GASTLY) .enemyMoveset(MoveId.SPLASH) .enemyLevel(5) diff --git a/test/moves/forests_curse.test.ts b/test/moves/forests_curse.test.ts index 77fec5d277d..81ced674a33 100644 --- a/test/moves/forests_curse.test.ts +++ b/test/moves/forests_curse.test.ts @@ -26,7 +26,7 @@ describe("Moves - Forest's Curse", () => { .moveset([MoveId.FORESTS_CURSE, MoveId.TRICK_OR_TREAT]) .ability(AbilityId.BALL_FETCH) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH); diff --git a/test/moves/fusion_bolt.test.ts b/test/moves/fusion_bolt.test.ts index a5263bfa364..c92a483a497 100644 --- a/test/moves/fusion_bolt.test.ts +++ b/test/moves/fusion_bolt.test.ts @@ -31,7 +31,7 @@ describe("Moves - Fusion Bolt", () => { .enemyMoveset(MoveId.SPLASH) .battleStyle("single") .startingWave(97) - .disableCrits(); + .criticalHits(false); }); it("should not make contact", async () => { @@ -45,5 +45,5 @@ describe("Moves - Fusion Bolt", () => { await game.toNextTurn(); expect(initialHp - partyMember.hp).toBe(0); - }, 20000); + }); }); diff --git a/test/moves/fusion_flare.test.ts b/test/moves/fusion_flare.test.ts index df6d390686f..c0df347fcce 100644 --- a/test/moves/fusion_flare.test.ts +++ b/test/moves/fusion_flare.test.ts @@ -31,7 +31,7 @@ describe("Moves - Fusion Flare", () => { .enemyMoveset(MoveId.REST) .battleStyle("single") .startingWave(97) - .disableCrits(); + .criticalHits(false); }); it("should thaw freeze status condition", async () => { diff --git a/test/moves/fusion_flare_bolt.test.ts b/test/moves/fusion_flare_bolt.test.ts index f10ede8717c..1967e9f12d1 100644 --- a/test/moves/fusion_flare_bolt.test.ts +++ b/test/moves/fusion_flare_bolt.test.ts @@ -40,7 +40,7 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => { .enemyMoveset(MoveId.REST) .battleStyle("double") .startingWave(97) - .disableCrits(); + .criticalHits(false); vi.spyOn(fusionFlare, "calculateBattlePower"); vi.spyOn(fusionBolt, "calculateBattlePower"); @@ -64,7 +64,7 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => { expect((game.scene.phaseManager.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionBolt.id); await game.phaseInterceptor.to(DamageAnimPhase, false); expect(fusionBolt.calculateBattlePower).toHaveLastReturnedWith(200); - }, 20000); + }); it("FUSION_BOLT should double power of subsequent FUSION_FLARE", async () => { await game.classicMode.startBattle([SpeciesId.ZEKROM, SpeciesId.ZEKROM]); @@ -84,7 +84,7 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => { expect((game.scene.phaseManager.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionFlare.id); await game.phaseInterceptor.to(DamageAnimPhase, false); expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(200); - }, 20000); + }); it("FUSION_FLARE should double power of subsequent FUSION_BOLT if a move failed in between", async () => { await game.classicMode.startBattle([SpeciesId.ZEKROM, SpeciesId.ZEKROM]); @@ -109,7 +109,7 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => { expect((game.scene.phaseManager.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionBolt.id); await game.phaseInterceptor.to(DamageAnimPhase, false); expect(fusionBolt.calculateBattlePower).toHaveLastReturnedWith(200); - }, 20000); + }); it("FUSION_FLARE should not double power of subsequent FUSION_BOLT if a move succeeded in between", async () => { game.override.enemyMoveset(MoveId.SPLASH); @@ -134,7 +134,7 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => { expect((game.scene.phaseManager.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionBolt.id); await game.phaseInterceptor.to(DamageAnimPhase, false); expect(fusionBolt.calculateBattlePower).toHaveLastReturnedWith(100); - }, 20000); + }); it("FUSION_FLARE should double power of subsequent FUSION_BOLT if moves are aimed at allies", async () => { await game.classicMode.startBattle([SpeciesId.ZEKROM, SpeciesId.RESHIRAM]); @@ -154,7 +154,7 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => { expect((game.scene.phaseManager.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionFlare.id); await game.phaseInterceptor.to(DamageAnimPhase, false); expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(200); - }, 20000); + }); it("FUSION_FLARE and FUSION_BOLT alternating throughout turn should double power of subsequent moves", async () => { game.override.enemyMoveset(fusionFlare.id); @@ -208,7 +208,7 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => { expect((game.scene.phaseManager.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionFlare.id); await game.phaseInterceptor.to(DamageAnimPhase, false); expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(200); - }, 20000); + }); it("FUSION_FLARE and FUSION_BOLT alternating throughout turn should double power of subsequent moves if moves are aimed at allies", async () => { game.override.enemyMoveset(fusionFlare.id); @@ -262,5 +262,5 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => { expect((game.scene.phaseManager.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionFlare.id); await game.phaseInterceptor.to(DamageAnimPhase, false); expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(200); - }, 20000); + }); }); diff --git a/test/moves/gastro_acid.test.ts b/test/moves/gastro_acid.test.ts index 28f2c8e8476..39167987809 100644 --- a/test/moves/gastro_acid.test.ts +++ b/test/moves/gastro_acid.test.ts @@ -3,6 +3,7 @@ import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { MoveResult } from "#enums/move-result"; +import { BattleType } from "#enums/battle-type"; import GameManager from "#test/testUtils/gameManager"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; @@ -22,82 +23,92 @@ describe("Moves - Gastro Acid", () => { beforeEach(() => { game = new GameManager(phaserGame); + game.override + .battleStyle("single") + .ability(AbilityId.BALL_FETCH) + .startingLevel(1) + .enemySpecies(SpeciesId.BIDOOF) + .enemyMoveset(MoveId.SPLASH) + .enemyAbility(AbilityId.WATER_ABSORB); + }); + + it("should suppress the target's ability", async () => { game.override.battleStyle("double"); - game.override.startingLevel(1); - game.override.enemyLevel(100); - game.override.ability(AbilityId.BALL_FETCH); - game.override.moveset([MoveId.GASTRO_ACID, MoveId.WATER_GUN, MoveId.SPLASH, MoveId.CORE_ENFORCER]); - game.override.enemySpecies(SpeciesId.BIDOOF); - game.override.enemyMoveset(MoveId.SPLASH); - game.override.enemyAbility(AbilityId.WATER_ABSORB); + await game.classicMode.startBattle([SpeciesId.BIDOOF, SpeciesId.BASCULIN]); + + game.move.use(MoveId.GASTRO_ACID, BattlerIndex.PLAYER, BattlerIndex.ENEMY); + game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER_2); + await game.toNextTurn(); + + const [enemy1, enemy2] = game.scene.getEnemyField(); + expect(enemy1.summonData.abilitySuppressed).toBe(true); + expect(enemy2.summonData.abilitySuppressed).toBe(false); + + game.move.use(MoveId.WATER_GUN, BattlerIndex.PLAYER, BattlerIndex.ENEMY); + game.move.use(MoveId.WATER_GUN, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY_2); + await game.toEndOfTurn(); + + expect(enemy1.summonData.abilitySuppressed).toBe(true); + expect(enemy2.summonData.abilitySuppressed).toBe(false); + expect(enemy1.hp).toBeLessThan(enemy1.getMaxHp()); + expect(enemy2.hp).toBe(enemy2.getMaxHp()); }); - it("suppresses effect of ability", async () => { - /* - * Expected flow (enemies have WATER ABSORD, can only use SPLASH) - * - player mon 1 uses GASTRO ACID, player mon 2 uses SPLASH - * - both player mons use WATER GUN on their respective enemy mon - * - player mon 1 should have dealt damage, player mon 2 should have not - */ + it("should be removed on switch", async () => { + game.override.battleType(BattleType.TRAINER); + await game.classicMode.startBattle([SpeciesId.BIDOOF]); - await game.classicMode.startBattle(); + game.move.use(MoveId.GASTRO_ACID); + await game.toNextTurn(); - game.move.select(MoveId.GASTRO_ACID, 0, BattlerIndex.ENEMY); - game.move.select(MoveId.SPLASH, 1); + const enemy = game.field.getEnemyPokemon(); + expect(enemy.summonData.abilitySuppressed).toBe(true); - await game.phaseInterceptor.to("TurnInitPhase"); + // switch enemy out and back in, should be removed + game.move.use(MoveId.SPLASH); + game.forceEnemyToSwitch(); + await game.toNextTurn(); + game.move.use(MoveId.SPLASH); + game.forceEnemyToSwitch(); + await game.toNextTurn(); - const enemyField = game.scene.getEnemyField(); - expect(enemyField[0].summonData.abilitySuppressed).toBe(true); - expect(enemyField[1].summonData.abilitySuppressed).toBe(false); - - game.move.select(MoveId.WATER_GUN, 0, BattlerIndex.ENEMY); - game.move.select(MoveId.WATER_GUN, 1, BattlerIndex.ENEMY_2); - - await game.phaseInterceptor.to("TurnEndPhase"); - - expect(enemyField[0].hp).toBeLessThan(enemyField[0].getMaxHp()); - expect(enemyField[1].isFullHp()).toBe(true); + expect(game.field.getEnemyPokemon()).toBe(enemy); + expect(enemy.summonData.abilitySuppressed).toBe(false); }); - it("fails if used on an enemy with an already-suppressed ability", async () => { - game.override.battleStyle("single"); + it("should fail if target's ability is already suppressed", async () => { + await game.classicMode.startBattle([SpeciesId.BIDOOF]); - await game.classicMode.startBattle(); - - game.move.select(MoveId.CORE_ENFORCER); + game.move.use(MoveId.CORE_ENFORCER); // Force player to be slower to enable Core Enforcer to proc its suppression effect await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + await game.toNextTurn(); - await game.phaseInterceptor.to("TurnInitPhase"); + game.move.use(MoveId.GASTRO_ACID); + await game.toNextTurn(); - game.move.select(MoveId.GASTRO_ACID); - - await game.phaseInterceptor.to("TurnInitPhase"); - - expect(game.scene.getPlayerPokemon()!.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + expect(game.field.getPlayerPokemon().getLastXMoves()[0].result).toBe(MoveResult.FAIL); }); - it("should suppress the passive of a target even if its main ability is unsuppressable and not suppress main abli", async () => { - game.override - .enemyAbility(AbilityId.COMATOSE) - .enemyPassiveAbility(AbilityId.WATER_ABSORB) - .moveset([MoveId.SPLASH, MoveId.GASTRO_ACID, MoveId.WATER_GUN]); + it("should suppress target's passive even if its main ability is unsuppressable", async () => { + game.override.enemyAbility(AbilityId.COMATOSE).enemyPassiveAbility(AbilityId.WATER_ABSORB); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); - const enemyPokemon = game.scene.getEnemyPokemon(); + const enemyPokemon = game.field.getEnemyPokemon(); - game.move.select(MoveId.GASTRO_ACID); + game.move.use(MoveId.GASTRO_ACID); await game.toNextTurn(); - expect(enemyPokemon?.summonData.abilitySuppressed).toBe(true); + expect(enemyPokemon.summonData.abilitySuppressed).toBe(true); game.move.select(MoveId.WATER_GUN); await game.toNextTurn(); - expect(enemyPokemon?.getHpRatio()).toBeLessThan(1); + // water gun should've dealt damage due to suppressed Water Absorb + expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); game.move.select(MoveId.SPORE); - await game.phaseInterceptor.to("BerryPhase"); + await game.toEndOfTurn(); - expect(enemyPokemon?.status?.effect).toBeFalsy(); + // Comatose should block stauts effect + expect(enemyPokemon.status?.effect).toBeUndefined(); }); }); diff --git a/test/moves/gigaton_hammer.test.ts b/test/moves/gigaton_hammer.test.ts index be7adf53aa7..d80743f4ed0 100644 --- a/test/moves/gigaton_hammer.test.ts +++ b/test/moves/gigaton_hammer.test.ts @@ -29,7 +29,7 @@ describe("Moves - Gigaton Hammer", () => { .startingLevel(10) .enemyLevel(100) .enemyMoveset(MoveId.SPLASH) - .disableCrits(); + .criticalHits(false); }); it("can't be used two turns in a row", async () => { @@ -52,7 +52,7 @@ describe("Moves - Gigaton Hammer", () => { const enemy2 = game.scene.getEnemyPokemon()!; expect(enemy2.hp).toBe(enemy2.getMaxHp()); - }, 20000); + }); it("can be used again if recalled and sent back out", async () => { game.override.startingWave(4); @@ -75,5 +75,5 @@ describe("Moves - Gigaton Hammer", () => { const enemy2 = game.scene.getEnemyPokemon()!; expect(enemy2.hp).toBeLessThan(enemy2.getMaxHp()); - }, 20000); + }); }); diff --git a/test/moves/glaive_rush.test.ts b/test/moves/glaive_rush.test.ts index 768c4a691b0..0b6f30da71a 100644 --- a/test/moves/glaive_rush.test.ts +++ b/test/moves/glaive_rush.test.ts @@ -24,17 +24,16 @@ describe("Moves - Glaive Rush", () => { game = new GameManager(phaserGame); game.override .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset([MoveId.GLAIVE_RUSH]) - .starterSpecies(SpeciesId.KLINK) .ability(AbilityId.BALL_FETCH) .moveset([MoveId.SHADOW_SNEAK, MoveId.AVALANCHE, MoveId.SPLASH, MoveId.GLAIVE_RUSH]); }); it("takes double damage from attacks", async () => { - await game.classicMode.startBattle(); + await game.classicMode.startBattle([SpeciesId.KLINK]); const enemy = game.scene.getEnemyPokemon()!; enemy.hp = 1000; @@ -49,7 +48,7 @@ describe("Moves - Glaive Rush", () => { }); it("always gets hit by attacks", async () => { - await game.classicMode.startBattle(); + await game.classicMode.startBattle([SpeciesId.KLINK]); const enemy = game.scene.getEnemyPokemon()!; enemy.hp = 1000; @@ -62,7 +61,7 @@ describe("Moves - Glaive Rush", () => { it("interacts properly with multi-lens", async () => { game.override.startingHeldItems([{ name: "MULTI_LENS", count: 2 }]).enemyMoveset([MoveId.AVALANCHE]); - await game.classicMode.startBattle(); + await game.classicMode.startBattle([SpeciesId.KLINK]); const player = game.scene.getPlayerPokemon()!; const enemy = game.scene.getEnemyPokemon()!; @@ -82,7 +81,7 @@ describe("Moves - Glaive Rush", () => { it("secondary effects only last until next move", async () => { game.override.enemyMoveset([MoveId.SHADOW_SNEAK]); - await game.classicMode.startBattle(); + await game.classicMode.startBattle([SpeciesId.KLINK]); const player = game.scene.getPlayerPokemon()!; const enemy = game.scene.getEnemyPokemon()!; @@ -106,7 +105,7 @@ describe("Moves - Glaive Rush", () => { }); it("secondary effects are removed upon switching", async () => { - game.override.enemyMoveset([MoveId.SHADOW_SNEAK]).starterSpecies(0); + game.override.enemyMoveset([MoveId.SHADOW_SNEAK]); await game.classicMode.startBattle([SpeciesId.KLINK, SpeciesId.FEEBAS]); const player = game.scene.getPlayerPokemon()!; @@ -127,8 +126,10 @@ describe("Moves - Glaive Rush", () => { }); it("secondary effects don't activate if move fails", async () => { - game.override.moveset([MoveId.SHADOW_SNEAK, MoveId.PROTECT, MoveId.SPLASH, MoveId.GLAIVE_RUSH]); - await game.classicMode.startBattle(); + game.override + .moveset([MoveId.SHADOW_SNEAK, MoveId.PROTECT, MoveId.SPLASH, MoveId.GLAIVE_RUSH]) + .enemyMoveset([MoveId.GLAIVE_RUSH, MoveId.SPLASH]); + await game.classicMode.startBattle([SpeciesId.KLINK]); const player = game.scene.getPlayerPokemon()!; const enemy = game.scene.getEnemyPokemon()!; @@ -137,15 +138,17 @@ describe("Moves - Glaive Rush", () => { player.hp = 1000; game.move.select(MoveId.PROTECT); + await game.move.forceEnemyMove(MoveId.GLAIVE_RUSH); await game.phaseInterceptor.to("TurnEndPhase"); game.move.select(MoveId.SHADOW_SNEAK); + await game.move.forceEnemyMove(MoveId.GLAIVE_RUSH); await game.phaseInterceptor.to("TurnEndPhase"); - game.override.enemyMoveset([MoveId.SPLASH]); const damagedHP1 = 1000 - enemy.hp; enemy.hp = 1000; game.move.select(MoveId.SHADOW_SNEAK); + await game.move.forceEnemyMove(MoveId.SPLASH); await game.phaseInterceptor.to("TurnEndPhase"); const damagedHP2 = 1000 - enemy.hp; diff --git a/test/moves/growth.test.ts b/test/moves/growth.test.ts index d468523aca7..6eec30be49f 100644 --- a/test/moves/growth.test.ts +++ b/test/moves/growth.test.ts @@ -24,11 +24,12 @@ describe("Moves - Growth", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleStyle("single"); - game.override.enemyAbility(AbilityId.MOXIE); - game.override.ability(AbilityId.INSOMNIA); - game.override.moveset([MoveId.GROWTH]); - game.override.enemyMoveset(MoveId.SPLASH); + game.override + .battleStyle("single") + .enemyAbility(AbilityId.MOXIE) + .ability(AbilityId.INSOMNIA) + .moveset([MoveId.GROWTH]) + .enemyMoveset(MoveId.SPLASH); }); it("should raise SPATK stat stage by 1", async () => { @@ -42,5 +43,5 @@ describe("Moves - Growth", () => { await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(TurnInitPhase); expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(1); - }, 20000); + }); }); diff --git a/test/moves/grudge.test.ts b/test/moves/grudge.test.ts index 63018a419e3..36030c88bad 100644 --- a/test/moves/grudge.test.ts +++ b/test/moves/grudge.test.ts @@ -26,7 +26,7 @@ describe("Moves - Grudge", () => { .moveset([MoveId.EMBER, MoveId.SPLASH]) .ability(AbilityId.BALL_FETCH) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.SHEDINJA) .enemyAbility(AbilityId.WONDER_GUARD) .enemyMoveset([MoveId.GRUDGE, MoveId.SPLASH]); diff --git a/test/moves/guard_split.test.ts b/test/moves/guard_split.test.ts index 47edba49c9a..878bb80f251 100644 --- a/test/moves/guard_split.test.ts +++ b/test/moves/guard_split.test.ts @@ -50,7 +50,7 @@ describe("Moves - Guard Split", () => { expect(player.getStat(Stat.SPDEF, false)).toBe(avgSpDef); expect(enemy.getStat(Stat.SPDEF, false)).toBe(avgSpDef); - }, 20000); + }); it("should be idempotent", async () => { game.override.enemyMoveset([MoveId.GUARD_SPLIT]); @@ -73,5 +73,5 @@ describe("Moves - Guard Split", () => { expect(player.getStat(Stat.SPDEF, false)).toBe(avgSpDef); expect(enemy.getStat(Stat.SPDEF, false)).toBe(avgSpDef); - }, 20000); + }); }); diff --git a/test/moves/hard_press.test.ts b/test/moves/hard_press.test.ts index 88a8c4a025b..e57c9af981f 100644 --- a/test/moves/hard_press.test.ts +++ b/test/moves/hard_press.test.ts @@ -27,12 +27,13 @@ describe("Moves - Hard Press", () => { beforeEach(() => { moveToCheck = allMoves[MoveId.HARD_PRESS]; game = new GameManager(phaserGame); - game.override.battleStyle("single"); - game.override.ability(AbilityId.BALL_FETCH); - game.override.enemySpecies(SpeciesId.MUNCHLAX); - game.override.enemyAbility(AbilityId.BALL_FETCH); - game.override.enemyMoveset(MoveId.SPLASH); - game.override.moveset([MoveId.HARD_PRESS]); + game.override + .battleStyle("single") + .ability(AbilityId.BALL_FETCH) + .enemySpecies(SpeciesId.MUNCHLAX) + .enemyAbility(AbilityId.BALL_FETCH) + .enemyMoveset(MoveId.SPLASH) + .moveset([MoveId.HARD_PRESS]); vi.spyOn(moveToCheck, "calculateBattlePower"); }); diff --git a/test/moves/haze.test.ts b/test/moves/haze.test.ts index e77542227b8..f3e3dafae0a 100644 --- a/test/moves/haze.test.ts +++ b/test/moves/haze.test.ts @@ -23,16 +23,15 @@ describe("Moves - Haze", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleStyle("single"); - - game.override.enemySpecies(SpeciesId.RATTATA); - game.override.enemyLevel(100); - game.override.enemyMoveset(MoveId.SPLASH); - game.override.enemyAbility(AbilityId.NONE); - - game.override.startingLevel(100); - game.override.moveset([MoveId.HAZE, MoveId.SWORDS_DANCE, MoveId.CHARM, MoveId.SPLASH]); - game.override.ability(AbilityId.NONE); + game.override + .battleStyle("single") + .enemySpecies(SpeciesId.RATTATA) + .enemyLevel(100) + .enemyMoveset(MoveId.SPLASH) + .enemyAbility(AbilityId.BALL_FETCH) + .startingLevel(100) + .moveset([MoveId.HAZE, MoveId.SWORDS_DANCE, MoveId.CHARM, MoveId.SPLASH]) + .ability(AbilityId.BALL_FETCH); }); it("should reset all stat changes of all Pokemon on field", async () => { diff --git a/test/moves/heal_block.test.ts b/test/moves/heal_block.test.ts index 39e8efea827..77a10927930 100644 --- a/test/moves/heal_block.test.ts +++ b/test/moves/heal_block.test.ts @@ -33,7 +33,7 @@ describe("Moves - Heal Block", () => { .ability(AbilityId.NO_GUARD) .enemyAbility(AbilityId.BALL_FETCH) .enemySpecies(SpeciesId.BLISSEY) - .disableCrits(); + .criticalHits(false); }); it("shouldn't stop damage from HP-drain attacks, just HP restoration", async () => { diff --git a/test/moves/hyper_beam.test.ts b/test/moves/hyper_beam.test.ts index 083482d16ef..bca7cba4e9a 100644 --- a/test/moves/hyper_beam.test.ts +++ b/test/moves/hyper_beam.test.ts @@ -26,14 +26,14 @@ describe("Moves - Hyper Beam", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleStyle("single"); - game.override.ability(AbilityId.BALL_FETCH); - game.override.enemySpecies(SpeciesId.SNORLAX); - game.override.enemyAbility(AbilityId.BALL_FETCH); - game.override.enemyMoveset([MoveId.SPLASH]); - game.override.enemyLevel(100); - - game.override.moveset([MoveId.HYPER_BEAM, MoveId.TACKLE]); + game.override + .battleStyle("single") + .ability(AbilityId.BALL_FETCH) + .enemySpecies(SpeciesId.SNORLAX) + .enemyAbility(AbilityId.BALL_FETCH) + .enemyMoveset([MoveId.SPLASH]) + .enemyLevel(100) + .moveset([MoveId.HYPER_BEAM, MoveId.TACKLE]); vi.spyOn(allMoves[MoveId.HYPER_BEAM], "accuracy", "get").mockReturnValue(100); }); diff --git a/test/moves/instruct.test.ts b/test/moves/instruct.test.ts index 74de4c5da17..d12859301b6 100644 --- a/test/moves/instruct.test.ts +++ b/test/moves/instruct.test.ts @@ -1,9 +1,11 @@ import { BattlerIndex } from "#enums/battler-index"; +import { RandomMoveAttr } from "#app/data/moves/move"; import { allMoves } from "#app/data/data-lists"; import type Pokemon from "#app/field/pokemon"; import { MoveResult } from "#enums/move-result"; import type { MovePhase } from "#app/phases/move-phase"; import { AbilityId } from "#enums/ability-id"; +import { MoveUseMode } from "#enums/move-use-mode"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { Stat } from "#enums/stat"; @@ -40,7 +42,7 @@ describe("Moves - Instruct", () => { .passiveAbility(AbilityId.NO_GUARD) .enemyLevel(100) .startingLevel(100) - .disableCrits(); + .criticalHits(false); }); it("should repeat target's last used move", async () => { @@ -141,6 +143,21 @@ describe("Moves - Instruct", () => { expect(game.scene.getPlayerPokemon()!.turnData.attacksReceived.length).toBe(3); }); + it("should fail on metronomed moves, even if also in moveset", async () => { + vi.spyOn(RandomMoveAttr.prototype, "getMoveOverride").mockReturnValue(MoveId.ABSORB); + await game.classicMode.startBattle([SpeciesId.AMOONGUSS]); + + const enemy = game.field.getEnemyPokemon(); + game.move.changeMoveset(enemy, [MoveId.METRONOME, MoveId.ABSORB]); + + game.move.use(MoveId.INSTRUCT); + await game.move.selectEnemyMove(MoveId.METRONOME); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + await game.toEndOfTurn(); + + expect(game.field.getPlayerPokemon().getLastXMoves()[0].result).toBe(MoveResult.FAIL); + }); + it("should respect enemy's status condition", async () => { game.override.moveset([MoveId.INSTRUCT, MoveId.THUNDER_WAVE]).enemyMoveset(MoveId.SONIC_BOOM); await game.classicMode.startBattle([SpeciesId.AMOONGUSS]); @@ -152,7 +169,7 @@ describe("Moves - Instruct", () => { game.move.select(MoveId.INSTRUCT); await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.phaseInterceptor.to("MovePhase"); - // force enemy's instructed move to bork and then immediately thaw out + // force enemy's instructed move (and only the instructed move) to fail await game.move.forceStatusActivation(true); await game.move.forceStatusActivation(false); await game.phaseInterceptor.to("TurnEndPhase", false); @@ -176,7 +193,7 @@ describe("Moves - Instruct", () => { await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.phaseInterceptor.to("TurnEndPhase", false); - const playerMoves = game.scene.getPlayerPokemon()!.getLastXMoves(-1)!; + const playerMoves = game.field.getPlayerPokemon().getLastXMoves(-1); expect(playerMoves[0].result).toBe(MoveResult.FAIL); expect(enemyPokemon.getMoveHistory().length).toBe(1); }); @@ -203,6 +220,7 @@ describe("Moves - Instruct", () => { expect(karp1.isFainted()).toBe(true); expect(karp2.isFainted()).toBe(true); }); + it("should allow for dancer copying of instructed dance move", async () => { game.override.battleStyle("double").enemyMoveset([MoveId.INSTRUCT, MoveId.SPLASH]).enemyLevel(1000); await game.classicMode.startBattle([SpeciesId.ORICORIO, SpeciesId.VOLCARONA]); @@ -231,19 +249,18 @@ describe("Moves - Instruct", () => { const amoonguss = game.scene.getPlayerPokemon()!; game.move.changeMoveset(amoonguss, MoveId.SEED_BOMB); - amoonguss.summonData.moveHistory = [ - { - move: MoveId.SEED_BOMB, - targets: [BattlerIndex.ENEMY], - result: MoveResult.SUCCESS, - }, - ]; + amoonguss.pushMoveHistory({ + move: MoveId.SEED_BOMB, + targets: [BattlerIndex.ENEMY], + result: MoveResult.SUCCESS, + useMode: MoveUseMode.NORMAL, + }); game.doSwitchPokemon(1); await game.phaseInterceptor.to("TurnEndPhase", false); - const enemyMoves = game.scene.getEnemyPokemon()!.getLastXMoves(-1)!; - expect(enemyMoves[0].result).toBe(MoveResult.FAIL); + const enemyMoves = game.field.getEnemyPokemon().getLastXMoves(-1); + expect(enemyMoves[0]?.result).toBe(MoveResult.FAIL); }); it("should fail if no move has yet been used by target", async () => { @@ -272,15 +289,9 @@ describe("Moves - Instruct", () => { await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER_2, BattlerIndex.PLAYER, BattlerIndex.ENEMY_2]); await game.phaseInterceptor.to("TurnEndPhase", false); - expect(game.scene.getPlayerField()[0].getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); - const enemyMove = game.scene.getEnemyField()[0]!.getLastXMoves()[0]; - expect(enemyMove.result).toBe(MoveResult.FAIL); - expect( - game.scene - .getEnemyField()[0] - .getMoveset() - .find(m => m?.moveId === MoveId.SONIC_BOOM)?.ppUsed, - ).toBe(1); + expect(game.field.getPlayerPokemon().getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); + expect(enemy1.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + expect(enemy1.getMoveset().find(m => m.moveId === MoveId.SONIC_BOOM)?.ppUsed).toBe(1); }); it("should not repeat enemy's move through protect", async () => { @@ -304,14 +315,12 @@ describe("Moves - Instruct", () => { const player = game.scene.getPlayerPokemon()!; const enemy = game.scene.getEnemyPokemon()!; - enemy.summonData.moveHistory = [ - { - move: MoveId.SONIC_BOOM, - targets: [BattlerIndex.PLAYER], - result: MoveResult.SUCCESS, - virtual: false, - }, - ]; + enemy.pushMoveHistory({ + move: MoveId.SONIC_BOOM, + targets: [BattlerIndex.PLAYER], + result: MoveResult.SUCCESS, + useMode: MoveUseMode.NORMAL, + }); game.move.select(MoveId.INSTRUCT); await game.move.selectEnemyMove(MoveId.HYPER_BEAM); @@ -337,7 +346,7 @@ describe("Moves - Instruct", () => { move: MoveId.ELECTRO_DRIFT, targets: [BattlerIndex.PLAYER], result: MoveResult.SUCCESS, - virtual: false, + useMode: MoveUseMode.NORMAL, }); game.move.select(MoveId.SPLASH); @@ -351,14 +360,12 @@ describe("Moves - Instruct", () => { await game.classicMode.startBattle([SpeciesId.LUCARIO, SpeciesId.BANETTE]); const enemyPokemon = game.scene.getEnemyPokemon()!; - enemyPokemon.summonData.moveHistory = [ - { - move: MoveId.WHIRLWIND, - targets: [BattlerIndex.PLAYER], - result: MoveResult.SUCCESS, - virtual: false, - }, - ]; + enemyPokemon.pushMoveHistory({ + move: MoveId.WHIRLWIND, + targets: [BattlerIndex.PLAYER], + result: MoveResult.SUCCESS, + useMode: MoveUseMode.NORMAL, + }); game.move.select(MoveId.INSTRUCT); await game.move.selectEnemyMove(MoveId.SPLASH); @@ -378,11 +385,20 @@ describe("Moves - Instruct", () => { .enemyMoveset([MoveId.SPLASH, MoveId.PSYCHIC_TERRAIN]); await game.classicMode.startBattle([SpeciesId.BANETTE, SpeciesId.KLEFKI]); - game.move.select(MoveId.QUICK_ATTACK, BattlerIndex.PLAYER, BattlerIndex.ENEMY); // succeeds due to terrain no + const banette = game.field.getPlayerPokemon(); + + game.move.select(MoveId.QUICK_ATTACK, BattlerIndex.PLAYER, BattlerIndex.ENEMY); game.move.select(MoveId.SPLASH, BattlerIndex.PLAYER_2); await game.move.selectEnemyMove(MoveId.SPLASH); await game.move.selectEnemyMove(MoveId.PSYCHIC_TERRAIN); await game.toNextTurn(); + expect(banette.getLastXMoves(-1)[0]).toEqual( + expect.objectContaining({ + move: MoveId.QUICK_ATTACK, + targets: [BattlerIndex.ENEMY], + result: MoveResult.SUCCESS, + }), + ); game.move.select(MoveId.SPLASH, BattlerIndex.PLAYER); game.move.select(MoveId.INSTRUCT, BattlerIndex.PLAYER_2, BattlerIndex.PLAYER); @@ -390,32 +406,74 @@ describe("Moves - Instruct", () => { await game.phaseInterceptor.to("TurnEndPhase", false); // quick attack failed when instructed - const banette = game.scene.getPlayerPokemon()!; expect(banette.getLastXMoves(-1)[1].move).toBe(MoveId.QUICK_ATTACK); expect(banette.getLastXMoves(-1)[1].result).toBe(MoveResult.FAIL); }); - it("should still work w/ prankster in psychic terrain", async () => { - game.override.battleStyle("double").enemyMoveset([MoveId.SPLASH, MoveId.PSYCHIC_TERRAIN]); + // TODO: Enable once Sky Drop is fully implemented + it.todo("should not work against Sky Dropped targets, even if user/target have No Guard", async () => { + game.override.battleStyle("double").ability(AbilityId.NO_GUARD); await game.classicMode.startBattle([SpeciesId.BANETTE, SpeciesId.KLEFKI]); - const [banette, klefki] = game.scene.getPlayerField()!; - game.move.changeMoveset(banette, [MoveId.VINE_WHIP, MoveId.SPLASH]); - game.move.changeMoveset(klefki, [MoveId.INSTRUCT, MoveId.SPLASH]); + const [banette, klefki] = game.scene.getPlayerField(); + banette.pushMoveHistory({ + move: MoveId.VINE_WHIP, + targets: [BattlerIndex.ENEMY], + result: MoveResult.SUCCESS, + useMode: MoveUseMode.NORMAL, + }); - game.move.select(MoveId.VINE_WHIP, BattlerIndex.PLAYER, BattlerIndex.ENEMY); - game.move.select(MoveId.SPLASH, BattlerIndex.PLAYER_2); - await game.move.selectEnemyMove(MoveId.SPLASH); - await game.move.selectEnemyMove(MoveId.PSYCHIC_TERRAIN); - await game.toNextTurn(); + // Attempt to instruct banette after having been sent airborne + game.move.use(MoveId.VINE_WHIP, BattlerIndex.PLAYER, BattlerIndex.ENEMY); + game.move.use(MoveId.INSTRUCT, BattlerIndex.PLAYER_2, BattlerIndex.PLAYER); + await game.move.forceEnemyMove(MoveId.SKY_DROP, BattlerIndex.PLAYER); + await game.move.forceEnemyMove(MoveId.ASTONISH, BattlerIndex.PLAYER); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER_2, BattlerIndex.PLAYER, BattlerIndex.ENEMY_2]); + await game.phaseInterceptor.to("TurnEndPhase", false); - game.move.select(MoveId.SPLASH, BattlerIndex.PLAYER); + // Klefki instruct fails due to banette being airborne, even though it got hit prior + expect(banette.visible).toBe(false); + expect(banette.isFullHp()).toBe(false); + expect(klefki.getLastXMoves()[0]).toMatchObject({ + move: MoveId.INSTRUCT, + targets: [BattlerIndex.PLAYER], + result: MoveResult.FAIL, + }); + }); + + it("should still work with prankster in psychic terrain", async () => { + game.override + .battleStyle("double") + .ability(AbilityId.PRANKSTER) + .enemyMoveset(MoveId.SPLASH) + .enemyAbility(AbilityId.PSYCHIC_SURGE); + await game.classicMode.startBattle([SpeciesId.BANETTE, SpeciesId.KLEFKI]); + + const [banette, klefki] = game.scene.getPlayerField(); + game.move.changeMoveset(banette, [MoveId.VINE_WHIP]); + game.move.changeMoveset(klefki, MoveId.INSTRUCT); + banette.pushMoveHistory({ + move: MoveId.VINE_WHIP, + targets: [BattlerIndex.ENEMY], + result: MoveResult.SUCCESS, + useMode: MoveUseMode.NORMAL, + }); + + game.move.select(MoveId.VINE_WHIP, BattlerIndex.PLAYER); game.move.select(MoveId.INSTRUCT, BattlerIndex.PLAYER_2, BattlerIndex.PLAYER); // copies vine whip await game.setTurnOrder([BattlerIndex.PLAYER_2, BattlerIndex.PLAYER, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]); await game.phaseInterceptor.to("TurnEndPhase", false); + + // Klefki instructing a non-priority move succeeds, ignoring the priority of Instruct itself expect(banette.getLastXMoves(-1)[1].move).toBe(MoveId.VINE_WHIP); expect(banette.getLastXMoves(-1)[2].move).toBe(MoveId.VINE_WHIP); - expect(banette.getMoveset().find(m => m?.moveId === MoveId.VINE_WHIP)?.ppUsed).toBe(2); + expect(klefki.getLastXMoves(-1)[0]).toEqual( + expect.objectContaining({ + move: MoveId.INSTRUCT, + targets: [BattlerIndex.PLAYER], + result: MoveResult.SUCCESS, + }), + ); }); it("should cause spread moves to correctly hit targets in doubles after singles", async () => { @@ -424,14 +482,15 @@ describe("Moves - Instruct", () => { .moveset([MoveId.BREAKING_SWIPE, MoveId.INSTRUCT, MoveId.SPLASH]) .enemyMoveset(MoveId.SONIC_BOOM) .enemySpecies(SpeciesId.AXEW) - .startingLevel(500); + .startingLevel(500) + .enemyLevel(1); await game.classicMode.startBattle([SpeciesId.KORAIDON, SpeciesId.KLEFKI]); const koraidon = game.scene.getPlayerField()[0]!; game.move.select(MoveId.BREAKING_SWIPE); await game.phaseInterceptor.to("TurnEndPhase", false); - expect(koraidon.getInverseHp()).toBe(0); + expect(koraidon.hp).toBe(koraidon.getMaxHp()); expect(koraidon.getLastXMoves(-1)[0].targets).toEqual([BattlerIndex.ENEMY]); await game.toNextWave(); @@ -439,9 +498,10 @@ describe("Moves - Instruct", () => { game.move.select(MoveId.INSTRUCT, BattlerIndex.PLAYER_2, BattlerIndex.PLAYER); await game.setTurnOrder([BattlerIndex.PLAYER_2, BattlerIndex.PLAYER, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]); await game.phaseInterceptor.to("TurnEndPhase", false); + // did not take damage since enemies died beforehand; // last move used hit both enemies - expect(koraidon.getInverseHp()).toBe(0); + expect(koraidon.hp).toBe(koraidon.getMaxHp()); expect(koraidon.getLastXMoves(-1)[1].targets?.sort()).toEqual([BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]); }); @@ -451,7 +511,8 @@ describe("Moves - Instruct", () => { .moveset([MoveId.BRUTAL_SWING, MoveId.INSTRUCT, MoveId.SPLASH]) .enemySpecies(SpeciesId.AXEW) .enemyMoveset(MoveId.SONIC_BOOM) - .startingLevel(500); + .startingLevel(500) + .enemyLevel(1); await game.classicMode.startBattle([SpeciesId.KORAIDON, SpeciesId.KLEFKI]); const koraidon = game.scene.getPlayerField()[0]!; @@ -459,22 +520,24 @@ describe("Moves - Instruct", () => { game.move.select(MoveId.BRUTAL_SWING); await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.phaseInterceptor.to("TurnEndPhase", false); - expect(koraidon.getInverseHp()).toBe(0); + + expect(koraidon.hp).toBe(koraidon.getMaxHp()); expect(koraidon.getLastXMoves(-1)[0].targets).toEqual([BattlerIndex.ENEMY]); + await game.toNextWave(); game.move.select(MoveId.SPLASH, BattlerIndex.PLAYER); game.move.select(MoveId.INSTRUCT, BattlerIndex.PLAYER_2, BattlerIndex.PLAYER); await game.setTurnOrder([BattlerIndex.PLAYER_2, BattlerIndex.PLAYER, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]); await game.phaseInterceptor.to("TurnEndPhase", false); + // did not take damage since enemies died beforehand; // last move used hit everything around it - expect(koraidon.getInverseHp()).toBe(0); - expect(koraidon.getLastXMoves(-1)[1].targets?.sort()).toEqual([ - BattlerIndex.PLAYER_2, - BattlerIndex.ENEMY, - BattlerIndex.ENEMY_2, - ]); + expect(koraidon.hp).toBe(koraidon.getMaxHp()); + expect(koraidon.getLastXMoves(-1)[1].targets).toHaveLength(3); + expect(koraidon.getLastXMoves(-1)[1].targets).toEqual( + expect.arrayContaining([BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]), + ); }); it("should cause multi-hit moves to hit the appropriate number of times in singles", async () => { @@ -547,7 +610,12 @@ describe("Moves - Instruct", () => { // Fake enemy 1 having attacked prior const [, player2, enemy1, enemy2] = game.scene.getField(); - enemy1.pushMoveHistory({ move: MoveId.ABSORB, targets: [BattlerIndex.PLAYER] }); + enemy1.pushMoveHistory({ + move: MoveId.ABSORB, + targets: [BattlerIndex.PLAYER], + result: MoveResult.SUCCESS, + useMode: MoveUseMode.NORMAL, + }); game.field.mockAbility(enemy1, AbilityId.STEADFAST); game.move.use(MoveId.AIR_SLASH, BattlerIndex.PLAYER, BattlerIndex.ENEMY); diff --git a/test/moves/jaw_lock.test.ts b/test/moves/jaw_lock.test.ts index e0815a4c3c9..4e103c14f98 100644 --- a/test/moves/jaw_lock.test.ts +++ b/test/moves/jaw_lock.test.ts @@ -36,7 +36,7 @@ describe("Moves - Jaw Lock", () => { .moveset([MoveId.JAW_LOCK, MoveId.SPLASH]) .startingLevel(100) .enemyLevel(100) - .disableCrits(); + .criticalHits(false); }); it("should trap the move's user and target", async () => { diff --git a/test/moves/lash_out.test.ts b/test/moves/lash_out.test.ts index c0c0881b340..33a58914978 100644 --- a/test/moves/lash_out.test.ts +++ b/test/moves/lash_out.test.ts @@ -25,7 +25,7 @@ describe("Moves - Lash Out", () => { game = new GameManager(phaserGame); game.override .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.FUR_COAT) .enemyMoveset([MoveId.GROWL]) diff --git a/test/moves/last-resort.test.ts b/test/moves/last-resort.test.ts index e4b8346053b..3cb98a0cd92 100644 --- a/test/moves/last-resort.test.ts +++ b/test/moves/last-resort.test.ts @@ -1,6 +1,7 @@ import { BattlerIndex } from "#enums/battler-index"; import { MoveResult } from "#enums/move-result"; import { AbilityId } from "#enums/ability-id"; +import { MoveUseMode } from "#enums/move-use-mode"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import GameManager from "#test/testUtils/gameManager"; @@ -34,7 +35,7 @@ describe("Moves - Last Resort", () => { game.override .ability(AbilityId.BALL_FETCH) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH); @@ -53,19 +54,19 @@ describe("Moves - Last Resort", () => { expectLastResortFail(); // Splash (1/3) - blissey.pushMoveHistory({ move: MoveId.SPLASH, targets: [BattlerIndex.PLAYER] }); + blissey.pushMoveHistory({ move: MoveId.SPLASH, targets: [BattlerIndex.PLAYER], useMode: MoveUseMode.NORMAL }); game.move.select(MoveId.LAST_RESORT); await game.phaseInterceptor.to("TurnEndPhase"); expectLastResortFail(); // Growl (2/3) - blissey.pushMoveHistory({ move: MoveId.GROWL, targets: [BattlerIndex.ENEMY] }); + blissey.pushMoveHistory({ move: MoveId.GROWL, targets: [BattlerIndex.ENEMY], useMode: MoveUseMode.NORMAL }); game.move.select(MoveId.LAST_RESORT); await game.phaseInterceptor.to("TurnEndPhase"); expectLastResortFail(); // Were last resort itself counted, it would error here // Growth (3/3) - blissey.pushMoveHistory({ move: MoveId.GROWTH, targets: [BattlerIndex.PLAYER] }); + blissey.pushMoveHistory({ move: MoveId.GROWTH, targets: [BattlerIndex.PLAYER], useMode: MoveUseMode.NORMAL }); game.move.select(MoveId.LAST_RESORT); await game.phaseInterceptor.to("TurnEndPhase"); expect(game.scene.getPlayerPokemon()?.getLastXMoves()[0]).toEqual( @@ -117,11 +118,12 @@ describe("Moves - Last Resort", () => { expect.objectContaining({ move: MoveId.LAST_RESORT, result: MoveResult.SUCCESS, - virtual: true, + useMode: MoveUseMode.FOLLOW_UP, }), expect.objectContaining({ move: MoveId.SLEEP_TALK, result: MoveResult.SUCCESS, + useMode: MoveUseMode.NORMAL, }), ]); }); diff --git a/test/moves/last_respects.test.ts b/test/moves/last_respects.test.ts index b2eb4c6695b..14aa5ad3309 100644 --- a/test/moves/last_respects.test.ts +++ b/test/moves/last_respects.test.ts @@ -32,7 +32,7 @@ describe("Moves - Last Respects", () => { basePower = move.power; game.override .battleStyle("single") - .disableCrits() + .criticalHits(false) .moveset([MoveId.LAST_RESPECTS, MoveId.EXPLOSION, MoveId.LUNAR_DANCE]) .ability(AbilityId.BALL_FETCH) .enemyAbility(AbilityId.BALL_FETCH) @@ -164,15 +164,13 @@ describe("Moves - Last Respects", () => { await game.toNextWave(); expect(game.scene.currentBattle.enemyFaints).toBe(0); + game.removeEnemyHeldItems(); + game.move.select(MoveId.LAST_RESPECTS); await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.phaseInterceptor.to("MoveEndPhase"); - const enemy = game.field.getEnemyPokemon(); - const player = game.field.getPlayerPokemon(); - const items = `Player items: ${player.getHeldItems()} | Enemy Items: ${enemy.getHeldItems()} |`; - - expect(move.calculateBattlePower, items).toHaveLastReturnedWith(50); + expect(move.calculateBattlePower).toHaveLastReturnedWith(50); }); it("should reset playerFaints count if we enter new trainer battle", async () => { diff --git a/test/moves/light_screen.test.ts b/test/moves/light_screen.test.ts index 404d30920c7..8c436aacf6a 100644 --- a/test/moves/light_screen.test.ts +++ b/test/moves/light_screen.test.ts @@ -41,7 +41,7 @@ describe("Moves - Light Screen", () => { .enemyLevel(100) .enemySpecies(SpeciesId.MAGIKARP) .enemyMoveset(MoveId.LIGHT_SCREEN) - .disableCrits(); + .criticalHits(false); }); it("reduces damage of special attacks by half in a single battle", async () => { diff --git a/test/moves/lucky_chant.test.ts b/test/moves/lucky_chant.test.ts index 58944027398..a57d231134b 100644 --- a/test/moves/lucky_chant.test.ts +++ b/test/moves/lucky_chant.test.ts @@ -1,11 +1,10 @@ import { AbilityId } from "#enums/ability-id"; -import { BattlerTagType } from "#app/enums/battler-tag-type"; +import { BattlerTagType } from "#enums/battler-tag-type"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; -import { BerryPhase } from "#app/phases/berry-phase"; -import { TurnEndPhase } from "#app/phases/turn-end-phase"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import GameManager from "#test/testUtils/gameManager"; +import { BattlerIndex } from "#enums/battler-index"; describe("Moves - Lucky Chant", () => { let phaserGame: Phaser.Game; @@ -29,74 +28,91 @@ describe("Moves - Lucky Chant", () => { .moveset([MoveId.LUCKY_CHANT, MoveId.SPLASH, MoveId.FOLLOW_ME]) .enemySpecies(SpeciesId.SNORLAX) .enemyAbility(AbilityId.INSOMNIA) - .enemyMoveset([MoveId.FLOWER_TRICK]) + .enemyMoveset(MoveId.TACKLE) .startingLevel(100) .enemyLevel(100); }); - it("should prevent critical hits from moves", async () => { + it("should prevent random critical hits from moves", async () => { + game.override.criticalHits(true); await game.classicMode.startBattle([SpeciesId.CHARIZARD]); - const playerPokemon = game.scene.getPlayerPokemon()!; + const charizard = game.scene.getPlayerPokemon()!; + expect(charizard).toBeDefined(); + const critSpy = vi.spyOn(charizard, "getCriticalHitResult"); // called on the defender (ie player) game.move.select(MoveId.SPLASH); + await game.phaseInterceptor.to("TurnEndPhase"); - await game.phaseInterceptor.to(TurnEndPhase); - - const firstTurnDamage = playerPokemon.getMaxHp() - playerPokemon.hp; + expect(critSpy).toHaveLastReturnedWith(true); + const firstTurnDamage = charizard.getInverseHp(); game.move.select(MoveId.LUCKY_CHANT); + await game.phaseInterceptor.to("TurnEndPhase"); - await game.phaseInterceptor.to(BerryPhase, false); + expect(critSpy).toHaveLastReturnedWith(false); + const secondTurnDamage = charizard.getInverseHp() - firstTurnDamage; + expect(secondTurnDamage).toBeLessThan(firstTurnDamage); + }); - const secondTurnDamage = playerPokemon.getMaxHp() - playerPokemon.hp - firstTurnDamage; + it("should prevent guaranteed critical hits from moves", async () => { + game.override.enemyMoveset(MoveId.FLOWER_TRICK); + await game.classicMode.startBattle([SpeciesId.CHARIZARD]); + + const charizard = game.scene.getPlayerPokemon()!; + expect(charizard).toBeDefined(); + + game.move.select(MoveId.SPLASH); + await game.phaseInterceptor.to("TurnEndPhase"); + + const firstTurnDamage = charizard.getInverseHp(); + + game.move.select(MoveId.LUCKY_CHANT); + await game.phaseInterceptor.to("TurnEndPhase"); + + const secondTurnDamage = charizard.getInverseHp() - firstTurnDamage; expect(secondTurnDamage).toBeLessThan(firstTurnDamage); }); it("should prevent critical hits against the user's ally", async () => { - game.override.battleStyle("double"); + game.override.battleStyle("double").criticalHits(true); await game.classicMode.startBattle([SpeciesId.CHARIZARD, SpeciesId.BLASTOISE]); - const playerPokemon = game.scene.getPlayerField(); + const charizard = game.scene.getPlayerPokemon()!; + expect(charizard).toBeDefined(); - game.move.select(MoveId.FOLLOW_ME); - game.move.select(MoveId.SPLASH, 1); + game.move.select(MoveId.FOLLOW_ME, BattlerIndex.PLAYER); + game.move.select(MoveId.SPLASH, BattlerIndex.PLAYER_2); + await game.phaseInterceptor.to("TurnEndPhase"); - await game.phaseInterceptor.to(TurnEndPhase); + const firstTurnDamage = charizard.getInverseHp(); - const firstTurnDamage = playerPokemon[0].getMaxHp() - playerPokemon[0].hp; + game.move.select(MoveId.FOLLOW_ME, BattlerIndex.PLAYER); + game.move.select(MoveId.LUCKY_CHANT, BattlerIndex.PLAYER_2); - game.move.select(MoveId.FOLLOW_ME); - game.move.select(MoveId.LUCKY_CHANT, 1); + await game.phaseInterceptor.to("TurnEndPhase"); - await game.phaseInterceptor.to(BerryPhase, false); - - const secondTurnDamage = playerPokemon[0].getMaxHp() - playerPokemon[0].hp - firstTurnDamage; + const secondTurnDamage = charizard.getInverseHp() - firstTurnDamage; expect(secondTurnDamage).toBeLessThan(firstTurnDamage); }); it("should prevent critical hits from field effects", async () => { - game.override.enemyMoveset([MoveId.TACKLE]); - await game.classicMode.startBattle([SpeciesId.CHARIZARD]); - const playerPokemon = game.scene.getPlayerPokemon()!; - const enemyPokemon = game.scene.getEnemyPokemon()!; - - enemyPokemon.addTag(BattlerTagType.ALWAYS_CRIT, 2, MoveId.NONE, 0); + const charizard = game.field.getPlayerPokemon(); + const snorlax = game.field.getEnemyPokemon(); + snorlax.addTag(BattlerTagType.ALWAYS_CRIT, 2, MoveId.NONE, 0); game.move.select(MoveId.SPLASH); + await game.phaseInterceptor.to("TurnEndPhase"); - await game.phaseInterceptor.to(TurnEndPhase); - - const firstTurnDamage = playerPokemon.getMaxHp() - playerPokemon.hp; + const firstTurnDamage = charizard.getInverseHp(); game.move.select(MoveId.LUCKY_CHANT); + await game.phaseInterceptor.to("TurnEndPhase"); - await game.phaseInterceptor.to(BerryPhase, false); - - const secondTurnDamage = playerPokemon.getMaxHp() - playerPokemon.hp - firstTurnDamage; + const secondTurnDamage = charizard.getInverseHp() - firstTurnDamage; expect(secondTurnDamage).toBeLessThan(firstTurnDamage); }); }); diff --git a/test/moves/lunar_blessing.test.ts b/test/moves/lunar_blessing.test.ts index 59d0c662859..2709ccbacf3 100644 --- a/test/moves/lunar_blessing.test.ts +++ b/test/moves/lunar_blessing.test.ts @@ -22,14 +22,13 @@ describe("Moves - Lunar Blessing", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleStyle("double"); - - game.override.enemySpecies(SpeciesId.SHUCKLE); - game.override.enemyMoveset(MoveId.SPLASH); - game.override.enemyAbility(AbilityId.BALL_FETCH); - - game.override.moveset([MoveId.LUNAR_BLESSING, MoveId.SPLASH]); - game.override.ability(AbilityId.BALL_FETCH); + game.override + .battleStyle("double") + .enemySpecies(SpeciesId.SHUCKLE) + .enemyMoveset(MoveId.SPLASH) + .enemyAbility(AbilityId.BALL_FETCH) + .moveset([MoveId.LUNAR_BLESSING, MoveId.SPLASH]) + .ability(AbilityId.BALL_FETCH); }); it("should restore 25% HP of the user and its ally", async () => { diff --git a/test/moves/magic_coat.test.ts b/test/moves/magic_coat.test.ts index a20aaf38043..566c206f1ba 100644 --- a/test/moves/magic_coat.test.ts +++ b/test/moves/magic_coat.test.ts @@ -31,7 +31,7 @@ describe("Moves - Magic Coat", () => { game.override .ability(AbilityId.BALL_FETCH) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.MAGIC_COAT); @@ -56,8 +56,7 @@ describe("Moves - Magic Coat", () => { }); it("should not reflect moves used on the next turn", async () => { - game.override.moveset([MoveId.GROWL, MoveId.SPLASH]); - game.override.enemyMoveset([MoveId.MAGIC_COAT, MoveId.SPLASH]); + game.override.moveset([MoveId.GROWL, MoveId.SPLASH]).enemyMoveset([MoveId.MAGIC_COAT, MoveId.SPLASH]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); // turn 1 @@ -82,8 +81,7 @@ describe("Moves - Magic Coat", () => { }); it("should individually bounce back multi-target moves when used by both targets in doubles", async () => { - game.override.battleStyle("double"); - game.override.moveset([MoveId.GROWL, MoveId.SPLASH]); + game.override.battleStyle("double").moveset([MoveId.GROWL, MoveId.SPLASH]); await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MAGIKARP]); game.move.select(MoveId.GROWL, 0); @@ -95,9 +93,10 @@ describe("Moves - Magic Coat", () => { }); it("should bounce back a spread status move against both pokemon", async () => { - game.override.battleStyle("double"); - game.override.moveset([MoveId.GROWL, MoveId.SPLASH]); - game.override.enemyMoveset([MoveId.SPLASH, MoveId.MAGIC_COAT]); + game.override + .battleStyle("double") + .moveset([MoveId.GROWL, MoveId.SPLASH]) + .enemyMoveset([MoveId.SPLASH, MoveId.MAGIC_COAT]); await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MAGIKARP]); game.move.select(MoveId.GROWL, 0); @@ -121,10 +120,11 @@ describe("Moves - Magic Coat", () => { }); it("should not bounce back a move that was just bounced", async () => { - game.override.battleStyle("double"); - game.override.ability(AbilityId.MAGIC_BOUNCE); - game.override.moveset([MoveId.GROWL, MoveId.MAGIC_COAT]); - game.override.enemyMoveset([MoveId.SPLASH, MoveId.MAGIC_COAT]); + game.override + .battleStyle("double") + .ability(AbilityId.MAGIC_BOUNCE) + .moveset([MoveId.GROWL, MoveId.MAGIC_COAT]) + .enemyMoveset([MoveId.SPLASH, MoveId.MAGIC_COAT]); await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MAGIKARP]); game.move.select(MoveId.MAGIC_COAT, 0); @@ -147,8 +147,7 @@ describe("Moves - Magic Coat", () => { }); it("should still bounce back a move from a mold breaker user", async () => { - game.override.ability(AbilityId.MOLD_BREAKER); - game.override.moveset([MoveId.GROWL]); + game.override.ability(AbilityId.MOLD_BREAKER).moveset([MoveId.GROWL]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); game.move.select(MoveId.GROWL); @@ -159,9 +158,8 @@ describe("Moves - Magic Coat", () => { }); it("should only bounce spikes back once when both targets use magic coat in doubles", async () => { - game.override.battleStyle("double"); + game.override.battleStyle("double").moveset([MoveId.SPIKES]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); - game.override.moveset([MoveId.SPIKES]); game.move.select(MoveId.SPIKES); await game.phaseInterceptor.to("BerryPhase"); @@ -171,9 +169,8 @@ describe("Moves - Magic Coat", () => { }); it("should not bounce back curse", async () => { - game.override.starterSpecies(SpeciesId.GASTLY); - await game.classicMode.startBattle([SpeciesId.GASTLY]); game.override.moveset([MoveId.CURSE]); + await game.classicMode.startBattle([SpeciesId.GASTLY]); game.move.select(MoveId.CURSE); await game.phaseInterceptor.to("BerryPhase"); @@ -183,9 +180,10 @@ describe("Moves - Magic Coat", () => { // TODO: encore is failing if the last move was virtual. it.todo("should not cause the bounced move to count for encore", async () => { - game.override.moveset([MoveId.GROWL, MoveId.ENCORE]); - game.override.enemyMoveset([MoveId.MAGIC_COAT, MoveId.TACKLE]); - game.override.enemyAbility(AbilityId.MAGIC_BOUNCE); + game.override + .moveset([MoveId.GROWL, MoveId.ENCORE]) + .enemyMoveset([MoveId.MAGIC_COAT, MoveId.TACKLE]) + .enemyAbility(AbilityId.MAGIC_BOUNCE); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); const enemyPokemon = game.scene.getEnemyPokemon()!; @@ -206,9 +204,8 @@ describe("Moves - Magic Coat", () => { // TODO: stomping tantrum should consider moves that were bounced. it.todo("should cause stomping tantrum to double in power when the last move was bounced", async () => { - game.override.battleStyle("single"); + game.override.battleStyle("single").moveset([MoveId.STOMPING_TANTRUM, MoveId.CHARM]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); - game.override.moveset([MoveId.STOMPING_TANTRUM, MoveId.CHARM]); const stomping_tantrum = allMoves[MoveId.STOMPING_TANTRUM]; vi.spyOn(stomping_tantrum, "calculateBattlePower"); @@ -249,8 +246,7 @@ describe("Moves - Magic Coat", () => { it("should respect immunities when bouncing a move", async () => { vi.spyOn(allMoves[MoveId.THUNDER_WAVE], "accuracy", "get").mockReturnValue(100); - game.override.moveset([MoveId.THUNDER_WAVE, MoveId.GROWL]); - game.override.ability(AbilityId.SOUNDPROOF); + game.override.moveset([MoveId.THUNDER_WAVE, MoveId.GROWL]).ability(AbilityId.SOUNDPROOF); await game.classicMode.startBattle([SpeciesId.PHANPY]); // Turn 1 - thunder wave immunity test diff --git a/test/moves/magnet_rise.test.ts b/test/moves/magnet_rise.test.ts index 16f449899ef..d8fc6a74225 100644 --- a/test/moves/magnet_rise.test.ts +++ b/test/moves/magnet_rise.test.ts @@ -28,7 +28,7 @@ describe("Moves - Magnet Rise", () => { .starterSpecies(SpeciesId.MAGNEZONE) .enemySpecies(SpeciesId.RATTATA) .enemyMoveset(MoveId.DRILL_RUN) - .disableCrits() + .criticalHits(false) .enemyLevel(1) .moveset([moveToUse, MoveId.SPLASH, MoveId.GRAVITY, MoveId.BATON_PASS]); }); @@ -42,7 +42,7 @@ describe("Moves - Magnet Rise", () => { const finalHp = game.scene.getPlayerParty()[0].hp; const hpLost = finalHp - startingHp; expect(hpLost).toBe(0); - }, 20000); + }); it("MAGNET RISE - Gravity", async () => { await game.classicMode.startBattle(); @@ -58,5 +58,5 @@ describe("Moves - Magnet Rise", () => { finalHp = game.scene.getPlayerParty()[0].hp; hpLost = finalHp - startingHp; expect(hpLost).not.toBe(0); - }, 20000); + }); }); diff --git a/test/moves/make_it_rain.test.ts b/test/moves/make_it_rain.test.ts index 0089dfb642d..13b8e8f1853 100644 --- a/test/moves/make_it_rain.test.ts +++ b/test/moves/make_it_rain.test.ts @@ -24,13 +24,14 @@ describe("Moves - Make It Rain", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleStyle("double"); - game.override.moveset([MoveId.MAKE_IT_RAIN, MoveId.SPLASH]); - game.override.enemySpecies(SpeciesId.SNORLAX); - game.override.enemyAbility(AbilityId.INSOMNIA); - game.override.enemyMoveset(MoveId.SPLASH); - game.override.startingLevel(100); - game.override.enemyLevel(100); + game.override + .battleStyle("double") + .moveset([MoveId.MAKE_IT_RAIN, MoveId.SPLASH]) + .enemySpecies(SpeciesId.SNORLAX) + .enemyAbility(AbilityId.INSOMNIA) + .enemyMoveset(MoveId.SPLASH) + .startingLevel(100) + .enemyLevel(100); }); it("should only lower SPATK stat stage by 1 once in a double battle", async () => { @@ -47,8 +48,9 @@ describe("Moves - Make It Rain", () => { }); it("should apply effects even if the target faints", async () => { - game.override.enemyLevel(1); // ensures the enemy will faint - game.override.battleStyle("single"); + game.override + .enemyLevel(1) // ensures the enemy will faint + .battleStyle("single"); await game.classicMode.startBattle([SpeciesId.CHARIZARD]); diff --git a/test/moves/mat_block.test.ts b/test/moves/mat_block.test.ts index c512503c9c4..d77f538a973 100644 --- a/test/moves/mat_block.test.ts +++ b/test/moves/mat_block.test.ts @@ -26,16 +26,14 @@ describe("Moves - Mat Block", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleStyle("double"); - - game.override.moveset([MoveId.MAT_BLOCK, MoveId.SPLASH]); - - game.override.enemySpecies(SpeciesId.SNORLAX); - game.override.enemyMoveset([MoveId.TACKLE]); - game.override.enemyAbility(AbilityId.INSOMNIA); - - game.override.startingLevel(100); - game.override.enemyLevel(100); + game.override + .battleStyle("double") + .moveset([MoveId.MAT_BLOCK, MoveId.SPLASH]) + .enemySpecies(SpeciesId.SNORLAX) + .enemyMoveset([MoveId.TACKLE]) + .enemyAbility(AbilityId.INSOMNIA) + .startingLevel(100) + .enemyLevel(100); }); test("should protect the user and allies from attack moves", async () => { diff --git a/test/moves/metal_burst.test.ts b/test/moves/metal_burst.test.ts index bcb9d938985..4d07d59c16d 100644 --- a/test/moves/metal_burst.test.ts +++ b/test/moves/metal_burst.test.ts @@ -28,7 +28,7 @@ describe("Moves - Metal Burst", () => { .ability(AbilityId.PURE_POWER) .startingLevel(10) .battleStyle("double") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.PICHU) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.TACKLE); diff --git a/test/moves/metronome.test.ts b/test/moves/metronome.test.ts index ec610eeed45..670f7bb9b88 100644 --- a/test/moves/metronome.test.ts +++ b/test/moves/metronome.test.ts @@ -1,9 +1,13 @@ +import { BattlerIndex } from "#enums/battler-index"; import { RechargingTag, SemiInvulnerableTag } from "#app/data/battler-tags"; import type { RandomMoveAttr } from "#app/data/moves/move"; import { allMoves } from "#app/data/data-lists"; import { AbilityId } from "#enums/ability-id"; import { Stat } from "#app/enums/stat"; +import { MoveResult } from "#enums/move-result"; import { CommandPhase } from "#app/phases/command-phase"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { MoveUseMode } from "#enums/move-use-mode"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import GameManager from "#test/testUtils/gameManager"; @@ -33,7 +37,6 @@ describe("Moves - Metronome", () => { .moveset([MoveId.METRONOME, MoveId.SPLASH]) .battleStyle("single") .startingLevel(100) - .starterSpecies(SpeciesId.REGIELEKI) .enemyLevel(100) .enemySpecies(SpeciesId.SHUCKLE) .enemyMoveset(MoveId.SPLASH) @@ -41,7 +44,7 @@ describe("Moves - Metronome", () => { }); it("should have one semi-invulnerable turn and deal damage on the second turn when a semi-invulnerable move is called", async () => { - await game.classicMode.startBattle(); + await game.classicMode.startBattle([SpeciesId.REGIELEKI]); const player = game.scene.getPlayerPokemon()!; const enemy = game.scene.getEnemyPokemon()!; vi.spyOn(randomMoveAttr, "getMoveOverride").mockReturnValue(MoveId.DIVE); @@ -57,7 +60,7 @@ describe("Moves - Metronome", () => { }); it("should apply secondary effects of a move", async () => { - await game.classicMode.startBattle(); + await game.classicMode.startBattle([SpeciesId.REGIELEKI]); const player = game.scene.getPlayerPokemon()!; vi.spyOn(randomMoveAttr, "getMoveOverride").mockReturnValue(MoveId.WOOD_HAMMER); @@ -68,7 +71,7 @@ describe("Moves - Metronome", () => { }); it("should recharge after using recharge move", async () => { - await game.classicMode.startBattle(); + await game.classicMode.startBattle([SpeciesId.REGIELEKI]); const player = game.scene.getPlayerPokemon()!; vi.spyOn(randomMoveAttr, "getMoveOverride").mockReturnValue(MoveId.HYPER_BEAM); vi.spyOn(allMoves[MoveId.HYPER_BEAM], "accuracy", "get").mockReturnValue(100); @@ -79,6 +82,41 @@ describe("Moves - Metronome", () => { expect(player.getTag(RechargingTag)).toBeTruthy(); }); + it("should charge for charging moves while still maintaining follow-up status", async () => { + game.override.moveset([]).enemyMoveset(MoveId.SPITE); + vi.spyOn(randomMoveAttr, "getMoveOverride").mockReturnValue(MoveId.SOLAR_BEAM); + await game.classicMode.startBattle([SpeciesId.REGIELEKI]); + + const player = game.field.getPlayerPokemon(); + game.move.changeMoveset(player, [MoveId.METRONOME, MoveId.SOLAR_BEAM]); + + const [metronomeMove, solarBeamMove] = player.getMoveset(); + expect(metronomeMove).toBeDefined(); + expect(solarBeamMove).toBeDefined(); + + game.move.select(MoveId.METRONOME); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(player.getTag(BattlerTagType.CHARGING)).toBeTruthy(); + const turn1PpUsed = metronomeMove.ppUsed; + expect.soft(turn1PpUsed).toBeGreaterThan(1); + expect(solarBeamMove.ppUsed).toBe(0); + + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); + await game.toNextTurn(); + + expect(player.getTag(BattlerTagType.CHARGING)).toBeFalsy(); + const turn2PpUsed = metronomeMove.ppUsed - turn1PpUsed; + expect(turn2PpUsed).toBeGreaterThan(1); + expect(solarBeamMove.ppUsed).toBe(0); + expect(player.getLastXMoves()[0]).toMatchObject({ + move: MoveId.SOLAR_BEAM, + result: MoveResult.SUCCESS, + useMode: MoveUseMode.FOLLOW_UP, + }); + }); + it("should only target ally for Aromatic Mist", async () => { game.override.battleStyle("double"); await game.classicMode.startBattle([SpeciesId.REGIELEKI, SpeciesId.RATTATA]); @@ -98,7 +136,7 @@ describe("Moves - Metronome", () => { }); it("should cause opponent to flee, and not crash for Roar", async () => { - await game.classicMode.startBattle(); + await game.classicMode.startBattle([SpeciesId.REGIELEKI]); vi.spyOn(randomMoveAttr, "getMoveOverride").mockReturnValue(MoveId.ROAR); const enemyPokemon = game.scene.getEnemyPokemon()!; diff --git a/test/moves/miracle_eye.test.ts b/test/moves/miracle_eye.test.ts index 1238ae6a650..9281931621f 100644 --- a/test/moves/miracle_eye.test.ts +++ b/test/moves/miracle_eye.test.ts @@ -23,7 +23,7 @@ describe("Moves - Miracle Eye", () => { beforeEach(() => { game = new GameManager(phaserGame); game.override - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.UMBREON) .enemyMoveset(MoveId.SPLASH) .enemyLevel(5) diff --git a/test/moves/mirror_move.test.ts b/test/moves/mirror_move.test.ts index fe40402e5a2..03de93c56dc 100644 --- a/test/moves/mirror_move.test.ts +++ b/test/moves/mirror_move.test.ts @@ -28,7 +28,7 @@ describe("Moves - Mirror Move", () => { .moveset([MoveId.MIRROR_MOVE, MoveId.SPLASH]) .ability(AbilityId.BALL_FETCH) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH); diff --git a/test/moves/mist.test.ts b/test/moves/mist.test.ts index ea4eb95a1eb..4191aae0a46 100644 --- a/test/moves/mist.test.ts +++ b/test/moves/mist.test.ts @@ -26,7 +26,7 @@ describe("Moves - Mist", () => { .moveset([MoveId.MIST, MoveId.SPLASH]) .ability(AbilityId.BALL_FETCH) .battleStyle("double") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.SNORLAX) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.GROWL); diff --git a/test/moves/moongeist_beam.test.ts b/test/moves/moongeist_beam.test.ts index 28bd3f70daa..367fe67cff5 100644 --- a/test/moves/moongeist_beam.test.ts +++ b/test/moves/moongeist_beam.test.ts @@ -27,7 +27,7 @@ describe("Moves - Moongeist Beam", () => { .ability(AbilityId.BALL_FETCH) .startingLevel(200) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.STURDY) .enemyMoveset(MoveId.SPLASH); diff --git a/test/moves/multi_target.test.ts b/test/moves/multi_target.test.ts index 139b669da7b..bc6b24c474d 100644 --- a/test/moves/multi_target.test.ts +++ b/test/moves/multi_target.test.ts @@ -24,7 +24,7 @@ describe("Multi-target damage reduction", () => { beforeEach(() => { game = new GameManager(phaserGame); game.override - .disableCrits() + .criticalHits(false) .battleStyle("double") .enemyLevel(100) .startingLevel(100) diff --git a/test/moves/order_up.test.ts b/test/moves/order_up.test.ts index adf37c0719e..fc505809fe6 100644 --- a/test/moves/order_up.test.ts +++ b/test/moves/order_up.test.ts @@ -30,7 +30,7 @@ describe("Moves - Order Up", () => { .moveset(MoveId.ORDER_UP) .ability(AbilityId.COMMANDER) .battleStyle("double") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.SNORLAX) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH) diff --git a/test/moves/parting_shot.test.ts b/test/moves/parting_shot.test.ts index f7a1b86fb34..fdeab6bfc7c 100644 --- a/test/moves/parting_shot.test.ts +++ b/test/moves/parting_shot.test.ts @@ -26,11 +26,12 @@ describe("Moves - Parting Shot", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleStyle("single"); - game.override.moveset([MoveId.PARTING_SHOT, MoveId.SPLASH]); - game.override.enemyMoveset(MoveId.SPLASH); - game.override.startingLevel(5); - game.override.enemyLevel(5); + game.override + .battleStyle("single") + .moveset([MoveId.PARTING_SHOT, MoveId.SPLASH]) + .enemyMoveset(MoveId.SPLASH) + .startingLevel(5) + .enemyLevel(5); }); test("Parting Shot when buffed by prankster should fail against dark types", async () => { diff --git a/test/moves/pollen_puff.test.ts b/test/moves/pollen_puff.test.ts index e6dcd2c41d0..cd0a138e96c 100644 --- a/test/moves/pollen_puff.test.ts +++ b/test/moves/pollen_puff.test.ts @@ -26,7 +26,7 @@ describe("Moves - Pollen Puff", () => { .moveset([MoveId.POLLEN_PUFF]) .ability(AbilityId.BALL_FETCH) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH); diff --git a/test/moves/powder.test.ts b/test/moves/powder.test.ts index 38e35d60335..8f47f2ff428 100644 --- a/test/moves/powder.test.ts +++ b/test/moves/powder.test.ts @@ -1,12 +1,10 @@ import { BattlerIndex } from "#enums/battler-index"; -import { PokemonMove } from "#app/data/moves/pokemon-move"; import { MoveResult } from "#enums/move-result"; import { BerryPhase } from "#app/phases/berry-phase"; -import { MoveEffectPhase } from "#app/phases/move-effect-phase"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; -import { PokemonType } from "#enums/pokemon-type"; import { SpeciesId } from "#enums/species-id"; +import { PokemonType } from "#enums/pokemon-type"; import { StatusEffect } from "#enums/status-effect"; import GameManager from "#test/testUtils/gameManager"; import Phaser from "phaser"; @@ -28,9 +26,8 @@ describe("Moves - Powder", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleStyle("single"); - game.override + .battleStyle("single") .enemySpecies(SpeciesId.SNORLAX) .enemyLevel(100) .enemyMoveset(MoveId.EMBER) @@ -45,14 +42,14 @@ describe("Moves - Powder", () => { await game.classicMode.startBattle([SpeciesId.CHARIZARD]); const enemyPokemon = game.scene.getEnemyPokemon()!; - enemyPokemon.moveset = [new PokemonMove(MoveId.EMBER)]; + game.move.changeMoveset(enemyPokemon, MoveId.EMBER); game.move.select(MoveId.POWDER); await game.phaseInterceptor.to(BerryPhase, false); expect(enemyPokemon.getLastXMoves()[0].result).toBe(MoveResult.FAIL); expect(enemyPokemon.hp).toBe(Math.ceil((3 * enemyPokemon.getMaxHp()) / 4)); - expect(enemyPokemon.moveset[0]!.ppUsed).toBe(1); + expect(enemyPokemon.moveset[0].ppUsed).toBe(1); await game.toNextTurn(); @@ -61,7 +58,7 @@ describe("Moves - Powder", () => { await game.phaseInterceptor.to(BerryPhase, false); expect(enemyPokemon.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); expect(enemyPokemon.hp).toBe(Math.ceil((3 * enemyPokemon.getMaxHp()) / 4)); - expect(enemyPokemon.moveset[0]!.ppUsed).toBe(2); + expect(enemyPokemon.moveset[0].ppUsed).toBe(2); }); it("should have no effect against Grass-type Pokemon", async () => { @@ -168,10 +165,10 @@ describe("Moves - Powder", () => { game.move.select(MoveId.FIERY_DANCE, 0, BattlerIndex.ENEMY); game.move.select(MoveId.POWDER, 1, BattlerIndex.ENEMY); - await game.phaseInterceptor.to(MoveEffectPhase); + await game.phaseInterceptor.to("MoveEffectPhase"); const enemyStartingHp = enemyPokemon.hp; - await game.phaseInterceptor.to(BerryPhase, false); + await game.toEndOfTurn(); // player should not take damage expect(enemyPokemon.getLastXMoves()[0].result).toBe(MoveResult.FAIL); @@ -182,7 +179,7 @@ describe("Moves - Powder", () => { ); }); - it("should cancel Fiery Dance, then prevent it from triggering Dancer", async () => { + it("should cancel Fiery Dance and prevent it from triggering Dancer", async () => { game.override.ability(AbilityId.DANCER).enemyMoveset(MoveId.FIERY_DANCE); await game.classicMode.startBattle([SpeciesId.CHARIZARD]); diff --git a/test/moves/power_split.test.ts b/test/moves/power_split.test.ts index 6e0763fb87d..c0fcd317182 100644 --- a/test/moves/power_split.test.ts +++ b/test/moves/power_split.test.ts @@ -50,7 +50,7 @@ describe("Moves - Power Split", () => { expect(player.getStat(Stat.SPATK, false)).toBe(avgSpAtk); expect(enemy.getStat(Stat.SPATK, false)).toBe(avgSpAtk); - }, 20000); + }); it("should be idempotent", async () => { game.override.enemyMoveset([MoveId.POWER_SPLIT]); @@ -73,5 +73,5 @@ describe("Moves - Power Split", () => { expect(player.getStat(Stat.SPATK, false)).toBe(avgSpAtk); expect(enemy.getStat(Stat.SPATK, false)).toBe(avgSpAtk); - }, 20000); + }); }); diff --git a/test/moves/protect.test.ts b/test/moves/protect.test.ts index 754a3433701..601afb43ec3 100644 --- a/test/moves/protect.test.ts +++ b/test/moves/protect.test.ts @@ -28,16 +28,14 @@ describe("Moves - Protect", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleStyle("single"); - - game.override.moveset([MoveId.PROTECT]); - game.override.enemySpecies(SpeciesId.SNORLAX); - - game.override.enemyAbility(AbilityId.INSOMNIA); - game.override.enemyMoveset([MoveId.TACKLE]); - - game.override.startingLevel(100); - game.override.enemyLevel(100); + game.override + .battleStyle("single") + .moveset([MoveId.PROTECT]) + .enemySpecies(SpeciesId.SNORLAX) + .enemyAbility(AbilityId.INSOMNIA) + .enemyMoveset([MoveId.TACKLE]) + .startingLevel(100) + .enemyLevel(100); }); test("should protect the user from attacks", async () => { diff --git a/test/moves/psycho_shift.test.ts b/test/moves/psycho_shift.test.ts index f92eea5fd38..e4eb2bd8ef6 100644 --- a/test/moves/psycho_shift.test.ts +++ b/test/moves/psycho_shift.test.ts @@ -27,7 +27,7 @@ describe("Moves - Psycho Shift", () => { .ability(AbilityId.BALL_FETCH) .statusEffect(StatusEffect.POISON) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyLevel(20) .enemyAbility(AbilityId.SYNCHRONIZE) diff --git a/test/moves/purify.test.ts b/test/moves/purify.test.ts index 0510260b755..2e8e312f23c 100644 --- a/test/moves/purify.test.ts +++ b/test/moves/purify.test.ts @@ -25,15 +25,14 @@ describe("Moves - Purify", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleStyle("single"); - - game.override.starterSpecies(SpeciesId.PYUKUMUKU); - game.override.startingLevel(10); - game.override.moveset([MoveId.PURIFY, MoveId.SIZZLY_SLIDE]); - - game.override.enemySpecies(SpeciesId.MAGIKARP); - game.override.enemyLevel(10); - game.override.enemyMoveset([MoveId.SPLASH, MoveId.NONE, MoveId.NONE, MoveId.NONE]); + game.override + .battleStyle("single") + .starterSpecies(SpeciesId.PYUKUMUKU) + .startingLevel(10) + .moveset([MoveId.PURIFY, MoveId.SIZZLY_SLIDE]) + .enemySpecies(SpeciesId.MAGIKARP) + .enemyLevel(10) + .enemyMoveset([MoveId.SPLASH]); }); test("Purify heals opponent status effect and restores user hp", async () => { diff --git a/test/moves/quash.test.ts b/test/moves/quash.test.ts index 7c8306acd22..158c409cf05 100644 --- a/test/moves/quash.test.ts +++ b/test/moves/quash.test.ts @@ -7,6 +7,7 @@ import { MoveResult } from "#enums/move-result"; import GameManager from "#test/testUtils/gameManager"; import Phaser from "phaser"; import { describe, beforeAll, afterEach, beforeEach, it, expect } from "vitest"; +import { MoveUseMode } from "#enums/move-use-mode"; describe("Moves - Quash", () => { let phaserGame: Phaser.Game; @@ -49,8 +50,8 @@ describe("Moves - Quash", () => { it("fails if the target has already moved", async () => { await game.classicMode.startBattle([SpeciesId.ACCELGOR, SpeciesId.RATTATA]); - game.move.select(MoveId.SPLASH, 0); - game.move.select(MoveId.QUASH, 1, BattlerIndex.PLAYER); + game.move.select(MoveId.SPLASH, BattlerIndex.PLAYER); + game.move.select(MoveId.QUASH, BattlerIndex.PLAYER_2, BattlerIndex.PLAYER); await game.phaseInterceptor.to("MoveEndPhase"); await game.phaseInterceptor.to("MoveEndPhase"); @@ -58,6 +59,39 @@ describe("Moves - Quash", () => { expect(game.scene.getPlayerField()[1].getLastXMoves(1)[0].result).toBe(MoveResult.FAIL); }); + // TODO: Enable once rampaging moves and move queue are fixed. + // Currently does literally nothing because `MoveUseMode` is overridden from move queue + // within `MovePhase`, but should be enabled once that jank is removed + it.todo("should maintain PP ignore status of rampaging moves", async () => { + game.override.moveset([]); + await game.classicMode.startBattle([SpeciesId.ACCELGOR, SpeciesId.RATTATA]); + + const [accelgor, rattata] = game.scene.getPlayerField(); + expect(accelgor).toBeDefined(); + expect(rattata).toBeDefined(); + + game.move.changeMoveset(accelgor, [MoveId.SPLASH, MoveId.QUASH]); + game.move.changeMoveset(rattata, MoveId.OUTRAGE); + + game.move.select(MoveId.SPLASH, BattlerIndex.PLAYER); + game.move.select(MoveId.OUTRAGE, BattlerIndex.PLAYER_2); + await game.phaseInterceptor.to("TurnEndPhase"); + + const outrageMove = rattata.getMoveset().find(m => m.moveId === MoveId.OUTRAGE); + expect(outrageMove?.ppUsed).toBe(1); + + game.move.select(MoveId.QUASH, BattlerIndex.PLAYER, BattlerIndex.PLAYER_2); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(accelgor.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); + expect(outrageMove?.ppUsed).toBe(1); + expect(rattata.getLastXMoves()[0]).toMatchObject({ + move: MoveId.OUTRAGE, + result: MoveResult.SUCCESS, + useMode: MoveUseMode.IGNORE_PP, + }); + }); + it("makes multiple quashed targets move in speed order at the end of the turn", async () => { game.override.enemySpecies(SpeciesId.NINJASK).enemyLevel(100); diff --git a/test/moves/quick_guard.test.ts b/test/moves/quick_guard.test.ts index 49f501fb839..7146b804ffa 100644 --- a/test/moves/quick_guard.test.ts +++ b/test/moves/quick_guard.test.ts @@ -25,16 +25,14 @@ describe("Moves - Quick Guard", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleStyle("double"); - - game.override.moveset([MoveId.QUICK_GUARD, MoveId.SPLASH, MoveId.FOLLOW_ME]); - - game.override.enemySpecies(SpeciesId.SNORLAX); - game.override.enemyMoveset([MoveId.QUICK_ATTACK]); - game.override.enemyAbility(AbilityId.INSOMNIA); - - game.override.startingLevel(100); - game.override.enemyLevel(100); + game.override + .battleStyle("double") + .moveset([MoveId.QUICK_GUARD, MoveId.SPLASH, MoveId.FOLLOW_ME]) + .enemySpecies(SpeciesId.SNORLAX) + .enemyMoveset([MoveId.QUICK_ATTACK]) + .enemyAbility(AbilityId.INSOMNIA) + .startingLevel(100) + .enemyLevel(100); }); test("should protect the user and allies from priority moves", async () => { @@ -51,8 +49,7 @@ describe("Moves - Quick Guard", () => { }); test("should protect the user and allies from Prankster-boosted moves", async () => { - game.override.enemyAbility(AbilityId.PRANKSTER); - game.override.enemyMoveset([MoveId.GROWL]); + game.override.enemyAbility(AbilityId.PRANKSTER).enemyMoveset([MoveId.GROWL]); await game.classicMode.startBattle([SpeciesId.CHARIZARD, SpeciesId.BLASTOISE]); @@ -84,8 +81,7 @@ describe("Moves - Quick Guard", () => { }); test("should fail if the user is the last to move in the turn", async () => { - game.override.battleStyle("single"); - game.override.enemyMoveset([MoveId.QUICK_GUARD]); + game.override.battleStyle("single").enemyMoveset([MoveId.QUICK_GUARD]); await game.classicMode.startBattle([SpeciesId.CHARIZARD]); diff --git a/test/moves/rage_powder.test.ts b/test/moves/rage_powder.test.ts index 807000a0ff0..845bff33d04 100644 --- a/test/moves/rage_powder.test.ts +++ b/test/moves/rage_powder.test.ts @@ -22,12 +22,13 @@ describe("Moves - Rage Powder", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleStyle("double"); - game.override.enemySpecies(SpeciesId.SNORLAX); - game.override.startingLevel(100); - game.override.enemyLevel(100); - game.override.moveset([MoveId.FOLLOW_ME, MoveId.RAGE_POWDER, MoveId.SPOTLIGHT, MoveId.QUICK_ATTACK]); - game.override.enemyMoveset([MoveId.RAGE_POWDER, MoveId.TACKLE, MoveId.SPLASH]); + game.override + .battleStyle("double") + .enemySpecies(SpeciesId.SNORLAX) + .startingLevel(100) + .enemyLevel(100) + .moveset([MoveId.FOLLOW_ME, MoveId.RAGE_POWDER, MoveId.SPOTLIGHT, MoveId.QUICK_ATTACK]) + .enemyMoveset([MoveId.RAGE_POWDER, MoveId.TACKLE, MoveId.SPLASH]); }); test("move effect should be bypassed by Grass type", async () => { diff --git a/test/moves/reflect.test.ts b/test/moves/reflect.test.ts index 5e10de42a3c..ef8c80070bf 100644 --- a/test/moves/reflect.test.ts +++ b/test/moves/reflect.test.ts @@ -41,7 +41,7 @@ describe("Moves - Reflect", () => { .enemyLevel(100) .enemySpecies(SpeciesId.MAGIKARP) .enemyMoveset(MoveId.REFLECT) - .disableCrits(); + .criticalHits(false); }); it("reduces damage of physical attacks by half in a single battle", async () => { diff --git a/test/moves/reflect_type.test.ts b/test/moves/reflect_type.test.ts index 86c70ed62f1..0915069764c 100644 --- a/test/moves/reflect_type.test.ts +++ b/test/moves/reflect_type.test.ts @@ -22,7 +22,11 @@ describe("Moves - Reflect Type", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.ability(AbilityId.BALL_FETCH).battleStyle("single").disableCrits().enemyAbility(AbilityId.BALL_FETCH); + game.override + .ability(AbilityId.BALL_FETCH) + .battleStyle("single") + .criticalHits(false) + .enemyAbility(AbilityId.BALL_FETCH); }); it("will make the user Normal/Grass if targetting a typeless Pokemon affected by Forest's Curse", async () => { diff --git a/test/moves/retaliate.test.ts b/test/moves/retaliate.test.ts index 16261c71dc3..dd5029a7c83 100644 --- a/test/moves/retaliate.test.ts +++ b/test/moves/retaliate.test.ts @@ -32,7 +32,7 @@ describe("Moves - Retaliate", () => { .enemyLevel(100) .moveset([MoveId.RETALIATE, MoveId.SPLASH]) .startingLevel(80) - .disableCrits(); + .criticalHits(false); }); it("increases power if ally died previous turn", async () => { diff --git a/test/moves/revival_blessing.test.ts b/test/moves/revival_blessing.test.ts index f22c0467378..1f2ba869e4f 100644 --- a/test/moves/revival_blessing.test.ts +++ b/test/moves/revival_blessing.test.ts @@ -28,7 +28,7 @@ describe("Moves - Revival Blessing", () => { .moveset([MoveId.SPLASH, MoveId.REVIVAL_BLESSING, MoveId.MEMENTO]) .ability(AbilityId.BALL_FETCH) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH); diff --git a/test/moves/role_play.test.ts b/test/moves/role_play.test.ts index 3c3f0afb1a3..cfa38ed38b7 100644 --- a/test/moves/role_play.test.ts +++ b/test/moves/role_play.test.ts @@ -26,7 +26,7 @@ describe("Moves - Role Play", () => { .moveset([MoveId.SPLASH, MoveId.ROLE_PLAY]) .ability(AbilityId.ADAPTABILITY) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH); diff --git a/test/moves/rollout.test.ts b/test/moves/rollout.test.ts index de990a1a750..9639a2e5408 100644 --- a/test/moves/rollout.test.ts +++ b/test/moves/rollout.test.ts @@ -23,18 +23,19 @@ describe("Moves - Rollout", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.disableCrits(); - game.override.battleStyle("single"); - game.override.starterSpecies(SpeciesId.RATTATA); - game.override.ability(AbilityId.BALL_FETCH); - game.override.enemySpecies(SpeciesId.BIDOOF); - game.override.enemyAbility(AbilityId.BALL_FETCH); - game.override.startingLevel(100); - game.override.enemyLevel(100); - game.override.enemyMoveset(MoveId.SPLASH); + game.override + .criticalHits(false) + .battleStyle("single") + .starterSpecies(SpeciesId.RATTATA) + .ability(AbilityId.BALL_FETCH) + .enemySpecies(SpeciesId.BIDOOF) + .enemyAbility(AbilityId.BALL_FETCH) + .startingLevel(100) + .enemyLevel(100) + .enemyMoveset(MoveId.SPLASH); }); - it("should double it's dmg on sequential uses but reset after 5", async () => { + it("should double its dmg on sequential uses but reset after 5", async () => { game.override.moveset([MoveId.ROLLOUT]); vi.spyOn(allMoves[MoveId.ROLLOUT], "accuracy", "get").mockReturnValue(100); //always hit diff --git a/test/moves/roost.test.ts b/test/moves/roost.test.ts index 1e588cb1e15..3707e0ead45 100644 --- a/test/moves/roost.test.ts +++ b/test/moves/roost.test.ts @@ -25,12 +25,13 @@ describe("Moves - Roost", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleStyle("single"); - game.override.enemySpecies(SpeciesId.RELICANTH); - game.override.startingLevel(100); - game.override.enemyLevel(100); - game.override.enemyMoveset(MoveId.EARTHQUAKE); - game.override.moveset([MoveId.ROOST, MoveId.BURN_UP, MoveId.DOUBLE_SHOCK]); + game.override + .battleStyle("single") + .enemySpecies(SpeciesId.RELICANTH) + .startingLevel(100) + .enemyLevel(100) + .enemyMoveset(MoveId.EARTHQUAKE) + .moveset([MoveId.ROOST, MoveId.BURN_UP, MoveId.DOUBLE_SHOCK]); }); /** diff --git a/test/moves/round.test.ts b/test/moves/round.test.ts index 630137b7dce..8e07a819131 100644 --- a/test/moves/round.test.ts +++ b/test/moves/round.test.ts @@ -28,7 +28,7 @@ describe("Moves - Round", () => { .moveset([MoveId.SPLASH, MoveId.ROUND]) .ability(AbilityId.BALL_FETCH) .battleStyle("double") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset([MoveId.SPLASH, MoveId.ROUND]) diff --git a/test/moves/safeguard.test.ts b/test/moves/safeguard.test.ts index 2ba53016833..8d5303e3feb 100644 --- a/test/moves/safeguard.test.ts +++ b/test/moves/safeguard.test.ts @@ -1,13 +1,12 @@ import { BattlerIndex } from "#enums/battler-index"; -import { PostDefendContactApplyStatusEffectAbAttr } from "#app/data/abilities/ability"; import { allAbilities } from "#app/data/data-lists"; -import { AbilityId } from "#enums/ability-id"; import { StatusEffect } from "#app/enums/status-effect"; import GameManager from "#test/testUtils/gameManager"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { AbilityId } from "#enums/ability-id"; describe("Moves - Safeguard", () => { let phaserGame: Phaser.Game; @@ -110,49 +109,50 @@ describe("Moves - Safeguard", () => { game.move.select(MoveId.SPLASH); await game.toNextTurn(); - expect(enemyPokemon.status?.effect).toEqual(StatusEffect.SLEEP); + expect(enemyPokemon.status?.effect).toBe(StatusEffect.SLEEP); }); - it("doesn't protect from self-inflicted via Rest or Flame Orb", async () => { + it("doesn't protect from self-inflicted status from Rest or Flame Orb", async () => { game.override.enemyHeldItems([{ name: "FLAME_ORB" }]); await game.classicMode.startBattle(); const enemyPokemon = game.scene.getEnemyPokemon()!; + enemyPokemon.hp = 1; game.move.select(MoveId.SPLASH); - await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + await game.move.forceEnemyMove(MoveId.SAFEGUARD); await game.toNextTurn(); - enemyPokemon.damageAndUpdate(1); - expect(enemyPokemon.status?.effect).toEqual(StatusEffect.BURN); + expect(enemyPokemon.status?.effect).toBe(StatusEffect.BURN); + + enemyPokemon.resetStatus(); - game.override.enemyMoveset([MoveId.REST]); - // Force the moveset to update mid-battle - // TODO: Remove after enemy AI rework is in - enemyPokemon.getMoveset(); game.move.select(MoveId.SPLASH); - enemyPokemon.damageAndUpdate(1); + await game.move.forceEnemyMove(MoveId.REST); await game.toNextTurn(); - expect(enemyPokemon.status?.effect).toEqual(StatusEffect.SLEEP); + expect(enemyPokemon.status?.effect).toBe(StatusEffect.SLEEP); }); it("protects from ability-inflicted status", async () => { - game.override.ability(AbilityId.STATIC); + await game.classicMode.startBattle(); + + const player = game.field.getPlayerPokemon(); + game.field.mockAbility(player, AbilityId.STATIC); vi.spyOn( - allAbilities[AbilityId.STATIC].getAttrs(PostDefendContactApplyStatusEffectAbAttr)[0], + allAbilities[AbilityId.STATIC].getAttrs("PostDefendContactApplyStatusEffectAbAttr")[0], "chance", "get", ).mockReturnValue(100); - await game.classicMode.startBattle(); - const enemyPokemon = game.scene.getEnemyPokemon()!; game.move.select(MoveId.SPLASH); - await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); - await game.toNextTurn(); - game.override.enemyMoveset([MoveId.TACKLE]); - game.move.select(MoveId.SPLASH); + await game.move.forceEnemyMove(MoveId.SAFEGUARD); await game.toNextTurn(); + game.move.select(MoveId.SPLASH); + await game.move.forceEnemyMove(MoveId.TACKLE); + await game.toNextTurn(); + + const enemyPokemon = game.field.getEnemyPokemon(); expect(enemyPokemon.status).toBeUndefined(); }); }); diff --git a/test/moves/scale_shot.test.ts b/test/moves/scale_shot.test.ts index a5872579003..e2c86091378 100644 --- a/test/moves/scale_shot.test.ts +++ b/test/moves/scale_shot.test.ts @@ -31,7 +31,7 @@ describe("Moves - Scale Shot", () => { game.override .moveset([MoveId.SCALE_SHOT]) .battleStyle("single") - .disableCrits() + .criticalHits(false) .ability(AbilityId.NO_GUARD) .passiveAbility(AbilityId.SKILL_LINK) .enemyMoveset(MoveId.SPLASH) diff --git a/test/moves/secret_power.test.ts b/test/moves/secret_power.test.ts index d555431656f..adb91162ae8 100644 --- a/test/moves/secret_power.test.ts +++ b/test/moves/secret_power.test.ts @@ -11,7 +11,6 @@ import { StatusEffect } from "#enums/status-effect"; import { BattlerIndex } from "#enums/battler-index"; import { ArenaTagType } from "#enums/arena-tag-type"; import { ArenaTagSide } from "#enums/arena-tag-side"; -import { MoveEffectChanceMultiplierAbAttr } from "#app/data/abilities/ability"; import { allAbilities } from "#app/data/data-lists"; describe("Moves - Secret Power", () => { @@ -34,7 +33,7 @@ describe("Moves - Secret Power", () => { .moveset([MoveId.SECRET_POWER]) .ability(AbilityId.BALL_FETCH) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyLevel(60) .enemyAbility(AbilityId.BALL_FETCH); @@ -68,7 +67,7 @@ describe("Moves - Secret Power", () => { .battleStyle("double"); await game.classicMode.startBattle([SpeciesId.BLASTOISE, SpeciesId.CHARIZARD]); - const sereneGraceAttr = allAbilities[AbilityId.SERENE_GRACE].getAttrs(MoveEffectChanceMultiplierAbAttr)[0]; + const sereneGraceAttr = allAbilities[AbilityId.SERENE_GRACE].getAttrs("MoveEffectChanceMultiplierAbAttr")[0]; vi.spyOn(sereneGraceAttr, "canApply"); game.move.select(MoveId.WATER_PLEDGE, 0, BattlerIndex.ENEMY); diff --git a/test/moves/simple_beam.test.ts b/test/moves/simple_beam.test.ts index 94609c3c4ac..f8549944ed9 100644 --- a/test/moves/simple_beam.test.ts +++ b/test/moves/simple_beam.test.ts @@ -25,7 +25,7 @@ describe("Moves - Simple Beam", () => { .moveset([MoveId.SPLASH, MoveId.SIMPLE_BEAM]) .ability(AbilityId.BALL_FETCH) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH); diff --git a/test/moves/sketch.test.ts b/test/moves/sketch.test.ts index c6fb7b4a32a..5e9ce9a9e84 100644 --- a/test/moves/sketch.test.ts +++ b/test/moves/sketch.test.ts @@ -30,7 +30,7 @@ describe("Moves - Sketch", () => { game.override .ability(AbilityId.BALL_FETCH) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.SHUCKLE) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH); diff --git a/test/moves/skill_swap.test.ts b/test/moves/skill_swap.test.ts index c4fb6005e28..7c2e6f5fd0a 100644 --- a/test/moves/skill_swap.test.ts +++ b/test/moves/skill_swap.test.ts @@ -26,7 +26,7 @@ describe("Moves - Skill Swap", () => { .moveset([MoveId.SPLASH, MoveId.SKILL_SWAP]) .ability(AbilityId.BALL_FETCH) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH); diff --git a/test/moves/sleep_talk.test.ts b/test/moves/sleep_talk.test.ts index 1d9aec77ea2..84e9d509a03 100644 --- a/test/moves/sleep_talk.test.ts +++ b/test/moves/sleep_talk.test.ts @@ -29,7 +29,7 @@ describe("Moves - Sleep Talk", () => { .statusEffect(StatusEffect.SLEEP) .ability(AbilityId.BALL_FETCH) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH) diff --git a/test/moves/spectral_thief.test.ts b/test/moves/spectral_thief.test.ts index 808e9174caf..e7b19eda506 100644 --- a/test/moves/spectral_thief.test.ts +++ b/test/moves/spectral_thief.test.ts @@ -29,7 +29,8 @@ describe("Moves - Spectral Thief", () => { .enemyMoveset(MoveId.SPLASH) .enemyAbility(AbilityId.BALL_FETCH) .moveset([MoveId.SPECTRAL_THIEF, MoveId.SPLASH]) - .ability(AbilityId.BALL_FETCH).disableCrits; + .ability(AbilityId.BALL_FETCH) + .criticalHits(false); }); it("should steal max possible positive stat changes and ignore negative ones.", async () => { diff --git a/test/moves/speed_swap.test.ts b/test/moves/speed_swap.test.ts index aef0d981e98..3723e7db740 100644 --- a/test/moves/speed_swap.test.ts +++ b/test/moves/speed_swap.test.ts @@ -47,5 +47,5 @@ describe("Moves - Speed Swap", () => { expect(player.getStat(Stat.SPD, false)).toBe(enemySpd); expect(enemy.getStat(Stat.SPD, false)).toBe(playerSpd); - }, 20000); + }); }); diff --git a/test/moves/spikes.test.ts b/test/moves/spikes.test.ts index d847f296e59..49492701d18 100644 --- a/test/moves/spikes.test.ts +++ b/test/moves/spikes.test.ts @@ -49,7 +49,7 @@ describe("Moves - Spikes", () => { const player = game.scene.getPlayerParty()[0]; expect(player.hp).toBe(player.getMaxHp()); - }, 20000); + }); it("should damage opposing pokemon that are forced to switch in", async () => { game.override.startingWave(5); @@ -63,7 +63,7 @@ describe("Moves - Spikes", () => { const enemy = game.scene.getEnemyParty()[0]; expect(enemy.hp).toBeLessThan(enemy.getMaxHp()); - }, 20000); + }); it("should damage opposing pokemon that choose to switch in", async () => { game.override.startingWave(5); @@ -78,12 +78,10 @@ describe("Moves - Spikes", () => { const enemy = game.scene.getEnemyParty()[0]; expect(enemy.hp).toBeLessThan(enemy.getMaxHp()); - }, 20000); + }); it("should work when all targets fainted", async () => { - game.override.enemySpecies(SpeciesId.DIGLETT); - game.override.battleStyle("double"); - game.override.startingLevel(50); + game.override.enemySpecies(SpeciesId.DIGLETT).battleStyle("double").startingLevel(50); await game.classicMode.startBattle([SpeciesId.RAYQUAZA, SpeciesId.ROWLET]); game.move.select(MoveId.EARTHQUAKE); @@ -91,5 +89,5 @@ describe("Moves - Spikes", () => { await game.phaseInterceptor.to("TurnEndPhase"); expect(game.scene.arena.getTagOnSide(ArenaTrapTag, ArenaTagSide.ENEMY)).toBeDefined(); - }, 20000); + }); }); diff --git a/test/moves/spit_up.test.ts b/test/moves/spit_up.test.ts index 83549c28f40..00cbe2fd0ad 100644 --- a/test/moves/spit_up.test.ts +++ b/test/moves/spit_up.test.ts @@ -2,7 +2,6 @@ import { Stat } from "#enums/stat"; import { StockpilingTag } from "#app/data/battler-tags"; import { allMoves } from "#app/data/data-lists"; import { BattlerTagType } from "#app/enums/battler-tag-type"; -import type { TurnMove } from "#app/field/pokemon"; import { MoveResult } from "#enums/move-result"; import GameManager from "#test/testUtils/gameManager"; import { AbilityId } from "#enums/ability-id"; @@ -32,15 +31,14 @@ describe("Moves - Spit Up", () => { spitUp = allMoves[MoveId.SPIT_UP]; game = new GameManager(phaserGame); - game.override.battleStyle("single"); - - game.override.enemySpecies(SpeciesId.RATTATA); - game.override.enemyMoveset(MoveId.SPLASH); - game.override.enemyAbility(AbilityId.NONE); - game.override.enemyLevel(2000); - - game.override.moveset(new Array(4).fill(spitUp.id)); - game.override.ability(AbilityId.NONE); + game.override + .battleStyle("single") + .enemySpecies(SpeciesId.RATTATA) + .enemyMoveset(MoveId.SPLASH) + .enemyAbility(AbilityId.NONE) + .enemyLevel(2000) + .moveset(MoveId.SPIT_UP) + .ability(AbilityId.NONE); vi.spyOn(spitUp, "calculateBattlePower"); }); @@ -127,7 +125,7 @@ describe("Moves - Spit Up", () => { game.move.select(MoveId.SPIT_UP); await game.phaseInterceptor.to(TurnInitPhase); - expect(pokemon.getMoveHistory().at(-1)).toMatchObject({ + expect(pokemon.getMoveHistory().at(-1)).toMatchObject({ move: MoveId.SPIT_UP, result: MoveResult.FAIL, targets: [game.scene.getEnemyPokemon()!.getBattlerIndex()], @@ -154,7 +152,7 @@ describe("Moves - Spit Up", () => { await game.phaseInterceptor.to(TurnInitPhase); - expect(pokemon.getMoveHistory().at(-1)).toMatchObject({ + expect(pokemon.getMoveHistory().at(-1)).toMatchObject({ move: MoveId.SPIT_UP, result: MoveResult.SUCCESS, targets: [game.scene.getEnemyPokemon()!.getBattlerIndex()], @@ -186,7 +184,7 @@ describe("Moves - Spit Up", () => { game.move.select(MoveId.SPIT_UP); await game.phaseInterceptor.to(TurnInitPhase); - expect(pokemon.getMoveHistory().at(-1)).toMatchObject({ + expect(pokemon.getMoveHistory().at(-1)).toMatchObject({ move: MoveId.SPIT_UP, result: MoveResult.SUCCESS, targets: [game.scene.getEnemyPokemon()!.getBattlerIndex()], diff --git a/test/moves/spotlight.test.ts b/test/moves/spotlight.test.ts index 602cedcaec9..a9e8cd7e2b6 100644 --- a/test/moves/spotlight.test.ts +++ b/test/moves/spotlight.test.ts @@ -22,13 +22,14 @@ describe("Moves - Spotlight", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleStyle("double"); - game.override.starterSpecies(SpeciesId.AMOONGUSS); - game.override.enemySpecies(SpeciesId.SNORLAX); - game.override.startingLevel(100); - game.override.enemyLevel(100); - game.override.moveset([MoveId.FOLLOW_ME, MoveId.RAGE_POWDER, MoveId.SPOTLIGHT, MoveId.QUICK_ATTACK]); - game.override.enemyMoveset([MoveId.FOLLOW_ME, MoveId.SPLASH]); + game.override + .battleStyle("double") + .starterSpecies(SpeciesId.AMOONGUSS) + .enemySpecies(SpeciesId.SNORLAX) + .startingLevel(100) + .enemyLevel(100) + .moveset([MoveId.FOLLOW_ME, MoveId.RAGE_POWDER, MoveId.SPOTLIGHT, MoveId.QUICK_ATTACK]) + .enemyMoveset([MoveId.FOLLOW_ME, MoveId.SPLASH]); }); test("move should redirect attacks to the target", async () => { diff --git a/test/moves/stockpile.test.ts b/test/moves/stockpile.test.ts index 4baa7949bc6..3ad47d8d85e 100644 --- a/test/moves/stockpile.test.ts +++ b/test/moves/stockpile.test.ts @@ -1,6 +1,5 @@ import { Stat } from "#enums/stat"; import { StockpilingTag } from "#app/data/battler-tags"; -import type { TurnMove } from "#app/field/pokemon"; import { MoveResult } from "#enums/move-result"; import { CommandPhase } from "#app/phases/command-phase"; import { TurnInitPhase } from "#app/phases/turn-init-phase"; @@ -27,15 +26,14 @@ describe("Moves - Stockpile", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleStyle("single"); - - game.override.enemySpecies(SpeciesId.RATTATA); - game.override.enemyMoveset(MoveId.SPLASH); - game.override.enemyAbility(AbilityId.NONE); - - game.override.startingLevel(2000); - game.override.moveset([MoveId.STOCKPILE, MoveId.SPLASH]); - game.override.ability(AbilityId.NONE); + game.override + .battleStyle("single") + .enemySpecies(SpeciesId.RATTATA) + .enemyMoveset(MoveId.SPLASH) + .enemyAbility(AbilityId.NONE) + .startingLevel(2000) + .moveset([MoveId.STOCKPILE, MoveId.SPLASH]) + .ability(AbilityId.NONE); }); it("gains a stockpile stack and raises user's DEF and SPDEF stat stages by 1 on each use, fails at max stacks (3)", async () => { @@ -73,7 +71,7 @@ describe("Moves - Stockpile", () => { expect(user.getStatStage(Stat.SPDEF)).toBe(3); expect(stockpilingTag).toBeDefined(); expect(stockpilingTag.stockpiledCount).toBe(3); - expect(user.getMoveHistory().at(-1)).toMatchObject({ + expect(user.getMoveHistory().at(-1)).toMatchObject({ result: MoveResult.FAIL, move: MoveId.STOCKPILE, targets: [user.getBattlerIndex()], diff --git a/test/moves/struggle.test.ts b/test/moves/struggle.test.ts index cd3c65217ed..e1373b29d33 100644 --- a/test/moves/struggle.test.ts +++ b/test/moves/struggle.test.ts @@ -25,7 +25,7 @@ describe("Moves - Struggle", () => { .moveset([MoveId.SPLASH]) .ability(AbilityId.BALL_FETCH) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH); @@ -43,8 +43,6 @@ describe("Moves - Struggle", () => { await game.phaseInterceptor.to("BerryPhase"); expect(stabSpy).toHaveReturnedWith(1); - - stabSpy.mockRestore(); }); it("should ignore type effectiveness", async () => { @@ -55,11 +53,8 @@ describe("Moves - Struggle", () => { game.move.select(MoveId.STRUGGLE); const moveEffectivenessSpy = vi.spyOn(enemy, "getMoveEffectiveness"); - await game.phaseInterceptor.to("BerryPhase"); expect(moveEffectivenessSpy).toHaveReturnedWith(1); - - moveEffectivenessSpy.mockRestore(); }); }); diff --git a/test/moves/substitute.test.ts b/test/moves/substitute.test.ts index 857f20c7fa0..182fefb74d7 100644 --- a/test/moves/substitute.test.ts +++ b/test/moves/substitute.test.ts @@ -138,8 +138,7 @@ describe("Moves - Substitute", () => { }); it("should be bypassed by attackers with Infiltrator", async () => { - game.override.enemyMoveset(MoveId.TACKLE); - game.override.enemyAbility(AbilityId.INFILTRATOR); + game.override.enemyMoveset(MoveId.TACKLE).enemyAbility(AbilityId.INFILTRATOR); await game.classicMode.startBattle([SpeciesId.BLASTOISE]); @@ -230,8 +229,7 @@ describe("Moves - Substitute", () => { }); it("should protect the user from flinching", async () => { - game.override.enemyMoveset(MoveId.FAKE_OUT); - game.override.startingLevel(1); // Ensures the Substitute will break + game.override.enemyMoveset(MoveId.FAKE_OUT).startingLevel(1); // Ensures the Substitute will break await game.classicMode.startBattle([SpeciesId.BLASTOISE]); @@ -298,9 +296,8 @@ describe("Moves - Substitute", () => { }); it("should prevent the user's items from being stolen", async () => { - game.override.enemyMoveset(MoveId.THIEF); + game.override.enemyMoveset(MoveId.THIEF).startingHeldItems([{ name: "BERRY", type: BerryType.SITRUS }]); vi.spyOn(allMoves[MoveId.THIEF], "attrs", "get").mockReturnValue([new StealHeldItemChanceAttr(1.0)]); // give Thief 100% steal rate - game.override.startingHeldItems([{ name: "BERRY", type: BerryType.SITRUS }]); await game.classicMode.startBattle([SpeciesId.BLASTOISE]); @@ -316,8 +313,7 @@ describe("Moves - Substitute", () => { }); it("should prevent the user's items from being removed", async () => { - game.override.moveset([MoveId.KNOCK_OFF]); - game.override.enemyHeldItems([{ name: "BERRY", type: BerryType.SITRUS }]); + game.override.moveset([MoveId.KNOCK_OFF]).enemyHeldItems([{ name: "BERRY", type: BerryType.SITRUS }]); await game.classicMode.startBattle([SpeciesId.BLASTOISE]); @@ -334,8 +330,7 @@ describe("Moves - Substitute", () => { }); it("move effect should prevent the user's berries from being stolen and eaten", async () => { - game.override.enemyMoveset(MoveId.BUG_BITE); - game.override.startingHeldItems([{ name: "BERRY", type: BerryType.SITRUS }]); + game.override.enemyMoveset(MoveId.BUG_BITE).startingHeldItems([{ name: "BERRY", type: BerryType.SITRUS }]); await game.classicMode.startBattle([SpeciesId.BLASTOISE]); @@ -412,8 +407,7 @@ describe("Moves - Substitute", () => { }); it("should prevent the source's Rough Skin from activating when hit", async () => { - game.override.enemyMoveset(MoveId.TACKLE); - game.override.ability(AbilityId.ROUGH_SKIN); + game.override.enemyMoveset(MoveId.TACKLE).ability(AbilityId.ROUGH_SKIN); await game.classicMode.startBattle([SpeciesId.BLASTOISE]); @@ -427,8 +421,7 @@ describe("Moves - Substitute", () => { }); it("should prevent the source's Focus Punch from failing when hit", async () => { - game.override.enemyMoveset(MoveId.TACKLE); - game.override.moveset([MoveId.FOCUS_PUNCH]); + game.override.enemyMoveset(MoveId.TACKLE).moveset([MoveId.FOCUS_PUNCH]); // Make Focus Punch 40 power to avoid a KO vi.spyOn(allMoves[MoveId.FOCUS_PUNCH], "calculateBattlePower").mockReturnValue(40); @@ -449,8 +442,7 @@ describe("Moves - Substitute", () => { }); it("should not allow Shell Trap to activate when attacked", async () => { - game.override.enemyMoveset(MoveId.TACKLE); - game.override.moveset([MoveId.SHELL_TRAP]); + game.override.enemyMoveset(MoveId.TACKLE).moveset([MoveId.SHELL_TRAP]); await game.classicMode.startBattle([SpeciesId.BLASTOISE]); @@ -466,8 +458,7 @@ describe("Moves - Substitute", () => { }); it("should not allow Beak Blast to burn opponents when hit", async () => { - game.override.enemyMoveset(MoveId.TACKLE); - game.override.moveset([MoveId.BEAK_BLAST]); + game.override.enemyMoveset(MoveId.TACKLE).moveset([MoveId.BEAK_BLAST]); await game.classicMode.startBattle([SpeciesId.BLASTOISE]); @@ -484,8 +475,7 @@ describe("Moves - Substitute", () => { }); it("should cause incoming attacks to not activate Counter", async () => { - game.override.enemyMoveset(MoveId.TACKLE); - game.override.moveset([MoveId.COUNTER]); + game.override.enemyMoveset(MoveId.TACKLE).moveset([MoveId.COUNTER]); await game.classicMode.startBattle([SpeciesId.BLASTOISE]); diff --git a/test/moves/swallow.test.ts b/test/moves/swallow.test.ts index bb95c2c593d..c511682011f 100644 --- a/test/moves/swallow.test.ts +++ b/test/moves/swallow.test.ts @@ -1,7 +1,6 @@ import { Stat } from "#enums/stat"; import { StockpilingTag } from "#app/data/battler-tags"; import { BattlerTagType } from "#app/enums/battler-tag-type"; -import type { TurnMove } from "#app/field/pokemon"; import { MoveResult } from "#enums/move-result"; import { MovePhase } from "#app/phases/move-phase"; import { TurnInitPhase } from "#app/phases/turn-init-phase"; @@ -134,7 +133,7 @@ describe("Moves - Swallow", () => { game.move.select(MoveId.SWALLOW); await game.phaseInterceptor.to(TurnInitPhase); - expect(pokemon.getMoveHistory().at(-1)).toMatchObject({ + expect(pokemon.getMoveHistory().at(-1)).toMatchObject({ move: MoveId.SWALLOW, result: MoveResult.FAIL, targets: [pokemon.getBattlerIndex()], @@ -159,7 +158,7 @@ describe("Moves - Swallow", () => { await game.phaseInterceptor.to(TurnInitPhase); - expect(pokemon.getMoveHistory().at(-1)).toMatchObject({ + expect(pokemon.getMoveHistory().at(-1)).toMatchObject({ move: MoveId.SWALLOW, result: MoveResult.SUCCESS, targets: [pokemon.getBattlerIndex()], @@ -190,7 +189,7 @@ describe("Moves - Swallow", () => { await game.phaseInterceptor.to(TurnInitPhase); - expect(pokemon.getMoveHistory().at(-1)).toMatchObject({ + expect(pokemon.getMoveHistory().at(-1)).toMatchObject({ move: MoveId.SWALLOW, result: MoveResult.SUCCESS, targets: [pokemon.getBattlerIndex()], diff --git a/test/moves/synchronoise.test.ts b/test/moves/synchronoise.test.ts index 176137c8180..9068a40693b 100644 --- a/test/moves/synchronoise.test.ts +++ b/test/moves/synchronoise.test.ts @@ -26,7 +26,7 @@ describe("Moves - Synchronoise", () => { .moveset([MoveId.SYNCHRONOISE]) .ability(AbilityId.BALL_FETCH) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH); diff --git a/test/moves/tackle.test.ts b/test/moves/tackle.test.ts index 83a267dc7e4..f3c0db85a39 100644 --- a/test/moves/tackle.test.ts +++ b/test/moves/tackle.test.ts @@ -31,19 +31,20 @@ describe("Moves - Tackle", () => { .startingWave(97) .moveset([moveToUse]) .enemyMoveset(MoveId.GROWTH) - .disableCrits(); + .criticalHits(false); }); it("TACKLE against ghost", async () => { const moveToUse = MoveId.TACKLE; game.override.enemySpecies(SpeciesId.GENGAR); + await game.classicMode.startBattle([SpeciesId.MIGHTYENA]); const hpOpponent = game.scene.currentBattle.enemyParty[0].hp; game.move.select(moveToUse); await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(TurnEndPhase); const hpLost = hpOpponent - game.scene.currentBattle.enemyParty[0].hp; expect(hpLost).toBe(0); - }, 20000); + }); it("TACKLE against not resistant", async () => { const moveToUse = MoveId.TACKLE; @@ -58,5 +59,5 @@ describe("Moves - Tackle", () => { const hpLost = hpOpponent - game.scene.currentBattle.enemyParty[0].hp; expect(hpLost).toBeGreaterThan(0); expect(hpLost).toBeLessThan(4); - }, 20000); + }); }); diff --git a/test/moves/tail_whip.test.ts b/test/moves/tail_whip.test.ts index 5118897a7cb..e98194b52dd 100644 --- a/test/moves/tail_whip.test.ts +++ b/test/moves/tail_whip.test.ts @@ -25,13 +25,14 @@ describe("Moves - Tail whip", () => { beforeEach(() => { game = new GameManager(phaserGame); const moveToUse = MoveId.TAIL_WHIP; - game.override.battleStyle("single"); - game.override.enemySpecies(SpeciesId.RATTATA); - game.override.enemyAbility(AbilityId.INSOMNIA); - game.override.ability(AbilityId.INSOMNIA); - game.override.startingLevel(2000); - game.override.moveset([moveToUse]); - game.override.enemyMoveset(MoveId.SPLASH); + game.override + .battleStyle("single") + .enemySpecies(SpeciesId.RATTATA) + .enemyAbility(AbilityId.INSOMNIA) + .ability(AbilityId.INSOMNIA) + .startingLevel(2000) + .moveset([moveToUse]) + .enemyMoveset(MoveId.SPLASH); }); it("should lower DEF stat stage by 1", async () => { @@ -45,5 +46,5 @@ describe("Moves - Tail whip", () => { await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(TurnInitPhase); expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(-1); - }, 20000); + }); }); diff --git a/test/moves/tar_shot.test.ts b/test/moves/tar_shot.test.ts index fefd1a30e1e..8d5ea4b3582 100644 --- a/test/moves/tar_shot.test.ts +++ b/test/moves/tar_shot.test.ts @@ -30,7 +30,7 @@ describe("Moves - Tar Shot", () => { .enemySpecies(SpeciesId.TANGELA) .enemyLevel(1000) .moveset([MoveId.TAR_SHOT, MoveId.FIRE_PUNCH]) - .disableCrits(); + .criticalHits(false); }); it("lowers the target's Speed stat by one stage and doubles the effectiveness of Fire-type moves used on the target", async () => { diff --git a/test/moves/tera_blast.test.ts b/test/moves/tera_blast.test.ts index 2f3bdcebfcf..fbebd428cfd 100644 --- a/test/moves/tera_blast.test.ts +++ b/test/moves/tera_blast.test.ts @@ -35,7 +35,7 @@ describe("Moves - Tera Blast", () => { game.override .battleStyle("single") - .disableCrits() + .criticalHits(false) .starterSpecies(SpeciesId.FEEBAS) .moveset([MoveId.TERA_BLAST]) .ability(AbilityId.BALL_FETCH) @@ -62,7 +62,7 @@ describe("Moves - Tera Blast", () => { await game.phaseInterceptor.to("MoveEffectPhase"); expect(spy).toHaveReturnedWith(2); - }, 20000); + }); it("increases power if user is Stellar tera type", async () => { await game.classicMode.startBattle(); diff --git a/test/moves/thousand_arrows.test.ts b/test/moves/thousand_arrows.test.ts index 20139ddad69..6721745499f 100644 --- a/test/moves/thousand_arrows.test.ts +++ b/test/moves/thousand_arrows.test.ts @@ -29,6 +29,7 @@ describe("Moves - Thousand Arrows", () => { .enemySpecies(SpeciesId.EELEKTROSS) .startingLevel(100) .enemyLevel(50) + .criticalHits(false) .ability(AbilityId.COMPOUND_EYES) .enemyAbility(AbilityId.STURDY) .moveset(MoveId.THOUSAND_ARROWS) diff --git a/test/moves/tidy_up.test.ts b/test/moves/tidy_up.test.ts index 8e79b6b130a..69c9df42b36 100644 --- a/test/moves/tidy_up.test.ts +++ b/test/moves/tidy_up.test.ts @@ -4,11 +4,11 @@ import { MoveEndPhase } from "#app/phases/move-end-phase"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; -import { SpeciesId } from "#enums/species-id"; import GameManager from "#test/testUtils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { SubstituteTag } from "#app/data/battler-tags"; +import { SpeciesId } from "#enums/species-id"; describe("Moves - Tidy Up", () => { let phaserGame: Phaser.Game; @@ -26,14 +26,15 @@ describe("Moves - Tidy Up", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleStyle("single"); - game.override.enemySpecies(SpeciesId.MAGIKARP); - game.override.enemyAbility(AbilityId.BALL_FETCH); - game.override.enemyMoveset(MoveId.SPLASH); - game.override.starterSpecies(SpeciesId.FEEBAS); - game.override.ability(AbilityId.BALL_FETCH); - game.override.moveset([MoveId.TIDY_UP]); - game.override.startingLevel(50); + game.override + .battleStyle("single") + .enemySpecies(SpeciesId.MAGIKARP) + .enemyAbility(AbilityId.BALL_FETCH) + .enemyMoveset(MoveId.SPLASH) + .starterSpecies(SpeciesId.FEEBAS) + .ability(AbilityId.BALL_FETCH) + .moveset([MoveId.TIDY_UP]) + .startingLevel(50); }); it("spikes are cleared", async () => { @@ -45,7 +46,7 @@ describe("Moves - Tidy Up", () => { game.move.select(MoveId.TIDY_UP); await game.phaseInterceptor.to(MoveEndPhase); expect(game.scene.arena.getTag(ArenaTagType.SPIKES)).toBeUndefined(); - }, 20000); + }); it("stealth rocks are cleared", async () => { game.override.moveset([MoveId.STEALTH_ROCK, MoveId.TIDY_UP]).enemyMoveset(MoveId.STEALTH_ROCK); @@ -56,7 +57,7 @@ describe("Moves - Tidy Up", () => { game.move.select(MoveId.TIDY_UP); await game.phaseInterceptor.to(MoveEndPhase); expect(game.scene.arena.getTag(ArenaTagType.STEALTH_ROCK)).toBeUndefined(); - }, 20000); + }); it("toxic spikes are cleared", async () => { game.override.moveset([MoveId.TOXIC_SPIKES, MoveId.TIDY_UP]).enemyMoveset(MoveId.TOXIC_SPIKES); @@ -67,7 +68,7 @@ describe("Moves - Tidy Up", () => { game.move.select(MoveId.TIDY_UP); await game.phaseInterceptor.to(MoveEndPhase); expect(game.scene.arena.getTag(ArenaTagType.TOXIC_SPIKES)).toBeUndefined(); - }, 20000); + }); it("sticky webs are cleared", async () => { game.override.moveset([MoveId.STICKY_WEB, MoveId.TIDY_UP]).enemyMoveset(MoveId.STICKY_WEB); @@ -79,7 +80,7 @@ describe("Moves - Tidy Up", () => { game.move.select(MoveId.TIDY_UP); await game.phaseInterceptor.to(MoveEndPhase); expect(game.scene.arena.getTag(ArenaTagType.STICKY_WEB)).toBeUndefined(); - }, 20000); + }); it("substitutes are cleared", async () => { game.override.moveset([MoveId.SUBSTITUTE, MoveId.TIDY_UP]).enemyMoveset(MoveId.SUBSTITUTE); @@ -96,7 +97,7 @@ describe("Moves - Tidy Up", () => { expect(p).toBeDefined(); expect(p!.getTag(SubstituteTag)).toBeUndefined(); }); - }, 20000); + }); it("user's stats are raised with no traps set", async () => { await game.classicMode.startBattle(); @@ -111,5 +112,5 @@ describe("Moves - Tidy Up", () => { expect(playerPokemon.getStatStage(Stat.ATK)).toBe(1); expect(playerPokemon.getStatStage(Stat.SPD)).toBe(1); - }, 20000); + }); }); diff --git a/test/moves/toxic.test.ts b/test/moves/toxic.test.ts index eb23885b4b5..a7ba52eb963 100644 --- a/test/moves/toxic.test.ts +++ b/test/moves/toxic.test.ts @@ -75,8 +75,7 @@ describe("Moves - Toxic", () => { }); it("moves other than Toxic should not hit semi-invulnerable targets even if user is Poison-type", async () => { - game.override.moveset(MoveId.SWIFT); - game.override.enemyMoveset(MoveId.FLY); + game.override.moveset(MoveId.SWIFT).enemyMoveset(MoveId.FLY); await game.classicMode.startBattle([SpeciesId.TOXAPEX]); game.move.select(MoveId.SWIFT); diff --git a/test/moves/transform.test.ts b/test/moves/transform.test.ts index 8ee65802b37..4fbaf0136ab 100644 --- a/test/moves/transform.test.ts +++ b/test/moves/transform.test.ts @@ -154,8 +154,7 @@ describe("Moves - Transform", () => { }); it("should stay transformed with the correct form after reload", async () => { - game.override.enemyMoveset([]).moveset([]); - game.override.enemySpecies(SpeciesId.DARMANITAN); + game.override.enemyMoveset([]).moveset([]).enemySpecies(SpeciesId.DARMANITAN); await game.classicMode.startBattle([SpeciesId.DITTO]); diff --git a/test/moves/trick_or_treat.test.ts b/test/moves/trick_or_treat.test.ts index 366c6ee60fe..861115e2503 100644 --- a/test/moves/trick_or_treat.test.ts +++ b/test/moves/trick_or_treat.test.ts @@ -26,7 +26,7 @@ describe("Moves - Trick Or Treat", () => { .moveset([MoveId.FORESTS_CURSE, MoveId.TRICK_OR_TREAT]) .ability(AbilityId.BALL_FETCH) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH); diff --git a/test/moves/u_turn.test.ts b/test/moves/u_turn.test.ts index 4212802298e..a2e29899ad8 100644 --- a/test/moves/u_turn.test.ts +++ b/test/moves/u_turn.test.ts @@ -29,7 +29,7 @@ describe("Moves - U-turn", () => { .startingWave(97) .moveset([MoveId.U_TURN]) .enemyMoveset(MoveId.SPLASH) - .disableCrits(); + .criticalHits(false); }); it("triggers regenerator a single time when a regenerator user switches out with u-turn", async () => { @@ -50,7 +50,7 @@ describe("Moves - U-turn", () => { ); expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase"); expect(game.scene.getPlayerPokemon()!.species.speciesId).toBe(SpeciesId.SHUCKLE); - }, 20000); + }); it("triggers rough skin on the u-turn user before a new pokemon is switched in", async () => { // arrange @@ -68,7 +68,7 @@ describe("Moves - U-turn", () => { expect(game.scene.getEnemyPokemon()!.waveData.abilityRevealed).toBe(true); // proxy for asserting ability activated expect(playerPkm.species.speciesId).toEqual(SpeciesId.RAICHU); expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase"); - }, 20000); + }); it("triggers contact abilities on the u-turn user (eg poison point) before a new pokemon is switched in", async () => { // arrange @@ -86,7 +86,7 @@ describe("Moves - U-turn", () => { expect(playerPkm.species.speciesId).toEqual(SpeciesId.RAICHU); expect(game.scene.getEnemyPokemon()!.waveData.abilityRevealed).toBe(true); // proxy for asserting ability activated expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase"); - }, 20000); + }); it("still forces a switch if u-turn KO's the opponent", async () => { game.override.startingLevel(1000); // Ensure that U-Turn KO's the opponent diff --git a/test/moves/upper_hand.test.ts b/test/moves/upper_hand.test.ts index e3d490ba6fa..fe38e75cf9a 100644 --- a/test/moves/upper_hand.test.ts +++ b/test/moves/upper_hand.test.ts @@ -27,7 +27,7 @@ describe("Moves - Upper Hand", () => { .moveset(MoveId.UPPER_HAND) .ability(AbilityId.BALL_FETCH) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.QUICK_ATTACK) diff --git a/test/moves/wide_guard.test.ts b/test/moves/wide_guard.test.ts index 65c3a0a805f..07c02158e94 100644 --- a/test/moves/wide_guard.test.ts +++ b/test/moves/wide_guard.test.ts @@ -25,16 +25,14 @@ describe("Moves - Wide Guard", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleStyle("double"); - - game.override.moveset([MoveId.WIDE_GUARD, MoveId.SPLASH, MoveId.SURF]); - - game.override.enemySpecies(SpeciesId.SNORLAX); - game.override.enemyMoveset([MoveId.SWIFT]); - game.override.enemyAbility(AbilityId.INSOMNIA); - - game.override.startingLevel(100); - game.override.enemyLevel(100); + game.override + .battleStyle("double") + .moveset([MoveId.WIDE_GUARD, MoveId.SPLASH, MoveId.SURF]) + .enemySpecies(SpeciesId.SNORLAX) + .enemyMoveset(MoveId.SWIFT) + .enemyAbility(AbilityId.INSOMNIA) + .startingLevel(100) + .enemyLevel(100); }); test("should protect the user and allies from multi-target attack moves", async () => { diff --git a/test/moves/will_o_wisp.test.ts b/test/moves/will_o_wisp.test.ts index 40495009d2a..336e84129c5 100644 --- a/test/moves/will_o_wisp.test.ts +++ b/test/moves/will_o_wisp.test.ts @@ -27,7 +27,7 @@ describe("Moves - Will-O-Wisp", () => { .moveset([MoveId.WILL_O_WISP, MoveId.SPLASH]) .ability(AbilityId.BALL_FETCH) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH); diff --git a/test/mystery-encounter/encounters/a-trainers-test-encounter.test.ts b/test/mystery-encounter/encounters/a-trainers-test-encounter.test.ts index d713db0aff8..82cac197fe9 100644 --- a/test/mystery-encounter/encounters/a-trainers-test-encounter.test.ts +++ b/test/mystery-encounter/encounters/a-trainers-test-encounter.test.ts @@ -38,10 +38,11 @@ describe("A Trainer's Test - Mystery Encounter", () => { beforeEach(async () => { game = new GameManager(phaserGame); scene = game.scene; - game.override.mysteryEncounterChance(100); - game.override.startingWave(defaultWave); - game.override.startingBiome(defaultBiome); - game.override.disableTrainerWaves(); + game.override + .mysteryEncounterChance(100) + .startingWave(defaultWave) + .startingBiome(defaultBiome) + .disableTrainerWaves(); const biomeMap = new Map([ [BiomeId.VOLCANO, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]], @@ -54,8 +55,6 @@ describe("A Trainer's Test - Mystery Encounter", () => { afterEach(() => { game.phaseInterceptor.restoreOg(); - vi.clearAllMocks(); - vi.resetAllMocks(); }); it("should have the correct properties", async () => { diff --git a/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts b/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts index f5bc1a62528..b6c4e4d85fb 100644 --- a/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts +++ b/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts @@ -38,10 +38,11 @@ describe("Absolute Avarice - Mystery Encounter", () => { beforeEach(async () => { game = new GameManager(phaserGame); scene = game.scene; - game.override.mysteryEncounterChance(100); - game.override.startingWave(defaultWave); - game.override.startingBiome(defaultBiome); - game.override.disableTrainerWaves(); + game.override + .mysteryEncounterChance(100) + .startingWave(defaultWave) + .startingBiome(defaultBiome) + .disableTrainerWaves(); vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( new Map([ @@ -53,8 +54,6 @@ describe("Absolute Avarice - Mystery Encounter", () => { afterEach(() => { game.phaseInterceptor.restoreOg(); - vi.clearAllMocks(); - vi.resetAllMocks(); }); it("should have the correct properties", async () => { @@ -71,8 +70,7 @@ describe("Absolute Avarice - Mystery Encounter", () => { }); it("should not spawn outside of proper biomes", async () => { - game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT); - game.override.startingBiome(BiomeId.VOLCANO); + game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT).startingBiome(BiomeId.VOLCANO); await game.runToMysteryEncounter(); expect(game.scene.currentBattle.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.ABSOLUTE_AVARICE); @@ -87,8 +85,7 @@ describe("Absolute Avarice - Mystery Encounter", () => { }); it("should spawn if player has enough berries", async () => { - game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT); - game.override.startingHeldItems([ + game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT).startingHeldItems([ { name: "BERRY", count: 2, type: BerryType.SITRUS }, { name: "BERRY", count: 3, type: BerryType.GANLON }, { name: "BERRY", count: 2, type: BerryType.APICOT }, @@ -139,7 +136,6 @@ describe("Absolute Avarice - Mystery Encounter", () => { expect(enemyField.length).toBe(1); expect(enemyField[0].species.speciesId).toBe(SpeciesId.GREEDENT); const moveset = enemyField[0].moveset.map(m => m.moveId); - expect(moveset?.length).toBe(4); expect(moveset).toEqual([MoveId.THRASH, MoveId.CRUNCH, MoveId.BODY_PRESS, MoveId.SLACK_OFF]); const movePhases = phaseSpy.mock.calls.filter(p => p[0] instanceof MovePhase).map(p => p[0]); diff --git a/test/mystery-encounter/encounters/an-offer-you-cant-refuse-encounter.test.ts b/test/mystery-encounter/encounters/an-offer-you-cant-refuse-encounter.test.ts index d208a859825..4f986f58b88 100644 --- a/test/mystery-encounter/encounters/an-offer-you-cant-refuse-encounter.test.ts +++ b/test/mystery-encounter/encounters/an-offer-you-cant-refuse-encounter.test.ts @@ -13,7 +13,7 @@ import { AnOfferYouCantRefuseEncounter } from "#app/data/mystery-encounters/enco import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { initSceneWithoutEncounterPhase } from "#test/testUtils/gameManagerUtils"; -import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { getPokemonSpecies } from "#app/utils/pokemon-utils"; import { MoveId } from "#enums/move-id"; import { ShinyRateBoosterModifier } from "#app/modifier/modifier"; import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; @@ -56,8 +56,6 @@ describe("An Offer You Can't Refuse - Mystery Encounter", () => { afterEach(() => { game.phaseInterceptor.restoreOg(); - vi.clearAllMocks(); - vi.resetAllMocks(); }); it("should have the correct properties", async () => { @@ -79,8 +77,7 @@ describe("An Offer You Can't Refuse - Mystery Encounter", () => { }); it("should not spawn outside of HUMAN_TRANSITABLE_BIOMES", async () => { - game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT); - game.override.startingBiome(BiomeId.VOLCANO); + game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT).startingBiome(BiomeId.VOLCANO); await game.runToMysteryEncounter(); expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe( diff --git a/test/mystery-encounter/encounters/berries-abound-encounter.test.ts b/test/mystery-encounter/encounters/berries-abound-encounter.test.ts index 566576995a5..8c2c6c608cd 100644 --- a/test/mystery-encounter/encounters/berries-abound-encounter.test.ts +++ b/test/mystery-encounter/encounters/berries-abound-encounter.test.ts @@ -57,8 +57,6 @@ describe("Berries Abound - Mystery Encounter", () => { afterEach(() => { game.phaseInterceptor.restoreOg(); - vi.clearAllMocks(); - vi.resetAllMocks(); }); it("should have the correct properties", async () => { diff --git a/test/mystery-encounter/encounters/bug-type-superfan-encounter.test.ts b/test/mystery-encounter/encounters/bug-type-superfan-encounter.test.ts index 8e58a0ca242..4da8ff7f643 100644 --- a/test/mystery-encounter/encounters/bug-type-superfan-encounter.test.ts +++ b/test/mystery-encounter/encounters/bug-type-superfan-encounter.test.ts @@ -154,10 +154,11 @@ describe("Bug-Type Superfan - Mystery Encounter", () => { beforeEach(async () => { game = new GameManager(phaserGame); scene = game.scene; - game.override.mysteryEncounterChance(100); - game.override.startingWave(defaultWave); - game.override.startingBiome(defaultBiome); - game.override.disableTrainerWaves(); + game.override + .mysteryEncounterChance(100) + .startingWave(defaultWave) + .startingBiome(defaultBiome) + .disableTrainerWaves(); vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( new Map([[BiomeId.CAVE, [MysteryEncounterType.BUG_TYPE_SUPERFAN]]]), @@ -166,8 +167,6 @@ describe("Bug-Type Superfan - Mystery Encounter", () => { afterEach(() => { game.phaseInterceptor.restoreOg(); - vi.clearAllMocks(); - vi.resetAllMocks(); }); it("should have the correct properties", async () => { diff --git a/test/mystery-encounter/encounters/clowning-around-encounter.test.ts b/test/mystery-encounter/encounters/clowning-around-encounter.test.ts index 778f8397417..85193d1ec72 100644 --- a/test/mystery-encounter/encounters/clowning-around-encounter.test.ts +++ b/test/mystery-encounter/encounters/clowning-around-encounter.test.ts @@ -4,7 +4,7 @@ import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; import { SpeciesId } from "#enums/species-id"; import GameManager from "#test/testUtils/gameManager"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { getPokemonSpecies } from "#app/utils/pokemon-utils"; import * as BattleAnims from "#app/data/battle-anims"; import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { generateModifierType } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; @@ -20,7 +20,7 @@ import { UiMode } from "#enums/ui-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { initSceneWithoutEncounterPhase } from "#test/testUtils/gameManagerUtils"; -import { ModifierTier } from "#app/modifier/modifier-tier"; +import { ModifierTier } from "#enums/modifier-tier"; import { ClowningAroundEncounter } from "#app/data/mystery-encounters/encounters/clowning-around-encounter"; import { TrainerType } from "#enums/trainer-type"; import { AbilityId } from "#enums/ability-id"; @@ -29,7 +29,7 @@ import { Button } from "#enums/buttons"; import type PartyUiHandler from "#app/ui/party-ui-handler"; import type OptionSelectUiHandler from "#app/ui/settings/option-select-ui-handler"; import type { PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; -import { modifierTypes } from "#app/modifier/modifier-type"; +import { modifierTypes } from "#app/data/data-lists"; import { BerryType } from "#enums/berry-type"; import type { PokemonHeldItemModifier } from "#app/modifier/modifier"; import { PokemonType } from "#enums/pokemon-type"; @@ -55,10 +55,11 @@ describe("Clowning Around - Mystery Encounter", () => { beforeEach(async () => { game = new GameManager(phaserGame); scene = game.scene; - game.override.mysteryEncounterChance(100); - game.override.startingWave(defaultWave); - game.override.startingBiome(defaultBiome); - game.override.disableTrainerWaves(); + game.override + .mysteryEncounterChance(100) + .startingWave(defaultWave) + .startingBiome(defaultBiome) + .disableTrainerWaves(); vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( new Map([[BiomeId.CAVE, [MysteryEncounterType.CLOWNING_AROUND]]]), @@ -67,8 +68,6 @@ describe("Clowning Around - Mystery Encounter", () => { afterEach(() => { game.phaseInterceptor.restoreOg(); - vi.clearAllMocks(); - vi.resetAllMocks(); }); it("should have the correct properties", async () => { diff --git a/test/mystery-encounter/encounters/dancing-lessons-encounter.test.ts b/test/mystery-encounter/encounters/dancing-lessons-encounter.test.ts index ee4aefd9904..e47c7cc1a42 100644 --- a/test/mystery-encounter/encounters/dancing-lessons-encounter.test.ts +++ b/test/mystery-encounter/encounters/dancing-lessons-encounter.test.ts @@ -41,10 +41,11 @@ describe("Dancing Lessons - Mystery Encounter", () => { beforeEach(async () => { game = new GameManager(phaserGame); scene = game.scene; - game.override.mysteryEncounterChance(100); - game.override.startingWave(defaultWave); - game.override.startingBiome(defaultBiome); - game.override.disableTrainerWaves(); + game.override + .mysteryEncounterChance(100) + .startingWave(defaultWave) + .startingBiome(defaultBiome) + .disableTrainerWaves(); vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( new Map([ @@ -56,8 +57,6 @@ describe("Dancing Lessons - Mystery Encounter", () => { afterEach(() => { game.phaseInterceptor.restoreOg(); - vi.clearAllMocks(); - vi.resetAllMocks(); }); it("should have the correct properties", async () => { @@ -74,8 +73,7 @@ describe("Dancing Lessons - Mystery Encounter", () => { }); it("should not spawn outside of proper biomes", async () => { - game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT); - game.override.startingBiome(BiomeId.SPACE); + game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT).startingBiome(BiomeId.SPACE); await game.runToMysteryEncounter(); expect(game.scene.currentBattle.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.DANCING_LESSONS); diff --git a/test/mystery-encounter/encounters/delibirdy-encounter.test.ts b/test/mystery-encounter/encounters/delibirdy-encounter.test.ts index 0d1094831bc..3ef8431cc2c 100644 --- a/test/mystery-encounter/encounters/delibirdy-encounter.test.ts +++ b/test/mystery-encounter/encounters/delibirdy-encounter.test.ts @@ -26,7 +26,7 @@ import { } from "#app/modifier/modifier"; import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; import { generateModifierType } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; -import { modifierTypes } from "#app/modifier/modifier-type"; +import { modifierTypes } from "#app/data/data-lists"; import { BerryType } from "#enums/berry-type"; const namespace = "mysteryEncounters/delibirdy"; @@ -46,10 +46,11 @@ describe("Delibird-y - Mystery Encounter", () => { beforeEach(async () => { game = new GameManager(phaserGame); scene = game.scene; - game.override.mysteryEncounterChance(100); - game.override.startingWave(defaultWave); - game.override.startingBiome(defaultBiome); - game.override.disableTrainerWaves(); + game.override + .mysteryEncounterChance(100) + .startingWave(defaultWave) + .startingBiome(defaultBiome) + .disableTrainerWaves(); vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( new Map([[BiomeId.CAVE, [MysteryEncounterType.DELIBIRDY]]]), @@ -58,8 +59,6 @@ describe("Delibird-y - Mystery Encounter", () => { afterEach(() => { game.phaseInterceptor.restoreOg(); - vi.clearAllMocks(); - vi.resetAllMocks(); }); it("should have the correct properties", async () => { diff --git a/test/mystery-encounter/encounters/department-store-sale-encounter.test.ts b/test/mystery-encounter/encounters/department-store-sale-encounter.test.ts index 3feb44bbe91..a82734d0c03 100644 --- a/test/mystery-encounter/encounters/department-store-sale-encounter.test.ts +++ b/test/mystery-encounter/encounters/department-store-sale-encounter.test.ts @@ -32,10 +32,11 @@ describe("Department Store Sale - Mystery Encounter", () => { beforeEach(async () => { game = new GameManager(phaserGame); scene = game.scene; - game.override.mysteryEncounterChance(100); - game.override.startingWave(defaultWave); - game.override.startingBiome(defaultBiome); - game.override.disableTrainerWaves(); + game.override + .mysteryEncounterChance(100) + .startingWave(defaultWave) + .startingBiome(defaultBiome) + .disableTrainerWaves(); const biomeMap = new Map([ [BiomeId.VOLCANO, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]], @@ -48,8 +49,6 @@ describe("Department Store Sale - Mystery Encounter", () => { afterEach(() => { game.phaseInterceptor.restoreOg(); - vi.clearAllMocks(); - vi.resetAllMocks(); }); it("should have the correct properties", async () => { @@ -74,8 +73,7 @@ describe("Department Store Sale - Mystery Encounter", () => { }); it("should not spawn outside of CIVILIZATION_ENCOUNTER_BIOMES", async () => { - game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON); - game.override.startingBiome(BiomeId.VOLCANO); + game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON).startingBiome(BiomeId.VOLCANO); await game.runToMysteryEncounter(); expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.DEPARTMENT_STORE_SALE); diff --git a/test/mystery-encounter/encounters/field-trip-encounter.test.ts b/test/mystery-encounter/encounters/field-trip-encounter.test.ts index f93de7dc955..f4b3c52eb65 100644 --- a/test/mystery-encounter/encounters/field-trip-encounter.test.ts +++ b/test/mystery-encounter/encounters/field-trip-encounter.test.ts @@ -33,11 +33,12 @@ describe("Field Trip - Mystery Encounter", () => { beforeEach(async () => { game = new GameManager(phaserGame); scene = game.scene; - game.override.mysteryEncounterChance(100); - game.override.startingWave(defaultWave); - game.override.startingBiome(defaultBiome); - game.override.disableTrainerWaves(); - game.override.moveset([MoveId.TACKLE, MoveId.UPROAR, MoveId.SWORDS_DANCE]); + game.override + .mysteryEncounterChance(100) + .startingWave(defaultWave) + .startingBiome(defaultBiome) + .disableTrainerWaves() + .moveset([MoveId.TACKLE, MoveId.UPROAR, MoveId.SWORDS_DANCE]); vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( new Map([[BiomeId.CAVE, [MysteryEncounterType.FIELD_TRIP]]]), @@ -46,8 +47,6 @@ describe("Field Trip - Mystery Encounter", () => { afterEach(() => { game.phaseInterceptor.restoreOg(); - vi.clearAllMocks(); - vi.resetAllMocks(); }); it("should have the correct properties", async () => { diff --git a/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts b/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts index 76b15106ef1..16adc47ff11 100644 --- a/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts +++ b/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts @@ -6,7 +6,7 @@ import GameManager from "#test/testUtils/gameManager"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { FieryFalloutEncounter } from "#app/data/mystery-encounters/encounters/fiery-fallout-encounter"; import { Gender } from "#app/data/gender"; -import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { getPokemonSpecies } from "#app/utils/pokemon-utils"; import * as BattleAnims from "#app/data/battle-anims"; import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { @@ -66,8 +66,6 @@ describe("Fiery Fallout - Mystery Encounter", () => { afterEach(() => { game.phaseInterceptor.restoreOg(); - vi.clearAllMocks(); - vi.resetAllMocks(); }); it("should have the correct properties", async () => { diff --git a/test/mystery-encounter/encounters/fight-or-flight-encounter.test.ts b/test/mystery-encounter/encounters/fight-or-flight-encounter.test.ts index e0e8b3d90d6..28db869004c 100644 --- a/test/mystery-encounter/encounters/fight-or-flight-encounter.test.ts +++ b/test/mystery-encounter/encounters/fight-or-flight-encounter.test.ts @@ -40,10 +40,11 @@ describe("Fight or Flight - Mystery Encounter", () => { beforeEach(async () => { game = new GameManager(phaserGame); scene = game.scene; - game.override.mysteryEncounterChance(100); - game.override.startingWave(defaultWave); - game.override.startingBiome(defaultBiome); - game.override.disableTrainerWaves(); + game.override + .mysteryEncounterChance(100) + .startingWave(defaultWave) + .startingBiome(defaultBiome) + .disableTrainerWaves(); vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( new Map([[BiomeId.CAVE, [MysteryEncounterType.FIGHT_OR_FLIGHT]]]), @@ -52,8 +53,6 @@ describe("Fight or Flight - Mystery Encounter", () => { afterEach(() => { game.phaseInterceptor.restoreOg(); - vi.clearAllMocks(); - vi.resetAllMocks(); }); it("should have the correct properties", async () => { diff --git a/test/mystery-encounter/encounters/fun-and-games-encounter.test.ts b/test/mystery-encounter/encounters/fun-and-games-encounter.test.ts index e31a3a5cc3c..ae7e269ea00 100644 --- a/test/mystery-encounter/encounters/fun-and-games-encounter.test.ts +++ b/test/mystery-encounter/encounters/fun-and-games-encounter.test.ts @@ -24,6 +24,7 @@ import { FunAndGamesEncounter } from "#app/data/mystery-encounters/encounters/fu import { MoveId } from "#enums/move-id"; import { Command } from "#enums/command"; import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { MoveUseMode } from "#enums/move-use-mode"; const namespace = "mysteryEncounters/funAndGames"; const defaultParty = [SpeciesId.LAPRAS, SpeciesId.GENGAR, SpeciesId.ABRA]; @@ -42,10 +43,11 @@ describe("Fun And Games! - Mystery Encounter", () => { beforeEach(async () => { game = new GameManager(phaserGame); scene = game.scene; - game.override.mysteryEncounterChance(100); - game.override.startingWave(defaultWave); - game.override.startingBiome(defaultBiome); - game.override.disableTrainerWaves(); + game.override + .mysteryEncounterChance(100) + .startingWave(defaultWave) + .startingBiome(defaultBiome) + .disableTrainerWaves(); const biomeMap = new Map([ [BiomeId.VOLCANO, [MysteryEncounterType.FIGHT_OR_FLIGHT]], @@ -58,8 +60,6 @@ describe("Fun And Games! - Mystery Encounter", () => { afterEach(() => { game.phaseInterceptor.restoreOg(); - vi.clearAllMocks(); - vi.resetAllMocks(); }); it("should have the correct properties", async () => { @@ -81,8 +81,7 @@ describe("Fun And Games! - Mystery Encounter", () => { }); it("should not spawn outside of CIVILIZATIONN biomes", async () => { - game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT); - game.override.startingBiome(BiomeId.VOLCANO); + game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT).startingBiome(BiomeId.VOLCANO); await game.runToMysteryEncounter(); expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.FUN_AND_GAMES); @@ -154,15 +153,15 @@ describe("Fun And Games! - Mystery Encounter", () => { }); // Turn 1 - (game.scene.phaseManager.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, false); + (game.scene.phaseManager.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, MoveUseMode.NORMAL); await game.phaseInterceptor.to(CommandPhase); // Turn 2 - (game.scene.phaseManager.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, false); + (game.scene.phaseManager.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, MoveUseMode.NORMAL); await game.phaseInterceptor.to(CommandPhase); // Turn 3 - (game.scene.phaseManager.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, false); + (game.scene.phaseManager.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, MoveUseMode.NORMAL); await game.phaseInterceptor.to(SelectModifierPhase, false); // Rewards @@ -181,7 +180,7 @@ describe("Fun And Games! - Mystery Encounter", () => { // Skip minigame scene.currentBattle.mysteryEncounter!.misc.turnsRemaining = 0; - (game.scene.phaseManager.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, false); + (game.scene.phaseManager.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, MoveUseMode.NORMAL); await game.phaseInterceptor.to(SelectModifierPhase, false); // Rewards @@ -210,7 +209,7 @@ describe("Fun And Games! - Mystery Encounter", () => { const wobbuffet = scene.getEnemyPokemon()!; wobbuffet.hp = Math.floor(0.2 * wobbuffet.getMaxHp()); scene.currentBattle.mysteryEncounter!.misc.turnsRemaining = 0; - (game.scene.phaseManager.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, false); + (game.scene.phaseManager.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, MoveUseMode.NORMAL); await game.phaseInterceptor.to(SelectModifierPhase, false); // Rewards @@ -240,7 +239,7 @@ describe("Fun And Games! - Mystery Encounter", () => { const wobbuffet = scene.getEnemyPokemon()!; wobbuffet.hp = Math.floor(0.1 * wobbuffet.getMaxHp()); scene.currentBattle.mysteryEncounter!.misc.turnsRemaining = 0; - (game.scene.phaseManager.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, false); + (game.scene.phaseManager.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, MoveUseMode.NORMAL); await game.phaseInterceptor.to(SelectModifierPhase, false); // Rewards @@ -270,7 +269,7 @@ describe("Fun And Games! - Mystery Encounter", () => { const wobbuffet = scene.getEnemyPokemon()!; wobbuffet.hp = 1; scene.currentBattle.mysteryEncounter!.misc.turnsRemaining = 0; - (game.scene.phaseManager.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, false); + (game.scene.phaseManager.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, MoveUseMode.NORMAL); await game.phaseInterceptor.to(SelectModifierPhase, false); // Rewards diff --git a/test/mystery-encounter/encounters/global-trade-system-encounter.test.ts b/test/mystery-encounter/encounters/global-trade-system-encounter.test.ts index 96c4adf67c2..bb598f4ae6e 100644 --- a/test/mystery-encounter/encounters/global-trade-system-encounter.test.ts +++ b/test/mystery-encounter/encounters/global-trade-system-encounter.test.ts @@ -11,13 +11,13 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; import { PokemonNatureWeightModifier } from "#app/modifier/modifier"; import { generateModifierType } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; -import { modifierTypes } from "#app/modifier/modifier-type"; +import { modifierTypes } from "#app/data/data-lists"; import { GlobalTradeSystemEncounter } from "#app/data/mystery-encounters/encounters/global-trade-system-encounter"; import { CIVILIZATION_ENCOUNTER_BIOMES } from "#app/data/mystery-encounters/mystery-encounters"; import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; import { UiMode } from "#enums/ui-mode"; import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; -import { ModifierTier } from "#app/modifier/modifier-tier"; +import { ModifierTier } from "#enums/modifier-tier"; import * as Utils from "#app/utils/common"; const namespace = "mysteryEncounters/globalTradeSystem"; @@ -37,10 +37,11 @@ describe("Global Trade System - Mystery Encounter", () => { beforeEach(async () => { game = new GameManager(phaserGame); scene = game.scene; - game.override.mysteryEncounterChance(100); - game.override.startingWave(defaultWave); - game.override.startingBiome(defaultBiome); - game.override.disableTrainerWaves(); + game.override + .mysteryEncounterChance(100) + .startingWave(defaultWave) + .startingBiome(defaultBiome) + .disableTrainerWaves(); const biomeMap = new Map([ [BiomeId.VOLCANO, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]], @@ -53,8 +54,6 @@ describe("Global Trade System - Mystery Encounter", () => { afterEach(() => { game.phaseInterceptor.restoreOg(); - vi.clearAllMocks(); - vi.resetAllMocks(); }); it("should have the correct properties", async () => { @@ -85,8 +84,7 @@ describe("Global Trade System - Mystery Encounter", () => { }); it("should not spawn outside of CIVILIZATION_ENCOUNTER_BIOMES", async () => { - game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON); - game.override.startingBiome(BiomeId.VOLCANO); + game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON).startingBiome(BiomeId.VOLCANO); await game.runToMysteryEncounter(); expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.GLOBAL_TRADE_SYSTEM); diff --git a/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts b/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts index 8f041a14002..a55806e5f48 100644 --- a/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts +++ b/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts @@ -1,7 +1,7 @@ import { LostAtSeaEncounter } from "#app/data/mystery-encounters/encounters/lost-at-sea-encounter"; import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; -import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { getPokemonSpecies } from "#app/utils/pokemon-utils"; import { BiomeId } from "#enums/biome-id"; import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; import { SpeciesId } from "#enums/species-id"; @@ -34,10 +34,11 @@ describe("Lost at Sea - Mystery Encounter", () => { beforeEach(async () => { game = new GameManager(phaserGame); scene = game.scene; - game.override.mysteryEncounterChance(100); - game.override.startingWave(defaultWave); - game.override.startingBiome(defaultBiome); - game.override.disableTrainerWaves(); + game.override + .mysteryEncounterChance(100) + .startingWave(defaultWave) + .startingBiome(defaultBiome) + .disableTrainerWaves(); vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( new Map([ @@ -49,8 +50,6 @@ describe("Lost at Sea - Mystery Encounter", () => { afterEach(() => { game.phaseInterceptor.restoreOg(); - vi.clearAllMocks(); - vi.resetAllMocks(); }); it("should have the correct properties", async () => { @@ -67,8 +66,7 @@ describe("Lost at Sea - Mystery Encounter", () => { }); it("should not spawn outside of sea biome", async () => { - game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON); - game.override.startingBiome(BiomeId.MOUNTAIN); + game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON).startingBiome(BiomeId.MOUNTAIN); await game.runToMysteryEncounter(); expect(game.scene.currentBattle.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.LOST_AT_SEA); diff --git a/test/mystery-encounter/encounters/mysterious-challengers-encounter.test.ts b/test/mystery-encounter/encounters/mysterious-challengers-encounter.test.ts index 948e42547de..478648d88a7 100644 --- a/test/mystery-encounter/encounters/mysterious-challengers-encounter.test.ts +++ b/test/mystery-encounter/encounters/mysterious-challengers-encounter.test.ts @@ -14,7 +14,7 @@ import { UiMode } from "#enums/ui-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { initSceneWithoutEncounterPhase } from "#test/testUtils/gameManagerUtils"; -import { ModifierTier } from "#app/modifier/modifier-tier"; +import { ModifierTier } from "#enums/modifier-tier"; import { MysteriousChallengersEncounter } from "#app/data/mystery-encounters/encounters/mysterious-challengers-encounter"; import { TrainerConfig } from "#app/data/trainers/trainer-config"; import { TrainerPartyCompoundTemplate } from "#app/data/trainers/TrainerPartyTemplate"; @@ -43,10 +43,11 @@ describe("Mysterious Challengers - Mystery Encounter", () => { beforeEach(async () => { game = new GameManager(phaserGame); scene = game.scene; - game.override.mysteryEncounterChance(100); - game.override.startingWave(defaultWave); - game.override.startingBiome(defaultBiome); - game.override.disableTrainerWaves(); + game.override + .mysteryEncounterChance(100) + .startingWave(defaultWave) + .startingBiome(defaultBiome) + .disableTrainerWaves(); const biomeMap = new Map([ [BiomeId.VOLCANO, [MysteryEncounterType.FIGHT_OR_FLIGHT]], @@ -59,8 +60,6 @@ describe("Mysterious Challengers - Mystery Encounter", () => { afterEach(() => { game.phaseInterceptor.restoreOg(); - vi.clearAllMocks(); - vi.resetAllMocks(); }); it("should have the correct properties", async () => { @@ -79,8 +78,7 @@ describe("Mysterious Challengers - Mystery Encounter", () => { }); it("should not spawn outside of HUMAN_TRANSITABLE_BIOMES", async () => { - game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT); - game.override.startingBiome(BiomeId.VOLCANO); + game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT).startingBiome(BiomeId.VOLCANO); await game.runToMysteryEncounter(); expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.MYSTERIOUS_CHALLENGERS); diff --git a/test/mystery-encounter/encounters/part-timer-encounter.test.ts b/test/mystery-encounter/encounters/part-timer-encounter.test.ts index be985ea0593..d36b387cae1 100644 --- a/test/mystery-encounter/encounters/part-timer-encounter.test.ts +++ b/test/mystery-encounter/encounters/part-timer-encounter.test.ts @@ -36,10 +36,11 @@ describe("Part-Timer - Mystery Encounter", () => { beforeEach(async () => { game = new GameManager(phaserGame); scene = game.scene; - game.override.mysteryEncounterChance(100); - game.override.startingWave(defaultWave); - game.override.startingBiome(defaultBiome); - game.override.disableTrainerWaves(); + game.override + .mysteryEncounterChance(100) + .startingWave(defaultWave) + .startingBiome(defaultBiome) + .disableTrainerWaves(); const biomeMap = new Map([ [BiomeId.VOLCANO, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]], @@ -52,8 +53,6 @@ describe("Part-Timer - Mystery Encounter", () => { afterEach(() => { game.phaseInterceptor.restoreOg(); - vi.clearAllMocks(); - vi.resetAllMocks(); }); it("should have the correct properties", async () => { @@ -76,8 +75,7 @@ describe("Part-Timer - Mystery Encounter", () => { }); it("should not spawn outside of CIVILIZATION_ENCOUNTER_BIOMES", async () => { - game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON); - game.override.startingBiome(BiomeId.VOLCANO); + game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON).startingBiome(BiomeId.VOLCANO); await game.runToMysteryEncounter(); expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.PART_TIMER); diff --git a/test/mystery-encounter/encounters/safari-zone.test.ts b/test/mystery-encounter/encounters/safari-zone.test.ts index dcaf25dd512..7ac26f48e59 100644 --- a/test/mystery-encounter/encounters/safari-zone.test.ts +++ b/test/mystery-encounter/encounters/safari-zone.test.ts @@ -38,10 +38,11 @@ describe("Safari Zone - Mystery Encounter", () => { beforeEach(async () => { game = new GameManager(phaserGame); scene = game.scene; - game.override.mysteryEncounterChance(100); - game.override.startingWave(defaultWave); - game.override.startingBiome(defaultBiome); - game.override.disableTrainerWaves(); + game.override + .mysteryEncounterChance(100) + .startingWave(defaultWave) + .startingBiome(defaultBiome) + .disableTrainerWaves(); const biomeMap = new Map([ [BiomeId.VOLCANO, [MysteryEncounterType.FIGHT_OR_FLIGHT]], @@ -54,8 +55,6 @@ describe("Safari Zone - Mystery Encounter", () => { afterEach(() => { game.phaseInterceptor.restoreOg(); - vi.clearAllMocks(); - vi.resetAllMocks(); }); it("should have the correct properties", async () => { @@ -72,8 +71,7 @@ describe("Safari Zone - Mystery Encounter", () => { }); it("should not spawn outside of the forest, swamp, or jungle biomes", async () => { - game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT); - game.override.startingBiome(BiomeId.VOLCANO); + game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT).startingBiome(BiomeId.VOLCANO); await game.runToMysteryEncounter(); expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.SAFARI_ZONE); diff --git a/test/mystery-encounter/encounters/teleporting-hijinks-encounter.test.ts b/test/mystery-encounter/encounters/teleporting-hijinks-encounter.test.ts index 6c8daed998c..2138298ee2b 100644 --- a/test/mystery-encounter/encounters/teleporting-hijinks-encounter.test.ts +++ b/test/mystery-encounter/encounters/teleporting-hijinks-encounter.test.ts @@ -64,8 +64,6 @@ describe("Teleporting Hijinks - Mystery Encounter", () => { afterEach(() => { game.phaseInterceptor.restoreOg(); - vi.clearAllMocks(); - vi.resetAllMocks(); }); it("should have the correct properties", async () => { @@ -82,8 +80,7 @@ describe("Teleporting Hijinks - Mystery Encounter", () => { }); it("should run in waves that are X1", async () => { - game.override.startingWave(11); - game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON); + game.override.startingWave(11).mysteryEncounterTier(MysteryEncounterTier.COMMON); await game.runToMysteryEncounter(); @@ -91,8 +88,7 @@ describe("Teleporting Hijinks - Mystery Encounter", () => { }); it("should run in waves that are X2", async () => { - game.override.startingWave(32); - game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON); + game.override.startingWave(32).mysteryEncounterTier(MysteryEncounterTier.COMMON); await game.runToMysteryEncounter(); @@ -100,8 +96,7 @@ describe("Teleporting Hijinks - Mystery Encounter", () => { }); it("should run in waves that are X3", async () => { - game.override.startingWave(23); - game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON); + game.override.startingWave(23).mysteryEncounterTier(MysteryEncounterTier.COMMON); await game.runToMysteryEncounter(); diff --git a/test/mystery-encounter/encounters/the-expert-breeder-encounter.test.ts b/test/mystery-encounter/encounters/the-expert-breeder-encounter.test.ts index 0d5f67b8815..c9d6f540191 100644 --- a/test/mystery-encounter/encounters/the-expert-breeder-encounter.test.ts +++ b/test/mystery-encounter/encounters/the-expert-breeder-encounter.test.ts @@ -40,10 +40,11 @@ describe("The Expert Pokémon Breeder - Mystery Encounter", () => { beforeEach(async () => { game = new GameManager(phaserGame); scene = game.scene; - game.override.mysteryEncounterChance(100); - game.override.startingWave(defaultWave); - game.override.startingBiome(defaultBiome); - game.override.disableTrainerWaves(); + game.override + .mysteryEncounterChance(100) + .startingWave(defaultWave) + .startingBiome(defaultBiome) + .disableTrainerWaves(); const biomeMap = new Map([ [BiomeId.VOLCANO, [MysteryEncounterType.FIGHT_OR_FLIGHT]], @@ -56,8 +57,6 @@ describe("The Expert Pokémon Breeder - Mystery Encounter", () => { afterEach(() => { game.phaseInterceptor.restoreOg(); - vi.clearAllMocks(); - vi.resetAllMocks(); }); it("should have the correct properties", async () => { @@ -84,8 +83,7 @@ describe("The Expert Pokémon Breeder - Mystery Encounter", () => { }); it("should not spawn outside of HUMAN_TRANSITABLE_BIOMES", async () => { - game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT); - game.override.startingBiome(BiomeId.VOLCANO); + game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT).startingBiome(BiomeId.VOLCANO); await game.runToMysteryEncounter(); expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe( diff --git a/test/mystery-encounter/encounters/the-pokemon-salesman-encounter.test.ts b/test/mystery-encounter/encounters/the-pokemon-salesman-encounter.test.ts index 13d7c2502a6..990e39014e2 100644 --- a/test/mystery-encounter/encounters/the-pokemon-salesman-encounter.test.ts +++ b/test/mystery-encounter/encounters/the-pokemon-salesman-encounter.test.ts @@ -38,10 +38,11 @@ describe("The Pokemon Salesman - Mystery Encounter", () => { beforeEach(async () => { game = new GameManager(phaserGame); scene = game.scene; - game.override.mysteryEncounterChance(100); - game.override.startingWave(defaultWave); - game.override.startingBiome(defaultBiome); - game.override.disableTrainerWaves(); + game.override + .mysteryEncounterChance(100) + .startingWave(defaultWave) + .startingBiome(defaultBiome) + .disableTrainerWaves(); const biomeMap = new Map([ [BiomeId.VOLCANO, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]], @@ -54,8 +55,6 @@ describe("The Pokemon Salesman - Mystery Encounter", () => { afterEach(() => { game.phaseInterceptor.restoreOg(); - vi.clearAllMocks(); - vi.resetAllMocks(); }); it("should have the correct properties", async () => { @@ -78,8 +77,7 @@ describe("The Pokemon Salesman - Mystery Encounter", () => { }); it("should not spawn outside of HUMAN_TRANSITABLE_BIOMES", async () => { - game.override.mysteryEncounterTier(MysteryEncounterTier.ULTRA); - game.override.startingBiome(BiomeId.VOLCANO); + game.override.mysteryEncounterTier(MysteryEncounterTier.ULTRA).startingBiome(BiomeId.VOLCANO); await game.runToMysteryEncounter(); expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.THE_POKEMON_SALESMAN); diff --git a/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts b/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts index 5926c1ed2e7..96060e4114c 100644 --- a/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts +++ b/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts @@ -4,7 +4,7 @@ import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { SpeciesId } from "#enums/species-id"; import GameManager from "#test/testUtils/gameManager"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { getPokemonSpecies } from "#app/utils/pokemon-utils"; import * as BattleAnims from "#app/data/battle-anims"; import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { @@ -65,8 +65,6 @@ describe("The Strong Stuff - Mystery Encounter", () => { afterEach(() => { game.phaseInterceptor.restoreOg(); - vi.clearAllMocks(); - vi.resetAllMocks(); }); it("should have the correct properties", async () => { @@ -83,8 +81,7 @@ describe("The Strong Stuff - Mystery Encounter", () => { }); it("should not spawn outside of CAVE biome", async () => { - game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON); - game.override.startingBiome(BiomeId.MOUNTAIN); + game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON).startingBiome(BiomeId.MOUNTAIN); await game.runToMysteryEncounter(); expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.THE_STRONG_STUFF); diff --git a/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts b/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts index 87a3852615d..012b88bcd73 100644 --- a/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts +++ b/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts @@ -17,7 +17,7 @@ import MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; import { TrainerType } from "#enums/trainer-type"; import { Nature } from "#enums/nature"; import { MoveId } from "#enums/move-id"; -import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { getPokemonSpecies } from "#app/utils/pokemon-utils"; import { TheWinstrateChallengeEncounter } from "#app/data/mystery-encounters/encounters/the-winstrate-challenge-encounter"; import { Status } from "#app/data/status-effect"; import { MysteryEncounterRewardsPhase } from "#app/phases/mystery-encounter-phases"; @@ -44,10 +44,11 @@ describe("The Winstrate Challenge - Mystery Encounter", () => { beforeEach(async () => { game = new GameManager(phaserGame); scene = game.scene; - game.override.mysteryEncounterChance(100); - game.override.startingWave(defaultWave); - game.override.startingBiome(defaultBiome); - game.override.disableTrainerWaves(); + game.override + .mysteryEncounterChance(100) + .startingWave(defaultWave) + .startingBiome(defaultBiome) + .disableTrainerWaves(); const biomeMap = new Map([ [BiomeId.VOLCANO, [MysteryEncounterType.FIGHT_OR_FLIGHT]], @@ -60,8 +61,6 @@ describe("The Winstrate Challenge - Mystery Encounter", () => { afterEach(() => { game.phaseInterceptor.restoreOg(); - vi.clearAllMocks(); - vi.resetAllMocks(); }); it("should have the correct properties", async () => { @@ -86,8 +85,7 @@ describe("The Winstrate Challenge - Mystery Encounter", () => { }); it("should not spawn outside of HUMAN_TRANSITABLE_BIOMES", async () => { - game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT); - game.override.startingBiome(BiomeId.VOLCANO); + game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT).startingBiome(BiomeId.VOLCANO); await game.runToMysteryEncounter(); expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.THE_WINSTRATE_CHALLENGE); @@ -307,7 +305,7 @@ describe("The Winstrate Challenge - Mystery Encounter", () => { ) as ModifierSelectUiHandler; expect(modifierSelectHandler.options.length).toEqual(1); expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toBe("MYSTERY_ENCOUNTER_MACHO_BRACE"); - }, 15000); + }); }); describe("Option 2 - Refuse the Challenge", () => { diff --git a/test/mystery-encounter/encounters/trash-to-treasure-encounter.test.ts b/test/mystery-encounter/encounters/trash-to-treasure-encounter.test.ts index 47c75eb19fc..9ab5f16d1b9 100644 --- a/test/mystery-encounter/encounters/trash-to-treasure-encounter.test.ts +++ b/test/mystery-encounter/encounters/trash-to-treasure-encounter.test.ts @@ -8,14 +8,15 @@ import { type EnemyPokemonConfig, generateModifierType, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; -import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { getPokemonSpecies } from "#app/utils/pokemon-utils"; import { BiomeId } from "#enums/biome-id"; import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; import { SpeciesId } from "#enums/species-id"; import { PokemonMove } from "#app/data/moves/pokemon-move"; import { HealShopCostModifier, HitHealModifier, TurnHealModifier } from "#app/modifier/modifier"; -import { ModifierTier } from "#app/modifier/modifier-tier"; -import { modifierTypes, type PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; +import { ModifierTier } from "#enums/modifier-tier"; +import type { PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; +import { modifierTypes } from "#app/data/data-lists"; import { CommandPhase } from "#app/phases/command-phase"; import { MovePhase } from "#app/phases/move-phase"; import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; @@ -50,10 +51,11 @@ describe("Trash to Treasure - Mystery Encounter", () => { beforeEach(async () => { game = new GameManager(phaserGame); scene = game.scene; - game.override.mysteryEncounterChance(100); - game.override.startingWave(defaultWave); - game.override.startingBiome(defaultBiome); - game.override.disableTrainerWaves(); + game.override + .mysteryEncounterChance(100) + .startingWave(defaultWave) + .startingBiome(defaultBiome) + .disableTrainerWaves(); vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( new Map([[BiomeId.CAVE, [MysteryEncounterType.TRASH_TO_TREASURE]]]), @@ -62,8 +64,6 @@ describe("Trash to Treasure - Mystery Encounter", () => { afterEach(() => { game.phaseInterceptor.restoreOg(); - vi.clearAllMocks(); - vi.resetAllMocks(); }); it("should have the correct properties", async () => { diff --git a/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts b/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts index 12545b8d70a..ec64a17d291 100644 --- a/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts +++ b/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts @@ -21,12 +21,12 @@ import { CommandPhase } from "#app/phases/command-phase"; import { UncommonBreedEncounter } from "#app/data/mystery-encounters/encounters/uncommon-breed-encounter"; import { MovePhase } from "#app/phases/move-phase"; import { speciesEggMoves } from "#app/data/balance/egg-moves"; -import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { getPokemonSpecies } from "#app/utils/pokemon-utils"; import { BerryType } from "#enums/berry-type"; import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; import { Stat } from "#enums/stat"; import type { BerryModifier } from "#app/modifier/modifier"; -import { modifierTypes } from "#app/modifier/modifier-type"; +import { modifierTypes } from "#app/data/data-lists"; import { AbilityId } from "#enums/ability-id"; const namespace = "mysteryEncounters/uncommonBreed"; @@ -62,8 +62,6 @@ describe("Uncommon Breed - Mystery Encounter", () => { afterEach(() => { game.phaseInterceptor.restoreOg(); - vi.clearAllMocks(); - vi.resetAllMocks(); }); it("should have the correct properties", async () => { diff --git a/test/mystery-encounter/encounters/weird-dream-encounter.test.ts b/test/mystery-encounter/encounters/weird-dream-encounter.test.ts index 2ad74b48540..475d5cc3c6e 100644 --- a/test/mystery-encounter/encounters/weird-dream-encounter.test.ts +++ b/test/mystery-encounter/encounters/weird-dream-encounter.test.ts @@ -19,7 +19,7 @@ import { WeirdDreamEncounter } from "#app/data/mystery-encounters/encounters/wei import * as EncounterTransformationSequence from "#app/data/mystery-encounters/utils/encounter-transformation-sequence"; import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; import { CommandPhase } from "#app/phases/command-phase"; -import { ModifierTier } from "#app/modifier/modifier-tier"; +import { ModifierTier } from "#enums/modifier-tier"; const namespace = "mysteryEncounters/weirdDream"; const defaultParty = [SpeciesId.MAGBY, SpeciesId.HAUNTER, SpeciesId.ABRA]; @@ -38,10 +38,11 @@ describe("Weird Dream - Mystery Encounter", () => { beforeEach(async () => { game = new GameManager(phaserGame); scene = game.scene; - game.override.mysteryEncounterChance(100); - game.override.startingWave(defaultWave); - game.override.startingBiome(defaultBiome); - game.override.disableTrainerWaves(); + game.override + .mysteryEncounterChance(100) + .startingWave(defaultWave) + .startingBiome(defaultBiome) + .disableTrainerWaves(); vi.spyOn(EncounterTransformationSequence, "doPokemonTransformationSequence").mockImplementation( () => new Promise(resolve => resolve()), ); @@ -53,8 +54,6 @@ describe("Weird Dream - Mystery Encounter", () => { afterEach(() => { game.phaseInterceptor.restoreOg(); - vi.clearAllMocks(); - vi.resetAllMocks(); }); it("should have the correct properties", async () => { diff --git a/test/mystery-encounter/mystery-encounter-utils.test.ts b/test/mystery-encounter/mystery-encounter-utils.test.ts index 80e2fb77f2b..b775ce8df60 100644 --- a/test/mystery-encounter/mystery-encounter-utils.test.ts +++ b/test/mystery-encounter/mystery-encounter-utils.test.ts @@ -14,7 +14,7 @@ import { getRandomSpeciesByStarterCost, koPlayerPokemon, } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; -import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { getPokemonSpecies } from "#app/utils/pokemon-utils"; import { PokemonType } from "#enums/pokemon-type"; import { MessagePhase } from "#app/phases/message-phase"; import GameManager from "#test/testUtils/gameManager"; @@ -250,6 +250,7 @@ describe("Mystery Encounter Utils", () => { it("gets species of specified types", () => { // Only 9 tiers are: Kyogre, Groudon, Rayquaza, Arceus, Zacian, Koraidon, Miraidon, Terapagos + // TODO: This has to be changed const result = getRandomSpeciesByStarterCost(9, undefined, [PokemonType.GROUND]); const pokeSpecies = getPokemonSpecies(result); expect(pokeSpecies.speciesId).toBe(SpeciesId.GROUDON); diff --git a/test/mystery-encounter/mystery-encounter.test.ts b/test/mystery-encounter/mystery-encounter.test.ts index 1f1d0f5826e..be1f153f8b1 100644 --- a/test/mystery-encounter/mystery-encounter.test.ts +++ b/test/mystery-encounter/mystery-encounter.test.ts @@ -24,8 +24,7 @@ describe("Mystery Encounters", () => { beforeEach(() => { game = new GameManager(phaserGame); scene = game.scene; - game.override.startingWave(11); - game.override.mysteryEncounterChance(100); + game.override.startingWave(11).mysteryEncounterChance(100); }); it("Spawns a mystery encounter", async () => { diff --git a/test/phases/form-change-phase.test.ts b/test/phases/form-change-phase.test.ts index 8531375a48b..b025f72f5e4 100644 --- a/test/phases/form-change-phase.test.ts +++ b/test/phases/form-change-phase.test.ts @@ -6,7 +6,7 @@ import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { PokemonType } from "#enums/pokemon-type"; import { generateModifierType } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; -import { modifierTypes } from "#app/modifier/modifier-type"; +import { modifierTypes } from "#app/data/data-lists"; describe("Form Change Phase", () => { let phaserGame: Phaser.Game; @@ -28,7 +28,7 @@ describe("Form Change Phase", () => { .moveset([MoveId.SPLASH]) .ability(AbilityId.BALL_FETCH) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH); diff --git a/test/phases/frenzy-move-reset.test.ts b/test/phases/frenzy-move-reset.test.ts index 878c58d8666..9c59e1da968 100644 --- a/test/phases/frenzy-move-reset.test.ts +++ b/test/phases/frenzy-move-reset.test.ts @@ -26,7 +26,7 @@ describe("Frenzy Move Reset", () => { game = new GameManager(phaserGame); game.override .battleStyle("single") - .disableCrits() + .criticalHits(false) .starterSpecies(SpeciesId.MAGIKARP) .moveset(MoveId.THRASH) .statusEffect(StatusEffect.PARALYSIS) diff --git a/test/phases/game-over-phase.test.ts b/test/phases/game-over-phase.test.ts index c430223b774..008f9fb68e8 100644 --- a/test/phases/game-over-phase.test.ts +++ b/test/phases/game-over-phase.test.ts @@ -6,7 +6,7 @@ import GameManager from "#test/testUtils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { achvs } from "#app/system/achv"; -import { Unlockables } from "#app/system/unlockables"; +import { Unlockables } from "#enums/unlockables"; describe("Game Over Phase", () => { let phaserGame: Phaser.Game; @@ -28,7 +28,7 @@ describe("Game Over Phase", () => { .moveset([MoveId.MEMENTO, MoveId.ICE_BEAM, MoveId.SPLASH]) .ability(AbilityId.BALL_FETCH) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH) .startingWave(200) diff --git a/test/phases/mystery-encounter-phase.test.ts b/test/phases/mystery-encounter-phase.test.ts index d078c2398b4..b17682d6c74 100644 --- a/test/phases/mystery-encounter-phase.test.ts +++ b/test/phases/mystery-encounter-phase.test.ts @@ -27,10 +27,7 @@ describe("Mystery Encounter Phases", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.startingWave(11); - game.override.mysteryEncounterChance(100); - // Seed guarantees wild encounter to be replaced by ME - game.override.seed("test"); + game.override.startingWave(11).mysteryEncounterChance(100).seed("test"); // Seed guarantees wild encounter to be replaced by ME }); describe("MysteryEncounterPhase", () => { diff --git a/test/phases/phases.test.ts b/test/phases/phases.test.ts index 8f7b1a1ea66..9d1c1804615 100644 --- a/test/phases/phases.test.ts +++ b/test/phases/phases.test.ts @@ -51,6 +51,6 @@ describe("Phases", () => { scene.phaseManager.unshiftPhase(unavailablePhase); await game.phaseInterceptor.to(UnavailablePhase); expect(scene.ui.getMode()).to.equal(UiMode.UNAVAILABLE); - }, 20000); + }); }); }); diff --git a/test/phases/select-modifier-phase.test.ts b/test/phases/select-modifier-phase.test.ts index 083b7d16f10..b6c3089e236 100644 --- a/test/phases/select-modifier-phase.test.ts +++ b/test/phases/select-modifier-phase.test.ts @@ -1,9 +1,10 @@ import type BattleScene from "#app/battle-scene"; -import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { getPokemonSpecies } from "#app/utils/pokemon-utils"; import { PlayerPokemon } from "#app/field/pokemon"; -import { ModifierTier } from "#app/modifier/modifier-tier"; +import { ModifierTier } from "#enums/modifier-tier"; import type { CustomModifierSettings } from "#app/modifier/modifier-type"; -import { ModifierTypeOption, modifierTypes } from "#app/modifier/modifier-type"; +import { ModifierTypeOption } from "#app/modifier/modifier-type"; +import { modifierTypes } from "#app/data/data-lists"; import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; import { UiMode } from "#enums/ui-mode"; @@ -41,8 +42,6 @@ describe("SelectModifierPhase", () => { afterEach(() => { game.phaseInterceptor.restoreOg(); - - vi.clearAllMocks(); }); it("should start a select modifier phase", async () => { diff --git a/test/reload.test.ts b/test/reload.test.ts index 8b817bbfe97..8e39df23f47 100644 --- a/test/reload.test.ts +++ b/test/reload.test.ts @@ -43,7 +43,7 @@ describe("Reload", () => { const postReloadRngState = Phaser.Math.RND.state(); expect(preReloadRngState).toBe(postReloadRngState); - }, 20000); + }); it("should not have RNG inconsistencies after a biome switch", async () => { game.override @@ -75,7 +75,7 @@ describe("Reload", () => { const postReloadRngState = Phaser.Math.RND.state(); expect(preReloadRngState).toBe(postReloadRngState); - }, 20000); + }); it("should not have weather inconsistencies after a biome switch", async () => { game.override @@ -101,7 +101,7 @@ describe("Reload", () => { const postReloadWeather = game.scene.arena.weather; expect(postReloadWeather).toStrictEqual(preReloadWeather); - }, 20000); + }); it("should not have RNG inconsistencies at a Daily run wild Pokemon fight", async () => { await game.dailyMode.startBattle(); @@ -113,7 +113,7 @@ describe("Reload", () => { const postReloadRngState = Phaser.Math.RND.state(); expect(preReloadRngState).toBe(postReloadRngState); - }, 20000); + }); it("should not have RNG inconsistencies at a Daily run double battle", async () => { game.override.battleStyle("double"); @@ -126,7 +126,7 @@ describe("Reload", () => { const postReloadRngState = Phaser.Math.RND.state(); expect(preReloadRngState).toBe(postReloadRngState); - }, 20000); + }); it("should not have RNG inconsistencies at a Daily run Gym Leader fight", async () => { game.override.battleStyle("single").startingWave(40); @@ -139,7 +139,7 @@ describe("Reload", () => { const postReloadRngState = Phaser.Math.RND.state(); expect(preReloadRngState).toBe(postReloadRngState); - }, 20000); + }); it("should not have RNG inconsistencies at a Daily run regular trainer fight", async () => { game.override.battleStyle("single").startingWave(45); @@ -152,7 +152,7 @@ describe("Reload", () => { const postReloadRngState = Phaser.Math.RND.state(); expect(preReloadRngState).toBe(postReloadRngState); - }, 20000); + }); it("should not have RNG inconsistencies at a Daily run wave 50 Boss fight", async () => { game.override.battleStyle("single").startingWave(50); @@ -165,5 +165,5 @@ describe("Reload", () => { const postReloadRngState = Phaser.Math.RND.state(); expect(preReloadRngState).toBe(postReloadRngState); - }, 20000); + }); }); diff --git a/test/testUtils/gameManager.ts b/test/testUtils/gameManager.ts index 128217a6472..b9f499d4e0c 100644 --- a/test/testUtils/gameManager.ts +++ b/test/testUtils/gameManager.ts @@ -6,7 +6,8 @@ import Trainer from "#app/field/trainer"; import { getGameMode } from "#app/game-mode"; import { GameModes } from "#enums/game-modes"; import { globalScene } from "#app/global-scene"; -import { ModifierTypeOption, modifierTypes } from "#app/modifier/modifier-type"; +import { ModifierTypeOption } from "#app/modifier/modifier-type"; +import { modifierTypes } from "#app/data/data-lists"; import overrides from "#app/overrides"; import { CheckSwitchPhase } from "#app/phases/check-switch-phase"; import { CommandPhase } from "#app/phases/command-phase"; @@ -352,15 +353,20 @@ export default class GameManager { }; } - /** Transition to the first {@linkcode CommandPhase} of the next turn. */ + /** + * Transition to the first {@linkcode CommandPhase} of the next turn. + * @returns A promise that resolves once the next {@linkcode CommandPhase} has been reached. + */ async toNextTurn() { await this.phaseInterceptor.to("TurnInitPhase"); await this.phaseInterceptor.to("CommandPhase"); + console.log("==================[New Turn]=================="); } /** Transition to the {@linkcode TurnEndPhase | end of the current turn}. */ async toEndOfTurn() { await this.phaseInterceptor.to("TurnEndPhase"); + console.log("==================[End of Turn]=================="); } /** @@ -370,6 +376,7 @@ export default class GameManager { async toNextWave() { this.doSelectModifier(); + // forcibly end the message box for switching pokemon this.onNextPrompt( "CheckSwitchPhase", UiMode.CONFIRM, @@ -380,7 +387,9 @@ export default class GameManager { () => this.isCurrentPhase(TurnInitPhase), ); - await this.toNextTurn(); + await this.phaseInterceptor.to("TurnInitPhase"); + await this.phaseInterceptor.to("CommandPhase"); + console.log("==================[New Wave]=================="); } /** diff --git a/test/testUtils/gameManagerUtils.ts b/test/testUtils/gameManagerUtils.ts index d582f8c04e3..57fd9b91d26 100644 --- a/test/testUtils/gameManagerUtils.ts +++ b/test/testUtils/gameManagerUtils.ts @@ -3,7 +3,8 @@ import { BattleType } from "#enums/battle-type"; import type BattleScene from "#app/battle-scene"; import { getDailyRunStarters } from "#app/data/daily-run"; import { Gender } from "#app/data/gender"; -import { getPokemonSpecies, getPokemonSpeciesForm } from "#app/data/pokemon-species"; +import { getPokemonSpeciesForm } from "#app/data/pokemon-species"; +import { getPokemonSpecies } from "#app/utils/pokemon-utils"; import { PlayerPokemon } from "#app/field/pokemon"; import { getGameMode } from "#app/game-mode"; import { GameModes } from "#enums/game-modes"; @@ -97,7 +98,7 @@ export function waitUntil(truth): Promise { }); } -/** Get the index of `move` from the moveset of the pokemon on the player's field at location `pokemonIndex` */ +/** Get the index of `move` from the moveset of the pokemon on the player's field at location `pokemonIndex`. */ export function getMovePosition(scene: BattleScene, pokemonIndex: 0 | 1, move: MoveId): number { const playerPokemon = scene.getPlayerField()[pokemonIndex]; const moveSet = playerPokemon.getMoveset(); diff --git a/test/testUtils/helpers/dailyModeHelper.ts b/test/testUtils/helpers/dailyModeHelper.ts index 8ee03ce5f89..4672d4dc787 100644 --- a/test/testUtils/helpers/dailyModeHelper.ts +++ b/test/testUtils/helpers/dailyModeHelper.ts @@ -16,8 +16,9 @@ export class DailyModeHelper extends GameManagerHelper { /** * Runs the daily game to the summon phase. * @returns A promise that resolves when the summon phase is reached. + * @remarks Please do not use for starting normal battles - use {@linkcode startBattle} instead */ - async runToSummon() { + async runToSummon(): Promise { await this.game.runToTitle(); if (this.game.override.disableShinies) { @@ -45,7 +46,7 @@ export class DailyModeHelper extends GameManagerHelper { * Transitions to the start of a battle. * @returns A promise that resolves when the battle is started. */ - async startBattle() { + async startBattle(): Promise { await this.runToSummon(); if (this.game.scene.battleStyle === BattleStyle.SWITCH) { diff --git a/test/testUtils/helpers/field-helper.ts b/test/testUtils/helpers/field-helper.ts index 72498fd8e16..2866c01209f 100644 --- a/test/testUtils/helpers/field-helper.ts +++ b/test/testUtils/helpers/field-helper.ts @@ -4,7 +4,7 @@ import type { globalScene } from "#app/global-scene"; // -- end tsdoc imports -- import type { BattlerIndex } from "#enums/battler-index"; -import type { Ability } from "#app/data/abilities/ability-class"; +import type { Ability } from "#app/data/abilities/ability"; import { allAbilities } from "#app/data/data-lists"; import type Pokemon from "#app/field/pokemon"; import type { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon"; diff --git a/test/testUtils/helpers/moveHelper.ts b/test/testUtils/helpers/moveHelper.ts index 1b799e12da7..ed1441a6a2f 100644 --- a/test/testUtils/helpers/moveHelper.ts +++ b/test/testUtils/helpers/moveHelper.ts @@ -12,6 +12,8 @@ import { UiMode } from "#enums/ui-mode"; import { getMovePosition } from "#test/testUtils/gameManagerUtils"; import { GameManagerHelper } from "#test/testUtils/helpers/gameManagerHelper"; import { vi } from "vitest"; +import { coerceArray } from "#app/utils/common"; +import { MoveUseMode } from "#enums/move-use-mode"; /** * Helper to handle a Pokemon's move @@ -20,6 +22,7 @@ export class MoveHelper extends GameManagerHelper { /** * Intercepts {@linkcode MoveEffectPhase} and mocks the phase's move's * accuracy to -1, guaranteeing a hit. + * @returns A promise that resolves once the next MoveEffectPhase has been reached (not run). */ public async forceHit(): Promise { await this.game.phaseInterceptor.to(MoveEffectPhase, false); @@ -30,7 +33,8 @@ export class MoveHelper extends GameManagerHelper { /** * Intercepts {@linkcode MoveEffectPhase} and mocks the phase's move's accuracy * to 0, guaranteeing a miss. - * @param firstTargetOnly - Whether the move should force miss on the first target only, in the case of multi-target moves. + * @param firstTargetOnly - Whether to only force a miss on the first target hit; default `false`. + * @returns A promise that resolves once the next MoveEffectPhase has been reached (not run). */ public async forceMiss(firstTargetOnly = false): Promise { await this.game.phaseInterceptor.to(MoveEffectPhase, false); @@ -63,7 +67,7 @@ export class MoveHelper extends GameManagerHelper { (this.game.scene.phaseManager.getCurrentPhase() as CommandPhase).handleCommand( Command.FIGHT, movePosition, - false, + MoveUseMode.NORMAL, ); }); @@ -91,7 +95,11 @@ export class MoveHelper extends GameManagerHelper { ); }); this.game.onNextPrompt("CommandPhase", UiMode.FIGHT, () => { - (this.game.scene.phaseManager.getCurrentPhase() as CommandPhase).handleCommand(Command.TERA, movePosition, false); + (this.game.scene.phaseManager.getCurrentPhase() as CommandPhase).handleCommand( + Command.TERA, + movePosition, + MoveUseMode.NORMAL, + ); }); if (targetIndex !== null) { @@ -157,9 +165,7 @@ export class MoveHelper extends GameManagerHelper { * @param moveset - The {@linkcode MoveId} (single or array) to change the Pokemon's moveset to. */ public changeMoveset(pokemon: Pokemon, moveset: MoveId | MoveId[]): void { - if (!Array.isArray(moveset)) { - moveset = [moveset]; - } + moveset = coerceArray(moveset); pokemon.moveset = []; moveset.forEach(move => { pokemon.moveset.push(new PokemonMove(move)); @@ -193,6 +199,7 @@ export class MoveHelper extends GameManagerHelper { target !== undefined && !legalTargets.multiple && legalTargets.targets.includes(target) ? [target] : enemy.getNextTargets(moveId), + useMode: MoveUseMode.NORMAL, }); /** @@ -241,6 +248,7 @@ export class MoveHelper extends GameManagerHelper { target !== undefined && !legalTargets.multiple && legalTargets.targets.includes(target) ? [target] : enemy.getNextTargets(moveId), + useMode: MoveUseMode.NORMAL, }); /** diff --git a/test/testUtils/helpers/overridesHelper.ts b/test/testUtils/helpers/overridesHelper.ts index 32219fa833c..3bf0fbbda47 100644 --- a/test/testUtils/helpers/overridesHelper.ts +++ b/test/testUtils/helpers/overridesHelper.ts @@ -4,7 +4,7 @@ import { AbilityId } from "#enums/ability-id"; import type { ModifierOverride } from "#app/modifier/modifier-type"; import type { BattleStyle } from "#app/overrides"; import Overrides, { defaultOverrides } from "#app/overrides"; -import type { Unlockables } from "#app/system/unlockables"; +import type { Unlockables } from "#enums/unlockables"; import { BiomeId } from "#enums/biome-id"; import { MoveId } from "#enums/move-id"; import type { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; @@ -14,7 +14,7 @@ import { StatusEffect } from "#enums/status-effect"; import type { WeatherType } from "#enums/weather-type"; import { expect, vi } from "vitest"; import { GameManagerHelper } from "./gameManagerHelper"; -import { shiftCharCodes } from "#app/utils/common"; +import { coerceArray, shiftCharCodes } from "#app/utils/common"; import type { RandomTrainerOverride } from "#app/overrides"; import type { BattleType } from "#enums/battle-type"; @@ -202,9 +202,7 @@ export class OverridesHelper extends GameManagerHelper { */ public moveset(moveset: MoveId | MoveId[]): this { vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue(moveset); - if (!Array.isArray(moveset)) { - moveset = [moveset]; - } + moveset = coerceArray(moveset); const movesetStr = moveset.map(moveId => MoveId[moveId]).join(", "); this.log(`Player Pokemon moveset set to ${movesetStr} (=[${moveset.join(", ")}])!`); return this; @@ -245,12 +243,15 @@ export class OverridesHelper extends GameManagerHelper { } /** - * Override each wave to not have critical hits + * Force random critical hit rolls to always or never suceed. + * @param crits - `true` to guarantee crits on eligible moves, `false` to force rolls to fail, `null` to disable override + * @remarks This does not bypass effects that guarantee or block critical hits; it merely mocks the chance-based rolls. * @returns `this` */ - public disableCrits(): this { - vi.spyOn(Overrides, "NEVER_CRIT_OVERRIDE", "get").mockReturnValue(true); - this.log("Critical hits are disabled!"); + public criticalHits(crits: boolean | null): this { + vi.spyOn(Overrides, "CRITICAL_HIT_OVERRIDE", "get").mockReturnValue(crits); + const freq = crits === true ? "always" : crits === false ? "never" : "randomly"; + this.log(`Critical hit rolls set to ${freq} succeed!`); return this; } @@ -382,9 +383,7 @@ export class OverridesHelper extends GameManagerHelper { */ public enemyMoveset(moveset: MoveId | MoveId[]): this { vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue(moveset); - if (!Array.isArray(moveset)) { - moveset = [moveset]; - } + moveset = coerceArray(moveset); const movesetStr = moveset.map(moveId => MoveId[moveId]).join(", "); this.log(`Enemy Pokemon moveset set to ${movesetStr} (=[${moveset.join(", ")}])!`); return this; diff --git a/test/testUtils/mocks/mocksContainer/mockContainer.ts b/test/testUtils/mocks/mocksContainer/mockContainer.ts index f1371643ce3..d31165bb847 100644 --- a/test/testUtils/mocks/mocksContainer/mockContainer.ts +++ b/test/testUtils/mocks/mocksContainer/mockContainer.ts @@ -1,3 +1,4 @@ +import { coerceArray } from "#app/utils/common"; import type MockTextureManager from "#test/testUtils/mocks/mockTextureManager"; import type { MockGameObject } from "../mockGameObject"; @@ -216,11 +217,7 @@ export default class MockContainer implements MockGameObject { } add(obj: MockGameObject | MockGameObject[]): this { - if (Array.isArray(obj)) { - this.list.push(...obj); - } else { - this.list.push(obj); - } + this.list.push(...coerceArray(obj)); return this; } @@ -232,18 +229,12 @@ export default class MockContainer implements MockGameObject { addAt(obj: MockGameObject | MockGameObject[], index = 0): this { // Adds a Game Object to this Container at the given index. - if (!Array.isArray(obj)) { - obj = [obj]; - } - this.list.splice(index, 0, ...obj); + this.list.splice(index, 0, ...coerceArray(obj)); return this; } remove(obj: MockGameObject | MockGameObject[], destroyChild = false): this { - if (!Array.isArray(obj)) { - obj = [obj]; - } - for (const item of obj) { + for (const item of coerceArray(obj)) { const index = this.list.indexOf(item); if (index !== -1) { this.list.splice(index, 1); diff --git a/test/testUtils/mocks/mocksContainer/mockRectangle.ts b/test/testUtils/mocks/mocksContainer/mockRectangle.ts index 7f54a0e255f..a8eeb370115 100644 --- a/test/testUtils/mocks/mocksContainer/mockRectangle.ts +++ b/test/testUtils/mocks/mocksContainer/mockRectangle.ts @@ -1,3 +1,4 @@ +import { coerceArray } from "#app/utils/common"; import type { MockGameObject } from "../mockGameObject"; export default class MockRectangle implements MockGameObject { @@ -50,11 +51,7 @@ export default class MockRectangle implements MockGameObject { add(obj: MockGameObject | MockGameObject[]): this { // Adds a child to this Game Object. - if (Array.isArray(obj)) { - this.list.push(...obj); - } else { - this.list.push(obj); - } + this.list.push(...coerceArray(obj)); return this; } diff --git a/test/testUtils/mocks/mocksContainer/mockSprite.ts b/test/testUtils/mocks/mocksContainer/mockSprite.ts index df36b3a29fd..ed1f1df6609 100644 --- a/test/testUtils/mocks/mocksContainer/mockSprite.ts +++ b/test/testUtils/mocks/mocksContainer/mockSprite.ts @@ -1,6 +1,8 @@ +import { coerceArray } from "#app/utils/common"; import Phaser from "phaser"; import type { MockGameObject } from "../mockGameObject"; -import Frame = Phaser.Textures.Frame; + +type Frame = Phaser.Textures.Frame; export default class MockSprite implements MockGameObject { private phaserSprite; @@ -204,11 +206,7 @@ export default class MockSprite implements MockGameObject { add(obj: MockGameObject | MockGameObject[]): this { // Adds a child to this Game Object. - if (Array.isArray(obj)) { - this.list.push(...obj); - } else { - this.list.push(obj); - } + this.list.push(...coerceArray(obj)); return this; } diff --git a/test/testUtils/phaseInterceptor.ts b/test/testUtils/phaseInterceptor.ts index 34fba2c145d..9d046fc85ba 100644 --- a/test/testUtils/phaseInterceptor.ts +++ b/test/testUtils/phaseInterceptor.ts @@ -210,7 +210,7 @@ export default class PhaseInterceptor { /** * Method to transition to a target phase. * @param phaseTo - The phase to transition to. - * @param runTarget - Whether or not to run the target phase. + * @param runTarget - Whether or not to run the target phase; default `true`. * @returns A promise that resolves when the transition is complete. */ async to(phaseTo: PhaseInterceptorPhase, runTarget = true): Promise { @@ -439,7 +439,7 @@ export default class PhaseInterceptor { * @param mode - The mode of the UI. * @param callback - The callback function to execute. * @param expireFn - The function to determine if the prompt has expired. - * @param awaitingActionInput + * @param awaitingActionInput - ???; default `false` */ addToNextPrompt( phaseTarget: string, diff --git a/test/testUtils/testFileInitialization.ts b/test/testUtils/testFileInitialization.ts index ba90cba7d5b..da390870300 100644 --- a/test/testUtils/testFileInitialization.ts +++ b/test/testUtils/testFileInitialization.ts @@ -5,6 +5,7 @@ import { initBiomes } from "#app/data/balance/biomes"; import { initEggMoves } from "#app/data/balance/egg-moves"; import { initPokemonPrevolutions, initPokemonStarters } from "#app/data/balance/pokemon-evolutions"; import { initMoves } from "#app/data/moves/move"; +import { initModifierPools } from "#app/modifier/init-modifier-pools"; import { initMysteryEncounters } from "#app/data/mystery-encounters/mystery-encounters"; import { initPokemonForms } from "#app/data/pokemon-forms"; import { initSpecies } from "#app/data/pokemon-species"; @@ -22,6 +23,7 @@ import InputText from "phaser3-rex-plugins/plugins/inputtext"; import BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext"; import { manageListeners } from "./listenersManager"; import { initI18n } from "#app/plugins/i18n"; +import { initModifierTypes } from "#app/modifier/modifier-type"; let wasInitialized = false; /** @@ -88,6 +90,8 @@ export function initTestFile() { if (!wasInitialized) { wasInitialized = true; initI18n(); + initModifierTypes(); + initModifierPools(); initVouchers(); initAchievements(); initStatsKeys(); diff --git a/test/ui/pokedex.test.ts b/test/ui/pokedex.test.ts index 573ce3fef89..13f595e0c60 100644 --- a/test/ui/pokedex.test.ts +++ b/test/ui/pokedex.test.ts @@ -1,12 +1,14 @@ import GameManager from "#test/testUtils/gameManager"; import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it, type MockInstance, vi } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import PokedexUiHandler from "#app/ui/pokedex-ui-handler"; import { FilterTextRow } from "#app/ui/filter-text"; import { allAbilities } from "#app/data/data-lists"; import { AbilityId } from "#enums/ability-id"; import { SpeciesId } from "#enums/species-id"; -import { allSpecies, getPokemonSpecies, type PokemonForm } from "#app/data/pokemon-species"; +import type { PokemonForm } from "#app/data/pokemon-species"; +import { getPokemonSpecies } from "#app/utils/pokemon-utils"; +import { allSpecies } from "#app/data/data-lists"; import { Button } from "#enums/buttons"; import { DropDownColumn } from "#enums/drop-down-column"; import type PokemonSpecies from "#app/data/pokemon-species"; @@ -46,7 +48,6 @@ function permutations(array: T[], length: number): T[][] { describe("UI - Pokedex", () => { let phaserGame: Phaser.Game; let game: GameManager; - const mocks: MockInstance[] = []; beforeAll(() => { phaserGame = new Phaser.Game({ @@ -55,9 +56,6 @@ describe("UI - Pokedex", () => { }); afterEach(() => { - while (mocks.length > 0) { - mocks.pop()?.mockRestore(); - } game.phaseInterceptor.restoreOg(); }); @@ -185,10 +183,10 @@ describe("UI - Pokedex", () => { checks.push(...pokemon.forms); } for (const p of checks) { - mocks.push(vi.spyOn(p, "ability1", "get").mockReturnValue(ability)); - mocks.push(vi.spyOn(p, "ability2", "get").mockReturnValue(ability2)); - mocks.push(vi.spyOn(p, "abilityHidden", "get").mockReturnValue(hidden)); - mocks.push(vi.spyOn(p, "getPassiveAbility").mockReturnValue(passive)); + vi.spyOn(p, "ability1", "get").mockReturnValue(ability); + vi.spyOn(p, "ability2", "get").mockReturnValue(ability2); + vi.spyOn(p, "abilityHidden", "get").mockReturnValue(hidden); + vi.spyOn(p, "getPassiveAbility").mockReturnValue(passive); } } @@ -502,13 +500,13 @@ describe("UI - Pokedex", () => { // Nab the pokemon that is selected for comparison later. // @ts-expect-error - `lastSpecies` is private - const selectedPokemon = pokedexHandler.lastSpecies.speciesId; + const selectedPokemon = pokedexHandler.lastSpeciesId.speciesId; for (let i = 0; i < 11; i++) { pokedexHandler.processInput(Button.DOWN); } // @ts-expect-error `lastSpecies` is private - expect(selectedPokemon).toEqual(pokedexHandler.lastSpecies.speciesId); + expect(selectedPokemon).toEqual(pokedexHandler.lastSpeciesId.speciesId); }, ); diff --git a/test/ui/starter-select.test.ts b/test/ui/starter-select.test.ts index be508a2b69c..8167ab17957 100644 --- a/test/ui/starter-select.test.ts +++ b/test/ui/starter-select.test.ts @@ -1,6 +1,6 @@ import { Gender } from "#app/data/gender"; import { Nature } from "#enums/nature"; -import { allSpecies } from "#app/data/pokemon-species"; +import { allSpecies } from "#app/data/data-lists"; import { GameModes } from "#enums/game-modes"; import { EncounterPhase } from "#app/phases/encounter-phase"; import { SelectStarterPhase } from "#app/phases/select-starter-phase"; @@ -94,7 +94,7 @@ describe("UI - Starter select", () => { expect(game.scene.getPlayerParty()[0].shiny).toBe(true); expect(game.scene.getPlayerParty()[0].variant).toBe(2); expect(game.scene.getPlayerParty()[0].gender).toBe(Gender.MALE); - }, 20000); + }); it("Bulbasaur - shiny - variant 2 female hardy overgrow", async () => { await game.importData("./test/testUtils/saves/everything.prsv"); @@ -156,7 +156,7 @@ describe("UI - Starter select", () => { expect(game.scene.getPlayerParty()[0].variant).toBe(2); expect(game.scene.getPlayerParty()[0].nature).toBe(Nature.HARDY); expect(game.scene.getPlayerParty()[0].getAbility().id).toBe(AbilityId.OVERGROW); - }, 20000); + }); it("Bulbasaur - shiny - variant 2 female lonely chlorophyl", async () => { await game.importData("./test/testUtils/saves/everything.prsv"); @@ -221,7 +221,7 @@ describe("UI - Starter select", () => { expect(game.scene.getPlayerParty()[0].gender).toBe(Gender.FEMALE); expect(game.scene.getPlayerParty()[0].nature).toBe(Nature.LONELY); expect(game.scene.getPlayerParty()[0].getAbility().id).toBe(AbilityId.CHLOROPHYLL); - }, 20000); + }); it("Bulbasaur - shiny - variant 2 female", async () => { await game.importData("./test/testUtils/saves/everything.prsv"); @@ -282,7 +282,7 @@ describe("UI - Starter select", () => { expect(game.scene.getPlayerParty()[0].shiny).toBe(true); expect(game.scene.getPlayerParty()[0].variant).toBe(2); expect(game.scene.getPlayerParty()[0].gender).toBe(Gender.FEMALE); - }, 20000); + }); it("Bulbasaur - not shiny", async () => { await game.importData("./test/testUtils/saves/everything.prsv"); @@ -342,7 +342,7 @@ describe("UI - Starter select", () => { expect(game.scene.getPlayerParty()[0].species.speciesId).toBe(SpeciesId.BULBASAUR); expect(game.scene.getPlayerParty()[0].shiny).toBe(false); expect(game.scene.getPlayerParty()[0].variant).toBe(0); - }, 20000); + }); it("Bulbasaur - shiny - variant 1", async () => { await game.importData("./test/testUtils/saves/everything.prsv"); @@ -404,7 +404,7 @@ describe("UI - Starter select", () => { expect(game.scene.getPlayerParty()[0].species.speciesId).toBe(SpeciesId.BULBASAUR); expect(game.scene.getPlayerParty()[0].shiny).toBe(true); expect(game.scene.getPlayerParty()[0].variant).toBe(1); - }, 20000); + }); it("Bulbasaur - shiny - variant 0", async () => { await game.importData("./test/testUtils/saves/everything.prsv"); @@ -465,7 +465,7 @@ describe("UI - Starter select", () => { expect(game.scene.getPlayerParty()[0].species.speciesId).toBe(SpeciesId.BULBASAUR); expect(game.scene.getPlayerParty()[0].shiny).toBe(true); expect(game.scene.getPlayerParty()[0].variant).toBe(0); - }, 20000); + }); it("Check if first pokemon in party is caterpie from gen 1 and 1rd row, 3rd column", async () => { await game.importData("./test/testUtils/saves/everything.prsv"); @@ -529,7 +529,7 @@ describe("UI - Starter select", () => { }); await game.phaseInterceptor.whenAboutToRun(EncounterPhase); expect(game.scene.getPlayerParty()[0].species.speciesId).toBe(SpeciesId.CATERPIE); - }, 20000); + }); it("Check if first pokemon in party is nidoran_m from gen 1 and 2nd row, 4th column (cursor (9+4)-1)", async () => { await game.importData("./test/testUtils/saves/everything.prsv"); @@ -595,5 +595,5 @@ describe("UI - Starter select", () => { }); await game.phaseInterceptor.whenAboutToRun(EncounterPhase); expect(game.scene.getPlayerParty()[0].species.speciesId).toBe(SpeciesId.NIDORAN_M); - }, 20000); + }); }); diff --git a/test/ui/transfer-item.test.ts b/test/ui/transfer-item.test.ts index b08c9823dcd..572d56c5903 100644 --- a/test/ui/transfer-item.test.ts +++ b/test/ui/transfer-item.test.ts @@ -26,17 +26,18 @@ describe("UI - Transfer Items", () => { beforeEach(async () => { game = new GameManager(phaserGame); - game.override.battleStyle("single"); - game.override.startingLevel(100); - game.override.startingWave(1); - game.override.startingHeldItems([ - { name: "BERRY", count: 1, type: BerryType.SITRUS }, - { name: "BERRY", count: 2, type: BerryType.APICOT }, - { name: "BERRY", count: 2, type: BerryType.LUM }, - ]); - game.override.moveset([MoveId.DRAGON_CLAW]); - game.override.enemySpecies(SpeciesId.MAGIKARP); - game.override.enemyMoveset([MoveId.SPLASH]); + game.override + .battleStyle("single") + .startingLevel(100) + .startingWave(1) + .startingHeldItems([ + { name: "BERRY", count: 1, type: BerryType.SITRUS }, + { name: "BERRY", count: 2, type: BerryType.APICOT }, + { name: "BERRY", count: 2, type: BerryType.LUM }, + ]) + .moveset([MoveId.DRAGON_CLAW]) + .enemySpecies(SpeciesId.MAGIKARP) + .enemyMoveset([MoveId.SPLASH]); await game.classicMode.startBattle([SpeciesId.RAYQUAZA, SpeciesId.RAYQUAZA, SpeciesId.RAYQUAZA]); @@ -76,7 +77,7 @@ describe("UI - Transfer Items", () => { }); await game.phaseInterceptor.to("SelectModifierPhase"); - }, 20000); + }); it("check transfer option for pokemon to transfer to", async () => { game.onNextPrompt("SelectModifierPhase", UiMode.PARTY, () => { @@ -97,5 +98,5 @@ describe("UI - Transfer Items", () => { }); await game.phaseInterceptor.to("SelectModifierPhase"); - }, 20000); + }); });