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/.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/create-test-boilerplate.js b/create-test-boilerplate.js deleted file mode 100644 index d47b7c4afeb..00000000000 --- a/create-test-boilerplate.js +++ /dev/null @@ -1,172 +0,0 @@ -/** - * This script creates a test boilerplate file in the appropriate - * directory based on the type selected. - * @example npm run create-test - */ - -import fs from "fs"; -import inquirer from "inquirer"; -import path from "path"; -import { fileURLToPath } from "url"; - -// Get the directory name of the current module file -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); -const typeChoices = ["Move", "Ability", "Item", "Mystery Encounter"]; - -/** - * Prompts the user to select a type via list. - * @returns {Promise<{selectedOption: string}>} the selected type - */ -async function promptTestType() { - const typeAnswer = await inquirer.prompt([ - { - type: "list", - name: "selectedOption", - message: "What type of test would you like to create:", - choices: [...typeChoices, "EXIT"], - }, - ]); - - if (typeAnswer.selectedOption === "EXIT") { - console.log("Exiting..."); - return process.exit(); - } - if (!typeChoices.includes(typeAnswer.selectedOption)) { - console.error(`Please provide a valid type (${typeChoices.join(", ")})!`); - return await promptTestType(); - } - - return typeAnswer; -} - -/** - * Prompts the user to provide a file name. - * @param {string} selectedType - * @returns {Promise<{userInput: string}>} the selected file name - */ -async function promptFileName(selectedType) { - const fileNameAnswer = await inquirer.prompt([ - { - type: "input", - name: "userInput", - message: `Please provide the name of the ${selectedType}:`, - }, - ]); - - if (!fileNameAnswer.userInput || fileNameAnswer.userInput.trim().length === 0) { - console.error("Please provide a valid file name!"); - return await promptFileName(selectedType); - } - - return fileNameAnswer; -} - -/** - * Runs the interactive create-test "CLI" - * @returns {Promise} - */ -async function runInteractive() { - const typeAnswer = await promptTestType(); - const fileNameAnswer = await promptFileName(typeAnswer.selectedOption); - - const type = typeAnswer.selectedOption.toLowerCase(); - // Convert fileName from kebab-case or camelCase to snake_case - const fileName = fileNameAnswer.userInput - .replace(/-+/g, "_") // Convert kebab-case (dashes) to underscores - .replace(/([a-z])([A-Z])/g, "$1_$2") // Convert camelCase to snake_case - .replace(/\s+/g, "_") // Replace spaces with underscores - .toLowerCase(); // Ensure all lowercase - // Format the description for the test case - - const formattedName = fileName.replace(/_/g, " ").replace(/\b\w/g, char => char.toUpperCase()); - // Determine the directory based on the type - let dir; - let description; - switch (type) { - case "move": - dir = path.join(__dirname, "test", "moves"); - description = `Moves - ${formattedName}`; - break; - case "ability": - dir = path.join(__dirname, "test", "abilities"); - description = `Abilities - ${formattedName}`; - break; - case "item": - dir = path.join(__dirname, "test", "items"); - description = `Items - ${formattedName}`; - break; - case "mystery encounter": - dir = path.join(__dirname, "test", "mystery-encounter", "encounters"); - description = `Mystery Encounter - ${formattedName}`; - break; - default: - console.error(`Invalid type. Please use one of the following: ${typeChoices.join(", ")}.`); - process.exit(1); - } - - // Define the content template - const content = `import { Abilities } from "#enums/abilities"; -import { Moves } from "#enums/moves"; -import { Species } from "#enums/species"; -import GameManager from "#test/testUtils/gameManager"; -import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; - -describe("${description}", () => { - 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([ Moves.SPLASH ]) - .ability(Abilities.BALL_FETCH) - .battleType("single") - .disableCrits() - .enemySpecies(Species.MAGIKARP) - .enemyAbility(Abilities.BALL_FETCH) - .enemyMoveset(Moves.SPLASH); - }); - - it("should do X", async () => { - await game.classicMode.startBattle([ Species.FEEBAS ]); - - game.move.select(Moves.SPLASH); - await game.phaseInterceptor.to("BerryPhase"); - - expect(true).toBe(true); - }); -}); -`; - - // Ensure the directory exists - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); - } - - // Create the file with the given name - const filePath = path.join(dir, `${fileName}.test.ts`); - - if (fs.existsSync(filePath)) { - console.error(`File "${fileName}.test.ts" already exists.`); - process.exit(1); - } - - // Write the template content to the file - fs.writeFileSync(filePath, content, "utf8"); - - console.log(`File created at: ${filePath}`); -} - -runInteractive(); 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 ce41dfc2a05..cd2b7b18494 100644 --- a/package.json +++ b/package.json @@ -13,15 +13,15 @@ "test:cov": "vitest run --coverage --no-isolate", "test:watch": "vitest watch --coverage --no-isolate", "test:silent": "vitest run --silent --no-isolate", + "test:create": "node scripts/create-test/create-test.js", "typecheck": "tsc --noEmit", "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", - "create-test": "node ./create-test-boilerplate.js", "postinstall": "npx lefthook install && npx lefthook run post-merge", "update-version:patch": "npm version patch --force --no-git-tag-version", "update-version:minor": "npm version minor --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/scripts/create-test/create-test.js b/scripts/create-test/create-test.js new file mode 100644 index 00000000000..e71994472f3 --- /dev/null +++ b/scripts/create-test/create-test.js @@ -0,0 +1,147 @@ +/** + * This script creates a test boilerplate file in the appropriate + * directory based on the type selected. + * @example npm run test:create + */ + +import chalk from "chalk"; +import inquirer from "inquirer"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +//#region Constants + +const version = "2.0.1"; +// Get the directory name of the current module file +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const projectRoot = path.join(__dirname, "..", ".."); +const boilerplateFilePath = path.join(__dirname, "test-boilerplate.ts"); +const choices = [ + { label: "Move", dir: "moves" }, + { label: "Ability", dir: "abilities" }, + { label: "Item", dir: "items" }, + { label: "Mystery Encounter", dir: "mystery-encounter/encounters" }, + { label: "Utils", dir: "utils" }, + { label: "UI", dir: "ui" }, +]; + +//#endregion +//#region Functions + +/** + * Get the path to a given folder in the test directory + * @param {...string} folders the subfolders to append to the base path + * @returns {string} the path to the requested folder + */ +function getTestFolderPath(...folders) { + return path.join(projectRoot, "test", ...folders); +} + +/** + * Prompts the user to select a type via list. + * @returns {Promise<{selectedOption: {label: string, dir: string}}>} the selected type + */ +async function promptTestType() { + const typeAnswer = await inquirer.prompt([ + { + type: "list", + name: "selectedOption", + message: "What type of test would you like to create:", + choices: [...choices.map(choice => ({ name: choice.label, value: choice })), "EXIT"], + }, + ]); + + if (typeAnswer.selectedOption === "EXIT") { + console.log("Exiting..."); + return process.exit(); + } + if (!choices.some(choice => choice.dir === typeAnswer.selectedOption.dir)) { + console.error(`Please provide a valid type: (${choices.map(choice => choice.label).join(", ")})!`); + return await promptTestType(); + } + + return typeAnswer; +} + +/** + * Prompts the user to provide a file name. + * @param {string} selectedType + * @returns {Promise<{userInput: string}>} the selected file name + */ +async function promptFileName(selectedType) { + /** @type {{userInput: string}} */ + const fileNameAnswer = await inquirer.prompt([ + { + type: "input", + name: "userInput", + message: `Please provide the name of the ${selectedType}:`, + }, + ]); + + if (!fileNameAnswer.userInput || fileNameAnswer.userInput.trim().length === 0) { + console.error("Please provide a valid file name!"); + return await promptFileName(selectedType); + } + + return fileNameAnswer; +} + +/** + * Runs the interactive test:create "CLI" + * @returns {Promise} + */ +async function runInteractive() { + console.group(chalk.grey(`Create Test - v${version}\n`)); + + try { + const typeAnswer = await promptTestType(); + const fileNameAnswer = await promptFileName(typeAnswer.selectedOption.label); + + const type = typeAnswer.selectedOption; + // Convert fileName from snake_case or camelCase to kebab-case + const fileName = fileNameAnswer.userInput + .replace(/_+/g, "-") // Convert snake_case (underscore) to kebab-case (dashes) + .replace(/([a-z])([A-Z])/g, "$1-$2") // Convert camelCase to kebab-case + .replace(/\s+/g, "-") // Replace spaces with dashes + .toLowerCase(); // Ensure all lowercase + // Format the description for the test case + + const formattedName = fileName.replace(/-/g, " ").replace(/\b\w/g, char => char.toUpperCase()); + // Determine the directory based on the type + const dir = getTestFolderPath(type.dir); + const description = `${type.label} - ${formattedName}`; + + // Define the content template + const content = fs.readFileSync(boilerplateFilePath, "utf8").replace("{{description}}", description); + + // Ensure the directory exists + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + + // Create the file with the given name + const filePath = path.join(dir, `${fileName}.test.ts`); + + if (fs.existsSync(filePath)) { + console.error(chalk.red.bold(`\n✗ File "${fileName}.test.ts" already exists!\n`)); + process.exit(1); + } + + // Write the template content to the file + fs.writeFileSync(filePath, content, "utf8"); + + console.log(chalk.green.bold(`\n✔ File created at: test/${type.dir}/${fileName}.test.ts\n`)); + console.groupEnd(); + } catch (err) { + console.error(chalk.red("✗ Error: ", err.message)); + } +} + +//#endregion +//#region Run + +runInteractive(); + +//#endregion diff --git a/scripts/create-test/test-boilerplate.ts b/scripts/create-test/test-boilerplate.ts new file mode 100644 index 00000000000..40912be6c5f --- /dev/null +++ b/scripts/create-test/test-boilerplate.ts @@ -0,0 +1,43 @@ +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"; + +describe("{{description}}", () => { + 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 + .ability(AbilityId.BALL_FETCH) + .battleStyle("single") + .disableCrits() + .enemySpecies(SpeciesId.MAGIKARP) + .enemyAbility(AbilityId.BALL_FETCH) + .enemyMoveset(MoveId.SPLASH) + .startingLevel(100) + .enemyLevel(100); + }); + + it("should do XYZ", async () => { + await game.classicMode.startBattle([SpeciesId.FEEBAS]); + + game.move.use(MoveId.SPLASH); + await game.toEndOfTurn(); + + expect(true).toBe(true); + }); +}); 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/@types/move-types.ts b/src/@types/move-types.ts new file mode 100644 index 00000000000..d9a06fd20ee --- /dev/null +++ b/src/@types/move-types.ts @@ -0,0 +1,56 @@ +import type { + AttackMove, + StatusMove, + SelfStatusMove, + ChargingAttackMove, + ChargingSelfStatusMove, + MoveAttrConstructorMap, + MoveAttr, +} from "#app/data/moves/move"; + +export type MoveAttrFilter = (attr: MoveAttr) => boolean; + +export type * from "#app/data/moves/move"; + +/** + * Map of move subclass names to their respective classes. + * Does not include the ChargeMove subclasses. For that, use `ChargingMoveClassMap`. + * + * @privateremarks + * The `never` field (`declare private _: never`) in some classes is necessary + * to ensure typescript does not improperly narrow a failed `is` guard to `never`. + * + * For example, if we did not have the never, and wrote + * ``` + * function Foo(move: Move) { + * if (move.is("AttackMove")) { + * + * } else if (move.is("StatusMove")) { // typescript errors on the `is`, saying that `move` is `never` + * + * } + * ``` + */ +export type MoveClassMap = { + AttackMove: AttackMove; + StatusMove: StatusMove; + SelfStatusMove: SelfStatusMove; +}; + +/** + * Union type of all move subclass names + */ +export type MoveKindString = "AttackMove" | "StatusMove" | "SelfStatusMove"; + +/** + * Map of move attribute names to attribute instances. + */ +export type MoveAttrMap = { + [K in keyof MoveAttrConstructorMap]: InstanceType; +}; + +/** + * Union type of all move attribute names as strings. + */ +export type MoveAttrString = keyof MoveAttrMap; + +export type ChargingMove = ChargingAttackMove | ChargingSelfStatusMove; diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 7743302cf94..81c65d85e06 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -58,35 +58,29 @@ 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"; import { BattleType } from "#enums/battle-type"; import type { GameMode } from "#app/game-mode"; -import { GameModes, getGameMode } from "#app/game-mode"; +import { getGameMode } from "#app/game-mode"; +import { GameModes } from "#enums/game-modes"; import FieldSpritePipeline from "#app/pipelines/field-sprite"; import SpritePipeline from "#app/pipelines/sprite"; import PartyExpBar from "#app/ui/party-exp-bar"; import type { TrainerSlot } from "./enums/trainer-slot"; import { trainerConfigs } from "#app/data/trainers/trainer-config"; -import Trainer, { TrainerVariant } from "#app/field/trainer"; +import Trainer from "#app/field/trainer"; +import { TrainerVariant } from "#enums/trainer-variant"; import type TrainerData from "#app/system/trainer-data"; import SoundFade from "phaser3-rex-plugins/plugins/soundfade"; import { pokemonPrevolutions } from "#app/data/balance/pokemon-evolutions"; @@ -101,13 +95,12 @@ import type UIPlugin from "phaser3-rex-plugins/templates/ui/ui-plugin"; import { addUiThemeOverrides } from "#app/ui/ui-theme"; import type PokemonData from "#app/system/pokemon-data"; import { Nature } from "#enums/nature"; -import type { SpeciesFormChange, SpeciesFormChangeTrigger } from "#app/data/pokemon-forms"; -import { - FormChangeItem, - pokemonFormChanges, - SpeciesFormChangeManualTrigger, - SpeciesFormChangeTimeOfDayTrigger, -} from "#app/data/pokemon-forms"; +import type { SpeciesFormChange } from "#app/data/pokemon-forms"; +import type { SpeciesFormChangeTrigger } from "./data/pokemon-forms/form-change-triggers"; +import { pokemonFormChanges } from "#app/data/pokemon-forms"; +import { SpeciesFormChangeTimeOfDayTrigger } from "./data/pokemon-forms/form-change-triggers"; +import { SpeciesFormChangeManualTrigger } from "./data/pokemon-forms/form-change-triggers"; +import { FormChangeItem } from "#enums/form-change-item"; import { getTypeRgb } from "#app/data/type"; import { PokemonType } from "#enums/pokemon-type"; import PokemonSpriteSparkleHandler from "#app/field/pokemon-sprite-sparkle-handler"; @@ -144,14 +137,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"; @@ -1263,7 +1255,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); } @@ -1468,7 +1460,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) @@ -1643,6 +1635,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 @@ -2744,7 +2739,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) { @@ -2784,13 +2779,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; } @@ -2813,7 +2808,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) { diff --git a/src/battle.ts b/src/battle.ts index 2ebfb634751..245705f4801 100644 --- a/src/battle.ts +++ b/src/battle.ts @@ -1,5 +1,5 @@ import { globalScene } from "#app/global-scene"; -import type { Command } from "./ui/command-ui-handler"; +import type { Command } from "#enums/command"; import { randomString, getEnumValues, @@ -10,9 +10,10 @@ import { randInt, randSeedFloat, } from "#app/utils/common"; -import Trainer, { TrainerVariant } from "./field/trainer"; +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"; @@ -29,18 +30,11 @@ 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"; - -export enum BattlerIndex { - ATTACKER = -1, - PLAYER, - PLAYER_2, - ENEMY, - ENEMY_2, -} +import { BattlerIndex } from "#enums/battler-index"; export interface TurnCommand { command: Command; @@ -179,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 d3594c389b6..f3b37563d11 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,3 +1,5 @@ +import { SpeciesId } from "#enums/species-id"; + /** The maximum size of the player's party */ export const PLAYER_PARTY_MAX_SIZE: number = 6; @@ -17,3 +19,78 @@ export const CHALLENGE_MODE_MYSTERY_ENCOUNTER_WAVES: [number, number] = [10, 180 /** The raw percentage power boost for type boost items*/ export const TYPE_BOOST_ITEM_BOOST_PERCENT = 20; + +/** + * The default species that a new player can choose from + */ +export const defaultStarterSpecies: SpeciesId[] = [ + SpeciesId.BULBASAUR, + SpeciesId.CHARMANDER, + SpeciesId.SQUIRTLE, + SpeciesId.CHIKORITA, + SpeciesId.CYNDAQUIL, + SpeciesId.TOTODILE, + SpeciesId.TREECKO, + SpeciesId.TORCHIC, + SpeciesId.MUDKIP, + SpeciesId.TURTWIG, + SpeciesId.CHIMCHAR, + SpeciesId.PIPLUP, + SpeciesId.SNIVY, + SpeciesId.TEPIG, + SpeciesId.OSHAWOTT, + SpeciesId.CHESPIN, + SpeciesId.FENNEKIN, + SpeciesId.FROAKIE, + SpeciesId.ROWLET, + SpeciesId.LITTEN, + SpeciesId.POPPLIO, + SpeciesId.GROOKEY, + SpeciesId.SCORBUNNY, + SpeciesId.SOBBLE, + SpeciesId.SPRIGATITO, + SpeciesId.FUECOCO, + SpeciesId.QUAXLY, +]; + +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 34ae8be78b6..c9684986c46 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -1,4 +1,5 @@ -import { HitResult, MoveResult } from "#app/field/pokemon"; +import { MoveResult } from "#enums/move-result"; +import { HitResult } from "#enums/hit-result"; import { BooleanHolder, NumberHolder, @@ -6,55 +7,38 @@ import { isNullOrUndefined, randSeedItem, randSeedInt, - type Constructor, randSeedFloat, + coerceArray, } from "#app/utils/common"; import { getPokemonNameWithAffix } from "#app/messages"; -import { BattlerTagLapseType, GroundedTag } from "#app/data/battler-tags"; +import { GroundedTag } from "#app/data/battler-tags"; +import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type"; import { getNonVolatileStatusEffects, getStatusEffectDescriptor, getStatusEffectHealText, } from "#app/data/status-effect"; import { Gender } from "#app/data/gender"; -import { - AttackMove, - FlinchAttr, - OneHitKOAttr, - HitHealAttr, - StatusMove, - SelfStatusMove, - VariablePowerAttr, - applyMoveAttrs, - RandomMovesetMoveAttr, - RandomMoveAttr, - NaturePowerAttr, - CopyMoveAttr, - NeutralDamageAgainstFlyingTypeMultiplierAttr, - FixedDamageAttr, -} from "#app/data/moves/move"; +import { applyMoveAttrs } from "../moves/apply-attrs"; import { allMoves } from "../data-lists"; -import { ArenaTagSide } from "#app/data/arena-tag"; +import { ArenaTagSide } from "#enums/arena-tag-side"; import { BerryModifier, HitHealModifier, PokemonHeldItemModifier } from "#app/modifier/modifier"; import { TerrainType } from "#app/data/terrain"; import { - SpeciesFormChangeAbilityTrigger, SpeciesFormChangeRevertWeatherFormTrigger, SpeciesFormChangeWeatherTrigger, -} from "#app/data/pokemon-forms"; +} from "../pokemon-forms/form-change-triggers"; +import { SpeciesFormChangeAbilityTrigger } from "../pokemon-forms/form-change-triggers"; import i18next from "i18next"; -import { Command } from "#app/ui/command-ui-handler"; +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"; @@ -68,13 +52,18 @@ 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 "../battle-anims"; -import { getBerryEffectFunc } from "../berry"; +import { CommonAnim } from "#enums/move-anims-common"; +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 { EnemyPokemon, PokemonMove } from "#app/field/pokemon"; +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"; import type { Weather } from "#app/data/weather"; import type { BattlerTag } from "#app/data/battler-tags"; @@ -83,13 +72,225 @@ import type { PokemonDefendCondition, PokemonStatStageChangeCondition, PokemonAttackCondition, - AbAttrApplyFunc, - AbAttrSuccessFunc, + AbAttrString, + AbAttrMap, } from "#app/@types/ability-types"; -import type { BattlerIndex } from "#app/battle"; +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() { @@ -143,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 { @@ -159,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; } @@ -520,7 +721,7 @@ export class AttackTypeImmunityAbAttr extends TypeImmunityAbAttr { ): boolean { return ( move.category !== MoveCategory.STATUS && - !move.hasAttr(NeutralDamageAgainstFlyingTypeMultiplierAttr) && + !move.hasAttr("NeutralDamageAgainstFlyingTypeMultiplierAttr") && super.canApplyPreDefend(pokemon, passive, simulated, attacker, move, cancelled, args) ); } @@ -693,7 +894,7 @@ export class NonSuperEffectiveImmunityAbAttr extends TypeImmunityAbAttr { args.length > 0 ? (args[0] as NumberHolder).value : pokemon.getAttackTypeEffectiveness(attacker.getMoveType(move), attacker, undefined, undefined, move); - return move instanceof AttackMove && modifierValue < 2; + return move.is("AttackMove") && modifierValue < 2; } override applyPreDefend( @@ -735,7 +936,7 @@ export class FullHpResistTypeAbAttr extends PreDefendAbAttr { const typeMultiplier = args[0]; return ( typeMultiplier instanceof NumberHolder && - !move?.hasAttr(FixedDamageAttr) && + !move?.hasAttr("FixedDamageAttr") && pokemon.isFullHp() && typeMultiplier.value > 0.5 ); @@ -980,7 +1181,7 @@ export class ReverseDrainAbAttr extends PostDefendAbAttr { _hitResult: HitResult | null, _args: any[], ): boolean { - return move.hasAttr(HitHealAttr); + return move.hasAttr("HitHealAttr"); } /** @@ -1490,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") ); } @@ -1669,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") ); } @@ -1716,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) ); @@ -1936,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)) ); } @@ -2061,10 +2260,10 @@ export class PokemonTypeChangeAbAttr extends PreAttackAbAttr { */ !move.findAttr( attr => - attr instanceof RandomMovesetMoveAttr || - attr instanceof RandomMoveAttr || - attr instanceof NaturePowerAttr || - attr instanceof CopyMoveAttr, + attr.is("RandomMovesetMoveAttr") || + attr.is("RandomMoveAttr") || + attr.is("NaturePowerAttr") || + attr.is("CopyMoveAttr"), ) ) { const moveType = pokemon.getMoveType(move); @@ -2536,33 +2735,26 @@ 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 { + canApplyExecutedMove(_pokemon: Pokemon, _simulated: boolean): boolean { return true; } - applyExecutedMove( - _pokemon: Pokemon, - _simulated: boolean, - ): void {} + applyExecutedMove(_pokemon: Pokemon, _simulated: boolean): void {} } /** - * 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: boolean = false) { + 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 { @@ -2668,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 })) && @@ -2733,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 })) && @@ -3326,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; @@ -4499,7 +4691,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( @@ -4926,13 +5118,13 @@ function getAnticipationCondition(): AbAttrCondition { } // the move's base type (not accounting for variable type changes) is super effective if ( - move.getMove() instanceof AttackMove && + move.getMove().is("AttackMove") && pokemon.getAttackTypeEffectiveness(move.getMove().type, opponent, true, undefined, move.getMove()) >= 2 ) { return true; } // move is a OHKO - if (move.getMove().hasAttr(OneHitKOAttr)) { + if (move.getMove().hasAttr("OneHitKOAttr")) { return true; } // edge case for hidden power, type is computed @@ -5001,9 +5193,9 @@ export class ForewarnAbAttr extends PostSummonAbAttr { let movePower = 0; for (const opponent of pokemon.getOpponents()) { for (const move of opponent.moveset) { - if (move?.getMove() instanceof StatusMove) { + if (move?.getMove().is("StatusMove")) { movePower = 1; - } else if (move?.getMove().hasAttr(OneHitKOAttr)) { + } else if (move?.getMove().hasAttr("OneHitKOAttr")) { movePower = 150; } else if ( move?.getMove().id === MoveId.COUNTER || @@ -5261,7 +5453,7 @@ export class PostWeatherLapseDamageAbAttr extends PostWeatherLapseAbAttr { _weather: Weather | null, _args: any[], ): boolean { - return !pokemon.hasAbilityWithAttr(BlockNonDirectDamageAbAttr); + return !pokemon.hasAbilityWithAttr("BlockNonDirectDamageAbAttr"); } override applyPostWeatherLapse( @@ -5562,7 +5754,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)); } /** @@ -5689,7 +5881,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, ); } @@ -5704,7 +5896,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) { @@ -5872,14 +6064,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() instanceof AttackMove || move.getMove() instanceof StatusMove) { + if (move.getMove().is("AttackMove") || move.getMove().is("StatusMove")) { const target = this.getTarget(dancer, source, targets); - phaseManager.unshiftNew("MovePhase", dancer, target, move, true, true); - } else if (move.getMove() instanceof SelfStatusMove) { + 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, + ); } } } @@ -6344,8 +6541,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( @@ -6504,7 +6701,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; } @@ -7178,64 +7375,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) {} @@ -7245,6 +7384,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; /** @@ -7347,7 +7487,7 @@ class ForceSwitchOutHelper { if (player) { const blockedByAbility = new BooleanHolder(false); - applyAbAttrs(ForceSwitchOutImmunityAbAttr, opponent, blockedByAbility); + applyAbAttrs("ForceSwitchOutImmunityAbAttr", opponent, blockedByAbility); return !blockedByAbility.value; } @@ -7385,7 +7525,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; @@ -7540,646 +7680,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: boolean = 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. @@ -8207,7 +7934,7 @@ export function initAbilities() { allAbilities.push( new Ability(AbilityId.NONE, 3), new Ability(AbilityId.STENCH, 3) - .attr(PostAttackApplyBattlerTagAbAttr, false, (user, target, move) => !move.hasAttr(FlinchAttr) && !move.hitsSubstitute(user, target) ? 10 : 0, BattlerTagType.FLINCHED), + .attr(PostAttackApplyBattlerTagAbAttr, false, (user, target, move) => !move.hasAttr("FlinchAttr") && !move.hitsSubstitute(user, target) ? 10 : 0, BattlerTagType.FLINCHED), new Ability(AbilityId.DRIZZLE, 3) .attr(PostSummonWeatherChangeAbAttr, WeatherType.RAIN) .attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.RAIN), @@ -8387,7 +8114,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) @@ -8514,14 +8241,14 @@ export function initAbilities() { new Ability(AbilityId.TECHNICIAN, 4) .attr(MovePowerBoostAbAttr, (user, target, move) => { const power = new NumberHolder(move.power); - applyMoveAttrs(VariablePowerAttr, user, target, move, power); + applyMoveAttrs("VariablePowerAttr", user, target, move, power); return power.value <= 60; }, 1.5), new Ability(AbilityId.LEAF_GUARD, 4) .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) })) @@ -8573,7 +8300,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) @@ -8595,7 +8322,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), @@ -8635,7 +8362,7 @@ export function initAbilities() { ) .edgeCase(), // Cannot recover berries used up by fling or natural gift (unimplemented) new Ability(AbilityId.TELEPATHY, 5) - .attr(MoveImmunityAbAttr, (pokemon, attacker, move) => pokemon.getAlly() === attacker && move instanceof AttackMove) + .attr(MoveImmunityAbAttr, (pokemon, attacker, move) => pokemon.getAlly() === attacker && move.is("AttackMove")) .ignorable(), new Ability(AbilityId.MOODY, 5) .attr(MoodyAbAttr), @@ -8658,10 +8385,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) @@ -8837,7 +8564,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)) @@ -8875,7 +8602,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) @@ -8935,7 +8662,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) @@ -9044,7 +8777,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 @@ -9064,7 +8797,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 ]), @@ -9076,10 +8809,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() @@ -9111,14 +8846,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) @@ -9176,12 +8911,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() @@ -9225,7 +8960,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) @@ -9247,7 +8982,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), @@ -9271,7 +9006,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 28b8c6acd41..494a0438b18 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -7,31 +7,20 @@ import { MoveTarget } from "#enums/MoveTarget"; import { MoveCategory } from "#enums/MoveCategory"; import { getPokemonNameWithAffix } from "#app/messages"; import type Pokemon from "#app/field/pokemon"; -import { HitResult } from "#app/field/pokemon"; +import { HitResult } from "#enums/hit-result"; import { StatusEffect } from "#enums/status-effect"; -import type { BattlerIndex } from "#app/battle"; -import { - BlockNonDirectDamageAbAttr, - InfiltratorAbAttr, - PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr, - ProtectStatAbAttr, - applyAbAttrs, - applyOnGainAbAttrs, - applyOnLoseAbAttrs, -} from "#app/data/abilities/ability"; +import type { BattlerIndex } from "#enums/battler-index"; +import { applyAbAttrs, applyOnGainAbAttrs, applyOnLoseAbAttrs } from "./abilities/apply-ab-attrs"; import { Stat } from "#enums/stat"; -import { CommonAnim, CommonBattleAnim } from "#app/data/battle-anims"; +import { CommonBattleAnim } from "#app/data/battle-anims"; +import { CommonAnim } from "#enums/move-anims-common"; import i18next from "i18next"; 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"; - -export enum ArenaTagSide { - BOTH, - PLAYER, - ENEMY, -} +import { ArenaTagSide } from "#enums/arena-tag-side"; +import { MoveUseMode } from "#enums/move-use-mode"; export abstract class ArenaTag { constructor( @@ -148,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; } @@ -213,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; } @@ -769,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; } @@ -887,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? } @@ -957,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; } @@ -1014,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; @@ -1448,8 +1437,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")); } } @@ -1461,7 +1450,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..d307f5cfbf6 100644 --- a/src/data/balance/pokemon-evolutions.ts +++ b/src/data/balance/pokemon-evolutions.ts @@ -11,7 +11,6 @@ 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 { speciesStarterCosts } from "./starters"; import i18next from "i18next"; @@ -275,9 +274,9 @@ class MoveTypeEvolutionCondition extends SpeciesEvolutionCondition { 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); + + p.getHeldItems().filter(m => m.is("DamageMoneyRewardModifier")).length + + globalScene.findModifiers(m => m.is("MoneyMultiplierModifier") + || m.is("ExtraModifierModifier") || m.is("TempExtraModifierModifier")).length > 9); this.description = i18next.t("pokemonEvolutions:treasure"); } } @@ -1794,8 +1793,8 @@ export const pokemonEvolutions: PokemonEvolutions = { ], [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, new SpeciesEvolutionCondition(p => p.getHeldItems().some(m => m.is("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.is("SpeciesStatBoosterModifier") && (m.type as SpeciesStatBoosterModifierType).key === "DEEP_SEA_SCALE")), SpeciesWildEvolutionDelay.VERY_LONG) ], [SpeciesId.BOLDORE]: [ new SpeciesEvolution(SpeciesId.GIGALITH, 1, EvolutionItem.LINKING_CORD, null, 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 321d9938b2f..be060b57e9c 100644 --- a/src/data/battle-anims.ts +++ b/src/data/battle-anims.ts @@ -1,111 +1,22 @@ import { globalScene } from "#app/global-scene"; -import { AttackMove, BeakBlastHeaderAttr, DelayedAttackAttr, SelfStatusMove } from "./moves/move"; -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 { BattlerIndex } from "../battle"; +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"; - -export enum AnimFrameTarget { - USER, - TARGET, - GRAPHIC, -} - -enum AnimFocus { - TARGET = 1, - USER, - USER_TARGET, - SCREEN, -} - -enum AnimBlendType { - NORMAL, - ADD, - SUBTRACT, -} - -export enum ChargeAnim { - FLY_CHARGING = 1000, - BOUNCE_CHARGING, - DIG_CHARGING, - FUTURE_SIGHT_CHARGING, - DIVE_CHARGING, - SOLAR_BEAM_CHARGING, - SHADOW_FORCE_CHARGING, - SKULL_BASH_CHARGING, - FREEZE_SHOCK_CHARGING, - SKY_DROP_CHARGING, - SKY_ATTACK_CHARGING, - ICE_BURN_CHARGING, - DOOM_DESIRE_CHARGING, - RAZOR_WIND_CHARGING, - PHANTOM_FORCE_CHARGING, - GEOMANCY_CHARGING, - SHADOW_BLADE_CHARGING, - SOLAR_BLADE_CHARGING, - BEAK_BLAST_CHARGING, - METEOR_BEAM_CHARGING, - ELECTRO_SHOT_CHARGING, -} - -export enum CommonAnim { - USE_ITEM = 2000, - HEALTH_UP, - TERASTALLIZE, - POISON = 2010, - TOXIC, - PARALYSIS, - SLEEP, - FROZEN, - BURN, - CONFUSION, - ATTRACT, - BIND, - WRAP, - CURSE_NO_GHOST, - LEECH_SEED, - FIRE_SPIN, - PROTECT, - COVET, - WHIRLPOOL, - BIDE, - SAND_TOMB, - QUICK_GUARD, - WIDE_GUARD, - CURSE, - MAGMA_STORM, - CLAMP, - SNAP_TRAP, - THUNDER_CAGE, - INFESTATION, - ORDER_UP_CURLY, - ORDER_UP_DROOPY, - ORDER_UP_STRETCHY, - RAGING_BULL_FIRE, - RAGING_BULL_WATER, - SALT_CURE, - POWDER, - SUNNY = 2100, - RAIN, - SANDSTORM, - HAIL, - SNOW, - WIND, - HEAVY_RAIN, - HARSH_SUN, - STRONG_WINDS, - MISTY_TERRAIN = 2110, - ELECTRIC_TERRAIN, - GRASSY_TERRAIN, - PSYCHIC_TERRAIN, - LOCK_ON = 2120, -} +import { AnimBlendType, AnimFrameTarget, AnimFocus, ChargeAnim, CommonAnim } from "#enums/move-anims-common"; +import { BattlerTagType } from "#enums/battler-tag-type"; export class AnimConfig { public id: number; @@ -531,7 +442,7 @@ export function initMoveAnim(move: MoveId): Promise { if (moveAnims.get(move) !== null) { const chargeAnimSource = allMoves[move].isChargingMove() ? allMoves[move] - : (allMoves[move].getAttrs(DelayedAttackAttr)[0] ?? allMoves[move].getAttrs(BeakBlastHeaderAttr)[0]); + : (allMoves[move].getAttrs("DelayedAttackAttr")[0] ?? allMoves[move].getAttrs("BeakBlastHeaderAttr")[0]); if (chargeAnimSource && chargeAnims.get(chargeAnimSource.chargeAnim) === null) { return; } @@ -542,12 +453,11 @@ export function initMoveAnim(move: MoveId): Promise { } } else { moveAnims.set(move, null); - const defaultMoveAnim = - allMoves[move] instanceof AttackMove - ? MoveId.TACKLE - : allMoves[move] instanceof SelfStatusMove - ? MoveId.FOCUS_ENERGY - : MoveId.TAIL_WHIP; + const defaultMoveAnim = allMoves[move].is("AttackMove") + ? MoveId.TACKLE + : allMoves[move].is("SelfStatusMove") + ? MoveId.FOCUS_ENERGY + : MoveId.TAIL_WHIP; const fetchAnimAndResolve = (move: MoveId) => { globalScene @@ -570,7 +480,7 @@ export function initMoveAnim(move: MoveId): Promise { } const chargeAnimSource = allMoves[move].isChargingMove() ? allMoves[move] - : (allMoves[move].getAttrs(DelayedAttackAttr)[0] ?? allMoves[move].getAttrs(BeakBlastHeaderAttr)[0]); + : (allMoves[move].getAttrs("DelayedAttackAttr")[0] ?? allMoves[move].getAttrs("BeakBlastHeaderAttr")[0]); if (chargeAnimSource) { initMoveChargeAnim(chargeAnimSource.chargeAnim).then(() => resolve()); } else { @@ -616,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) { @@ -703,7 +613,7 @@ export function loadMoveAnimAssets(moveIds: MoveId[], startLoad?: boolean): Prom for (const moveId of moveIds) { const chargeAnimSource = allMoves[moveId].isChargingMove() ? allMoves[moveId] - : (allMoves[moveId].getAttrs(DelayedAttackAttr)[0] ?? allMoves[moveId].getAttrs(BeakBlastHeaderAttr)[0]); + : (allMoves[moveId].getAttrs("DelayedAttackAttr")[0] ?? allMoves[moveId].getAttrs("BeakBlastHeaderAttr")[0]); if (chargeAnimSource) { const moveChargeAnims = chargeAnims.get(chargeAnimSource.chargeAnim); moveAnimations.push(moveChargeAnims instanceof AnimConfig ? moveChargeAnims : moveChargeAnims![0]); // TODO: is the bang correct? @@ -867,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? @@ -941,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 d4f62237446..8405fd1dd4d 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -1,37 +1,27 @@ 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 { ChargeAnim, CommonAnim, CommonBattleAnim, MoveChargeAnim } from "#app/data/battle-anims"; +import { CommonBattleAnim, MoveChargeAnim } from "#app/data/battle-anims"; +import { ChargeAnim, CommonAnim } from "#enums/move-anims-common"; import type Move from "#app/data/moves/move"; -import { - applyMoveAttrs, - ConsecutiveUseDoublePowerAttr, - HealOnAllyAttr, - StatusCategoryOnAllyAttr, -} from "#app/data/moves/move"; +import { applyMoveAttrs } from "./moves/apply-attrs"; import { allMoves } from "./data-lists"; import { MoveFlags } from "#enums/MoveFlags"; import { MoveCategory } from "#enums/MoveCategory"; -import { SpeciesFormChangeAbilityTrigger } from "#app/data/pokemon-forms"; +import { SpeciesFormChangeAbilityTrigger } from "./pokemon-forms/form-change-triggers"; import { getStatusEffectHealText } from "#app/data/status-effect"; import { TerrainType } from "#app/data/terrain"; import { PokemonType } from "#enums/pokemon-type"; import type Pokemon from "#app/field/pokemon"; -import { HitResult, MoveResult } from "#app/field/pokemon"; +import { MoveResult } from "#enums/move-result"; +import { HitResult } from "#enums/hit-result"; import { getPokemonNameWithAffix } from "#app/messages"; 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"; @@ -41,20 +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"; -export enum BattlerTagLapseType { - FAINT, - MOVE, - PRE_MOVE, - AFTER_MOVE, - MOVE_EFFECT, - TURN_END, - HIT, - /** Tag lapses AFTER_HIT, applying its effects even if the user faints */ - AFTER_HIT, - CUSTOM, -} - +/** + * 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[]; @@ -72,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; @@ -91,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) @@ -147,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) { @@ -299,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( @@ -359,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; @@ -368,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); } @@ -410,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; - } } /** @@ -446,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 */ @@ -663,7 +621,7 @@ export class FlinchedTag extends BattlerTag { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), }), ); - applyAbAttrs(FlinchEffectAbAttr, pokemon, null); + applyAbAttrs("FlinchEffectAbAttr", pokemon, null); return true; } @@ -692,6 +650,7 @@ export class InterruptedTag extends BattlerTag { move: MoveId.NONE, result: MoveResult.OTHER, targets: [], + useMode: MoveUseMode.NORMAL, }); } @@ -957,7 +916,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( @@ -968,7 +927,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(), @@ -1017,42 +976,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; } } @@ -1094,7 +1056,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 }); @@ -1147,34 +1109,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( @@ -1190,7 +1140,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, + ), ); } } @@ -1453,7 +1409,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 }); @@ -1492,16 +1448,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), @@ -1696,7 +1642,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, @@ -1926,24 +1872,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; } } @@ -2292,7 +2243,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); @@ -2346,7 +2297,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 }); @@ -2681,7 +2632,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 }); @@ -2804,8 +2755,8 @@ export class HealBlockTag extends MoveRestrictionBattlerTag { */ override isMoveTargetRestricted(move: MoveId, user: Pokemon, target: Pokemon) { const moveCategory = new NumberHolder(allMoves[move].category); - applyMoveAttrs(StatusCategoryOnAllyAttr, user, target, allMoves[move], moveCategory); - return allMoves[move].hasAttr(HealOnAllyAttr) && moveCategory.value === MoveCategory.STATUS; + applyMoveAttrs("StatusCategoryOnAllyAttr", user, target, allMoves[move], moveCategory); + return allMoves[move].hasAttr("HealOnAllyAttr") && moveCategory.value === MoveCategory.STATUS; } /** @@ -3071,8 +3022,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); @@ -3141,7 +3092,7 @@ export class TormentTag extends MoveRestrictionBattlerTag { // This checks for locking / momentum moves like Rollout and Hydro Cannon + if the user is under the influence of BattlerTagType.FRENZY // Because Uproar's unique behavior is not implemented, it does not check for Uproar. Torment has been marked as partial in moves.ts const moveObj = allMoves[lastMove.move]; - const isUnaffected = moveObj.hasAttr(ConsecutiveUseDoublePowerAttr) || user.getTag(BattlerTagType.FRENZY); + const isUnaffected = moveObj.hasAttr("ConsecutiveUseDoublePowerAttr") || user.getTag(BattlerTagType.FRENZY); const validLastMoveResult = lastMove.result === MoveResult.SUCCESS || lastMove.result === MoveResult.MISS; return lastMove.move === move && validLastMoveResult && lastMove.move !== MoveId.STRUGGLE && !isUnaffected; } diff --git a/src/data/berry.ts b/src/data/berry.ts index 52ea1c44391..7d1e62362a8 100644 --- a/src/data/berry.ts +++ b/src/data/berry.ts @@ -1,9 +1,9 @@ import { getPokemonNameWithAffix } from "../messages"; import type Pokemon from "../field/pokemon"; -import { HitResult } 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 8dd303c34fd..683fb48a9ba 100644 --- a/src/data/challenge.ts +++ b/src/data/challenge.ts @@ -2,17 +2,18 @@ import { BooleanHolder, type NumberHolder, randSeedItem } from "#app/utils/commo import { deepCopy } from "#app/utils/data"; import i18next from "i18next"; import type { DexAttrProps, GameData } from "#app/system/game-data"; -import { defaultStarterSpecies } 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 { speciesStarterCosts } from "#app/data/balance/starters"; import type Pokemon from "#app/field/pokemon"; -import { PokemonMove } from "#app/field/pokemon"; +import { PokemonMove } from "./moves/pokemon-move"; import type { FixedBattleConfig } from "#app/battle"; import { getRandomTrainerFunc } from "#app/battle"; import { ClassicFixedBossWaves } from "#enums/fixed-boss-waves"; import { BattleType } from "#enums/battle-type"; -import Trainer, { TrainerVariant } from "#app/field/trainer"; +import Trainer from "#app/field/trainer"; +import { TrainerVariant } from "#enums/trainer-variant"; import { PokemonType } from "#enums/pokemon-type"; import { Challenges } from "#enums/challenges"; import { SpeciesId } from "#enums/species-id"; @@ -20,97 +21,16 @@ 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"; +import { ChallengeType } from "#enums/challenge-type"; +import type { MoveSourceType } from "#enums/move-source-type"; /** A constant for the default max cost of the starting party before a run */ const DEFAULT_PARTY_MAX_COST = 10; -/** - * An enum for all the challenge types. The parameter entries on these describe the - * parameters to use when calling the applyChallenges function. - */ -export enum ChallengeType { - /** - * Challenges which modify what starters you can choose - * @see {@link Challenge.applyStarterChoice} - */ - STARTER_CHOICE, - /** - * Challenges which modify how many starter points you have - * @see {@link Challenge.applyStarterPoints} - */ - STARTER_POINTS, - /** - * Challenges which modify how many starter points you have - * @see {@link Challenge.applyStarterPointCost} - */ - STARTER_COST, - /** - * Challenges which modify your starters in some way - * @see {@link Challenge.applyStarterModify} - */ - STARTER_MODIFY, - /** - * Challenges which limit which pokemon you can have in battle. - * @see {@link Challenge.applyPokemonInBattle} - */ - POKEMON_IN_BATTLE, - /** - * Adds or modifies the fixed battles in a run - * @see {@link Challenge.applyFixedBattle} - */ - FIXED_BATTLES, - /** - * Modifies the effectiveness of Type matchups in battle - * @see {@linkcode Challenge.applyTypeEffectiveness} - */ - TYPE_EFFECTIVENESS, - /** - * Modifies what level the AI pokemon are. UNIMPLEMENTED. - */ - AI_LEVEL, - /** - * Modifies how many move slots the AI has. UNIMPLEMENTED. - */ - AI_MOVE_SLOTS, - /** - * Modifies if a pokemon has its passive. UNIMPLEMENTED. - */ - PASSIVE_ACCESS, - /** - * Modifies the game mode settings in some way. UNIMPLEMENTED. - */ - GAME_MODE_MODIFY, - /** - * Modifies what level AI pokemon can access a move. UNIMPLEMENTED. - */ - MOVE_ACCESS, - /** - * Modifies what weight AI pokemon have when generating movesets. UNIMPLEMENTED. - */ - MOVE_WEIGHT, - /** - * Modifies what the pokemon stats for Flip Stat Mode. - */ - FLIP_STAT, -} - -/** - * Used for challenge types that modify movesets, these denote the various sources of moves for pokemon. - */ -export enum MoveSourceType { - LEVEL_UP, // Currently unimplemented for move access - RELEARNER, // Relearner moves currently unimplemented - COMMON_TM, - GREAT_TM, - ULTRA_TM, - COMMON_EGG, - RARE_EGG, -} - /** * A challenge object. Exists only to serve as a base class. */ diff --git a/src/data/data-lists.ts b/src/data/data-lists.ts index c763a001280..4d482dc2d7d 100644 --- a/src/data/data-lists.ts +++ b/src/data/data-lists.ts @@ -1,5 +1,9 @@ -import type { Ability } from "./abilities/ability-class"; +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[] = []; + +// 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/moves/apply-attrs.ts b/src/data/moves/apply-attrs.ts new file mode 100644 index 00000000000..98e7b6d791f --- /dev/null +++ b/src/data/moves/apply-attrs.ts @@ -0,0 +1,58 @@ +/** + * Module holding functions to apply move attributes. + * Must not import anything that is not a type. + */ +import type Pokemon from "#app/field/pokemon"; +import type { default as Move, MoveAttr } from "./move"; +import type { ChargingMove } from "#app/@types/move-types"; +import type { MoveAttrFilter, MoveAttrString } from "#app/@types/move-types"; + +function applyMoveAttrsInternal( + attrFilter: MoveAttrFilter, + user: Pokemon | null, + target: Pokemon | null, + move: Move, + args: any[], +): void { + move.attrs.filter(attr => attrFilter(attr)).forEach(attr => attr.apply(user, target, move, args)); +} + +function applyMoveChargeAttrsInternal( + attrFilter: MoveAttrFilter, + user: Pokemon | null, + target: Pokemon | null, + move: ChargingMove, + args: any[], +): void { + move.chargeAttrs.filter(attr => attrFilter(attr)).forEach(attr => attr.apply(user, target, move, args)); +} + +export function applyMoveAttrs( + attrType: MoveAttrString, + user: Pokemon | null, + target: Pokemon | null, + move: Move, + ...args: any[] +): void { + applyMoveAttrsInternal((attr: MoveAttr) => attr.is(attrType), user, target, move, args); +} + +export function applyFilteredMoveAttrs( + attrFilter: MoveAttrFilter, + user: Pokemon, + target: Pokemon | null, + move: Move, + ...args: any[] +): void { + applyMoveAttrsInternal(attrFilter, user, target, move, args); +} + +export function applyMoveChargeAttrs( + attrType: MoveAttrString, + user: Pokemon | null, + target: Pokemon | null, + move: ChargingMove, + ...args: any[] +): void { + applyMoveChargeAttrsInternal((attr: MoveAttr) => attr.is(attrType), user, target, move, args); +} 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-utils.ts b/src/data/moves/move-utils.ts index 3323d6f4a0c..20723ab0347 100644 --- a/src/data/moves/move-utils.ts +++ b/src/data/moves/move-utils.ts @@ -1,5 +1,14 @@ -import { MoveTarget } from "#enums/MoveTarget"; +import type Pokemon from "#app/field/pokemon"; +import type { BattlerIndex } from "#enums/battler-index"; +import type { MoveId } from "#enums/move-id"; +import type { MoveTargetSet, UserMoveConditionFunc } from "./move"; import type Move from "./move"; +import { NumberHolder, isNullOrUndefined } from "#app/utils/common"; +import { MoveTarget } from "#enums/MoveTarget"; +import { PokemonType } from "#enums/pokemon-type"; +import { allMoves } from "#app/data/data-lists"; +import { applyMoveAttrs } from "./apply-attrs"; +import { BattlerTagType } from "#enums/battler-tag-type"; /** * Return whether the move targets the field @@ -18,3 +27,88 @@ export function isFieldTargeted(move: Move): boolean { } return false; } + +export function getMoveTargets(user: Pokemon, move: MoveId, replaceTarget?: MoveTarget): MoveTargetSet { + const variableTarget = new NumberHolder(0); + user.getOpponents(false).forEach(p => applyMoveAttrs("VariableTargetAttr", user, p, allMoves[move], variableTarget)); + + let moveTarget: MoveTarget | undefined; + if (allMoves[move].hasAttr("VariableTargetAttr")) { + moveTarget = variableTarget.value; + } else if (replaceTarget !== undefined) { + moveTarget = replaceTarget; + } else if (move) { + moveTarget = allMoves[move].moveTarget; + } else if (move === undefined) { + moveTarget = MoveTarget.NEAR_ENEMY; + } + const opponents = user.getOpponents(false); + + let set: Pokemon[] = []; + let multiple = false; + const ally: Pokemon | undefined = user.getAlly(); + + switch (moveTarget) { + case MoveTarget.USER: + case MoveTarget.PARTY: + set = [user]; + break; + case MoveTarget.NEAR_OTHER: + case MoveTarget.OTHER: + case MoveTarget.ALL_NEAR_OTHERS: + case MoveTarget.ALL_OTHERS: + set = !isNullOrUndefined(ally) ? opponents.concat([ally]) : opponents; + multiple = moveTarget === MoveTarget.ALL_NEAR_OTHERS || moveTarget === MoveTarget.ALL_OTHERS; + break; + case MoveTarget.NEAR_ENEMY: + case MoveTarget.ALL_NEAR_ENEMIES: + case MoveTarget.ALL_ENEMIES: + case MoveTarget.ENEMY_SIDE: + set = opponents; + multiple = moveTarget !== MoveTarget.NEAR_ENEMY; + break; + case MoveTarget.RANDOM_NEAR_ENEMY: + set = [opponents[user.randBattleSeedInt(opponents.length)]]; + break; + case MoveTarget.ATTACKER: + return { targets: [-1 as BattlerIndex], multiple: false }; + case MoveTarget.NEAR_ALLY: + case MoveTarget.ALLY: + set = !isNullOrUndefined(ally) ? [ally] : []; + break; + case MoveTarget.USER_OR_NEAR_ALLY: + case MoveTarget.USER_AND_ALLIES: + case MoveTarget.USER_SIDE: + set = !isNullOrUndefined(ally) ? [user, ally] : [user]; + multiple = moveTarget !== MoveTarget.USER_OR_NEAR_ALLY; + break; + case MoveTarget.ALL: + case MoveTarget.BOTH_SIDES: + set = (!isNullOrUndefined(ally) ? [user, ally] : [user]).concat(opponents); + multiple = true; + break; + case MoveTarget.CURSE: + { + const extraTargets = !isNullOrUndefined(ally) ? [ally] : []; + set = user.getTypes(true).includes(PokemonType.GHOST) ? opponents.concat(extraTargets) : [user]; + } + break; + } + + return { + targets: set + .filter(p => p?.isActive(true)) + .map(p => p.getBattlerIndex()) + .filter(t => t !== undefined), + multiple, + }; +} + +export const frenzyMissFunc: UserMoveConditionFunc = (user: Pokemon, move: Move) => { + while (user.getMoveQueue().length && user.getMoveQueue()[0].move === move.id) { + user.getMoveQueue().shift(); + } + user.removeTag(BattlerTagType.FRENZY); // FRENZY tag should be disrupted on miss/no effect + + return true; +}; diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 976c0cd7d97..f61e8debc9f 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -1,4 +1,5 @@ -import { ChargeAnim, MoveChargeAnim } from "../battle-anims"; +import { MoveChargeAnim } from "../battle-anims"; +import { ChargeAnim } from "#enums/move-anims-common"; import { CommandedTag, EncoreTag, @@ -14,14 +15,11 @@ import { import { getPokemonNameWithAffix } from "../../messages"; import type { AttackMoveResult, TurnMove } from "../../field/pokemon"; import type Pokemon from "../../field/pokemon"; -import { - EnemyPokemon, - FieldPosition, - HitResult, - MoveResult, - PlayerPokemon, - PokemonMove, -} from "../../field/pokemon"; +import type { EnemyPokemon } from "#app/field/pokemon"; +import { PokemonMove } from "./pokemon-move"; +import { MoveResult } from "#enums/move-result"; +import { HitResult } from "#enums/hit-result"; +import { FieldPosition } from "#enums/field-position"; import { getNonVolatileStatusEffects, getStatusEffectHealText, @@ -32,40 +30,15 @@ import { PokemonType } from "#enums/pokemon-type"; import { BooleanHolder, NumberHolder, isNullOrUndefined, toDmgValue, randSeedItem, randSeedInt, getEnumValues, toReadableString, type Constructor, randSeedFloat } from "#app/utils/common"; import { WeatherType } from "#enums/weather-type"; import type { ArenaTrapTag } from "../arena-tag"; -import { ArenaTagSide, WeakenMoveTypeTag } 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, @@ -75,11 +48,11 @@ import { PokemonMultiHitModifier, PreserveBerryModifier, } from "../../modifier/modifier"; -import type { BattlerIndex } from "../../battle"; +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 { Command } from "../../ui/command-ui-handler"; +import { ModifierPoolType } from "#enums/modifier-pool-type"; +import { Command } from "#enums/command"; import i18next from "i18next"; import type { Localizable } from "#app/@types/locales"; import { getBerryEffectFunc } from "../berry"; @@ -97,37 +70,33 @@ 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"; +import { SpeciesFormChangeRevertWeatherFormTrigger } from "../pokemon-forms/form-change-triggers"; import type { GameMode } from "#app/game-mode"; -import { applyChallenges, ChallengeType } from "../challenge"; +import { applyChallenges } from "../challenge"; +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"; type MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => boolean; -type UserMoveConditionFunc = (user: Pokemon, move: Move) => boolean; +export type UserMoveConditionFunc = (user: Pokemon, move: Move) => boolean; -export default class Move implements Localizable { +export default abstract class Move implements Localizable { public id: MoveId; public name: string; private _type: PokemonType; @@ -147,6 +116,17 @@ export default class Move implements Localizable { private flags: number = 0; private nameAppend: string = ""; + /** + * Check if the move is of the given subclass without requiring `instanceof`. + * + * ⚠️ Does _not_ work for {@linkcode ChargingAttackMove} and {@linkcode ChargingSelfStatusMove} subclasses. For those, + * use {@linkcode isChargingMove} instead. + * + * @param moveKind - The string name of the move to check against + * @returns Whether this move is of the provided type. + */ + public abstract is(moveKind: K): this is MoveClassMap[K]; + constructor(id: MoveId, type: PokemonType, category: MoveCategory, defaultMoveTarget: MoveTarget, power: number, accuracy: number, pp: number, chance: number, priority: number, generation: number) { this.id = id; this._type = type; @@ -188,8 +168,12 @@ export default class Move implements Localizable { * @param attrType any attribute that extends {@linkcode MoveAttr} * @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); + getAttrs(attrType: T): (MoveAttrMap[T])[] { + const targetAttr = MoveAttrs[attrType]; + if (!targetAttr) { + return []; + } + return this.attrs.filter((a): a is MoveAttrMap[T] => a instanceof targetAttr); } /** @@ -197,8 +181,13 @@ export default class Move implements Localizable { * @param attrType any attribute that extends {@linkcode MoveAttr} * @returns true if the move has attribute `attrType` */ - hasAttr(attrType: Constructor): boolean { - return this.attrs.some((attr) => attr instanceof attrType); + hasAttr(attrType: MoveAttrString): boolean { + const targetAttr = MoveAttrs[attrType]; + // Guard against invalid attrType + if (!targetAttr) { + return false; + } + return this.attrs.some((attr) => attr instanceof targetAttr); } /** @@ -354,7 +343,7 @@ export default 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) @@ -645,14 +634,14 @@ export default 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; } @@ -661,7 +650,7 @@ export default 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; } @@ -672,7 +661,7 @@ export default 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; } @@ -768,14 +757,14 @@ export default class Move implements Localizable { calculateBattleAccuracy(user: Pokemon, target: Pokemon, simulated: boolean = false) { const moveAccuracy = new NumberHolder(this.accuracy); - applyMoveAttrs(VariableAccuracyAttr, user, target, this, moveAccuracy); - applyPreDefendAbAttrs(WonderSkinAbAttr, target, user, this, { value: false }, simulated, moveAccuracy); + applyMoveAttrs("VariableAccuracyAttr", user, target, this, moveAccuracy); + applyPreDefendAbAttrs("WonderSkinAbAttr", target, user, this, { value: false }, simulated, moveAccuracy); if (moveAccuracy.value === -1) { return moveAccuracy.value; } - const isOhko = this.hasAttr(OneHitKOAccuracyAttr); + const isOhko = this.hasAttr("OneHitKOAccuracyAttr"); if (!isOhko) { globalScene.applyModifiers(PokemonMoveAccuracyBoosterModifier, user.isPlayer(), user, moveAccuracy); @@ -812,25 +801,25 @@ export default 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)) { + 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) { @@ -838,7 +827,7 @@ export default 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; @@ -847,9 +836,9 @@ export default class Move implements Localizable { power.value *= typeBoost.boostValue; } - applyMoveAttrs(VariablePowerAttr, source, target, this, power); + applyMoveAttrs("VariablePowerAttr", source, target, this, power); - if (!this.hasAttr(TypelessAttr)) { + if (!this.hasAttr("TypelessAttr")) { globalScene.arena.applyTags(WeakenMoveTypeTag, simulated, typeChangeHolder.value, power); globalScene.applyModifiers(AttackTypeBoosterModifier, source.isPlayer(), source, typeChangeHolder.value, power); } @@ -864,8 +853,8 @@ export default class Move implements Localizable { getPriority(user: Pokemon, simulated: boolean = true) { const priority = new NumberHolder(this.priority); - applyMoveAttrs(IncrementMovePriorityAttr, user, null, this, priority); - applyAbAttrs(ChangeMovePriorityAbAttr, user, null, simulated, this, priority); + applyMoveAttrs("IncrementMovePriorityAttr", user, null, this, priority); + applyAbAttrs("ChangeMovePriorityAbAttr", user, null, simulated, this, priority); return priority.value; } @@ -885,7 +874,7 @@ export default class Move implements Localizable { } else if (this.id === MoveId.TRIPLE_KICK) { effectivePower = 47.07; } else { - const multiHitAttr = this.getAttrs(MultiHitAttr)[0]; + const multiHitAttr = this.getAttrs("MultiHitAttr")[0]; if (multiHitAttr) { effectivePower = multiHitAttr.calculateExpectedHitCount(this) * this.power; } else { @@ -898,10 +887,10 @@ export default class Move implements Localizable { // These are intentionally not else-if statements even though there are no // pokemon moves that have more than one of these attributes. This allows // the function to future proof new moves / custom move behaviors. - if (this.hasAttr(DelayedAttackAttr)) { + if (this.hasAttr("DelayedAttackAttr")) { numTurns += 2; } - if (this.hasAttr(RechargeAttr)) { + if (this.hasAttr("RechargeAttr")) { numTurns += 1; } if (this.isChargingMove()) { @@ -927,10 +916,10 @@ export default class Move implements Localizable { const isMultiTarget = multiple && targets.length > 1; // ...cannot enhance multi-hit or sacrificial moves - const exceptAttrs: Constructor[] = [ - MultiHitAttr, - SacrificialAttr, - SacrificialAttrOnHit + const exceptAttrs: MoveAttrString[] = [ + "MultiHitAttr", + "SacrificialAttr", + "SacrificialAttrOnHit" ]; // ...and cannot enhance these specific moves @@ -956,6 +945,13 @@ export default class Move implements Localizable { } export class AttackMove extends Move { + /** 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. + */ + declare private _: never; + override is(moveKind: K): this is MoveClassMap[K] { + return moveKind === "AttackMove"; + } constructor(id: MoveId, type: PokemonType, category: MoveCategory, power: number, accuracy: number, pp: number, chance: number, priority: number, generation: number) { super(id, type, category, MoveTarget.NEAR_OTHER, power, accuracy, pp, chance, priority, generation); @@ -985,7 +981,7 @@ export class AttackMove extends Move { const [ thisStat, offStat ]: EffectiveStat[] = this.category === MoveCategory.PHYSICAL ? [ Stat.ATK, Stat.SPATK ] : [ Stat.SPATK, Stat.ATK ]; const statHolder = new NumberHolder(user.getEffectiveStat(thisStat, target)); const offStatValue = user.getEffectiveStat(offStat, target); - applyMoveAttrs(VariableAtkAttr, user, target, move, statHolder); + applyMoveAttrs("VariableAtkAttr", user, target, move, statHolder); const statRatio = offStatValue / statHolder.value; if (statRatio <= 0.75) { attackScore *= 2; @@ -994,7 +990,7 @@ export class AttackMove extends Move { } const power = new NumberHolder(this.calculateEffectivePower()); - applyMoveAttrs(VariablePowerAttr, user, target, move, power); + applyMoveAttrs("VariablePowerAttr", user, target, move, power); attackScore += Math.floor(power.value / 5); @@ -1003,20 +999,42 @@ export class AttackMove extends Move { } export class StatusMove extends Move { + /** 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. + */ + declare private _: never; constructor(id: MoveId, type: PokemonType, accuracy: number, pp: number, chance: number, priority: number, generation: number) { super(id, type, MoveCategory.STATUS, MoveTarget.NEAR_OTHER, -1, accuracy, pp, chance, priority, generation); } + + override is(moveKind: K): this is MoveClassMap[K] { + return moveKind === "StatusMove"; + } } export class SelfStatusMove extends Move { + /** 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. + */ + declare private _: never; constructor(id: MoveId, type: PokemonType, accuracy: number, pp: number, chance: number, priority: number, generation: number) { super(id, type, MoveCategory.STATUS, MoveTarget.USER, -1, accuracy, pp, chance, priority, generation); } + + override is(moveKind: K): this is MoveClassMap[K] { + return moveKind === "SelfStatusMove"; + } } -type SubMove = new (...args: any[]) => Move; +// TODO: Figure out how to improve the signature of this so that +// the `ChargeMove` function knows that the argument `Base` is a specific subclass of move that cannot +// be abstract. +// Right now, I only know how to do this by using the type conjunction (the & operators) +type SubMove = new (...args: any[]) => Move & { + is(moveKind: K): this is MoveClassMap[K]; +}; -function ChargeMove(Base: TBase) { +function ChargeMove(Base: TBase, nameAppend: string) { return class extends Base { /** The animation to play during the move's charging phase */ public readonly chargeAnim: ChargeAnim = ChargeAnim[`${MoveId[this.id]}_CHARGING`]; @@ -1060,8 +1078,12 @@ function ChargeMove(Base: TBase) { * @returns Array of attributes that match `attrType`, or an empty array if * no matches are found. */ - getChargeAttrs(attrType: Constructor): T[] { - return this.chargeAttrs.filter((attr): attr is T => attr instanceof attrType); + getChargeAttrs(attrType: T): (MoveAttrMap[T])[] { + const targetAttr = MoveAttrs[attrType]; + if (!targetAttr) { + return []; + } + return this.chargeAttrs.filter((attr): attr is MoveAttrMap[T] => attr instanceof targetAttr); } /** @@ -1069,8 +1091,12 @@ function ChargeMove(Base: TBase) { * @param attrType any attribute that extends {@linkcode MoveAttr} * @returns `true` if a matching attribute is found; `false` otherwise */ - hasChargeAttr(attrType: Constructor): boolean { - return this.chargeAttrs.some((attr) => attr instanceof attrType); + hasChargeAttr(attrType: T): boolean { + const targetAttr = MoveAttrs[attrType]; + if (!targetAttr) { + return false; + } + return this.chargeAttrs.some((attr) => attr instanceof targetAttr); } /** @@ -1088,10 +1114,8 @@ function ChargeMove(Base: TBase) { }; } -export class ChargingAttackMove extends ChargeMove(AttackMove) {} -export class ChargingSelfStatusMove extends ChargeMove(SelfStatusMove) {} - -export type ChargingMove = ChargingAttackMove | ChargingSelfStatusMove; +export class ChargingAttackMove extends ChargeMove(AttackMove, "ChargingAttackMove") {} +export class ChargingSelfStatusMove extends ChargeMove(SelfStatusMove, "ChargingSelfStatusMove") {} /** * Base class defining all {@linkcode Move} Attributes @@ -1102,6 +1126,22 @@ export abstract class MoveAttr { /** Should this {@linkcode Move} target the user? */ public selfTarget: boolean; + /** + * Return whether this attribute is of the given type. + * + * @remarks + * Used to avoid requring 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: T): this is MoveAttrMap[T] { + const targetAttr = MoveAttrs[attr]; + if (!targetAttr) { + return false; + } + return this instanceof targetAttr; + } + constructor(selfTarget: boolean = false) { this.selfTarget = selfTarget; } @@ -1266,15 +1306,15 @@ 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)) { + if ((!move.hasAttr("FlinchAttr") || moveChance.value <= move.chance) && !move.hasAttr("SecretPowerAttr")) { const userSide = user.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; globalScene.arena.applyTagsForSide(ArenaTagType.WATER_FIRE_PLEDGE, userSide, false, moveChance); } if (!selfEffect) { - applyPreDefendAbAttrs(IgnoreMoveEffectsAbAttr, target, user, null, null, !showAbility, moveChance); + applyPreDefendAbAttrs("IgnoreMoveEffectsAbAttr", target, user, null, null, !showAbility, moveChance); } return moveChance.value; } @@ -1652,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) { @@ -1786,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 @@ -1985,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) { @@ -2108,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: @@ -2215,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); @@ -2226,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 { @@ -2331,7 +2372,7 @@ export class MultiHitAttr extends MoveAttr { */ apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { const hitType = new NumberHolder(this.intrinsicMultiHitType); - applyMoveAttrs(ChangeMultiHitTypeAttr, user, target, move, hitType); + applyMoveAttrs("ChangeMultiHitTypeAttr", user, target, move, hitType); this.multiHitType = hitType.value; (args[0] as NumberHolder).value = this.getHitCount(user, target); @@ -2356,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) { @@ -2464,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; } } @@ -2620,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; @@ -2737,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); } } @@ -2763,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; } @@ -2777,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); @@ -2818,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; } @@ -2968,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; }; } @@ -3032,7 +3073,11 @@ export class WeatherInstantChargeAttr extends InstantChargeAttr { } export class OverrideMoveEffectAttr extends MoveAttr { - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + /** 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. + */ + declare private _: never; + override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { return true; } } @@ -3071,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 { @@ -3114,7 +3159,7 @@ export class AwaitCombinedPledgeAttr extends OverrideMoveEffectAttr { const allyMovePhase = globalScene.phaseManager.findPhase((phase) => phase.is("MovePhase") && phase.pokemon.isPlayer() === user.isPlayer()); if (allyMovePhase) { const allyMove = allyMovePhase.move.getMove(); - if (allyMove !== move && allyMove.hasAttr(AwaitCombinedPledgeAttr)) { + if (allyMove !== move && allyMove.hasAttr("AwaitCombinedPledgeAttr")) { [ user, allyMovePhase.pokemon ].forEach((p) => p.turnData.combiningPledge = move.id); // "{userPokemonName} is waiting for {allyPokemonName}'s move..." @@ -3235,22 +3280,22 @@ export class StatStageChangeAttr extends MoveEffectAttr { switch (stat) { case Stat.ATK: if (this.selfTarget) { - noEffect = !user.getMoveset().find(m => m instanceof AttackMove && m.category === MoveCategory.PHYSICAL); + noEffect = !user.getMoveset().find(m => {const mv = m.getMove(); return mv.is("AttackMove") && mv.category === MoveCategory.PHYSICAL;} ); } break; case Stat.DEF: if (!this.selfTarget) { - noEffect = !user.getMoveset().find(m => m instanceof AttackMove && m.category === MoveCategory.PHYSICAL); + noEffect = !user.getMoveset().find(m => {const mv = m.getMove(); return mv.is("AttackMove") && mv.category === MoveCategory.PHYSICAL;} ); } break; case Stat.SPATK: if (this.selfTarget) { - noEffect = !user.getMoveset().find(m => m instanceof AttackMove && m.category === MoveCategory.SPECIAL); + noEffect = !user.getMoveset().find(m => {const mv = m.getMove(); return mv.is("AttackMove") && mv.category === MoveCategory.PHYSICAL;} ); } break; case Stat.SPDEF: if (!this.selfTarget) { - noEffect = !user.getMoveset().find(m => m instanceof AttackMove && m.category === MoveCategory.SPECIAL); + noEffect = !user.getMoveset().find(m => {const mv = m.getMove(); return mv.is("AttackMove") && mv.category === MoveCategory.PHYSICAL;} ); } break; } @@ -4028,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; } } @@ -4105,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; @@ -5364,7 +5421,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; } @@ -5405,28 +5462,26 @@ 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 + applyMoveAttrs("AddBattlerTagAttr", user, target, move, args); + user.lapseTag(BattlerTagType.FRENZY); } return true; } } -export const frenzyMissFunc: UserMoveConditionFunc = (user: Pokemon, move: Move) => { - while (user.getMoveQueue().length && user.getMoveQueue()[0].move === move.id) { - user.getMoveQueue().shift(); - } - user.removeTag(BattlerTagType.FRENZY); // FRENZY tag should be disrupted on miss/no effect - - return true; -}; - /** * Attribute that grants {@link https://bulbapedia.bulbagarden.net/wiki/Semi-invulnerable_turn | semi-invulnerability} to the user during * the associated move's charging phase. Should only be used for {@linkcode ChargingMove | ChargingMoves} as a `chargeAttr`. @@ -5781,7 +5836,7 @@ export class ProtectAttr extends AddBattlerTagAttr { while (moveHistory.length) { turnMove = moveHistory.shift(); - if (!allMoves[turnMove?.move ?? MoveId.NONE].hasAttr(ProtectAttr) || turnMove?.result !== MoveResult.SUCCESS) { + if (!allMoves[turnMove?.move ?? MoveId.NONE].hasAttr("ProtectAttr") || turnMove?.result !== MoveResult.SUCCESS) { break; } timesUsed++; @@ -5912,7 +5967,7 @@ export class AddArenaTagAttr extends MoveEffectAttr { } if ((move.chance < 0 || move.chance === 100 || user.randBattleSeedInt(100) < move.chance) && user.getLastXMoves(1)[0]?.result === MoveResult.SUCCESS) { - const side = ((this.selfSideTarget ? user : target).isPlayer() !== (move.hasAttr(AddArenaTrapTagAttr) && target === user)) ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; + const side = ((this.selfSideTarget ? user : target).isPlayer() !== (move.hasAttr("AddArenaTrapTagAttr") && target === user)) ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; globalScene.arena.addTag(this.tagType, this.turnCount, move.id, user.id, side); return true; } @@ -6175,6 +6230,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); @@ -6233,7 +6289,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)) { @@ -6322,7 +6378,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)) { @@ -6365,7 +6421,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) }); } @@ -6376,7 +6432,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { return (user, target, move) => { const switchOutTarget = (this.selfSwitch ? user : target); const player = switchOutTarget.isPlayer(); - const forceSwitchAttr = move.getAttrs(ForceSwitchOutAttr).find(attr => attr.switchType === SwitchType.FORCE_SWITCH); + const forceSwitchAttr = move.getAttrs("ForceSwitchOutAttr").find(attr => attr.switchType === SwitchType.FORCE_SWITCH); if (!this.selfSwitch) { if (move.hitsSubstitute(user, target)) { @@ -6405,7 +6461,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { } const blockedByAbility = new BooleanHolder(false); - applyAbAttrs(ForceSwitchOutImmunityAbAttr, target, blockedByAbility); + applyAbAttrs("ForceSwitchOutImmunityAbAttr", target, blockedByAbility); if (blockedByAbility.value) { return false; } @@ -6716,20 +6772,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; } } @@ -6824,9 +6886,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: @@ -6951,14 +7014,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; } } @@ -6977,64 +7040,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() ]; } } @@ -7043,15 +7105,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 @@ -7061,6 +7123,7 @@ export class RepeatMoveAttr extends MoveEffectAttr { MoveId.PETAL_DANCE, MoveId.THRASH, MoveId.ICE_BALL, + MoveId.UPROAR, // Multi-turn Moves MoveId.BIDE, MoveId.SHELL_TRAP, @@ -7098,23 +7161,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; }; } @@ -7145,53 +7219,48 @@ export class ReducePpMoveAttr extends MoveEffectAttr { /** * Reduces the PP of the target's last-used move by an amount based on this attribute instance's {@linkcode reduction}. * - * @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; + } } @@ -7207,40 +7276,36 @@ export class AttackReducePpMoveAttr extends ReducePpMoveAttr { /** * Checks if the target has used a move prior to the attack. PP-reduction is applied through the super class if so. * - * @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; } @@ -7253,14 +7318,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); @@ -7268,8 +7337,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 })); @@ -7307,9 +7377,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; } @@ -7332,28 +7402,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) }; } } @@ -7793,19 +7845,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)) }; } } @@ -7827,27 +7879,26 @@ export class VariableTargetAttr extends MoveAttr { } /** - * Attribute for {@linkcode MoveId.AFTER_YOU} + * Attribute to cause the target to move immediately after the user. * - * [After You - Move | Bulbapedia](https://bulbapedia.bulbagarden.net/wiki/After_You_(move)) + * Used by {@linkcode Moves.AFTER_YOU}. */ export class AfterYouAttr extends MoveEffectAttr { /** - * 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; @@ -7872,6 +7923,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 - @@ -7884,7 +7936,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) ); } } @@ -7892,7 +7944,13 @@ export class ForceLastAttr extends MoveEffectAttr { } } -/** Returns whether a {@linkcode MovePhase} has been forced last and the corresponding pokemon is slower than {@linkcode target} */ +/** + * Returns whether a {@linkcode MovePhase} has been forced last and the corresponding pokemon is slower than {@linkcode target}. + + * TODO: + - Make this a class method + - Make this look at speed order from TurnStartPhase +*/ const phaseForcedSlower = (phase: MovePhase, target: Pokemon, trickRoom: boolean): boolean => { let slower: boolean; // quashed pokemon still have speed ties @@ -7912,7 +7970,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 })); @@ -7945,58 +8003,6 @@ const attackedByItemMessageFunc = (user: Pokemon, target: Pokemon, move: Move) = return message; }; -export type MoveAttrFilter = (attr: MoveAttr) => boolean; - -function applyMoveAttrsInternal( - attrFilter: MoveAttrFilter, - user: Pokemon | null, - target: Pokemon | null, - move: Move, - args: any[], -): void { - move.attrs.filter((attr) => attrFilter(attr)).forEach((attr) => attr.apply(user, target, move, args)); -} - -function applyMoveChargeAttrsInternal( - attrFilter: MoveAttrFilter, - user: Pokemon | null, - target: Pokemon | null, - move: ChargingMove, - args: any[], -): void { - move.chargeAttrs.filter((attr) => attrFilter(attr)).forEach((attr) => attr.apply(user, target, move, args)); -} - -export function applyMoveAttrs( - attrType: Constructor, - user: Pokemon | null, - target: Pokemon | null, - move: Move, - ...args: any[] -): void { - applyMoveAttrsInternal((attr: MoveAttr) => attr instanceof attrType, user, target, move, args); -} - -export function applyFilteredMoveAttrs( - attrFilter: MoveAttrFilter, - user: Pokemon, - target: Pokemon | null, - move: Move, - ...args: any[] -): void { - applyMoveAttrsInternal(attrFilter, user, target, move, args); -} - -export function applyMoveChargeAttrs( - attrType: Constructor, - user: Pokemon | null, - target: Pokemon | null, - move: ChargingMove, - ...args: any[] -): void { - applyMoveChargeAttrsInternal((attr: MoveAttr) => attr instanceof attrType, user, target, move, args); -} - export class MoveCondition { protected func: MoveConditionFunc; @@ -8039,8 +8045,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 @@ -8073,6 +8078,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 @@ -8086,7 +8092,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; } @@ -8128,9 +8135,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); }; } } @@ -8175,73 +8182,232 @@ export type MoveTargetSet = { multiple: boolean; }; -export function getMoveTargets(user: Pokemon, move: MoveId, replaceTarget?: MoveTarget): MoveTargetSet { - const variableTarget = new NumberHolder(0); - user.getOpponents(false).forEach(p => applyMoveAttrs(VariableTargetAttr, user, p, allMoves[move], variableTarget)); +/** + * Map of Move attributes to their respective classes. Used for instanceof checks. + */ +const MoveAttrs = Object.freeze({ + MoveEffectAttr, + MoveHeaderAttr, + MessageHeaderAttr, + AddBattlerTagAttr, + AddBattlerTagHeaderAttr, + BeakBlastHeaderAttr, + PreMoveMessageAttr, + PreUseInterruptAttr, + RespectAttackTypeImmunityAttr, + IgnoreOpponentStatStagesAttr, + HighCritAttr, + CritOnlyAttr, + FixedDamageAttr, + UserHpDamageAttr, + TargetHalfHpDamageAttr, + MatchHpAttr, + CounterDamageAttr, + LevelDamageAttr, + RandomLevelDamageAttr, + ModifiedDamageAttr, + SurviveDamageAttr, + SplashAttr, + CelebrateAttr, + RecoilAttr, + SacrificialAttr, + SacrificialAttrOnHit, + HalfSacrificialAttr, + AddSubstituteAttr, + HealAttr, + PartyStatusCureAttr, + FlameBurstAttr, + SacrificialFullRestoreAttr, + IgnoreWeatherTypeDebuffAttr, + WeatherHealAttr, + PlantHealAttr, + SandHealAttr, + BoostHealAttr, + HealOnAllyAttr, + HitHealAttr, + IncrementMovePriorityAttr, + MultiHitAttr, + ChangeMultiHitTypeAttr, + WaterShurikenMultiHitTypeAttr, + StatusEffectAttr, + MultiStatusEffectAttr, + PsychoShiftEffectAttr, + StealHeldItemChanceAttr, + RemoveHeldItemAttr, + EatBerryAttr, + StealEatBerryAttr, + HealStatusEffectAttr, + BypassSleepAttr, + BypassBurnDamageReductionAttr, + WeatherChangeAttr, + ClearWeatherAttr, + TerrainChangeAttr, + ClearTerrainAttr, + OneHitKOAttr, + InstantChargeAttr, + WeatherInstantChargeAttr, + OverrideMoveEffectAttr, + DelayedAttackAttr, + AwaitCombinedPledgeAttr, + StatStageChangeAttr, + SecretPowerAttr, + PostVictoryStatStageChangeAttr, + AcupressureStatStageChangeAttr, + GrowthStatStageChangeAttr, + CutHpStatStageBoostAttr, + OrderUpStatBoostAttr, + CopyStatsAttr, + InvertStatsAttr, + ResetStatsAttr, + SwapStatStagesAttr, + HpSplitAttr, + VariablePowerAttr, + LessPPMorePowerAttr, + MovePowerMultiplierAttr, + BeatUpAttr, + DoublePowerChanceAttr, + ConsecutiveUsePowerMultiplierAttr, + ConsecutiveUseDoublePowerAttr, + ConsecutiveUseMultiBasePowerAttr, + WeightPowerAttr, + ElectroBallPowerAttr, + GyroBallPowerAttr, + LowHpPowerAttr, + CompareWeightPowerAttr, + HpPowerAttr, + OpponentHighHpPowerAttr, + FirstAttackDoublePowerAttr, + TurnDamagedDoublePowerAttr, + MagnitudePowerAttr, + AntiSunlightPowerDecreaseAttr, + FriendshipPowerAttr, + RageFistPowerAttr, + PositiveStatStagePowerAttr, + PunishmentPowerAttr, + PresentPowerAttr, + WaterShurikenPowerAttr, + SpitUpPowerAttr, + SwallowHealAttr, + MultiHitPowerIncrementAttr, + LastMoveDoublePowerAttr, + CombinedPledgePowerAttr, + CombinedPledgeStabBoostAttr, + RoundPowerAttr, + CueNextRoundAttr, + StatChangeBeforeDmgCalcAttr, + SpectralThiefAttr, + VariableAtkAttr, + TargetAtkUserAtkAttr, + DefAtkAttr, + VariableDefAttr, + DefDefAttr, + VariableAccuracyAttr, + ThunderAccuracyAttr, + StormAccuracyAttr, + AlwaysHitMinimizeAttr, + ToxicAccuracyAttr, + BlizzardAccuracyAttr, + VariableMoveCategoryAttr, + PhotonGeyserCategoryAttr, + TeraMoveCategoryAttr, + TeraBlastPowerAttr, + StatusCategoryOnAllyAttr, + ShellSideArmCategoryAttr, + VariableMoveTypeAttr, + FormChangeItemTypeAttr, + TechnoBlastTypeAttr, + AuraWheelTypeAttr, + RagingBullTypeAttr, + IvyCudgelTypeAttr, + WeatherBallTypeAttr, + TerrainPulseTypeAttr, + HiddenPowerTypeAttr, + TeraBlastTypeAttr, + TeraStarstormTypeAttr, + MatchUserTypeAttr, + CombinedPledgeTypeAttr, + VariableMoveTypeMultiplierAttr, + NeutralDamageAgainstFlyingTypeMultiplierAttr, + IceNoEffectTypeAttr, + FlyingTypeMultiplierAttr, + VariableMoveTypeChartAttr, + FreezeDryAttr, + OneHitKOAccuracyAttr, + SheerColdAccuracyAttr, + MissEffectAttr, + NoEffectAttr, + TypelessAttr, + BypassRedirectAttr, + FrenzyAttr, + SemiInvulnerableAttr, + LeechSeedAttr, + FallDownAttr, + GulpMissileTagAttr, + JawLockAttr, + CurseAttr, + LapseBattlerTagAttr, + RemoveBattlerTagAttr, + FlinchAttr, + ConfuseAttr, + RechargeAttr, + TrapAttr, + ProtectAttr, + IgnoreAccuracyAttr, + FaintCountdownAttr, + RemoveAllSubstitutesAttr, + HitsTagAttr, + HitsTagForDoubleDamageAttr, + AddArenaTagAttr, + RemoveArenaTagsAttr, + AddArenaTrapTagAttr, + AddArenaTrapTagHitAttr, + RemoveArenaTrapAttr, + RemoveScreensAttr, + SwapArenaTagsAttr, + AddPledgeEffectAttr, + RevivalBlessingAttr, + ForceSwitchOutAttr, + ChillyReceptionAttr, + RemoveTypeAttr, + CopyTypeAttr, + CopyBiomeTypeAttr, + ChangeTypeAttr, + AddTypeAttr, + FirstMoveTypeAttr, + CallMoveAttr, + RandomMoveAttr, + RandomMovesetMoveAttr, + NaturePowerAttr, + CopyMoveAttr, + RepeatMoveAttr, + ReducePpMoveAttr, + AttackReducePpMoveAttr, + MovesetCopyMoveAttr, + SketchAttr, + AbilityChangeAttr, + AbilityCopyAttr, + AbilityGiveAttr, + SwitchAbilitiesAttr, + SuppressAbilitiesAttr, + TransformAttr, + SwapStatAttr, + ShiftStatAttr, + AverageStatsAttr, + MoneyAttr, + DestinyBondAttr, + AddBattlerTagIfBoostedAttr, + StatusIfBoostedAttr, + LastResortAttr, + VariableTargetAttr, + AfterYouAttr, + ForceLastAttr, + HitsSameTypeAttr, + ResistLastMoveTypeAttr, + ExposedMoveAttr, +}); - let moveTarget: MoveTarget | undefined; - if (allMoves[move].hasAttr(VariableTargetAttr)) { - moveTarget = variableTarget.value; - } else if (replaceTarget !== undefined) { - moveTarget = replaceTarget; - } else if (move) { - moveTarget = allMoves[move].moveTarget; - } else if (move === undefined) { - moveTarget = MoveTarget.NEAR_ENEMY; - } - const opponents = user.getOpponents(false); - - let set: Pokemon[] = []; - let multiple = false; - const ally: Pokemon | undefined = user.getAlly(); - - switch (moveTarget) { - case MoveTarget.USER: - case MoveTarget.PARTY: - set = [ user ]; - break; - case MoveTarget.NEAR_OTHER: - case MoveTarget.OTHER: - case MoveTarget.ALL_NEAR_OTHERS: - case MoveTarget.ALL_OTHERS: - set = !isNullOrUndefined(ally) ? (opponents.concat([ ally ])) : opponents; - multiple = moveTarget === MoveTarget.ALL_NEAR_OTHERS || moveTarget === MoveTarget.ALL_OTHERS; - break; - case MoveTarget.NEAR_ENEMY: - case MoveTarget.ALL_NEAR_ENEMIES: - case MoveTarget.ALL_ENEMIES: - case MoveTarget.ENEMY_SIDE: - set = opponents; - multiple = moveTarget !== MoveTarget.NEAR_ENEMY; - break; - case MoveTarget.RANDOM_NEAR_ENEMY: - set = [ opponents[user.randBattleSeedInt(opponents.length)] ]; - break; - case MoveTarget.ATTACKER: - return { targets: [ -1 as BattlerIndex ], multiple: false }; - case MoveTarget.NEAR_ALLY: - case MoveTarget.ALLY: - set = !isNullOrUndefined(ally) ? [ ally ] : []; - break; - case MoveTarget.USER_OR_NEAR_ALLY: - case MoveTarget.USER_AND_ALLIES: - case MoveTarget.USER_SIDE: - set = !isNullOrUndefined(ally) ? [ user, ally ] : [ user ]; - multiple = moveTarget !== MoveTarget.USER_OR_NEAR_ALLY; - break; - case MoveTarget.ALL: - case MoveTarget.BOTH_SIDES: - set = (!isNullOrUndefined(ally) ? [ user, ally ] : [ user ]).concat(opponents); - multiple = true; - break; - case MoveTarget.CURSE: - const extraTargets = !isNullOrUndefined(ally) ? [ ally ] : []; - set = user.getTypes(true).includes(PokemonType.GHOST) ? (opponents.concat(extraTargets)) : [ user ]; - break; - } - - return { targets: set.filter(p => p?.isActive(true)).map(p => p.getBattlerIndex()).filter(t => t !== undefined), multiple }; -} +/** Map of of move attribute names to their constructors */ +export type MoveAttrConstructorMap = typeof MoveAttrs; export const selfStatLowerMoves: MoveId[] = []; @@ -8389,9 +8555,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(), @@ -8940,7 +9106,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) @@ -9822,7 +9991,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), @@ -10393,9 +10569,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) @@ -10453,7 +10632,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), @@ -11255,7 +11440,7 @@ export function initMoves() { .attr(StatusEffectAttr, StatusEffect.TOXIC) ); allMoves.map(m => { - if (m.getAttrs(StatStageChangeAttr).some(a => a.selfTarget && a.stages < 0)) { + if (m.getAttrs("StatStageChangeAttr").some(a => a.selfTarget && a.stages < 0)) { selfStatLowerMoves.push(m.id); } }); diff --git a/src/data/moves/pokemon-move.ts b/src/data/moves/pokemon-move.ts new file mode 100644 index 00000000000..daad199fbbd --- /dev/null +++ b/src/data/moves/pokemon-move.ts @@ -0,0 +1,92 @@ +import type Pokemon from "#app/field/pokemon"; +import { toDmgValue } from "#app/utils/common"; +import type { MoveId } from "#enums/move-id"; +import { allMoves } from "../data-lists"; +import type Move from "./move"; + +/** + * Wrapper class for the {@linkcode Move} class for Pokemon to interact with. + * These are the moves assigned to a {@linkcode Pokemon} object. + * It links to {@linkcode Move} class via the move ID. + * Compared to {@linkcode Move}, this class also tracks things like + * PP Ups recieved, PP used, etc. + * @see {@linkcode isUsable} - checks if move is restricted, out of PP, or not implemented. + * @see {@linkcode getMove} - returns {@linkcode Move} object by looking it up via ID. + * @see {@linkcode usePp} - removes a point of PP from the move. + * @see {@linkcode getMovePp} - returns amount of PP a move currently has. + * @see {@linkcode getPpRatio} - returns the current PP amount / max PP amount. + * @see {@linkcode getName} - returns name of {@linkcode Move}. + **/ +export class PokemonMove { + public moveId: MoveId; + public ppUsed: number; + public ppUp: number; + + /** + * If defined and nonzero, overrides the maximum PP of the move (e.g., due to move being copied by Transform). + * This also nullifies all effects of `ppUp`. + */ + public maxPpOverride?: number; + + constructor(moveId: MoveId, ppUsed = 0, ppUp = 0, maxPpOverride?: number) { + this.moveId = moveId; + this.ppUsed = ppUsed; + this.ppUp = ppUp; + this.maxPpOverride = maxPpOverride; + } + + /** + * Checks whether the move can be selected or performed by a Pokemon, without consideration for the move's targets. + * The move is unusable if it is out of PP, restricted by an effect, or unimplemented. + * + * @param pokemon - {@linkcode Pokemon} that would be using this move + * @param ignorePp - If `true`, skips the PP check + * @param ignoreRestrictionTags - If `true`, skips the check for move restriction tags (see {@link MoveRestrictionBattlerTag}) + * @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; + } + + if (this.getMove().name.endsWith(" (N)")) { + return false; + } + + return ignorePp || this.ppUsed < this.getMovePp() || this.getMove().pp === -1; + } + + getMove(): Move { + return allMoves[this.moveId]; + } + + /** + * Sets {@link ppUsed} for this move and ensures the value does not exceed {@link getMovePp} + * @param count Amount of PP to use + */ + usePp(count = 1) { + this.ppUsed = Math.min(this.ppUsed + count, this.getMovePp()); + } + + getMovePp(): number { + return this.maxPpOverride || this.getMove().pp + this.ppUp * toDmgValue(this.getMove().pp / 5); + } + + getPpRatio(): number { + return 1 - this.ppUsed / this.getMovePp(); + } + + getName(): string { + return this.getMove().name; + } + + /** + * Copies an existing move or creates a valid {@linkcode PokemonMove} object from json representing one + * @param source The data for the move to copy; can be a {@linkcode PokemonMove} or JSON object representing one + * @returns A valid {@linkcode PokemonMove} object + */ + static loadMove(source: PokemonMove | any): PokemonMove { + 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 82fbc0efd69..e3741226335 100644 --- a/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts +++ b/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts @@ -7,9 +7,10 @@ import { transitionMysteryEncounterIntroVisuals, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import type Pokemon from "#app/field/pokemon"; -import { EnemyPokemon, PokemonMove } 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"; @@ -25,7 +26,7 @@ import { getPokemonSpecies } from "#app/data/pokemon-species"; import { MoveId } from "#enums/move-id"; import { BattlerTagType } from "#enums/battler-tag-type"; import { randInt } from "#app/utils/common"; -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { applyModifierTypeToPlayerPokemon, catchPokemon, @@ -37,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"; @@ -306,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..271346616d1 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"; 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 938a136bced..df06f40c159 100644 --- a/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts +++ b/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts @@ -24,7 +24,7 @@ import { TrainerType } from "#enums/trainer-type"; import { SpeciesId } from "#enums/species-id"; import type { PlayerPokemon } from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon"; -import { PokemonMove } from "#app/field/pokemon"; +import { PokemonMove } from "#app/data/moves/pokemon-move"; import { getEncounterText, showEncounterDialogue } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { MoveId } from "#enums/move-id"; import type { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; @@ -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 19c4948aeab..b873e30fe0c 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"; @@ -37,11 +37,10 @@ import { UiMode } from "#enums/ui-mode"; 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/field/pokemon"; -import { Ability } from "#app/data/abilities/ability-class"; +import { PokemonMove } from "#app/data/moves/pokemon-move"; import { BerryModifier } from "#app/modifier/modifier"; import { BerryType } from "#enums/berry-type"; -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { MoveId } from "#enums/move-id"; import { EncounterBattleAnim } from "#app/data/battle-anims"; import { MoveCategory } from "#enums/MoveCategory"; @@ -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 80465e1d20c..42af5339a80 100644 --- a/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts +++ b/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { globalScene } from "#app/global-scene"; import { EncounterBattleAnim } from "#app/data/battle-anims"; import type MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; @@ -23,9 +23,10 @@ import { getPokemonSpecies } from "#app/data/pokemon-species"; import { TrainerSlot } from "#enums/trainer-slot"; import type { PlayerPokemon } from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon"; -import { EnemyPokemon, PokemonMove } 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"; @@ -39,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"; @@ -213,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..002b38f445d 100644 --- a/src/data/mystery-encounters/encounters/dark-deal-encounter.ts +++ b/src/data/mystery-encounters/encounters/dark-deal-encounter.ts @@ -3,7 +3,7 @@ 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 { modifierTypes } from "#app/data/data-lists"; import { getPokemonSpecies } from "#app/data/pokemon-species"; import type MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; import { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; diff --git a/src/data/mystery-encounters/encounters/delibirdy-encounter.ts b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts index 40893d93930..692ffe6e80e 100644 --- a/src/data/mystery-encounters/encounters/delibirdy-encounter.ts +++ b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts @@ -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 2cd6123838b..6ab0f8a6a4b 100644 --- a/src/data/mystery-encounters/encounters/field-trip-encounter.ts +++ b/src/data/mystery-encounters/encounters/field-trip-encounter.ts @@ -7,8 +7,9 @@ import { setEncounterExp, setEncounterRewards, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; -import type { PlayerPokemon, PokemonMove } from "#app/field/pokemon"; -import { modifierTypes } from "#app/modifier/modifier-type"; +import type { PlayerPokemon } from "#app/field/pokemon"; +import type { PokemonMove } from "#app/data/moves/pokemon-move"; +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 8b0e5a08020..94dc4ab6153 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"; @@ -24,9 +23,9 @@ import { SpeciesId } from "#enums/species-id"; import { getPokemonSpecies } from "#app/data/pokemon-species"; import { Gender } from "#app/data/gender"; import { PokemonType } from "#enums/pokemon-type"; -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import type Pokemon from "#app/field/pokemon"; -import { PokemonMove } from "#app/field/pokemon"; +import { PokemonMove } from "#app/data/moves/pokemon-move"; import { MoveId } from "#enums/move-id"; import { EncounterBattleAnim } from "#app/data/battle-anims"; import { WeatherType } from "#enums/weather-type"; @@ -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 a52641f857d..48557541512 100644 --- a/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts +++ b/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts @@ -13,7 +13,7 @@ import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/myst import { TrainerSlot } from "#enums/trainer-slot"; import type { PlayerPokemon } from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon"; -import { FieldPosition } from "#app/field/pokemon"; +import { FieldPosition } from "#enums/field-position"; import { getPokemonSpecies } from "#app/data/pokemon-species"; import { MoneyRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; @@ -25,8 +25,8 @@ import { getPokemonNameWithAffix } from "#app/messages"; 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"; -import { modifierTypes } from "#app/modifier/modifier-type"; +import { SpeciesFormChangeActiveTrigger } from "#app/data/pokemon-forms/form-change-triggers"; +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 6ecce46ae24..0393bcdaa62 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"; @@ -33,7 +30,8 @@ import { } from "#app/utils/common"; import type { PlayerPokemon } from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon"; -import { EnemyPokemon, PokemonMove } from "#app/field/pokemon"; +import { EnemyPokemon } from "#app/field/pokemon"; +import { PokemonMove } from "#app/data/moves/pokemon-move"; import type { PokemonHeldItemModifier } from "#app/modifier/modifier"; import { HiddenAbilityRateBoosterModifier, 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 009639291de..8eea0623cd4 100644 --- a/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts +++ b/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts @@ -11,7 +11,7 @@ import { applyDamageToPokemon } from "#app/data/mystery-encounters/utils/encount import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; -import { PokemonMove } from "#app/field/pokemon"; +import { PokemonMove } from "#app/data/moves/pokemon-move"; const OPTION_1_REQUIRED_MOVE = MoveId.SURF; const OPTION_2_REQUIRED_MOVE = MoveId.FLY; 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..022b0125fde 100644 --- a/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts +++ b/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts @@ -16,7 +16,7 @@ import { } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { getPokemonSpecies } from "#app/data/pokemon-species"; 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/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 bb1529e3695..6b6cd71af1f 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"; @@ -21,8 +21,9 @@ import { import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { Nature } from "#enums/nature"; import { MoveId } from "#enums/move-id"; -import { BattlerIndex } from "#app/battle"; -import { AiType, PokemonMove } from "#app/field/pokemon"; +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 { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; @@ -30,6 +31,7 @@ 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"; @@ -136,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..0676b40c548 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 @@ -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-strong-stuff-encounter.ts b/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts index be347fb0035..adf4f9dde8f 100644 --- a/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts @@ -8,7 +8,7 @@ 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"; @@ -17,17 +17,18 @@ import { getPokemonSpecies } from "#app/data/pokemon-species"; import { SpeciesId } from "#enums/species-id"; import { Nature } from "#enums/nature"; import type Pokemon from "#app/field/pokemon"; -import { PokemonMove } from "#app/field/pokemon"; +import { PokemonMove } from "#app/data/moves/pokemon-move"; import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { modifyPlayerPokemonBST } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { MoveId } from "#enums/move-id"; -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { BattlerTagType } from "#enums/battler-tag-type"; import { BerryType } from "#enums/berry-type"; 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 4b17260098f..e2c87d8c0ae 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"; @@ -23,12 +23,12 @@ 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"; -import { applyPostBattleInitAbAttrs, PostBattleInitAbAttr } from "#app/data/abilities/ability"; +import { SpeciesFormChangeAbilityTrigger } from "#app/data/pokemon-forms/form-change-triggers"; +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 b17a39ae694..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 { @@ -12,7 +12,7 @@ import { speciesStarterCosts } from "#app/data/balance/starters"; import type { PlayerPokemon } from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon"; import type { PokemonHeldItemModifier } from "#app/modifier/modifier"; -import { AbilityAttr } from "#app/system/game-data"; +import { AbilityAttr } from "#enums/ability-attr"; import PokemonData from "#app/system/pokemon-data"; import type { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; import { isNullOrUndefined, randSeedShuffle } from "#app/utils/common"; 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 a0051058d02..62413b96523 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 { ModifierTier } from "#enums/modifier-tier"; import { getPokemonSpecies } from "#app/data/pokemon-species"; import { MoveId } from "#enums/move-id"; -import { BattlerIndex } from "#app/battle"; -import { PokemonMove } from "#app/field/pokemon"; +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 411ecdec080..44e578540dd 100644 --- a/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts +++ b/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts @@ -10,7 +10,7 @@ import { import { CHARMING_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups"; import type Pokemon from "#app/field/pokemon"; import type { EnemyPokemon } from "#app/field/pokemon"; -import { PokemonMove } from "#app/field/pokemon"; +import { PokemonMove } from "#app/data/moves/pokemon-move"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { globalScene } from "#app/global-scene"; import type MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; @@ -29,14 +29,14 @@ import { import PokemonData from "#app/system/pokemon-data"; import { isNullOrUndefined, randSeedInt } from "#app/utils/common"; import type { MoveId } from "#enums/move-id"; -import { BattlerIndex } from "#app/battle"; -import { SelfStatusMove } from "#app/data/moves/move"; +import { BattlerIndex } from "#enums/battler-index"; import { PokeballType } from "#enums/pokeball"; import { BattlerTagType } from "#enums/battler-tag-type"; import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; 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"; @@ -175,13 +175,13 @@ export const UncommonBreedEncounter: MysteryEncounter = MysteryEncounterBuilder. // Check what type of move the egg move is to determine target const pokemonMove = new PokemonMove(eggMove); const move = pokemonMove.getMove(); - const target = move instanceof SelfStatusMove ? BattlerIndex.ENEMY : BattlerIndex.PLAYER; + const target = move.is("SelfStatusMove") ? BattlerIndex.ENEMY : BattlerIndex.PLAYER; encounter.startOfBattleEffects.push({ 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 2b1f775b78e..83e876d1aa8 100644 --- a/src/data/mystery-encounters/encounters/weird-dream-encounter.ts +++ b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts @@ -16,7 +16,7 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import type { PlayerPokemon } from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon"; -import { PokemonMove } 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"; @@ -25,7 +25,7 @@ import { HiddenAbilityRateBoosterModifier, PokemonFormChangeItemModifier } from 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 +34,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 88d9ba402a9..a6e6e84846f 100644 --- a/src/data/mystery-encounters/mystery-encounter-requirements.ts +++ b/src/data/mystery-encounters/mystery-encounter-requirements.ts @@ -2,14 +2,16 @@ 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 { FormChangeItem, pokemonFormChanges, SpeciesFormChangeItemTrigger } from "#app/data/pokemon-forms"; +import { pokemonFormChanges } from "#app/data/pokemon-forms"; +import { SpeciesFormChangeItemTrigger } from "../pokemon-forms/form-change-triggers"; +import { FormChangeItem } from "#enums/form-change-item"; import { StatusEffect } from "#enums/status-effect"; import { PokemonType } from "#enums/pokemon-type"; 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"; @@ -270,7 +272,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 { @@ -292,7 +294,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 { @@ -358,7 +360,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 { @@ -424,7 +426,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 { @@ -464,7 +466,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 { @@ -502,7 +504,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 { @@ -556,7 +558,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 { @@ -607,7 +609,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 { @@ -663,7 +665,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 { @@ -708,7 +710,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 { @@ -783,7 +785,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 { @@ -841,7 +843,7 @@ export class CanEvolveWithItemRequirement extends EncounterPokemonRequirement { super(); this.minNumberOfPokemon = minNumberOfPokemon; this.invertQuery = invertQuery; - this.requiredEvolutionItem = Array.isArray(evolutionItems) ? evolutionItems : [evolutionItems]; + this.requiredEvolutionItem = coerceArray(evolutionItems); } override meetsRequirement(): boolean { @@ -906,7 +908,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; } @@ -970,7 +972,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 e305252ed0f..fa97a7f4d40 100644 --- a/src/data/mystery-encounters/mystery-encounter.ts +++ b/src/data/mystery-encounters/mystery-encounter.ts @@ -1,7 +1,8 @@ import type { EnemyPartyConfig } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; -import type { PlayerPokemon, PokemonMove } from "#app/field/pokemon"; +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"; @@ -20,22 +21,22 @@ import { StatusEffectRequirement, WaveRangeRequirement, } from "./mystery-encounter-requirements"; -import type { BattlerIndex } from "#app/battle"; +import type { BattlerIndex } from "#enums/battler-index"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; -import type { GameModes } from "#app/game-mode"; +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; @@ -253,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[] = []; /** @@ -716,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 }); } @@ -728,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 }); } @@ -740,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 7698be7b15d..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/field/pokemon"; -import { isNullOrUndefined } from "#app/utils/common"; +import { PokemonMove } from "#app/data/moves/pokemon-move"; +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 b86cbaa18c9..eaa4f08ef70 100644 --- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -1,36 +1,36 @@ import type Battle from "#app/battle"; -import { BattlerIndex } from "#app/battle"; 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 { AiType, PlayerPokemon } from "#app/field/pokemon"; +import type { PlayerPokemon } from "#app/field/pokemon"; +import type { AiType } from "#enums/ai-type"; import type Pokemon from "#app/field/pokemon"; -import { EnemyPokemon, FieldPosition, PokemonMove } from "#app/field/pokemon"; +import { EnemyPokemon } from "#app/field/pokemon"; +import { PokemonMove } from "#app/data/moves/pokemon-move"; +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"; import i18next from "i18next"; -import Trainer, { TrainerVariant } from "#app/field/trainer"; +import Trainer from "#app/field/trainer"; +import { TrainerVariant } from "#enums/trainer-variant"; import type { Gender } from "#app/data/gender"; import type { Nature } from "#enums/nature"; import type { MoveId } from "#enums/move-id"; @@ -448,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)); } @@ -791,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)); @@ -973,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..4671869a2ba 100644 --- a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts @@ -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.ts b/src/data/pokemon-forms.ts index 47eb355c8b6..ea129454034 100644 --- a/src/data/pokemon-forms.ts +++ b/src/data/pokemon-forms.ts @@ -1,139 +1,30 @@ -import { PokemonFormChangeItemModifier } from "../modifier/modifier"; import type Pokemon from "../field/pokemon"; -import { StatusEffect } from "#enums/status-effect"; import { allMoves } from "./data-lists"; import { MoveCategory } from "#enums/MoveCategory"; import type { Constructor, nil } from "#app/utils/common"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; -import type { TimeOfDay } from "#enums/time-of-day"; -import { getPokemonNameWithAffix } from "#app/messages"; -import i18next from "i18next"; import { WeatherType } from "#enums/weather-type"; -import { Challenges } from "#app/enums/challenges"; import { SpeciesFormKey } from "#enums/species-form-key"; import { globalScene } from "#app/global-scene"; - -export enum FormChangeItem { - NONE, - - ABOMASITE, - ABSOLITE, - AERODACTYLITE, - AGGRONITE, - ALAKAZITE, - ALTARIANITE, - AMPHAROSITE, - AUDINITE, - BANETTITE, - BEEDRILLITE, - BLASTOISINITE, - BLAZIKENITE, - CAMERUPTITE, - CHARIZARDITE_X, - CHARIZARDITE_Y, - DIANCITE, - GALLADITE, - GARCHOMPITE, - GARDEVOIRITE, - GENGARITE, - GLALITITE, - GYARADOSITE, - HERACRONITE, - HOUNDOOMINITE, - KANGASKHANITE, - LATIASITE, - LATIOSITE, - LOPUNNITE, - LUCARIONITE, - MANECTITE, - MAWILITE, - MEDICHAMITE, - METAGROSSITE, - MEWTWONITE_X, - MEWTWONITE_Y, - PIDGEOTITE, - PINSIRITE, - RAYQUAZITE, - SABLENITE, - SALAMENCITE, - SCEPTILITE, - SCIZORITE, - SHARPEDONITE, - SLOWBRONITE, - STEELIXITE, - SWAMPERTITE, - TYRANITARITE, - VENUSAURITE, - - BLUE_ORB = 50, - RED_ORB, - ADAMANT_CRYSTAL, - LUSTROUS_GLOBE, - GRISEOUS_CORE, - REVEAL_GLASS, - MAX_MUSHROOMS, - DARK_STONE, - LIGHT_STONE, - PRISON_BOTTLE, - RUSTED_SWORD, - RUSTED_SHIELD, - ICY_REINS_OF_UNITY, - SHADOW_REINS_OF_UNITY, - ULTRANECROZIUM_Z, - - SHARP_METEORITE = 100, - HARD_METEORITE, - SMOOTH_METEORITE, - GRACIDEA, - SHOCK_DRIVE, - BURN_DRIVE, - CHILL_DRIVE, - DOUSE_DRIVE, - N_SOLARIZER, - N_LUNARIZER, - WELLSPRING_MASK, - HEARTHFLAME_MASK, - CORNERSTONE_MASK, - FIST_PLATE, - SKY_PLATE, - TOXIC_PLATE, - EARTH_PLATE, - STONE_PLATE, - INSECT_PLATE, - SPOOKY_PLATE, - IRON_PLATE, - FLAME_PLATE, - SPLASH_PLATE, - MEADOW_PLATE, - ZAP_PLATE, - MIND_PLATE, - ICICLE_PLATE, - DRACO_PLATE, - DREAD_PLATE, - PIXIE_PLATE, - BLANK_PLATE, // TODO: Find a potential use for this - LEGEND_PLATE, // TODO: Find a potential use for this - FIGHTING_MEMORY, - FLYING_MEMORY, - POISON_MEMORY, - GROUND_MEMORY, - ROCK_MEMORY, - BUG_MEMORY, - GHOST_MEMORY, - STEEL_MEMORY, - FIRE_MEMORY, - WATER_MEMORY, - GRASS_MEMORY, - ELECTRIC_MEMORY, - PSYCHIC_MEMORY, - ICE_MEMORY, - DRAGON_MEMORY, - DARK_MEMORY, - FAIRY_MEMORY, - NORMAL_MEMORY, // TODO: Find a potential use for this -} +import { FormChangeItem } from "#enums/form-change-item"; +import { + MeloettaFormChangePostMoveTrigger, + SpeciesDefaultFormMatchTrigger, + SpeciesFormChangeAbilityTrigger, + SpeciesFormChangeActiveTrigger, + SpeciesFormChangeCompoundTrigger, + SpeciesFormChangeItemTrigger, + SpeciesFormChangeLapseTeraTrigger, + SpeciesFormChangeManualTrigger, + SpeciesFormChangeMoveLearnedTrigger, + SpeciesFormChangePreMoveTrigger, + SpeciesFormChangeRevertWeatherFormTrigger, + SpeciesFormChangeTeraTrigger, + type SpeciesFormChangeTrigger, + SpeciesFormChangeWeatherTrigger, +} from "./pokemon-forms/form-change-triggers"; export type SpeciesFormChangeConditionPredicate = (p: Pokemon) => boolean; export type SpeciesFormChangeConditionEnforceFunc = (p: Pokemon) => void; @@ -214,347 +105,6 @@ export class SpeciesFormChangeCondition { } } -export abstract class SpeciesFormChangeTrigger { - public description = ""; - - canChange(_pokemon: Pokemon): boolean { - return true; - } - - hasTriggerType(triggerType: Constructor): boolean { - return this instanceof triggerType; - } -} - -export class SpeciesFormChangeManualTrigger extends SpeciesFormChangeTrigger {} - -export class SpeciesFormChangeAbilityTrigger extends SpeciesFormChangeTrigger { - public description: string = i18next.t("pokemonEvolutions:Forms.ability"); -} - -export class SpeciesFormChangeCompoundTrigger { - public description = ""; - public triggers: SpeciesFormChangeTrigger[]; - - constructor(...triggers: SpeciesFormChangeTrigger[]) { - this.triggers = triggers; - this.description = this.triggers - .filter(trigger => trigger?.description?.length > 0) - .map(trigger => trigger.description) - .join(", "); - } - - canChange(pokemon: Pokemon): boolean { - for (const trigger of this.triggers) { - if (!trigger.canChange(pokemon)) { - return false; - } - } - - return true; - } - - hasTriggerType(triggerType: Constructor): boolean { - return !!this.triggers.find(t => t.hasTriggerType(triggerType)); - } -} - -export class SpeciesFormChangeItemTrigger extends SpeciesFormChangeTrigger { - public item: FormChangeItem; - public active: boolean; - - constructor(item: FormChangeItem, active = true) { - super(); - this.item = item; - this.active = active; - this.description = this.active - ? i18next.t("pokemonEvolutions:Forms.item", { - item: i18next.t(`modifierType:FormChangeItem.${FormChangeItem[this.item]}`), - }) - : i18next.t("pokemonEvolutions:Forms.deactivateItem", { - item: i18next.t(`modifierType:FormChangeItem.${FormChangeItem[this.item]}`), - }); - } - - canChange(pokemon: Pokemon): boolean { - return !!globalScene.findModifier( - m => - m instanceof PokemonFormChangeItemModifier && - m.pokemonId === pokemon.id && - m.formChangeItem === this.item && - m.active === this.active, - ); - } -} - -export class SpeciesFormChangeTimeOfDayTrigger extends SpeciesFormChangeTrigger { - public timesOfDay: TimeOfDay[]; - - constructor(...timesOfDay: TimeOfDay[]) { - super(); - this.timesOfDay = timesOfDay; - this.description = i18next.t("pokemonEvolutions:Forms.timeOfDay"); - } - - canChange(_pokemon: Pokemon): boolean { - return this.timesOfDay.indexOf(globalScene.arena.getTimeOfDay()) > -1; - } -} - -export class SpeciesFormChangeActiveTrigger extends SpeciesFormChangeTrigger { - public active: boolean; - - constructor(active = false) { - super(); - this.active = active; - this.description = this.active - ? i18next.t("pokemonEvolutions:Forms.enter") - : i18next.t("pokemonEvolutions:Forms.leave"); - } - - canChange(pokemon: Pokemon): boolean { - return pokemon.isActive(true) === this.active; - } -} - -export class SpeciesFormChangeStatusEffectTrigger extends SpeciesFormChangeTrigger { - public statusEffects: StatusEffect[]; - public invert: boolean; - - constructor(statusEffects: StatusEffect | StatusEffect[], invert = false) { - super(); - if (!Array.isArray(statusEffects)) { - statusEffects = [statusEffects]; - } - this.statusEffects = statusEffects; - this.invert = invert; - this.description = i18next.t("pokemonEvolutions:Forms.statusEffect"); - } - - canChange(pokemon: Pokemon): boolean { - return this.statusEffects.indexOf(pokemon.status?.effect || StatusEffect.NONE) > -1 !== this.invert; - } -} - -export class SpeciesFormChangeMoveLearnedTrigger extends SpeciesFormChangeTrigger { - public move: MoveId; - public known: boolean; - - constructor(move: MoveId, known = true) { - super(); - this.move = move; - this.known = known; - const moveKey = MoveId[this.move] - .split("_") - .filter(f => f) - .map((f, i) => (i ? `${f[0]}${f.slice(1).toLowerCase()}` : f.toLowerCase())) - .join("") as unknown as string; - this.description = known - ? i18next.t("pokemonEvolutions:Forms.moveLearned", { - move: i18next.t(`move:${moveKey}.name`), - }) - : i18next.t("pokemonEvolutions:Forms.moveForgotten", { - move: i18next.t(`move:${moveKey}.name`), - }); - } - - canChange(pokemon: Pokemon): boolean { - return !!pokemon.moveset.filter(m => m.moveId === this.move).length === this.known; - } -} - -export abstract class SpeciesFormChangeMoveTrigger extends SpeciesFormChangeTrigger { - public movePredicate: (m: MoveId) => boolean; - public used: boolean; - - constructor(move: MoveId | ((m: MoveId) => boolean), used = true) { - super(); - this.movePredicate = typeof move === "function" ? move : (m: MoveId) => m === move; - this.used = used; - } -} - -export class SpeciesFormChangePreMoveTrigger extends SpeciesFormChangeMoveTrigger { - description = i18next.t("pokemonEvolutions:Forms.preMove"); - - canChange(pokemon: Pokemon): boolean { - const command = globalScene.currentBattle.turnCommands[pokemon.getBattlerIndex()]; - return !!command?.move && this.movePredicate(command.move.move) === this.used; - } -} - -export class SpeciesFormChangePostMoveTrigger extends SpeciesFormChangeMoveTrigger { - description = i18next.t("pokemonEvolutions:Forms.postMove"); - - canChange(pokemon: Pokemon): boolean { - return ( - pokemon.summonData && !!pokemon.getLastXMoves(1).filter(m => this.movePredicate(m.move)).length === this.used - ); - } -} - -export class MeloettaFormChangePostMoveTrigger extends SpeciesFormChangePostMoveTrigger { - override canChange(pokemon: Pokemon): boolean { - if (globalScene.gameMode.hasChallenge(Challenges.SINGLE_TYPE)) { - return false; - } - // Meloetta will not transform if it has the ability Sheer Force when using Relic Song - if (pokemon.hasAbility(AbilityId.SHEER_FORCE)) { - return false; - } - return super.canChange(pokemon); - } -} - -export class SpeciesDefaultFormMatchTrigger extends SpeciesFormChangeTrigger { - private formKey: string; - - constructor(formKey: string) { - super(); - this.formKey = formKey; - this.description = ""; - } - - canChange(pokemon: Pokemon): boolean { - return ( - this.formKey === - pokemon.species.forms[globalScene.getSpeciesFormIndex(pokemon.species, pokemon.gender, pokemon.getNature(), true)] - .formKey - ); - } -} - -/** - * Class used for triggering form changes based on the user's Tera type. - * Used by Ogerpon and Terapagos. - * @extends SpeciesFormChangeTrigger - */ -export class SpeciesFormChangeTeraTrigger extends SpeciesFormChangeTrigger { - description = i18next.t("pokemonEvolutions:Forms.tera"); -} - -/** - * Class used for triggering form changes based on the user's lapsed Tera type. - * Used by Ogerpon and Terapagos. - * @extends SpeciesFormChangeTrigger - */ -export class SpeciesFormChangeLapseTeraTrigger extends SpeciesFormChangeTrigger { - description = i18next.t("pokemonEvolutions:Forms.teraLapse"); -} - -/** - * Class used for triggering form changes based on weather. - * Used by Castform and Cherrim. - * @extends SpeciesFormChangeTrigger - */ -export class SpeciesFormChangeWeatherTrigger extends SpeciesFormChangeTrigger { - /** The ability that triggers the form change */ - public ability: AbilityId; - /** The list of weathers that trigger the form change */ - public weathers: WeatherType[]; - - constructor(ability: AbilityId, weathers: WeatherType[]) { - super(); - this.ability = ability; - this.weathers = weathers; - this.description = i18next.t("pokemonEvolutions:Forms.weather"); - } - - /** - * Checks if the Pokemon has the required ability and is in the correct weather while - * the weather or ability is also not suppressed. - * @param {Pokemon} pokemon the pokemon that is trying to do the form change - * @returns `true` if the Pokemon can change forms, `false` otherwise - */ - canChange(pokemon: Pokemon): boolean { - const currentWeather = globalScene.arena.weather?.weatherType ?? WeatherType.NONE; - const isWeatherSuppressed = globalScene.arena.weather?.isEffectSuppressed(); - const isAbilitySuppressed = pokemon.summonData.abilitySuppressed; - - return ( - !isAbilitySuppressed && - !isWeatherSuppressed && - pokemon.hasAbility(this.ability) && - this.weathers.includes(currentWeather) - ); - } -} - -/** - * Class used for reverting to the original form when the weather runs out - * or when the user loses the ability/is suppressed. - * Used by Castform and Cherrim. - * @extends SpeciesFormChangeTrigger - */ -export class SpeciesFormChangeRevertWeatherFormTrigger extends SpeciesFormChangeTrigger { - /** The ability that triggers the form change*/ - public ability: AbilityId; - /** The list of weathers that will also trigger a form change to original form */ - public weathers: WeatherType[]; - - constructor(ability: AbilityId, weathers: WeatherType[]) { - super(); - this.ability = ability; - this.weathers = weathers; - this.description = i18next.t("pokemonEvolutions:Forms.weatherRevert"); - } - - /** - * Checks if the Pokemon has the required ability and the weather is one that will revert - * the Pokemon to its original form or the weather or ability is suppressed - * @param {Pokemon} pokemon the pokemon that is trying to do the form change - * @returns `true` if the Pokemon will revert to its original form, `false` otherwise - */ - canChange(pokemon: Pokemon): boolean { - if (pokemon.hasAbility(this.ability, false, true)) { - const currentWeather = globalScene.arena.weather?.weatherType ?? WeatherType.NONE; - const isWeatherSuppressed = globalScene.arena.weather?.isEffectSuppressed(); - const isAbilitySuppressed = pokemon.summonData.abilitySuppressed; - const summonDataAbility = pokemon.summonData.ability; - const isAbilityChanged = summonDataAbility !== this.ability && summonDataAbility !== AbilityId.NONE; - - if (this.weathers.includes(currentWeather) || isWeatherSuppressed || isAbilitySuppressed || isAbilityChanged) { - return true; - } - } - return false; - } -} - -export function getSpeciesFormChangeMessage(pokemon: Pokemon, formChange: SpeciesFormChange, preName: string): string { - const isMega = formChange.formKey.indexOf(SpeciesFormKey.MEGA) > -1; - const isGmax = formChange.formKey.indexOf(SpeciesFormKey.GIGANTAMAX) > -1; - const isEmax = formChange.formKey.indexOf(SpeciesFormKey.ETERNAMAX) > -1; - const isRevert = !isMega && formChange.formKey === pokemon.species.forms[0].formKey; - if (isMega) { - return i18next.t("battlePokemonForm:megaChange", { - preName, - pokemonName: pokemon.name, - }); - } - if (isGmax) { - return i18next.t("battlePokemonForm:gigantamaxChange", { - preName, - pokemonName: pokemon.name, - }); - } - if (isEmax) { - return i18next.t("battlePokemonForm:eternamaxChange", { - preName, - pokemonName: pokemon.name, - }); - } - if (isRevert) { - return i18next.t("battlePokemonForm:revertChange", { - pokemonName: getPokemonNameWithAffix(pokemon), - }); - } - if (pokemon.getAbility().id === AbilityId.DISGUISE) { - return i18next.t("battlePokemonForm:disguiseChange"); - } - return i18next.t("battlePokemonForm:formChange", { preName }); -} - /** * Gives a condition for form changing checking if a species is registered as caught in the player's dex data. * Used for fusion forms such as Kyurem and Necrozma. diff --git a/src/data/pokemon-forms/form-change-triggers.ts b/src/data/pokemon-forms/form-change-triggers.ts new file mode 100644 index 00000000000..3726781d9e3 --- /dev/null +++ b/src/data/pokemon-forms/form-change-triggers.ts @@ -0,0 +1,345 @@ +import i18next from "i18next"; +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"; +import type { PokemonFormChangeItemModifier } from "#app/modifier/modifier"; +import { getPokemonNameWithAffix } from "#app/messages"; +import { globalScene } from "#app/global-scene"; +import { FormChangeItem } from "#enums/form-change-item"; +import { AbilityId } from "#enums/ability-id"; +import { Challenges } from "#enums/challenges"; +import { MoveId } from "#enums/move-id"; +import { SpeciesFormKey } from "#enums/species-form-key"; +import { StatusEffect } from "#enums/status-effect"; +import { WeatherType } from "#enums/weather-type"; + +export abstract class SpeciesFormChangeTrigger { + public description = ""; + + canChange(_pokemon: Pokemon): boolean { + return true; + } + + hasTriggerType(triggerType: Constructor): boolean { + return this instanceof triggerType; + } +} + +export class SpeciesFormChangeManualTrigger extends SpeciesFormChangeTrigger {} + +export class SpeciesFormChangeAbilityTrigger extends SpeciesFormChangeTrigger { + public description: string = i18next.t("pokemonEvolutions:Forms.ability"); +} + +export class SpeciesFormChangeCompoundTrigger { + public description = ""; + public triggers: SpeciesFormChangeTrigger[]; + + constructor(...triggers: SpeciesFormChangeTrigger[]) { + this.triggers = triggers; + this.description = this.triggers + .filter(trigger => trigger?.description?.length > 0) + .map(trigger => trigger.description) + .join(", "); + } + + canChange(pokemon: Pokemon): boolean { + for (const trigger of this.triggers) { + if (!trigger.canChange(pokemon)) { + return false; + } + } + + return true; + } + + hasTriggerType(triggerType: Constructor): boolean { + return !!this.triggers.find(t => t.hasTriggerType(triggerType)); + } +} + +export class SpeciesFormChangeItemTrigger extends SpeciesFormChangeTrigger { + public item: FormChangeItem; + public active: boolean; + + constructor(item: FormChangeItem, active = true) { + super(); + this.item = item; + this.active = active; + this.description = this.active + ? i18next.t("pokemonEvolutions:Forms.item", { + item: i18next.t(`modifierType:FormChangeItem.${FormChangeItem[this.item]}`), + }) + : i18next.t("pokemonEvolutions:Forms.deactivateItem", { + item: i18next.t(`modifierType:FormChangeItem.${FormChangeItem[this.item]}`), + }); + } + + canChange(pokemon: Pokemon): boolean { + return !!globalScene.findModifier(r => { + // Assume that if m has the `formChangeItem` property, then it is a PokemonFormChangeItemModifier + const m = r as PokemonFormChangeItemModifier; + return ( + "formChangeItem" in m && + m.pokemonId === pokemon.id && + m.formChangeItem === this.item && + m.active === this.active + ); + }); + } +} + +export class SpeciesFormChangeTimeOfDayTrigger extends SpeciesFormChangeTrigger { + public timesOfDay: TimeOfDay[]; + + constructor(...timesOfDay: TimeOfDay[]) { + super(); + this.timesOfDay = timesOfDay; + this.description = i18next.t("pokemonEvolutions:Forms.timeOfDay"); + } + + canChange(_pokemon: Pokemon): boolean { + return this.timesOfDay.indexOf(globalScene.arena.getTimeOfDay()) > -1; + } +} +export class SpeciesFormChangeActiveTrigger extends SpeciesFormChangeTrigger { + public active: boolean; + + constructor(active = false) { + super(); + this.active = active; + this.description = this.active + ? i18next.t("pokemonEvolutions:Forms.enter") + : i18next.t("pokemonEvolutions:Forms.leave"); + } + + canChange(pokemon: Pokemon): boolean { + return pokemon.isActive(true) === this.active; + } +} + +export class SpeciesFormChangeStatusEffectTrigger extends SpeciesFormChangeTrigger { + public statusEffects: StatusEffect[]; + public invert: boolean; + + constructor(statusEffects: StatusEffect | StatusEffect[], invert = false) { + super(); + this.statusEffects = coerceArray(statusEffects); + this.invert = invert; + // this.description = i18next.t("pokemonEvolutions:Forms.statusEffect"); + } + + canChange(pokemon: Pokemon): boolean { + return this.statusEffects.indexOf(pokemon.status?.effect || StatusEffect.NONE) > -1 !== this.invert; + } +} + +export class SpeciesFormChangeMoveLearnedTrigger extends SpeciesFormChangeTrigger { + public move: MoveId; + public known: boolean; + + constructor(move: MoveId, known = true) { + super(); + this.move = move; + this.known = known; + const moveKey = MoveId[this.move] + .split("_") + .filter(f => f) + .map((f, i) => (i ? `${f[0]}${f.slice(1).toLowerCase()}` : f.toLowerCase())) + .join("") as unknown as string; + this.description = known + ? i18next.t("pokemonEvolutions:Forms.moveLearned", { + move: i18next.t(`move:${moveKey}.name`), + }) + : i18next.t("pokemonEvolutions:Forms.moveForgotten", { + move: i18next.t(`move:${moveKey}.name`), + }); + } + + canChange(pokemon: Pokemon): boolean { + return !!pokemon.moveset.filter(m => m.moveId === this.move).length === this.known; + } +} + +export abstract class SpeciesFormChangeMoveTrigger extends SpeciesFormChangeTrigger { + public movePredicate: (m: MoveId) => boolean; + public used: boolean; + + constructor(move: MoveId | ((m: MoveId) => boolean), used = true) { + super(); + this.movePredicate = typeof move === "function" ? move : (m: MoveId) => m === move; + this.used = used; + } +} + +export class SpeciesFormChangePreMoveTrigger extends SpeciesFormChangeMoveTrigger { + description = i18next.t("pokemonEvolutions:Forms.preMove"); + canChange(pokemon: Pokemon): boolean { + const command = globalScene.currentBattle.turnCommands[pokemon.getBattlerIndex()]; + return !!command?.move && this.movePredicate(command.move.move) === this.used; + } +} + +export class SpeciesFormChangePostMoveTrigger extends SpeciesFormChangeMoveTrigger { + description = i18next.t("pokemonEvolutions:Forms.postMove"); + canChange(pokemon: Pokemon): boolean { + return ( + pokemon.summonData && !!pokemon.getLastXMoves(1).filter(m => this.movePredicate(m.move)).length === this.used + ); + } +} + +export class MeloettaFormChangePostMoveTrigger extends SpeciesFormChangePostMoveTrigger { + override canChange(pokemon: Pokemon): boolean { + if (globalScene.gameMode.hasChallenge(Challenges.SINGLE_TYPE)) { + return false; + } + // Meloetta will not transform if it has the ability Sheer Force when using Relic Song + if (pokemon.hasAbility(AbilityId.SHEER_FORCE)) { + return false; + } + return super.canChange(pokemon); + } +} + +export class SpeciesDefaultFormMatchTrigger extends SpeciesFormChangeTrigger { + private formKey: string; + + constructor(formKey: string) { + super(); + this.formKey = formKey; + this.description = ""; + } + + canChange(pokemon: Pokemon): boolean { + return ( + this.formKey === + pokemon.species.forms[globalScene.getSpeciesFormIndex(pokemon.species, pokemon.gender, pokemon.getNature(), true)] + .formKey + ); + } +} + +/** + * Class used for triggering form changes based on the user's Tera type. + * Used by Ogerpon and Terapagos. + */ +export class SpeciesFormChangeTeraTrigger extends SpeciesFormChangeTrigger {} + +/** + * Class used for triggering form changes based on the user's lapsed Tera type. + * Used by Ogerpon and Terapagos. + */ +export class SpeciesFormChangeLapseTeraTrigger extends SpeciesFormChangeTrigger {} + +/** + * Class used for triggering form changes based on weather. + * Used by Castform and Cherrim. + */ +export class SpeciesFormChangeWeatherTrigger extends SpeciesFormChangeTrigger { + /** The ability that triggers the form change */ + public ability: AbilityId; + /** The list of weathers that trigger the form change */ + public weathers: WeatherType[]; + + constructor(ability: AbilityId, weathers: WeatherType[]) { + super(); + this.ability = ability; + this.weathers = weathers; + this.description = i18next.t("pokemonEvolutions:Forms.weather"); + } + + /** + * Checks if the Pokemon has the required ability and is in the correct weather while + * the weather or ability is also not suppressed. + * @param pokemon - The pokemon that is trying to do the form change + * @returns `true` if the Pokemon can change forms, `false` otherwise + */ + canChange(pokemon: Pokemon): boolean { + const currentWeather = globalScene.arena.weather?.weatherType ?? WeatherType.NONE; + const isWeatherSuppressed = globalScene.arena.weather?.isEffectSuppressed(); + const isAbilitySuppressed = pokemon.summonData.abilitySuppressed; + + return ( + !isAbilitySuppressed && + !isWeatherSuppressed && + pokemon.hasAbility(this.ability) && + this.weathers.includes(currentWeather) + ); + } +} + +/** + * Class used for reverting to the original form when the weather runs out + * or when the user loses the ability/is suppressed. + * Used by Castform and Cherrim. + */ +export class SpeciesFormChangeRevertWeatherFormTrigger extends SpeciesFormChangeTrigger { + /** The ability that triggers the form change*/ + public ability: AbilityId; + /** The list of weathers that will also trigger a form change to original form */ + public weathers: WeatherType[]; + + constructor(ability: AbilityId, weathers: WeatherType[]) { + super(); + this.ability = ability; + this.weathers = weathers; + this.description = i18next.t("pokemonEvolutions:Forms.weatherRevert"); + } + + /** + * Checks if the Pokemon has the required ability and the weather is one that will revert + * the Pokemon to its original form or the weather or ability is suppressed + * @param {Pokemon} pokemon the pokemon that is trying to do the form change + * @returns `true` if the Pokemon will revert to its original form, `false` otherwise + */ + canChange(pokemon: Pokemon): boolean { + if (pokemon.hasAbility(this.ability, false, true)) { + const currentWeather = globalScene.arena.weather?.weatherType ?? WeatherType.NONE; + const isWeatherSuppressed = globalScene.arena.weather?.isEffectSuppressed(); + const isAbilitySuppressed = pokemon.summonData.abilitySuppressed; + const summonDataAbility = pokemon.summonData.ability; + const isAbilityChanged = summonDataAbility !== this.ability && summonDataAbility !== AbilityId.NONE; + + if (this.weathers.includes(currentWeather) || isWeatherSuppressed || isAbilitySuppressed || isAbilityChanged) { + return true; + } + } + return false; + } +} + +export function getSpeciesFormChangeMessage(pokemon: Pokemon, formChange: SpeciesFormChange, preName: string): string { + const isMega = formChange.formKey.indexOf(SpeciesFormKey.MEGA) > -1; + const isGmax = formChange.formKey.indexOf(SpeciesFormKey.GIGANTAMAX) > -1; + const isEmax = formChange.formKey.indexOf(SpeciesFormKey.ETERNAMAX) > -1; + const isRevert = !isMega && formChange.formKey === pokemon.species.forms[0].formKey; + if (isMega) { + return i18next.t("battlePokemonForm:megaChange", { + preName, + pokemonName: pokemon.name, + }); + } + if (isGmax) { + return i18next.t("battlePokemonForm:gigantamaxChange", { + preName, + pokemonName: pokemon.name, + }); + } + if (isEmax) { + return i18next.t("battlePokemonForm:eternamaxChange", { + preName, + pokemonName: pokemon.name, + }); + } + if (isRevert) { + return i18next.t("battlePokemonForm:revertChange", { + pokemonName: getPokemonNameWithAffix(pokemon), + }); + } + if (pokemon.getAbility().id === AbilityId.DISGUISE) { + return i18next.t("battlePokemonForm:disguiseChange"); + } + return i18next.t("battlePokemonForm:formChange", { preName }); +} diff --git a/src/data/pokemon-species.ts b/src/data/pokemon-species.ts index 36a8bbb0520..bb6ede7731d 100644 --- a/src/data/pokemon-species.ts +++ b/src/data/pokemon-species.ts @@ -7,7 +7,8 @@ import i18next from "i18next"; import type { AnySound } from "#app/battle-scene"; import { globalScene } from "#app/global-scene"; import type { GameMode } from "#app/game-mode"; -import { DexAttr, type StarterMoveset } from "#app/system/game-data"; +import type { StarterMoveset } from "#app/system/game-data"; +import { DexAttr } from "#enums/dex-attr"; import { isNullOrUndefined, capitalizeString, @@ -83,13 +84,14 @@ export const normalForm: SpeciesId[] = [ /** * Gets the {@linkcode PokemonSpecies} object associated with the {@linkcode SpeciesId} enum given - * @param species The species to fetch + * @param species - The {@linkcode SpeciesId} to fetch. + * If an array of `SpeciesId`s is passed (such as for named trainer spawn pools), + * one will be selected at random. * @returns The associated {@linkcode PokemonSpecies} object */ 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 + // TODO: this RNG roll should not be handled by this function species = species[Math.floor(Math.random() * species.length)]; } if (species >= 2000) { diff --git a/src/data/terrain.ts b/src/data/terrain.ts index 5b6063cee68..b3ee62ac2f9 100644 --- a/src/data/terrain.ts +++ b/src/data/terrain.ts @@ -1,8 +1,7 @@ import type Pokemon from "../field/pokemon"; import type Move from "./moves/move"; import { PokemonType } from "#enums/pokemon-type"; -import { ProtectAttr } from "./moves/move"; -import type { BattlerIndex } from "#app/battle"; +import type { BattlerIndex } from "#enums/battler-index"; import i18next from "i18next"; export enum TerrainType { @@ -55,7 +54,7 @@ export class Terrain { isMoveTerrainCancelled(user: Pokemon, targets: BattlerIndex[], move: Move): boolean { switch (this.terrainType) { case TerrainType.PSYCHIC: - if (!move.hasAttr(ProtectAttr)) { + if (!move.hasAttr("ProtectAttr")) { // Cancels move if the move has positive priority and targets a Pokemon grounded on the Psychic Terrain return ( move.getPriority(user) > 0 && diff --git a/src/data/trainers/TrainerPartyTemplate.ts b/src/data/trainers/TrainerPartyTemplate.ts index 86201589276..135fe669825 100644 --- a/src/data/trainers/TrainerPartyTemplate.ts +++ b/src/data/trainers/TrainerPartyTemplate.ts @@ -1,7 +1,7 @@ import { startingWave } from "#app/starting-wave"; import { globalScene } from "#app/global-scene"; import { PartyMemberStrength } from "#enums/party-member-strength"; -import { GameModes } from "#app/game-mode"; +import { GameModes } from "#enums/game-modes"; import { ClassicFixedBossWaves } from "#enums/fixed-boss-waves"; export class TrainerPartyTemplate { diff --git a/src/data/trainers/trainer-config.ts b/src/data/trainers/trainer-config.ts index 8e704b0b301..063dddafee8 100644 --- a/src/data/trainers/trainer-config.ts +++ b/src/data/trainers/trainer-config.ts @@ -1,12 +1,19 @@ import { globalScene } from "#app/global-scene"; -import { modifierTypes } from "#app/modifier/modifier-type"; -import { PokemonMove } from "#app/field/pokemon"; -import { toReadableString, isNullOrUndefined, randSeedItem, randSeedInt, randSeedIntRange } from "#app/utils/common"; +import { modifierTypes } from "../data-lists"; +import { PokemonMove } from "../moves/pokemon-move"; +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 { tmSpecies } from "#app/data/balance/tms"; -import { doubleBattleDialogue } from "#app/data/dialogue"; -import { TrainerVariant } from "#app/field/trainer"; +import { doubleBattleDialogue } from "../double-battle-dialogue"; +import { TrainerVariant } from "#enums/trainer-variant"; import { getIsInitialized, initI18n } from "#app/plugins/i18n"; import i18next from "i18next"; import { Gender } from "#app/data/gender"; @@ -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 3bd2e38824d..425e15b12a8 100644 --- a/src/data/weather.ts +++ b/src/data/weather.ts @@ -4,14 +4,13 @@ import { getPokemonNameWithAffix } from "../messages"; import type Pokemon from "../field/pokemon"; import { PokemonType } from "#enums/pokemon-type"; import type Move from "./moves/move"; -import { AttackMove } 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; @@ -95,9 +94,9 @@ export class Weather { switch (this.weatherType) { case WeatherType.HARSH_SUN: - return move instanceof AttackMove && moveType === PokemonType.WATER; + return move.is("AttackMove") && moveType === PokemonType.WATER; case WeatherType.HEAVY_RAIN: - return move instanceof AttackMove && moveType === PokemonType.FIRE; + return move.is("AttackMove") && moveType === PokemonType.FIRE; } return false; @@ -109,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/ability-attr.ts b/src/enums/ability-attr.ts new file mode 100644 index 00000000000..5f7d107f2d1 --- /dev/null +++ b/src/enums/ability-attr.ts @@ -0,0 +1,11 @@ +/** + * Not to be confused with an Ability Attribute. + * This is an object literal storing the slot that an ability can occupy. + */ +export const AbilityAttr = Object.freeze({ + ABILITY_1: 1, + ABILITY_2: 2, + ABILITY_HIDDEN: 4, +}); + +export type AbilityAttr = typeof AbilityAttr[keyof typeof AbilityAttr]; \ No newline at end of file diff --git a/src/enums/ai-type.ts b/src/enums/ai-type.ts new file mode 100644 index 00000000000..13931172a4a --- /dev/null +++ b/src/enums/ai-type.ts @@ -0,0 +1,5 @@ +export enum AiType { + RANDOM, + SMART_RANDOM, + SMART +} diff --git a/src/enums/arena-tag-side.ts b/src/enums/arena-tag-side.ts new file mode 100644 index 00000000000..3e326ce158a --- /dev/null +++ b/src/enums/arena-tag-side.ts @@ -0,0 +1,5 @@ +export enum ArenaTagSide { + BOTH, + PLAYER, + ENEMY +} diff --git a/src/enums/battler-index.ts b/src/enums/battler-index.ts new file mode 100644 index 00000000000..32b1684c86c --- /dev/null +++ b/src/enums/battler-index.ts @@ -0,0 +1,7 @@ +export enum BattlerIndex { + ATTACKER = -1, + PLAYER, + PLAYER_2, + ENEMY, + ENEMY_2 +} diff --git a/src/enums/battler-tag-lapse-type.ts b/src/enums/battler-tag-lapse-type.ts new file mode 100644 index 00000000000..4375e87e4e0 --- /dev/null +++ b/src/enums/battler-tag-lapse-type.ts @@ -0,0 +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 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, + /** The tag has some other custom activation or removal condition. */ + CUSTOM, +} diff --git a/src/enums/challenge-type.ts b/src/enums/challenge-type.ts new file mode 100644 index 00000000000..d9b1fce3e6e --- /dev/null +++ b/src/enums/challenge-type.ts @@ -0,0 +1,69 @@ +/** + * An enum for all the challenge types. The parameter entries on these describe the + * parameters to use when calling the applyChallenges function. + */ +export enum ChallengeType { + /** + * Challenges which modify what starters you can choose + * @see {@link Challenge.applyStarterChoice} + */ + STARTER_CHOICE, + /** + * Challenges which modify how many starter points you have + * @see {@link Challenge.applyStarterPoints} + */ + STARTER_POINTS, + /** + * Challenges which modify how many starter points you have + * @see {@link Challenge.applyStarterPointCost} + */ + STARTER_COST, + /** + * Challenges which modify your starters in some way + * @see {@link Challenge.applyStarterModify} + */ + STARTER_MODIFY, + /** + * Challenges which limit which pokemon you can have in battle. + * @see {@link Challenge.applyPokemonInBattle} + */ + POKEMON_IN_BATTLE, + /** + * Adds or modifies the fixed battles in a run + * @see {@link Challenge.applyFixedBattle} + */ + FIXED_BATTLES, + /** + * Modifies the effectiveness of Type matchups in battle + * @see {@linkcode Challenge.applyTypeEffectiveness} + */ + TYPE_EFFECTIVENESS, + /** + * Modifies what level the AI pokemon are. UNIMPLEMENTED. + */ + AI_LEVEL, + /** + * Modifies how many move slots the AI has. UNIMPLEMENTED. + */ + AI_MOVE_SLOTS, + /** + * Modifies if a pokemon has its passive. UNIMPLEMENTED. + */ + PASSIVE_ACCESS, + /** + * Modifies the game mode settings in some way. UNIMPLEMENTED. + */ + GAME_MODE_MODIFY, + /** + * Modifies what level AI pokemon can access a move. UNIMPLEMENTED. + */ + MOVE_ACCESS, + /** + * Modifies what weight AI pokemon have when generating movesets. UNIMPLEMENTED. + */ + MOVE_WEIGHT, + /** + * Modifies what the pokemon stats for Flip Stat Mode. + */ + FLIP_STAT +} diff --git a/src/enums/command.ts b/src/enums/command.ts new file mode 100644 index 00000000000..4cd626bb066 --- /dev/null +++ b/src/enums/command.ts @@ -0,0 +1,7 @@ +export enum Command { + FIGHT = 0, + BALL, + POKEMON, + RUN, + TERA +} diff --git a/src/enums/dex-attr.ts b/src/enums/dex-attr.ts new file mode 100644 index 00000000000..ee5ceb43ef2 --- /dev/null +++ b/src/enums/dex-attr.ts @@ -0,0 +1,11 @@ +export const DexAttr = Object.freeze({ + NON_SHINY: 1n, + SHINY: 2n, + MALE: 4n, + FEMALE: 8n, + DEFAULT_VARIANT: 16n, + VARIANT_2: 32n, + VARIANT_3: 64n, + DEFAULT_FORM: 128n, +}); +export type DexAttr = typeof DexAttr[keyof typeof DexAttr]; 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/field-position.ts b/src/enums/field-position.ts new file mode 100644 index 00000000000..5b7f9c6c570 --- /dev/null +++ b/src/enums/field-position.ts @@ -0,0 +1,5 @@ +export enum FieldPosition { + CENTER, + LEFT, + RIGHT +} diff --git a/src/enums/form-change-item.ts b/src/enums/form-change-item.ts new file mode 100644 index 00000000000..15620eafd0a --- /dev/null +++ b/src/enums/form-change-item.ts @@ -0,0 +1,119 @@ +export enum FormChangeItem { + NONE, + + ABOMASITE, + ABSOLITE, + AERODACTYLITE, + AGGRONITE, + ALAKAZITE, + ALTARIANITE, + AMPHAROSITE, + AUDINITE, + BANETTITE, + BEEDRILLITE, + BLASTOISINITE, + BLAZIKENITE, + CAMERUPTITE, + CHARIZARDITE_X, + CHARIZARDITE_Y, + DIANCITE, + GALLADITE, + GARCHOMPITE, + GARDEVOIRITE, + GENGARITE, + GLALITITE, + GYARADOSITE, + HERACRONITE, + HOUNDOOMINITE, + KANGASKHANITE, + LATIASITE, + LATIOSITE, + LOPUNNITE, + LUCARIONITE, + MANECTITE, + MAWILITE, + MEDICHAMITE, + METAGROSSITE, + MEWTWONITE_X, + MEWTWONITE_Y, + PIDGEOTITE, + PINSIRITE, + RAYQUAZITE, + SABLENITE, + SALAMENCITE, + SCEPTILITE, + SCIZORITE, + SHARPEDONITE, + SLOWBRONITE, + STEELIXITE, + SWAMPERTITE, + TYRANITARITE, + VENUSAURITE, + + BLUE_ORB = 50, + RED_ORB, + ADAMANT_CRYSTAL, + LUSTROUS_GLOBE, + GRISEOUS_CORE, + REVEAL_GLASS, + MAX_MUSHROOMS, + DARK_STONE, + LIGHT_STONE, + PRISON_BOTTLE, + RUSTED_SWORD, + RUSTED_SHIELD, + ICY_REINS_OF_UNITY, + SHADOW_REINS_OF_UNITY, + ULTRANECROZIUM_Z, + + SHARP_METEORITE = 100, + HARD_METEORITE, + SMOOTH_METEORITE, + GRACIDEA, + SHOCK_DRIVE, + BURN_DRIVE, + CHILL_DRIVE, + DOUSE_DRIVE, + N_SOLARIZER, + N_LUNARIZER, + WELLSPRING_MASK, + HEARTHFLAME_MASK, + CORNERSTONE_MASK, + FIST_PLATE, + SKY_PLATE, + TOXIC_PLATE, + EARTH_PLATE, + STONE_PLATE, + INSECT_PLATE, + SPOOKY_PLATE, + IRON_PLATE, + FLAME_PLATE, + SPLASH_PLATE, + MEADOW_PLATE, + ZAP_PLATE, + MIND_PLATE, + ICICLE_PLATE, + DRACO_PLATE, + DREAD_PLATE, + PIXIE_PLATE, + BLANK_PLATE,// TODO: Find a potential use for this + LEGEND_PLATE,// TODO: Find a potential use for this + FIGHTING_MEMORY, + FLYING_MEMORY, + POISON_MEMORY, + GROUND_MEMORY, + ROCK_MEMORY, + BUG_MEMORY, + GHOST_MEMORY, + STEEL_MEMORY, + FIRE_MEMORY, + WATER_MEMORY, + GRASS_MEMORY, + ELECTRIC_MEMORY, + PSYCHIC_MEMORY, + ICE_MEMORY, + DRAGON_MEMORY, + DARK_MEMORY, + FAIRY_MEMORY, + NORMAL_MEMORY +} diff --git a/src/enums/game-modes.ts b/src/enums/game-modes.ts new file mode 100644 index 00000000000..837b634621c --- /dev/null +++ b/src/enums/game-modes.ts @@ -0,0 +1,7 @@ +export enum GameModes { + CLASSIC, + ENDLESS, + SPLICED_ENDLESS, + DAILY, + CHALLENGE +} diff --git a/src/enums/hit-result.ts b/src/enums/hit-result.ts new file mode 100644 index 00000000000..3e62587dd6c --- /dev/null +++ b/src/enums/hit-result.ts @@ -0,0 +1,15 @@ +export enum HitResult { + EFFECTIVE = 1, + SUPER_EFFECTIVE, + NOT_VERY_EFFECTIVE, + ONE_HIT_KO, + NO_EFFECT, + STATUS, + HEAL, + FAIL, + MISS, + INDIRECT, + IMMUNE, + CONFUSION, + INDIRECT_KO +} diff --git a/src/enums/learn-move-situation.ts b/src/enums/learn-move-situation.ts new file mode 100644 index 00000000000..9b329d0f3de --- /dev/null +++ b/src/enums/learn-move-situation.ts @@ -0,0 +1,8 @@ +export enum LearnMoveSituation { + MISC, + LEVEL_UP, + RELEARN, + EVOLUTION, + EVOLUTION_FUSED,// If fusionSpecies has Evolved + EVOLUTION_FUSED_BASE +} diff --git a/src/enums/learn-move-type.ts b/src/enums/learn-move-type.ts new file mode 100644 index 00000000000..442639c1bc7 --- /dev/null +++ b/src/enums/learn-move-type.ts @@ -0,0 +1,8 @@ +export enum LearnMoveType { + /** For learning a move via level-up, evolution, or other non-item-based event */ + LEARN_MOVE, + /** For learning a move via Memory Mushroom */ + MEMORY, + /** For learning a move via TM */ + TM +} 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-anims-common.ts b/src/enums/move-anims-common.ts new file mode 100644 index 00000000000..f21e4c8be4a --- /dev/null +++ b/src/enums/move-anims-common.ts @@ -0,0 +1,95 @@ +export enum AnimFrameTarget { + USER, + TARGET, + GRAPHIC +} + +export enum AnimFocus { + TARGET = 1, + USER, + USER_TARGET, + SCREEN +} + +export enum AnimBlendType { + NORMAL, + ADD, + SUBTRACT +} + +export enum ChargeAnim { + FLY_CHARGING = 1000, + BOUNCE_CHARGING, + DIG_CHARGING, + FUTURE_SIGHT_CHARGING, + DIVE_CHARGING, + SOLAR_BEAM_CHARGING, + SHADOW_FORCE_CHARGING, + SKULL_BASH_CHARGING, + FREEZE_SHOCK_CHARGING, + SKY_DROP_CHARGING, + SKY_ATTACK_CHARGING, + ICE_BURN_CHARGING, + DOOM_DESIRE_CHARGING, + RAZOR_WIND_CHARGING, + PHANTOM_FORCE_CHARGING, + GEOMANCY_CHARGING, + SHADOW_BLADE_CHARGING, + SOLAR_BLADE_CHARGING, + BEAK_BLAST_CHARGING, + METEOR_BEAM_CHARGING, + ELECTRO_SHOT_CHARGING +} + +export enum CommonAnim { + USE_ITEM = 2000, + HEALTH_UP, + TERASTALLIZE, + POISON = 2010, + TOXIC, + PARALYSIS, + SLEEP, + FROZEN, + BURN, + CONFUSION, + ATTRACT, + BIND, + WRAP, + CURSE_NO_GHOST, + LEECH_SEED, + FIRE_SPIN, + PROTECT, + COVET, + WHIRLPOOL, + BIDE, + SAND_TOMB, + QUICK_GUARD, + WIDE_GUARD, + CURSE, + MAGMA_STORM, + CLAMP, + SNAP_TRAP, + THUNDER_CAGE, + INFESTATION, + ORDER_UP_CURLY, + ORDER_UP_DROOPY, + ORDER_UP_STRETCHY, + RAGING_BULL_FIRE, + RAGING_BULL_WATER, + SALT_CURE, + POWDER, + SUNNY = 2100, + RAIN, + SANDSTORM, + HAIL, + SNOW, + WIND, + HEAVY_RAIN, + HARSH_SUN, + STRONG_WINDS, + MISTY_TERRAIN = 2110, + ELECTRIC_TERRAIN, + GRASSY_TERRAIN, + PSYCHIC_TERRAIN, + LOCK_ON = 2120 +} diff --git a/src/enums/move-result.ts b/src/enums/move-result.ts new file mode 100644 index 00000000000..d402f5b1aed --- /dev/null +++ b/src/enums/move-result.ts @@ -0,0 +1,7 @@ +export enum MoveResult { + PENDING, + SUCCESS, + FAIL, + MISS, + OTHER +} diff --git a/src/enums/move-source-type.ts b/src/enums/move-source-type.ts new file mode 100644 index 00000000000..d9afb07e7f7 --- /dev/null +++ b/src/enums/move-source-type.ts @@ -0,0 +1,12 @@ +/** + * Used for challenge types that modify movesets, these denote the various sources of moves for pokemon. + */ +export enum MoveSourceType { + LEVEL_UP,// Currently unimplemented for move access + RELEARNER,// Relearner moves currently unimplemented + COMMON_TM, + GREAT_TM, + ULTRA_TM, + COMMON_EGG, + RARE_EGG +} 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/trainer-variant.ts b/src/enums/trainer-variant.ts new file mode 100644 index 00000000000..cd8d71cc1b9 --- /dev/null +++ b/src/enums/trainer-variant.ts @@ -0,0 +1,5 @@ +export enum TrainerVariant { + DEFAULT, + FEMALE, + DOUBLE +} 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/events/arena.ts b/src/events/arena.ts index ad77289b76b..3b65506db98 100644 --- a/src/events/arena.ts +++ b/src/events/arena.ts @@ -1,4 +1,4 @@ -import type { ArenaTagSide } from "#app/data/arena-tag"; +import type { ArenaTagSide } from "#enums/arena-tag-side"; import type { ArenaTagType } from "#enums/arena-tag-type"; import type { TerrainType } from "#app/data/terrain"; import type { WeatherType } from "#enums/weather-type"; diff --git a/src/field/arena.ts b/src/field/arena.ts index 82a8afbedad..aece908d653 100644 --- a/src/field/arena.ts +++ b/src/field/arena.ts @@ -12,21 +12,19 @@ import { getLegendaryWeatherContinuesMessage, Weather, } from "#app/data/weather"; -import { CommonAnim } from "#app/data/battle-anims"; +import { CommonAnim } from "#enums/move-anims-common"; import type { PokemonType } from "#enums/pokemon-type"; import type Move from "#app/data/moves/move"; import type { ArenaTag } from "#app/data/arena-tag"; -import { ArenaTagSide, ArenaTrapTag, getArenaTag } from "#app/data/arena-tag"; -import type { BattlerIndex } from "#app/battle"; +import { ArenaTrapTag, getArenaTag } from "#app/data/arena-tag"; +import { ArenaTagSide } from "#enums/arena-tag-side"; +import type { BattlerIndex } from "#enums/battler-index"; import { Terrain, TerrainType } from "#app/data/terrain"; 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"; @@ -37,7 +35,10 @@ import { SpeciesId } from "#enums/species-id"; import { TimeOfDay } from "#enums/time-of-day"; import { TrainerType } from "#enums/trainer-type"; import { AbilityId } from "#enums/ability-id"; -import { SpeciesFormChangeRevertWeatherFormTrigger, SpeciesFormChangeWeatherTrigger } from "#app/data/pokemon-forms"; +import { + SpeciesFormChangeRevertWeatherFormTrigger, + SpeciesFormChangeWeatherTrigger, +} from "#app/data/pokemon-forms/form-change-triggers"; import { WeatherType } from "#enums/weather-type"; import { FieldEffectModifier } from "#app/modifier/modifier"; @@ -261,7 +262,7 @@ export class Arena { return 5; } break; - case SpeciesId.LYCANROC: + case SpeciesId.LYCANROC: { const timeOfDay = this.getTimeOfDay(); switch (timeOfDay) { case TimeOfDay.DAY: @@ -273,6 +274,7 @@ export class Arena { return 1; } break; + } } return 0; @@ -370,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; @@ -459,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; @@ -763,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/damage-number-handler.ts b/src/field/damage-number-handler.ts index bfb85018dd6..b8b3ed76e18 100644 --- a/src/field/damage-number-handler.ts +++ b/src/field/damage-number-handler.ts @@ -1,9 +1,9 @@ import { TextStyle, addTextObject } from "../ui/text"; import type { DamageResult } from "./pokemon"; import type Pokemon from "./pokemon"; -import { HitResult } from "./pokemon"; +import { HitResult } from "#enums/hit-result"; import { formatStat, fixedInt } from "#app/utils/common"; -import type { BattlerIndex } from "../battle"; +import type { BattlerIndex } from "#enums/battler-index"; import { globalScene } from "#app/global-scene"; type TextAndShadowArr = [string | null, string | null]; 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 58bb07c98e2..ea6ae2d486e 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -9,36 +9,8 @@ import BattleInfo from "#app/ui/battle-info/battle-info"; import { EnemyBattleInfo } from "#app/ui/battle-info/enemy-battle-info"; import { PlayerBattleInfo } from "#app/ui/battle-info/player-battle-info"; import type Move from "#app/data/moves/move"; -import { - HighCritAttr, - HitsTagAttr, - applyMoveAttrs, - FixedDamageAttr, - VariableAtkAttr, - TypelessAttr, - CritOnlyAttr, - getMoveTargets, - OneHitKOAttr, - VariableMoveTypeAttr, - VariableDefAttr, - AttackMove, - ModifiedDamageAttr, - VariableMoveTypeMultiplierAttr, - IgnoreOpponentStatStagesAttr, - SacrificialAttr, - VariableMoveCategoryAttr, - CounterDamageAttr, - StatStageChangeAttr, - RechargeAttr, - IgnoreWeatherTypeDebuffAttr, - BypassBurnDamageReductionAttr, - SacrificialAttrOnHit, - OneHitKOAccuracyAttr, - RespectAttackTypeImmunityAttr, - CombinedPledgeStabBoostAttr, - VariableMoveTypeChartAttr, - HpSplitAttr, -} from "#app/data/moves/move"; +import { getMoveTargets } from "#app/data/moves/move-utils"; +import { applyMoveAttrs } from "#app/data/moves/apply-attrs"; import { allMoves } from "#app/data/data-lists"; import { MoveTarget } from "#enums/MoveTarget"; import { MoveCategory } from "#enums/MoveCategory"; @@ -66,9 +38,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"; @@ -116,7 +88,6 @@ import { import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "#app/data/balance/tms"; import { BattlerTag, - BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, @@ -135,67 +106,31 @@ import { loadBattlerTag, type GrudgeTag, } from "../data/battler-tags"; +import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type"; import { WeatherType } from "#enums/weather-type"; -import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "#app/data/arena-tag"; +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 "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { UiMode } from "#enums/ui-mode"; import type { PartyOption } from "#app/ui/party-ui-handler"; import PartyUiHandler, { PartyUiMode } from "#app/ui/party-ui-handler"; @@ -204,7 +139,7 @@ import type { LevelMoves } from "#app/data/balance/pokemon-level-moves"; import { EVOLVE_MOVE, RELEARN_MOVE } from "#app/data/balance/pokemon-level-moves"; import { achvs } from "#app/system/achv"; import type { StarterDataEntry, StarterMoveset } from "#app/system/game-data"; -import { DexAttr } from "#app/system/game-data"; +import { DexAttr } from "#enums/dex-attr"; import { QuantizerCelebi, argbFromRgba, rgbaFromArgb } from "@material/material-color-utilities"; import { getNatureStatMultiplier } from "#app/data/nature"; import type { SpeciesFormChange } from "#app/data/pokemon-forms"; @@ -213,14 +148,15 @@ import { SpeciesFormChangeLapseTeraTrigger, SpeciesFormChangeMoveLearnedTrigger, SpeciesFormChangePostMoveTrigger, -} from "#app/data/pokemon-forms"; +} from "#app/data/pokemon-forms/form-change-triggers"; import { TerrainType } from "#app/data/terrain"; 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 { applyChallenges, ChallengeType } from "#app/data/challenge"; +import { ModifierTier } from "#enums/modifier-tier"; +import { applyChallenges } from "#app/data/challenge"; +import { ChallengeType } from "#enums/challenge-type"; import { AbilityId } from "#enums/ability-id"; import { ArenaTagType } from "#enums/arena-tag-type"; import { BattleSpec } from "#enums/battle-spec"; @@ -249,21 +185,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"; - -export enum LearnMoveSituation { - MISC, - LEVEL_UP, - RELEARN, - EVOLUTION, - EVOLUTION_FUSED, // If fusionSpecies has Evolved - EVOLUTION_FUSED_BASE, // If fusion's base species has Evolved -} - -export enum FieldPosition { - CENTER, - LEFT, - RIGHT, -} +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 = { @@ -395,7 +324,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; @@ -1435,10 +1364,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { */ getCritStage(source: Pokemon, move: Move): number { const critStage = new NumberHolder(0); - applyMoveAttrs(HighCritAttr, source, this, move, critStage); + 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) { @@ -1461,7 +1390,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { */ getMoveCategory(target: Pokemon, move: Move): MoveCategory { const moveCategory = new NumberHolder(move.category); - applyMoveAttrs(VariableMoveCategoryAttr, this, target, move, moveCategory); + applyMoveAttrs("VariableMoveCategoryAttr", this, target, move, moveCategory); return moveCategory.value; } @@ -1499,19 +1428,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, @@ -1838,9 +1775,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 = []; @@ -2094,15 +2029,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)) { @@ -2187,7 +2118,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; @@ -2198,10 +2129,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) @@ -2242,13 +2173,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) @@ -2264,7 +2199,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); } @@ -2335,7 +2270,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; @@ -2356,8 +2291,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { public getMoveType(move: Move, simulated = true): PokemonType { const moveTypeHolder = new NumberHolder(move.type); - applyMoveAttrs(VariableMoveTypeAttr, this, null, move, moveTypeHolder); - applyPreAttackAbAttrs(MoveTypeChangeAbAttr, this, null, move, simulated, moveTypeHolder); + applyMoveAttrs("VariableMoveTypeAttr", this, null, move, 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 @@ -2400,18 +2335,18 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return this.turnData?.moveEffectiveness; } - if (move.hasAttr(TypelessAttr)) { + if (move.hasAttr("TypelessAttr")) { return 1; } const moveType = source.getMoveType(move); const typeMultiplier = new NumberHolder( - move.category !== MoveCategory.STATUS || move.hasAttr(RespectAttackTypeImmunityAttr) + move.category !== MoveCategory.STATUS || move.hasAttr("RespectAttackTypeImmunityAttr") ? this.getAttackTypeEffectiveness(moveType, source, false, simulated, move, useIllusion) : 1, ); - applyMoveAttrs(VariableMoveTypeMultiplierAttr, source, this, move, typeMultiplier); + applyMoveAttrs("VariableMoveTypeMultiplierAttr", source, this, move, typeMultiplier); if (this.getTypes(true, true).find(t => move.isTypeImmune(source, this, t))) { typeMultiplier.value = 0; } @@ -2422,23 +2357,23 @@ 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), ); } } const immuneTags = this.findTags(tag => tag instanceof TypeImmuneTag && tag.immuneType === moveType); for (const tag of immuneTags) { - if (move && !move.getAttrs(HitsTagAttr).some(attr => attr.tagType === tag.tagType)) { + if (move && !move.getAttrs("HitsTagAttr").some(attr => attr.tagType === tag.tagType)) { typeMultiplier.value = 0; break; } @@ -2446,7 +2381,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)) { @@ -2494,12 +2429,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const multiplier = new NumberHolder(getTypeDamageMultiplier(moveType, defType)); applyChallenges(ChallengeType.TYPE_EFFECTIVENESS, multiplier); if (move) { - applyMoveAttrs(VariableMoveTypeChartAttr, null, this, move, multiplier, defType); + applyMoveAttrs("VariableMoveTypeChartAttr", null, this, move, multiplier, defType); } 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) { @@ -3124,24 +3059,26 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { // Bosses never get self ko moves or Pain Split if (this.isBoss()) { - movePool = movePool.filter(m => !allMoves[m[0]].hasAttr(SacrificialAttr) && !allMoves[m[0]].hasAttr(HpSplitAttr)); + movePool = movePool.filter( + m => !allMoves[m[0]].hasAttr("SacrificialAttr") && !allMoves[m[0]].hasAttr("HpSplitAttr"), + ); } // No one gets Memento or Final Gambit - movePool = movePool.filter(m => !allMoves[m[0]].hasAttr(SacrificialAttrOnHit)); + movePool = movePool.filter(m => !allMoves[m[0]].hasAttr("SacrificialAttrOnHit")); if (this.hasTrainer()) { // Trainers never get OHKO moves - movePool = movePool.filter(m => !allMoves[m[0]].hasAttr(OneHitKOAttr)); + movePool = movePool.filter(m => !allMoves[m[0]].hasAttr("OneHitKOAttr")); // Half the weight of self KO moves - movePool = movePool.map(m => [m[0], m[1] * (allMoves[m[0]].hasAttr(SacrificialAttr) ? 0.5 : 1)]); + movePool = movePool.map(m => [m[0], m[1] * (allMoves[m[0]].hasAttr("SacrificialAttr") ? 0.5 : 1)]); // Trainers get a weight bump to stat buffing moves movePool = movePool.map(m => [ m[0], - m[1] * (allMoves[m[0]].getAttrs(StatStageChangeAttr).some(a => a.stages > 1 && a.selfTarget) ? 1.25 : 1), + m[1] * (allMoves[m[0]].getAttrs("StatStageChangeAttr").some(a => a.stages > 1 && a.selfTarget) ? 1.25 : 1), ]); // Trainers get a weight decrease to multiturn moves movePool = movePool.map(m => [ m[0], - m[1] * (!!allMoves[m[0]].isChargingMove() || !!allMoves[m[0]].hasAttr(RechargeAttr) ? 0.7 : 1), + m[1] * (!!allMoves[m[0]].isChargingMove() || !!allMoves[m[0]].hasAttr("RechargeAttr") ? 0.7 : 1), ]); } @@ -3194,7 +3131,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) { @@ -3208,7 +3145,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { !this.moveset.some( mo => m[0] === mo.moveId || - (allMoves[m[0]].hasAttr(SacrificialAttr) && mo.getMove().hasAttr(SacrificialAttr)), // Only one self-KO move allowed + (allMoves[m[0]].hasAttr("SacrificialAttr") && mo.getMove().hasAttr("SacrificialAttr")), // Only one self-KO move allowed ), ) .map(m => { @@ -3237,7 +3174,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { !this.moveset.some( mo => m[0] === mo.moveId || - (allMoves[m[0]].hasAttr(SacrificialAttr) && mo.getMove().hasAttr(SacrificialAttr)), // Only one self-KO move allowed + (allMoves[m[0]].hasAttr("SacrificialAttr") && mo.getMove().hasAttr("SacrificialAttr")), // Only one self-KO move allowed ), ); } @@ -3247,7 +3184,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 @@ -3442,10 +3379,10 @@ 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); + applyMoveAttrs("IgnoreOpponentStatStagesAttr", this, opponent, move, ignoreStatStage); } } @@ -3470,7 +3407,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @returns The calculated accuracy multiplier. */ getAccuracyMultiplier(target: Pokemon, sourceMove: Move): number { - const isOhko = sourceMove.hasAttr(OneHitKOAccuracyAttr); + const isOhko = sourceMove.hasAttr("OneHitKOAccuracyAttr"); if (isOhko) { return 1; } @@ -3481,9 +3418,9 @@ 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); - applyMoveAttrs(IgnoreOpponentStatStagesAttr, this, target, sourceMove, 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); @@ -3502,16 +3439,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; @@ -3566,7 +3520,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { simulated, ), ); - applyMoveAttrs(VariableAtkAttr, source, this, move, sourceAtk); + applyMoveAttrs("VariableAtkAttr", source, this, move, sourceAtk); /** * This Pokemon's defensive stat for the given move's category. @@ -3584,7 +3538,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { simulated, ), ); - applyMoveAttrs(VariableDefAttr, source, this, move, targetDef); + applyMoveAttrs("VariableDefAttr", source, this, move, targetDef); /** * The attack's base damage, as determined by the source's level, move power @@ -3611,7 +3565,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { */ calculateStabMultiplier(source: Pokemon, move: Move, ignoreSourceAbility: boolean, simulated: boolean): number { // If the move has the Typeless attribute, it doesn't get STAB (e.g. struggle) - if (move.hasAttr(TypelessAttr)) { + if (move.hasAttr("TypelessAttr")) { return 1; } const sourceTypes = source.getTypes(); @@ -3623,10 +3577,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { stabMultiplier.value += 0.5; } - applyMoveAttrs(CombinedPledgeStabBoostAttr, source, this, move, stabMultiplier); + 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) { @@ -3672,7 +3626,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const defendingSide = this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; const variableCategory = new NumberHolder(move.category); - applyMoveAttrs(VariableMoveCategoryAttr, source, this, move, variableCategory); + applyMoveAttrs("VariableMoveCategoryAttr", source, this, move, variableCategory); const moveCategory = variableCategory.value as MoveCategory; /** The move's type after type-changing effects are applied */ @@ -3697,7 +3651,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const arenaAttackTypeMultiplier = new NumberHolder( globalScene.arena.getAttackTypeMultiplier(moveType, source.isGrounded()), ); - applyMoveAttrs(IgnoreWeatherTypeDebuffAttr, source, this, move, arenaAttackTypeMultiplier); + applyMoveAttrs("IgnoreWeatherTypeDebuffAttr", source, this, move, arenaAttackTypeMultiplier); const isTypeImmune = typeMultiplier * arenaAttackTypeMultiplier.value === 0; @@ -3711,7 +3665,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { // If the attack deals fixed damage, return a result with that much damage const fixedDamage = new NumberHolder(0); - applyMoveAttrs(FixedDamageAttr, source, this, move, fixedDamage); + applyMoveAttrs("FixedDamageAttr", source, this, move, fixedDamage); if (fixedDamage.value) { const multiLensMultiplier = new NumberHolder(1); globalScene.applyModifiers( @@ -3733,7 +3687,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { // If the attack is a one-hit KO move, return a result with damage equal to this Pokemon's HP const isOneHitKo = new BooleanHolder(false); - applyMoveAttrs(OneHitKOAttr, source, this, move, isOneHitKo); + applyMoveAttrs("OneHitKOAttr", source, this, move, isOneHitKo); if (isOneHitKo.value) { return { cancelled: false, @@ -3775,7 +3729,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { ); if (!ignoreSourceAbility) { applyPreAttackAbAttrs( - AddSecondStrikeAbAttr, + "AddSecondStrikeAbAttr", source, this, move, @@ -3793,7 +3747,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] @@ -3810,11 +3764,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { isPhysical && source.status && source.status.effect === StatusEffect.BURN && - !move.hasAttr(BypassBurnDamageReductionAttr) + !move.hasAttr("BypassBurnDamageReductionAttr") ) { const burnDamageReductionCancelled = new BooleanHolder(false); if (!ignoreSourceAbility) { - applyAbAttrs(BypassBurnDamageReductionAbAttr, source, burnDamageReductionCancelled, simulated); + applyAbAttrs("BypassBurnDamageReductionAbAttr", source, burnDamageReductionCancelled, simulated); } if (!burnDamageReductionCancelled.value) { burnMultiplier = 0.5; @@ -3844,7 +3798,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { */ const hitsTagMultiplier = new NumberHolder(1); move - .getAttrs(HitsTagAttr) + .getAttrs("HitsTagAttr") .filter(hta => hta.doubleDamage) .forEach(hta => { if (this.getTag(hta.tagType)) { @@ -3878,7 +3832,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 */ @@ -3891,20 +3845,20 @@ 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); } } // This attribute may modify damage arbitrarily, so be careful about changing its order of application. - applyMoveAttrs(ModifiedDamageAttr, source, this, move, damage); + 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) @@ -3937,7 +3891,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { 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)) { + if (noCritTag || Overrides.NEVER_CRIT_OVERRIDE || move.hasAttr("FixedDamageAttr")) { return false; } const isCritical = new BooleanHolder(false); @@ -3945,14 +3899,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (source.getTag(BattlerTagType.ALWAYS_CRIT)) { isCritical.value = true; } - applyMoveAttrs(CritOnlyAttr, source, this, move, isCritical); - applyAbAttrs(ConditionalCritAbAttr, source, null, simulated, isCritical, this, move); + 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); } - applyAbAttrs(BlockCritAbAttr, this, null, simulated, isCritical); + applyAbAttrs("BlockCritAbAttr", this, null, simulated, isCritical); return isCritical.value; } @@ -4059,7 +4013,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; } @@ -4107,11 +4061,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; @@ -4127,13 +4081,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; } @@ -4149,7 +4103,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; @@ -4363,10 +4320,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( @@ -4422,14 +4410,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(); @@ -4497,7 +4487,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); @@ -4653,7 +4644,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; } @@ -4702,14 +4693,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, @@ -4866,7 +4857,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; } @@ -4890,7 +4881,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 ) { @@ -5415,7 +5406,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); @@ -5475,7 +5466,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; @@ -6021,7 +6012,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; @@ -6201,33 +6192,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); @@ -6235,6 +6232,7 @@ export class EnemyPokemon extends Pokemon { return { move: encoreMove.moveId, targets: this.getNextTargets(encoreMove.moveId), + useMode: MoveUseMode.NORMAL, }; } } @@ -6242,7 +6240,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: { @@ -6266,7 +6264,7 @@ export class EnemyPokemon extends Pokemon { .targets.map(ind => fieldPokemon[ind]) .filter(p => this.isPlayer() !== p.isPlayer()); // Only considers critical hits for crit-only moves or when this Pokemon is under the effect of Laser Focus - const isCritical = move.hasAttr(CritOnlyAttr) || !!this.getTag(BattlerTagType.ALWAYS_CRIT); + const isCritical = move.hasAttr("CritOnlyAttr") || !!this.getTag(BattlerTagType.ALWAYS_CRIT); return ( move.category !== MoveCategory.STATUS && @@ -6335,7 +6333,7 @@ export class EnemyPokemon extends Pokemon { ![MoveId.SUCKER_PUNCH, MoveId.UPPER_HAND, MoveId.THUNDERCLAP].includes(move.id) ) { targetScore = -20; - } else if (move instanceof AttackMove) { + } else if (move.is("AttackMove")) { /** * Attack moves are given extra multipliers to their base benefit score based on * the move's type effectiveness against the target and whether the move is a STAB move. @@ -6411,14 +6409,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, }; } @@ -6456,7 +6460,7 @@ export class EnemyPokemon extends Pokemon { if (!sortedBenefitScores.length) { // Set target to BattlerIndex.ATTACKER when using a counter move // This is the same as when the player does so - if (move.hasAttr(CounterDamageAttr)) { + if (move.hasAttr("CounterDamageAttr")) { return [BattlerIndex.ATTACKER]; } @@ -6764,10 +6768,9 @@ interface IllusionData { export interface TurnMove { move: MoveId; targets: BattlerIndex[]; + useMode: MoveUseMode; result?: MoveResult; - virtual?: boolean; turn?: number; - ignorePP?: boolean; } export interface AttackMoveResult { @@ -6786,6 +6789,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; @@ -6905,7 +6914,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; @@ -6927,8 +6935,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; /** @@ -6939,36 +6948,6 @@ export class PokemonTurnData { public berriesEaten: BerryType[] = []; } -export enum AiType { - RANDOM, - SMART_RANDOM, - SMART, -} - -export enum MoveResult { - PENDING, - SUCCESS, - FAIL, - MISS, - OTHER, -} - -export enum HitResult { - EFFECTIVE = 1, - SUPER_EFFECTIVE, - NOT_VERY_EFFECTIVE, - ONE_HIT_KO, - NO_EFFECT, - STATUS, - HEAL, - FAIL, - MISS, - INDIRECT, - IMMUNE, - CONFUSION, - INDIRECT_KO, -} - export type DamageResult = | HitResult.EFFECTIVE | HitResult.SUPER_EFFECTIVE @@ -6987,91 +6966,3 @@ export interface DamageCalculationResult { /** The damage dealt by the move */ damage: number; } - -/** - * Wrapper class for the {@linkcode Move} class for Pokemon to interact with. - * These are the moves assigned to a {@linkcode Pokemon} object. - * It links to {@linkcode Move} class via the move ID. - * Compared to {@linkcode Move}, this class also tracks things like - * PP Ups recieved, PP used, etc. - * @see {@linkcode isUsable} - checks if move is restricted, out of PP, or not implemented. - * @see {@linkcode getMove} - returns {@linkcode Move} object by looking it up via ID. - * @see {@linkcode usePp} - removes a point of PP from the move. - * @see {@linkcode getMovePp} - returns amount of PP a move currently has. - * @see {@linkcode getPpRatio} - returns the current PP amount / max PP amount. - * @see {@linkcode getName} - returns name of {@linkcode Move}. - **/ -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). - * This also nullifies all effects of `ppUp`. - */ - public maxPpOverride?: number; - - constructor(moveId: MoveId, ppUsed = 0, ppUp = 0, virtual = false, maxPpOverride?: number) { - this.moveId = moveId; - this.ppUsed = ppUsed; - this.ppUp = ppUp; - this.virtual = virtual; - this.maxPpOverride = maxPpOverride; - } - - /** - * Checks whether the move can be selected or performed by a Pokemon, without consideration for the move's targets. - * The move is unusable if it is out of PP, restricted by an effect, or unimplemented. - * - * @param pokemon - {@linkcode Pokemon} that would be using this move - * @param ignorePp - If `true`, skips the PP check - * @param ignoreRestrictionTags - If `true`, skips the check for move restriction tags (see {@link MoveRestrictionBattlerTag}) - * @returns `true` if the move can be selected and used by the Pokemon, otherwise `false`. - */ - isUsable(pokemon: Pokemon, ignorePp = false, ignoreRestrictionTags = false): boolean { - if (this.moveId && !ignoreRestrictionTags && pokemon.isMoveRestricted(this.moveId, pokemon)) { - return false; - } - - if (this.getMove().name.endsWith(" (N)")) { - return false; - } - - return ignorePp || this.ppUsed < this.getMovePp() || this.getMove().pp === -1; - } - - getMove(): Move { - return allMoves[this.moveId]; - } - - /** - * Sets {@link ppUsed} for this move and ensures the value does not exceed {@link getMovePp} - * @param count Amount of PP to use - */ - usePp(count = 1) { - this.ppUsed = Math.min(this.ppUsed + count, this.getMovePp()); - } - - getMovePp(): number { - return this.maxPpOverride || this.getMove().pp + this.ppUp * toDmgValue(this.getMove().pp / 5); - } - - getPpRatio(): number { - return 1 - this.ppUsed / this.getMovePp(); - } - - getName(): string { - return this.getMove().name; - } - - /** - * Copies an existing move or creates a valid {@linkcode PokemonMove} object from json representing one - * @param source The data for the move to copy; can be a {@linkcode PokemonMove} or JSON object representing one - * @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); - } -} diff --git a/src/field/trainer.ts b/src/field/trainer.ts index 244a23185da..8d950b08507 100644 --- a/src/field/trainer.ts +++ b/src/field/trainer.ts @@ -13,19 +13,15 @@ import { TeraAIMode } from "#enums/tera-ai-mode"; import type { EnemyPokemon } from "#app/field/pokemon"; import { randSeedWeightedItem, randSeedItem, randSeedInt } from "#app/utils/common"; import type { PersistentModifier } from "#app/modifier/modifier"; -import { ArenaTagSide, ArenaTrapTag } from "#app/data/arena-tag"; +import { ArenaTrapTag } from "#app/data/arena-tag"; +import { ArenaTagSide } from "#enums/arena-tag-side"; import { getIsInitialized, initI18n } from "#app/plugins/i18n"; import i18next from "i18next"; import { PartyMemberStrength } from "#enums/party-member-strength"; import { SpeciesId } from "#enums/species-id"; import { TrainerType } from "#enums/trainer-type"; import { signatureSpecies } from "#app/data/balance/signature-species"; - -export enum TrainerVariant { - DEFAULT, - FEMALE, - DOUBLE, -} +import { TrainerVariant } from "#enums/trainer-variant"; export default class Trainer extends Phaser.GameObjects.Container { public config: TrainerConfig; diff --git a/src/game-mode.ts b/src/game-mode.ts index 7ad8a6a83e9..a6fc8195175 100644 --- a/src/game-mode.ts +++ b/src/game-mode.ts @@ -2,7 +2,8 @@ import i18next from "i18next"; import type { FixedBattleConfigs } from "./battle"; import { classicFixedBattles, FixedBattleConfig } from "./battle"; import type { Challenge } from "./data/challenge"; -import { allChallenges, applyChallenges, ChallengeType, copyChallenge } 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 type { Arena } from "./field/arena"; @@ -14,14 +15,7 @@ import { Challenges } from "./enums/challenges"; import { globalScene } from "#app/global-scene"; import { getDailyStartingBiome } from "./data/daily-run"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES, CHALLENGE_MODE_MYSTERY_ENCOUNTER_WAVES } from "./constants"; - -export enum GameModes { - CLASSIC, - ENDLESS, - SPLICED_ENDLESS, - DAILY, - CHALLENGE, -} +import { GameModes } from "#enums/game-modes"; interface GameModeConfig { isClassic?: boolean; 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 ccbc202407b..cf373e6441a 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -2,19 +2,16 @@ 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 { AttackMove } from "#app/data/moves/move"; -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 { - FormChangeItem, - pokemonFormChanges, - SpeciesFormChangeCondition, - SpeciesFormChangeItemTrigger, -} from "#app/data/pokemon-forms"; +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"; import { getStatusEffectDescriptor } from "#app/data/status-effect"; import { PokemonType } from "#enums/pokemon-type"; -import type { EnemyPokemon, PlayerPokemon, PokemonMove } from "#app/field/pokemon"; +import type { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon"; +import type { PokemonMove } from "#app/data/moves/pokemon-move"; import type Pokemon from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; import { @@ -100,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"; @@ -116,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"; @@ -130,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 { @@ -153,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, @@ -221,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; } @@ -302,7 +305,7 @@ export interface GeneratedPersistentModifierType { getPregenArgs(): any[]; } -class AddPokeballModifierType extends ModifierType { +export class AddPokeballModifierType extends ModifierType { private pokeballType: PokeballType; private count: number; @@ -332,7 +335,7 @@ class AddPokeballModifierType extends ModifierType { } } -class AddVoucherModifierType extends ModifierType { +export class AddVoucherModifierType extends ModifierType { private voucherType: VoucherType; private count: number; @@ -1339,7 +1342,7 @@ class AttackTypeBoosterModifierTypeGenerator extends ModifierTypeGenerator { p .getMoveset() .map(m => m.getMove()) - .filter(m => m instanceof AttackMove) + .filter(m => m.is("AttackMove")) .map(m => m.type), ); if (!attackMoveTypes.length) { @@ -1634,7 +1637,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) { @@ -1797,52 +1800,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; @@ -1853,7 +1811,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); } @@ -1873,39 +1831,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; } ); @@ -1913,9 +1871,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), @@ -2424,747 +2382,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 = {}; @@ -3182,28 +2408,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. @@ -3317,7 +2521,7 @@ export interface CustomModifierSettings { } export function getModifierTypeFuncById(id: string): ModifierTypeFunc { - return modifierTypes[id]; + return modifierTypeInitObj[id]; } /** @@ -3370,12 +2574,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 = @@ -3454,7 +2658,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) { @@ -3475,29 +2679,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(); } @@ -3552,7 +2756,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; } @@ -3653,7 +2857,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--; @@ -3664,7 +2868,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 { @@ -3714,6 +2918,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; @@ -3782,3 +2987,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 b6f96db751a..e11f2c07ce8 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -1,16 +1,18 @@ 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 { type FormChangeItem, SpeciesFormChangeItemTrigger } from "#app/data/pokemon-forms"; +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 "#app/phases/learn-move-phase"; +import { LearnMoveType } from "#enums/learn-move-type"; import type { VoucherType } from "#app/system/voucher"; -import { Command } from "#app/ui/command-ui-handler"; +import { Command } from "#enums/command"; import { addTextObject, TextStyle } from "#app/ui/text"; import { BooleanHolder, hslToHex, isNullOrUndefined, NumberHolder, randSeedFloat, toDmgValue } from "#app/utils/common"; import { BattlerTagType } from "#enums/battler-tag-type"; @@ -23,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; @@ -163,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; } @@ -187,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; @@ -1592,7 +1609,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( @@ -1876,7 +1893,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) @@ -1964,7 +1981,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; } @@ -3214,7 +3231,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?.() ?? []; } /** @@ -3786,7 +3803,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); } @@ -3828,7 +3845,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); } @@ -3846,3 +3863,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 86e1708248d..5858be77dac 100644 --- a/src/overrides.ts +++ b/src/overrides.ts @@ -1,10 +1,10 @@ import { type PokeballCounts } from "#app/battle-scene"; import { EvolutionItem } from "#app/data/balance/pokemon-evolutions"; import { Gender } from "#app/data/gender"; -import { FormChangeItem } from "#app/data/pokemon-forms"; +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"; @@ -272,7 +272,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 +285,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-capture-phase.ts b/src/phases/attempt-capture-phase.ts index 4f3f54a7e5b..f4e6725935a 100644 --- a/src/phases/attempt-capture-phase.ts +++ b/src/phases/attempt-capture-phase.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { PLAYER_PARTY_MAX_SIZE } from "#app/constants"; import { SubstituteTag } from "#app/data/battler-tags"; import { 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 6e40e299e7c..c126f3306b9 100644 --- a/src/phases/berry-phase.ts +++ b/src/phases/berry-phase.ts @@ -1,10 +1,5 @@ -import { - applyAbAttrs, - PreventBerryUseAbAttr, - HealFromBerryUseAbAttr, - RepeatBerryNextTurnAbAttr, -} from "#app/data/abilities/ability"; -import { CommonAnim } from "#app/data/battle-anims"; +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"; import { BerryModifier } from "#app/modifier/modifier"; @@ -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/check-status-effect-phase.ts b/src/phases/check-status-effect-phase.ts index e4793fae076..43495e038e9 100644 --- a/src/phases/check-status-effect-phase.ts +++ b/src/phases/check-status-effect-phase.ts @@ -1,5 +1,5 @@ import { Phase } from "#app/phase"; -import type { BattlerIndex } from "#app/battle"; +import type { BattlerIndex } from "#enums/battler-index"; import { globalScene } from "#app/global-scene"; export class CheckStatusEffectPhase extends Phase { diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index afd9cb3bf93..754d54de70a 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -4,7 +4,7 @@ import { BattleType } from "#enums/battle-type"; import type { EncoreTag } from "#app/data/battler-tags"; import { TrappedTag } from "#app/data/battler-tags"; import type { MoveTargetSet } from "#app/data/moves/move"; -import { getMoveTargets } from "#app/data/moves/move"; +import { getMoveTargets } from "#app/data/moves/move-utils"; import { speciesStarterCosts } from "#app/data/balance/starters"; import { AbilityId } from "#enums/ability-id"; import { BattlerTagType } from "#app/enums/battler-tag-type"; @@ -12,16 +12,17 @@ import { BiomeId } from "#enums/biome-id"; import { MoveId } from "#enums/move-id"; import { PokeballType } from "#enums/pokeball"; import type { PlayerPokemon, TurnMove } from "#app/field/pokemon"; -import { FieldPosition } from "#app/field/pokemon"; +import { FieldPosition } from "#enums/field-position"; import { getPokemonNameWithAffix } from "#app/messages"; -import { Command } from "#app/ui/command-ui-handler"; +import { Command } from "#enums/command"; import { UiMode } from "#enums/ui-mode"; import i18next from "i18next"; import { FieldPhase } from "./field-phase"; import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; import { isNullOrUndefined } from "#app/utils/common"; -import { ArenaTagSide } from "#app/data/arena-tag"; +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/common-anim-phase.ts b/src/phases/common-anim-phase.ts index 4a27db3a651..abfe8ed99f0 100644 --- a/src/phases/common-anim-phase.ts +++ b/src/phases/common-anim-phase.ts @@ -1,6 +1,6 @@ -import type { BattlerIndex } from "#app/battle"; +import type { BattlerIndex } from "#enums/battler-index"; import { globalScene } from "#app/global-scene"; -import type { CommonAnim } from "#app/data/battle-anims"; +import type { CommonAnim } from "#enums/move-anims-common"; import { CommonBattleAnim } from "#app/data/battle-anims"; import { PokemonPhase } from "./pokemon-phase"; diff --git a/src/phases/damage-anim-phase.ts b/src/phases/damage-anim-phase.ts index 85cb26e0a09..aa5a0a6c3e6 100644 --- a/src/phases/damage-anim-phase.ts +++ b/src/phases/damage-anim-phase.ts @@ -1,7 +1,8 @@ import { globalScene } from "#app/global-scene"; -import type { BattlerIndex } from "#app/battle"; +import type { BattlerIndex } from "#enums/battler-index"; import { BattleSpec } from "#enums/battle-spec"; -import { type DamageResult, HitResult } from "#app/field/pokemon"; +import type { DamageResult } from "#app/field/pokemon"; +import { HitResult } from "#enums/hit-result"; import { fixedInt } from "#app/utils/common"; import { PokemonPhase } from "#app/phases/pokemon-phase"; diff --git a/src/phases/encounter-phase.ts b/src/phases/encounter-phase.ts index e3b33122ac2..f2c23384627 100644 --- a/src/phases/encounter-phase.ts +++ b/src/phases/encounter-phase.ts @@ -1,13 +1,8 @@ -import { BattlerIndex } from "#app/battle"; +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"; @@ -17,10 +12,11 @@ import { TrainerSlot } from "#enums/trainer-slot"; import { getRandomWeatherType } from "#app/data/weather"; import { EncounterPhaseEvent } from "#app/events/battle-scene"; import type Pokemon from "#app/field/pokemon"; -import { FieldPosition } 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/enemy-command-phase.ts b/src/phases/enemy-command-phase.ts index a81fc4d2107..0dc41a592e0 100644 --- a/src/phases/enemy-command-phase.ts +++ b/src/phases/enemy-command-phase.ts @@ -1,6 +1,6 @@ import { globalScene } from "#app/global-scene"; -import { BattlerIndex } from "#app/battle"; -import { Command } from "#app/ui/command-ui-handler"; +import { BattlerIndex } from "#enums/battler-index"; +import { Command } from "#enums/command"; import { FieldPhase } from "./field-phase"; import { AbilityId } from "#enums/ability-id"; import { BattlerTagType } from "#enums/battler-tag-type"; diff --git a/src/phases/evolution-phase.ts b/src/phases/evolution-phase.ts index eaedb6d32b0..bcc93b028bd 100644 --- a/src/phases/evolution-phase.ts +++ b/src/phases/evolution-phase.ts @@ -10,7 +10,7 @@ import { UiMode } from "#enums/ui-mode"; import { cos, sin } from "#app/field/anims"; import type { PlayerPokemon } from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon"; -import { LearnMoveSituation } from "#app/field/pokemon"; +import { LearnMoveSituation } from "#enums/learn-move-situation"; import { getTypeRgb } from "#app/data/type"; import i18next from "i18next"; import { getPokemonNameWithAffix } from "#app/messages"; diff --git a/src/phases/faint-phase.ts b/src/phases/faint-phase.ts index 38376af4356..675a198d096 100644 --- a/src/phases/faint-phase.ts +++ b/src/phases/faint-phase.ts @@ -1,24 +1,21 @@ -import type { BattlerIndex } from "#app/battle"; +import type { BattlerIndex } from "#enums/battler-index"; import { BattleType } from "#enums/battle-type"; import { globalScene } from "#app/global-scene"; import { applyPostFaintAbAttrs, applyPostKnockOutAbAttrs, applyPostVictoryAbAttrs, - PostFaintAbAttr, - PostKnockOutAbAttr, - PostVictoryAbAttr, -} from "#app/data/abilities/ability"; -import { BattlerTagLapseType } from "#app/data/battler-tags"; +} from "#app/data/abilities/apply-ab-attrs"; +import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type"; import { battleSpecDialogue } from "#app/data/dialogue"; -import { PostVictoryStatStageChangeAttr } from "#app/data/moves/move"; import { allMoves } from "#app/data/data-lists"; -import { SpeciesFormChangeActiveTrigger } from "#app/data/pokemon-forms"; +import { SpeciesFormChangeActiveTrigger } from "#app/data/pokemon-forms/form-change-triggers"; import { BattleSpec } from "#app/enums/battle-spec"; import { StatusEffect } from "#app/enums/status-effect"; import type { EnemyPokemon } from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon"; -import { HitResult, PokemonMove } from "#app/field/pokemon"; +import { PokemonMove } from "#app/data/moves/pokemon-move"; +import { HitResult } from "#enums/hit-result"; import type { PlayerPokemon } from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; import { PokemonInstantReviveModifier } from "#app/modifier/modifier"; @@ -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,20 +128,20 @@ 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); + const pvattrs = pvmove.getAttrs("PostVictoryStatStageChangeAttr"); if (pvattrs.length) { for (const pvattr of pvattrs) { pvattr.applyPostVictory(defeatSource, defeatSource, pvmove); diff --git a/src/phases/form-change-phase.ts b/src/phases/form-change-phase.ts index 3813359d432..13cd410ef87 100644 --- a/src/phases/form-change-phase.ts +++ b/src/phases/form-change-phase.ts @@ -2,7 +2,7 @@ import { globalScene } from "#app/global-scene"; import { fixedInt } from "#app/utils/common"; import { achvs } from "../system/achv"; import type { SpeciesFormChange } from "../data/pokemon-forms"; -import { getSpeciesFormChangeMessage } from "../data/pokemon-forms"; +import { getSpeciesFormChangeMessage } from "#app/data/pokemon-forms/form-change-triggers"; import type { PlayerPokemon } from "../field/pokemon"; import { UiMode } from "#enums/ui-mode"; import type PartyUiHandler from "../ui/party-ui-handler"; diff --git a/src/phases/game-over-phase.ts b/src/phases/game-over-phase.ts index 5fabc5cee81..2cd8bf120b5 100644 --- a/src/phases/game-over-phase.ts +++ b/src/phases/game-over-phase.ts @@ -7,11 +7,11 @@ import type PokemonSpecies from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/data/pokemon-species"; 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/learn-move-phase.ts b/src/phases/learn-move-phase.ts index 7464cebe7da..e24efa63b5a 100644 --- a/src/phases/learn-move-phase.ts +++ b/src/phases/learn-move-phase.ts @@ -2,7 +2,7 @@ import { globalScene } from "#app/global-scene"; import { initMoveAnim, loadMoveAnimAssets } from "#app/data/battle-anims"; import type Move from "#app/data/moves/move"; import { allMoves } from "#app/data/data-lists"; -import { SpeciesFormChangeMoveLearnedTrigger } from "#app/data/pokemon-forms"; +import { SpeciesFormChangeMoveLearnedTrigger } from "#app/data/pokemon-forms/form-change-triggers"; import { MoveId } from "#enums/move-id"; import { getPokemonNameWithAffix } from "#app/messages"; import Overrides from "#app/overrides"; @@ -12,15 +12,7 @@ import { UiMode } from "#enums/ui-mode"; import i18next from "i18next"; import { PlayerPartyMemberPokemonPhase } from "#app/phases/player-party-member-pokemon-phase"; import type Pokemon from "#app/field/pokemon"; - -export enum LearnMoveType { - /** For learning a move via level-up, evolution, or other non-item-based event */ - LEARN_MOVE, - /** For learning a move via Memory Mushroom */ - MEMORY, - /** For learning a move via TM */ - TM, -} +import { LearnMoveType } from "#enums/learn-move-type"; export class LearnMovePhase extends PlayerPartyMemberPokemonPhase { public readonly phaseName = "LearnMovePhase"; 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 a481f4e37b8..51b8fe96ff6 100644 --- a/src/phases/move-charge-phase.ts +++ b/src/phases/move-charge-phase.ts @@ -1,17 +1,18 @@ import { globalScene } from "#app/global-scene"; -import type { BattlerIndex } from "#app/battle"; +import type { BattlerIndex } from "#enums/battler-index"; import { MoveChargeAnim } from "#app/data/battle-anims"; -import { applyMoveChargeAttrs, MoveEffectAttr, InstantChargeAttr } from "#app/data/moves/move"; -import type { PokemonMove } from "#app/field/pokemon"; +import { applyMoveChargeAttrs } from "#app/data/moves/apply-attrs"; +import type { PokemonMove } from "#app/data/moves/pokemon-move"; import type Pokemon from "#app/field/pokemon"; -import { MoveResult } from "#app/field/pokemon"; +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,13 +49,14 @@ 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, () => { move.showChargeText(user, target); - applyMoveChargeAttrs(MoveEffectAttr, user, target, move); + applyMoveChargeAttrs("MoveEffectAttr", user, target, move); user.addTag(BattlerTagType.CHARGING, 1, move.id, user.id); this.end(); }); @@ -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 84072b393f1..35c803ee1d7 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -1,58 +1,37 @@ -import { BattlerIndex } from "#app/battle"; +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"; -import { ArenaTagSide, ConditionalProtectTag } from "#app/data/arena-tag"; +} 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"; import { - BattlerTagLapseType, DamageProtectedTag, ProtectedTag, SemiInvulnerableTag, SubstituteTag, TypeBoostTag, } from "#app/data/battler-tags"; +import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type"; import type { MoveAttr } from "#app/data/moves/move"; -import { - applyFilteredMoveAttrs, - applyMoveAttrs, - AttackMove, - DelayedAttackAttr, - FlinchAttr, - getMoveTargets, - HitsTagAttr, - MissEffectAttr, - MoveEffectAttr, - MultiHitAttr, - NoEffectAttr, - OneHitKOAttr, - OverrideMoveEffectAttr, - StatChangeBeforeDmgCalcAttr, - ToxicAccuracyAttr, -} from "#app/data/moves/move"; +import { getMoveTargets } from "#app/data/moves/move-utils"; +import { applyFilteredMoveAttrs, applyMoveAttrs } from "#app/data/moves/apply-attrs"; import { MoveEffectTrigger } from "#enums/MoveEffectTrigger"; import { MoveFlags } from "#enums/MoveFlags"; import { MoveTarget } from "#enums/MoveTarget"; import { MoveCategory } from "#enums/MoveCategory"; -import { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms"; +import { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms/form-change-triggers"; import { PokemonType } from "#enums/pokemon-type"; -import { type DamageResult, PokemonMove, type TurnMove } from "#app/field/pokemon"; +import type { DamageResult, TurnMove } from "#app/field/pokemon"; +import { PokemonMove } from "#app/data/moves/pokemon-move"; import type Pokemon from "#app/field/pokemon"; -import { HitResult, MoveResult } from "#app/field/pokemon"; +import { MoveResult } from "#enums/move-result"; +import { HitResult } from "#enums/hit-result"; import { getPokemonNameWithAffix } from "#app/messages"; import { ContactHeldItemTransferChanceModifier, @@ -75,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? */ @@ -96,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 @@ -179,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() @@ -191,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")); @@ -202,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, ), ); } @@ -238,13 +221,13 @@ export class MoveEffectPhase extends PokemonPhase { case HitCheckResult.NO_EFFECT_NO_MESSAGE: case HitCheckResult.PROTECTED: case HitCheckResult.TARGET_NOT_ON_FIELD: - applyMoveAttrs(NoEffectAttr, user, target, this.move); + applyMoveAttrs("NoEffectAttr", user, target, this.move); break; case HitCheckResult.MISS: globalScene.phaseManager.queueMessage( i18next.t("battle:attackMissed", { pokemonNameWithAffix: getPokemonNameWithAffix(target) }), ); - applyMoveAttrs(MissEffectAttr, user, target, this.move); + applyMoveAttrs("MissEffectAttr", user, target, this.move); break; case HitCheckResult.REFLECTED: this.queueReflectedMove(user, target); @@ -274,7 +257,7 @@ export class MoveEffectPhase extends PokemonPhase { globalScene.currentBattle.lastPlayerInvolved = this.fieldIndex; } - const isDelayedAttack = this.move.hasAttr(DelayedAttackAttr); + const isDelayedAttack = this.move.hasAttr("DelayedAttackAttr"); /** If the user was somehow removed from the field and it's not a delayed attack, end this phase */ if (!user.isOnField()) { if (!isDelayedAttack) { @@ -299,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) { @@ -311,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; @@ -327,9 +320,9 @@ export class MoveEffectPhase extends PokemonPhase { if (user.turnData.hitsLeft === -1) { const hitCount = new NumberHolder(1); // Assume single target for multi hit - applyMoveAttrs(MultiHitAttr, user, this.getFirstTarget() ?? null, move, hitCount); + 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 @@ -337,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); @@ -359,7 +347,7 @@ export class MoveEffectPhase extends PokemonPhase { // Play the animation if the move was successful against any of its targets or it has a POST_TARGET effect (like self destruct) if ( this.moveHistoryEntry.result === MoveResult.SUCCESS || - move.getAttrs(MoveEffectAttr).some(attr => attr.trigger === MoveEffectTrigger.POST_TARGET) + move.getAttrs("MoveEffectAttr").some(attr => attr.trigger === MoveEffectTrigger.POST_TARGET) ) { const firstTarget = this.getFirstTarget(); new MoveAnim( @@ -382,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 { @@ -411,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(); } @@ -443,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); } @@ -455,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)) { + 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) { @@ -475,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)? */ @@ -497,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)))) ); } @@ -564,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]; } @@ -592,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]; } } @@ -600,7 +599,7 @@ export class MoveEffectPhase extends PokemonPhase { const bypassAccuracy = bypassAccAndInvuln || target.getTag(BattlerTagType.ALWAYS_GET_HIT) || - (target.getTag(BattlerTagType.TELEKINESIS) && !this.move.hasAttr(OneHitKOAttr)); + (target.getTag(BattlerTagType.TELEKINESIS) && !this.move.hasAttr("OneHitKOAttr")); if (moveAccuracy === -1 || bypassAccuracy) { return [HitCheckResult.HIT, effectiveness]; @@ -638,10 +637,10 @@ 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)) { + if (this.move.hasAttr("ToxicAccuracyAttr") && user.isOfType(PokemonType.POISON)) { return true; } // TODO: Fix lock on / mind reader check. @@ -666,7 +665,7 @@ export class MoveEffectPhase extends PokemonPhase { return false; } const move = this.move; - return move.getAttrs(HitsTagAttr).some(hta => hta.tagType === semiInvulnerableTag.tagType); + return move.getAttrs("HitsTagAttr").some(hta => hta.tagType === semiInvulnerableTag.tagType); } /** @returns The {@linkcode Pokemon} using this phase's invoked move */ @@ -677,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]; } @@ -722,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 */ @@ -746,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, @@ -757,7 +763,7 @@ export class MoveEffectPhase extends PokemonPhase { ): void { applyFilteredMoveAttrs( (attr: MoveAttr) => - attr instanceof MoveEffectAttr && + attr.is("MoveEffectAttr") && attr.trigger === triggerType && (isNullOrUndefined(selfTarget) || attr.selfTarget === selfTarget) && (!attr.firstHitOnly || this.firstHit) && @@ -792,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); @@ -801,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); } } } @@ -820,7 +827,7 @@ export class MoveEffectPhase extends PokemonPhase { * Apply stat changes from {@linkcode move} and gives it to {@linkcode source} * before damage calculation */ - applyMoveAttrs(StatChangeBeforeDmgCalcAttr, user, target, this.move); + applyMoveAttrs("StatChangeBeforeDmgCalcAttr", user, target, this.move); const { result, damage: dmg } = target.getAttackDamage({ source: user, @@ -995,15 +1002,15 @@ 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 instanceof AttackMove) { + if (!user.isPlayer() && this.move.is("AttackMove")) { globalScene.applyShuffledModifiers(EnemyAttackStatusEffectChanceModifier, false, target); } // Apply Grip Claw's chance to steal an item from the target - if (this.move instanceof AttackMove) { + if (this.move.is("AttackMove")) { globalScene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target); } } diff --git a/src/phases/move-end-phase.ts b/src/phases/move-end-phase.ts index 6642b97773b..8c8f2ac5239 100644 --- a/src/phases/move-end-phase.ts +++ b/src/phases/move-end-phase.ts @@ -1,8 +1,8 @@ import { globalScene } from "#app/global-scene"; -import { BattlerTagLapseType } from "#app/data/battler-tags"; +import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type"; import { PokemonPhase } from "./pokemon-phase"; -import type { BattlerIndex } from "#app/battle"; -import { applyPostSummonAbAttrs, PostSummonRemoveEffectAbAttr } from "#app/data/abilities/ability"; +import type { BattlerIndex } from "#enums/battler-index"; +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-header-phase.ts b/src/phases/move-header-phase.ts index 50100e827d6..8c2d184c3f5 100644 --- a/src/phases/move-header-phase.ts +++ b/src/phases/move-header-phase.ts @@ -1,5 +1,5 @@ -import { applyMoveAttrs, MoveHeaderAttr } from "#app/data/moves/move"; -import type { PokemonMove } from "#app/field/pokemon"; +import { applyMoveAttrs } from "#app/data/moves/apply-attrs"; +import type { PokemonMove } from "#app/data/moves/pokemon-move"; import type Pokemon from "#app/field/pokemon"; import { BattlePhase } from "./battle-phase"; @@ -23,7 +23,7 @@ export class MoveHeaderPhase extends BattlePhase { super.start(); if (this.canMove()) { - applyMoveAttrs(MoveHeaderAttr, this.pokemon, null, this.move.getMove()); + applyMoveAttrs("MoveHeaderAttr", this.pokemon, null, this.move.getMove()); } this.end(); } diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index 7fc6a86e3f7..41a1042387b 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -1,68 +1,52 @@ -import { BattlerIndex } from "#app/battle"; +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 "#app/data/battle-anims"; -import { BattlerTagLapseType, CenterOfAttentionTag } from "#app/data/battler-tags"; -import { - AddArenaTrapTagAttr, - applyMoveAttrs, - BypassRedirectAttr, - BypassSleepAttr, - CopyMoveAttr, - DelayedAttackAttr, - frenzyMissFunc, - HealStatusEffectAttr, - PreMoveMessageAttr, - PreUseInterruptAttr, -} from "#app/data/moves/move"; +import { CommonAnim } from "#enums/move-anims-common"; +import { CenterOfAttentionTag } from "#app/data/battler-tags"; +import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type"; +import { applyMoveAttrs } from "#app/data/moves/apply-attrs"; import { allMoves } from "#app/data/data-lists"; import { MoveFlags } from "#enums/MoveFlags"; -import { SpeciesFormChangePreMoveTrigger } from "#app/data/pokemon-forms"; +import { SpeciesFormChangePreMoveTrigger } from "#app/data/pokemon-forms/form-change-triggers"; import { getStatusEffectActivationText, getStatusEffectHealText } from "#app/data/status-effect"; import { PokemonType } from "#enums/pokemon-type"; import { getTerrainBlockMessage, getWeatherBlockMessage } from "#app/data/weather"; import { MoveUsedEvent } from "#app/events/battle-scene"; -import type { PokemonMove } from "#app/field/pokemon"; +import type { PokemonMove } from "#app/data/moves/pokemon-move"; import type Pokemon from "#app/field/pokemon"; -import { MoveResult } from "#app/field/pokemon"; +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 { public readonly phaseName = "MovePhase"; 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; } @@ -84,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; } @@ -136,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; } @@ -144,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(); @@ -204,8 +181,8 @@ export class MovePhase extends BattlePhase { const moveQueue = this.pokemon.getMoveQueue(); if ( - (targets.length === 0 && !this.move.getMove().hasAttr(AddArenaTrapTagAttr)) || - (moveQueue.length && moveQueue[0].move === MoveId.NONE) + (targets.length === 0 && !this.move.getMove().hasAttr("AddArenaTrapTagAttr")) || + (moveQueue.length > 0 && moveQueue[0].move === MoveId.NONE) ) { this.showMoveText(); this.showFailedText(); @@ -218,80 +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 instanceof HealStatusEffectAttr && attr.selfTarget && attr.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); } } @@ -299,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); @@ -325,13 +321,14 @@ export class MovePhase extends BattlePhase { if (fail) { this.showMoveText(); this.showFailedText(); - return this.end(); + this.end(); + return; } } let success = true; // Check if there are any attributes that can interrupt the move, overriding the fail message. - for (const move of this.move.getMove().getAttrs(PreUseInterruptAttr)) { + for (const move of this.move.getMove().getAttrs("PreUseInterruptAttr")) { if (move.apply(this.pokemon, targets[0], this.move.getMove())) { success = false; break; @@ -342,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)); } /** @@ -370,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? @@ -386,7 +381,7 @@ export class MovePhase extends BattlePhase { } // Update the battle's "last move" pointer, unless we're currently mimicking a move. - if (!allMoves[this.move.moveId].hasAttr(CopyMoveAttr)) { + if (!allMoves[this.move.moveId].hasAttr("CopyMoveAttr")) { // The last move used is unaffected by moves that fail if (success) { globalScene.currentBattle.lastMove = this.move.moveId; @@ -401,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); @@ -439,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); }); } } @@ -452,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); @@ -477,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, + ); } /** @@ -488,7 +489,7 @@ export class MovePhase extends BattlePhase { "MoveEndPhase", this.pokemon.getBattlerIndex(), this.getActiveTargetPokemon(), - this.followUp, + isVirtual(this.useMode), ); super.end(); @@ -503,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; } @@ -521,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; @@ -543,24 +546,24 @@ export class MovePhase extends BattlePhase { }); if (currentTarget !== redirectTarget.value) { - const bypassRedirectAttrs = this.move.getMove().getAttrs(BypassRedirectAttr); + const bypassRedirectAttrs = this.move.getMove().getAttrs("BypassRedirectAttr"); bypassRedirectAttrs.forEach(attr => { if (!attr.abilitiesOnly || redirectedByAbility) { redirectTarget.value = currentTarget; } }); - 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, ); } @@ -618,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(); @@ -635,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); @@ -658,13 +662,13 @@ 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(), }), 500, ); - applyMoveAttrs(PreMoveMessageAttr, this.pokemon, this.pokemon.getOpponents(false)[0], this.move.getMove()); + applyMoveAttrs("PreMoveMessageAttr", this.pokemon, this.pokemon.getOpponents(false)[0], this.move.getMove()); } public showFailedText(failedText: string = i18next.t("battle:attackFailed")): void { diff --git a/src/phases/mystery-encounter-phases.ts b/src/phases/mystery-encounter-phases.ts index 1f27db7ff64..9aae796211f 100644 --- a/src/phases/mystery-encounter-phases.ts +++ b/src/phases/mystery-encounter-phases.ts @@ -1,4 +1,4 @@ -import { BattlerTagLapseType } from "#app/data/battler-tags"; +import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type"; import type { OptionPhaseCallback } from "#app/data/mystery-encounters/mystery-encounter-option"; import type MysteryEncounterOption from "#app/data/mystery-encounters/mystery-encounter-option"; import { SeenEncounterData } from "#app/data/mystery-encounters/mystery-encounter-save-data"; 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 2982bc982d9..dc26d070029 100644 --- a/src/phases/obtain-status-effect-phase.ts +++ b/src/phases/obtain-status-effect-phase.ts @@ -1,13 +1,14 @@ import { globalScene } from "#app/global-scene"; -import type { BattlerIndex } from "#app/battle"; -import { CommonBattleAnim, CommonAnim } from "#app/data/battle-anims"; +import type { BattlerIndex } from "#enums/battler-index"; +import { CommonBattleAnim } from "#app/data/battle-anims"; +import { CommonAnim } from "#enums/move-anims-common"; import { getStatusEffectObtainText, getStatusEffectOverlapText } from "#app/data/status-effect"; import { StatusEffect } from "#app/enums/status-effect"; import type Pokemon from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; import { PokemonPhase } from "./pokemon-phase"; -import { SpeciesFormChangeStatusEffectTrigger } from "#app/data/pokemon-forms"; -import { applyPostSetStatusAbAttrs, PostSetStatusAbAttr } from "#app/data/abilities/ability"; +import { SpeciesFormChangeStatusEffectTrigger } from "#app/data/pokemon-forms/form-change-triggers"; +import { applyPostSetStatusAbAttrs } from "#app/data/abilities/apply-ab-attrs"; import { isNullOrUndefined } from "#app/utils/common"; export class ObtainStatusEffectPhase extends PokemonPhase { @@ -52,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-heal-phase.ts b/src/phases/pokemon-heal-phase.ts index 6e9b6b5c622..cf6cf40a923 100644 --- a/src/phases/pokemon-heal-phase.ts +++ b/src/phases/pokemon-heal-phase.ts @@ -1,9 +1,9 @@ import { globalScene } from "#app/global-scene"; -import type { BattlerIndex } from "#app/battle"; -import { CommonAnim } from "#app/data/battle-anims"; +import type { BattlerIndex } from "#enums/battler-index"; +import { CommonAnim } from "#enums/move-anims-common"; import { getStatusEffectHealText } from "#app/data/status-effect"; import { StatusEffect } from "#app/enums/status-effect"; -import { HitResult } from "#app/field/pokemon"; +import { HitResult } from "#enums/hit-result"; import { getPokemonNameWithAffix } from "#app/messages"; import { HealingBoosterModifier } from "#app/modifier/modifier"; import { HealAchv } from "#app/system/achv"; diff --git a/src/phases/pokemon-phase.ts b/src/phases/pokemon-phase.ts index 8c30512cdc4..cfa30f0d06e 100644 --- a/src/phases/pokemon-phase.ts +++ b/src/phases/pokemon-phase.ts @@ -1,9 +1,13 @@ import { globalScene } from "#app/global-scene"; -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; 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 4f18a19b2fb..938915309d9 100644 --- a/src/phases/pokemon-transform-phase.ts +++ b/src/phases/pokemon-transform-phase.ts @@ -1,8 +1,8 @@ -import type { BattlerIndex } from "#app/battle"; +import type { BattlerIndex } from "#enums/battler-index"; import { BattlerTagType } from "#enums/battler-tag-type"; import { MoveId } from "#enums/move-id"; import { EFFECTIVE_STATS, BATTLE_STATS } from "#enums/stat"; -import { PokemonMove } from "#app/field/pokemon"; +import { PokemonMove } from "#app/data/moves/pokemon-move"; import { globalScene } from "#app/global-scene"; import { PokemonPhase } from "./pokemon-phase"; import { getPokemonNameWithAffix } from "#app/messages"; @@ -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 33fb012492d..e0a3bb5c00b 100644 --- a/src/phases/post-turn-status-effect-phase.ts +++ b/src/phases/post-turn-status-effect-phase.ts @@ -1,14 +1,8 @@ import { globalScene } from "#app/global-scene"; -import type { BattlerIndex } from "#app/battle"; -import { - applyAbAttrs, - applyPostDamageAbAttrs, - BlockNonDirectDamageAbAttr, - BlockStatusDamageAbAttr, - PostDamageAbAttr, - ReduceBurnDamageAbAttr, -} from "#app/data/abilities/ability"; -import { CommonBattleAnim, CommonAnim } from "#app/data/battle-anims"; +import type { BattlerIndex } from "#enums/battler-index"; +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"; import { BattleSpec } from "#app/enums/battle-spec"; import { StatusEffect } from "#app/enums/status-effect"; @@ -28,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( @@ -45,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 6b65c2a5140..41b691844bf 100644 --- a/src/phases/quiet-form-change-phase.ts +++ b/src/phases/quiet-form-change-phase.ts @@ -1,7 +1,10 @@ import { globalScene } from "#app/global-scene"; import { SemiInvulnerableTag } from "#app/data/battler-tags"; import type { SpeciesFormChange } from "#app/data/pokemon-forms"; -import { getSpeciesFormChangeMessage, SpeciesFormChangeTeraTrigger } from "#app/data/pokemon-forms"; +import { + getSpeciesFormChangeMessage, + SpeciesFormChangeTeraTrigger, +} from "#app/data/pokemon-forms/form-change-triggers"; import { getTypeRgb } from "#app/data/type"; import { BattleSpec } from "#app/enums/battle-spec"; import { BattlerTagType } from "#app/enums/battler-tag-type"; @@ -9,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"; @@ -31,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); @@ -182,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/return-phase.ts b/src/phases/return-phase.ts index 6365256d40a..a8233f98bd1 100644 --- a/src/phases/return-phase.ts +++ b/src/phases/return-phase.ts @@ -1,5 +1,5 @@ import { globalScene } from "#app/global-scene"; -import { SpeciesFormChangeActiveTrigger } from "#app/data/pokemon-forms"; +import { SpeciesFormChangeActiveTrigger } from "#app/data/pokemon-forms/form-change-triggers"; import { SwitchType } from "#enums/switch-type"; import { SwitchSummonPhase } from "./switch-summon-phase"; 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/scan-ivs-phase.ts b/src/phases/scan-ivs-phase.ts index df68a2d1cab..d296d87ca88 100644 --- a/src/phases/scan-ivs-phase.ts +++ b/src/phases/scan-ivs-phase.ts @@ -1,5 +1,5 @@ import { globalScene } from "#app/global-scene"; -import type { BattlerIndex } from "#app/battle"; +import type { BattlerIndex } from "#enums/battler-index"; import { PERMANENT_STATS, Stat } from "#app/enums/stat"; import { getPokemonNameWithAffix } from "#app/messages"; import { getTextColor, TextStyle } from "#app/ui/text"; 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 88d4fe06a9b..e7e87f5a25f 100644 --- a/src/phases/select-starter-phase.ts +++ b/src/phases/select-starter-phase.ts @@ -1,7 +1,8 @@ import { globalScene } from "#app/global-scene"; -import { applyChallenges, ChallengeType } from "#app/data/challenge"; +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"; +import { SpeciesFormChangeMoveLearnedTrigger } from "#app/data/pokemon-forms/form-change-triggers"; import { getPokemonSpecies } from "#app/data/pokemon-species"; import { overrideHeldItems, overrideModifiers } from "#app/modifier/modifier"; import Overrides from "#app/overrides"; diff --git a/src/phases/select-target-phase.ts b/src/phases/select-target-phase.ts index fcbd3aeb679..6d47ac18021 100644 --- a/src/phases/select-target-phase.ts +++ b/src/phases/select-target-phase.ts @@ -1,6 +1,6 @@ import { globalScene } from "#app/global-scene"; -import type { BattlerIndex } from "#app/battle"; -import { Command } from "#app/ui/command-ui-handler"; +import type { BattlerIndex } from "#enums/battler-index"; +import { Command } from "#enums/command"; import { UiMode } from "#enums/ui-mode"; import { PokemonPhase } from "./pokemon-phase"; import i18next from "#app/plugins/i18n"; diff --git a/src/phases/shiny-sparkle-phase.ts b/src/phases/shiny-sparkle-phase.ts index 93d7dd67209..53866af89e6 100644 --- a/src/phases/shiny-sparkle-phase.ts +++ b/src/phases/shiny-sparkle-phase.ts @@ -1,5 +1,5 @@ import { globalScene } from "#app/global-scene"; -import type { BattlerIndex } from "#app/battle"; +import type { BattlerIndex } from "#enums/battler-index"; import { PokemonPhase } from "./pokemon-phase"; export class ShinySparklePhase extends PokemonPhase { diff --git a/src/phases/show-ability-phase.ts b/src/phases/show-ability-phase.ts index af295b72622..0f568819cde 100644 --- a/src/phases/show-ability-phase.ts +++ b/src/phases/show-ability-phase.ts @@ -1,5 +1,5 @@ import { globalScene } from "#app/global-scene"; -import type { BattlerIndex } from "#app/battle"; +import type { BattlerIndex } from "#enums/battler-index"; import { PokemonPhase } from "./pokemon-phase"; import { getPokemonNameWithAffix } from "#app/messages"; diff --git a/src/phases/stat-stage-change-phase.ts b/src/phases/stat-stage-change-phase.ts index ad2eeae1c48..e73f72f7a63 100644 --- a/src/phases/stat-stage-change-phase.ts +++ b/src/phases/stat-stage-change-phase.ts @@ -1,17 +1,12 @@ import { globalScene } from "#app/global-scene"; -import type { BattlerIndex } from "#app/battle"; +import type { BattlerIndex } from "#enums/battler-index"; import { applyAbAttrs, applyPostStatStageChangeAbAttrs, applyPreStatStageChangeAbAttrs, - ConditionalUserFieldProtectStatAbAttr, - PostStatStageChangeAbAttr, - ProtectStatAbAttr, - ReflectStatStageChangeAbAttr, - StatStageChangeCopyAbAttr, - StatStageChangeMultiplierAbAttr, -} from "#app/data/abilities/ability"; -import { ArenaTagSide, MistTag } from "#app/data/arena-tag"; +} 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"; import type Pokemon from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; @@ -131,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; @@ -151,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, @@ -163,7 +158,7 @@ export class StatStageChangePhase extends PokemonPhase { const ally = pokemon.getAlly(); if (!isNullOrUndefined(ally)) { applyPreStatStageChangeAbAttrs( - ConditionalUserFieldProtectStatAbAttr, + "ConditionalUserFieldProtectStatAbAttr", ally, stat, cancelled, @@ -179,7 +174,7 @@ export class StatStageChangePhase extends PokemonPhase { !this.comingFromMirrorArmorUser ) { applyPreStatStageChangeAbAttrs( - ReflectStatStageChangeAbAttr, + "ReflectStatStageChangeAbAttr", pokemon, stat, cancelled, @@ -227,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 921466dfead..ad93452331f 100644 --- a/src/phases/summon-phase.ts +++ b/src/phases/summon-phase.ts @@ -1,16 +1,16 @@ import { BattleType } from "#enums/battle-type"; import { getPokeballAtlasKey, getPokeballTintColor } from "#app/data/pokeball"; -import { SpeciesFormChangeActiveTrigger } from "#app/data/pokemon-forms"; +import { SpeciesFormChangeActiveTrigger } from "#app/data/pokemon-forms/form-change-triggers"; import { TrainerSlot } from "#enums/trainer-slot"; import { PlayerGender } from "#app/enums/player-gender"; import { addPokeballOpenParticles } from "#app/field/anims"; import type Pokemon from "#app/field/pokemon"; -import { FieldPosition } from "#app/field/pokemon"; +import { FieldPosition } from "#enums/field-position"; 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 103af3db275..af03cc42b54 100644 --- a/src/phases/switch-summon-phase.ts +++ b/src/phases/switch-summon-phase.ts @@ -1,20 +1,13 @@ import { globalScene } from "#app/global-scene"; -import { - applyPreSummonAbAttrs, - applyPreSwitchOutAbAttrs, - PostDamageForceSwitchAbAttr, - PreSummonAbAttr, - PreSwitchOutAbAttr, -} from "#app/data/abilities/ability"; -import { ForceSwitchOutAttr } from "#app/data/moves/move"; +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"; +import { SpeciesFormChangeActiveTrigger } from "#app/data/pokemon-forms/form-change-triggers"; import { TrainerSlot } from "#enums/trainer-slot"; import type Pokemon from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; import { SwitchEffectTransferModifier } from "#app/modifier/modifier"; -import { Command } from "#app/ui/command-ui-handler"; +import { Command } from "#enums/command"; import i18next from "i18next"; import { SummonPhase } from "./summon-phase"; import { SubstituteTag } from "#app/data/battler-tags"; @@ -131,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; @@ -175,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. @@ -226,9 +207,9 @@ export class SwitchSummonPhase extends SummonPhase { const currentCommand = globalScene.currentBattle.turnCommands[this.fieldIndex]?.command; const lastPokemonIsForceSwitchedAndNotFainted = - lastUsedMove?.hasAttr(ForceSwitchOutAttr) && !this.lastPokemon.isFainted(); + 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`. @@ -264,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/tera-phase.ts b/src/phases/tera-phase.ts index 5f403f5419e..a6025e20488 100644 --- a/src/phases/tera-phase.ts +++ b/src/phases/tera-phase.ts @@ -5,8 +5,9 @@ import i18next from "i18next"; import { globalScene } from "#app/global-scene"; import { PokemonType } from "#enums/pokemon-type"; import { achvs } from "#app/system/achv"; -import { SpeciesFormChangeTeraTrigger } from "#app/data/pokemon-forms"; -import { CommonAnim, CommonBattleAnim } from "#app/data/battle-anims"; +import { SpeciesFormChangeTeraTrigger } from "#app/data/pokemon-forms/form-change-triggers"; +import { CommonBattleAnim } from "#app/data/battle-anims"; +import { CommonAnim } from "#enums/move-anims-common"; export class TeraPhase extends BattlePhase { public readonly phaseName = "TeraPhase"; diff --git a/src/phases/title-phase.ts b/src/phases/title-phase.ts index 37ce294f237..5e36081b899 100644 --- a/src/phases/title-phase.ts +++ b/src/phases/title-phase.ts @@ -3,17 +3,15 @@ import { BattleType } from "#enums/battle-type"; import { fetchDailyRunSeed, getDailyRunStarters } from "#app/data/daily-run"; import { Gender } from "#app/data/gender"; import { getBiomeKey } from "#app/field/arena"; -import { GameMode, GameModes, getGameMode } from "#app/game-mode"; +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/toggle-double-position-phase.ts b/src/phases/toggle-double-position-phase.ts index a6b8705f580..596bf87eb5b 100644 --- a/src/phases/toggle-double-position-phase.ts +++ b/src/phases/toggle-double-position-phase.ts @@ -1,5 +1,5 @@ import { globalScene } from "#app/global-scene"; -import { FieldPosition } from "#app/field/pokemon"; +import { FieldPosition } from "#enums/field-position"; import { BattlePhase } from "./battle-phase"; export class ToggleDoublePositionPhase extends BattlePhase { 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 85590d667d6..ab46292c1d2 100644 --- a/src/phases/turn-end-phase.ts +++ b/src/phases/turn-end-phase.ts @@ -1,5 +1,5 @@ -import { applyPostTurnAbAttrs, PostTurnAbAttr } from "#app/data/abilities/ability"; -import { BattlerTagLapseType } from "#app/data/battler-tags"; +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"; import { TurnEndEvent } from "#app/events/battle-scene"; @@ -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-init-phase.ts b/src/phases/turn-init-phase.ts index e9d6f60af5e..8d0508c5ebb 100644 --- a/src/phases/turn-init-phase.ts +++ b/src/phases/turn-init-phase.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { handleMysteryEncounterBattleStartEffects, handleMysteryEncounterTurnStartEffects, diff --git a/src/phases/turn-start-phase.ts b/src/phases/turn-start-phase.ts index 557b67b6091..6219907fb68 100644 --- a/src/phases/turn-start-phase.ts +++ b/src/phases/turn-start-phase.ts @@ -1,15 +1,14 @@ -import { applyAbAttrs, BypassSpeedChanceAbAttr, PreventBypassSpeedChanceAbAttr } from "#app/data/abilities/ability"; -import { MoveHeaderAttr } from "#app/data/moves/move"; +import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs"; import { allMoves } from "#app/data/data-lists"; import { AbilityId } from "#enums/ability-id"; import { Stat } from "#app/enums/stat"; import type Pokemon from "#app/field/pokemon"; -import { PokemonMove } from "#app/field/pokemon"; +import { PokemonMove } from "#app/data/moves/pokemon-move"; import { BypassSpeedChanceModifier } from "#app/modifier/modifier"; -import { Command } from "#app/ui/command-ui-handler"; +import { Command } from "#enums/command"; import { randSeedShuffle, BooleanHolder } from "#app/utils/common"; import { FieldPhase } from "./field-phase"; -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { TrickRoomTag } from "#app/data/arena-tag"; import { SwitchType } from "#enums/switch-type"; import { globalScene } from "#app/global-scene"; @@ -64,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); } @@ -125,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(); @@ -158,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 ca24e474cde..ae5b727c2a6 100644 --- a/src/phases/victory-phase.ts +++ b/src/phases/victory-phase.ts @@ -1,8 +1,8 @@ -import type { BattlerIndex } from "#app/battle"; +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 2918cd462df..d9239220376 100644 --- a/src/phases/weather-effect-phase.ts +++ b/src/phases/weather-effect-phase.ts @@ -1,20 +1,16 @@ import { globalScene } from "#app/global-scene"; import { applyPreWeatherEffectAbAttrs, - SuppressWeatherEffectAbAttr, - PreWeatherDamageAbAttr, applyAbAttrs, - BlockNonDirectDamageAbAttr, applyPostWeatherLapseAbAttrs, - PostWeatherLapseAbAttr, -} from "#app/data/abilities/ability"; -import { CommonAnim } from "#app/data/battle-anims"; +} 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"; import { BattlerTagType } from "#app/enums/battler-tag-type"; import { WeatherType } from "#app/enums/weather-type"; import type Pokemon from "#app/field/pokemon"; -import { HitResult } from "#app/field/pokemon"; +import { HitResult } from "#enums/hit-result"; import { BooleanHolder, toDmgValue } from "#app/utils/common"; import { CommonAnimPhase } from "./common-anim-phase"; @@ -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..8ca9005096f 100644 --- a/src/plugins/i18n.ts +++ b/src/plugins/i18n.ts @@ -174,7 +174,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 2949ecd51cf..00b46b9e5f4 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -13,8 +13,9 @@ 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 { GameModes, getGameMode } from "#app/game-mode"; +import { Unlockables } from "#enums/unlockables"; +import { getGameMode } from "#app/game-mode"; +import { GameModes } from "#enums/game-modes"; import { BattleType } from "#enums/battle-type"; import TrainerData from "#app/system/trainer-data"; import { trainerConfigs } from "#app/data/trainers/trainer-config"; @@ -31,7 +32,7 @@ import { GameStats } from "#app/system/game-stats"; import { Tutorial } from "#app/tutorial"; import { speciesEggMoves } from "#app/data/balance/egg-moves"; import { allMoves } from "#app/data/data-lists"; -import { TrainerVariant } from "#app/field/trainer"; +import { TrainerVariant } from "#enums/trainer-variant"; import type { Variant } from "#app/sprites/variant"; import { setSettingGamepad, SettingGamepad, settingGamepadDefaults } from "#app/system/settings/settings-gamepad"; import type { SettingKeyboard } from "#app/system/settings/settings-keyboard"; @@ -46,7 +47,8 @@ import { GameDataType } from "#enums/game-data-type"; import type { MoveId } from "#enums/move-id"; import { PlayerGender } from "#enums/player-gender"; import { SpeciesId } from "#enums/species-id"; -import { applyChallenges, ChallengeType } from "#app/data/challenge"; +import { applyChallenges } from "#app/data/challenge"; +import { ChallengeType } from "#enums/challenge-type"; import { WeatherType } from "#enums/weather-type"; import { TerrainType } from "#app/data/terrain"; import { RUN_HISTORY_LIMIT } from "#app/ui/run-history-ui-handler"; @@ -62,40 +64,12 @@ import { ArenaTrapTag } from "#app/data/arena-tag"; import { pokemonFormChanges } from "#app/data/pokemon-forms"; import type { PokemonType } from "#enums/pokemon-type"; import type { DexData, DexEntry } from "../@types/dex-data"; +import { DexAttr } from "#enums/dex-attr"; +import { AbilityAttr } from "#enums/ability-attr"; +import { defaultStarterSpecies, saveKey } from "#app/constants"; +import { encrypt, decrypt } from "#app/utils/data"; -export const defaultStarterSpecies: SpeciesId[] = [ - SpeciesId.BULBASAUR, - SpeciesId.CHARMANDER, - SpeciesId.SQUIRTLE, - SpeciesId.CHIKORITA, - SpeciesId.CYNDAQUIL, - SpeciesId.TOTODILE, - SpeciesId.TREECKO, - SpeciesId.TORCHIC, - SpeciesId.MUDKIP, - SpeciesId.TURTWIG, - SpeciesId.CHIMCHAR, - SpeciesId.PIPLUP, - SpeciesId.SNIVY, - SpeciesId.TEPIG, - SpeciesId.OSHAWOTT, - SpeciesId.CHESPIN, - SpeciesId.FENNEKIN, - SpeciesId.FROAKIE, - SpeciesId.ROWLET, - SpeciesId.LITTEN, - SpeciesId.POPPLIO, - SpeciesId.GROOKEY, - SpeciesId.SCORBUNNY, - SpeciesId.SOBBLE, - SpeciesId.SPRIGATITO, - SpeciesId.FUECOCO, - SpeciesId.QUAXLY, -]; - -const saveKey = "x0i2O7WRiANTqPmZ"; // Temporary; secure encryption is not yet necessary - -export function getDataTypeKey(dataType: GameDataType, slotId = 0): string { +function getDataTypeKey(dataType: GameDataType, slotId = 0): string { switch (dataType) { case GameDataType.SYSTEM: return "data"; @@ -117,20 +91,6 @@ export function getDataTypeKey(dataType: GameDataType, slotId = 0): string { } } -export function encrypt(data: string, bypassLogin: boolean): string { - return (bypassLogin - ? (data: string) => btoa(encodeURIComponent(data)) - : (data: string) => AES.encrypt(data, saveKey))(data) as unknown as string; // TODO: is this correct? -} - -export function decrypt(data: string, bypassLogin: boolean): string { - return ( - bypassLogin - ? (data: string) => decodeURIComponent(atob(data)) - : (data: string) => AES.decrypt(data, saveKey).toString(enc.Utf8) - )(data); -} - // TODO: Move all these exported interfaces to @types export interface SystemSaveData { trainerId: number; @@ -214,10 +174,6 @@ export interface StarterAttributes { tera?: PokemonType; } -export interface StarterPreferences { - [key: number]: StarterAttributes; -} - export interface DexAttrProps { shiny: boolean; female: boolean; @@ -234,53 +190,6 @@ export interface RunEntry { isFavorite: boolean; } -export const DexAttr = { - NON_SHINY: 1n, - SHINY: 2n, - MALE: 4n, - FEMALE: 8n, - DEFAULT_VARIANT: 16n, - VARIANT_2: 32n, - VARIANT_3: 64n, - DEFAULT_FORM: 128n, -}; - -export const AbilityAttr = { - ABILITY_1: 1, - ABILITY_2: 2, - ABILITY_HIDDEN: 4, -}; - -// the latest data saved/loaded for the Starter Preferences. Required to reduce read/writes. Initialize as "{}", since this is the default value and no data needs to be stored if present. -// if they ever add private static variables, move this into StarterPrefs -const StarterPrefers_DEFAULT: string = "{}"; -let StarterPrefers_private_latest: string = StarterPrefers_DEFAULT; - -// called on starter selection show once -export function loadStarterPreferences(): StarterPreferences { - return JSON.parse( - (StarterPrefers_private_latest = - localStorage.getItem(`starterPrefs_${loggedInUser?.username}`) || StarterPrefers_DEFAULT), - ); -} - -// called on starter selection clear, always -export function saveStarterPreferences(prefs: StarterPreferences): void { - const pStr: string = JSON.stringify(prefs); - if (pStr !== StarterPrefers_private_latest) { - // something changed, store the update - localStorage.setItem(`starterPrefs_${loggedInUser?.username}`, pStr); - // update the latest prefs - StarterPrefers_private_latest = pStr; - } -} -// This is its own class as StarterPreferences... -// - don't need to be loaded on startup -// - isn't stored with other data -// - don't require to be encrypted -// - shouldn't require calls outside of the starter selection -export class StarterPrefs {} - export interface StarterDataEntry { moveset: StarterMoveset | StarterFormMoveData | null; eggMoves: number; diff --git a/src/system/pokemon-data.ts b/src/system/pokemon-data.ts index 62fb2f4a2e4..7571f0cc82f 100644 --- a/src/system/pokemon-data.ts +++ b/src/system/pokemon-data.ts @@ -5,7 +5,8 @@ import { Nature } from "#enums/nature"; import { PokeballType } from "#enums/pokeball"; import { getPokemonSpecies, getPokemonSpeciesForm } from "../data/pokemon-species"; import { Status } from "../data/status-effect"; -import Pokemon, { EnemyPokemon, PokemonBattleData, PokemonMove, PokemonSummonData } from "../field/pokemon"; +import Pokemon, { EnemyPokemon, PokemonBattleData, PokemonSummonData } from "../field/pokemon"; +import { PokemonMove } from "#app/data/moves/pokemon-move"; import { TrainerSlot } from "#enums/trainer-slot"; import type { Variant } from "#app/sprites/variant"; import type { BiomeId } from "#enums/biome-id"; 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/trainer-data.ts b/src/system/trainer-data.ts index 0e6298309bc..7c9bffd99ee 100644 --- a/src/system/trainer-data.ts +++ b/src/system/trainer-data.ts @@ -1,5 +1,6 @@ import type { TrainerType } from "#enums/trainer-type"; -import Trainer, { TrainerVariant } from "../field/trainer"; +import Trainer from "../field/trainer"; +import { TrainerVariant } from "#enums/trainer-variant"; export default class TrainerData { public trainerType: TrainerType; diff --git a/src/system/unlockables.ts b/src/system/unlockables.ts index 2c396aad1f9..72588858eae 100644 --- a/src/system/unlockables.ts +++ b/src/system/unlockables.ts @@ -1,12 +1,7 @@ import i18next from "i18next"; -import { GameMode, GameModes } from "../game-mode"; - -export enum Unlockables { - ENDLESS_MODE, - MINI_BLACK_HOLE, - SPLICED_ENDLESS_MODE, - EVIOLITE, -} +import { GameMode } from "../game-mode"; +import { GameModes } from "#enums/game-modes"; +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 9e30ccdc2a7..fbbde49bc08 100644 --- a/src/system/version_migration/versions/v1_0_4.ts +++ b/src/system/version_migration/versions/v1_0_4.ts @@ -1,6 +1,8 @@ import { SettingKeys } from "#app/system/settings/settings"; import type { SystemSaveData, SessionSaveData } from "#app/system/game-data"; -import { AbilityAttr, defaultStarterSpecies, DexAttr } 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 { CustomPokemonData } from "#app/data/custom-pokemon-data"; import { isNullOrUndefined } from "#app/utils/common"; 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 dc7c0f48640..e309959317e 100644 --- a/src/system/version_migration/versions/v1_7_0.ts +++ b/src/system/version_migration/versions/v1_7_0.ts @@ -2,7 +2,8 @@ import type { SessionSaveMigrator } from "#app/@types/SessionSaveMigrator"; import type { SystemSaveMigrator } from "#app/@types/SystemSaveMigrator"; import { getPokemonSpecies, getPokemonSpeciesForm } from "#app/data/pokemon-species"; import { globalScene } from "#app/global-scene"; -import { DexAttr, type SessionSaveData, type SystemSaveData } from "#app/system/game-data"; +import type { SessionSaveData, SystemSaveData } from "#app/system/game-data"; +import { DexAttr } from "#enums/dex-attr"; import { isNullOrUndefined } from "#app/utils/common"; /** diff --git a/src/system/version_migration/versions/v1_8_3.ts b/src/system/version_migration/versions/v1_8_3.ts index cce37a53767..bd963290800 100644 --- a/src/system/version_migration/versions/v1_8_3.ts +++ b/src/system/version_migration/versions/v1_8_3.ts @@ -1,6 +1,7 @@ import type { SystemSaveMigrator } from "#app/@types/SystemSaveMigrator"; import { getPokemonSpecies } from "#app/data/pokemon-species"; -import { DexAttr, type SystemSaveData } from "#app/system/game-data"; +import type { SystemSaveData } from "#app/system/game-data"; +import { DexAttr } from "#enums/dex-attr"; import { SpeciesId } from "#enums/species-id"; /** diff --git a/src/system/version_migration/versions/v1_9_0.ts b/src/system/version_migration/versions/v1_9_0.ts index 0f22b85d072..0bd5a422ffb 100644 --- a/src/system/version_migration/versions/v1_9_0.ts +++ b/src/system/version_migration/versions/v1_9_0.ts @@ -1,5 +1,5 @@ import type { SessionSaveMigrator } from "#app/@types/SessionSaveMigrator"; -import { PokemonMove } from "#app/field/pokemon"; +import { PokemonMove } from "#app/data/moves/pokemon-move"; import type { SessionSaveData } from "#app/system/game-data"; import type PokemonData from "#app/system/pokemon-data"; import { MoveId } from "#enums/move-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/arena-flyout.ts b/src/ui/arena-flyout.ts index ab3bd13b47a..fec02ffb660 100644 --- a/src/ui/arena-flyout.ts +++ b/src/ui/arena-flyout.ts @@ -1,6 +1,7 @@ import { addTextObject, TextStyle } from "./text"; import { globalScene } from "#app/global-scene"; -import { ArenaTagSide, ArenaTrapTag } from "#app/data/arena-tag"; +import { ArenaTrapTag } from "#app/data/arena-tag"; +import { ArenaTagSide } from "#enums/arena-tag-side"; import { WeatherType } from "#enums/weather-type"; import { TerrainType } from "#app/data/terrain"; import { addWindow, WindowVariant } from "./ui-theme"; diff --git a/src/ui/ball-ui-handler.ts b/src/ui/ball-ui-handler.ts index bb1e8d0e85e..11fb485164a 100644 --- a/src/ui/ball-ui-handler.ts +++ b/src/ui/ball-ui-handler.ts @@ -1,6 +1,6 @@ import { getPokeballName } from "../data/pokeball"; import { addTextObject, getTextStyleOptions, TextStyle } from "./text"; -import { Command } from "./command-ui-handler"; +import { Command } from "#enums/command"; import { UiMode } from "#enums/ui-mode"; import UiHandler from "./ui-handler"; import { addWindow } from "./ui-theme"; diff --git a/src/ui/command-ui-handler.ts b/src/ui/command-ui-handler.ts index 0d38672323a..8df399b6d9b 100644 --- a/src/ui/command-ui-handler.ts +++ b/src/ui/command-ui-handler.ts @@ -11,14 +11,7 @@ import { TerastallizeAccessModifier } from "#app/modifier/modifier"; import { PokemonType } from "#enums/pokemon-type"; import { getTypeRgb } from "#app/data/type"; import { SpeciesId } from "#enums/species-id"; - -export enum Command { - FIGHT = 0, - BALL, - POKEMON, - RUN, - TERA, -} +import { Command } from "#enums/command"; export default class CommandUiHandler extends UiHandler { private commandsContainer: Phaser.GameObjects.Container; 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..1b1aed91203 100644 --- a/src/ui/egg-gacha-ui-handler.ts +++ b/src/ui/egg-gacha-ui-handler.ts @@ -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 3fe06cdf039..14cd10d0d6f 100644 --- a/src/ui/fight-ui-handler.ts +++ b/src/ui/fight-ui-handler.ts @@ -3,18 +3,20 @@ import { globalScene } from "#app/global-scene"; import { addTextObject, TextStyle } from "./text"; import { getTypeDamageMultiplierColor } from "#app/data/type"; import { PokemonType } from "#enums/pokemon-type"; -import { Command } from "./command-ui-handler"; +import { Command } from "#enums/command"; import { UiMode } from "#enums/ui-mode"; import UiHandler from "./ui-handler"; import { getLocalizedSpriteKey, fixedInt, padInt } from "#app/utils/common"; import { MoveCategory } from "#enums/MoveCategory"; import i18next from "i18next"; import { Button } from "#enums/buttons"; -import type { EnemyPokemon, PokemonMove } from "#app/field/pokemon"; +import type { EnemyPokemon } from "#app/field/pokemon"; +import type { PokemonMove } from "#app/data/moves/pokemon-move"; 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"; @@ -138,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/game-stats-ui-handler.ts b/src/ui/game-stats-ui-handler.ts index dc184a34866..4213a244fdb 100644 --- a/src/ui/game-stats-ui-handler.ts +++ b/src/ui/game-stats-ui-handler.ts @@ -5,7 +5,7 @@ import UiHandler from "#app/ui/ui-handler"; import { addWindow } from "#app/ui/ui-theme"; import { getPlayTimeString, formatFancyLargeNumber, toReadableString } from "#app/utils/common"; import type { GameData } from "#app/system/game-data"; -import { DexAttr } from "#app/system/game-data"; +import { DexAttr } from "#enums/dex-attr"; import { speciesStarterCosts } from "#app/data/balance/starters"; import { Button } from "#enums/buttons"; import i18next from "i18next"; diff --git a/src/ui/hatched-pokemon-container.ts b/src/ui/hatched-pokemon-container.ts index 9d1c13e19d5..8b9a5309a9c 100644 --- a/src/ui/hatched-pokemon-container.ts +++ b/src/ui/hatched-pokemon-container.ts @@ -1,7 +1,7 @@ import type { EggHatchData } from "#app/data/egg-hatch-data"; import { Gender } from "#app/data/gender"; import { getVariantTint } from "#app/sprites/variant"; -import { DexAttr } from "#app/system/game-data"; +import { DexAttr } from "#enums/dex-attr"; import { globalScene } from "#app/global-scene"; import type PokemonSpecies from "#app/data/pokemon-species"; import type PokemonIconAnimHandler from "./pokemon-icon-anim-handler"; 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 6ce192751df..cf6333f4580 100644 --- a/src/ui/party-ui-handler.ts +++ b/src/ui/party-ui-handler.ts @@ -1,27 +1,25 @@ -import type { PlayerPokemon, PokemonMove } 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 "#app/field/pokemon"; +import { MoveResult } from "#enums/move-result"; import { addBBCodeTextObject, addTextObject, getTextColor, TextStyle } from "#app/ui/text"; -import { Command } from "#app/ui/command-ui-handler"; +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 { ForceSwitchOutAttr } from "#app/data/moves/move"; +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"; import PokemonIconAnimHandler, { PokemonIconAnimMode } from "#app/ui/pokemon-icon-anim-handler"; import { pokemonEvolutions } from "#app/data/balance/pokemon-evolutions"; import { addWindow } from "#app/ui/ui-theme"; -import { SpeciesFormChangeItemTrigger, FormChangeItem } from "#app/data/pokemon-forms"; +import { SpeciesFormChangeItemTrigger } from "#app/data/pokemon-forms/form-change-triggers"; +import { FormChangeItem } from "#enums/form-change-item"; import { getVariantTint } from "#app/sprites/variant"; import { Button } from "#enums/buttons"; -import { applyChallenges, ChallengeType } from "#app/data/challenge"; +import { applyChallenges } from "#app/data/challenge"; +import { ChallengeType } from "#enums/challenge-type"; import MoveInfoOverlay from "#app/ui/move-info-overlay"; import i18next from "i18next"; import type BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext"; @@ -224,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) }); @@ -552,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; @@ -685,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[]; } @@ -926,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[]; @@ -1163,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[]) ?? [] ); } @@ -1388,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(); @@ -1398,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]; @@ -1413,6 +1411,7 @@ export default class PartyUiHandler extends MessageUiHandler { } } break; + } } } else if (this.partyUiMode === PartyUiMode.REMEMBER_MOVE_MODIFIER) { const learnableLevelMoves = pokemon.getLearnableLevelMoves(); @@ -1563,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 a81265e1a55..7ef4f8f920b 100644 --- a/src/ui/pokedex-page-ui-handler.ts +++ b/src/ui/pokedex-page-ui-handler.ts @@ -22,7 +22,8 @@ import { starterPassiveAbilities } from "#app/data/balance/passives"; import { PokemonType } from "#enums/pokemon-type"; import type { StarterAttributes } from "#app/system/game-data"; import type { DexEntry } from "#app/@types/dex-data"; -import { AbilityAttr, DexAttr } from "#app/system/game-data"; +import { AbilityAttr } from "#enums/ability-attr"; +import { DexAttr } from "#enums/dex-attr"; import type { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; import MessageUiHandler from "#app/ui/message-ui-handler"; import { StatsContainer } from "#app/ui/stats-container"; diff --git a/src/ui/pokedex-ui-handler.ts b/src/ui/pokedex-ui-handler.ts index 96451041306..5b292e7232f 100644 --- a/src/ui/pokedex-ui-handler.ts +++ b/src/ui/pokedex-ui-handler.ts @@ -11,9 +11,12 @@ import { allSpecies, getPokemonSpeciesForm, getPokerusStarters, normalForm } fro import { getStarterValueFriendshipCap, speciesStarterCosts, POKERUS_STARTER_COUNT } from "#app/data/balance/starters"; import { catchableSpecies } from "#app/data/balance/biomes"; import { PokemonType } from "#enums/pokemon-type"; -import type { DexAttrProps, StarterAttributes, StarterPreferences } from "#app/system/game-data"; +import type { DexAttrProps, StarterAttributes } from "#app/system/game-data"; +import type { StarterPreferences } from "#app/utils/data"; import type { DexEntry } from "#app/@types/dex-data"; -import { AbilityAttr, DexAttr, loadStarterPreferences } from "#app/system/game-data"; +import { loadStarterPreferences } from "#app/utils/data"; +import { AbilityAttr } from "#enums/ability-attr"; +import { DexAttr } from "#enums/dex-attr"; import MessageUiHandler from "#app/ui/message-ui-handler"; import PokemonIconAnimHandler, { PokemonIconAnimMode } from "#app/ui/pokemon-icon-anim-handler"; import { TextStyle, addTextObject } from "#app/ui/text"; 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/pokemon-info-container.ts b/src/ui/pokemon-info-container.ts index 3dbe3b7af7d..0056c3e2f11 100644 --- a/src/ui/pokemon-info-container.ts +++ b/src/ui/pokemon-info-container.ts @@ -8,7 +8,7 @@ import type Pokemon from "../field/pokemon"; import i18next from "i18next"; import type { StarterDataEntry } from "../system/game-data"; import type { DexEntry } from "#app/@types/dex-data"; -import { DexAttr } from "../system/game-data"; +import { DexAttr } from "#enums/dex-attr"; import { fixedInt, getShinyDescriptor } from "#app/utils/common"; import ConfirmUiHandler from "./confirm-ui-handler"; import { StatsContainer } from "./stats-container"; diff --git a/src/ui/run-history-ui-handler.ts b/src/ui/run-history-ui-handler.ts index 92c5a2fde07..06ef590c1e8 100644 --- a/src/ui/run-history-ui-handler.ts +++ b/src/ui/run-history-ui-handler.ts @@ -1,5 +1,5 @@ import { globalScene } from "#app/global-scene"; -import { GameModes } from "../game-mode"; +import { GameModes } from "#enums/game-modes"; import { TextStyle, addTextObject } from "./text"; import { UiMode } from "#enums/ui-mode"; import { addWindow } from "./ui-theme"; @@ -11,7 +11,7 @@ import { Button } from "../enums/buttons"; import { BattleType } from "#enums/battle-type"; import type { RunEntry } from "../system/game-data"; import { PlayerGender } from "#enums/player-gender"; -import { TrainerVariant } from "../field/trainer"; +import { TrainerVariant } from "#enums/trainer-variant"; import { RunDisplayMode } from "#app/ui/run-info-ui-handler"; export type RunSelectCallback = (cursor: number) => void; diff --git a/src/ui/run-info-ui-handler.ts b/src/ui/run-info-ui-handler.ts index c8dade8878f..78ab4a40407 100644 --- a/src/ui/run-info-ui-handler.ts +++ b/src/ui/run-info-ui-handler.ts @@ -1,4 +1,4 @@ -import { GameModes } from "../game-mode"; +import { GameModes } from "#enums/game-modes"; import UiHandler from "./ui-handler"; import type { SessionSaveData } from "../system/game-data"; import { TextStyle, addTextObject, addBBCodeTextObject, getTextColor } from "./text"; @@ -10,7 +10,7 @@ import type PokemonData from "../system/pokemon-data"; import i18next from "i18next"; import { Button } from "../enums/buttons"; import { BattleType } from "#enums/battle-type"; -import { TrainerVariant } from "../field/trainer"; +import { TrainerVariant } from "#enums/trainer-variant"; import { Challenges } from "#enums/challenges"; import { getLuckString, getLuckTextTint } from "../modifier/modifier-type"; import RoundRectangle from "phaser3-rex-plugins/plugins/roundrectangle"; @@ -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 47226de3354..7c8b05b6f76 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"; @@ -22,10 +22,13 @@ import type PokemonSpecies from "#app/data/pokemon-species"; import { allSpecies, getPokemonSpeciesForm, getPokerusStarters } from "#app/data/pokemon-species"; import { getStarterValueFriendshipCap, speciesStarterCosts, POKERUS_STARTER_COUNT } from "#app/data/balance/starters"; import { PokemonType } from "#enums/pokemon-type"; -import { GameModes } from "#app/game-mode"; -import type { DexAttrProps, StarterMoveset, StarterAttributes, StarterPreferences } from "#app/system/game-data"; +import { GameModes } from "#enums/game-modes"; +import type { DexAttrProps, StarterMoveset, StarterAttributes } from "#app/system/game-data"; +import type { StarterPreferences } from "#app/utils/data"; import type { DexEntry } from "#app/@types/dex-data"; -import { AbilityAttr, DexAttr, loadStarterPreferences, saveStarterPreferences } from "#app/system/game-data"; +import { loadStarterPreferences, saveStarterPreferences } from "#app/utils/data"; +import { AbilityAttr } from "#enums/ability-attr"; +import { DexAttr } from "#enums/dex-attr"; import { Tutorial, handleTutorial } from "#app/tutorial"; import type { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; import MessageUiHandler from "#app/ui/message-ui-handler"; @@ -38,7 +41,8 @@ import { Egg } from "#app/data/egg"; import Overrides from "#app/overrides"; import { SettingKeyboard } from "#app/system/settings/settings-keyboard"; import { Passive as PassiveAttr } from "#enums/passive"; -import { applyChallenges, ChallengeType } from "#app/data/challenge"; +import { applyChallenges } from "#app/data/challenge"; +import { ChallengeType } from "#enums/challenge-type"; import MoveInfoOverlay from "#app/ui/move-info-overlay"; import { getEggTierForSpecies } from "#app/data/egg"; import { Device } from "#enums/devices"; @@ -1759,7 +1763,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; @@ -1811,6 +1815,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 a6f640b436f..d30322de293 100644 --- a/src/ui/summary-ui-handler.ts +++ b/src/ui/summary-ui-handler.ts @@ -13,7 +13,8 @@ import { formatStat, getShinyDescriptor, } from "#app/utils/common"; -import type { PlayerPokemon, PokemonMove } from "#app/field/pokemon"; +import type { PlayerPokemon } from "#app/field/pokemon"; +import type { PokemonMove } from "#app/data/moves/pokemon-move"; import { getStarterValueFriendshipCap, speciesStarterCosts } from "#app/data/balance/starters"; import { argbFromRgba } from "@material/material-color-utilities"; import { getTypeRgb } from "#app/data/type"; @@ -32,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/target-select-ui-handler.ts b/src/ui/target-select-ui-handler.ts index a2f89d38970..8106e4de2da 100644 --- a/src/ui/target-select-ui-handler.ts +++ b/src/ui/target-select-ui-handler.ts @@ -1,8 +1,8 @@ -import { BattlerIndex } from "../battle"; +import { BattlerIndex } from "#enums/battler-index"; import { UiMode } from "#enums/ui-mode"; import UiHandler from "./ui-handler"; import { isNullOrUndefined, fixedInt } from "#app/utils/common"; -import { getMoveTargets } from "../data/moves/move"; +import { getMoveTargets } from "#app/data/moves/move-utils"; import { Button } from "#enums/buttons"; import type { MoveId } from "#enums/move-id"; import type Pokemon from "#app/field/pokemon"; 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/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/data.ts b/src/utils/data.ts index 33623dc5e40..5a28657d034 100644 --- a/src/utils/data.ts +++ b/src/utils/data.ts @@ -1,3 +1,8 @@ +import { loggedInUser } from "#app/account"; +import type { StarterAttributes } from "#app/system/game-data"; +import { AES, enc } from "crypto-js"; +import { saveKey } from "#app/constants"; + /** * Perform a deep copy of an object. * @param values - The object to be deep copied. @@ -38,3 +43,45 @@ export function deepMergeSpriteData(dest: object, source: object) { } } } + +export function encrypt(data: string, bypassLogin: boolean): string { + return (bypassLogin + ? (data: string) => btoa(encodeURIComponent(data)) + : (data: string) => AES.encrypt(data, saveKey))(data) as unknown as string; // TODO: is this correct? +} + +export function decrypt(data: string, bypassLogin: boolean): string { + return ( + bypassLogin + ? (data: string) => decodeURIComponent(atob(data)) + : (data: string) => AES.decrypt(data, saveKey).toString(enc.Utf8) + )(data); +} + +// the latest data saved/loaded for the Starter Preferences. Required to reduce read/writes. Initialize as "{}", since this is the default value and no data needs to be stored if present. +// if they ever add private static variables, move this into StarterPrefs +const StarterPrefers_DEFAULT: string = "{}"; +let StarterPrefers_private_latest: string = StarterPrefers_DEFAULT; + +export interface StarterPreferences { + [key: number]: StarterAttributes; +} +// called on starter selection show once + +export function loadStarterPreferences(): StarterPreferences { + return JSON.parse( + (StarterPrefers_private_latest = + localStorage.getItem(`starterPrefs_${loggedInUser?.username}`) || StarterPrefers_DEFAULT), + ); +} +// called on starter selection clear, always + +export function saveStarterPreferences(prefs: StarterPreferences): void { + const pStr: string = JSON.stringify(prefs); + if (pStr !== StarterPrefers_private_latest) { + // something changed, store the update + localStorage.setItem(`starterPrefs_${loggedInUser?.username}`, pStr); + // update the latest prefs + StarterPrefers_private_latest = pStr; + } +} 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/test/abilities/ability_activation_order.test.ts b/test/abilities/ability_activation_order.test.ts new file mode 100644 index 00000000000..04adf40b623 --- /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") + .disableCrits() + .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 bf6ef72c3f1..2cfea64d20e 100644 --- a/test/abilities/analytic.test.ts +++ b/test/abilities/analytic.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { isBetween, toDmgValue } from "#app/utils/common"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; diff --git a/test/abilities/aroma_veil.test.ts b/test/abilities/aroma_veil.test.ts index 00baa6b6268..56cce749b9f 100644 --- a/test/abilities/aroma_veil.test.ts +++ b/test/abilities/aroma_veil.test.ts @@ -6,7 +6,7 @@ import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { BattlerTagType } from "#enums/battler-tag-type"; import { ArenaTagType } from "#enums/arena-tag-type"; -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import type { PlayerPokemon } from "#app/field/pokemon"; describe("Moves - Aroma Veil", () => { 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/battle_bond.test.ts b/test/abilities/battle_bond.test.ts index b4ce73d107b..adb4a9c6e7a 100644 --- a/test/abilities/battle_bond.test.ts +++ b/test/abilities/battle_bond.test.ts @@ -1,4 +1,3 @@ -import { MultiHitAttr } from "#app/data/moves/move"; import { allMoves } from "#app/data/data-lists"; import { MultiHitType } from "#enums/MultiHitType"; import { Status } from "#app/data/status-effect"; @@ -66,7 +65,7 @@ describe("Abilities - BATTLE BOND", () => { vi.spyOn(waterShuriken, "calculateBattlePower"); let actualMultiHitType: MultiHitType | null = null; - const multiHitAttr = waterShuriken.getAttrs(MultiHitAttr)[0]; + const multiHitAttr = waterShuriken.getAttrs("MultiHitAttr")[0]; vi.spyOn(multiHitAttr, "getHitCount").mockImplementation(() => { actualMultiHitType = multiHitAttr.getMultiHitType(); return 3; diff --git a/test/abilities/beast_boost.test.ts b/test/abilities/beast_boost.test.ts index 17ba4020961..8be9156ce07 100644 --- a/test/abilities/beast_boost.test.ts +++ b/test/abilities/beast_boost.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; @@ -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 bb2670ec2f0..39d9d0f4d7f 100644 --- a/test/abilities/commander.test.ts +++ b/test/abilities/commander.test.ts @@ -1,11 +1,11 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { BattlerTagType } from "#enums/battler-tag-type"; import { PokemonAnimType } from "#enums/pokemon-anim-type"; import type { EffectiveStat } from "#enums/stat"; import { Stat } from "#enums/stat"; import { StatusEffect } from "#enums/status-effect"; import { WeatherType } from "#enums/weather-type"; -import { MoveResult } from "#app/field/pokemon"; +import { MoveResult } from "#enums/move-result"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; 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/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/dancer.test.ts b/test/abilities/dancer.test.ts index 518c96ea124..2a4a3c36bcc 100644 --- a/test/abilities/dancer.test.ts +++ b/test/abilities/dancer.test.ts @@ -1,5 +1,5 @@ -import { BattlerIndex } from "#app/battle"; -import { MoveResult } from "#app/field/pokemon"; +import { BattlerIndex } from "#enums/battler-index"; +import { MoveResult } from "#enums/move-result"; import type { MovePhase } from "#app/phases/move-phase"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; diff --git a/test/abilities/desolate-land.test.ts b/test/abilities/desolate-land.test.ts index bcd980187ac..90565d9caf8 100644 --- a/test/abilities/desolate-land.test.ts +++ b/test/abilities/desolate-land.test.ts @@ -1,7 +1,7 @@ import { PokeballType } from "#app/enums/pokeball"; import { WeatherType } from "#app/enums/weather-type"; import type { CommandPhase } from "#app/phases/command-phase"; -import { Command } from "#app/ui/command-ui-handler"; +import { Command } from "#enums/command"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; diff --git a/test/abilities/disguise.test.ts b/test/abilities/disguise.test.ts index 15ded41527a..4ccd825e574 100644 --- a/test/abilities/disguise.test.ts +++ b/test/abilities/disguise.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { toDmgValue } from "#app/utils/common"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; @@ -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/early_bird.test.ts b/test/abilities/early_bird.test.ts index e158ce1888b..e19f2653b57 100644 --- a/test/abilities/early_bird.test.ts +++ b/test/abilities/early_bird.test.ts @@ -1,5 +1,5 @@ import { Status } from "#app/data/status-effect"; -import { MoveResult } from "#app/field/pokemon"; +import { MoveResult } from "#enums/move-result"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; diff --git a/test/abilities/flash_fire.test.ts b/test/abilities/flash_fire.test.ts index 8fabda95c80..fe17013f49f 100644 --- a/test/abilities/flash_fire.test.ts +++ b/test/abilities/flash_fire.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { BattlerTagType } from "#app/enums/battler-tag-type"; import { SpeciesId } from "#enums/species-id"; import { MovePhase } from "#app/phases/move-phase"; @@ -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 d4a03bb2330..f072afe7f34 100644 --- a/test/abilities/flower_gift.test.ts +++ b/test/abilities/flower_gift.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { allAbilities } from "#app/data/data-lists"; import { AbilityId } from "#enums/ability-id"; import { Stat } from "#app/enums/stat"; @@ -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 bd76541495d..d3e99185c2f 100644 --- a/test/abilities/flower_veil.test.ts +++ b/test/abilities/flower_veil.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; @@ -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 b87519ae80a..8d3a679c917 100644 --- a/test/abilities/forecast.test.ts +++ b/test/abilities/forecast.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { allAbilities } from "#app/data/data-lists"; import { AbilityId } from "#enums/ability-id"; import { WeatherType } from "#app/enums/weather-type"; diff --git a/test/abilities/friend_guard.test.ts b/test/abilities/friend_guard.test.ts index d401fa96feb..c5809e18e96 100644 --- a/test/abilities/friend_guard.test.ts +++ b/test/abilities/friend_guard.test.ts @@ -4,7 +4,7 @@ import { AbilityId } from "#enums/ability-id"; import GameManager from "#test/testUtils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { allAbilities } from "#app/data/data-lists"; import { allMoves } from "#app/data/data-lists"; import { MoveCategory } from "#enums/MoveCategory"; diff --git a/test/abilities/good_as_gold.test.ts b/test/abilities/good_as_gold.test.ts index d6c80a5347f..3b6600eabd4 100644 --- a/test/abilities/good_as_gold.test.ts +++ b/test/abilities/good_as_gold.test.ts @@ -1,5 +1,5 @@ -import { BattlerIndex } from "#app/battle"; -import { ArenaTagSide } from "#app/data/arena-tag"; +import { BattlerIndex } from "#enums/battler-index"; +import { ArenaTagSide } from "#enums/arena-tag-side"; import { allAbilities } from "#app/data/data-lists"; import { ArenaTagType } from "#app/enums/arena-tag-type"; import { BattlerTagType } from "#app/enums/battler-tag-type"; @@ -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 3ad138749a8..a8b09461ea0 100644 --- a/test/abilities/gorilla_tactics.test.ts +++ b/test/abilities/gorilla_tactics.test.ts @@ -1,15 +1,19 @@ -import { BattlerIndex } from "#app/battle"; +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 b56b316484f..b41eea9ce18 100644 --- a/test/abilities/gulp_missile.test.ts +++ b/test/abilities/gulp_missile.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import type Pokemon from "#app/field/pokemon"; import { AbilityId } from "#enums/ability-id"; import { BattlerTagType } from "#enums/battler-tag-type"; diff --git a/test/abilities/harvest.test.ts b/test/abilities/harvest.test.ts index 5a6ddf35459..40e6a6abd62 100644 --- a/test/abilities/harvest.test.ts +++ b/test/abilities/harvest.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { PostTurnRestoreBerryAbAttr } from "#app/data/abilities/ability"; import type Pokemon from "#app/field/pokemon"; import { BerryModifier, PreserveBerryModifier } from "#app/modifier/modifier"; diff --git a/test/abilities/healer.test.ts b/test/abilities/healer.test.ts index 7f71be7b86e..94272848e16 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(() => { @@ -38,30 +35,28 @@ describe("Abilities - Healer", () => { .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/honey_gather.test.ts b/test/abilities/honey_gather.test.ts index a069654ae21..06d351b2a61 100644 --- a/test/abilities/honey_gather.test.ts +++ b/test/abilities/honey_gather.test.ts @@ -1,5 +1,5 @@ import type { CommandPhase } from "#app/phases/command-phase"; -import { Command } from "#app/ui/command-ui-handler"; +import { Command } from "#enums/command"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; diff --git a/test/abilities/hustle.test.ts b/test/abilities/hustle.test.ts index 58aa01bb39a..7ac636b15b2 100644 --- a/test/abilities/hustle.test.ts +++ b/test/abilities/hustle.test.ts @@ -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 6ef91981cf3..c42713d7e6c 100644 --- a/test/abilities/ice_face.test.ts +++ b/test/abilities/ice_face.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { MoveEffectPhase } from "#app/phases/move-effect-phase"; import { MoveEndPhase } from "#app/phases/move-end-phase"; import { QuietFormChangePhase } from "#app/phases/quiet-form-change-phase"; @@ -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/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 891a716f7a4..03b55e925cd 100644 --- a/test/abilities/infiltrator.test.ts +++ b/test/abilities/infiltrator.test.ts @@ -1,4 +1,4 @@ -import { ArenaTagSide } from "#app/data/arena-tag"; +import { ArenaTagSide } from "#enums/arena-tag-side"; import { allMoves } from "#app/data/data-lists"; import { ArenaTagType } from "#enums/arena-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type"; 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 777ccb88ba9..a69f5e94067 100644 --- a/test/abilities/lightningrod.test.ts +++ b/test/abilities/lightningrod.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; diff --git a/test/abilities/magic_bounce.test.ts b/test/abilities/magic_bounce.test.ts index 8c3eeec974c..fb4f273950a 100644 --- a/test/abilities/magic_bounce.test.ts +++ b/test/abilities/magic_bounce.test.ts @@ -1,6 +1,6 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { allAbilities } from "#app/data/data-lists"; -import { ArenaTagSide } from "#app/data/arena-tag"; +import { ArenaTagSide } from "#enums/arena-tag-side"; import { allMoves } from "#app/data/data-lists"; import { ArenaTagType } from "#app/enums/arena-tag-type"; import { BattlerTagType } from "#app/enums/battler-tag-type"; @@ -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 c36b35c09b7..4274e692a2e 100644 --- a/test/abilities/magic_guard.test.ts +++ b/test/abilities/magic_guard.test.ts @@ -1,4 +1,5 @@ -import { ArenaTagSide, getArenaTag } from "#app/data/arena-tag"; +import { getArenaTag } from "#app/data/arena-tag"; +import { ArenaTagSide } from "#enums/arena-tag-side"; import { getStatusEffectCatchRateMultiplier } from "#app/data/status-effect"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { AbilityId } from "#enums/ability-id"; @@ -29,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) @@ -88,8 +89,9 @@ 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, MoveId.WORRY_SEED, MoveId.WORRY_SEED, MoveId.WORRY_SEED]) + .statusEffect(StatusEffect.POISON); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); @@ -107,8 +109,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]); @@ -129,8 +130,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]); @@ -207,8 +207,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()!; @@ -330,8 +329,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]); @@ -352,8 +352,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]); @@ -376,8 +375,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]); @@ -399,8 +397,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]); @@ -421,9 +418,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/mirror_armor.test.ts b/test/abilities/mirror_armor.test.ts index 319e47cbfb3..71b072c7d06 100644 --- a/test/abilities/mirror_armor.test.ts +++ b/test/abilities/mirror_armor.test.ts @@ -5,7 +5,7 @@ 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 "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; // TODO: When Magic Bounce is implemented, make a test for its interaction with mirror guard, use screech @@ -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 099fb54c998..e2e8a73da9d 100644 --- a/test/abilities/mold_breaker.test.ts +++ b/test/abilities/mold_breaker.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { globalScene } from "#app/global-scene"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; diff --git a/test/abilities/moxie.test.ts b/test/abilities/moxie.test.ts index 04ca68325e6..a85ed081448 100644 --- a/test/abilities/moxie.test.ts +++ b/test/abilities/moxie.test.ts @@ -5,7 +5,7 @@ import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { EnemyCommandPhase } from "#app/phases/enemy-command-phase"; import { VictoryPhase } from "#app/phases/victory-phase"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; @@ -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/mycelium_might.test.ts b/test/abilities/mycelium_might.test.ts index 1f236f2c2fe..7bfb3fb7b81 100644 --- a/test/abilities/mycelium_might.test.ts +++ b/test/abilities/mycelium_might.test.ts @@ -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 2d1fef29688..51d2bed3ff0 100644 --- a/test/abilities/neutralizing_gas.test.ts +++ b/test/abilities/neutralizing_gas.test.ts @@ -1,7 +1,6 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import type { CommandPhase } from "#app/phases/command-phase"; -import { Command } from "#app/ui/command-ui-handler"; -import { PostSummonWeatherChangeAbAttr } from "#app/data/abilities/ability"; +import { Command } from "#enums/command"; import { AbilityId } from "#enums/ability-id"; import { ArenaTagType } from "#enums/arena-tag-type"; import { MoveId } from "#enums/move-id"; @@ -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/no_guard.test.ts b/test/abilities/no_guard.test.ts index 5b127204f9f..dc35e0e1b9a 100644 --- a/test/abilities/no_guard.test.ts +++ b/test/abilities/no_guard.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { MoveEffectPhase } from "#app/phases/move-effect-phase"; import { MoveEndPhase } from "#app/phases/move-end-phase"; import { AbilityId } from "#enums/ability-id"; diff --git a/test/abilities/normal-move-type-change.test.ts b/test/abilities/normal-move-type-change.test.ts index 578a6ad2a21..d9e39b32a7c 100644 --- a/test/abilities/normal-move-type-change.test.ts +++ b/test/abilities/normal-move-type-change.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { allMoves } from "#app/data/data-lists"; import { PokemonType } from "#enums/pokemon-type"; import { AbilityId } from "#enums/ability-id"; @@ -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..2e000e76a27 100644 --- a/test/abilities/normalize.test.ts +++ b/test/abilities/normalize.test.ts @@ -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/parental_bond.test.ts b/test/abilities/parental_bond.test.ts index 73efd0f57e2..f9bd703d751 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") + .disableCrits() + .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/pastel_veil.test.ts b/test/abilities/pastel_veil.test.ts index 8a3aec918d0..b8e8873ed36 100644 --- a/test/abilities/pastel_veil.test.ts +++ b/test/abilities/pastel_veil.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { AbilityId } from "#enums/ability-id"; import { CommandPhase } from "#app/phases/command-phase"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; diff --git a/test/abilities/perish_body.test.ts b/test/abilities/perish_body.test.ts index 9668bf349a8..5b98aab5f6c 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") + .disableCrits() + .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 3bf74acaca7..ba7bb5d084b 100644 --- a/test/abilities/protosynthesis.test.ts +++ b/test/abilities/protosynthesis.test.ts @@ -5,7 +5,7 @@ import { SpeciesId } from "#enums/species-id"; import { Stat } from "#enums/stat"; import GameManager from "#test/testUtils/gameManager"; import Phaser from "phaser"; -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; describe("Abilities - Protosynthesis", () => { 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..62d5d782fc5 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") + .disableCrits() + .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/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..4b649275d83 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") + .disableCrits() + .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 bfdbd5324bb..c5c12fe217e 100644 --- a/test/abilities/serene_grace.test.ts +++ b/test/abilities/serene_grace.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; @@ -6,7 +6,6 @@ import GameManager from "#test/testUtils/gameManager"; import Phaser from "phaser"; import { allMoves } from "#app/data/data-lists"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { FlinchAttr } from "#app/data/moves/move"; describe("Abilities - Serene Grace", () => { let phaserGame: Phaser.Game; @@ -39,7 +38,7 @@ describe("Abilities - Serene Grace", () => { await game.classicMode.startBattle([SpeciesId.SHUCKLE]); const airSlashMove = allMoves[MoveId.AIR_SLASH]; - const airSlashFlinchAttr = airSlashMove.getAttrs(FlinchAttr)[0]; + const airSlashFlinchAttr = airSlashMove.getAttrs("FlinchAttr")[0]; vi.spyOn(airSlashFlinchAttr, "getMoveChance"); game.move.select(MoveId.AIR_SLASH); diff --git a/test/abilities/sheer_force.test.ts b/test/abilities/sheer_force.test.ts index a5b1cf3b5b2..b597afd8293 100644 --- a/test/abilities/sheer_force.test.ts +++ b/test/abilities/sheer_force.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { PokemonType } from "#enums/pokemon-type"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; @@ -7,7 +7,6 @@ import { Stat } from "#enums/stat"; import GameManager from "#test/testUtils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { FlinchAttr } from "#app/data/moves/move"; import { allMoves } from "#app/data/data-lists"; describe("Abilities - Sheer Force", () => { @@ -43,7 +42,7 @@ describe("Abilities - Sheer Force", () => { const airSlashMove = allMoves[MoveId.AIR_SLASH]; vi.spyOn(airSlashMove, "calculateBattlePower"); - const airSlashFlinchAttr = airSlashMove.getAttrs(FlinchAttr)[0]; + const airSlashFlinchAttr = airSlashMove.getAttrs("FlinchAttr")[0]; vi.spyOn(airSlashFlinchAttr, "getMoveChance"); game.move.select(MoveId.AIR_SLASH); @@ -70,7 +69,7 @@ describe("Abilities - Sheer Force", () => { await game.phaseInterceptor.to("BerryPhase", false); expect(bindMove.calculateBattlePower).toHaveLastReturnedWith(bindMove.power); - }, 20000); + }); it("Sheer Force does not boost the base damage of moves with no secondary effect", async () => { game.override.moveset([MoveId.TACKLE]); @@ -98,7 +97,7 @@ describe("Abilities - Sheer Force", () => { const enemyPokemon = game.scene.getEnemyPokemon(); const headbuttMove = allMoves[MoveId.HEADBUTT]; vi.spyOn(headbuttMove, "calculateBattlePower"); - const headbuttFlinchAttr = headbuttMove.getAttrs(FlinchAttr)[0]; + const headbuttFlinchAttr = headbuttMove.getAttrs("FlinchAttr")[0]; vi.spyOn(headbuttFlinchAttr, "getMoveChance"); game.move.select(MoveId.HEADBUTT); diff --git a/test/abilities/shield_dust.test.ts b/test/abilities/shield_dust.test.ts index 03b175c3ca5..6bb63fd16a5 100644 --- a/test/abilities/shield_dust.test.ts +++ b/test/abilities/shield_dust.test.ts @@ -1,10 +1,5 @@ -import { BattlerIndex } from "#app/battle"; -import { - applyAbAttrs, - applyPreDefendAbAttrs, - IgnoreMoveEffectsAbAttr, - MoveEffectChanceMultiplierAbAttr, -} from "#app/data/abilities/ability"; +import { BattlerIndex } from "#enums/battler-index"; +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/speed_boost.test.ts b/test/abilities/speed_boost.test.ts index 245bf22345d..a4445d085f3 100644 --- a/test/abilities/speed_boost.test.ts +++ b/test/abilities/speed_boost.test.ts @@ -6,7 +6,7 @@ import GameManager from "#test/testUtils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import type { CommandPhase } from "#app/phases/command-phase"; -import { Command } from "#app/ui/command-ui-handler"; +import { Command } from "#enums/command"; import { AttemptRunPhase } from "#app/phases/attempt-run-phase"; describe("Abilities - Speed Boost", () => { diff --git a/test/abilities/stakeout.test.ts b/test/abilities/stakeout.test.ts index 24c8c47df5c..ba4325e0295 100644 --- a/test/abilities/stakeout.test.ts +++ b/test/abilities/stakeout.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { isBetween } from "#app/utils/common"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; diff --git a/test/abilities/stall.test.ts b/test/abilities/stall.test.ts index df40bed3e90..74fd2f67f83 100644 --- a/test/abilities/stall.test.ts +++ b/test/abilities/stall.test.ts @@ -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 36a2112edda..3a6c0aba100 100644 --- a/test/abilities/storm_drain.test.ts +++ b/test/abilities/storm_drain.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; 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/supreme_overlord.test.ts b/test/abilities/supreme_overlord.test.ts index 6c2ca2d3677..7143b590f68 100644 --- a/test/abilities/supreme_overlord.test.ts +++ b/test/abilities/supreme_overlord.test.ts @@ -2,7 +2,7 @@ import { MoveId } from "#enums/move-id"; import type Move from "#app/data/moves/move"; import { AbilityId } from "#enums/ability-id"; import { SpeciesId } from "#enums/species-id"; -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { MoveEffectPhase } from "#app/phases/move-effect-phase"; import GameManager from "#test/testUtils/gameManager"; import Phaser from "phaser"; diff --git a/test/abilities/sweet_veil.test.ts b/test/abilities/sweet_veil.test.ts index 131feaf7f56..ed9cb20afcc 100644 --- a/test/abilities/sweet_veil.test.ts +++ b/test/abilities/sweet_veil.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { AbilityId } from "#enums/ability-id"; import { BattlerTagType } from "#app/enums/battler-tag-type"; import { CommandPhase } from "#app/phases/command-phase"; @@ -69,10 +69,7 @@ describe("Abilities - Sweet Veil", () => { }); it("prevents the user and its allies already drowsy due to Yawn from falling asleep.", async () => { - game.override.enemySpecies(SpeciesId.PIKACHU); - game.override.enemyLevel(5); - game.override.startingLevel(5); - game.override.enemyMoveset(MoveId.SPLASH); + game.override.enemySpecies(SpeciesId.PIKACHU).enemyLevel(5).startingLevel(5).enemyMoveset(MoveId.SPLASH); await game.classicMode.startBattle([SpeciesId.SHUCKLE, SpeciesId.SHUCKLE, SpeciesId.SWIRLIX]); diff --git a/test/abilities/tera_shell.test.ts b/test/abilities/tera_shell.test.ts index 5889115ee95..a52a01862ff 100644 --- a/test/abilities/tera_shell.test.ts +++ b/test/abilities/tera_shell.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; @@ -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/unburden.test.ts b/test/abilities/unburden.test.ts index b1b10c378a3..6e24e10d168 100644 --- a/test/abilities/unburden.test.ts +++ b/test/abilities/unburden.test.ts @@ -1,5 +1,4 @@ -import { BattlerIndex } from "#app/battle"; -import { PostItemLostAbAttr } from "#app/data/abilities/ability"; +import { BattlerIndex } from "#enums/battler-index"; 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 77f5de41bb1..a15beac8b2c 100644 --- a/test/abilities/victory_star.test.ts +++ b/test/abilities/victory_star.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; diff --git a/test/abilities/volt_absorb.test.ts b/test/abilities/volt_absorb.test.ts index 2e6abf30885..707a889f951 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 "#app/battle"; +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").disableCrits(); }); 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/wimp_out.test.ts b/test/abilities/wimp_out.test.ts index 2c2ab636961..05c848a75c0 100644 --- a/test/abilities/wimp_out.test.ts +++ b/test/abilities/wimp_out.test.ts @@ -1,5 +1,5 @@ -import { BattlerIndex } from "#app/battle"; -import { ArenaTagSide } from "#app/data/arena-tag"; +import { BattlerIndex } from "#enums/battler-index"; +import { ArenaTagSide } from "#enums/arena-tag-side"; import { allMoves } from "#app/data/data-lists"; import GameManager from "#test/testUtils/gameManager"; import { toDmgValue } from "#app/utils/common"; 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..70d2f564218 100644 --- a/test/abilities/zen_mode.test.ts +++ b/test/abilities/zen_mode.test.ts @@ -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/arena_gravity.test.ts b/test/arena/arena_gravity.test.ts index 776bfa0a564..36fe0b58308 100644 --- a/test/arena/arena_gravity.test.ts +++ b/test/arena/arena_gravity.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { allMoves } from "#app/data/data-lists"; import { AbilityId } from "#enums/ability-id"; import { ArenaTagType } from "#enums/arena-tag-type"; 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_hail.test.ts b/test/arena/weather_hail.test.ts index 072fbd20498..27cf46fa9f2 100644 --- a/test/arena/weather_hail.test.ts +++ b/test/arena/weather_hail.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { WeatherType } from "#enums/weather-type"; 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/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 b93c913d09a..a71dca111e3 100644 --- a/test/battle/battle.test.ts +++ b/test/battle/battle.test.ts @@ -1,6 +1,7 @@ import { allSpecies } from "#app/data/pokemon-species"; import { Stat } from "#enums/stat"; -import { GameModes, getGameMode } from "#app/game-mode"; +import { getGameMode } from "#app/game-mode"; +import { GameModes } from "#enums/game-modes"; import { BattleEndPhase } from "#app/phases/battle-end-phase"; import { CommandPhase } from "#app/phases/command-phase"; import { DamageAnimPhase } from "#app/phases/damage-anim-phase"; @@ -9,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"; @@ -61,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); @@ -82,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"); @@ -124,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++) { @@ -145,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); @@ -165,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); @@ -179,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); @@ -200,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"); @@ -246,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; @@ -293,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); @@ -304,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; @@ -335,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..555b3a9c554 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"; diff --git a/test/battle/double_battle.test.ts b/test/battle/double_battle.test.ts index 31bfc480c47..8e606a99ae0 100644 --- a/test/battle/double_battle.test.ts +++ b/test/battle/double_battle.test.ts @@ -1,6 +1,7 @@ import { Status } from "#app/data/status-effect"; import { AbilityId } from "#enums/ability-id"; -import { GameModes, getGameMode } from "#app/game-mode"; +import { getGameMode } from "#app/game-mode"; +import { GameModes } from "#enums/game-modes"; import { BattleEndPhase } from "#app/phases/battle-end-phase"; import { TurnInitPhase } from "#app/phases/turn-init-phase"; import { MoveId } from "#enums/move-id"; @@ -55,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/inverse_battle.test.ts b/test/battle/inverse_battle.test.ts index b0d9c7bb755..66cab3e2d84 100644 --- a/test/battle/inverse_battle.test.ts +++ b/test/battle/inverse_battle.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { PokemonType } from "#enums/pokemon-type"; import { AbilityId } from "#enums/ability-id"; import { ArenaTagType } from "#enums/arena-tag-type"; 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/battlerTags/octolock.test.ts b/test/battlerTags/octolock.test.ts index 63784ed7f1b..d0214f495fc 100644 --- a/test/battlerTags/octolock.test.ts +++ b/test/battlerTags/octolock.test.ts @@ -1,6 +1,7 @@ import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; import type Pokemon from "#app/field/pokemon"; -import { BattlerTagLapseType, OctolockTag, TrappedTag } from "#app/data/battler-tags"; +import { OctolockTag, TrappedTag } from "#app/data/battler-tags"; +import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type"; import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; import { Stat } from "#enums/stat"; import GameManager from "#test/testUtils/gameManager"; diff --git a/test/battlerTags/substitute.test.ts b/test/battlerTags/substitute.test.ts index 703e0ae75f6..06aedab19f4 100644 --- a/test/battlerTags/substitute.test.ts +++ b/test/battlerTags/substitute.test.ts @@ -1,9 +1,10 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { PokemonTurnData, TurnMove } from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon"; -import { MoveResult } from "#app/field/pokemon"; +import { MoveResult } from "#enums/move-result"; import type BattleScene from "#app/battle-scene"; -import { BattlerTagLapseType, BindTag, SubstituteTag } from "#app/data/battler-tags"; +import { BindTag, SubstituteTag } from "#app/data/battler-tags"; +import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type"; import { MoveId } from "#enums/move-id"; import { PokemonAnimType } from "#app/enums/pokemon-anim-type"; import * as messages from "#app/messages"; diff --git a/test/data/status_effect.test.ts b/test/data/status_effect.test.ts index 4a543a04a1b..a5bfb33c023 100644 --- a/test/data/status_effect.test.ts +++ b/test/data/status_effect.test.ts @@ -6,18 +6,19 @@ import { getStatusEffectObtainText, getStatusEffectOverlapText, } from "#app/data/status-effect"; -import { MoveResult } from "#app/field/pokemon"; +import { MoveResult } from "#enums/move-result"; 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 { 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", () => { @@ -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); }); }); diff --git a/test/eggs/egg.test.ts b/test/eggs/egg.test.ts index e37a7c00aa9..7792756a8a3 100644 --- a/test/eggs/egg.test.ts +++ b/test/eggs/egg.test.ts @@ -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 05c6594bad6..6814cf5e12a 100644 --- a/test/endless_boss.test.ts +++ b/test/endless_boss.test.ts @@ -1,6 +1,6 @@ import { BiomeId } from "#enums/biome-id"; import { SpeciesId } from "#enums/species-id"; -import { GameModes } from "#app/game-mode"; +import { GameModes } from "#enums/game-modes"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import GameManager from "#test/testUtils/gameManager"; diff --git a/test/enemy_command.test.ts b/test/enemy_command.test.ts index 8d6d1756a69..fabb1f1fdb0 100644 --- a/test/enemy_command.test.ts +++ b/test/enemy_command.test.ts @@ -5,7 +5,7 @@ import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import type { EnemyPokemon } from "#app/field/pokemon"; -import { AiType } from "#app/field/pokemon"; +import { AiType } from "#enums/ai-type"; import { randSeedInt } from "#app/utils/common"; import GameManager from "#test/testUtils/gameManager"; import Phaser from "phaser"; diff --git a/test/escape-calculations.test.ts b/test/escape-calculations.test.ts index fd1e411e786..2cc0934f7c1 100644 --- a/test/escape-calculations.test.ts +++ b/test/escape-calculations.test.ts @@ -1,6 +1,6 @@ import { AttemptRunPhase } from "#app/phases/attempt-run-phase"; import type { CommandPhase } from "#app/phases/command-phase"; -import { Command } from "#app/ui/command-ui-handler"; +import { Command } from "#enums/command"; import { NumberHolder } from "#app/utils/common"; import { AbilityId } from "#enums/ability-id"; import { SpeciesId } from "#enums/species-id"; @@ -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 f1f894a5984..d3fcd3214bb 100644 --- a/test/final_boss.test.ts +++ b/test/final_boss.test.ts @@ -1,4 +1,4 @@ -import { GameModes } from "#app/game-mode"; +import { GameModes } from "#enums/game-modes"; import { TurnHeldItemTransferModifier } from "#app/modifier/modifier"; import { AbilityId } from "#enums/ability-id"; import { BiomeId } from "#enums/biome-id"; diff --git a/test/game-mode.test.ts b/test/game-mode.test.ts index 0483d18e492..c5ce1e02852 100644 --- a/test/game-mode.test.ts +++ b/test/game-mode.test.ts @@ -1,5 +1,6 @@ import type { GameMode } from "#app/game-mode"; -import { GameModes, getGameMode } from "#app/game-mode"; +import { getGameMode } from "#app/game-mode"; +import { GameModes } from "#enums/game-modes"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import * as Utils from "#app/utils/common"; import GameManager from "#test/testUtils/gameManager"; @@ -14,8 +15,6 @@ describe("game-mode", () => { }); afterEach(() => { game.phaseInterceptor.restoreOg(); - vi.clearAllMocks(); - vi.resetAllMocks(); }); beforeEach(() => { game = new GameManager(phaserGame); diff --git a/test/imports.test.ts b/test/imports.test.ts index 540620d8bb4..8a5c735e950 100644 --- a/test/imports.test.ts +++ b/test/imports.test.ts @@ -4,7 +4,7 @@ import { describe, expect, it } from "vitest"; async function importModule() { try { initStatsKeys(); - const { PokemonMove } = await import("#app/field/pokemon"); + const { PokemonMove } = await import("#app/data/moves/pokemon-move"); const { SpeciesId: Species } = await import("#enums/species-id"); return { PokemonMove, 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/grip_claw.test.ts b/test/items/grip_claw.test.ts index 3c51e7f868d..9c3e6548140 100644 --- a/test/items/grip_claw.test.ts +++ b/test/items/grip_claw.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import type Pokemon from "#app/field/pokemon"; import type { ContactHeldItemTransferChanceModifier } from "#app/modifier/modifier"; import { AbilityId } from "#enums/ability-id"; 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 be697eabcf8..11e4c04ec21 100644 --- a/test/items/multi_lens.test.ts +++ b/test/items/multi_lens.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { Stat } from "#enums/stat"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; 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 2ab3c5ffe74..54927130869 100644 --- a/test/items/reviver_seed.test.ts +++ b/test/items/reviver_seed.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { allMoves } from "#app/data/data-lists"; import { BattlerTagType } from "#app/enums/battler-tag-type"; import type { PokemonInstantReviveModifier } from "#app/modifier/modifier"; 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 0e5ee1e7a8f..37186dcc7a5 100644 --- a/test/moves/after_you.test.ts +++ b/test/moves/after_you.test.ts @@ -1,7 +1,8 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { AbilityId } from "#enums/ability-id"; -import { MoveResult } from "#app/field/pokemon"; +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 2cfb7a76317..132f83cd4c1 100644 --- a/test/moves/alluring_voice.test.ts +++ b/test/moves/alluring_voice.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { AbilityId } from "#enums/ability-id"; import { BattlerTagType } from "#app/enums/battler-tag-type"; import { BerryPhase } from "#app/phases/berry-phase"; diff --git a/test/moves/assist.test.ts b/test/moves/assist.test.ts index c0bdf2deea2..bc51f8bd06a 100644 --- a/test/moves/assist.test.ts +++ b/test/moves/assist.test.ts @@ -1,6 +1,6 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { Stat } from "#app/enums/stat"; -import { MoveResult } from "#app/field/pokemon"; +import { MoveResult } from "#enums/move-result"; import { CommandPhase } from "#app/phases/command-phase"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; diff --git a/test/moves/aurora_veil.test.ts b/test/moves/aurora_veil.test.ts index 7a00be40d3d..98b490b0e32 100644 --- a/test/moves/aurora_veil.test.ts +++ b/test/moves/aurora_veil.test.ts @@ -1,7 +1,6 @@ import type BattleScene from "#app/battle-scene"; -import { ArenaTagSide } from "#app/data/arena-tag"; +import { ArenaTagSide } from "#enums/arena-tag-side"; import type Move from "#app/data/moves/move"; -import { CritOnlyAttr } from "#app/data/moves/move"; import { allMoves } from "#app/data/data-lists"; import { ArenaTagType } from "#app/enums/arena-tag-type"; import type Pokemon from "#app/field/pokemon"; @@ -166,7 +165,7 @@ const getMockedMoveDamage = (defender: Pokemon, attacker: Pokemon, move: Move) = const side = defender.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; if (globalScene.arena.getTagOnSide(ArenaTagType.AURORA_VEIL, side)) { - if (move.getAttrs(CritOnlyAttr).length === 0) { + if (move.getAttrs("CritOnlyAttr").length === 0) { globalScene.arena.applyTagsForSide( ArenaTagType.AURORA_VEIL, side, diff --git a/test/moves/baneful_bunker.test.ts b/test/moves/baneful_bunker.test.ts index 80b9b5470b1..c9cc2025a58 100644 --- a/test/moves/baneful_bunker.test.ts +++ b/test/moves/baneful_bunker.test.ts @@ -4,7 +4,7 @@ import GameManager from "#test/testUtils/gameManager"; import { SpeciesId } from "#enums/species-id"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { StatusEffect } from "#app/enums/status-effect"; describe("Moves - Baneful Bunker", () => { @@ -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 b39e51428b1..a010636034a 100644 --- a/test/moves/baton_pass.test.ts +++ b/test/moves/baton_pass.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import GameManager from "#test/testUtils/gameManager"; import { AbilityId } from "#enums/ability-id"; import { BattlerTagType } from "#enums/battler-tag-type"; @@ -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 2a74f661922..9e1898d9cd1 100644 --- a/test/moves/burning_jealousy.test.ts +++ b/test/moves/burning_jealousy.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { allMoves } from "#app/data/data-lists"; import { AbilityId } from "#enums/ability-id"; import { StatusEffect } from "#app/enums/status-effect"; diff --git a/test/moves/camouflage.test.ts b/test/moves/camouflage.test.ts index 53c44f1386b..03364265179 100644 --- a/test/moves/camouflage.test.ts +++ b/test/moves/camouflage.test.ts @@ -3,7 +3,7 @@ import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { TerrainType } from "#app/data/terrain"; import { PokemonType } from "#enums/pokemon-type"; -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import GameManager from "#test/testUtils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; diff --git a/test/moves/ceaseless_edge.test.ts b/test/moves/ceaseless_edge.test.ts index 65f9b69509d..1dec98fe3a8 100644 --- a/test/moves/ceaseless_edge.test.ts +++ b/test/moves/ceaseless_edge.test.ts @@ -1,4 +1,5 @@ -import { ArenaTagSide, ArenaTrapTag } from "#app/data/arena-tag"; +import { ArenaTrapTag } from "#app/data/arena-tag"; +import { ArenaTagSide } from "#enums/arena-tag-side"; import { allMoves } from "#app/data/data-lists"; import { AbilityId } from "#enums/ability-id"; import { ArenaTagType } from "#app/enums/arena-tag-type"; @@ -26,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); }); @@ -81,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 02f7ac2165c..cf5e791d0b9 100644 --- a/test/moves/chloroblast.test.ts +++ b/test/moves/chloroblast.test.ts @@ -1,4 +1,4 @@ -import { MoveResult } from "#app/field/pokemon"; +import { MoveResult } from "#enums/move-result"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; 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 51d7d36535f..e11a2262a43 100644 --- a/test/moves/copycat.test.ts +++ b/test/moves/copycat.test.ts @@ -1,9 +1,9 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { RandomMoveAttr } from "#app/data/moves/move"; -import { allMoves } from "#app/data/data-lists"; import { Stat } from "#app/enums/stat"; -import { MoveResult } from "#app/field/pokemon"; +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) .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/destiny_bond.test.ts b/test/moves/destiny_bond.test.ts index 81c17551bec..a78d46b464b 100644 --- a/test/moves/destiny_bond.test.ts +++ b/test/moves/destiny_bond.test.ts @@ -1,5 +1,5 @@ import type { ArenaTrapTag } from "#app/data/arena-tag"; -import { ArenaTagSide } from "#app/data/arena-tag"; +import { ArenaTagSide } from "#enums/arena-tag-side"; import { allMoves } from "#app/data/data-lists"; import { AbilityId } from "#enums/ability-id"; import { ArenaTagType } from "#enums/arena-tag-type"; @@ -8,7 +8,7 @@ import { SpeciesId } from "#enums/species-id"; import GameManager from "#test/testUtils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { StatusEffect } from "#enums/status-effect"; import { PokemonInstantReviveModifier } from "#app/modifier/modifier"; diff --git a/test/moves/dig.test.ts b/test/moves/dig.test.ts index 052845ec50d..a80ac0405fe 100644 --- a/test/moves/dig.test.ts +++ b/test/moves/dig.test.ts @@ -1,11 +1,11 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { allMoves } from "#app/data/data-lists"; 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 { StatusEffect } from "#enums/status-effect"; -import { MoveResult } from "#app/field/pokemon"; +import { MoveResult } from "#enums/move-result"; import { describe, beforeAll, afterEach, beforeEach, it, expect } from "vitest"; import GameManager from "#test/testUtils/gameManager"; @@ -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 eacf8bf4857..b113acb9525 100644 --- a/test/moves/disable.test.ts +++ b/test/moves/disable.test.ts @@ -1,10 +1,13 @@ -import { BattlerIndex } from "#app/battle"; -import { MoveResult } from "#app/field/pokemon"; +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/dive.test.ts b/test/moves/dive.test.ts index cce1b5a6d26..9c467976775 100644 --- a/test/moves/dive.test.ts +++ b/test/moves/dive.test.ts @@ -1,6 +1,6 @@ import { BattlerTagType } from "#enums/battler-tag-type"; import { StatusEffect } from "#enums/status-effect"; -import { MoveResult } from "#app/field/pokemon"; +import { MoveResult } from "#enums/move-result"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; diff --git a/test/moves/doodle.test.ts b/test/moves/doodle.test.ts index 686bef144dd..d4d09b3a195 100644 --- a/test/moves/doodle.test.ts +++ b/test/moves/doodle.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { Stat } from "#app/enums/stat"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; diff --git a/test/moves/dragon_cheer.test.ts b/test/moves/dragon_cheer.test.ts index b1eaa3ad747..56feac513a1 100644 --- a/test/moves/dragon_cheer.test.ts +++ b/test/moves/dragon_cheer.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { PokemonType } from "#enums/pokemon-type"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; diff --git a/test/moves/dragon_rage.test.ts b/test/moves/dragon_rage.test.ts index c2a0a1bc979..f0be6748c7f 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(); @@ -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.disableCrits().enemyAbility(AbilityId.ICE_SCALES); game.move.select(MoveId.DRAGON_RAGE); await game.phaseInterceptor.to(TurnEndPhase); diff --git a/test/moves/dragon_tail.test.ts b/test/moves/dragon_tail.test.ts index 07441d9fb2d..8c456f27853 100644 --- a/test/moves/dragon_tail.test.ts +++ b/test/moves/dragon_tail.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { allMoves } from "#app/data/data-lists"; import { Status } from "#app/data/status-effect"; import { Challenges } from "#enums/challenges"; diff --git a/test/moves/dynamax_cannon.test.ts b/test/moves/dynamax_cannon.test.ts index 6207ef9b6ca..bf8a63f044c 100644 --- a/test/moves/dynamax_cannon.test.ts +++ b/test/moves/dynamax_cannon.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { allMoves } from "#app/data/data-lists"; import { DamageAnimPhase } from "#app/phases/damage-anim-phase"; import { MoveEffectPhase } from "#app/phases/move-effect-phase"; @@ -38,15 +38,7 @@ describe("Moves - Dynamax Cannon", () => { .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"); + vi.spyOn(allMoves[MoveId.DYNAMAX_CANNON], "calculateBattlePower"); }); it("should return 100 power against an enemy below level cap", async () => { @@ -59,7 +51,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 100 power against an enemy at level cap", async () => { game.override.enemyLevel(10); @@ -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/electrify.test.ts b/test/moves/electrify.test.ts index 00f96d570a3..b6a3cac9fff 100644 --- a/test/moves/electrify.test.ts +++ b/test/moves/electrify.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { PokemonType } from "#enums/pokemon-type"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; diff --git a/test/moves/electro_shot.test.ts b/test/moves/electro_shot.test.ts index 160f90163e3..1d69e9c2fa7 100644 --- a/test/moves/electro_shot.test.ts +++ b/test/moves/electro_shot.test.ts @@ -1,7 +1,7 @@ import { BattlerTagType } from "#enums/battler-tag-type"; import { Stat } from "#enums/stat"; import { WeatherType } from "#enums/weather-type"; -import { MoveResult } from "#app/field/pokemon"; +import { MoveResult } from "#enums/move-result"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; @@ -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 cded90c4a73..120d065d528 100644 --- a/test/moves/encore.test.ts +++ b/test/moves/encore.test.ts @@ -1,6 +1,6 @@ import { BattlerTagType } from "#enums/battler-tag-type"; -import { BattlerIndex } from "#app/battle"; -import { MoveResult } from "#app/field/pokemon"; +import { BattlerIndex } from "#enums/battler-index"; +import { MoveResult } from "#enums/move-result"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; diff --git a/test/moves/fairy_lock.test.ts b/test/moves/fairy_lock.test.ts index 5624d038595..74524d67b38 100644 --- a/test/moves/fairy_lock.test.ts +++ b/test/moves/fairy_lock.test.ts @@ -1,4 +1,4 @@ -import { ArenaTagSide } from "#app/data/arena-tag"; +import { ArenaTagSide } from "#enums/arena-tag-side"; import { ArenaTagType } from "#app/enums/arena-tag-type"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; diff --git a/test/moves/false_swipe.test.ts b/test/moves/false_swipe.test.ts index 48e4de6fb65..c98b76f1ef1 100644 --- a/test/moves/false_swipe.test.ts +++ b/test/moves/false_swipe.test.ts @@ -1,4 +1,4 @@ -import { MoveResult } from "#app/field/pokemon"; +import { MoveResult } from "#enums/move-result"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; 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..231eab427e1 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") + .disableCrits() + .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..e994dc3e568 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]) + .disableCrits() + .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/fly.test.ts b/test/moves/fly.test.ts index 964d1b65a1e..7d8a6ee659e 100644 --- a/test/moves/fly.test.ts +++ b/test/moves/fly.test.ts @@ -1,13 +1,13 @@ import { BattlerTagType } from "#enums/battler-tag-type"; import { StatusEffect } from "#enums/status-effect"; -import { MoveResult } from "#app/field/pokemon"; +import { MoveResult } from "#enums/move-result"; 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, it, expect, vi } from "vitest"; -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { allMoves } from "#app/data/data-lists"; describe("Moves - Fly", () => { diff --git a/test/moves/follow_me.test.ts b/test/moves/follow_me.test.ts index 567320a18e9..a99ac0b6c00 100644 --- a/test/moves/follow_me.test.ts +++ b/test/moves/follow_me.test.ts @@ -1,5 +1,5 @@ import { Stat } from "#enums/stat"; -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { AbilityId } from "#enums/ability-id"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { MoveId } from "#enums/move-id"; @@ -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/freeze_dry.test.ts b/test/moves/freeze_dry.test.ts index dc6af507b16..f1577d3d6c5 100644 --- a/test/moves/freeze_dry.test.ts +++ b/test/moves/freeze_dry.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; diff --git a/test/moves/fusion_bolt.test.ts b/test/moves/fusion_bolt.test.ts index a5263bfa364..cb3d75f56eb 100644 --- a/test/moves/fusion_bolt.test.ts +++ b/test/moves/fusion_bolt.test.ts @@ -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_bolt.test.ts b/test/moves/fusion_flare_bolt.test.ts index 1a5d9e44bab..f9e55b72f6c 100644 --- a/test/moves/fusion_flare_bolt.test.ts +++ b/test/moves/fusion_flare_bolt.test.ts @@ -1,5 +1,5 @@ import { Stat } from "#enums/stat"; -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { allMoves } from "#app/data/data-lists"; import type Move from "#app/data/moves/move"; import { DamageAnimPhase } from "#app/phases/damage-anim-phase"; @@ -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 6e06f441959..dbaa53dcb4f 100644 --- a/test/moves/gastro_acid.test.ts +++ b/test/moves/gastro_acid.test.ts @@ -1,8 +1,8 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; -import { MoveResult } from "#app/field/pokemon"; +import { MoveResult } from "#enums/move-result"; import GameManager from "#test/testUtils/gameManager"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; @@ -22,14 +22,15 @@ describe("Moves - Gastro Acid", () => { beforeEach(() => { game = new GameManager(phaserGame); - 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); + game.override + .battleStyle("double") + .startingLevel(1) + .enemyLevel(100) + .ability(AbilityId.NONE) + .moveset([MoveId.GASTRO_ACID, MoveId.WATER_GUN, MoveId.SPLASH, MoveId.CORE_ENFORCER]) + .enemySpecies(SpeciesId.BIDOOF) + .enemyMoveset(MoveId.SPLASH) + .enemyAbility(AbilityId.WATER_ABSORB); }); it("suppresses effect of ability", async () => { diff --git a/test/moves/geomancy.test.ts b/test/moves/geomancy.test.ts index 16244fed93f..452022b4cf1 100644 --- a/test/moves/geomancy.test.ts +++ b/test/moves/geomancy.test.ts @@ -1,6 +1,6 @@ import type { EffectiveStat } from "#enums/stat"; import { Stat } from "#enums/stat"; -import { MoveResult } from "#app/field/pokemon"; +import { MoveResult } from "#enums/move-result"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; diff --git a/test/moves/gigaton_hammer.test.ts b/test/moves/gigaton_hammer.test.ts index e61a383acc5..e8c9d316372 100644 --- a/test/moves/gigaton_hammer.test.ts +++ b/test/moves/gigaton_hammer.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import GameManager from "#test/testUtils/gameManager"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; @@ -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..2aead405fb6 100644 --- a/test/moves/glaive_rush.test.ts +++ b/test/moves/glaive_rush.test.ts @@ -28,13 +28,12 @@ describe("Moves - Glaive Rush", () => { .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 b6ef25d41ff..63018a419e3 100644 --- a/test/moves/grudge.test.ts +++ b/test/moves/grudge.test.ts @@ -1,7 +1,7 @@ import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import GameManager from "#test/testUtils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; 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 58e1a5e04a5..39e8efea827 100644 --- a/test/moves/heal_block.test.ts +++ b/test/moves/heal_block.test.ts @@ -1,5 +1,5 @@ -import { BattlerIndex } from "#app/battle"; -import { ArenaTagSide } from "#app/data/arena-tag"; +import { BattlerIndex } from "#enums/battler-index"; +import { ArenaTagSide } from "#enums/arena-tag-side"; import GameManager from "#test/testUtils/gameManager"; import { AbilityId } from "#enums/ability-id"; import { ArenaTagType } from "#enums/arena-tag-type"; 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 e0cfa93cf59..6167b6fc4dd 100644 --- a/test/moves/instruct.test.ts +++ b/test/moves/instruct.test.ts @@ -1,9 +1,11 @@ -import { BattlerIndex } from "#app/battle"; +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 "#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"; @@ -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 6084a47ad99..e0815a4c3c9 100644 --- a/test/moves/jaw_lock.test.ts +++ b/test/moves/jaw_lock.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { AbilityId } from "#enums/ability-id"; import { BattlerTagType } from "#app/enums/battler-tag-type"; import { BerryPhase } from "#app/phases/berry-phase"; diff --git a/test/moves/lash_out.test.ts b/test/moves/lash_out.test.ts index a63ab2a467c..c0c0881b340 100644 --- a/test/moves/lash_out.test.ts +++ b/test/moves/lash_out.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { allMoves } from "#app/data/data-lists"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; diff --git a/test/moves/last-resort.test.ts b/test/moves/last-resort.test.ts index 0e6c1a6ba15..3b57bc5cf61 100644 --- a/test/moves/last-resort.test.ts +++ b/test/moves/last-resort.test.ts @@ -1,6 +1,7 @@ -import { BattlerIndex } from "#app/battle"; -import { MoveResult } from "#app/field/pokemon"; +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"; @@ -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 96a8b5955d0..08256d3ebfb 100644 --- a/test/moves/last_respects.test.ts +++ b/test/moves/last_respects.test.ts @@ -1,5 +1,5 @@ import { MoveId } from "#enums/move-id"; -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { SpeciesId } from "#enums/species-id"; import { AbilityId } from "#enums/ability-id"; import GameManager from "#test/testUtils/gameManager"; @@ -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 4230790e688..404d30920c7 100644 --- a/test/moves/light_screen.test.ts +++ b/test/moves/light_screen.test.ts @@ -1,7 +1,6 @@ import type BattleScene from "#app/battle-scene"; -import { ArenaTagSide } from "#app/data/arena-tag"; +import { ArenaTagSide } from "#enums/arena-tag-side"; import type Move from "#app/data/moves/move"; -import { CritOnlyAttr } from "#app/data/moves/move"; import { allMoves } from "#app/data/data-lists"; import { AbilityId } from "#enums/ability-id"; import { ArenaTagType } from "#app/enums/arena-tag-type"; @@ -129,7 +128,7 @@ const getMockedMoveDamage = (defender: Pokemon, attacker: Pokemon, move: Move) = const side = defender.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; if (globalScene.arena.getTagOnSide(ArenaTagType.LIGHT_SCREEN, side)) { - if (move.getAttrs(CritOnlyAttr).length === 0) { + if (move.getAttrs("CritOnlyAttr").length === 0) { globalScene.arena.applyTagsForSide( ArenaTagType.LIGHT_SCREEN, side, 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 4f9e3977305..7bbd4779da6 100644 --- a/test/moves/magic_coat.test.ts +++ b/test/moves/magic_coat.test.ts @@ -1,11 +1,11 @@ -import { BattlerIndex } from "#app/battle"; -import { ArenaTagSide } from "#app/data/arena-tag"; +import { BattlerIndex } from "#enums/battler-index"; +import { ArenaTagSide } from "#enums/arena-tag-side"; import { allMoves } from "#app/data/data-lists"; import { ArenaTagType } from "#app/enums/arena-tag-type"; import { BattlerTagType } from "#app/enums/battler-tag-type"; import { Stat } from "#app/enums/stat"; import { StatusEffect } from "#app/enums/status-effect"; -import { MoveResult } from "#app/field/pokemon"; +import { MoveResult } from "#enums/move-result"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; @@ -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..496c0609411 100644 --- a/test/moves/magnet_rise.test.ts +++ b/test/moves/magnet_rise.test.ts @@ -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 0222dd222be..bcb9d938985 100644 --- a/test/moves/metal_burst.test.ts +++ b/test/moves/metal_burst.test.ts @@ -1,5 +1,5 @@ -import { BattlerIndex } from "#app/battle"; -import { MoveResult } from "#app/field/pokemon"; +import { BattlerIndex } from "#enums/battler-index"; +import { MoveResult } from "#enums/move-result"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; diff --git a/test/moves/metronome.test.ts b/test/moves/metronome.test.ts index 0e6db09ae5c..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 { RandomMoveAttr } from "#app/data/moves/move"; +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"; @@ -27,13 +31,12 @@ describe("Moves - Metronome", () => { }); beforeEach(() => { - randomMoveAttr = allMoves[MoveId.METRONOME].getAttrs(RandomMoveAttr)[0]; + randomMoveAttr = allMoves[MoveId.METRONOME].getAttrs("RandomMoveAttr")[0]; game = new GameManager(phaserGame); game.override .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 dd5fb1c355b..1238ae6a650 100644 --- a/test/moves/miracle_eye.test.ts +++ b/test/moves/miracle_eye.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { MoveEffectPhase } from "#app/phases/move-effect-phase"; diff --git a/test/moves/mirror_move.test.ts b/test/moves/mirror_move.test.ts index 18e115745b9..fe40402e5a2 100644 --- a/test/moves/mirror_move.test.ts +++ b/test/moves/mirror_move.test.ts @@ -1,6 +1,6 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { Stat } from "#app/enums/stat"; -import { MoveResult } from "#app/field/pokemon"; +import { MoveResult } from "#enums/move-result"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; diff --git a/test/moves/moongeist_beam.test.ts b/test/moves/moongeist_beam.test.ts index f462d81943f..28bd3f70daa 100644 --- a/test/moves/moongeist_beam.test.ts +++ b/test/moves/moongeist_beam.test.ts @@ -1,4 +1,3 @@ -import { RandomMoveAttr } from "#app/data/moves/move"; import { allMoves } from "#app/data/data-lists"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; @@ -49,7 +48,7 @@ describe("Moves - Moongeist Beam", () => { // Also covers Photon Geyser and Sunsteel Strike it("should not ignore enemy abilities when called by another move, such as metronome", async () => { await game.classicMode.startBattle([SpeciesId.MILOTIC]); - vi.spyOn(allMoves[MoveId.METRONOME].getAttrs(RandomMoveAttr)[0], "getMoveOverride").mockReturnValue( + vi.spyOn(allMoves[MoveId.METRONOME].getAttrs("RandomMoveAttr")[0], "getMoveOverride").mockReturnValue( MoveId.MOONGEIST_BEAM, ); diff --git a/test/moves/multi_target.test.ts b/test/moves/multi_target.test.ts index 4572097296c..139b669da7b 100644 --- a/test/moves/multi_target.test.ts +++ b/test/moves/multi_target.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { AbilityId } from "#enums/ability-id"; import { SpeciesId } from "#enums/species-id"; import { toDmgValue } from "#app/utils/common"; diff --git a/test/moves/order_up.test.ts b/test/moves/order_up.test.ts index c6ca7ac06da..adf37c0719e 100644 --- a/test/moves/order_up.test.ts +++ b/test/moves/order_up.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { BattlerTagType } from "#enums/battler-tag-type"; import { PokemonAnimType } from "#enums/pokemon-anim-type"; import type { EffectiveStat } from "#enums/stat"; 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/plasma_fists.test.ts b/test/moves/plasma_fists.test.ts index 9493f69d70f..7d1985be13e 100644 --- a/test/moves/plasma_fists.test.ts +++ b/test/moves/plasma_fists.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { PokemonType } from "#enums/pokemon-type"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; diff --git a/test/moves/pledge_moves.test.ts b/test/moves/pledge_moves.test.ts index 2500563d44e..f653e245f6f 100644 --- a/test/moves/pledge_moves.test.ts +++ b/test/moves/pledge_moves.test.ts @@ -1,7 +1,6 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { allAbilities } from "#app/data/data-lists"; -import { ArenaTagSide } from "#app/data/arena-tag"; -import { FlinchAttr } from "#app/data/moves/move"; +import { ArenaTagSide } from "#enums/arena-tag-side"; import { allMoves } from "#app/data/data-lists"; import { PokemonType } from "#enums/pokemon-type"; import { ArenaTagType } from "#enums/arena-tag-type"; @@ -228,7 +227,7 @@ describe("Moves - Pledge Moves", () => { await game.classicMode.startBattle([SpeciesId.BLASTOISE, SpeciesId.CHARIZARD]); - const ironHeadFlinchAttr = allMoves[MoveId.IRON_HEAD].getAttrs(FlinchAttr)[0]; + const ironHeadFlinchAttr = allMoves[MoveId.IRON_HEAD].getAttrs("FlinchAttr")[0]; vi.spyOn(ironHeadFlinchAttr, "getMoveChance"); game.move.select(MoveId.WATER_PLEDGE, 0, BattlerIndex.ENEMY); diff --git a/test/moves/pollen_puff.test.ts b/test/moves/pollen_puff.test.ts index d61303bcfcc..e6dcd2c41d0 100644 --- a/test/moves/pollen_puff.test.ts +++ b/test/moves/pollen_puff.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; diff --git a/test/moves/powder.test.ts b/test/moves/powder.test.ts index d335e29996e..8f47f2ff428 100644 --- a/test/moves/powder.test.ts +++ b/test/moves/powder.test.ts @@ -1,11 +1,10 @@ -import { BattlerIndex } from "#app/battle"; -import { MoveResult, PokemonMove } from "#app/field/pokemon"; +import { BattlerIndex } from "#enums/battler-index"; +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"; @@ -27,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) @@ -44,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(); @@ -60,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 () => { @@ -167,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); @@ -181,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 519021023fa..601afb43ec3 100644 --- a/test/moves/protect.test.ts +++ b/test/moves/protect.test.ts @@ -6,9 +6,10 @@ import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { Stat } from "#enums/stat"; import { allMoves } from "#app/data/data-lists"; -import { ArenaTagSide, ArenaTrapTag } from "#app/data/arena-tag"; -import { BattlerIndex } from "#app/battle"; -import { MoveResult } from "#app/field/pokemon"; +import { ArenaTrapTag } from "#app/data/arena-tag"; +import { ArenaTagSide } from "#enums/arena-tag-side"; +import { BattlerIndex } from "#enums/battler-index"; +import { MoveResult } from "#enums/move-result"; describe("Moves - Protect", () => { let phaserGame: Phaser.Game; @@ -27,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/purify.test.ts b/test/moves/purify.test.ts index cab0a70818f..2e8e312f23c 100644 --- a/test/moves/purify.test.ts +++ b/test/moves/purify.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { Status } from "#app/data/status-effect"; import type { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon"; import { MoveEndPhase } from "#app/phases/move-end-phase"; @@ -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 88cb0aba31f..158c409cf05 100644 --- a/test/moves/quash.test.ts +++ b/test/moves/quash.test.ts @@ -1,12 +1,13 @@ import { SpeciesId } from "#enums/species-id"; import { MoveId } from "#enums/move-id"; import { AbilityId } from "#enums/ability-id"; -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { WeatherType } from "#enums/weather-type"; -import { MoveResult } from "#app/field/pokemon"; +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 0c0ab8d6ad0..7146b804ffa 100644 --- a/test/moves/quick_guard.test.ts +++ b/test/moves/quick_guard.test.ts @@ -5,8 +5,8 @@ import { SpeciesId } from "#enums/species-id"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { Stat } from "#enums/stat"; -import { BattlerIndex } from "#app/battle"; -import { MoveResult } from "#app/field/pokemon"; +import { BattlerIndex } from "#enums/battler-index"; +import { MoveResult } from "#enums/move-result"; describe("Moves - Quick Guard", () => { let phaserGame: Phaser.Game; @@ -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_fist.test.ts b/test/moves/rage_fist.test.ts index cd9147637a5..31dd987cb87 100644 --- a/test/moves/rage_fist.test.ts +++ b/test/moves/rage_fist.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; diff --git a/test/moves/rage_powder.test.ts b/test/moves/rage_powder.test.ts index e3212e9876c..845bff33d04 100644 --- a/test/moves/rage_powder.test.ts +++ b/test/moves/rage_powder.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; @@ -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 d13fb7a095c..5e10de42a3c 100644 --- a/test/moves/reflect.test.ts +++ b/test/moves/reflect.test.ts @@ -1,7 +1,6 @@ import type BattleScene from "#app/battle-scene"; -import { ArenaTagSide } from "#app/data/arena-tag"; +import { ArenaTagSide } from "#enums/arena-tag-side"; import type Move from "#app/data/moves/move"; -import { CritOnlyAttr } from "#app/data/moves/move"; import { allMoves } from "#app/data/data-lists"; import { AbilityId } from "#enums/ability-id"; import { ArenaTagType } from "#app/enums/arena-tag-type"; @@ -145,7 +144,7 @@ const getMockedMoveDamage = (defender: Pokemon, attacker: Pokemon, move: Move) = const side = defender.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; if (globalScene.arena.getTagOnSide(ArenaTagType.REFLECT, side)) { - if (move.getAttrs(CritOnlyAttr).length === 0) { + if (move.getAttrs("CritOnlyAttr").length === 0) { globalScene.arena.applyTagsForSide(ArenaTagType.REFLECT, side, false, attacker, move.category, multiplierHolder); } } diff --git a/test/moves/revival_blessing.test.ts b/test/moves/revival_blessing.test.ts index 07ebf657c66..f22c0467378 100644 --- a/test/moves/revival_blessing.test.ts +++ b/test/moves/revival_blessing.test.ts @@ -1,5 +1,5 @@ -import { BattlerIndex } from "#app/battle"; -import { MoveResult } from "#app/field/pokemon"; +import { BattlerIndex } from "#enums/battler-index"; +import { MoveResult } from "#enums/move-result"; import { toDmgValue } from "#app/utils/common"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; diff --git a/test/moves/rollout.test.ts b/test/moves/rollout.test.ts index de990a1a750..b9faf438ae5 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 + .disableCrits() + .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 76aab1e572f..3707e0ead45 100644 --- a/test/moves/roost.test.ts +++ b/test/moves/roost.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { PokemonType } from "#enums/pokemon-type"; import { BattlerTagType } from "#app/enums/battler-tag-type"; import { MoveId } from "#enums/move-id"; @@ -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 503ce125582..630137b7dce 100644 --- a/test/moves/round.test.ts +++ b/test/moves/round.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { allMoves } from "#app/data/data-lists"; import type { MoveEffectPhase } from "#app/phases/move-effect-phase"; import { AbilityId } from "#enums/ability-id"; diff --git a/test/moves/safeguard.test.ts b/test/moves/safeguard.test.ts index fc8bef80d6d..8d5303e3feb 100644 --- a/test/moves/safeguard.test.ts +++ b/test/moves/safeguard.test.ts @@ -1,13 +1,12 @@ -import { BattlerIndex } from "#app/battle"; -import { PostDefendContactApplyStatusEffectAbAttr } from "#app/data/abilities/ability"; +import { BattlerIndex } from "#enums/battler-index"; 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 f61a9a1e276..a5872579003 100644 --- a/test/moves/scale_shot.test.ts +++ b/test/moves/scale_shot.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { allMoves } from "#app/data/data-lists"; import { DamageAnimPhase } from "#app/phases/damage-anim-phase"; import { MoveEffectPhase } from "#app/phases/move-effect-phase"; diff --git a/test/moves/secret_power.test.ts b/test/moves/secret_power.test.ts index ac06f03698c..39498782b58 100644 --- a/test/moves/secret_power.test.ts +++ b/test/moves/secret_power.test.ts @@ -8,10 +8,9 @@ import GameManager from "#test/testUtils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { StatusEffect } from "#enums/status-effect"; -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { ArenaTagType } from "#enums/arena-tag-type"; -import { ArenaTagSide } from "#app/data/arena-tag"; -import { MoveEffectChanceMultiplierAbAttr } from "#app/data/abilities/ability"; +import { ArenaTagSide } from "#enums/arena-tag-side"; import { allAbilities } from "#app/data/data-lists"; describe("Moves - Secret Power", () => { @@ -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/shed_tail.test.ts b/test/moves/shed_tail.test.ts index 84a5d5ba914..81ab9215bc9 100644 --- a/test/moves/shed_tail.test.ts +++ b/test/moves/shed_tail.test.ts @@ -1,5 +1,5 @@ import { SubstituteTag } from "#app/data/battler-tags"; -import { MoveResult } from "#app/field/pokemon"; +import { MoveResult } from "#enums/move-result"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; diff --git a/test/moves/shell_side_arm.test.ts b/test/moves/shell_side_arm.test.ts index ad79091a5e3..35246e10e3f 100644 --- a/test/moves/shell_side_arm.test.ts +++ b/test/moves/shell_side_arm.test.ts @@ -1,5 +1,5 @@ -import { BattlerIndex } from "#app/battle"; -import { ShellSideArmCategoryAttr } from "#app/data/moves/move"; +import { BattlerIndex } from "#enums/battler-index"; +import type { ShellSideArmCategoryAttr } from "#app/data/moves/move"; import { allMoves } from "#app/data/data-lists"; import type Move from "#app/data/moves/move"; import { AbilityId } from "#enums/ability-id"; @@ -27,7 +27,7 @@ describe("Moves - Shell Side Arm", () => { beforeEach(() => { shellSideArm = allMoves[MoveId.SHELL_SIDE_ARM]; - shellSideArmAttr = shellSideArm.getAttrs(ShellSideArmCategoryAttr)[0]; + shellSideArmAttr = shellSideArm.getAttrs("ShellSideArmCategoryAttr")[0]; game = new GameManager(phaserGame); game.override .moveset([MoveId.SHELL_SIDE_ARM, MoveId.SPLASH]) diff --git a/test/moves/shell_trap.test.ts b/test/moves/shell_trap.test.ts index 9a38bf4486b..a3f55cef10f 100644 --- a/test/moves/shell_trap.test.ts +++ b/test/moves/shell_trap.test.ts @@ -1,8 +1,8 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { allMoves } from "#app/data/data-lists"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; -import { MoveResult } from "#app/field/pokemon"; +import { MoveResult } from "#enums/move-result"; import { BerryPhase } from "#app/phases/berry-phase"; import { MoveEndPhase } from "#app/phases/move-end-phase"; import { MovePhase } from "#app/phases/move-phase"; diff --git a/test/moves/sketch.test.ts b/test/moves/sketch.test.ts index 23e7f4ef3ab..c6fb7b4a32a 100644 --- a/test/moves/sketch.test.ts +++ b/test/moves/sketch.test.ts @@ -1,12 +1,13 @@ import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; -import { MoveResult, PokemonMove } from "#app/field/pokemon"; +import { PokemonMove } from "#app/data/moves/pokemon-move"; +import { MoveResult } from "#enums/move-result"; import GameManager from "#test/testUtils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { StatusEffect } from "#app/enums/status-effect"; -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { RandomMoveAttr } from "#app/data/moves/move"; import { allMoves } from "#app/data/data-lists"; diff --git a/test/moves/sleep_talk.test.ts b/test/moves/sleep_talk.test.ts index 820a5f9082a..1d9aec77ea2 100644 --- a/test/moves/sleep_talk.test.ts +++ b/test/moves/sleep_talk.test.ts @@ -1,6 +1,6 @@ import { Stat } from "#app/enums/stat"; import { StatusEffect } from "#app/enums/status-effect"; -import { MoveResult } from "#app/field/pokemon"; +import { MoveResult } from "#enums/move-result"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; diff --git a/test/moves/solar_beam.test.ts b/test/moves/solar_beam.test.ts index 7515843a13f..55d2ee5d13c 100644 --- a/test/moves/solar_beam.test.ts +++ b/test/moves/solar_beam.test.ts @@ -1,7 +1,7 @@ import { allMoves } from "#app/data/data-lists"; import { BattlerTagType } from "#enums/battler-tag-type"; import { WeatherType } from "#enums/weather-type"; -import { MoveResult } from "#app/field/pokemon"; +import { MoveResult } from "#enums/move-result"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; diff --git a/test/moves/spectral_thief.test.ts b/test/moves/spectral_thief.test.ts index df560169078..808e9174caf 100644 --- a/test/moves/spectral_thief.test.ts +++ b/test/moves/spectral_thief.test.ts @@ -1,5 +1,5 @@ import { AbilityId } from "#enums/ability-id"; -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { Stat } from "#enums/stat"; import { allMoves } from "#app/data/data-lists"; import { MoveId } from "#enums/move-id"; 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 278c4510239..49492701d18 100644 --- a/test/moves/spikes.test.ts +++ b/test/moves/spikes.test.ts @@ -4,7 +4,8 @@ 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 { ArenaTagSide, ArenaTrapTag } from "#app/data/arena-tag"; +import { ArenaTrapTag } from "#app/data/arena-tag"; +import { ArenaTagSide } from "#enums/arena-tag-side"; describe("Moves - Spikes", () => { let phaserGame: Phaser.Game; @@ -48,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); @@ -62,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); @@ -77,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); @@ -90,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 2d576168f4c..00cbe2fd0ad 100644 --- a/test/moves/spit_up.test.ts +++ b/test/moves/spit_up.test.ts @@ -2,8 +2,7 @@ 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 "#app/field/pokemon"; +import { MoveResult } from "#enums/move-result"; import GameManager from "#test/testUtils/gameManager"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-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 2798dfa282a..a9e8cd7e2b6 100644 --- a/test/moves/spotlight.test.ts +++ b/test/moves/spotlight.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; @@ -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/steamroller.test.ts b/test/moves/steamroller.test.ts index f4f8131ff7b..4eb011c47f5 100644 --- a/test/moves/steamroller.test.ts +++ b/test/moves/steamroller.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { allMoves } from "#app/data/data-lists"; import { BattlerTagType } from "#app/enums/battler-tag-type"; import type { DamageCalculationResult } from "#app/field/pokemon"; diff --git a/test/moves/stockpile.test.ts b/test/moves/stockpile.test.ts index 5bf2b74d4d9..3ad47d8d85e 100644 --- a/test/moves/stockpile.test.ts +++ b/test/moves/stockpile.test.ts @@ -1,7 +1,6 @@ import { Stat } from "#enums/stat"; import { StockpilingTag } from "#app/data/battler-tags"; -import type { TurnMove } from "#app/field/pokemon"; -import { MoveResult } 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"; import { AbilityId } from "#enums/ability-id"; @@ -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..2175dc557b3 100644 --- a/test/moves/struggle.test.ts +++ b/test/moves/struggle.test.ts @@ -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 454e729a67b..182fefb74d7 100644 --- a/test/moves/substitute.test.ts +++ b/test/moves/substitute.test.ts @@ -1,12 +1,12 @@ -import { BattlerIndex } from "#app/battle"; -import { ArenaTagSide } from "#app/data/arena-tag"; +import { BattlerIndex } from "#enums/battler-index"; +import { ArenaTagSide } from "#enums/arena-tag-side"; import { SubstituteTag, TrappedTag } from "#app/data/battler-tags"; import { StealHeldItemChanceAttr } from "#app/data/moves/move"; import { allMoves } from "#app/data/data-lists"; -import { MoveResult } from "#app/field/pokemon"; +import { MoveResult } from "#enums/move-result"; import type { CommandPhase } from "#app/phases/command-phase"; import GameManager from "#test/testUtils/gameManager"; -import { Command } from "#app/ui/command-ui-handler"; +import { Command } from "#enums/command"; import { UiMode } from "#enums/ui-mode"; import { AbilityId } from "#enums/ability-id"; import { ArenaTagType } from "#enums/arena-tag-type"; @@ -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 4452636c3af..c511682011f 100644 --- a/test/moves/swallow.test.ts +++ b/test/moves/swallow.test.ts @@ -1,8 +1,7 @@ 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 "#app/field/pokemon"; +import { MoveResult } from "#enums/move-result"; import { MovePhase } from "#app/phases/move-phase"; import { TurnInitPhase } from "#app/phases/turn-init-phase"; import { AbilityId } from "#enums/ability-id"; @@ -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/syrup_bomb.test.ts b/test/moves/syrup_bomb.test.ts index 76bc7e7bae0..4b2821b439c 100644 --- a/test/moves/syrup_bomb.test.ts +++ b/test/moves/syrup_bomb.test.ts @@ -5,7 +5,7 @@ import { BattlerTagType } from "#enums/battler-tag-type"; import { Stat } from "#enums/stat"; import GameManager from "#test/testUtils/gameManager"; import Phaser from "phaser"; -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; describe("Moves - SYRUP BOMB", () => { diff --git a/test/moves/tackle.test.ts b/test/moves/tackle.test.ts index 83a267dc7e4..96faa27ad38 100644 --- a/test/moves/tackle.test.ts +++ b/test/moves/tackle.test.ts @@ -37,13 +37,14 @@ describe("Moves - Tackle", () => { 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/tailwind.test.ts b/test/moves/tailwind.test.ts index 83078d7bf58..874934fc8f3 100644 --- a/test/moves/tailwind.test.ts +++ b/test/moves/tailwind.test.ts @@ -1,4 +1,4 @@ -import { ArenaTagSide } from "#app/data/arena-tag"; +import { ArenaTagSide } from "#enums/arena-tag-side"; import { ArenaTagType } from "#app/enums/arena-tag-type"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; diff --git a/test/moves/tar_shot.test.ts b/test/moves/tar_shot.test.ts index 21eace0ef1f..fefd1a30e1e 100644 --- a/test/moves/tar_shot.test.ts +++ b/test/moves/tar_shot.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { PokemonType } from "#enums/pokemon-type"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; diff --git a/test/moves/taunt.test.ts b/test/moves/taunt.test.ts index e214bd77ef7..fabb95f98b2 100644 --- a/test/moves/taunt.test.ts +++ b/test/moves/taunt.test.ts @@ -4,7 +4,7 @@ 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 { MoveResult } from "#app/field/pokemon"; +import { MoveResult } from "#enums/move-result"; import { BattlerTagType } from "#enums/battler-tag-type"; describe("Moves - Taunt", () => { diff --git a/test/moves/telekinesis.test.ts b/test/moves/telekinesis.test.ts index 18df17f1587..5c9f1e22395 100644 --- a/test/moves/telekinesis.test.ts +++ b/test/moves/telekinesis.test.ts @@ -3,11 +3,11 @@ import { allMoves } from "#app/data/data-lists"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; -import { MoveResult } from "#app/field/pokemon"; +import { MoveResult } from "#enums/move-result"; import GameManager from "#test/testUtils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, it, expect, vi } from "vitest"; -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; describe("Moves - Telekinesis", () => { let phaserGame: Phaser.Game; diff --git a/test/moves/tera_blast.test.ts b/test/moves/tera_blast.test.ts index 4e3f9c6869b..3372fd21539 100644 --- a/test/moves/tera_blast.test.ts +++ b/test/moves/tera_blast.test.ts @@ -1,6 +1,6 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { Stat } from "#enums/stat"; -import { TeraMoveCategoryAttr } from "#app/data/moves/move"; +import type { TeraMoveCategoryAttr } from "#app/data/moves/move"; import { allMoves } from "#app/data/data-lists"; import type Move from "#app/data/moves/move"; import { PokemonType } from "#enums/pokemon-type"; @@ -23,7 +23,7 @@ describe("Moves - Tera Blast", () => { type: Phaser.HEADLESS, }); moveToCheck = allMoves[MoveId.TERA_BLAST]; - teraBlastAttr = moveToCheck.getAttrs(TeraMoveCategoryAttr)[0]; + teraBlastAttr = moveToCheck.getAttrs("TeraMoveCategoryAttr")[0]; }); afterEach(() => { @@ -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/tera_starstorm.test.ts b/test/moves/tera_starstorm.test.ts index bd1fa14398d..2eaf2ec0372 100644 --- a/test/moves/tera_starstorm.test.ts +++ b/test/moves/tera_starstorm.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { PokemonType } from "#enums/pokemon-type"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; diff --git a/test/moves/thousand_arrows.test.ts b/test/moves/thousand_arrows.test.ts index 428e7c4fbe4..9ecdd94a94f 100644 --- a/test/moves/thousand_arrows.test.ts +++ b/test/moves/thousand_arrows.test.ts @@ -24,12 +24,13 @@ describe("Moves - Thousand Arrows", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleStyle("single"); - game.override.enemySpecies(SpeciesId.TOGETIC); - game.override.startingLevel(100); - game.override.enemyLevel(100); - game.override.moveset([MoveId.THOUSAND_ARROWS]); - game.override.enemyMoveset([MoveId.SPLASH, MoveId.SPLASH, MoveId.SPLASH, MoveId.SPLASH]); + game.override + .battleStyle("single") + .enemySpecies(SpeciesId.TOGETIC) + .startingLevel(100) + .enemyLevel(100) + .moveset([MoveId.THOUSAND_ARROWS]) + .enemyMoveset(MoveId.SPLASH); }); it("move should hit and ground Flying-type targets", async () => { @@ -50,8 +51,7 @@ describe("Moves - Thousand Arrows", () => { }); it("move should hit and ground targets with Levitate", async () => { - game.override.enemySpecies(SpeciesId.SNORLAX); - game.override.enemyAbility(AbilityId.LEVITATE); + game.override.enemySpecies(SpeciesId.SNORLAX).enemyAbility(AbilityId.LEVITATE); await game.classicMode.startBattle([SpeciesId.ILLUMISE]); diff --git a/test/moves/throat_chop.test.ts b/test/moves/throat_chop.test.ts index c1c9c4e94ad..aa92f657c86 100644 --- a/test/moves/throat_chop.test.ts +++ b/test/moves/throat_chop.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { Stat } from "#app/enums/stat"; 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/torment.test.ts b/test/moves/torment.test.ts index b35b16249ef..e4d926d9601 100644 --- a/test/moves/torment.test.ts +++ b/test/moves/torment.test.ts @@ -4,7 +4,7 @@ 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 { MoveResult } from "#app/field/pokemon"; +import { MoveResult } from "#enums/move-result"; import { BattlerTagType } from "#enums/battler-tag-type"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; diff --git a/test/moves/toxic.test.ts b/test/moves/toxic.test.ts index 5cdfe78d502..a7ba52eb963 100644 --- a/test/moves/toxic.test.ts +++ b/test/moves/toxic.test.ts @@ -4,7 +4,7 @@ import GameManager from "#test/testUtils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { StatusEffect } from "#enums/status-effect"; -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { allMoves } from "#app/data/data-lists"; describe("Moves - Toxic", () => { @@ -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/toxic_spikes.test.ts b/test/moves/toxic_spikes.test.ts index 11ed7514633..bf1f5acc1cb 100644 --- a/test/moves/toxic_spikes.test.ts +++ b/test/moves/toxic_spikes.test.ts @@ -1,7 +1,8 @@ import type { ArenaTrapTag } from "#app/data/arena-tag"; -import { ArenaTagSide } from "#app/data/arena-tag"; +import { ArenaTagSide } from "#enums/arena-tag-side"; import type { SessionSaveData } from "#app/system/game-data"; -import { decrypt, encrypt, GameData } from "#app/system/game-data"; +import { GameData } from "#app/system/game-data"; +import { decrypt, encrypt } from "#app/utils/data"; import { AbilityId } from "#enums/ability-id"; import { ArenaTagType } from "#enums/arena-tag-type"; import { MoveId } from "#enums/move-id"; diff --git a/test/moves/transform.test.ts b/test/moves/transform.test.ts index ca326da5748..4fbaf0136ab 100644 --- a/test/moves/transform.test.ts +++ b/test/moves/transform.test.ts @@ -6,7 +6,7 @@ import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { MoveId } from "#enums/move-id"; import { Stat, EFFECTIVE_STATS } from "#enums/stat"; import { AbilityId } from "#enums/ability-id"; -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; // TODO: Add more tests once Transform is fully implemented describe("Moves - Transform", () => { @@ -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/triple_arrows.test.ts b/test/moves/triple_arrows.test.ts index 6a14a7642fa..89ccb4e5b04 100644 --- a/test/moves/triple_arrows.test.ts +++ b/test/moves/triple_arrows.test.ts @@ -1,4 +1,4 @@ -import { FlinchAttr, StatStageChangeAttr } from "#app/data/moves/move"; +import type { FlinchAttr, StatStageChangeAttr } from "#app/data/moves/move"; import { allMoves } from "#app/data/data-lists"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; @@ -20,8 +20,8 @@ describe("Moves - Triple Arrows", () => { type: Phaser.HEADLESS, }); tripleArrows = allMoves[MoveId.TRIPLE_ARROWS]; - flinchAttr = tripleArrows.getAttrs(FlinchAttr)[0]; - defDropAttr = tripleArrows.getAttrs(StatStageChangeAttr)[0]; + flinchAttr = tripleArrows.getAttrs("FlinchAttr")[0]; + defDropAttr = tripleArrows.getAttrs("StatStageChangeAttr")[0]; }); afterEach(() => { diff --git a/test/moves/u_turn.test.ts b/test/moves/u_turn.test.ts index 4212802298e..5d4cad3a5ad 100644 --- a/test/moves/u_turn.test.ts +++ b/test/moves/u_turn.test.ts @@ -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 741594c7e47..e3d490ba6fa 100644 --- a/test/moves/upper_hand.test.ts +++ b/test/moves/upper_hand.test.ts @@ -1,5 +1,5 @@ -import { BattlerIndex } from "#app/battle"; -import { MoveResult } from "#app/field/pokemon"; +import { BattlerIndex } from "#enums/battler-index"; +import { MoveResult } from "#enums/move-result"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; diff --git a/test/moves/whirlwind.test.ts b/test/moves/whirlwind.test.ts index c457bdb67d7..00d7c16561c 100644 --- a/test/moves/whirlwind.test.ts +++ b/test/moves/whirlwind.test.ts @@ -1,7 +1,7 @@ import { BattlerTagType } from "#enums/battler-tag-type"; import { Challenges } from "#enums/challenges"; import { PokemonType } from "#enums/pokemon-type"; -import { MoveResult } from "#app/field/pokemon"; +import { MoveResult } from "#enums/move-result"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; @@ -11,7 +11,7 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vite import { Status } from "#app/data/status-effect"; import { StatusEffect } from "#enums/status-effect"; import { globalScene } from "#app/global-scene"; -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { BattleType } from "#enums/battle-type"; import { TrainerType } from "#enums/trainer-type"; 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 ce747dbad0b..40495009d2a 100644 --- a/test/moves/will_o_wisp.test.ts +++ b/test/moves/will_o_wisp.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; 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 786090aa8d6..4b0f1d50b74 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 @@ -8,7 +8,7 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vite import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { runMysteryEncounterToEnd } from "#test/mystery-encounter/encounter-test-utils"; import type BattleScene from "#app/battle-scene"; -import { PokemonMove } from "#app/field/pokemon"; +import { PokemonMove } from "#app/data/moves/pokemon-move"; import { AnOfferYouCantRefuseEncounter } from "#app/data/mystery-encounters/encounters/an-offer-you-cant-refuse-encounter"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; @@ -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 7e569d0cdf7..4da8ff7f643 100644 --- a/test/mystery-encounter/encounters/bug-type-superfan-encounter.test.ts +++ b/test/mystery-encounter/encounters/bug-type-superfan-encounter.test.ts @@ -11,7 +11,7 @@ import { } from "#test/mystery-encounter/encounter-test-utils"; import { MoveId } from "#enums/move-id"; import type BattleScene from "#app/battle-scene"; -import { PokemonMove } from "#app/field/pokemon"; +import { PokemonMove } from "#app/data/moves/pokemon-move"; import { UiMode } from "#enums/ui-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; @@ -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 fc0ef0e5a86..738b3a54067 100644 --- a/test/mystery-encounter/encounters/clowning-around-encounter.test.ts +++ b/test/mystery-encounter/encounters/clowning-around-encounter.test.ts @@ -15,12 +15,12 @@ import { import { MoveId } from "#enums/move-id"; import type BattleScene from "#app/battle-scene"; import type Pokemon from "#app/field/pokemon"; -import { PokemonMove } from "#app/field/pokemon"; +import { PokemonMove } from "#app/data/moves/pokemon-move"; 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 76a562b5851..e47c7cc1a42 100644 --- a/test/mystery-encounter/encounters/dancing-lessons-encounter.test.ts +++ b/test/mystery-encounter/encounters/dancing-lessons-encounter.test.ts @@ -17,7 +17,7 @@ import { MoveId } from "#enums/move-id"; import { DancingLessonsEncounter } from "#app/data/mystery-encounters/encounters/dancing-lessons-encounter"; import { UiMode } from "#enums/ui-mode"; import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; -import { PokemonMove } from "#app/field/pokemon"; +import { PokemonMove } from "#app/data/moves/pokemon-move"; import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; import { CommandPhase } from "#app/phases/command-phase"; import { MovePhase } from "#app/phases/move-phase"; @@ -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..5e6b8507a91 100644 --- a/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts +++ b/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts @@ -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 7038fff3117..28db869004c 100644 --- a/test/mystery-encounter/encounters/fight-or-flight-encounter.test.ts +++ b/test/mystery-encounter/encounters/fight-or-flight-encounter.test.ts @@ -11,7 +11,7 @@ import { } from "#test/mystery-encounter/encounter-test-utils"; import { MoveId } from "#enums/move-id"; import type BattleScene from "#app/battle-scene"; -import { PokemonMove } from "#app/field/pokemon"; +import { PokemonMove } from "#app/data/moves/pokemon-move"; import { UiMode } from "#enums/ui-mode"; import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; @@ -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 4eaf2cef1da..ae7e269ea00 100644 --- a/test/mystery-encounter/encounters/fun-and-games-encounter.test.ts +++ b/test/mystery-encounter/encounters/fun-and-games-encounter.test.ts @@ -22,8 +22,9 @@ import { CommandPhase } from "#app/phases/command-phase"; import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; import { FunAndGamesEncounter } from "#app/data/mystery-encounters/encounters/fun-and-games-encounter"; import { MoveId } from "#enums/move-id"; -import { Command } from "#app/ui/command-ui-handler"; +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..aa367277ac6 100644 --- a/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts +++ b/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts @@ -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 fa85f373a35..d36b387cae1 100644 --- a/test/mystery-encounter/encounters/part-timer-encounter.test.ts +++ b/test/mystery-encounter/encounters/part-timer-encounter.test.ts @@ -14,7 +14,7 @@ import { CIVILIZATION_ENCOUNTER_BIOMES } from "#app/data/mystery-encounters/myst import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { PartTimerEncounter } from "#app/data/mystery-encounters/encounters/part-timer-encounter"; -import { PokemonMove } from "#app/field/pokemon"; +import { PokemonMove } from "#app/data/moves/pokemon-move"; import { MoveId } from "#enums/move-id"; import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; @@ -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 e50a19a0a80..d8626a7bf8a 100644 --- a/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts +++ b/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts @@ -17,7 +17,7 @@ import { TheStrongStuffEncounter } from "#app/data/mystery-encounters/encounters import { Nature } from "#enums/nature"; import { BerryType } from "#enums/berry-type"; import { BattlerTagType } from "#enums/battler-tag-type"; -import { PokemonMove } from "#app/field/pokemon"; +import { PokemonMove } from "#app/data/moves/pokemon-move"; import { UiMode } from "#enums/ui-mode"; import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; import { BerryModifier, PokemonBaseStatTotalModifier } from "#app/modifier/modifier"; @@ -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..87caa6ccd40 100644 --- a/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts +++ b/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts @@ -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 999936c8832..2bffa26ff4a 100644 --- a/test/mystery-encounter/encounters/trash-to-treasure-encounter.test.ts +++ b/test/mystery-encounter/encounters/trash-to-treasure-encounter.test.ts @@ -12,10 +12,11 @@ import { getPokemonSpecies } from "#app/data/pokemon-species"; import { BiomeId } from "#enums/biome-id"; import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; import { SpeciesId } from "#enums/species-id"; -import { PokemonMove } from "#app/field/pokemon"; +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 0c3131de1d2..65c266a5a6c 100644 --- a/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts +++ b/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts @@ -10,7 +10,7 @@ import { } from "#test/mystery-encounter/encounter-test-utils"; import { MoveId } from "#enums/move-id"; import type BattleScene from "#app/battle-scene"; -import { PokemonMove } from "#app/field/pokemon"; +import { PokemonMove } from "#app/data/moves/pokemon-move"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { initSceneWithoutEncounterPhase } from "#test/testUtils/gameManagerUtils"; @@ -26,7 +26,7 @@ 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..f327d8f9e9c 100644 --- a/test/mystery-encounter/mystery-encounter-utils.test.ts +++ b/test/mystery-encounter/mystery-encounter-utils.test.ts @@ -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..99c072bdafe 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; diff --git a/test/phases/frenzy-move-reset.test.ts b/test/phases/frenzy-move-reset.test.ts index 1879a14d301..878c58d8666 100644 --- a/test/phases/frenzy-move-reset.test.ts +++ b/test/phases/frenzy-move-reset.test.ts @@ -1,4 +1,4 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { AbilityId } from "#enums/ability-id"; import { BattlerTagType } from "#enums/battler-tag-type"; import { StatusEffect } from "#enums/status-effect"; diff --git a/test/phases/game-over-phase.test.ts b/test/phases/game-over-phase.test.ts index c430223b774..d45330697fc 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; 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..6e92861260e 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 { 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 6c535ca4722..8e39df23f47 100644 --- a/test/reload.test.ts +++ b/test/reload.test.ts @@ -1,4 +1,4 @@ -import { GameModes } from "#app/game-mode"; +import { GameModes } from "#enums/game-modes"; import { pokerogueApi } from "#app/plugins/api/pokerogue-api"; import type OptionSelectUiHandler from "#app/ui/settings/option-select-ui-handler"; import { UiMode } from "#enums/ui-mode"; @@ -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 437c8d9f083..b9f499d4e0c 100644 --- a/test/testUtils/gameManager.ts +++ b/test/testUtils/gameManager.ts @@ -1,11 +1,13 @@ import { updateUserInfo } from "#app/account"; -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import BattleScene from "#app/battle-scene"; import type { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon"; import Trainer from "#app/field/trainer"; -import { GameModes, getGameMode } from "#app/game-mode"; +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"; @@ -351,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]=================="); } /** @@ -369,6 +376,7 @@ export default class GameManager { async toNextWave() { this.doSelectModifier(); + // forcibly end the message box for switching pokemon this.onNextPrompt( "CheckSwitchPhase", UiMode.CONFIRM, @@ -379,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 18dd83995a3..d582f8c04e3 100644 --- a/test/testUtils/gameManagerUtils.ts +++ b/test/testUtils/gameManagerUtils.ts @@ -5,7 +5,8 @@ import { getDailyRunStarters } from "#app/data/daily-run"; import { Gender } from "#app/data/gender"; import { getPokemonSpecies, getPokemonSpeciesForm } from "#app/data/pokemon-species"; import { PlayerPokemon } from "#app/field/pokemon"; -import { GameModes, getGameMode } from "#app/game-mode"; +import { getGameMode } from "#app/game-mode"; +import { GameModes } from "#enums/game-modes"; import type { StarterMoveset } from "#app/system/game-data"; import type { Starter } from "#app/ui/starter-select-ui-handler"; import { MoveId } from "#enums/move-id"; diff --git a/test/testUtils/helpers/classicModeHelper.ts b/test/testUtils/helpers/classicModeHelper.ts index c4f086bd628..eff97483777 100644 --- a/test/testUtils/helpers/classicModeHelper.ts +++ b/test/testUtils/helpers/classicModeHelper.ts @@ -1,6 +1,7 @@ import { BattleStyle } from "#app/enums/battle-style"; import type { SpeciesId } from "#enums/species-id"; -import { GameModes, getGameMode } from "#app/game-mode"; +import { getGameMode } from "#app/game-mode"; +import { GameModes } from "#enums/game-modes"; import overrides from "#app/overrides"; import { CommandPhase } from "#app/phases/command-phase"; import { EncounterPhase } from "#app/phases/encounter-phase"; diff --git a/test/testUtils/helpers/field-helper.ts b/test/testUtils/helpers/field-helper.ts index aa01ef7497d..2866c01209f 100644 --- a/test/testUtils/helpers/field-helper.ts +++ b/test/testUtils/helpers/field-helper.ts @@ -3,8 +3,8 @@ import type { globalScene } from "#app/global-scene"; // -- end tsdoc imports -- -import type { BattlerIndex } from "#app/battle"; -import type { Ability } from "#app/data/abilities/ability-class"; +import type { BattlerIndex } from "#enums/battler-index"; +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 c7dea05b095..ed1441a6a2f 100644 --- a/test/testUtils/helpers/moveHelper.ts +++ b/test/testUtils/helpers/moveHelper.ts @@ -1,17 +1,19 @@ -import type { BattlerIndex } from "#app/battle"; -import { getMoveTargets } from "#app/data/moves/move"; +import type { BattlerIndex } from "#enums/battler-index"; +import { getMoveTargets } from "#app/data/moves/move-utils"; import type Pokemon from "#app/field/pokemon"; -import { PokemonMove } from "#app/field/pokemon"; +import { PokemonMove } from "#app/data/moves/pokemon-move"; import Overrides from "#app/overrides"; import type { CommandPhase } from "#app/phases/command-phase"; import type { EnemyCommandPhase } from "#app/phases/enemy-command-phase"; import { MoveEffectPhase } from "#app/phases/move-effect-phase"; -import { Command } from "#app/ui/command-ui-handler"; +import { Command } from "#enums/command"; import { MoveId } from "#enums/move-id"; import { UiMode } from "#enums/ui-mode"; import { getMovePosition } from "#test/testUtils/gameManagerUtils"; import { GameManagerHelper } from "#test/testUtils/helpers/gameManagerHelper"; import { vi } from "vitest"; +import { coerceArray } from "#app/utils/common"; +import { 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..9869c7450e2 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; @@ -382,9 +380,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/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..d3fc4b11968 100644 --- a/test/ui/pokedex.test.ts +++ b/test/ui/pokedex.test.ts @@ -1,6 +1,6 @@ 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"; @@ -46,7 +46,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 +54,6 @@ describe("UI - Pokedex", () => { }); afterEach(() => { - while (mocks.length > 0) { - mocks.pop()?.mockRestore(); - } game.phaseInterceptor.restoreOg(); }); @@ -185,10 +181,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 +498,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 2884323b4ea..3e540c4e2c5 100644 --- a/test/ui/starter-select.test.ts +++ b/test/ui/starter-select.test.ts @@ -1,7 +1,7 @@ import { Gender } from "#app/data/gender"; import { Nature } from "#enums/nature"; import { allSpecies } from "#app/data/pokemon-species"; -import { GameModes } from "#app/game-mode"; +import { GameModes } from "#enums/game-modes"; import { EncounterPhase } from "#app/phases/encounter-phase"; import { SelectStarterPhase } from "#app/phases/select-starter-phase"; import type { TitlePhase } from "#app/phases/title-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); + }); });