diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml index 2850418bc59..a8eea069ba6 100644 --- a/.github/workflows/eslint.yml +++ b/.github/workflows/eslint.yml @@ -21,10 +21,10 @@ jobs: steps: - name: Check out Git repository # Step to check out the repository - uses: actions/checkout@v2 # Use the checkout action version 2 + uses: actions/checkout@v4 # Use the checkout action version 4 - name: Set up Node.js # Step to set up Node.js environment - uses: actions/setup-node@v1 # Use the setup-node action version 1 + uses: actions/setup-node@v4 # Use the setup-node action version 4 with: node-version: 20 # Specify Node.js version 20 @@ -32,4 +32,4 @@ jobs: run: npm ci # Use 'npm ci' to install dependencies - name: eslint # Step to run linters - run: npm run eslint-ci \ No newline at end of file + run: npm run eslint-ci diff --git a/README.md b/README.md index 0f9ed992352..b26fd56a7b1 100644 --- a/README.md +++ b/README.md @@ -55,8 +55,8 @@ Check out [Github Issues](https://github.com/pagefaultgames/pokerogue/issues) to - Pokémon Sword/Shield - Pokémon Legends: Arceus - Pokémon Scarlet/Violet - - Firel (Custom Laboratory, Metropolis, Seabed, and Space biome music) - - Lmz (Custom Jungle biome music) + - Firel (Custom Ice Cave, Laboratory, Metropolis, Plains, Power Plant, Seabed, Space, and Volcano biome music) + - Lmz (Custom Ancient Ruins, Jungle, and Lake biome music) - Andr06 (Custom Slum and Sea biome music) ### 🎵 Sound Effects diff --git a/create-test-boilerplate.js b/create-test-boilerplate.js index d9cdbd4e7cf..6d9cde966d5 100644 --- a/create-test-boilerplate.js +++ b/create-test-boilerplate.js @@ -1,7 +1,3 @@ -import fs from 'fs'; -import path from 'path'; -import { fileURLToPath } from 'url'; - /** * This script creates a test boilerplate file for a move or ability. * @param {string} type - The type of test to create. Either "move", "ability", @@ -10,63 +6,108 @@ import { fileURLToPath } from 'url'; * @example npm run create-test move tackle */ +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"]; -// Get the arguments from the command line -const args = process.argv.slice(2); -const type = args[0]; // "move" or "ability" -let fileName = args[1]; // The file name +/** + * 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 (!type || !fileName) { - console.error('Please provide a type ("move", "ability", or "item") and a file name.'); - process.exit(1); + if (typeAnswer.selectedOption === "EXIT") { + console.log("Exiting..."); + return process.exit(); + } else if (!typeChoices.includes(typeAnswer.selectedOption)) { + console.error('Please provide a valid type ("move", "ability", or "item")!'); + return await promptTestType(); + } + + return typeAnswer; } -// Convert fileName from kebab-case or camelCase to snake_case -fileName = fileName - .replace(/-+/g, '_') // Convert kebab-case (dashes) to underscores - .replace(/([a-z])([A-Z])/g, '$1_$2') // Convert camelCase to snake_case - .toLowerCase(); // Ensure all lowercase +/** + * 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 a file name for the ${selectedType} test:`, + }, + ]); -// Format the description for the test case -const formattedName = fileName - .replace(/_/g, ' ') - .replace(/\b\w/g, char => char.toUpperCase()); + if (!fileNameAnswer.userInput || fileNameAnswer.userInput.trim().length === 0) { + console.error("Please provide a valid file name!"); + return await promptFileName(selectedType); + } -// Determine the directory based on the type -let dir; -let description; -if (type === 'move') { - dir = path.join(__dirname, 'src', 'test', 'moves'); - description = `Moves - ${formattedName}`; -} else if (type === 'ability') { - dir = path.join(__dirname, 'src', 'test', 'abilities'); - description = `Abilities - ${formattedName}`; -} else if (type === "item") { - dir = path.join(__dirname, 'src', 'test', 'items'); - description = `Items - ${formattedName}`; -} else { - console.error('Invalid type. Please use "move", "ability", or "item".'); - process.exit(1); + return fileNameAnswer; } -// Ensure the directory exists -if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); -} +/** + * Runs the interactive create-test "CLI" + * @returns {Promise} + */ +async function runInteractive() { + const typeAnswer = await promptTestType(); + const fileNameAnswer = await promptFileName(typeAnswer.selectedOption); -// Create the file with the given name -const filePath = path.join(dir, `${fileName}.test.ts`); + 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 -if (fs.existsSync(filePath)) { - console.error(`File "${fileName}.test.ts" already exists.`); - process.exit(1); -} + 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, "src", "test", "moves"); + description = `Moves - ${formattedName}`; + break; + case "ability": + dir = path.join(__dirname, "src", "test", "abilities"); + description = `Abilities - ${formattedName}`; + break; + case "item": + dir = path.join(__dirname, "src", "test", "items"); + description = `Items - ${formattedName}`; + break; + case "mystery encounter": + dir = path.join(__dirname, "src", "test", "mystery-encounter", "encounters"); + description = `Mystery Encounter - ${formattedName}`; + break; + default: + console.error('Invalid type. Please use "move", "ability", or "item".'); + process.exit(1); + } -// Define the content template -const content = `import { Abilities } from "#enums/abilities"; + // 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/utils/gameManager"; @@ -76,7 +117,6 @@ import { afterEach, beforeAll, beforeEach, describe, it, expect } from "vitest"; describe("${description}", () => { let phaserGame: Phaser.Game; let game: GameManager; - const TIMEOUT = 20 * 1000; beforeAll(() => { phaserGame = new Phaser.Game({ @@ -100,11 +140,27 @@ describe("${description}", () => { it("test case", async () => { // await game.classicMode.startBattle([Species.MAGIKARP]); // game.move.select(Moves.SPLASH); - }, TIMEOUT); + }); }); `; -// Write the template content to the file -fs.writeFileSync(filePath, content, 'utf8'); + // Ensure the directory exists + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } -console.log(`File created at: ${filePath}`); + // 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/index.css b/index.css index 1274f2fcead..2ec106516d2 100644 --- a/index.css +++ b/index.css @@ -26,10 +26,36 @@ body { #app { display: flex; justify-content: center; + align-items: center; } #app > div:first-child { - transform-origin: top !important; + transform-origin: center !important; +} + +/* + Supports automatic vertical centering as suggested in PR#1114, but only via CSS + + Condition factorized to deduce CSS rules: + true if (isLandscape && !isMobile() && !hasTouchscreen() || (hasTouchscreen() && !isTouchControlsEnabled)) +*/ + +/* isLandscape && !isMobile() && !hasTouchscreen() */ +@media (orientation: landscape) and (pointer: fine) { + #app { + align-items: center; + } +} + +@media (pointer: coarse) { + /* hasTouchscreen() && !isTouchControlsEnabled */ + body:has(> #touchControls[class=visible]) #app { + align-items: start; + } + + body:has(> #touchControls[class=visible]) #app > div:first-child { + transform-origin: top !important; + } } #layout:fullscreen #dpad, #layout:fullscreen { diff --git a/package-lock.json b/package-lock.json index 4a447554819..344100e4f6e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "dependency-cruiser": "^16.3.10", "eslint": "^9.7.0", "eslint-plugin-import-x": "^4.2.1", + "inquirer": "^11.0.2", "jsdom": "^24.0.0", "lefthook": "^1.6.12", "phaser3spectorjs": "^0.0.8", @@ -1070,6 +1071,281 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@inquirer/checkbox": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-3.0.1.tgz", + "integrity": "sha512-0hm2nrToWUdD6/UHnel/UKGdk1//ke5zGUpHIvk5ZWmaKezlGxZkOJXNSWsdxO/rEqTkbB3lNC2J6nBElV2aAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/figures": "^1.0.6", + "@inquirer/type": "^2.0.0", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/confirm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-4.0.1.tgz", + "integrity": "sha512-46yL28o2NJ9doViqOy0VDcoTzng7rAb6yPQKU7VDLqkmbCaH4JqK4yk4XqlzNWy9PVC5pG1ZUXPBQv+VqnYs2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/core": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-9.2.1.tgz", + "integrity": "sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/figures": "^1.0.6", + "@inquirer/type": "^2.0.0", + "@types/mute-stream": "^0.0.4", + "@types/node": "^22.5.5", + "@types/wrap-ansi": "^3.0.0", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^1.0.0", + "signal-exit": "^4.1.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/core/node_modules/@types/node": { + "version": "22.5.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.5.tgz", + "integrity": "sha512-Xjs4y5UPO/CLdzpgR6GirZJx36yScjh73+2NlLlkFRSoQN8B0DpfXPdZGnvVmLRLOsqDpOfTNv7D9trgGhmOIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@inquirer/core/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@inquirer/core/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/core/node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@inquirer/core/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/editor": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-3.0.1.tgz", + "integrity": "sha512-VA96GPFaSOVudjKFraokEEmUQg/Lub6OXvbIEZU1SDCmBzRkHGhxoFAVaF30nyiB4m5cEbDgiI2QRacXZ2hw9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0", + "external-editor": "^3.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/expand": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-3.0.1.tgz", + "integrity": "sha512-ToG8d6RIbnVpbdPdiN7BCxZGiHOTomOX94C2FaT5KOHupV40tKEDozp12res6cMIfRKrXLJyexAZhWVHgbALSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.6.tgz", + "integrity": "sha512-yfZzps3Cso2UbM7WlxKwZQh2Hs6plrbjs1QnzQDZhK2DgyCo6D8AaHps9olkNcUFlcYERMqU3uJSp1gmy3s/qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-3.0.1.tgz", + "integrity": "sha512-BDuPBmpvi8eMCxqC5iacloWqv+5tQSJlUafYWUe31ow1BVXjW2a5qe3dh4X/Z25Wp22RwvcaLCc2siHobEOfzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/number": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-2.0.1.tgz", + "integrity": "sha512-QpR8jPhRjSmlr/mD2cw3IR8HRO7lSVOnqUvQa8scv1Lsr3xoAMMworcYW3J13z3ppjBFBD2ef1Ci6AE5Qn8goQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/password": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-3.0.1.tgz", + "integrity": "sha512-haoeEPUisD1NeE2IanLOiFr4wcTXGWrBOyAyPZi1FfLJuXOzNmxCJPgUrGYKVh+Y8hfGJenIfz5Wb/DkE9KkMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0", + "ansi-escapes": "^4.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/prompts": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-6.0.1.tgz", + "integrity": "sha512-yl43JD/86CIj3Mz5mvvLJqAOfIup7ncxfJ0Btnl0/v5TouVUyeEdcpknfgc+yMevS/48oH9WAkkw93m7otLb/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^3.0.1", + "@inquirer/confirm": "^4.0.1", + "@inquirer/editor": "^3.0.1", + "@inquirer/expand": "^3.0.1", + "@inquirer/input": "^3.0.1", + "@inquirer/number": "^2.0.1", + "@inquirer/password": "^3.0.1", + "@inquirer/rawlist": "^3.0.1", + "@inquirer/search": "^2.0.1", + "@inquirer/select": "^3.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/rawlist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-3.0.1.tgz", + "integrity": "sha512-VgRtFIwZInUzTiPLSfDXK5jLrnpkuSOh1ctfaoygKAdPqjcjKYmGh6sCY1pb0aGnCGsmhUxoqLDUAU0ud+lGXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/search": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-2.0.1.tgz", + "integrity": "sha512-r5hBKZk3g5MkIzLVoSgE4evypGqtOannnB3PKTG9NRZxyFRKcfzrdxXXPcoJQsxJPzvdSU2Rn7pB7lw0GCmGAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/figures": "^1.0.6", + "@inquirer/type": "^2.0.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/select": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-3.0.1.tgz", + "integrity": "sha512-lUDGUxPhdWMkN/fHy1Lk7pF3nK1fh/gqeyWXmctefhxLYxlDsc7vsPBEpxrfVGDsVdyYJsiJoD4bJ1b623cV1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/figures": "^1.0.6", + "@inquirer/type": "^2.0.0", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-2.0.0.tgz", + "integrity": "sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==", + "dev": true, + "license": "MIT", + "dependencies": { + "mute-stream": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1563,6 +1839,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mute-stream": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz", + "integrity": "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/node": { "version": "20.14.11", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.11.tgz", @@ -1585,6 +1871,13 @@ "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==", "dev": true }, + "node_modules/@types/wrap-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", + "integrity": "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==", + "dev": true, + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.0.0-alpha.58", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.0.0-alpha.58.tgz", @@ -1970,6 +2263,22 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -2192,6 +2501,13 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true, + "license": "MIT" + }, "node_modules/check-error": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", @@ -2202,6 +2518,16 @@ "node": ">= 16" } }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -3057,6 +3383,21 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3579,6 +3920,19 @@ "i18next": ">=8.4.0" } }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", @@ -3626,6 +3980,26 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/inquirer": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-11.0.2.tgz", + "integrity": "sha512-pnbn3nL+JFrTw/pLhzyE/IQ3+gA3n5JxTAZQDjB6qu4gbjOaiTnpZbxT6HY2DDCT7bzDjTTsd3snRP+B6N//Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/prompts": "^6.0.1", + "@inquirer/type": "^2.0.0", + "@types/mute-stream": "^0.0.4", + "ansi-escapes": "^4.3.2", + "mute-stream": "^1.0.0", + "run-async": "^3.0.0", + "rxjs": "^7.8.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/interpret": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", @@ -4456,6 +4830,16 @@ "mustache": "bin/mustache" } }, + "node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -4605,6 +4989,16 @@ "node": ">= 0.8.0" } }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -5092,6 +5486,16 @@ "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", "dev": true }, + "node_modules/run-async": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", + "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -5116,6 +5520,16 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/safe-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz", @@ -5538,6 +5952,19 @@ "node": ">=14.0.0" } }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -5673,6 +6100,19 @@ "node": ">= 0.8.0" } }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typedoc": { "version": "0.26.5", "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.26.5.tgz", @@ -6346,6 +6786,19 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", + "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index dddf5aedebd..2109604c969 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "dependency-cruiser": "^16.3.10", "eslint": "^9.7.0", "eslint-plugin-import-x": "^4.2.1", + "inquirer": "^11.0.2", "jsdom": "^24.0.0", "lefthook": "^1.6.12", "phaser3spectorjs": "^0.0.8", diff --git a/public/audio/bgm/battle_star_admin.mp3 b/public/audio/bgm/battle_star_admin.mp3 new file mode 100644 index 00000000000..461a9a2b262 Binary files /dev/null and b/public/audio/bgm/battle_star_admin.mp3 differ diff --git a/public/audio/bgm/battle_star_boss.mp3 b/public/audio/bgm/battle_star_boss.mp3 new file mode 100644 index 00000000000..51cb33139c6 Binary files /dev/null and b/public/audio/bgm/battle_star_boss.mp3 differ diff --git a/public/audio/bgm/battle_star_grunt.mp3 b/public/audio/bgm/battle_star_grunt.mp3 new file mode 100644 index 00000000000..13da4900eed Binary files /dev/null and b/public/audio/bgm/battle_star_grunt.mp3 differ diff --git a/public/audio/bgm/ice_cave.mp3 b/public/audio/bgm/ice_cave.mp3 index 5d1b9e9e354..9d1c7c06bf0 100644 Binary files a/public/audio/bgm/ice_cave.mp3 and b/public/audio/bgm/ice_cave.mp3 differ diff --git a/public/audio/bgm/jungle.mp3 b/public/audio/bgm/jungle.mp3 index 3a21c9bdb41..fbb770b0ad3 100644 Binary files a/public/audio/bgm/jungle.mp3 and b/public/audio/bgm/jungle.mp3 differ diff --git a/public/audio/bgm/laboratory.mp3 b/public/audio/bgm/laboratory.mp3 index e2b617e590a..1eba66ef56f 100644 Binary files a/public/audio/bgm/laboratory.mp3 and b/public/audio/bgm/laboratory.mp3 differ diff --git a/public/audio/bgm/lake.mp3 b/public/audio/bgm/lake.mp3 index c61fef15e42..e6762935770 100644 Binary files a/public/audio/bgm/lake.mp3 and b/public/audio/bgm/lake.mp3 differ diff --git a/public/audio/bgm/metropolis.mp3 b/public/audio/bgm/metropolis.mp3 index 98c2eb396b6..514c9ae15b1 100644 Binary files a/public/audio/bgm/metropolis.mp3 and b/public/audio/bgm/metropolis.mp3 differ diff --git a/public/audio/bgm/mystery_encounter_delibirdy.mp3 b/public/audio/bgm/mystery_encounter_delibirdy.mp3 new file mode 100644 index 00000000000..515a429aaba Binary files /dev/null and b/public/audio/bgm/mystery_encounter_delibirdy.mp3 differ diff --git a/public/audio/bgm/mystery_encounter_fun_and_games.mp3 b/public/audio/bgm/mystery_encounter_fun_and_games.mp3 new file mode 100644 index 00000000000..7864bf7a73d Binary files /dev/null and b/public/audio/bgm/mystery_encounter_fun_and_games.mp3 differ diff --git a/public/audio/bgm/mystery_encounter_gen_5_gts.mp3 b/public/audio/bgm/mystery_encounter_gen_5_gts.mp3 new file mode 100644 index 00000000000..d03c8f8d4d5 Binary files /dev/null and b/public/audio/bgm/mystery_encounter_gen_5_gts.mp3 differ diff --git a/public/audio/bgm/mystery_encounter_gen_6_gts.mp3 b/public/audio/bgm/mystery_encounter_gen_6_gts.mp3 new file mode 100644 index 00000000000..c921a01c204 Binary files /dev/null and b/public/audio/bgm/mystery_encounter_gen_6_gts.mp3 differ diff --git a/public/audio/bgm/mystery_encounter_weird_dream.mp3 b/public/audio/bgm/mystery_encounter_weird_dream.mp3 new file mode 100644 index 00000000000..433e07bab08 Binary files /dev/null and b/public/audio/bgm/mystery_encounter_weird_dream.mp3 differ diff --git a/public/audio/bgm/plains.mp3 b/public/audio/bgm/plains.mp3 index 6c7a008bce6..ff364600b4a 100644 Binary files a/public/audio/bgm/plains.mp3 and b/public/audio/bgm/plains.mp3 differ diff --git a/public/audio/bgm/power_plant.mp3 b/public/audio/bgm/power_plant.mp3 index 9813ad40a11..152667fcba6 100644 Binary files a/public/audio/bgm/power_plant.mp3 and b/public/audio/bgm/power_plant.mp3 differ diff --git a/public/audio/bgm/ruins.mp3 b/public/audio/bgm/ruins.mp3 index 62f31893423..3692c71562f 100644 Binary files a/public/audio/bgm/ruins.mp3 and b/public/audio/bgm/ruins.mp3 differ diff --git a/public/audio/bgm/volcano.mp3 b/public/audio/bgm/volcano.mp3 index 093bb86813b..a67bbd111de 100644 Binary files a/public/audio/bgm/volcano.mp3 and b/public/audio/bgm/volcano.mp3 differ diff --git a/public/battle-anims/clanging-scales.json b/public/battle-anims/clanging-scales.json index de1a3d5248f..e2135a1a9b4 100644 --- a/public/battle-anims/clanging-scales.json +++ b/public/battle-anims/clanging-scales.json @@ -27,7 +27,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": 0, @@ -115,7 +115,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": 0, @@ -215,7 +215,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": 0, @@ -315,7 +315,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": 0, @@ -414,7 +414,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": 0, @@ -538,7 +538,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": 23, @@ -685,7 +685,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": -19, @@ -784,7 +784,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": 26, @@ -883,7 +883,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": 23.5, @@ -994,7 +994,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": 9, @@ -1069,7 +1069,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": -18.5, @@ -1157,7 +1157,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": 37.5, @@ -1221,7 +1221,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": 0, @@ -1284,7 +1284,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": 0, @@ -1348,7 +1348,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": 0, @@ -1448,7 +1448,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": 0, @@ -1548,7 +1548,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": 0, @@ -1647,7 +1647,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": 0, @@ -1759,7 +1759,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": -25.5, @@ -1870,7 +1870,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": 12, @@ -1957,7 +1957,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": -27, @@ -2044,7 +2044,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": -16, @@ -2143,7 +2143,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": -26.5, @@ -2230,7 +2230,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": 23, @@ -2306,7 +2306,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": 24, @@ -2346,7 +2346,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": -27, diff --git a/public/battle-anims/encounter-dance.json b/public/battle-anims/encounter-dance.json new file mode 100644 index 00000000000..4be7f0756ee --- /dev/null +++ b/public/battle-anims/encounter-dance.json @@ -0,0 +1,951 @@ +{ + "id": 686, + "graphic": "PRAS- Dragon Dance", + "frames": [ + [ + { + "x": 4, + "y": -8, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "priority": 1, + "focus": 2 + }, + { + "x": 12, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "blendType": 1, + "target": 2, + "graphicFrame": 0, + "opacity": 70, + "tone": [ + 0, + 0, + 0, + 255 + ], + "priority": 1, + "focus": 3 + }, + { + "x": -12, + "y": -0.5, + "zoomX": 100, + "zoomY": 100, + "mirror": true, + "visible": true, + "blendType": 1, + "target": 2, + "graphicFrame": 0, + "opacity": 70, + "tone": [ + 0, + 0, + 0, + 255 + ], + "priority": 1, + "focus": 3 + } + ], + [ + { + "x": 12, + "y": -12, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "priority": 1, + "focus": 2 + }, + { + "x": 16, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "blendType": 1, + "target": 2, + "graphicFrame": 0, + "opacity": 155, + "tone": [ + 0, + 0, + 0, + 255 + ], + "priority": 1, + "focus": 3 + }, + { + "x": -16, + "y": -0.5, + "zoomX": 100, + "zoomY": 100, + "mirror": true, + "visible": true, + "blendType": 1, + "target": 2, + "graphicFrame": 0, + "opacity": 155, + "tone": [ + 0, + 0, + 0, + 255 + ], + "priority": 1, + "focus": 3 + } + ], + [ + { + "x": 24, + "y": -12, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "priority": 1, + "focus": 2 + }, + { + "x": 20, + "y": 0, + "zoomX": 108, + "zoomY": 100, + "visible": true, + "blendType": 1, + "target": 2, + "graphicFrame": 0, + "opacity": 155, + "tone": [ + 0, + 0, + 0, + 255 + ], + "priority": 1, + "focus": 3 + }, + { + "x": -20, + "y": -0.5, + "zoomX": 108, + "zoomY": 100, + "mirror": true, + "visible": true, + "blendType": 1, + "target": 2, + "graphicFrame": 0, + "opacity": 155, + "tone": [ + 0, + 0, + 0, + 255 + ], + "priority": 1, + "focus": 3 + } + ], + [ + { + "x": 32, + "y": -8, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "priority": 1, + "focus": 2 + }, + { + "x": 24, + "y": 0, + "zoomX": 108, + "zoomY": 100, + "visible": true, + "blendType": 1, + "target": 2, + "graphicFrame": 0, + "opacity": 155, + "tone": [ + 0, + 0, + 0, + 255 + ], + "priority": 1, + "focus": 3 + }, + { + "x": -24, + "y": -0.5, + "zoomX": 108, + "zoomY": 100, + "mirror": true, + "visible": true, + "blendType": 1, + "target": 2, + "graphicFrame": 0, + "opacity": 155, + "tone": [ + 0, + 0, + 0, + 255 + ], + "priority": 1, + "focus": 3 + } + ], + [ + { + "x": 36, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "priority": 1, + "focus": 2 + }, + { + "x": 28, + "y": 0, + "zoomX": 108, + "zoomY": 100, + "visible": true, + "blendType": 1, + "target": 2, + "graphicFrame": 0, + "opacity": 70, + "tone": [ + 0, + 0, + 0, + 255 + ], + "priority": 1, + "focus": 3 + }, + { + "x": -28, + "y": -0.5, + "zoomX": 108, + "zoomY": 100, + "mirror": true, + "visible": true, + "blendType": 1, + "target": 2, + "graphicFrame": 0, + "opacity": 70, + "tone": [ + 0, + 0, + 0, + 255 + ], + "priority": 1, + "focus": 3 + } + ], + [ + { + "x": 36, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": 36, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": 32, + "y": -8, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": 24, + "y": -12, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": 12, + "y": -12, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": 4, + "y": -8, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": -4, + "y": -8, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "priority": 1, + "focus": 2 + }, + { + "x": 12, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "blendType": 1, + "target": 2, + "graphicFrame": 0, + "opacity": 70, + "tone": [ + 0, + 0, + 0, + 255 + ], + "priority": 1, + "focus": 3 + }, + { + "x": -12, + "y": -0.5, + "zoomX": 100, + "zoomY": 100, + "mirror": true, + "visible": true, + "blendType": 1, + "target": 2, + "graphicFrame": 0, + "opacity": 70, + "tone": [ + 0, + 0, + 0, + 255 + ], + "priority": 1, + "focus": 3 + } + ], + [ + { + "x": -12, + "y": -12, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "priority": 1, + "focus": 2 + }, + { + "x": 16, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "blendType": 1, + "target": 2, + "graphicFrame": 0, + "opacity": 155, + "tone": [ + 0, + 0, + 0, + 255 + ], + "priority": 1, + "focus": 3 + }, + { + "x": -16, + "y": -0.5, + "zoomX": 100, + "zoomY": 100, + "mirror": true, + "visible": true, + "blendType": 1, + "target": 2, + "graphicFrame": 0, + "opacity": 155, + "tone": [ + 0, + 0, + 0, + 255 + ], + "priority": 1, + "focus": 3 + } + ], + [ + { + "x": -24, + "y": -12, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "priority": 1, + "focus": 2 + }, + { + "x": 20, + "y": 0, + "zoomX": 108, + "zoomY": 100, + "visible": true, + "blendType": 1, + "target": 2, + "graphicFrame": 0, + "opacity": 155, + "tone": [ + 0, + 0, + 0, + 255 + ], + "priority": 1, + "focus": 3 + }, + { + "x": -20, + "y": -0.5, + "zoomX": 108, + "zoomY": 100, + "mirror": true, + "visible": true, + "blendType": 1, + "target": 2, + "graphicFrame": 0, + "opacity": 155, + "tone": [ + 0, + 0, + 0, + 255 + ], + "priority": 1, + "focus": 3 + } + ], + [ + { + "x": -32, + "y": -8, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "priority": 1, + "focus": 2 + }, + { + "x": 24, + "y": 0, + "zoomX": 108, + "zoomY": 100, + "visible": true, + "blendType": 1, + "target": 2, + "graphicFrame": 0, + "opacity": 155, + "tone": [ + 0, + 0, + 0, + 255 + ], + "priority": 1, + "focus": 3 + }, + { + "x": -24, + "y": -0.5, + "zoomX": 108, + "zoomY": 100, + "mirror": true, + "visible": true, + "blendType": 1, + "target": 2, + "graphicFrame": 0, + "opacity": 155, + "tone": [ + 0, + 0, + 0, + 255 + ], + "priority": 1, + "focus": 3 + } + ], + [ + { + "x": -36, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "priority": 1, + "focus": 2 + }, + { + "x": 28, + "y": 0, + "zoomX": 108, + "zoomY": 100, + "visible": true, + "blendType": 1, + "target": 2, + "graphicFrame": 0, + "opacity": 70, + "tone": [ + 0, + 0, + 0, + 255 + ], + "priority": 1, + "focus": 3 + }, + { + "x": -28, + "y": -0.5, + "zoomX": 108, + "zoomY": 100, + "mirror": true, + "visible": true, + "blendType": 1, + "target": 2, + "graphicFrame": 0, + "opacity": 70, + "tone": [ + 0, + 0, + 0, + 255 + ], + "priority": 1, + "focus": 3 + } + ], + [ + { + "x": -36, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": -36, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": -32, + "y": -8, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": -24, + "y": -12, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": -12, + "y": -12, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": -4, + "y": -8, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": 1, + "focus": 2 + } + ] + ], + "frameTimedEvents": { + "0": [ + { + "frameIndex": 0, + "resourceName": "PRSFX- Attract.wav", + "volume": 100, + "pitch": 100, + "eventType": "AnimTimedSoundEvent" + } + ], + "1": [ + { + "frameIndex": 0, + "resourceName": "PRSFX- Ally Switch.wav", + "volume": 80, + "pitch": 100, + "eventType": "AnimTimedSoundEvent" + } + ] + }, + "position": 4, + "hue": 0 +} \ No newline at end of file diff --git a/public/battle-anims/encounter-magma-bg.json b/public/battle-anims/encounter-magma-bg.json new file mode 100644 index 00000000000..bb22f721d9a --- /dev/null +++ b/public/battle-anims/encounter-magma-bg.json @@ -0,0 +1,66 @@ +{ + "frames": [ + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [] + ], + "frameTimedEvents": { + "0": [ + { + "frameIndex": 0, + "resourceName": "PRAS- Fire BG", + "bgX": 0, + "bgY": 0, + "opacity": 0, + "duration": 35, + "eventType": "AnimTimedAddBgEvent" + }, + { + "frameIndex": 0, + "resourceName": "", + "bgX": 0, + "bgY": 0, + "opacity": 255, + "duration": 12, + "eventType": "AnimTimedUpdateBgEvent" + } + ], + "25": [ + { + "frameIndex": 25, + "resourceName": "", + "bgX": 0, + "bgY": 0, + "opacity": 0, + "duration": 8, + "eventType": "AnimTimedUpdateBgEvent" + } + ] + }, + "position": 1, + "hue": 0 +} \ No newline at end of file diff --git a/public/battle-anims/encounter-magma-spout.json b/public/battle-anims/encounter-magma-spout.json new file mode 100644 index 00000000000..21f3bec585f --- /dev/null +++ b/public/battle-anims/encounter-magma-spout.json @@ -0,0 +1,902 @@ +{ + "graphic": "PRAS- Magma Storm", + "frames": [ + [ + { + "x": 101, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 0, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 152, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 0, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 124.5, + "y": -78.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 0, + "opacity": 255, + "priority": 4, + "focus": 1 + } + ], + [ + { + "x": 101, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 1, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 152, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 1, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 124.5, + "y": -78.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 1, + "opacity": 255, + "priority": 4, + "focus": 1 + } + ], + [ + { + "x": 101, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 2, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 152, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 2, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 124.5, + "y": -78.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 2, + "opacity": 255, + "priority": 4, + "focus": 1 + } + ], + [ + { + "x": 101, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 3, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 152, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 3, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 124.5, + "y": -78.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 3, + "opacity": 255, + "priority": 4, + "focus": 1 + } + ], + [ + { + "x": 101, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 152, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 124.5, + "y": -78.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 255, + "priority": 4, + "focus": 1 + } + ], + [ + { + "x": 101, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 5, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 152, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 5, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 124.5, + "y": -78.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 5, + "opacity": 255, + "priority": 4, + "focus": 1 + } + ], + [ + { + "x": 101, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 6, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 152, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 6, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 124.5, + "y": -78.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 6, + "opacity": 255, + "priority": 4, + "focus": 1 + } + ], + [ + { + "x": 101, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 7, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 152, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 7, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 124.5, + "y": -78.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 7, + "opacity": 255, + "priority": 4, + "focus": 1 + } + ], + [ + { + "x": 120, + "y": -56, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 8, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 144, + "y": -84, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 8, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 100, + "y": -86.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 8, + "opacity": 255, + "priority": 4, + "focus": 1 + } + ], + [ + { + "x": 140, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 9, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 136, + "y": -92, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 9, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 108, + "y": -78.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 9, + "opacity": 255, + "priority": 4, + "focus": 1 + } + ], + [ + { + "x": 152, + "y": -76, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 10, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 116, + "y": -88, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 10, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 128, + "y": -62.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 10, + "opacity": 255, + "priority": 4, + "focus": 1 + } + ], + [ + { + "x": 136, + "y": -96, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 7, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 100, + "y": -76, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 7, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 148, + "y": -66.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 7, + "opacity": 255, + "priority": 4, + "focus": 1 + } + ], + [ + { + "x": 108, + "y": -92, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 8, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 120, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 8, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 144, + "y": -86.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 8, + "opacity": 255, + "priority": 4, + "focus": 1 + } + ], + [ + { + "x": 100, + "y": -76, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 9, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 136, + "y": -68, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 9, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 128, + "y": -94.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 9, + "opacity": 255, + "priority": 4, + "focus": 1 + } + ], + [ + { + "x": 100.5, + "y": -70, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 10, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 144, + "y": -66, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 10, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 126, + "y": -86.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 10, + "opacity": 255, + "priority": 4, + "priority": 4, + "focus": 1 + } + ], + [ + { + "x": 101, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 6, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 152, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 6, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 124.5, + "y": -78.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 6, + "opacity": 255, + "priority": 4, + "focus": 1 + } + ], + [ + { + "x": 101, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 5, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 152, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 5, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 124.5, + "y": -78.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 5, + "opacity": 255, + "priority": 4, + "focus": 1 + } + ], + [ + { + "x": 101, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 152, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 124.5, + "y": -78.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 255, + "priority": 4, + "focus": 1 + } + ], + [ + { + "x": 101, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 3, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 152, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 3, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 124.5, + "y": -78.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 3, + "opacity": 255, + "priority": 4, + "focus": 1 + } + ], + [ + { + "x": 101, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 2, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 152, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 2, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 124.5, + "y": -78.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 2, + "opacity": 255, + "priority": 4, + "focus": 1 + } + ], + [ + { + "x": 101, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 1, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 152, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 1, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 124.5, + "y": -78.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 1, + "opacity": 255, + "priority": 4, + "focus": 1 + } + ], + [ + { + "x": 101, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 0, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 152, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 0, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 124.5, + "y": -78.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 0, + "opacity": 255, + "priority": 4, + "focus": 1 + } + ], + [ + { + "x": 101, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 0, + "opacity": 130, + "priority": 4, + "focus": 1 + }, + { + "x": 152, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 0, + "opacity": 130, + "priority": 4, + "focus": 1 + }, + { + "x": 124.5, + "y": -78.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 0, + "opacity": 140, + "priority": 4, + "focus": 1 + } + ] + ], + "frameTimedEvents": { + "0": [ + { + "frameIndex": 0, + "resourceName": "PRSFX- Magma Storm1.wav", + "volume": 100, + "pitch": 100, + "eventType": "AnimTimedSoundEvent" + } + ], + "8": [ + { + "frameIndex": 8, + "resourceName": "PRSFX- Magma Storm2.wav", + "volume": 100, + "pitch": 100, + "eventType": "AnimTimedSoundEvent" + } + ] + }, + "position": 1, + "hue": 0 +} \ No newline at end of file diff --git a/public/battle-anims/encounter-smokescreen.json b/public/battle-anims/encounter-smokescreen.json new file mode 100644 index 00000000000..286cbe13a03 --- /dev/null +++ b/public/battle-anims/encounter-smokescreen.json @@ -0,0 +1,822 @@ +{ + "graphic": "PRAS- Smokescreen", + "frames": [ + [ + { + "x": 15.5, + "y": 12.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 0, + "opacity": 100, + "priority": 4, + "focus": 2 + } + ], + [ + { + "x": 15.5, + "y": 8.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 0, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 8, + "opacity": 50, + "priority": 4, + "focus": 2 + } + ], + [ + { + "x": 15.5, + "y": 0.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 1, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": 0, + "y": -4, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 8, + "opacity": 100, + "priority": 4, + "focus": 2 + } + ], + [ + { + "x": 15.5, + "y": -3.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 2, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": 0, + "y": -4, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 8, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": -11, + "y": 21.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 0, + "opacity": 50, + "priority": 4, + "focus": 2 + } + ], + [ + { + "x": 15.5, + "y": -7.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 2, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": 0, + "y": -8, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 7, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": -11, + "y": 17.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 0, + "opacity": 100, + "priority": 4, + "focus": 2 + } + ], + [ + { + "x": 15.5, + "y": -11.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 3, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": 0, + "y": -12, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 6, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": -11, + "y": 13.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 0, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": 11, + "y": 21, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 8, + "opacity": 50, + "priority": 4, + "focus": 2 + } + ], + [ + { + "x": 15.5, + "y": -15.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": 0, + "y": -16, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 6, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": -11, + "y": 5.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 1, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": 11, + "y": 17, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 8, + "opacity": 100, + "priority": 4, + "focus": 2 + } + ], + [ + { + "x": 15.5, + "y": -19.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 100, + "priority": 4, + "focus": 2 + }, + { + "x": 0, + "y": -20, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 5, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": -11, + "y": 0.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 2, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": 11, + "y": 13, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 8, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": -12.5, + "y": 8.5, + "zoomX": 100, + "zoomY": 100, + "mirror": true, + "visible": true, + "target": 2, + "graphicFrame": 8, + "opacity": 50, + "priority": 4, + "focus": 2 + } + ], + [ + { + "x": 15.5, + "y": -23.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 50, + "priority": 4, + "focus": 2 + }, + { + "x": 0, + "y": -24, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 5, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": -11, + "y": -2.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 2, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": 11, + "y": 9, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 7, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": -12.5, + "y": 4.5, + "zoomX": 100, + "zoomY": 100, + "mirror": true, + "visible": true, + "target": 2, + "graphicFrame": 8, + "opacity": 100, + "priority": 4, + "focus": 2 + } + ], + [ + { + "x": -11, + "y": -6.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 3, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": 0, + "y": -28, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": 11, + "y": 5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 6, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": -12.5, + "y": 0.5, + "zoomX": 100, + "zoomY": 100, + "mirror": true, + "visible": true, + "target": 2, + "graphicFrame": 8, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": 4.5, + "y": 23, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 0, + "opacity": 50, + "priority": 4, + "focus": 2 + } + ], + [ + { + "x": -11, + "y": -10.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": 0, + "y": -32, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 100, + "priority": 4, + "focus": 2 + }, + { + "x": 11, + "y": 1, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 6, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": -12.5, + "y": -3.5, + "zoomX": 100, + "zoomY": 100, + "mirror": true, + "visible": true, + "target": 2, + "graphicFrame": 7, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": 4.5, + "y": 19, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 0, + "opacity": 100, + "priority": 4, + "focus": 2 + } + ], + [ + { + "x": -11, + "y": -14.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 100, + "priority": 4, + "focus": 2 + }, + { + "x": 0, + "y": -36, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 50, + "priority": 4, + "focus": 2 + }, + { + "x": 11, + "y": -3, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 5, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": -12.5, + "y": -7.5, + "zoomX": 100, + "zoomY": 100, + "mirror": true, + "visible": true, + "target": 2, + "graphicFrame": 6, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": 4.5, + "y": 15, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 0, + "opacity": 150, + "priority": 4, + "focus": 2 + } + ], + [ + { + "x": -11, + "y": -18.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 50, + "priority": 4, + "focus": 2 + }, + { + "x": 11, + "y": -7, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": -12.5, + "y": -11.5, + "zoomX": 100, + "zoomY": 100, + "mirror": true, + "visible": true, + "target": 2, + "graphicFrame": 6, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": 4.5, + "y": 7, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 1, + "opacity": 150, + "priority": 4, + "focus": 2 + } + ], + [ + { + "x": -12.5, + "y": -15.5, + "zoomX": 100, + "zoomY": 100, + "mirror": true, + "visible": true, + "target": 2, + "graphicFrame": 5, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": 11, + "y": -11, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 100, + "priority": 4, + "focus": 2 + }, + { + "x": 4.5, + "y": 3, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 2, + "opacity": 150, + "priority": 4, + "focus": 2 + } + ], + [ + { + "x": -12.5, + "y": -19.5, + "zoomX": 100, + "zoomY": 100, + "mirror": true, + "visible": true, + "target": 2, + "graphicFrame": 5, + "opacity": 100, + "priority": 4, + "focus": 2 + }, + { + "x": 11, + "y": -15, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 50, + "priority": 4, + "focus": 2 + }, + { + "x": 4.5, + "y": -1, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 2, + "opacity": 150, + "priority": 4, + "focus": 2 + } + ], + [ + { + "x": -12.5, + "y": -23.5, + "zoomX": 100, + "zoomY": 100, + "mirror": true, + "visible": true, + "target": 2, + "graphicFrame": 5, + "opacity": 50, + "priority": 4, + "focus": 2 + }, + { + "x": 4.5, + "y": -5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 3, + "opacity": 150, + "priority": 4, + "focus": 2 + } + ], + [ + { + "x": 4.5, + "y": -9, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 150, + "priority": 4, + "focus": 2 + } + ], + [ + { + "x": 4.5, + "y": -13, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 100, + "priority": 4, + "focus": 2 + } + ], + [ + { + "x": 4.5, + "y": -17, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 50, + "priority": 4, + "focus": 2 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": 4, + "focus": 3 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": 4, + "focus": 3 + } + ], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [] + ], + "frameTimedEvents": { + "0": [ + { + "frameIndex": 0, + "resourceName": "PRSFX- Haze.wav", + "volume": 100, + "pitch": 85, + "eventType": "AnimTimedSoundEvent" + }, + { + "frameIndex": 0, + "resourceName": "Explosion1.m4a", + "volume": 100, + "pitch": 85, + "eventType": "AnimTimedSoundEvent" + } + ] + }, + "position": 2, + "hue": 0 +} \ No newline at end of file diff --git a/public/fonts/pkmnems.ttf b/public/fonts/pkmnems.ttf index 0aa3a19b417..b0b50d0f10f 100644 Binary files a/public/fonts/pkmnems.ttf and b/public/fonts/pkmnems.ttf differ diff --git a/public/images/items.json b/public/images/items.json index 442b93d657b..779823d1293 100644 --- a/public/images/items.json +++ b/public/images/items.json @@ -4,8 +4,8 @@ "image": "items.png", "format": "RGBA8888", "size": { - "w": 426, - "h": 426 + "w": 431, + "h": 431 }, "scale": 1, "frames": [ @@ -93,6 +93,27 @@ "h": 28 } }, + { + "filename": "leaders_crest", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 2, + "y": 3, + "w": 29, + "h": 27 + }, + "frame": { + "x": 61, + "y": 0, + "w": 29, + "h": 27 + } + }, { "filename": "ribbon_gen2", "rotated": false, @@ -171,8 +192,8 @@ "h": 26 }, "frame": { - "x": 61, - "y": 0, + "x": 59, + "y": 27, "w": 27, "h": 26 } @@ -339,7 +360,7 @@ "h": 26 }, "frame": { - "x": 88, + "x": 90, "y": 0, "w": 24, "h": 26 @@ -387,6 +408,27 @@ "h": 28 } }, + { + "filename": "black_glasses", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 8, + "w": 23, + "h": 17 + }, + "frame": { + "x": 0, + "y": 414, + "w": 23, + "h": 17 + } + }, { "filename": "ability_charm", "rotated": false, @@ -402,7 +444,7 @@ "h": 26 }, "frame": { - "x": 112, + "x": 114, "y": 0, "w": 23, "h": 26 @@ -423,7 +465,7 @@ "h": 22 }, "frame": { - "x": 135, + "x": 137, "y": 0, "w": 27, "h": 22 @@ -444,7 +486,7 @@ "h": 21 }, "frame": { - "x": 162, + "x": 164, "y": 0, "w": 28, "h": 21 @@ -465,7 +507,7 @@ "h": 21 }, "frame": { - "x": 190, + "x": 192, "y": 0, "w": 28, "h": 21 @@ -486,7 +528,7 @@ "h": 21 }, "frame": { - "x": 218, + "x": 220, "y": 0, "w": 28, "h": 21 @@ -507,7 +549,7 @@ "h": 21 }, "frame": { - "x": 246, + "x": 248, "y": 0, "w": 28, "h": 21 @@ -528,7 +570,7 @@ "h": 21 }, "frame": { - "x": 274, + "x": 276, "y": 0, "w": 28, "h": 21 @@ -549,7 +591,7 @@ "h": 21 }, "frame": { - "x": 302, + "x": 304, "y": 0, "w": 28, "h": 21 @@ -570,7 +612,7 @@ "h": 20 }, "frame": { - "x": 330, + "x": 332, "y": 0, "w": 26, "h": 20 @@ -591,7 +633,7 @@ "h": 20 }, "frame": { - "x": 356, + "x": 358, "y": 0, "w": 26, "h": 20 @@ -612,14 +654,14 @@ "h": 20 }, "frame": { - "x": 382, + "x": 384, "y": 0, "w": 25, "h": 20 } }, { - "filename": "lock_capsule", + "filename": "ribbon_gen6", "rotated": false, "trimmed": true, "sourceSize": { @@ -627,16 +669,16 @@ "h": 32 }, "spriteSourceSize": { - "x": 7, - "y": 5, - "w": 19, - "h": 22 + "x": 5, + "y": 2, + "w": 22, + "h": 28 }, "frame": { - "x": 407, + "x": 409, "y": 0, - "w": 19, - "h": 22 + "w": 22, + "h": 28 } }, { @@ -724,7 +766,7 @@ } }, { - "filename": "ribbon_gen6", + "filename": "ribbon_gen8", "rotated": false, "trimmed": true, "sourceSize": { @@ -744,27 +786,6 @@ "h": 28 } }, - { - "filename": "ribbon_gen8", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 2, - "w": 22, - "h": 28 - }, - "frame": { - "x": 22, - "y": 237, - "w": 22, - "h": 28 - } - }, { "filename": "black_augurite", "rotated": false, @@ -781,7 +802,7 @@ }, "frame": { "x": 22, - "y": 265, + "y": 237, "w": 22, "h": 25 } @@ -802,7 +823,7 @@ }, "frame": { "x": 22, - "y": 290, + "y": 262, "w": 23, "h": 24 } @@ -823,7 +844,7 @@ }, "frame": { "x": 22, - "y": 314, + "y": 286, "w": 24, "h": 24 } @@ -844,7 +865,7 @@ }, "frame": { "x": 22, - "y": 338, + "y": 310, "w": 24, "h": 24 } @@ -865,7 +886,7 @@ }, "frame": { "x": 22, - "y": 362, + "y": 334, "w": 24, "h": 24 } @@ -886,13 +907,13 @@ }, "frame": { "x": 22, - "y": 386, + "y": 358, "w": 24, "h": 24 } }, { - "filename": "mega_bracelet", + "filename": "earth_plate", "rotated": false, "trimmed": true, "sourceSize": { @@ -900,20 +921,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, - "y": 8, - "w": 20, - "h": 16 + "x": 4, + "y": 4, + "w": 24, + "h": 24 }, "frame": { "x": 22, - "y": 410, - "w": 20, - "h": 16 + "y": 382, + "w": 24, + "h": 24 } }, { - "filename": "relic_band", + "filename": "fist_plate", "rotated": false, "trimmed": true, "sourceSize": { @@ -921,16 +942,16 @@ "h": 32 }, "spriteSourceSize": { - "x": 7, - "y": 9, - "w": 17, - "h": 16 + "x": 4, + "y": 4, + "w": 24, + "h": 24 }, "frame": { - "x": 42, - "y": 410, - "w": 17, - "h": 16 + "x": 23, + "y": 406, + "w": 24, + "h": 24 } }, { @@ -955,7 +976,7 @@ } }, { - "filename": "abomasite", + "filename": "mega_bracelet", "rotated": false, "trimmed": true, "sourceSize": { @@ -963,20 +984,41 @@ "h": 32 }, "spriteSourceSize": { - "x": 8, + "x": 6, "y": 8, - "w": 16, + "w": 20, "h": 16 }, "frame": { "x": 28, "y": 70, - "w": 16, + "w": 20, "h": 16 } }, { - "filename": "absolite", + "filename": "choice_specs", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 8, + "w": 24, + "h": 18 + }, + "frame": { + "x": 59, + "y": 53, + "w": 24, + "h": 18 + } + }, + { + "filename": "calcium", "rotated": false, "trimmed": true, "sourceSize": { @@ -985,15 +1027,36 @@ }, "spriteSourceSize": { "x": 8, - "y": 8, + "y": 4, "w": 16, - "h": 16 + "h": 24 }, "frame": { - "x": 44, - "y": 70, + "x": 39, + "y": 86, "w": 16, - "h": 16 + "h": 24 + } + }, + { + "filename": "carbos", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 4, + "w": 16, + "h": 24 + }, + "frame": { + "x": 39, + "y": 110, + "w": 16, + "h": 24 } }, { @@ -1010,52 +1073,10 @@ "w": 21, "h": 24 }, - "frame": { - "x": 39, - "y": 86, - "w": 21, - "h": 24 - } - }, - { - "filename": "earth_plate", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 4, - "w": 24, - "h": 24 - }, - "frame": { - "x": 39, - "y": 110, - "w": 24, - "h": 24 - } - }, - { - "filename": "fist_plate", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 4, - "w": 24, - "h": 24 - }, "frame": { "x": 39, "y": 134, - "w": 24, + "w": 21, "h": 24 } }, @@ -1158,7 +1179,7 @@ "h": 24 }, "frame": { - "x": 44, + "x": 45, "y": 254, "w": 24, "h": 24 @@ -1179,7 +1200,7 @@ "h": 24 }, "frame": { - "x": 45, + "x": 46, "y": 278, "w": 24, "h": 24 @@ -1270,7 +1291,7 @@ } }, { - "filename": "ability_capsule", + "filename": "kings_rock", "rotated": false, "trimmed": true, "sourceSize": { @@ -1278,41 +1299,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 4, - "y": 9, - "w": 24, - "h": 14 - }, - "frame": { - "x": 135, - "y": 22, - "w": 24, - "h": 14 - } - }, - { - "filename": "calcium", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 8, + "x": 5, "y": 4, - "w": 16, + "w": 23, "h": 24 }, "frame": { - "x": 59, - "y": 27, - "w": 16, + "x": 47, + "y": 398, + "w": 23, "h": 24 } }, { - "filename": "lucky_punch_master", + "filename": "silver_powder", "rotated": false, "trimmed": true, "sourceSize": { @@ -1321,99 +1321,15 @@ }, "spriteSourceSize": { "x": 4, - "y": 4, + "y": 11, "w": 24, - "h": 24 + "h": 15 }, "frame": { - "x": 75, - "y": 26, + "x": 48, + "y": 71, "w": 24, - "h": 24 - } - }, - { - "filename": "lucky_punch_ultra", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 4, - "w": 24, - "h": 24 - }, - "frame": { - "x": 99, - "y": 26, - "w": 24, - "h": 24 - } - }, - { - "filename": "revive", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 10, - "y": 8, - "w": 12, - "h": 17 - }, - "frame": { - "x": 123, - "y": 26, - "w": 12, - "h": 17 - } - }, - { - "filename": "big_mushroom", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 6, - "w": 19, - "h": 19 - }, - "frame": { - "x": 59, - "y": 51, - "w": 19, - "h": 19 - } - }, - { - "filename": "clefairy_doll", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 5, - "w": 24, - "h": 23 - }, - "frame": { - "x": 78, - "y": 50, - "w": 24, - "h": 23 + "h": 15 } }, { @@ -1431,117 +1347,12 @@ "h": 24 }, "frame": { - "x": 60, - "y": 70, + "x": 55, + "y": 86, "w": 18, "h": 24 } }, - { - "filename": "coin_case", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 5, - "w": 24, - "h": 23 - }, - "frame": { - "x": 78, - "y": 73, - "w": 24, - "h": 23 - } - }, - { - "filename": "kings_rock", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 4, - "w": 23, - "h": 24 - }, - "frame": { - "x": 102, - "y": 50, - "w": 23, - "h": 24 - } - }, - { - "filename": "berry_pouch", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 5, - "w": 23, - "h": 23 - }, - "frame": { - "x": 102, - "y": 74, - "w": 23, - "h": 23 - } - }, - { - "filename": "aerodactylite", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 8, - "y": 8, - "w": 16, - "h": 16 - }, - "frame": { - "x": 60, - "y": 94, - "w": 16, - "h": 16 - } - }, - { - "filename": "carbos", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 8, - "y": 4, - "w": 16, - "h": 24 - }, - "frame": { - "x": 63, - "y": 110, - "w": 16, - "h": 24 - } - }, { "filename": "ether", "rotated": false, @@ -1557,8 +1368,8 @@ "h": 24 }, "frame": { - "x": 63, - "y": 134, + "x": 55, + "y": 110, "w": 18, "h": 24 } @@ -1578,14 +1389,56 @@ "h": 24 }, "frame": { - "x": 63, - "y": 158, + "x": 60, + "y": 134, "w": 18, "h": 24 } }, { - "filename": "lustrous_globe", + "filename": "hp_up", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 4, + "w": 16, + "h": 24 + }, + "frame": { + "x": 63, + "y": 158, + "w": 16, + "h": 24 + } + }, + { + "filename": "iron", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 4, + "w": 16, + "h": 24 + }, + "frame": { + "x": 63, + "y": 182, + "w": 16, + "h": 24 + } + }, + { + "filename": "lucky_punch_master", "rotated": false, "trimmed": true, "sourceSize": { @@ -1599,35 +1452,14 @@ "h": 24 }, "frame": { - "x": 63, - "y": 182, + "x": 68, + "y": 206, "w": 24, "h": 24 } }, { - "filename": "max_revive", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 4, - "w": 22, - "h": 24 - }, - "frame": { - "x": 68, - "y": 206, - "w": 22, - "h": 24 - } - }, - { - "filename": "meadow_plate", + "filename": "lucky_punch_ultra", "rotated": false, "trimmed": true, "sourceSize": { @@ -1648,28 +1480,7 @@ } }, { - "filename": "mind_plate", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 4, - "w": 24, - "h": 24 - }, - "frame": { - "x": 68, - "y": 254, - "w": 24, - "h": 24 - } - }, - { - "filename": "muscle_band", + "filename": "lustrous_globe", "rotated": false, "trimmed": true, "sourceSize": { @@ -1684,13 +1495,34 @@ }, "frame": { "x": 69, + "y": 254, + "w": 24, + "h": 24 + } + }, + { + "filename": "meadow_plate", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 4, + "w": 24, + "h": 24 + }, + "frame": { + "x": 70, "y": 278, "w": 24, "h": 24 } }, { - "filename": "pixie_plate", + "filename": "mind_plate", "rotated": false, "trimmed": true, "sourceSize": { @@ -1711,7 +1543,7 @@ } }, { - "filename": "salac_berry", + "filename": "muscle_band", "rotated": false, "trimmed": true, "sourceSize": { @@ -1732,7 +1564,7 @@ } }, { - "filename": "scanner", + "filename": "pixie_plate", "rotated": false, "trimmed": true, "sourceSize": { @@ -1753,7 +1585,7 @@ } }, { - "filename": "silk_scarf", + "filename": "salac_berry", "rotated": false, "trimmed": true, "sourceSize": { @@ -1774,7 +1606,7 @@ } }, { - "filename": "sky_plate", + "filename": "scanner", "rotated": false, "trimmed": true, "sourceSize": { @@ -1788,35 +1620,14 @@ "h": 24 }, "frame": { - "x": 59, + "x": 70, "y": 398, "w": 24, "h": 24 } }, { - "filename": "hp_up", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 8, - "y": 4, - "w": 16, - "h": 24 - }, - "frame": { - "x": 83, - "y": 398, - "w": 16, - "h": 24 - } - }, - { - "filename": "reveal_glass", + "filename": "ability_capsule", "rotated": false, "trimmed": true, "sourceSize": { @@ -1825,183 +1636,15 @@ }, "spriteSourceSize": { "x": 4, - "y": 4, - "w": 23, - "h": 24 - }, - "frame": { - "x": 79, - "y": 96, - "w": 23, - "h": 24 - } - }, - { - "filename": "dynamax_band", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 4, - "w": 23, - "h": 23 - }, - "frame": { - "x": 102, - "y": 97, - "w": 23, - "h": 23 - } - }, - { - "filename": "splash_plate", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 4, + "y": 9, "w": 24, - "h": 24 + "h": 14 }, "frame": { - "x": 81, - "y": 120, + "x": 137, + "y": 22, "w": 24, - "h": 24 - } - }, - { - "filename": "spooky_plate", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 4, - "w": 24, - "h": 24 - }, - "frame": { - "x": 81, - "y": 144, - "w": 24, - "h": 24 - } - }, - { - "filename": "oval_charm", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 4, - "w": 21, - "h": 24 - }, - "frame": { - "x": 105, - "y": 120, - "w": 21, - "h": 24 - } - }, - { - "filename": "shiny_charm", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 4, - "w": 21, - "h": 24 - }, - "frame": { - "x": 105, - "y": 144, - "w": 21, - "h": 24 - } - }, - { - "filename": "stone_plate", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 4, - "w": 24, - "h": 24 - }, - "frame": { - "x": 87, - "y": 168, - "w": 24, - "h": 24 - } - }, - { - "filename": "iron", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 8, - "y": 4, - "w": 16, - "h": 24 - }, - "frame": { - "x": 111, - "y": 168, - "w": 16, - "h": 24 - } - }, - { - "filename": "sun_stone", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 4, - "w": 24, - "h": 24 - }, - "frame": { - "x": 90, - "y": 192, - "w": 24, - "h": 24 + "h": 14 } }, { @@ -2019,14 +1662,14 @@ "h": 24 }, "frame": { - "x": 114, - "y": 192, + "x": 86, + "y": 27, "w": 17, "h": 24 } }, { - "filename": "toxic_plate", + "filename": "silk_scarf", "rotated": false, "trimmed": true, "sourceSize": { @@ -2040,14 +1683,14 @@ "h": 24 }, "frame": { - "x": 92, - "y": 216, + "x": 103, + "y": 26, "w": 24, "h": 24 } }, { - "filename": "zap_plate", + "filename": "clefairy_doll", "rotated": false, "trimmed": true, "sourceSize": { @@ -2056,15 +1699,78 @@ }, "spriteSourceSize": { "x": 4, - "y": 4, + "y": 5, "w": 24, - "h": 24 + "h": 23 }, "frame": { - "x": 92, - "y": 240, + "x": 127, + "y": 36, "w": 24, - "h": 24 + "h": 23 + } + }, + { + "filename": "coin_case", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 5, + "w": 24, + "h": 23 + }, + "frame": { + "x": 103, + "y": 50, + "w": 24, + "h": 23 + } + }, + { + "filename": "big_nugget", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 6, + "w": 20, + "h": 20 + }, + "frame": { + "x": 83, + "y": 53, + "w": 20, + "h": 20 + } + }, + { + "filename": "dragon_scale", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 8, + "w": 24, + "h": 18 + }, + "frame": { + "x": 127, + "y": 59, + "w": 24, + "h": 18 } }, { @@ -2082,281 +1788,8 @@ "h": 24 }, "frame": { - "x": 116, - "y": 216, - "w": 18, - "h": 24 - } - }, - { - "filename": "max_ether", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 7, - "y": 4, - "w": 18, - "h": 24 - }, - "frame": { - "x": 116, - "y": 240, - "w": 18, - "h": 24 - } - }, - { - "filename": "expert_belt", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 4, - "w": 24, - "h": 23 - }, - "frame": { - "x": 93, - "y": 264, - "w": 24, - "h": 23 - } - }, - { - "filename": "black_belt", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 4, - "w": 22, - "h": 23 - }, - "frame": { - "x": 117, - "y": 264, - "w": 22, - "h": 23 - } - }, - { - "filename": "silver_powder", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 11, - "w": 24, - "h": 15 - }, - "frame": { - "x": 93, - "y": 287, - "w": 24, - "h": 15 - } - }, - { - "filename": "griseous_core", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 23, - "h": 23 - }, - "frame": { - "x": 94, - "y": 302, - "w": 23, - "h": 23 - } - }, - { - "filename": "hearthflame_mask", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 4, - "w": 24, - "h": 23 - }, - "frame": { - "x": 94, - "y": 325, - "w": 24, - "h": 23 - } - }, - { - "filename": "leppa_berry", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 5, - "w": 24, - "h": 23 - }, - "frame": { - "x": 94, - "y": 348, - "w": 24, - "h": 23 - } - }, - { - "filename": "scope_lens", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 5, - "w": 24, - "h": 23 - }, - "frame": { - "x": 94, - "y": 371, - "w": 24, - "h": 23 - } - }, - { - "filename": "bug_tera_shard", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 4, - "w": 22, - "h": 23 - }, - "frame": { - "x": 117, - "y": 287, - "w": 22, - "h": 23 - } - }, - { - "filename": "red_orb", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 4, - "w": 20, - "h": 24 - }, - "frame": { - "x": 99, - "y": 394, - "w": 20, - "h": 24 - } - }, - { - "filename": "candy_overlay", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 8, - "y": 12, - "w": 16, - "h": 15 - }, - "frame": { - "x": 117, - "y": 310, - "w": 16, - "h": 15 - } - }, - { - "filename": "max_lure", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 8, - "y": 4, - "w": 17, - "h": 24 - }, - "frame": { - "x": 118, - "y": 325, - "w": 17, - "h": 24 - } - }, - { - "filename": "max_potion", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 7, - "y": 4, - "w": 18, - "h": 24 - }, - "frame": { - "x": 118, - "y": 349, + "x": 151, + "y": 36, "w": 18, "h": 24 } @@ -2376,35 +1809,14 @@ "h": 21 }, "frame": { - "x": 118, - "y": 373, + "x": 151, + "y": 60, "w": 23, "h": 21 } }, { - "filename": "dark_tera_shard", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 4, - "w": 22, - "h": 23 - }, - "frame": { - "x": 119, - "y": 394, - "w": 22, - "h": 23 - } - }, - { - "filename": "choice_specs", + "filename": "sky_plate", "rotated": false, "trimmed": true, "sourceSize": { @@ -2413,19 +1825,208 @@ }, "spriteSourceSize": { "x": 4, - "y": 8, + "y": 4, "w": 24, - "h": 18 + "h": 24 }, "frame": { - "x": 135, - "y": 36, + "x": 169, + "y": 21, "w": 24, - "h": 18 + "h": 24 } }, { - "filename": "twisted_spoon", + "filename": "splash_plate", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 4, + "w": 24, + "h": 24 + }, + "frame": { + "x": 193, + "y": 21, + "w": 24, + "h": 24 + } + }, + { + "filename": "spooky_plate", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 4, + "w": 24, + "h": 24 + }, + "frame": { + "x": 217, + "y": 21, + "w": 24, + "h": 24 + } + }, + { + "filename": "stone_plate", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 4, + "w": 24, + "h": 24 + }, + "frame": { + "x": 241, + "y": 21, + "w": 24, + "h": 24 + } + }, + { + "filename": "sun_stone", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 4, + "w": 24, + "h": 24 + }, + "frame": { + "x": 265, + "y": 21, + "w": 24, + "h": 24 + } + }, + { + "filename": "toxic_plate", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 4, + "w": 24, + "h": 24 + }, + "frame": { + "x": 289, + "y": 21, + "w": 24, + "h": 24 + } + }, + { + "filename": "max_revive", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 4, + "w": 22, + "h": 24 + }, + "frame": { + "x": 313, + "y": 21, + "w": 22, + "h": 24 + } + }, + { + "filename": "zap_plate", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 4, + "w": 24, + "h": 24 + }, + "frame": { + "x": 335, + "y": 20, + "w": 24, + "h": 24 + } + }, + { + "filename": "expert_belt", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 4, + "w": 24, + "h": 23 + }, + "frame": { + "x": 359, + "y": 20, + "w": 24, + "h": 23 + } + }, + { + "filename": "hearthflame_mask", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 4, + "w": 24, + "h": 23 + }, + "frame": { + "x": 383, + "y": 20, + "w": 24, + "h": 23 + } + }, + { + "filename": "leppa_berry", "rotated": false, "trimmed": true, "sourceSize": { @@ -2439,12 +2040,33 @@ "h": 23 }, "frame": { - "x": 125, - "y": 54, + "x": 407, + "y": 28, "w": 24, "h": 23 } }, + { + "filename": "candy_overlay", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 12, + "w": 16, + "h": 15 + }, + "frame": { + "x": 169, + "y": 45, + "w": 16, + "h": 15 + } + }, { "filename": "exp_balance", "rotated": false, @@ -2460,14 +2082,14 @@ "h": 22 }, "frame": { - "x": 125, - "y": 77, + "x": 185, + "y": 45, "w": 24, "h": 22 } }, { - "filename": "amulet_coin", + "filename": "exp_share", "rotated": false, "trimmed": true, "sourceSize": { @@ -2475,62 +2097,104 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, + "x": 4, + "y": 5, + "w": 24, + "h": 22 + }, + "frame": { + "x": 209, + "y": 45, + "w": 24, + "h": 22 + } + }, + { + "filename": "peat_block", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 5, + "w": 24, + "h": 22 + }, + "frame": { + "x": 233, + "y": 45, + "w": 24, + "h": 22 + } + }, + { + "filename": "scope_lens", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 5, + "w": 24, + "h": 23 + }, + "frame": { + "x": 257, + "y": 45, + "w": 24, + "h": 23 + } + }, + { + "filename": "twisted_spoon", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 5, + "w": 24, + "h": 23 + }, + "frame": { + "x": 281, + "y": 45, + "w": 24, + "h": 23 + } + }, + { + "filename": "berry_pouch", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, "y": 5, "w": 23, - "h": 21 + "h": 23 }, "frame": { - "x": 125, - "y": 99, + "x": 305, + "y": 45, "w": 23, - "h": 21 - } - }, - { - "filename": "dragon_tera_shard", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 4, - "w": 22, - "h": 23 - }, - "frame": { - "x": 126, - "y": 120, - "w": 22, "h": 23 } }, { - "filename": "electric_tera_shard", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 4, - "w": 22, - "h": 23 - }, - "frame": { - "x": 126, - "y": 143, - "w": 22, - "h": 23 - } - }, - { - "filename": "dragon_fang", + "filename": "black_belt", "rotated": false, "trimmed": true, "sourceSize": { @@ -2539,19 +2203,19 @@ }, "spriteSourceSize": { "x": 5, - "y": 5, - "w": 21, + "y": 4, + "w": 22, "h": 23 }, "frame": { - "x": 127, - "y": 166, - "w": 21, + "x": 328, + "y": 45, + "w": 22, "h": 23 } }, { - "filename": "super_lure", + "filename": "max_ether", "rotated": false, "trimmed": true, "sourceSize": { @@ -2559,15 +2223,36 @@ "h": 32 }, "spriteSourceSize": { - "x": 8, + "x": 7, "y": 4, - "w": 17, + "w": 18, "h": 24 }, "frame": { - "x": 131, - "y": 189, - "w": 17, + "x": 350, + "y": 44, + "w": 18, + "h": 24 + } + }, + { + "filename": "reveal_glass", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 4, + "w": 23, + "h": 24 + }, + "frame": { + "x": 368, + "y": 43, + "w": 23, "h": 24 } }, @@ -2586,56 +2271,14 @@ "h": 24 }, "frame": { - "x": 134, - "y": 213, + "x": 391, + "y": 43, "w": 16, "h": 24 } }, { - "filename": "pp_max", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 8, - "y": 4, - "w": 16, - "h": 24 - }, - "frame": { - "x": 134, - "y": 237, - "w": 16, - "h": 24 - } - }, - { - "filename": "pp_up", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 8, - "y": 4, - "w": 16, - "h": 24 - }, - "frame": { - "x": 149, - "y": 54, - "w": 16, - "h": 24 - } - }, - { - "filename": "auspicious_armor", + "filename": "golden_net", "rotated": false, "trimmed": true, "sourceSize": { @@ -2645,478 +2288,16 @@ "spriteSourceSize": { "x": 4, "y": 5, - "w": 23, + "w": 24, "h": 21 }, "frame": { - "x": 149, - "y": 78, - "w": 23, + "x": 407, + "y": 51, + "w": 24, "h": 21 } }, - { - "filename": "exp_share", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 5, - "w": 24, - "h": 22 - }, - "frame": { - "x": 148, - "y": 99, - "w": 24, - "h": 22 - } - }, - { - "filename": "leek", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 5, - "w": 23, - "h": 23 - }, - "frame": { - "x": 148, - "y": 121, - "w": 23, - "h": 23 - } - }, - { - "filename": "rare_candy", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 5, - "w": 23, - "h": 23 - }, - "frame": { - "x": 148, - "y": 144, - "w": 23, - "h": 23 - } - }, - { - "filename": "rarer_candy", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 5, - "w": 23, - "h": 23 - }, - "frame": { - "x": 148, - "y": 167, - "w": 23, - "h": 23 - } - }, - { - "filename": "fairy_tera_shard", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 4, - "w": 22, - "h": 23 - }, - "frame": { - "x": 148, - "y": 190, - "w": 22, - "h": 23 - } - }, - { - "filename": "fighting_tera_shard", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 4, - "w": 22, - "h": 23 - }, - "frame": { - "x": 150, - "y": 213, - "w": 22, - "h": 23 - } - }, - { - "filename": "fire_stone", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 23 - }, - "frame": { - "x": 150, - "y": 236, - "w": 22, - "h": 23 - } - }, - { - "filename": "protein", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 8, - "y": 4, - "w": 16, - "h": 24 - }, - "frame": { - "x": 139, - "y": 261, - "w": 16, - "h": 24 - } - }, - { - "filename": "repel", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 8, - "y": 4, - "w": 16, - "h": 24 - }, - "frame": { - "x": 139, - "y": 285, - "w": 16, - "h": 24 - } - }, - { - "filename": "fire_tera_shard", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 4, - "w": 22, - "h": 23 - }, - "frame": { - "x": 155, - "y": 259, - "w": 22, - "h": 23 - } - }, - { - "filename": "flying_tera_shard", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 4, - "w": 22, - "h": 23 - }, - "frame": { - "x": 155, - "y": 282, - "w": 22, - "h": 23 - } - }, - { - "filename": "super_repel", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 8, - "y": 4, - "w": 16, - "h": 24 - }, - "frame": { - "x": 159, - "y": 22, - "w": 16, - "h": 24 - } - }, - { - "filename": "peat_block", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 5, - "w": 24, - "h": 22 - }, - "frame": { - "x": 175, - "y": 21, - "w": 24, - "h": 22 - } - }, - { - "filename": "healing_charm", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 23, - "h": 22 - }, - "frame": { - "x": 199, - "y": 21, - "w": 23, - "h": 22 - } - }, - { - "filename": "rusted_sword", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 5, - "w": 23, - "h": 22 - }, - "frame": { - "x": 222, - "y": 21, - "w": 23, - "h": 22 - } - }, - { - "filename": "bug_memory", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 245, - "y": 21, - "w": 22, - "h": 22 - } - }, - { - "filename": "charcoal", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 267, - "y": 21, - "w": 22, - "h": 22 - } - }, - { - "filename": "dark_memory", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 289, - "y": 21, - "w": 22, - "h": 22 - } - }, - { - "filename": "dire_hit", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 311, - "y": 21, - "w": 22, - "h": 22 - } - }, - { - "filename": "focus_sash", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 4, - "w": 22, - "h": 23 - }, - "frame": { - "x": 333, - "y": 20, - "w": 22, - "h": 23 - } - }, - { - "filename": "ghost_tera_shard", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 4, - "w": 22, - "h": 23 - }, - "frame": { - "x": 355, - "y": 20, - "w": 22, - "h": 23 - } - }, - { - "filename": "grass_tera_shard", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 4, - "w": 22, - "h": 23 - }, - "frame": { - "x": 377, - "y": 20, - "w": 22, - "h": 23 - } - }, { "filename": "icy_reins_of_unity", "rotated": false, @@ -3132,33 +2313,12 @@ "h": 20 }, "frame": { - "x": 399, - "y": 22, + "x": 174, + "y": 67, "w": 24, "h": 20 } }, - { - "filename": "dragon_scale", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 8, - "w": 24, - "h": 18 - }, - "frame": { - "x": 175, - "y": 43, - "w": 24, - "h": 18 - } - }, { "filename": "metal_powder", "rotated": false, @@ -3174,8 +2334,8 @@ "h": 20 }, "frame": { - "x": 199, - "y": 43, + "x": 198, + "y": 67, "w": 24, "h": 20 } @@ -3195,8 +2355,8 @@ "h": 20 }, "frame": { - "x": 223, - "y": 43, + "x": 222, + "y": 67, "w": 24, "h": 20 } @@ -3216,8 +2376,8 @@ "h": 20 }, "frame": { - "x": 247, - "y": 43, + "x": 246, + "y": 68, "w": 24, "h": 20 } @@ -3237,8 +2397,8 @@ "h": 20 }, "frame": { - "x": 271, - "y": 43, + "x": 270, + "y": 68, "w": 24, "h": 20 } @@ -3258,8 +2418,8 @@ "h": 20 }, "frame": { - "x": 295, - "y": 43, + "x": 294, + "y": 68, "w": 24, "h": 20 } @@ -3279,12 +2439,75 @@ "h": 20 }, "frame": { - "x": 319, - "y": 43, + "x": 318, + "y": 68, "w": 24, "h": 20 } }, + { + "filename": "amulet_coin", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 5, + "w": 23, + "h": 21 + }, + "frame": { + "x": 342, + "y": 68, + "w": 23, + "h": 21 + } + }, + { + "filename": "auspicious_armor", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 5, + "w": 23, + "h": 21 + }, + "frame": { + "x": 368, + "y": 67, + "w": 23, + "h": 21 + } + }, + { + "filename": "pp_max", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 4, + "w": 16, + "h": 24 + }, + "frame": { + "x": 391, + "y": 67, + "w": 16, + "h": 24 + } + }, { "filename": "binding_band", "rotated": false, @@ -3300,56 +2523,14 @@ "h": 20 }, "frame": { - "x": 343, - "y": 43, + "x": 407, + "y": 72, "w": 23, "h": 20 } }, { - "filename": "moon_stone", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 6, - "w": 23, - "h": 21 - }, - "frame": { - "x": 366, - "y": 43, - "w": 23, - "h": 21 - } - }, - { - "filename": "black_glasses", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 8, - "w": 23, - "h": 17 - }, - "frame": { - "x": 165, - "y": 61, - "w": 23, - "h": 17 - } - }, - { - "filename": "unknown", + "filename": "max_lure", "rotated": false, "trimmed": true, "sourceSize": { @@ -3359,39 +2540,18 @@ "spriteSourceSize": { "x": 8, "y": 4, - "w": 16, + "w": 17, "h": 24 }, "frame": { - "x": 172, - "y": 78, - "w": 16, + "x": 73, + "y": 87, + "w": 17, "h": 24 } }, { - "filename": "apicot_berry", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 6, - "w": 19, - "h": 20 - }, - "frame": { - "x": 172, - "y": 102, - "w": 19, - "h": 20 - } - }, - { - "filename": "ground_tera_shard", + "filename": "bug_tera_shard", "rotated": false, "trimmed": true, "sourceSize": { @@ -3405,140 +2565,14 @@ "h": 23 }, "frame": { - "x": 171, - "y": 122, + "x": 73, + "y": 111, "w": 22, "h": 23 } }, { - "filename": "ice_tera_shard", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 4, - "w": 22, - "h": 23 - }, - "frame": { - "x": 171, - "y": 145, - "w": 22, - "h": 23 - } - }, - { - "filename": "dna_splicers", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 171, - "y": 168, - "w": 22, - "h": 22 - } - }, - { - "filename": "never_melt_ice", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 23 - }, - "frame": { - "x": 170, - "y": 190, - "w": 22, - "h": 23 - } - }, - { - "filename": "lansat_berry", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 4, - "w": 21, - "h": 23 - }, - "frame": { - "x": 172, - "y": 213, - "w": 21, - "h": 23 - } - }, - { - "filename": "leaf_stone", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 21, - "h": 23 - }, - "frame": { - "x": 172, - "y": 236, - "w": 21, - "h": 23 - } - }, - { - "filename": "zinc", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 8, - "y": 4, - "w": 16, - "h": 24 - }, - "frame": { - "x": 177, - "y": 259, - "w": 16, - "h": 24 - } - }, - { - "filename": "berry_pot", + "filename": "max_potion", "rotated": false, "trimmed": true, "sourceSize": { @@ -3547,19 +2581,19 @@ }, "spriteSourceSize": { "x": 7, - "y": 5, + "y": 4, "w": 18, - "h": 22 + "h": 24 }, "frame": { - "x": 177, - "y": 283, + "x": 78, + "y": 134, "w": 18, - "h": 22 + "h": 24 } }, { - "filename": "normal_tera_shard", + "filename": "oval_charm", "rotated": false, "trimmed": true, "sourceSize": { @@ -3569,39 +2603,18 @@ "spriteSourceSize": { "x": 6, "y": 4, - "w": 22, - "h": 23 + "w": 21, + "h": 24 }, "frame": { - "x": 188, - "y": 63, - "w": 22, - "h": 23 + "x": 79, + "y": 158, + "w": 21, + "h": 24 } }, { - "filename": "petaya_berry", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 23 - }, - "frame": { - "x": 210, - "y": 63, - "w": 22, - "h": 23 - } - }, - { - "filename": "poison_tera_shard", + "filename": "shiny_charm", "rotated": false, "trimmed": true, "sourceSize": { @@ -3611,18 +2624,18 @@ "spriteSourceSize": { "x": 6, "y": 4, - "w": 22, - "h": 23 + "w": 21, + "h": 24 }, "frame": { - "x": 232, - "y": 63, - "w": 22, - "h": 23 + "x": 79, + "y": 182, + "w": 21, + "h": 24 } }, { - "filename": "psychic_tera_shard", + "filename": "dynamax_band", "rotated": false, "trimmed": true, "sourceSize": { @@ -3630,125 +2643,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, + "x": 4, "y": 4, - "w": 22, + "w": 23, "h": 23 }, "frame": { - "x": 254, - "y": 63, - "w": 22, + "x": 90, + "y": 73, + "w": 23, "h": 23 } }, { - "filename": "reaper_cloth", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 23 - }, - "frame": { - "x": 276, - "y": 63, - "w": 22, - "h": 23 - } - }, - { - "filename": "rock_tera_shard", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 4, - "w": 22, - "h": 23 - }, - "frame": { - "x": 298, - "y": 63, - "w": 22, - "h": 23 - } - }, - { - "filename": "steel_tera_shard", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 4, - "w": 22, - "h": 23 - }, - "frame": { - "x": 320, - "y": 63, - "w": 22, - "h": 23 - } - }, - { - "filename": "stellar_tera_shard", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 4, - "w": 22, - "h": 23 - }, - "frame": { - "x": 342, - "y": 63, - "w": 22, - "h": 23 - } - }, - { - "filename": "dragon_memory", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 364, - "y": 64, - "w": 22, - "h": 22 - } - }, - { - "filename": "aggronite", + "filename": "eviolite", "rotated": false, "trimmed": true, "sourceSize": { @@ -3758,14 +2666,392 @@ "spriteSourceSize": { "x": 8, "y": 8, - "w": 16, - "h": 16 + "w": 15, + "h": 15 }, "frame": { - "x": 188, - "y": 86, + "x": 90, + "y": 96, + "w": 15, + "h": 15 + } + }, + { + "filename": "dark_tera_shard", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 4, + "w": 22, + "h": 23 + }, + "frame": { + "x": 95, + "y": 111, + "w": 22, + "h": 23 + } + }, + { + "filename": "red_orb", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 4, + "w": 20, + "h": 24 + }, + "frame": { + "x": 96, + "y": 134, + "w": 20, + "h": 24 + } + }, + { + "filename": "pp_up", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 4, "w": 16, - "h": 16 + "h": 24 + }, + "frame": { + "x": 100, + "y": 158, + "w": 16, + "h": 24 + } + }, + { + "filename": "protein", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 4, + "w": 16, + "h": 24 + }, + "frame": { + "x": 100, + "y": 182, + "w": 16, + "h": 24 + } + }, + { + "filename": "griseous_core", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 23, + "h": 23 + }, + "frame": { + "x": 92, + "y": 206, + "w": 23, + "h": 23 + } + }, + { + "filename": "leek", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 5, + "w": 23, + "h": 23 + }, + "frame": { + "x": 92, + "y": 229, + "w": 23, + "h": 23 + } + }, + { + "filename": "dragon_tera_shard", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 4, + "w": 22, + "h": 23 + }, + "frame": { + "x": 93, + "y": 252, + "w": 22, + "h": 23 + } + }, + { + "filename": "dragon_fang", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 21, + "h": 23 + }, + "frame": { + "x": 94, + "y": 275, + "w": 21, + "h": 23 + } + }, + { + "filename": "electric_tera_shard", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 4, + "w": 22, + "h": 23 + }, + "frame": { + "x": 94, + "y": 298, + "w": 22, + "h": 23 + } + }, + { + "filename": "fairy_tera_shard", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 4, + "w": 22, + "h": 23 + }, + "frame": { + "x": 94, + "y": 321, + "w": 22, + "h": 23 + } + }, + { + "filename": "fighting_tera_shard", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 4, + "w": 22, + "h": 23 + }, + "frame": { + "x": 94, + "y": 344, + "w": 22, + "h": 23 + } + }, + { + "filename": "fire_stone", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 23 + }, + "frame": { + "x": 94, + "y": 367, + "w": 22, + "h": 23 + } + }, + { + "filename": "fire_tera_shard", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 4, + "w": 22, + "h": 23 + }, + "frame": { + "x": 94, + "y": 390, + "w": 22, + "h": 23 + } + }, + { + "filename": "relic_crown", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 7, + "w": 23, + "h": 18 + }, + "frame": { + "x": 94, + "y": 413, + "w": 23, + "h": 18 + } + }, + { + "filename": "prism_scale", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 9, + "y": 8, + "w": 15, + "h": 15 + }, + "frame": { + "x": 105, + "y": 96, + "w": 15, + "h": 15 + } + }, + { + "filename": "coupon", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 7, + "w": 23, + "h": 19 + }, + "frame": { + "x": 113, + "y": 77, + "w": 23, + "h": 19 + } + }, + { + "filename": "full_heal", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 9, + "y": 4, + "w": 15, + "h": 23 + }, + "frame": { + "x": 136, + "y": 77, + "w": 15, + "h": 23 + } + }, + { + "filename": "golden_mystic_ticket", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 7, + "w": 23, + "h": 19 + }, + "frame": { + "x": 151, + "y": 81, + "w": 23, + "h": 19 } }, { @@ -3783,8 +3069,8 @@ "h": 17 }, "frame": { - "x": 204, - "y": 86, + "x": 174, + "y": 87, "w": 23, "h": 17 } @@ -3804,14 +3090,14 @@ "h": 17 }, "frame": { - "x": 227, - "y": 86, + "x": 197, + "y": 87, "w": 23, "h": 17 } }, { - "filename": "coupon", + "filename": "douse_drive", "rotated": false, "trimmed": true, "sourceSize": { @@ -3820,19 +3106,334 @@ }, "spriteSourceSize": { "x": 4, - "y": 7, + "y": 8, "w": 23, + "h": 17 + }, + "frame": { + "x": 220, + "y": 87, + "w": 23, + "h": 17 + } + }, + { + "filename": "healing_charm", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 23, + "h": 22 + }, + "frame": { + "x": 243, + "y": 88, + "w": 23, + "h": 22 + } + }, + { + "filename": "macho_brace", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 5, + "w": 23, + "h": 23 + }, + "frame": { + "x": 266, + "y": 88, + "w": 23, + "h": 23 + } + }, + { + "filename": "rare_candy", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 5, + "w": 23, + "h": 23 + }, + "frame": { + "x": 289, + "y": 88, + "w": 23, + "h": 23 + } + }, + { + "filename": "rarer_candy", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 5, + "w": 23, + "h": 23 + }, + "frame": { + "x": 312, + "y": 88, + "w": 23, + "h": 23 + } + }, + { + "filename": "rusted_sword", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 5, + "w": 23, + "h": 22 + }, + "frame": { + "x": 335, + "y": 89, + "w": 23, + "h": 22 + } + }, + { + "filename": "abomasite", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 8, + "w": 16, + "h": 16 + }, + "frame": { + "x": 120, + "y": 96, + "w": 16, + "h": 16 + } + }, + { + "filename": "bug_memory", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 117, + "y": 112, + "w": 22, + "h": 22 + } + }, + { + "filename": "flying_tera_shard", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 4, + "w": 22, + "h": 23 + }, + "frame": { + "x": 116, + "y": 134, + "w": 22, + "h": 23 + } + }, + { + "filename": "focus_sash", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 4, + "w": 22, + "h": 23 + }, + "frame": { + "x": 116, + "y": 157, + "w": 22, + "h": 23 + } + }, + { + "filename": "ghost_tera_shard", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 4, + "w": 22, + "h": 23 + }, + "frame": { + "x": 116, + "y": 180, + "w": 22, + "h": 23 + } + }, + { + "filename": "grass_tera_shard", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 4, + "w": 22, + "h": 23 + }, + "frame": { + "x": 139, + "y": 100, + "w": 22, + "h": 23 + } + }, + { + "filename": "berry_juice", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 24, + "h": 23 + }, + "spriteSourceSize": { + "x": 1, + "y": 1, + "w": 22, + "h": 21 + }, + "frame": { + "x": 139, + "y": 123, + "w": 22, + "h": 21 + } + }, + { + "filename": "ground_tera_shard", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 4, + "w": 22, + "h": 23 + }, + "frame": { + "x": 138, + "y": 144, + "w": 22, + "h": 23 + } + }, + { + "filename": "ice_tera_shard", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 4, + "w": 22, + "h": 23 + }, + "frame": { + "x": 138, + "y": 167, + "w": 22, + "h": 23 + } + }, + { + "filename": "black_sludge", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 24, + "h": 24 + }, + "spriteSourceSize": { + "x": 1, + "y": 2, + "w": 22, "h": 19 }, "frame": { - "x": 250, - "y": 86, - "w": 23, + "x": 138, + "y": 190, + "w": 22, "h": 19 } }, { - "filename": "golden_mystic_ticket", + "filename": "never_melt_ice", "rotated": false, "trimmed": true, "sourceSize": { @@ -3840,20 +3441,83 @@ "h": 32 }, "spriteSourceSize": { - "x": 4, - "y": 7, - "w": 23, - "h": 19 + "x": 5, + "y": 5, + "w": 22, + "h": 23 }, "frame": { - "x": 273, - "y": 86, - "w": 23, - "h": 19 + "x": 161, + "y": 104, + "w": 22, + "h": 23 } }, { - "filename": "mystic_ticket", + "filename": "normal_tera_shard", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 4, + "w": 22, + "h": 23 + }, + "frame": { + "x": 183, + "y": 104, + "w": 22, + "h": 23 + } + }, + { + "filename": "petaya_berry", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 23 + }, + "frame": { + "x": 205, + "y": 104, + "w": 22, + "h": 23 + } + }, + { + "filename": "repel", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 4, + "w": 16, + "h": 24 + }, + "frame": { + "x": 227, + "y": 104, + "w": 16, + "h": 24 + } + }, + { + "filename": "moon_stone", "rotated": false, "trimmed": true, "sourceSize": { @@ -3862,15 +3526,15 @@ }, "spriteSourceSize": { "x": 4, - "y": 7, + "y": 6, "w": 23, - "h": 19 + "h": 21 }, "frame": { - "x": 296, - "y": 86, + "x": 243, + "y": 110, "w": 23, - "h": 19 + "h": 21 } }, { @@ -3888,8 +3552,8 @@ "h": 21 }, "frame": { - "x": 319, - "y": 86, + "x": 266, + "y": 111, "w": 23, "h": 21 } @@ -3909,768 +3573,12 @@ "h": 21 }, "frame": { - "x": 342, - "y": 86, + "x": 289, + "y": 111, "w": 23, "h": 21 } }, - { - "filename": "deep_sea_tooth", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 6, - "w": 22, - "h": 21 - }, - "frame": { - "x": 365, - "y": 86, - "w": 22, - "h": 21 - } - }, - { - "filename": "dawn_stone", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 6, - "w": 20, - "h": 21 - }, - "frame": { - "x": 389, - "y": 43, - "w": 20, - "h": 21 - } - }, - { - "filename": "hyper_potion", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 8, - "y": 5, - "w": 17, - "h": 23 - }, - "frame": { - "x": 409, - "y": 42, - "w": 17, - "h": 23 - } - }, - { - "filename": "electirizer", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 386, - "y": 64, - "w": 22, - "h": 22 - } - }, - { - "filename": "sachet", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 4, - "w": 18, - "h": 23 - }, - "frame": { - "x": 408, - "y": 65, - "w": 18, - "h": 23 - } - }, - { - "filename": "dusk_stone", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 6, - "w": 21, - "h": 21 - }, - "frame": { - "x": 387, - "y": 86, - "w": 21, - "h": 21 - } - }, - { - "filename": "razor_fang", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 7, - "y": 6, - "w": 18, - "h": 20 - }, - "frame": { - "x": 408, - "y": 88, - "w": 18, - "h": 20 - } - }, - { - "filename": "pair_of_tickets", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 7, - "w": 23, - "h": 19 - }, - "frame": { - "x": 191, - "y": 103, - "w": 23, - "h": 19 - } - }, - { - "filename": "sharp_beak", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 21, - "h": 23 - }, - "frame": { - "x": 193, - "y": 122, - "w": 21, - "h": 23 - } - }, - { - "filename": "water_tera_shard", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 4, - "w": 22, - "h": 23 - }, - "frame": { - "x": 214, - "y": 103, - "w": 22, - "h": 23 - } - }, - { - "filename": "whipped_dream", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 4, - "w": 21, - "h": 23 - }, - "frame": { - "x": 193, - "y": 145, - "w": 21, - "h": 23 - } - }, - { - "filename": "wide_lens", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 4, - "w": 22, - "h": 23 - }, - "frame": { - "x": 214, - "y": 126, - "w": 22, - "h": 23 - } - }, - { - "filename": "electric_memory", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 193, - "y": 168, - "w": 22, - "h": 22 - } - }, - { - "filename": "enigma_berry", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 192, - "y": 190, - "w": 22, - "h": 22 - } - }, - { - "filename": "blunder_policy", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 6, - "w": 22, - "h": 19 - }, - "frame": { - "x": 214, - "y": 149, - "w": 22, - "h": 19 - } - }, - { - "filename": "fairy_memory", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 215, - "y": 168, - "w": 22, - "h": 22 - } - }, - { - "filename": "fighting_memory", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 214, - "y": 190, - "w": 22, - "h": 22 - } - }, - { - "filename": "fire_memory", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 193, - "y": 212, - "w": 22, - "h": 22 - } - }, - { - "filename": "flying_memory", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 193, - "y": 234, - "w": 22, - "h": 22 - } - }, - { - "filename": "ganlon_berry", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 193, - "y": 256, - "w": 22, - "h": 22 - } - }, - { - "filename": "ghost_memory", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 215, - "y": 212, - "w": 22, - "h": 22 - } - }, - { - "filename": "grass_memory", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 215, - "y": 234, - "w": 22, - "h": 22 - } - }, - { - "filename": "ground_memory", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 215, - "y": 256, - "w": 22, - "h": 22 - } - }, - { - "filename": "guard_spec", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 195, - "y": 278, - "w": 22, - "h": 22 - } - }, - { - "filename": "hard_meteorite", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 7, - "y": 5, - "w": 20, - "h": 22 - }, - "frame": { - "x": 217, - "y": 278, - "w": 20, - "h": 22 - } - }, - { - "filename": "ice_memory", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 236, - "y": 105, - "w": 22, - "h": 22 - } - }, - { - "filename": "ice_stone", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 258, - "y": 105, - "w": 22, - "h": 22 - } - }, - { - "filename": "magmarizer", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 236, - "y": 127, - "w": 22, - "h": 22 - } - }, - { - "filename": "mini_black_hole", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 280, - "y": 105, - "w": 22, - "h": 22 - } - }, - { - "filename": "normal_memory", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 258, - "y": 127, - "w": 22, - "h": 22 - } - }, - { - "filename": "poison_memory", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 280, - "y": 127, - "w": 22, - "h": 22 - } - }, - { - "filename": "dubious_disc", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 7, - "w": 22, - "h": 19 - }, - "frame": { - "x": 236, - "y": 149, - "w": 22, - "h": 19 - } - }, - { - "filename": "protector", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 237, - "y": 168, - "w": 22, - "h": 22 - } - }, - { - "filename": "psychic_memory", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 236, - "y": 190, - "w": 22, - "h": 22 - } - }, - { - "filename": "mystic_water", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 5, - "w": 20, - "h": 23 - }, - "frame": { - "x": 237, - "y": 212, - "w": 20, - "h": 23 - } - }, - { - "filename": "potion", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 8, - "y": 5, - "w": 17, - "h": 23 - }, - "frame": { - "x": 302, - "y": 105, - "w": 17, - "h": 23 - } - }, { "filename": "wellspring_mask", "rotated": false, @@ -4686,14 +3594,14 @@ "h": 21 }, "frame": { - "x": 319, - "y": 107, + "x": 312, + "y": 111, "w": 23, "h": 21 } }, { - "filename": "liechi_berry", + "filename": "charcoal", "rotated": false, "trimmed": true, "sourceSize": { @@ -4702,15 +3610,204 @@ }, "spriteSourceSize": { "x": 5, - "y": 6, + "y": 5, "w": 22, - "h": 21 + "h": 22 }, "frame": { - "x": 302, - "y": 128, + "x": 335, + "y": 111, "w": 22, - "h": 21 + "h": 22 + } + }, + { + "filename": "mystic_ticket", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 7, + "w": 23, + "h": 19 + }, + "frame": { + "x": 161, + "y": 127, + "w": 23, + "h": 19 + } + }, + { + "filename": "poison_tera_shard", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 4, + "w": 22, + "h": 23 + }, + "frame": { + "x": 160, + "y": 146, + "w": 22, + "h": 23 + } + }, + { + "filename": "psychic_tera_shard", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 4, + "w": 22, + "h": 23 + }, + "frame": { + "x": 160, + "y": 169, + "w": 22, + "h": 23 + } + }, + { + "filename": "pair_of_tickets", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 7, + "w": 23, + "h": 19 + }, + "frame": { + "x": 184, + "y": 127, + "w": 23, + "h": 19 + } + }, + { + "filename": "reaper_cloth", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 23 + }, + "frame": { + "x": 182, + "y": 146, + "w": 22, + "h": 23 + } + }, + { + "filename": "rock_tera_shard", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 4, + "w": 22, + "h": 23 + }, + "frame": { + "x": 182, + "y": 169, + "w": 22, + "h": 23 + } + }, + { + "filename": "blue_orb", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 6, + "w": 20, + "h": 20 + }, + "frame": { + "x": 207, + "y": 127, + "w": 20, + "h": 20 + } + }, + { + "filename": "steel_tera_shard", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 4, + "w": 22, + "h": 23 + }, + "frame": { + "x": 204, + "y": 147, + "w": 22, + "h": 23 + } + }, + { + "filename": "dark_memory", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 204, + "y": 170, + "w": 22, + "h": 22 } }, { @@ -4728,8 +3825,8 @@ "h": 20 }, "frame": { - "x": 342, - "y": 107, + "x": 160, + "y": 192, "w": 23, "h": 20 } @@ -4749,12 +3846,852 @@ "h": 20 }, "frame": { - "x": 365, - "y": 107, + "x": 183, + "y": 192, "w": 23, "h": 20 } }, + { + "filename": "dawn_stone", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 6, + "w": 20, + "h": 21 + }, + "frame": { + "x": 206, + "y": 192, + "w": 20, + "h": 21 + } + }, + { + "filename": "super_repel", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 4, + "w": 16, + "h": 24 + }, + "frame": { + "x": 227, + "y": 128, + "w": 16, + "h": 24 + } + }, + { + "filename": "deep_sea_tooth", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 6, + "w": 22, + "h": 21 + }, + "frame": { + "x": 243, + "y": 131, + "w": 22, + "h": 21 + } + }, + { + "filename": "stellar_tera_shard", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 4, + "w": 22, + "h": 23 + }, + "frame": { + "x": 226, + "y": 152, + "w": 22, + "h": 23 + } + }, + { + "filename": "water_tera_shard", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 4, + "w": 22, + "h": 23 + }, + "frame": { + "x": 226, + "y": 175, + "w": 22, + "h": 23 + } + }, + { + "filename": "deep_sea_scale", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 6, + "w": 22, + "h": 20 + }, + "frame": { + "x": 265, + "y": 132, + "w": 22, + "h": 20 + } + }, + { + "filename": "wide_lens", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 4, + "w": 22, + "h": 23 + }, + "frame": { + "x": 248, + "y": 152, + "w": 22, + "h": 23 + } + }, + { + "filename": "dire_hit", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 248, + "y": 175, + "w": 22, + "h": 22 + } + }, + { + "filename": "dna_splicers", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 287, + "y": 132, + "w": 22, + "h": 22 + } + }, + { + "filename": "dragon_memory", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 309, + "y": 132, + "w": 22, + "h": 22 + } + }, + { + "filename": "super_lure", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 4, + "w": 17, + "h": 24 + }, + "frame": { + "x": 270, + "y": 152, + "w": 17, + "h": 24 + } + }, + { + "filename": "electirizer", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 287, + "y": 154, + "w": 22, + "h": 22 + } + }, + { + "filename": "electric_memory", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 309, + "y": 154, + "w": 22, + "h": 22 + } + }, + { + "filename": "enigma_berry", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 270, + "y": 176, + "w": 22, + "h": 22 + } + }, + { + "filename": "fairy_memory", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 292, + "y": 176, + "w": 22, + "h": 22 + } + }, + { + "filename": "fighting_memory", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 331, + "y": 133, + "w": 22, + "h": 22 + } + }, + { + "filename": "fire_memory", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 331, + "y": 155, + "w": 22, + "h": 22 + } + }, + { + "filename": "hyper_potion", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 5, + "w": 17, + "h": 23 + }, + "frame": { + "x": 314, + "y": 176, + "w": 17, + "h": 23 + } + }, + { + "filename": "flying_memory", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 331, + "y": 177, + "w": 22, + "h": 22 + } + }, + { + "filename": "blunder_policy", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 6, + "w": 22, + "h": 19 + }, + "frame": { + "x": 226, + "y": 198, + "w": 22, + "h": 19 + } + }, + { + "filename": "fairy_feather", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 7, + "w": 22, + "h": 20 + }, + "frame": { + "x": 248, + "y": 197, + "w": 22, + "h": 20 + } + }, + { + "filename": "dubious_disc", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 7, + "w": 22, + "h": 19 + }, + "frame": { + "x": 270, + "y": 198, + "w": 22, + "h": 19 + } + }, + { + "filename": "ganlon_berry", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 292, + "y": 198, + "w": 22, + "h": 22 + } + }, + { + "filename": "ghost_memory", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 314, + "y": 199, + "w": 22, + "h": 22 + } + }, + { + "filename": "berry_pot", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 7, + "y": 5, + "w": 18, + "h": 22 + }, + "frame": { + "x": 336, + "y": 199, + "w": 18, + "h": 22 + } + }, + { + "filename": "grass_memory", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 116, + "y": 203, + "w": 22, + "h": 22 + } + }, + { + "filename": "ground_memory", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 115, + "y": 225, + "w": 22, + "h": 22 + } + }, + { + "filename": "guard_spec", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 115, + "y": 247, + "w": 22, + "h": 22 + } + }, + { + "filename": "ice_memory", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 115, + "y": 269, + "w": 22, + "h": 22 + } + }, + { + "filename": "ice_stone", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 138, + "y": 209, + "w": 22, + "h": 22 + } + }, + { + "filename": "lansat_berry", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 4, + "w": 21, + "h": 23 + }, + "frame": { + "x": 137, + "y": 231, + "w": 21, + "h": 23 + } + }, + { + "filename": "leaf_stone", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 21, + "h": 23 + }, + "frame": { + "x": 137, + "y": 254, + "w": 21, + "h": 23 + } + }, + { + "filename": "liechi_berry", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 6, + "w": 22, + "h": 21 + }, + "frame": { + "x": 160, + "y": 212, + "w": 22, + "h": 21 + } + }, + { + "filename": "magmarizer", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 182, + "y": 212, + "w": 22, + "h": 22 + } + }, + { + "filename": "mini_black_hole", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 204, + "y": 213, + "w": 22, + "h": 22 + } + }, + { + "filename": "moon_flute", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 158, + "y": 233, + "w": 22, + "h": 22 + } + }, + { + "filename": "normal_memory", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 158, + "y": 255, + "w": 22, + "h": 22 + } + }, + { + "filename": "poison_memory", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 180, + "y": 234, + "w": 22, + "h": 22 + } + }, + { + "filename": "protector", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 180, + "y": 256, + "w": 22, + "h": 22 + } + }, + { + "filename": "psychic_memory", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 202, + "y": 235, + "w": 22, + "h": 22 + } + }, { "filename": "rock_memory", "rotated": false, @@ -4770,12 +4707,33 @@ "h": 22 }, "frame": { - "x": 237, - "y": 235, + "x": 202, + "y": 257, "w": 22, "h": 22 } }, + { + "filename": "malicious_armor", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 6, + "w": 22, + "h": 20 + }, + "frame": { + "x": 226, + "y": 217, + "w": 22, + "h": 20 + } + }, { "filename": "scroll_of_darkness", "rotated": false, @@ -4791,8 +4749,8 @@ "h": 22 }, "frame": { - "x": 237, - "y": 257, + "x": 248, + "y": 217, "w": 22, "h": 22 } @@ -4812,14 +4770,14 @@ "h": 22 }, "frame": { - "x": 237, - "y": 279, + "x": 270, + "y": 217, "w": 22, "h": 22 } }, { - "filename": "upgrade", + "filename": "sharp_beak", "rotated": false, "trimmed": true, "sourceSize": { @@ -4828,15 +4786,15 @@ }, "spriteSourceSize": { "x": 5, - "y": 7, - "w": 22, - "h": 19 + "y": 5, + "w": 21, + "h": 23 }, "frame": { - "x": 258, - "y": 149, - "w": 22, - "h": 19 + "x": 224, + "y": 237, + "w": 21, + "h": 23 } }, { @@ -4854,8 +4812,8 @@ "h": 22 }, "frame": { - "x": 259, - "y": 168, + "x": 292, + "y": 220, "w": 22, "h": 22 } @@ -4875,12 +4833,33 @@ "h": 22 }, "frame": { - "x": 258, - "y": 190, + "x": 314, + "y": 221, "w": 22, "h": 22 } }, + { + "filename": "dusk_stone", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 6, + "w": 21, + "h": 21 + }, + "frame": { + "x": 224, + "y": 260, + "w": 21, + "h": 21 + } + }, { "filename": "steel_memory", "rotated": false, @@ -4896,98 +4875,14 @@ "h": 22 }, "frame": { - "x": 257, - "y": 212, + "x": 245, + "y": 239, "w": 22, "h": 22 } }, { - "filename": "big_nugget", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 6, - "w": 20, - "h": 20 - }, - "frame": { - "x": 388, - "y": 107, - "w": 20, - "h": 20 - } - }, - { - "filename": "oval_stone", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 7, - "y": 7, - "w": 18, - "h": 19 - }, - "frame": { - "x": 408, - "y": 108, - "w": 18, - "h": 19 - } - }, - { - "filename": "metal_alloy", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 7, - "w": 21, - "h": 19 - }, - "frame": { - "x": 280, - "y": 149, - "w": 21, - "h": 19 - } - }, - { - "filename": "sitrus_berry", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 5, - "w": 20, - "h": 22 - }, - "frame": { - "x": 281, - "y": 168, - "w": 20, - "h": 22 - } - }, - { - "filename": "thick_club", + "filename": "sun_flute", "rotated": false, "trimmed": true, "sourceSize": { @@ -5001,894 +4896,12 @@ "h": 22 }, "frame": { - "x": 301, - "y": 149, + "x": 267, + "y": 239, "w": 22, "h": 22 } }, - { - "filename": "thunder_stone", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 280, - "y": 190, - "w": 22, - "h": 22 - } - }, - { - "filename": "tm_bug", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 279, - "y": 212, - "w": 22, - "h": 22 - } - }, - { - "filename": "tm_dark", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 259, - "y": 234, - "w": 22, - "h": 22 - } - }, - { - "filename": "tm_dragon", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 259, - "y": 256, - "w": 22, - "h": 22 - } - }, - { - "filename": "tm_electric", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 259, - "y": 278, - "w": 22, - "h": 22 - } - }, - { - "filename": "tm_fairy", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 281, - "y": 234, - "w": 22, - "h": 22 - } - }, - { - "filename": "tm_fighting", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 281, - "y": 256, - "w": 22, - "h": 22 - } - }, - { - "filename": "tm_fire", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 281, - "y": 278, - "w": 22, - "h": 22 - } - }, - { - "filename": "lum_berry", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 7, - "w": 20, - "h": 19 - }, - "frame": { - "x": 301, - "y": 171, - "w": 20, - "h": 19 - } - }, - { - "filename": "metal_coat", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 5, - "w": 19, - "h": 22 - }, - "frame": { - "x": 302, - "y": 190, - "w": 19, - "h": 22 - } - }, - { - "filename": "tm_flying", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 301, - "y": 212, - "w": 22, - "h": 22 - } - }, - { - "filename": "tm_ghost", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 303, - "y": 234, - "w": 22, - "h": 22 - } - }, - { - "filename": "tm_grass", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 303, - "y": 256, - "w": 22, - "h": 22 - } - }, - { - "filename": "tm_ground", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 303, - "y": 278, - "w": 22, - "h": 22 - } - }, - { - "filename": "poison_barb", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 6, - "w": 21, - "h": 21 - }, - "frame": { - "x": 324, - "y": 128, - "w": 21, - "h": 21 - } - }, - { - "filename": "tm_ice", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 323, - "y": 149, - "w": 22, - "h": 22 - } - }, - { - "filename": "tm_normal", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 321, - "y": 171, - "w": 22, - "h": 22 - } - }, - { - "filename": "tm_poison", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 345, - "y": 127, - "w": 22, - "h": 22 - } - }, - { - "filename": "tm_psychic", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 345, - "y": 149, - "w": 22, - "h": 22 - } - }, - { - "filename": "tm_rock", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 343, - "y": 171, - "w": 22, - "h": 22 - } - }, - { - "filename": "tm_steel", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 367, - "y": 127, - "w": 22, - "h": 22 - } - }, - { - "filename": "tm_water", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 367, - "y": 149, - "w": 22, - "h": 22 - } - }, - { - "filename": "water_memory", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 365, - "y": 171, - "w": 22, - "h": 22 - } - }, - { - "filename": "water_stone", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 389, - "y": 127, - "w": 22, - "h": 22 - } - }, - { - "filename": "full_heal", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 9, - "y": 4, - "w": 15, - "h": 23 - }, - "frame": { - "x": 411, - "y": 127, - "w": 15, - "h": 23 - } - }, - { - "filename": "x_accuracy", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 389, - "y": 149, - "w": 22, - "h": 22 - } - }, - { - "filename": "leftovers", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 8, - "y": 5, - "w": 15, - "h": 22 - }, - "frame": { - "x": 411, - "y": 150, - "w": 15, - "h": 22 - } - }, - { - "filename": "x_attack", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 387, - "y": 171, - "w": 22, - "h": 22 - } - }, - { - "filename": "super_potion", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 8, - "y": 5, - "w": 17, - "h": 23 - }, - "frame": { - "x": 409, - "y": 172, - "w": 17, - "h": 23 - } - }, - { - "filename": "power_herb", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 7, - "w": 20, - "h": 19 - }, - "frame": { - "x": 321, - "y": 193, - "w": 20, - "h": 19 - } - }, - { - "filename": "x_defense", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 323, - "y": 212, - "w": 22, - "h": 22 - } - }, - { - "filename": "razor_claw", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 7, - "w": 20, - "h": 19 - }, - "frame": { - "x": 341, - "y": 193, - "w": 20, - "h": 19 - } - }, - { - "filename": "x_sp_atk", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 325, - "y": 234, - "w": 22, - "h": 22 - } - }, - { - "filename": "x_sp_def", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 325, - "y": 256, - "w": 22, - "h": 22 - } - }, - { - "filename": "x_speed", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 325, - "y": 278, - "w": 22, - "h": 22 - } - }, - { - "filename": "deep_sea_scale", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 6, - "w": 22, - "h": 20 - }, - "frame": { - "x": 361, - "y": 193, - "w": 22, - "h": 20 - } - }, - { - "filename": "fairy_feather", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 7, - "w": 22, - "h": 20 - }, - "frame": { - "x": 383, - "y": 193, - "w": 22, - "h": 20 - } - }, - { - "filename": "shiny_stone", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 6, - "w": 21, - "h": 21 - }, - "frame": { - "x": 405, - "y": 195, - "w": 21, - "h": 21 - } - }, - { - "filename": "mystery_egg", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 8, - "y": 8, - "w": 16, - "h": 18 - }, - "frame": { - "x": 345, - "y": 212, - "w": 16, - "h": 18 - } - }, - { - "filename": "douse_drive", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 8, - "w": 23, - "h": 17 - }, - "frame": { - "x": 361, - "y": 213, - "w": 23, - "h": 17 - } - }, - { - "filename": "masterpiece_teacup", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 7, - "w": 21, - "h": 18 - }, - "frame": { - "x": 384, - "y": 213, - "w": 21, - "h": 18 - } - }, - { - "filename": "zoom_lens", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 6, - "w": 21, - "h": 21 - }, - "frame": { - "x": 405, - "y": 216, - "w": 21, - "h": 21 - } - }, { "filename": "sweet_apple", "rotated": false, @@ -5904,8 +4917,8 @@ "h": 21 }, "frame": { - "x": 347, - "y": 230, + "x": 245, + "y": 261, "w": 22, "h": 21 } @@ -5925,12 +4938,54 @@ "h": 21 }, "frame": { - "x": 347, - "y": 251, + "x": 267, + "y": 261, "w": 22, "h": 21 } }, + { + "filename": "thick_club", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 289, + "y": 242, + "w": 22, + "h": 22 + } + }, + { + "filename": "hard_meteorite", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 7, + "y": 5, + "w": 20, + "h": 22 + }, + "frame": { + "x": 336, + "y": 221, + "w": 20, + "h": 22 + } + }, { "filename": "tart_apple", "rotated": false, @@ -5946,56 +5001,14 @@ "h": 21 }, "frame": { - "x": 347, - "y": 272, + "x": 289, + "y": 264, "w": 22, "h": 21 } }, { - "filename": "eviolite", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 8, - "y": 8, - "w": 15, - "h": 15 - }, - "frame": { - "x": 369, - "y": 230, - "w": 15, - "h": 15 - } - }, - { - "filename": "sharp_meteorite", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 8, - "w": 21, - "h": 18 - }, - "frame": { - "x": 384, - "y": 231, - "w": 21, - "h": 18 - } - }, - { - "filename": "unremarkable_teacup", + "filename": "thunder_stone", "rotated": false, "trimmed": true, "sourceSize": { @@ -6004,145 +5017,19 @@ }, "spriteSourceSize": { "x": 5, - "y": 7, - "w": 21, - "h": 18 - }, - "frame": { - "x": 405, - "y": 237, - "w": 21, - "h": 18 - } - }, - { - "filename": "prism_scale", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 9, - "y": 8, - "w": 15, - "h": 15 - }, - "frame": { - "x": 369, - "y": 245, - "w": 15, - "h": 15 - } - }, - { - "filename": "metronome", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 7, "y": 5, - "w": 17, + "w": 22, "h": 22 }, "frame": { - "x": 369, - "y": 260, - "w": 17, + "x": 311, + "y": 243, + "w": 22, "h": 22 } }, { - "filename": "quick_claw", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 6, - "w": 19, - "h": 21 - }, - "frame": { - "x": 386, - "y": 249, - "w": 19, - "h": 21 - } - }, - { - "filename": "blue_orb", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 6, - "w": 20, - "h": 20 - }, - "frame": { - "x": 405, - "y": 255, - "w": 20, - "h": 20 - } - }, - { - "filename": "candy_jar", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 6, - "w": 19, - "h": 20 - }, - "frame": { - "x": 386, - "y": 270, - "w": 19, - "h": 20 - } - }, - { - "filename": "golden_egg", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 7, - "y": 6, - "w": 17, - "h": 20 - }, - "frame": { - "x": 369, - "y": 282, - "w": 17, - "h": 20 - } - }, - { - "filename": "malicious_armor", + "filename": "tm_bug", "rotated": false, "trimmed": true, "sourceSize": { @@ -6151,183 +5038,15 @@ }, "spriteSourceSize": { "x": 5, - "y": 6, + "y": 5, "w": 22, - "h": 20 + "h": 22 }, "frame": { - "x": 347, - "y": 293, + "x": 333, + "y": 243, "w": 22, - "h": 20 - } - }, - { - "filename": "everstone", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 8, - "w": 20, - "h": 17 - }, - "frame": { - "x": 405, - "y": 275, - "w": 20, - "h": 17 - } - }, - { - "filename": "hard_stone", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 6, - "w": 19, - "h": 20 - }, - "frame": { - "x": 386, - "y": 290, - "w": 19, - "h": 20 - } - }, - { - "filename": "gb", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 6, - "w": 20, - "h": 20 - }, - "frame": { - "x": 405, - "y": 292, - "w": 20, - "h": 20 - } - }, - { - "filename": "lucky_egg", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 7, - "y": 6, - "w": 17, - "h": 20 - }, - "frame": { - "x": 369, - "y": 302, - "w": 17, - "h": 20 - } - }, - { - "filename": "miracle_seed", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 7, - "w": 19, - "h": 19 - }, - "frame": { - "x": 386, - "y": 310, - "w": 19, - "h": 19 - } - }, - { - "filename": "magnet", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 6, - "w": 20, - "h": 20 - }, - "frame": { - "x": 405, - "y": 312, - "w": 20, - "h": 20 - } - }, - { - "filename": "relic_crown", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 7, - "w": 23, - "h": 18 - }, - "frame": { - "x": 195, - "y": 300, - "w": 23, - "h": 18 - } - }, - { - "filename": "spell_tag", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 7, - "y": 6, - "w": 19, - "h": 21 - }, - "frame": { - "x": 218, - "y": 300, - "w": 19, - "h": 21 + "h": 22 } }, { @@ -6345,14 +5064,14 @@ "h": 20 }, "frame": { - "x": 237, - "y": 301, + "x": 311, + "y": 265, "w": 22, "h": 20 } }, { - "filename": "mb", + "filename": "tm_dark", "rotated": false, "trimmed": true, "sourceSize": { @@ -6360,79 +5079,16 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, - "y": 6, - "w": 20, - "h": 20 + "x": 5, + "y": 5, + "w": 22, + "h": 22 }, "frame": { - "x": 259, - "y": 300, - "w": 20, - "h": 20 - } - }, - { - "filename": "pb", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 6, - "w": 20, - "h": 20 - }, - "frame": { - "x": 279, - "y": 300, - "w": 20, - "h": 20 - } - }, - { - "filename": "pb_gold", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 6, - "w": 20, - "h": 20 - }, - "frame": { - "x": 299, - "y": 300, - "w": 20, - "h": 20 - } - }, - { - "filename": "rb", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 6, - "w": 20, - "h": 20 - }, - "frame": { - "x": 319, - "y": 300, - "w": 20, - "h": 20 + "x": 333, + "y": 265, + "w": 22, + "h": 22 } }, { @@ -6450,8 +5106,890 @@ "h": 17 }, "frame": { - "x": 155, - "y": 305, + "x": 137, + "y": 277, + "w": 23, + "h": 17 + } + }, + { + "filename": "whipped_dream", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 4, + "w": 21, + "h": 23 + }, + "frame": { + "x": 116, + "y": 291, + "w": 21, + "h": 23 + } + }, + { + "filename": "mystic_water", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 5, + "w": 20, + "h": 23 + }, + "frame": { + "x": 116, + "y": 314, + "w": 20, + "h": 23 + } + }, + { + "filename": "sitrus_berry", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 5, + "w": 20, + "h": 22 + }, + "frame": { + "x": 116, + "y": 337, + "w": 20, + "h": 22 + } + }, + { + "filename": "tm_dragon", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 137, + "y": 294, + "w": 22, + "h": 22 + } + }, + { + "filename": "tm_electric", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 136, + "y": 316, + "w": 22, + "h": 22 + } + }, + { + "filename": "tm_fairy", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 136, + "y": 338, + "w": 22, + "h": 22 + } + }, + { + "filename": "gb", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 6, + "w": 20, + "h": 20 + }, + "frame": { + "x": 116, + "y": 359, + "w": 20, + "h": 20 + } + }, + { + "filename": "tm_fighting", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 116, + "y": 379, + "w": 22, + "h": 22 + } + }, + { + "filename": "upgrade", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 7, + "w": 22, + "h": 19 + }, + "frame": { + "x": 136, + "y": 360, + "w": 22, + "h": 19 + } + }, + { + "filename": "tm_fire", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 138, + "y": 379, + "w": 22, + "h": 22 + } + }, + { + "filename": "everstone", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 8, + "w": 20, + "h": 17 + }, + "frame": { + "x": 160, + "y": 277, + "w": 20, + "h": 17 + } + }, + { + "filename": "tm_flying", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 159, + "y": 294, + "w": 22, + "h": 22 + } + }, + { + "filename": "tm_ghost", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 158, + "y": 316, + "w": 22, + "h": 22 + } + }, + { + "filename": "tm_grass", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 158, + "y": 338, + "w": 22, + "h": 22 + } + }, + { + "filename": "metal_alloy", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 7, + "w": 21, + "h": 19 + }, + "frame": { + "x": 158, + "y": 360, + "w": 21, + "h": 19 + } + }, + { + "filename": "lock_capsule", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 7, + "y": 5, + "w": 19, + "h": 22 + }, + "frame": { + "x": 160, + "y": 379, + "w": 19, + "h": 22 + } + }, + { + "filename": "relic_band", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 7, + "y": 9, + "w": 17, + "h": 16 + }, + "frame": { + "x": 180, + "y": 278, + "w": 17, + "h": 16 + } + }, + { + "filename": "metal_coat", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 5, + "w": 19, + "h": 22 + }, + "frame": { + "x": 181, + "y": 294, + "w": 19, + "h": 22 + } + }, + { + "filename": "tm_ground", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 180, + "y": 316, + "w": 22, + "h": 22 + } + }, + { + "filename": "tm_ice", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 180, + "y": 338, + "w": 22, + "h": 22 + } + }, + { + "filename": "tm_normal", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 179, + "y": 360, + "w": 22, + "h": 22 + } + }, + { + "filename": "tm_poison", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 179, + "y": 382, + "w": 22, + "h": 22 + } + }, + { + "filename": "tm_psychic", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 117, + "y": 401, + "w": 22, + "h": 22 + } + }, + { + "filename": "tm_rock", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 139, + "y": 401, + "w": 22, + "h": 22 + } + }, + { + "filename": "sachet", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 4, + "w": 18, + "h": 23 + }, + "frame": { + "x": 161, + "y": 401, + "w": 18, + "h": 23 + } + }, + { + "filename": "tm_steel", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 179, + "y": 404, + "w": 22, + "h": 22 + } + }, + { + "filename": "leftovers", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 5, + "w": 15, + "h": 22 + }, + "frame": { + "x": 358, + "y": 89, + "w": 15, + "h": 22 + } + }, + { + "filename": "razor_fang", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 7, + "y": 6, + "w": 18, + "h": 20 + }, + "frame": { + "x": 373, + "y": 88, + "w": 18, + "h": 20 + } + }, + { + "filename": "metronome", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 7, + "y": 5, + "w": 17, + "h": 22 + }, + "frame": { + "x": 357, + "y": 111, + "w": 17, + "h": 22 + } + }, + { + "filename": "tm_water", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 353, + "y": 133, + "w": 22, + "h": 22 + } + }, + { + "filename": "water_memory", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 353, + "y": 155, + "w": 22, + "h": 22 + } + }, + { + "filename": "water_stone", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 353, + "y": 177, + "w": 22, + "h": 22 + } + }, + { + "filename": "x_accuracy", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 354, + "y": 199, + "w": 22, + "h": 22 + } + }, + { + "filename": "x_attack", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 356, + "y": 221, + "w": 22, + "h": 22 + } + }, + { + "filename": "x_defense", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 355, + "y": 243, + "w": 22, + "h": 22 + } + }, + { + "filename": "x_sp_atk", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 355, + "y": 265, + "w": 22, + "h": 22 + } + }, + { + "filename": "potion", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 5, + "w": 17, + "h": 23 + }, + "frame": { + "x": 374, + "y": 108, + "w": 17, + "h": 23 + } + }, + { + "filename": "unknown", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 4, + "w": 16, + "h": 24 + }, + "frame": { + "x": 391, + "y": 91, + "w": 16, + "h": 24 + } + }, + { + "filename": "x_sp_def", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 407, + "y": 92, + "w": 22, + "h": 22 + } + }, + { + "filename": "zinc", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 4, + "w": 16, + "h": 24 + }, + "frame": { + "x": 375, + "y": 131, + "w": 16, + "h": 24 + } + }, + { + "filename": "super_potion", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 5, + "w": 17, + "h": 23 + }, + "frame": { + "x": 391, + "y": 115, + "w": 17, + "h": 23 + } + }, + { + "filename": "wise_glasses", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 8, + "w": 23, + "h": 17 + }, + "frame": { + "x": 408, + "y": 114, "w": 23, "h": 17 } @@ -6471,14 +6009,14 @@ "h": 22 }, "frame": { - "x": 178, - "y": 305, + "x": 375, + "y": 155, "w": 17, "h": 22 } }, { - "filename": "wise_glasses", + "filename": "x_speed", "rotated": false, "trimmed": true, "sourceSize": { @@ -6486,18 +6024,648 @@ "h": 32 }, "spriteSourceSize": { - "x": 4, + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 375, + "y": 177, + "w": 22, + "h": 22 + } + }, + { + "filename": "poison_barb", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 6, + "w": 21, + "h": 21 + }, + "frame": { + "x": 376, + "y": 199, + "w": 21, + "h": 21 + } + }, + { + "filename": "quick_claw", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 6, + "w": 19, + "h": 21 + }, + "frame": { + "x": 378, + "y": 220, + "w": 19, + "h": 21 + } + }, + { + "filename": "absolite", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, "y": 8, - "w": 23, + "w": 16, + "h": 16 + }, + "frame": { + "x": 391, + "y": 138, + "w": 16, + "h": 16 + } + }, + { + "filename": "shiny_stone", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 6, + "w": 21, + "h": 21 + }, + "frame": { + "x": 392, + "y": 154, + "w": 21, + "h": 21 + } + }, + { + "filename": "oval_stone", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 7, + "y": 7, + "w": 18, + "h": 19 + }, + "frame": { + "x": 413, + "y": 131, + "w": 18, + "h": 19 + } + }, + { + "filename": "baton", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 7, + "y": 7, + "w": 18, + "h": 18 + }, + "frame": { + "x": 413, + "y": 150, + "w": 18, + "h": 18 + } + }, + { + "filename": "candy", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 7, + "y": 11, + "w": 18, + "h": 18 + }, + "frame": { + "x": 413, + "y": 168, + "w": 18, + "h": 18 + } + }, + { + "filename": "mystery_egg", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 8, + "w": 16, + "h": 18 + }, + "frame": { + "x": 397, + "y": 175, + "w": 16, + "h": 18 + } + }, + { + "filename": "dark_stone", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 7, + "y": 7, + "w": 18, + "h": 18 + }, + "frame": { + "x": 413, + "y": 186, + "w": 18, + "h": 18 + } + }, + { + "filename": "aerodactylite", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 8, + "w": 16, + "h": 16 + }, + "frame": { + "x": 397, + "y": 193, + "w": 16, + "h": 16 + } + }, + { + "filename": "flame_orb", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 7, + "y": 7, + "w": 18, + "h": 18 + }, + "frame": { + "x": 413, + "y": 204, + "w": 18, + "h": 18 + } + }, + { + "filename": "aggronite", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 8, + "w": 16, + "h": 16 + }, + "frame": { + "x": 397, + "y": 209, + "w": 16, + "h": 16 + } + }, + { + "filename": "light_ball", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 7, + "y": 7, + "w": 18, + "h": 18 + }, + "frame": { + "x": 413, + "y": 222, + "w": 18, + "h": 18 + } + }, + { + "filename": "alakazite", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 8, + "w": 16, + "h": 16 + }, + "frame": { + "x": 397, + "y": 225, + "w": 16, + "h": 16 + } + }, + { + "filename": "light_stone", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 7, + "y": 7, + "w": 18, + "h": 18 + }, + "frame": { + "x": 413, + "y": 240, + "w": 18, + "h": 18 + } + }, + { + "filename": "zoom_lens", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 6, + "w": 21, + "h": 21 + }, + "frame": { + "x": 200, + "y": 279, + "w": 21, + "h": 21 + } + }, + { + "filename": "lum_berry", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 7, + "w": 20, + "h": 19 + }, + "frame": { + "x": 221, + "y": 281, + "w": 20, + "h": 19 + } + }, + { + "filename": "masterpiece_teacup", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 7, + "w": 21, + "h": 18 + }, + "frame": { + "x": 241, + "y": 282, + "w": 21, + "h": 18 + } + }, + { + "filename": "old_gateau", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 8, + "w": 21, + "h": 18 + }, + "frame": { + "x": 262, + "y": 282, + "w": 21, + "h": 18 + } + }, + { + "filename": "altarianite", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 8, + "w": 16, + "h": 16 + }, + "frame": { + "x": 200, + "y": 300, + "w": 16, + "h": 16 + } + }, + { + "filename": "sharp_meteorite", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 8, + "w": 21, + "h": 18 + }, + "frame": { + "x": 216, + "y": 300, + "w": 21, + "h": 18 + } + }, + { + "filename": "unremarkable_teacup", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 7, + "w": 21, + "h": 18 + }, + "frame": { + "x": 237, + "y": 300, + "w": 21, + "h": 18 + } + }, + { + "filename": "magnet", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 6, + "w": 20, + "h": 20 + }, + "frame": { + "x": 258, + "y": 300, + "w": 20, + "h": 20 + } + }, + { + "filename": "mb", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 6, + "w": 20, + "h": 20 + }, + "frame": { + "x": 283, + "y": 285, + "w": 20, + "h": 20 + } + }, + { + "filename": "pb", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 6, + "w": 20, + "h": 20 + }, + "frame": { + "x": 303, + "y": 285, + "w": 20, + "h": 20 + } + }, + { + "filename": "pb_gold", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 6, + "w": 20, + "h": 20 + }, + "frame": { + "x": 278, + "y": 305, + "w": 20, + "h": 20 + } + }, + { + "filename": "rb", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 6, + "w": 20, + "h": 20 + }, + "frame": { + "x": 298, + "y": 305, + "w": 20, + "h": 20 + } + }, + { + "filename": "revive", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 10, + "y": 8, + "w": 12, "h": 17 }, "frame": { - "x": 195, - "y": 318, - "w": 23, + "x": 202, + "y": 316, + "w": 12, "h": 17 } }, + { + "filename": "power_herb", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 7, + "w": 20, + "h": 19 + }, + "frame": { + "x": 214, + "y": 318, + "w": 20, + "h": 19 + } + }, + { + "filename": "razor_claw", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 7, + "w": 20, + "h": 19 + }, + "frame": { + "x": 234, + "y": 318, + "w": 20, + "h": 19 + } + }, { "filename": "smooth_meteorite", "rotated": false, @@ -6513,8 +6681,8 @@ "h": 20 }, "frame": { - "x": 218, - "y": 321, + "x": 254, + "y": 320, "w": 20, "h": 20 } @@ -6534,33 +6702,12 @@ "h": 20 }, "frame": { - "x": 238, - "y": 321, + "x": 274, + "y": 325, "w": 20, "h": 20 } }, - { - "filename": "alakazite", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 8, - "y": 8, - "w": 16, - "h": 16 - }, - "frame": { - "x": 139, - "y": 309, - "w": 16, - "h": 16 - } - }, { "filename": "ub", "rotated": false, @@ -6576,12 +6723,54 @@ "h": 20 }, "frame": { - "x": 135, + "x": 294, "y": 325, "w": 20, "h": 20 } }, + { + "filename": "spell_tag", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 7, + "y": 6, + "w": 19, + "h": 21 + }, + "frame": { + "x": 202, + "y": 337, + "w": 19, + "h": 21 + } + }, + { + "filename": "apicot_berry", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 6, + "w": 19, + "h": 20 + }, + "frame": { + "x": 221, + "y": 337, + "w": 19, + "h": 20 + } + }, { "filename": "white_herb", "rotated": false, @@ -6597,12 +6786,96 @@ "h": 19 }, "frame": { - "x": 155, - "y": 322, + "x": 323, + "y": 287, "w": 20, "h": 19 } }, + { + "filename": "big_mushroom", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 6, + "w": 19, + "h": 19 + }, + "frame": { + "x": 343, + "y": 287, + "w": 19, + "h": 19 + } + }, + { + "filename": "candy_jar", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 6, + "w": 19, + "h": 20 + }, + "frame": { + "x": 362, + "y": 287, + "w": 19, + "h": 20 + } + }, + { + "filename": "hard_stone", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 6, + "w": 19, + "h": 20 + }, + "frame": { + "x": 318, + "y": 306, + "w": 19, + "h": 20 + } + }, + { + "filename": "miracle_seed", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 7, + "w": 19, + "h": 19 + }, + "frame": { + "x": 337, + "y": 306, + "w": 19, + "h": 19 + } + }, { "filename": "wl_ability_urge", "rotated": false, @@ -6618,8 +6891,8 @@ "h": 18 }, "frame": { - "x": 175, - "y": 327, + "x": 314, + "y": 326, "w": 20, "h": 18 } @@ -6639,14 +6912,14 @@ "h": 18 }, "frame": { - "x": 136, - "y": 345, + "x": 356, + "y": 307, "w": 20, "h": 18 } }, { - "filename": "baton", + "filename": "golden_egg", "rotated": false, "trimmed": true, "sourceSize": { @@ -6655,15 +6928,15 @@ }, "spriteSourceSize": { "x": 7, - "y": 7, - "w": 18, - "h": 18 + "y": 6, + "w": 17, + "h": 20 }, "frame": { - "x": 156, - "y": 341, - "w": 18, - "h": 18 + "x": 376, + "y": 307, + "w": 17, + "h": 20 } }, { @@ -6681,579 +6954,12 @@ "h": 18 }, "frame": { - "x": 174, - "y": 345, + "x": 393, + "y": 241, "w": 20, "h": 18 } }, - { - "filename": "wl_burn_heal", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 8, - "w": 20, - "h": 18 - }, - "frame": { - "x": 195, - "y": 335, - "w": 20, - "h": 18 - } - }, - { - "filename": "wl_custom_spliced", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 8, - "w": 20, - "h": 18 - }, - "frame": { - "x": 215, - "y": 341, - "w": 20, - "h": 18 - } - }, - { - "filename": "wl_custom_thief", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 8, - "w": 20, - "h": 18 - }, - "frame": { - "x": 235, - "y": 341, - "w": 20, - "h": 18 - } - }, - { - "filename": "wl_elixir", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 8, - "w": 20, - "h": 18 - }, - "frame": { - "x": 194, - "y": 353, - "w": 20, - "h": 18 - } - }, - { - "filename": "wl_ether", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 8, - "w": 20, - "h": 18 - }, - "frame": { - "x": 214, - "y": 359, - "w": 20, - "h": 18 - } - }, - { - "filename": "wl_full_heal", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 8, - "w": 20, - "h": 18 - }, - "frame": { - "x": 234, - "y": 359, - "w": 20, - "h": 18 - } - }, - { - "filename": "candy", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 7, - "y": 11, - "w": 18, - "h": 18 - }, - "frame": { - "x": 156, - "y": 359, - "w": 18, - "h": 18 - } - }, - { - "filename": "wl_full_restore", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 8, - "w": 20, - "h": 18 - }, - "frame": { - "x": 174, - "y": 363, - "w": 20, - "h": 18 - } - }, - { - "filename": "wl_guard_spec", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 8, - "w": 20, - "h": 18 - }, - "frame": { - "x": 194, - "y": 371, - "w": 20, - "h": 18 - } - }, - { - "filename": "wl_hyper_potion", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 8, - "w": 20, - "h": 18 - }, - "frame": { - "x": 214, - "y": 377, - "w": 20, - "h": 18 - } - }, - { - "filename": "wl_ice_heal", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 8, - "w": 20, - "h": 18 - }, - "frame": { - "x": 234, - "y": 377, - "w": 20, - "h": 18 - } - }, - { - "filename": "relic_gold", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 9, - "y": 11, - "w": 15, - "h": 11 - }, - "frame": { - "x": 141, - "y": 363, - "w": 15, - "h": 11 - } - }, - { - "filename": "wl_item_drop", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 8, - "w": 20, - "h": 18 - }, - "frame": { - "x": 141, - "y": 377, - "w": 20, - "h": 18 - } - }, - { - "filename": "wl_item_urge", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 8, - "w": 20, - "h": 18 - }, - "frame": { - "x": 141, - "y": 395, - "w": 20, - "h": 18 - } - }, - { - "filename": "wl_max_elixir", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 8, - "w": 20, - "h": 18 - }, - "frame": { - "x": 161, - "y": 381, - "w": 20, - "h": 18 - } - }, - { - "filename": "wl_max_ether", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 8, - "w": 20, - "h": 18 - }, - "frame": { - "x": 161, - "y": 399, - "w": 20, - "h": 18 - } - }, - { - "filename": "wl_max_potion", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 8, - "w": 20, - "h": 18 - }, - "frame": { - "x": 181, - "y": 389, - "w": 20, - "h": 18 - } - }, - { - "filename": "wl_max_revive", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 8, - "w": 20, - "h": 18 - }, - "frame": { - "x": 181, - "y": 407, - "w": 20, - "h": 18 - } - }, - { - "filename": "wl_paralyze_heal", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 8, - "w": 20, - "h": 18 - }, - "frame": { - "x": 201, - "y": 395, - "w": 20, - "h": 18 - } - }, - { - "filename": "wl_potion", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 8, - "w": 20, - "h": 18 - }, - "frame": { - "x": 221, - "y": 395, - "w": 20, - "h": 18 - } - }, - { - "filename": "dark_stone", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 7, - "y": 7, - "w": 18, - "h": 18 - }, - "frame": { - "x": 241, - "y": 395, - "w": 18, - "h": 18 - } - }, - { - "filename": "flame_orb", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 7, - "y": 7, - "w": 18, - "h": 18 - }, - "frame": { - "x": 255, - "y": 341, - "w": 18, - "h": 18 - } - }, - { - "filename": "wl_reset_urge", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 8, - "w": 20, - "h": 18 - }, - "frame": { - "x": 254, - "y": 359, - "w": 20, - "h": 18 - } - }, - { - "filename": "wl_revive", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 8, - "w": 20, - "h": 18 - }, - "frame": { - "x": 254, - "y": 377, - "w": 20, - "h": 18 - } - }, - { - "filename": "light_ball", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 7, - "y": 7, - "w": 18, - "h": 18 - }, - "frame": { - "x": 259, - "y": 395, - "w": 18, - "h": 18 - } - }, - { - "filename": "wl_super_potion", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 8, - "w": 20, - "h": 18 - }, - "frame": { - "x": 259, - "y": 320, - "w": 20, - "h": 18 - } - }, - { - "filename": "light_stone", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 7, - "y": 7, - "w": 18, - "h": 18 - }, - "frame": { - "x": 279, - "y": 320, - "w": 18, - "h": 18 - } - }, { "filename": "toxic_orb", "rotated": false, @@ -7269,14 +6975,14 @@ "h": 18 }, "frame": { - "x": 297, - "y": 320, + "x": 413, + "y": 258, "w": 18, "h": 18 } }, { - "filename": "altarianite", + "filename": "wl_burn_heal", "rotated": false, "trimmed": true, "sourceSize": { @@ -7284,16 +6990,16 @@ "h": 32 }, "spriteSourceSize": { - "x": 8, + "x": 6, "y": 8, - "w": 16, - "h": 16 + "w": 20, + "h": 18 }, "frame": { - "x": 315, - "y": 320, - "w": 16, - "h": 16 + "x": 393, + "y": 259, + "w": 20, + "h": 18 } }, { @@ -7311,8 +7017,8 @@ "h": 16 }, "frame": { - "x": 273, - "y": 338, + "x": 377, + "y": 243, "w": 16, "h": 16 } @@ -7332,12 +7038,138 @@ "h": 16 }, "frame": { - "x": 289, - "y": 338, + "x": 377, + "y": 259, "w": 16, "h": 16 } }, + { + "filename": "relic_gold", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 9, + "y": 11, + "w": 15, + "h": 11 + }, + "frame": { + "x": 377, + "y": 275, + "w": 15, + "h": 11 + } + }, + { + "filename": "lucky_egg", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 7, + "y": 6, + "w": 17, + "h": 20 + }, + "frame": { + "x": 381, + "y": 286, + "w": 17, + "h": 20 + } + }, + { + "filename": "wl_custom_spliced", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 8, + "w": 20, + "h": 18 + }, + "frame": { + "x": 398, + "y": 277, + "w": 20, + "h": 18 + } + }, + { + "filename": "wl_custom_thief", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 8, + "w": 20, + "h": 18 + }, + "frame": { + "x": 398, + "y": 295, + "w": 20, + "h": 18 + } + }, + { + "filename": "wl_elixir", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 8, + "w": 20, + "h": 18 + }, + "frame": { + "x": 393, + "y": 313, + "w": 20, + "h": 18 + } + }, + { + "filename": "wl_ether", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 8, + "w": 20, + "h": 18 + }, + "frame": { + "x": 240, + "y": 340, + "w": 20, + "h": 18 + } + }, { "filename": "banettite", "rotated": false, @@ -7353,12 +7185,96 @@ "h": 16 }, "frame": { - "x": 274, - "y": 354, + "x": 413, + "y": 313, "w": 16, "h": 16 } }, + { + "filename": "wl_full_heal", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 8, + "w": 20, + "h": 18 + }, + "frame": { + "x": 202, + "y": 358, + "w": 20, + "h": 18 + } + }, + { + "filename": "wl_full_restore", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 8, + "w": 20, + "h": 18 + }, + "frame": { + "x": 201, + "y": 376, + "w": 20, + "h": 18 + } + }, + { + "filename": "wl_guard_spec", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 8, + "w": 20, + "h": 18 + }, + "frame": { + "x": 201, + "y": 394, + "w": 20, + "h": 18 + } + }, + { + "filename": "wl_hyper_potion", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 8, + "w": 20, + "h": 18 + }, + "frame": { + "x": 201, + "y": 412, + "w": 20, + "h": 18 + } + }, { "filename": "beedrillite", "rotated": false, @@ -7374,12 +7290,33 @@ "h": 16 }, "frame": { - "x": 274, - "y": 370, + "x": 222, + "y": 357, "w": 16, "h": 16 } }, + { + "filename": "wl_ice_heal", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 8, + "w": 20, + "h": 18 + }, + "frame": { + "x": 238, + "y": 358, + "w": 20, + "h": 18 + } + }, { "filename": "blastoisinite", "rotated": false, @@ -7395,12 +7332,243 @@ "h": 16 }, "frame": { - "x": 290, - "y": 354, + "x": 222, + "y": 373, "w": 16, "h": 16 } }, + { + "filename": "wl_item_drop", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 8, + "w": 20, + "h": 18 + }, + "frame": { + "x": 221, + "y": 389, + "w": 20, + "h": 18 + } + }, + { + "filename": "wl_item_urge", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 8, + "w": 20, + "h": 18 + }, + "frame": { + "x": 221, + "y": 407, + "w": 20, + "h": 18 + } + }, + { + "filename": "wl_max_elixir", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 8, + "w": 20, + "h": 18 + }, + "frame": { + "x": 241, + "y": 376, + "w": 20, + "h": 18 + } + }, + { + "filename": "wl_max_ether", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 8, + "w": 20, + "h": 18 + }, + "frame": { + "x": 241, + "y": 394, + "w": 20, + "h": 18 + } + }, + { + "filename": "wl_max_potion", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 8, + "w": 20, + "h": 18 + }, + "frame": { + "x": 241, + "y": 412, + "w": 20, + "h": 18 + } + }, + { + "filename": "wl_max_revive", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 8, + "w": 20, + "h": 18 + }, + "frame": { + "x": 258, + "y": 358, + "w": 20, + "h": 18 + } + }, + { + "filename": "wl_paralyze_heal", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 8, + "w": 20, + "h": 18 + }, + "frame": { + "x": 261, + "y": 376, + "w": 20, + "h": 18 + } + }, + { + "filename": "wl_potion", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 8, + "w": 20, + "h": 18 + }, + "frame": { + "x": 261, + "y": 394, + "w": 20, + "h": 18 + } + }, + { + "filename": "wl_reset_urge", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 8, + "w": 20, + "h": 18 + }, + "frame": { + "x": 261, + "y": 412, + "w": 20, + "h": 18 + } + }, + { + "filename": "wl_revive", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 8, + "w": 20, + "h": 18 + }, + "frame": { + "x": 278, + "y": 345, + "w": 20, + "h": 18 + } + }, + { + "filename": "wl_super_potion", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 8, + "w": 20, + "h": 18 + }, + "frame": { + "x": 298, + "y": 345, + "w": 20, + "h": 18 + } + }, { "filename": "blazikenite", "rotated": false, @@ -7416,8 +7584,8 @@ "h": 16 }, "frame": { - "x": 290, - "y": 370, + "x": 318, + "y": 344, "w": 16, "h": 16 } @@ -7437,8 +7605,8 @@ "h": 16 }, "frame": { - "x": 305, - "y": 338, + "x": 334, + "y": 326, "w": 16, "h": 16 } @@ -7458,8 +7626,8 @@ "h": 16 }, "frame": { - "x": 306, - "y": 354, + "x": 334, + "y": 342, "w": 16, "h": 16 } @@ -7479,8 +7647,8 @@ "h": 16 }, "frame": { - "x": 306, - "y": 370, + "x": 350, + "y": 325, "w": 16, "h": 16 } @@ -7500,8 +7668,8 @@ "h": 16 }, "frame": { - "x": 331, - "y": 320, + "x": 350, + "y": 341, "w": 16, "h": 16 } @@ -7521,8 +7689,8 @@ "h": 16 }, "frame": { - "x": 321, - "y": 336, + "x": 366, + "y": 327, "w": 16, "h": 16 } @@ -7542,8 +7710,8 @@ "h": 16 }, "frame": { - "x": 322, - "y": 352, + "x": 366, + "y": 343, "w": 16, "h": 16 } @@ -7563,8 +7731,8 @@ "h": 16 }, "frame": { - "x": 322, - "y": 368, + "x": 382, + "y": 331, "w": 16, "h": 16 } @@ -7584,8 +7752,8 @@ "h": 16 }, "frame": { - "x": 337, - "y": 336, + "x": 398, + "y": 331, "w": 16, "h": 16 } @@ -7605,8 +7773,8 @@ "h": 16 }, "frame": { - "x": 338, - "y": 352, + "x": 414, + "y": 329, "w": 16, "h": 16 } @@ -7626,8 +7794,8 @@ "h": 16 }, "frame": { - "x": 338, - "y": 368, + "x": 382, + "y": 347, "w": 16, "h": 16 } @@ -7647,8 +7815,8 @@ "h": 16 }, "frame": { - "x": 347, - "y": 313, + "x": 398, + "y": 347, "w": 16, "h": 16 } @@ -7668,8 +7836,8 @@ "h": 16 }, "frame": { - "x": 277, - "y": 386, + "x": 414, + "y": 345, "w": 16, "h": 16 } @@ -7689,8 +7857,8 @@ "h": 16 }, "frame": { - "x": 293, - "y": 386, + "x": 281, + "y": 363, "w": 16, "h": 16 } @@ -7710,8 +7878,8 @@ "h": 16 }, "frame": { - "x": 309, - "y": 386, + "x": 281, + "y": 379, "w": 16, "h": 16 } @@ -7731,8 +7899,8 @@ "h": 16 }, "frame": { - "x": 325, - "y": 384, + "x": 297, + "y": 363, "w": 16, "h": 16 } @@ -7752,8 +7920,8 @@ "h": 16 }, "frame": { - "x": 341, - "y": 384, + "x": 281, + "y": 395, "w": 16, "h": 16 } @@ -7773,8 +7941,8 @@ "h": 16 }, "frame": { - "x": 277, - "y": 402, + "x": 297, + "y": 379, "w": 16, "h": 16 } @@ -7794,8 +7962,8 @@ "h": 16 }, "frame": { - "x": 293, - "y": 402, + "x": 281, + "y": 411, "w": 16, "h": 16 } @@ -7815,8 +7983,8 @@ "h": 16 }, "frame": { - "x": 309, - "y": 402, + "x": 297, + "y": 395, "w": 16, "h": 16 } @@ -7836,8 +8004,8 @@ "h": 16 }, "frame": { - "x": 325, - "y": 400, + "x": 297, + "y": 411, "w": 16, "h": 16 } @@ -7857,8 +8025,8 @@ "h": 16 }, "frame": { - "x": 341, - "y": 400, + "x": 313, + "y": 363, "w": 16, "h": 16 } @@ -7878,8 +8046,8 @@ "h": 16 }, "frame": { - "x": 353, - "y": 329, + "x": 313, + "y": 379, "w": 16, "h": 16 } @@ -7899,8 +8067,8 @@ "h": 16 }, "frame": { - "x": 369, - "y": 322, + "x": 313, + "y": 395, "w": 16, "h": 16 } @@ -7920,8 +8088,8 @@ "h": 16 }, "frame": { - "x": 354, - "y": 345, + "x": 313, + "y": 411, "w": 16, "h": 16 } @@ -7941,8 +8109,8 @@ "h": 16 }, "frame": { - "x": 354, - "y": 361, + "x": 350, + "y": 357, "w": 16, "h": 16 } @@ -7962,8 +8130,8 @@ "h": 16 }, "frame": { - "x": 385, - "y": 329, + "x": 366, + "y": 359, "w": 16, "h": 16 } @@ -7983,8 +8151,8 @@ "h": 16 }, "frame": { - "x": 401, - "y": 332, + "x": 334, + "y": 358, "w": 16, "h": 16 } @@ -8004,8 +8172,8 @@ "h": 16 }, "frame": { - "x": 357, - "y": 377, + "x": 382, + "y": 363, "w": 16, "h": 16 } @@ -8025,8 +8193,8 @@ "h": 16 }, "frame": { - "x": 357, - "y": 393, + "x": 398, + "y": 363, "w": 16, "h": 16 } @@ -8046,8 +8214,8 @@ "h": 16 }, "frame": { - "x": 357, - "y": 409, + "x": 414, + "y": 361, "w": 16, "h": 16 } @@ -8067,8 +8235,8 @@ "h": 16 }, "frame": { - "x": 370, - "y": 345, + "x": 329, + "y": 374, "w": 16, "h": 16 } @@ -8088,8 +8256,8 @@ "h": 16 }, "frame": { - "x": 370, - "y": 361, + "x": 329, + "y": 390, "w": 16, "h": 16 } @@ -8109,8 +8277,8 @@ "h": 16 }, "frame": { - "x": 373, - "y": 377, + "x": 329, + "y": 406, "w": 16, "h": 16 } @@ -8130,8 +8298,8 @@ "h": 16 }, "frame": { - "x": 373, - "y": 393, + "x": 345, + "y": 374, "w": 16, "h": 16 } @@ -8151,8 +8319,8 @@ "h": 16 }, "frame": { - "x": 373, - "y": 409, + "x": 345, + "y": 390, "w": 16, "h": 16 } @@ -8172,8 +8340,8 @@ "h": 16 }, "frame": { - "x": 386, - "y": 348, + "x": 345, + "y": 406, "w": 16, "h": 16 } @@ -8193,8 +8361,8 @@ "h": 16 }, "frame": { - "x": 402, - "y": 348, + "x": 361, + "y": 375, "w": 16, "h": 16 } @@ -8214,8 +8382,8 @@ "h": 16 }, "frame": { - "x": 389, - "y": 364, + "x": 361, + "y": 391, "w": 16, "h": 16 } @@ -8235,8 +8403,8 @@ "h": 16 }, "frame": { - "x": 389, - "y": 380, + "x": 361, + "y": 407, "w": 16, "h": 16 } @@ -8247,6 +8415,6 @@ "meta": { "app": "https://www.codeandweb.com/texturepacker", "version": "3.0", - "smartupdate": "$TexturePacker:SmartUpdate:c004184e48566e1da6f13477a3348fd3:dc1a5489f7821641aade35ba290bbea7:110e074689c9edd2c54833ce2e4d9270$" + "smartupdate": "$TexturePacker:SmartUpdate:934ea4080bad980d4fea720cc771f133:ed564bc47b79b15a763de57045178e88:110e074689c9edd2c54833ce2e4d9270$" } } diff --git a/public/images/items.png b/public/images/items.png index 4c366e4d72a..5f032b30cfb 100644 Binary files a/public/images/items.png and b/public/images/items.png differ diff --git a/public/images/items/berry_juice.png b/public/images/items/berry_juice.png new file mode 100644 index 00000000000..c0986b804f9 Binary files /dev/null and b/public/images/items/berry_juice.png differ diff --git a/public/images/items/black_sludge.png b/public/images/items/black_sludge.png new file mode 100644 index 00000000000..39684a40310 Binary files /dev/null and b/public/images/items/black_sludge.png differ diff --git a/public/images/items/golden_net.png b/public/images/items/golden_net.png new file mode 100644 index 00000000000..5fea1ee7dba Binary files /dev/null and b/public/images/items/golden_net.png differ diff --git a/public/images/items/leaders_crest.png b/public/images/items/leaders_crest.png new file mode 100644 index 00000000000..45cf1656374 Binary files /dev/null and b/public/images/items/leaders_crest.png differ diff --git a/public/images/items/macho_brace.png b/public/images/items/macho_brace.png new file mode 100644 index 00000000000..2085829e1ce Binary files /dev/null and b/public/images/items/macho_brace.png differ diff --git a/public/images/items/moon_flute.png b/public/images/items/moon_flute.png new file mode 100644 index 00000000000..893cb6a7579 Binary files /dev/null and b/public/images/items/moon_flute.png differ diff --git a/public/images/items/old_gateau.png b/public/images/items/old_gateau.png new file mode 100644 index 00000000000..c910e90f101 Binary files /dev/null and b/public/images/items/old_gateau.png differ diff --git a/public/images/items/sun_flute.png b/public/images/items/sun_flute.png new file mode 100644 index 00000000000..7010c9fefbd Binary files /dev/null and b/public/images/items/sun_flute.png differ diff --git a/public/images/mystery-encounters/berries_abound_bush.json b/public/images/mystery-encounters/berries_abound_bush.json new file mode 100644 index 00000000000..749031d7da8 --- /dev/null +++ b/public/images/mystery-encounters/berries_abound_bush.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "berries_abound_bush.png", + "format": "RGBA8888", + "size": { + "w": 49, + "h": 53 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 49, + "h": 53 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 49, + "h": 53 + }, + "frame": { + "x": 0, + "y": 0, + "w": 49, + "h": 53 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:d5f83625477b5f98b726343f4a3a396f:f4665258986e97345cfeee041b4b8bcf:e7781fcc447e6d12deb2af78c9493c7f$" + } +} diff --git a/public/images/mystery-encounters/berries_abound_bush.png b/public/images/mystery-encounters/berries_abound_bush.png new file mode 100644 index 00000000000..e9be20b4863 Binary files /dev/null and b/public/images/mystery-encounters/berries_abound_bush.png differ diff --git a/public/images/mystery-encounters/dark_deal_porygon.json b/public/images/mystery-encounters/dark_deal_porygon.json new file mode 100644 index 00000000000..5a48d95c18d --- /dev/null +++ b/public/images/mystery-encounters/dark_deal_porygon.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "dark_deal_porygon.png", + "format": "RGBA8888", + "size": { + "w": 36, + "h": 45 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 36, + "h": 45 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 44, + "h": 44 + }, + "frame": { + "x": 0, + "y": 0, + "w": 36, + "h": 45 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:895f0a79b89fa0fb44167f4584fd9a22:357b46953b7e17c6b2f43a62d52855d8:cc1ed0e4f90aaa9dcf1b39a0af1283b0$" + } +} diff --git a/public/images/mystery-encounters/dark_deal_porygon.png b/public/images/mystery-encounters/dark_deal_porygon.png new file mode 100644 index 00000000000..168999fb0f4 Binary files /dev/null and b/public/images/mystery-encounters/dark_deal_porygon.png differ diff --git a/public/images/mystery-encounters/dark_deal_scientist.json b/public/images/mystery-encounters/dark_deal_scientist.json new file mode 100644 index 00000000000..95db5d1b71a --- /dev/null +++ b/public/images/mystery-encounters/dark_deal_scientist.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "dark_deal_scientist.png", + "format": "RGBA8888", + "size": { + "w": 46, + "h": 76 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 44, + "h": 74 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 44, + "h": 74 + }, + "frame": { + "x": 1, + "y": 1, + "w": 44, + "h": 74 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:a7f8ff2bbb362868f51125c254eb6681:cf76e61ddd31a8f46af67ced168c44a2:4fc09abe16c0608828269e5da81d0744$" + } +} diff --git a/public/images/mystery-encounters/dark_deal_scientist.png b/public/images/mystery-encounters/dark_deal_scientist.png new file mode 100644 index 00000000000..453cb767ec1 Binary files /dev/null and b/public/images/mystery-encounters/dark_deal_scientist.png differ diff --git a/public/images/mystery-encounters/department_store_sale_lady.json b/public/images/mystery-encounters/department_store_sale_lady.json new file mode 100644 index 00000000000..5ba5b2019ff --- /dev/null +++ b/public/images/mystery-encounters/department_store_sale_lady.json @@ -0,0 +1,734 @@ +{ + "textures": [ + { + "image": "department_store_sale_lady.png", + "format": "RGBA8888", + "size": { + "w": 399, + "h": 360 + }, + "scale": 1, + "frames": [ + { + "filename": "0000.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 8, + "y": 8, + "w": 56, + "h": 72 + }, + "frame": { + "x": 0, + "y": 0, + "w": 56, + "h": 72 + } + }, + { + "filename": "0001.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 8, + "y": 8, + "w": 56, + "h": 72 + }, + "frame": { + "x": 57, + "y": 0, + "w": 56, + "h": 72 + } + }, + { + "filename": "0002.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 8, + "y": 8, + "w": 56, + "h": 72 + }, + "frame": { + "x": 114, + "y": 0, + "w": 56, + "h": 72 + } + }, + { + "filename": "0003.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 9, + "y": 8, + "w": 55, + "h": 72 + }, + "frame": { + "x": 171, + "y": 0, + "w": 55, + "h": 72 + } + }, + { + "filename": "0004.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 11, + "y": 8, + "w": 54, + "h": 72 + }, + "frame": { + "x": 228, + "y": 0, + "w": 54, + "h": 72 + } + }, + { + "filename": "0005.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 11, + "y": 8, + "w": 54, + "h": 72 + }, + "frame": { + "x": 285, + "y": 0, + "w": 54, + "h": 72 + } + }, + { + "filename": "0006.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 14, + "y": 8, + "w": 52, + "h": 72 + }, + "frame": { + "x": 342, + "y": 0, + "w": 52, + "h": 72 + } + }, + { + "filename": "0007.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 20, + "y": 8, + "w": 48, + "h": 72 + }, + "frame": { + "x": 0, + "y": 72, + "w": 48, + "h": 72 + } + }, + { + "filename": "0008.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 22, + "y": 8, + "w": 47, + "h": 72 + }, + "frame": { + "x": 57, + "y": 72, + "w": 47, + "h": 72 + } + }, + { + "filename": "0009.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 22, + "y": 8, + "w": 47, + "h": 72 + }, + "frame": { + "x": 114, + "y": 72, + "w": 47, + "h": 72 + } + }, + { + "filename": "0010.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 22, + "y": 8, + "w": 48, + "h": 72 + }, + "frame": { + "x": 171, + "y": 72, + "w": 48, + "h": 72 + } + }, + { + "filename": "0011.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 22, + "y": 8, + "w": 48, + "h": 72 + }, + "frame": { + "x": 228, + "y": 72, + "w": 48, + "h": 72 + } + }, + { + "filename": "0012.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 22, + "y": 8, + "w": 48, + "h": 72 + }, + "frame": { + "x": 285, + "y": 72, + "w": 48, + "h": 72 + } + }, + { + "filename": "0013.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 22, + "y": 8, + "w": 48, + "h": 72 + }, + "frame": { + "x": 342, + "y": 72, + "w": 48, + "h": 72 + } + }, + { + "filename": "0014.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 22, + "y": 8, + "w": 49, + "h": 72 + }, + "frame": { + "x": 0, + "y": 144, + "w": 49, + "h": 72 + } + }, + { + "filename": "0015.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 22, + "y": 8, + "w": 49, + "h": 72 + }, + "frame": { + "x": 57, + "y": 144, + "w": 49, + "h": 72 + } + }, + { + "filename": "0016.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 22, + "y": 8, + "w": 49, + "h": 72 + }, + "frame": { + "x": 114, + "y": 144, + "w": 49, + "h": 72 + } + }, + { + "filename": "0017.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 22, + "y": 8, + "w": 49, + "h": 72 + }, + "frame": { + "x": 171, + "y": 144, + "w": 49, + "h": 72 + } + }, + { + "filename": "0018.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 22, + "y": 8, + "w": 48, + "h": 72 + }, + "frame": { + "x": 228, + "y": 144, + "w": 48, + "h": 72 + } + }, + { + "filename": "0019.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 22, + "y": 8, + "w": 48, + "h": 72 + }, + "frame": { + "x": 285, + "y": 144, + "w": 48, + "h": 72 + } + }, + { + "filename": "0020.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 22, + "y": 8, + "w": 48, + "h": 72 + }, + "frame": { + "x": 342, + "y": 144, + "w": 48, + "h": 72 + } + }, + { + "filename": "0021.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 22, + "y": 8, + "w": 48, + "h": 72 + }, + "frame": { + "x": 0, + "y": 216, + "w": 48, + "h": 72 + } + }, + { + "filename": "0022.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 20, + "y": 8, + "w": 50, + "h": 72 + }, + "frame": { + "x": 57, + "y": 216, + "w": 50, + "h": 72 + } + }, + { + "filename": "0023.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 18, + "y": 8, + "w": 51, + "h": 72 + }, + "frame": { + "x": 114, + "y": 216, + "w": 51, + "h": 72 + } + }, + { + "filename": "0024.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 18, + "y": 8, + "w": 51, + "h": 72 + }, + "frame": { + "x": 171, + "y": 216, + "w": 51, + "h": 72 + } + }, + { + "filename": "0025.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 15, + "y": 8, + "w": 53, + "h": 72 + }, + "frame": { + "x": 228, + "y": 216, + "w": 53, + "h": 72 + } + }, + { + "filename": "0026.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 10, + "y": 8, + "w": 57, + "h": 72 + }, + "frame": { + "x": 285, + "y": 216, + "w": 57, + "h": 72 + } + }, + { + "filename": "0027.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 10, + "y": 8, + "w": 56, + "h": 72 + }, + "frame": { + "x": 342, + "y": 216, + "w": 56, + "h": 72 + } + }, + { + "filename": "0028.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 10, + "y": 8, + "w": 56, + "h": 72 + }, + "frame": { + "x": 0, + "y": 288, + "w": 56, + "h": 72 + } + }, + { + "filename": "0029.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 9, + "y": 8, + "w": 55, + "h": 72 + }, + "frame": { + "x": 57, + "y": 288, + "w": 55, + "h": 72 + } + }, + { + "filename": "0030.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 8, + "y": 8, + "w": 56, + "h": 72 + }, + "frame": { + "x": 114, + "y": 288, + "w": 56, + "h": 72 + } + }, + { + "filename": "0031.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 8, + "y": 8, + "w": 56, + "h": 72 + }, + "frame": { + "x": 171, + "y": 288, + "w": 56, + "h": 72 + } + }, + { + "filename": "0032.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 8, + "y": 8, + "w": 56, + "h": 72 + }, + "frame": { + "x": 228, + "y": 288, + "w": 56, + "h": 72 + } + }, + { + "filename": "0033.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 8, + "y": 8, + "w": 56, + "h": 72 + }, + "frame": { + "x": 285, + "y": 288, + "w": 56, + "h": 72 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:e7f062304401dbd7b3ec79512f0ff4cb:0136dac01331f88892a3df26aeab78f5:1ed1e22abb9b55d76337a5a599835c06$" + } +} diff --git a/public/images/mystery-encounters/department_store_sale_lady.png b/public/images/mystery-encounters/department_store_sale_lady.png new file mode 100644 index 00000000000..9dcc1281c9e Binary files /dev/null and b/public/images/mystery-encounters/department_store_sale_lady.png differ diff --git a/public/images/mystery-encounters/encounter_exclaim.png b/public/images/mystery-encounters/encounter_exclaim.png new file mode 100644 index 00000000000..a7727f4da2e Binary files /dev/null and b/public/images/mystery-encounters/encounter_exclaim.png differ diff --git a/public/images/mystery-encounters/encounter_radar.png b/public/images/mystery-encounters/encounter_radar.png new file mode 100644 index 00000000000..deb9426c269 Binary files /dev/null and b/public/images/mystery-encounters/encounter_radar.png differ diff --git a/public/images/mystery-encounters/field_trip_teacher.json b/public/images/mystery-encounters/field_trip_teacher.json new file mode 100644 index 00000000000..52a304b3421 --- /dev/null +++ b/public/images/mystery-encounters/field_trip_teacher.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "field_trip_teacher.png", + "format": "RGBA8888", + "size": { + "w": 43, + "h": 74 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 19, + "y": 8, + "w": 41, + "h": 72 + }, + "frame": { + "x": 1, + "y": 1, + "w": 41, + "h": 72 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:506e5a4ce79c134a7b4af90a90aef244:1b81d3d84bf12cedc419805eaff82548:59bc5dd000b5e72588320b473e31c312$" + } +} diff --git a/public/images/mystery-encounters/field_trip_teacher.png b/public/images/mystery-encounters/field_trip_teacher.png new file mode 100644 index 00000000000..b4332bc0032 Binary files /dev/null and b/public/images/mystery-encounters/field_trip_teacher.png differ diff --git a/public/images/mystery-encounters/fun_and_games_game.json b/public/images/mystery-encounters/fun_and_games_game.json new file mode 100644 index 00000000000..71fb30fda33 --- /dev/null +++ b/public/images/mystery-encounters/fun_and_games_game.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "fun_and_games_game.png", + "format": "RGBA8888", + "size": { + "w": 38, + "h": 82 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 38, + "h": 82 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 38, + "h": 82 + }, + "frame": { + "x": 0, + "y": 0, + "w": 38, + "h": 82 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:d40b6742392c2fe8ca0735b3f561e319:5dcda5410b12f0aa75eb0dd1fbcbe4f9:d171fb17d3017d1f655cd8dd14c252b7$" + } +} diff --git a/public/images/mystery-encounters/fun_and_games_game.png b/public/images/mystery-encounters/fun_and_games_game.png new file mode 100644 index 00000000000..03a3b9c9cbc Binary files /dev/null and b/public/images/mystery-encounters/fun_and_games_game.png differ diff --git a/public/images/mystery-encounters/fun_and_games_man.json b/public/images/mystery-encounters/fun_and_games_man.json new file mode 100644 index 00000000000..9536e108055 --- /dev/null +++ b/public/images/mystery-encounters/fun_and_games_man.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "fun_and_games_man.png", + "format": "RGBA8888", + "size": { + "w": 50, + "h": 77 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 15, + "y": 3, + "w": 50, + "h": 77 + }, + "frame": { + "x": 0, + "y": 0, + "w": 50, + "h": 77 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:e80aa9a809a7cca6d05992cb82f6dbd9:ea9962edd1cdc1e503deecf2ce1863c1:55647352b6547cf03212506309f2abf5$" + } +} diff --git a/public/images/mystery-encounters/fun_and_games_man.png b/public/images/mystery-encounters/fun_and_games_man.png new file mode 100644 index 00000000000..05f94dbd33d Binary files /dev/null and b/public/images/mystery-encounters/fun_and_games_man.png differ diff --git a/public/images/mystery-encounters/fun_and_games_wobbuffet.json b/public/images/mystery-encounters/fun_and_games_wobbuffet.json new file mode 100644 index 00000000000..2f218cd208b --- /dev/null +++ b/public/images/mystery-encounters/fun_and_games_wobbuffet.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "fun_and_games_wobbuffet.png", + "format": "RGBA8888", + "size": { + "w": 45, + "h": 55 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 45, + "h": 55 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 45, + "h": 55 + }, + "frame": { + "x": 0, + "y": 0, + "w": 45, + "h": 55 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:879de17da906ea52e5a71afacb88fcf6:90f64e8eaac4ff1e67373f60c3d98d36:a090cb3294ca1218a4f90ecb97df81d7$" + } +} diff --git a/public/images/mystery-encounters/fun_and_games_wobbuffet.png b/public/images/mystery-encounters/fun_and_games_wobbuffet.png new file mode 100644 index 00000000000..37e7220196a Binary files /dev/null and b/public/images/mystery-encounters/fun_and_games_wobbuffet.png differ diff --git a/public/images/mystery-encounters/global_trade_system.json b/public/images/mystery-encounters/global_trade_system.json new file mode 100644 index 00000000000..ae5d96127b7 --- /dev/null +++ b/public/images/mystery-encounters/global_trade_system.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "global_trade_system.png", + "format": "RGBA8888", + "size": { + "w": 77, + "h": 78 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 77, + "h": 78 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 77, + "h": 78 + }, + "frame": { + "x": 0, + "y": 0, + "w": 77, + "h": 78 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:8a51d7a17b3d8c32f0e5e4a0f15daeb4:6eba29c5345847f735d8b69a05fc49d1:98ad8b8b8d8c4865d7d23ec97b516594$" + } +} diff --git a/public/images/mystery-encounters/global_trade_system.png b/public/images/mystery-encounters/global_trade_system.png new file mode 100644 index 00000000000..cb0ffb0ab20 Binary files /dev/null and b/public/images/mystery-encounters/global_trade_system.png differ diff --git a/public/images/mystery-encounters/lost_at_sea_buoy.json b/public/images/mystery-encounters/lost_at_sea_buoy.json new file mode 100644 index 00000000000..ba5d9567fe5 --- /dev/null +++ b/public/images/mystery-encounters/lost_at_sea_buoy.json @@ -0,0 +1,19 @@ +{ "frames": [ + { + "filename": "0001.png", + "frame": { "x": 0, "y": 0, "w": 46, "h": 60 }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { "x": 0, "y": 0, "w": 46, "h": 60 }, + "sourceSize": { "w": 46, "h": 60 } + } + ], + "meta": { + "app": "https://www.aseprite.org/", + "version": "1.3.7-x64", + "image": "buoy-sheet.png", + "format": "RGBA8888", + "size": { "w": 46, "h": 60 }, + "scale": "1" + } +} diff --git a/public/images/mystery-encounters/lost_at_sea_buoy.png b/public/images/mystery-encounters/lost_at_sea_buoy.png new file mode 100644 index 00000000000..fb957ac29f0 Binary files /dev/null and b/public/images/mystery-encounters/lost_at_sea_buoy.png differ diff --git a/public/images/mystery-encounters/mysterious_chest_blue.json b/public/images/mystery-encounters/mysterious_chest_blue.json new file mode 100644 index 00000000000..c55294a7bdc --- /dev/null +++ b/public/images/mystery-encounters/mysterious_chest_blue.json @@ -0,0 +1,209 @@ +{ + "textures": [ + { + "image": "mysterious_chest_blue.png", + "format": "RGBA8888", + "size": { + "w": 54, + "h": 492 + }, + "scale": 1, + "frames": [ + { + "filename": "0000.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 46, + "h": 39 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 46, + "h": 39 + }, + "frame": { + "x": 0, + "y": 0, + "w": 46, + "h": 39 + } + }, + { + "filename": "0001.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 47, + "h": 35 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 47, + "h": 35 + }, + "frame": { + "x": 0, + "y": 39, + "w": 47, + "h": 35 + } + }, + { + "filename": "0002.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 46, + "h": 39 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 46, + "h": 39 + }, + "frame": { + "x": 0, + "y": 74, + "w": 46, + "h": 39 + } + }, + { + "filename": "0003.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 46, + "h": 46 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 46, + "h": 46 + }, + "frame": { + "x": 0, + "y": 113, + "w": 46, + "h": 46 + } + }, + { + "filename": "0004.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 53, + "h": 65 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 53, + "h": 65 + }, + "frame": { + "x": 0, + "y": 159, + "w": 53, + "h": 65 + } + }, + { + "filename": "0005.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 54, + "h": 67 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 54, + "h": 67 + }, + "frame": { + "x": 0, + "y": 224, + "w": 54, + "h": 67 + } + }, + { + "filename": "0006.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 54, + "h": 67 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 54, + "h": 67 + }, + "frame": { + "x": 0, + "y": 291, + "w": 54, + "h": 67 + } + }, + { + "filename": "0007.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 54, + "h": 67 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 54, + "h": 67 + }, + "frame": { + "x": 0, + "y": 358, + "w": 54, + "h": 67 + } + }, + { + "filename": "0008.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 54, + "h": 67 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 54, + "h": 67 + }, + "frame": { + "x": 0, + "y": 425, + "w": 54, + "h": 67 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:017ecc2437e580a185f9843f97e80da5:f44ef1c27a4a17183a5bcf1f7fc8ce6a:f4f3c064e6c93b8d1290f93bee927f60$" + } +} diff --git a/public/images/mystery-encounters/mysterious_chest_blue.png b/public/images/mystery-encounters/mysterious_chest_blue.png new file mode 100644 index 00000000000..e67bdcafa04 Binary files /dev/null and b/public/images/mystery-encounters/mysterious_chest_blue.png differ diff --git a/public/images/mystery-encounters/mysterious_chest_red.json b/public/images/mystery-encounters/mysterious_chest_red.json new file mode 100644 index 00000000000..fe560ecf43c --- /dev/null +++ b/public/images/mystery-encounters/mysterious_chest_red.json @@ -0,0 +1,209 @@ +{ + "textures": [ + { + "image": "mysterious_chest_red.png", + "format": "RGBA8888", + "size": { + "w": 54, + "h": 492 + }, + "scale": 1, + "frames": [ + { + "filename": "0000.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 46, + "h": 39 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 46, + "h": 39 + }, + "frame": { + "x": 0, + "y": 0, + "w": 46, + "h": 39 + } + }, + { + "filename": "0001.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 47, + "h": 35 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 47, + "h": 35 + }, + "frame": { + "x": 0, + "y": 39, + "w": 47, + "h": 35 + } + }, + { + "filename": "0002.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 46, + "h": 39 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 46, + "h": 39 + }, + "frame": { + "x": 0, + "y": 74, + "w": 46, + "h": 39 + } + }, + { + "filename": "0003.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 46, + "h": 46 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 46, + "h": 46 + }, + "frame": { + "x": 0, + "y": 113, + "w": 46, + "h": 46 + } + }, + { + "filename": "0004.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 53, + "h": 65 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 53, + "h": 65 + }, + "frame": { + "x": 0, + "y": 159, + "w": 53, + "h": 65 + } + }, + { + "filename": "0005.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 54, + "h": 67 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 54, + "h": 67 + }, + "frame": { + "x": 0, + "y": 224, + "w": 54, + "h": 67 + } + }, + { + "filename": "0006.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 54, + "h": 67 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 54, + "h": 67 + }, + "frame": { + "x": 0, + "y": 291, + "w": 54, + "h": 67 + } + }, + { + "filename": "0007.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 54, + "h": 67 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 54, + "h": 67 + }, + "frame": { + "x": 0, + "y": 358, + "w": 54, + "h": 67 + } + }, + { + "filename": "0008.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 54, + "h": 67 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 54, + "h": 67 + }, + "frame": { + "x": 0, + "y": 425, + "w": 54, + "h": 67 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:2a0b6c93c5be115efa635d40780603f0:b5fde49f991c2ecc49afedd80cc8a544:a163d960e9966469ae4dde4b53c13496$" + } +} diff --git a/public/images/mystery-encounters/mysterious_chest_red.png b/public/images/mystery-encounters/mysterious_chest_red.png new file mode 100644 index 00000000000..c20a8218be6 Binary files /dev/null and b/public/images/mystery-encounters/mysterious_chest_red.png differ diff --git a/public/images/mystery-encounters/part_timer_crate.json b/public/images/mystery-encounters/part_timer_crate.json new file mode 100644 index 00000000000..0bc67774770 --- /dev/null +++ b/public/images/mystery-encounters/part_timer_crate.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "part_timer_crate.png", + "format": "RGBA8888", + "size": { + "w": 71, + "h": 52 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 56 + }, + "spriteSourceSize": { + "x": 5, + "y": 4, + "w": 71, + "h": 52 + }, + "frame": { + "x": 0, + "y": 0, + "w": 71, + "h": 52 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:c8df5f0b35fb9c2a69b0e4aaa9fa9f91:f1d4643c26f2aed86ad77d354e669aaf:0c073e3c2048ea0779db9429e5e1d8bc$" + } +} diff --git a/public/images/mystery-encounters/part_timer_crate.png b/public/images/mystery-encounters/part_timer_crate.png new file mode 100644 index 00000000000..fb70a6e534a Binary files /dev/null and b/public/images/mystery-encounters/part_timer_crate.png differ diff --git a/public/images/mystery-encounters/pokemon_salesman.json b/public/images/mystery-encounters/pokemon_salesman.json new file mode 100644 index 00000000000..23d9df44f2b --- /dev/null +++ b/public/images/mystery-encounters/pokemon_salesman.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "pokemon_salesman.png", + "format": "RGBA8888", + "size": { + "w": 40, + "h": 80 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 21, + "y": 2, + "w": 38, + "h": 78 + }, + "frame": { + "x": 1, + "y": 1, + "w": 38, + "h": 78 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:dd57e3db21f3933c15be65bec261f4c1:05c7ef32252a5c2d3ad007b7e26fabd7:ae82f52e471ed81e2558206f05476cd7$" + } +} diff --git a/public/images/mystery-encounters/pokemon_salesman.png b/public/images/mystery-encounters/pokemon_salesman.png new file mode 100644 index 00000000000..1251dd8eda7 Binary files /dev/null and b/public/images/mystery-encounters/pokemon_salesman.png differ diff --git a/public/images/mystery-encounters/safari_zone.json b/public/images/mystery-encounters/safari_zone.json new file mode 100644 index 00000000000..fe81d1b9f53 --- /dev/null +++ b/public/images/mystery-encounters/safari_zone.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "safari_zone.png", + "format": "RGBA8888", + "size": { + "w": 120, + "h": 84 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 118, + "h": 82 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 118, + "h": 82 + }, + "frame": { + "x": 1, + "y": 1, + "w": 118, + "h": 82 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:6fad7a61e47043b974153148b4fd3997:5ec4d0890f2f03446daf22c8ae8ba77b:87aa745cd95eef6cbf38935230f4e10f$" + } +} diff --git a/public/images/mystery-encounters/safari_zone.png b/public/images/mystery-encounters/safari_zone.png new file mode 100644 index 00000000000..375d66ebbe9 Binary files /dev/null and b/public/images/mystery-encounters/safari_zone.png differ diff --git a/public/images/mystery-encounters/safari_zone_bait.json b/public/images/mystery-encounters/safari_zone_bait.json new file mode 100644 index 00000000000..4786dd34840 --- /dev/null +++ b/public/images/mystery-encounters/safari_zone_bait.json @@ -0,0 +1,83 @@ +{ + "textures": [ + { + "image": "safari_zone_bait.png", + "format": "RGBA8888", + "size": { + "w": 14, + "h": 43 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 12, + "h": 16 + }, + "spriteSourceSize": { + "x": 0, + "y": 3, + "w": 12, + "h": 13 + }, + "frame": { + "x": 1, + "y": 1, + "w": 12, + "h": 13 + } + }, + { + "filename": "0002.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 12, + "h": 16 + }, + "spriteSourceSize": { + "x": 0, + "y": 3, + "w": 12, + "h": 13 + }, + "frame": { + "x": 1, + "y": 16, + "w": 12, + "h": 13 + } + }, + { + "filename": "0003.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 12, + "h": 16 + }, + "spriteSourceSize": { + "x": 0, + "y": 5, + "w": 11, + "h": 11 + }, + "frame": { + "x": 1, + "y": 31, + "w": 11, + "h": 11 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:f0ec04fcd67ac346dce973693711d032:b697e09191c4312b8faaa0a080a309b7:1af241a52e61fa01ca849aa03c112f85$" + } +} diff --git a/public/images/mystery-encounters/safari_zone_bait.png b/public/images/mystery-encounters/safari_zone_bait.png new file mode 100644 index 00000000000..7de9169d187 Binary files /dev/null and b/public/images/mystery-encounters/safari_zone_bait.png differ diff --git a/public/images/mystery-encounters/safari_zone_mud.json b/public/images/mystery-encounters/safari_zone_mud.json new file mode 100644 index 00000000000..8f58857351e --- /dev/null +++ b/public/images/mystery-encounters/safari_zone_mud.json @@ -0,0 +1,104 @@ +{ + "textures": [ + { + "image": "safari_zone_mud.png", + "format": "RGBA8888", + "size": { + "w": 14, + "h": 68 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 12, + "h": 20 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 12, + "h": 13 + }, + "frame": { + "x": 1, + "y": 1, + "w": 12, + "h": 13 + } + }, + { + "filename": "0002.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 12, + "h": 20 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 12, + "h": 14 + }, + "frame": { + "x": 1, + "y": 16, + "w": 12, + "h": 14 + } + }, + { + "filename": "0003.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 12, + "h": 20 + }, + "spriteSourceSize": { + "x": 0, + "y": 1, + "w": 12, + "h": 16 + }, + "frame": { + "x": 1, + "y": 32, + "w": 12, + "h": 16 + } + }, + { + "filename": "0004.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 12, + "h": 20 + }, + "spriteSourceSize": { + "x": 0, + "y": 3, + "w": 12, + "h": 17 + }, + "frame": { + "x": 1, + "y": 50, + "w": 12, + "h": 17 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:4f18a8effb8f01eb70f9f25b8294c1bf:ad663a73c51f780bbf45d00a52519553:c64f6b8befc3d5e9f836246d2b9536be$" + } +} diff --git a/public/images/mystery-encounters/safari_zone_mud.png b/public/images/mystery-encounters/safari_zone_mud.png new file mode 100644 index 00000000000..2ba7cb00047 Binary files /dev/null and b/public/images/mystery-encounters/safari_zone_mud.png differ diff --git a/public/images/mystery-encounters/shady_vitamin_dealer.json b/public/images/mystery-encounters/shady_vitamin_dealer.json new file mode 100644 index 00000000000..43c707d05ca --- /dev/null +++ b/public/images/mystery-encounters/shady_vitamin_dealer.json @@ -0,0 +1,797 @@ +{ + "textures": [ + { + "image": "shady_vitamin_dealer.png", + "format": "RGBA8888", + "size": { + "w": 424, + "h": 390 + }, + "scale": 1, + "frames": [ + { + "filename": "0000.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 43, + "h": 78 + }, + "frame": { + "x": 0, + "y": 0, + "w": 43, + "h": 78 + } + }, + { + "filename": "0001.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 43, + "h": 78 + }, + "frame": { + "x": 53, + "y": 0, + "w": 43, + "h": 78 + } + }, + { + "filename": "0002.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 43, + "h": 78 + }, + "frame": { + "x": 106, + "y": 0, + "w": 43, + "h": 78 + } + }, + { + "filename": "0003.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 43, + "h": 78 + }, + "frame": { + "x": 159, + "y": 0, + "w": 43, + "h": 78 + } + }, + { + "filename": "0004.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 44, + "h": 78 + }, + "frame": { + "x": 212, + "y": 0, + "w": 44, + "h": 78 + } + }, + { + "filename": "0005.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 44, + "h": 78 + }, + "frame": { + "x": 265, + "y": 0, + "w": 44, + "h": 78 + } + }, + { + "filename": "0006.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 44, + "h": 78 + }, + "frame": { + "x": 318, + "y": 0, + "w": 44, + "h": 78 + } + }, + { + "filename": "0007.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 44, + "h": 78 + }, + "frame": { + "x": 371, + "y": 0, + "w": 44, + "h": 78 + } + }, + { + "filename": "0008.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 44, + "h": 78 + }, + "frame": { + "x": 0, + "y": 78, + "w": 44, + "h": 78 + } + }, + { + "filename": "0009.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 44, + "h": 78 + }, + "frame": { + "x": 53, + "y": 78, + "w": 44, + "h": 78 + } + }, + { + "filename": "0010.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 48, + "h": 78 + }, + "frame": { + "x": 106, + "y": 78, + "w": 48, + "h": 78 + } + }, + { + "filename": "0011.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 50, + "h": 78 + }, + "frame": { + "x": 159, + "y": 78, + "w": 50, + "h": 78 + } + }, + { + "filename": "0012.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 53, + "h": 78 + }, + "frame": { + "x": 212, + "y": 78, + "w": 53, + "h": 78 + } + }, + { + "filename": "0013.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 53, + "h": 78 + }, + "frame": { + "x": 265, + "y": 78, + "w": 53, + "h": 78 + } + }, + { + "filename": "0014.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 52, + "h": 78 + }, + "frame": { + "x": 318, + "y": 78, + "w": 52, + "h": 78 + } + }, + { + "filename": "0015.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 51, + "h": 78 + }, + "frame": { + "x": 371, + "y": 78, + "w": 51, + "h": 78 + } + }, + { + "filename": "0016.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 52, + "h": 78 + }, + "frame": { + "x": 0, + "y": 156, + "w": 52, + "h": 78 + } + }, + { + "filename": "0017.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 52, + "h": 78 + }, + "frame": { + "x": 53, + "y": 156, + "w": 52, + "h": 78 + } + }, + { + "filename": "0018.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 53, + "h": 78 + }, + "frame": { + "x": 106, + "y": 156, + "w": 53, + "h": 78 + } + }, + { + "filename": "0019.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 53, + "h": 78 + }, + "frame": { + "x": 159, + "y": 156, + "w": 53, + "h": 78 + } + }, + { + "filename": "0020.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 53, + "h": 78 + }, + "frame": { + "x": 212, + "y": 156, + "w": 53, + "h": 78 + } + }, + { + "filename": "0021.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 52, + "h": 78 + }, + "frame": { + "x": 265, + "y": 156, + "w": 52, + "h": 78 + } + }, + { + "filename": "0022.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 51, + "h": 78 + }, + "frame": { + "x": 318, + "y": 156, + "w": 51, + "h": 78 + } + }, + { + "filename": "0023.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 51, + "h": 78 + }, + "frame": { + "x": 371, + "y": 156, + "w": 51, + "h": 78 + } + }, + { + "filename": "0024.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 51, + "h": 78 + }, + "frame": { + "x": 0, + "y": 234, + "w": 51, + "h": 78 + } + }, + { + "filename": "0025.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 50, + "h": 78 + }, + "frame": { + "x": 53, + "y": 234, + "w": 50, + "h": 78 + } + }, + { + "filename": "0026.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 48, + "h": 78 + }, + "frame": { + "x": 106, + "y": 234, + "w": 48, + "h": 78 + } + }, + { + "filename": "0027.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 46, + "h": 78 + }, + "frame": { + "x": 159, + "y": 234, + "w": 46, + "h": 78 + } + }, + { + "filename": "0028.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 46, + "h": 78 + }, + "frame": { + "x": 212, + "y": 234, + "w": 46, + "h": 78 + } + }, + { + "filename": "0029.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 44, + "h": 78 + }, + "frame": { + "x": 265, + "y": 234, + "w": 44, + "h": 78 + } + }, + { + "filename": "0030.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 44, + "h": 78 + }, + "frame": { + "x": 318, + "y": 234, + "w": 44, + "h": 78 + } + }, + { + "filename": "0031.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 44, + "h": 78 + }, + "frame": { + "x": 371, + "y": 234, + "w": 44, + "h": 78 + } + }, + { + "filename": "0032.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 44, + "h": 78 + }, + "frame": { + "x": 0, + "y": 312, + "w": 44, + "h": 78 + } + }, + { + "filename": "0033.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 43, + "h": 78 + }, + "frame": { + "x": 53, + "y": 312, + "w": 43, + "h": 78 + } + }, + { + "filename": "0034.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 43, + "h": 78 + }, + "frame": { + "x": 106, + "y": 312, + "w": 43, + "h": 78 + } + }, + { + "filename": "0035.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 43, + "h": 78 + }, + "frame": { + "x": 159, + "y": 312, + "w": 43, + "h": 78 + } + }, + { + "filename": "0036.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 43, + "h": 78 + }, + "frame": { + "x": 212, + "y": 312, + "w": 43, + "h": 78 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:4deb068879a8ac195cb4f00c8b17b7f5:b32f0f90436649264b6f3c49b09ac06a:05e903aa75b8e50c28334d9b5e14c85a$" + } +} diff --git a/public/images/mystery-encounters/shady_vitamin_dealer.png b/public/images/mystery-encounters/shady_vitamin_dealer.png new file mode 100644 index 00000000000..967d82973e6 Binary files /dev/null and b/public/images/mystery-encounters/shady_vitamin_dealer.png differ diff --git a/public/images/mystery-encounters/teleporting_hijinks_teleporter.json b/public/images/mystery-encounters/teleporting_hijinks_teleporter.json new file mode 100644 index 00000000000..04a3acd4369 --- /dev/null +++ b/public/images/mystery-encounters/teleporting_hijinks_teleporter.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "teleporting_hijinks_teleporter.png", + "format": "RGBA8888", + "size": { + "w": 74, + "h": 79 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 74, + "h": 79 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 74, + "h": 79 + }, + "frame": { + "x": 0, + "y": 0, + "w": 74, + "h": 79 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:937d8502b98f79720118061b6021e108:2b4f9db00d5b0997b42a5466f808509b:ce1615396ce7b0a146766d50b319bb81$" + } +} diff --git a/public/images/mystery-encounters/teleporting_hijinks_teleporter.png b/public/images/mystery-encounters/teleporting_hijinks_teleporter.png new file mode 100644 index 00000000000..9a049c30ab1 Binary files /dev/null and b/public/images/mystery-encounters/teleporting_hijinks_teleporter.png differ diff --git a/public/images/mystery-encounters/training_session_gear.json b/public/images/mystery-encounters/training_session_gear.json new file mode 100644 index 00000000000..8196c03f305 --- /dev/null +++ b/public/images/mystery-encounters/training_session_gear.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "training_session_gear.png", + "format": "RGBA8888", + "size": { + "w": 76, + "h": 57 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 76, + "h": 57 + }, + "spriteSourceSize": { + "x": 10, + "y": 3, + "w": 56, + "h": 54 + }, + "frame": { + "x": 8, + "y": 0, + "w": 56, + "h": 54 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:895f0a79b89fa0fb44167f4584fd9a22:357b46953b7e17c6b2f43a62d52855d8:cc1ed0e4f90aaa9dcf1b39a0af1283b0$" + } +} diff --git a/public/images/mystery-encounters/training_session_gear.png b/public/images/mystery-encounters/training_session_gear.png new file mode 100644 index 00000000000..42c3a9bb7d4 Binary files /dev/null and b/public/images/mystery-encounters/training_session_gear.png differ diff --git a/public/images/mystery-encounters/weird_dream_woman.json b/public/images/mystery-encounters/weird_dream_woman.json new file mode 100644 index 00000000000..49ebc001d18 --- /dev/null +++ b/public/images/mystery-encounters/weird_dream_woman.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "weird_dream_woman.png", + "format": "RGBA8888", + "size": { + "w": 78, + "h": 86 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 78, + "h": 86 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 78, + "h": 86 + }, + "frame": { + "x": 0, + "y": 0, + "w": 78, + "h": 86 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:65266da62e9d2953511c0d68ae431345:c1ca63690bed8dd5af71bb443910c830:56468b7a2883e66dadcd2af13ebd8010$" + } +} diff --git a/public/images/mystery-encounters/weird_dream_woman.png b/public/images/mystery-encounters/weird_dream_woman.png new file mode 100644 index 00000000000..50d04667152 Binary files /dev/null and b/public/images/mystery-encounters/weird_dream_woman.png differ diff --git a/public/images/pokemon/966-caph-starmobile.json b/public/images/pokemon/966-caph-starmobile.json new file mode 100644 index 00000000000..96c5aada282 --- /dev/null +++ b/public/images/pokemon/966-caph-starmobile.json @@ -0,0 +1,19 @@ +{ "frames": [ + { + "filename": "0001.png", + "frame": { "x": 0, "y": 0, "w": 94, "h": 94 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 26, "y": 0, "w": 94, "h": 94 }, + "sourceSize": { "w": 120, "h": 94 } + } + ], + "meta": { + "app": "https://www.aseprite.org/", + "version": "1.3.8.1-x64", + "image": "966-caph-starmobile.png", + "format": "RGBA8888", + "size": { "w": 94, "h": 94 }, + "scale": "1" + } +} diff --git a/public/images/pokemon/966-caph-starmobile.png b/public/images/pokemon/966-caph-starmobile.png new file mode 100644 index 00000000000..987782e529e Binary files /dev/null and b/public/images/pokemon/966-caph-starmobile.png differ diff --git a/public/images/pokemon/966-navi-starmobile.json b/public/images/pokemon/966-navi-starmobile.json new file mode 100644 index 00000000000..6a39310af00 --- /dev/null +++ b/public/images/pokemon/966-navi-starmobile.json @@ -0,0 +1,19 @@ +{ "frames": [ + { + "filename": "0001.png", + "frame": { "x": 0, "y": 0, "w": 94, "h": 94 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 26, "y": 0, "w": 94, "h": 94 }, + "sourceSize": { "w": 120, "h": 94 } + } + ], + "meta": { + "app": "https://www.aseprite.org/", + "version": "1.3.8.1-x64", + "image": "966-navi-starmobile.png", + "format": "RGBA8888", + "size": { "w": 94, "h": 94 }, + "scale": "1" + } +} diff --git a/public/images/pokemon/966-navi-starmobile.png b/public/images/pokemon/966-navi-starmobile.png new file mode 100644 index 00000000000..41d0fd4690c Binary files /dev/null and b/public/images/pokemon/966-navi-starmobile.png differ diff --git a/public/images/pokemon/966-ruchbah-starmobile.json b/public/images/pokemon/966-ruchbah-starmobile.json new file mode 100644 index 00000000000..c75a5630f45 --- /dev/null +++ b/public/images/pokemon/966-ruchbah-starmobile.json @@ -0,0 +1,19 @@ +{ "frames": [ + { + "filename": "0001.png", + "frame": { "x": 0, "y": 0, "w": 94, "h": 94 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 26, "y": 0, "w": 94, "h": 94 }, + "sourceSize": { "w": 120, "h": 94 } + } + ], + "meta": { + "app": "https://www.aseprite.org/", + "version": "1.3.8.1-x64", + "image": "966-ruchbah-starmobile.png", + "format": "RGBA8888", + "size": { "w": 94, "h": 94 }, + "scale": "1" + } +} diff --git a/public/images/pokemon/966-ruchbah-starmobile.png b/public/images/pokemon/966-ruchbah-starmobile.png new file mode 100644 index 00000000000..765f1fe5eaa Binary files /dev/null and b/public/images/pokemon/966-ruchbah-starmobile.png differ diff --git a/public/images/pokemon/966-schedar-starmobile.json b/public/images/pokemon/966-schedar-starmobile.json new file mode 100644 index 00000000000..59f77f3c975 --- /dev/null +++ b/public/images/pokemon/966-schedar-starmobile.json @@ -0,0 +1,19 @@ +{ "frames": [ + { + "filename": "0001.png", + "frame": { "x": 0, "y": 0, "w": 94, "h": 94 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 26, "y": 0, "w": 94, "h": 94 }, + "sourceSize": { "w": 120, "h": 94 } + } + ], + "meta": { + "app": "https://www.aseprite.org/", + "version": "1.3.8.1-x64", + "image": "966-schedar-starmobile.png", + "format": "RGBA8888", + "size": { "w": 94, "h": 94 }, + "scale": "1" + } +} diff --git a/public/images/pokemon/966-schedar-starmobile.png b/public/images/pokemon/966-schedar-starmobile.png new file mode 100644 index 00000000000..4cbc60f581f Binary files /dev/null and b/public/images/pokemon/966-schedar-starmobile.png differ diff --git a/public/images/pokemon/966-segin-starmobile.json b/public/images/pokemon/966-segin-starmobile.json new file mode 100644 index 00000000000..98b3938643b --- /dev/null +++ b/public/images/pokemon/966-segin-starmobile.json @@ -0,0 +1,19 @@ +{ "frames": [ + { + "filename": "0001.png", + "frame": { "x": 0, "y": 0, "w": 94, "h": 94 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 26, "y": 0, "w": 94, "h": 94 }, + "sourceSize": { "w": 120, "h": 94 } + } + ], + "meta": { + "app": "https://www.aseprite.org/", + "version": "1.3.8.1-x64", + "image": "966-segin-starmobile.png", + "format": "RGBA8888", + "size": { "w": 94, "h": 94 }, + "scale": "1" + } +} diff --git a/public/images/pokemon/966-segin-starmobile.png b/public/images/pokemon/966-segin-starmobile.png new file mode 100644 index 00000000000..fab6b1f62ee Binary files /dev/null and b/public/images/pokemon/966-segin-starmobile.png differ diff --git a/public/images/pokemon/back/966-caph-starmobile.json b/public/images/pokemon/back/966-caph-starmobile.json new file mode 100644 index 00000000000..d71eccd11d7 --- /dev/null +++ b/public/images/pokemon/back/966-caph-starmobile.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "966-caph-starmobile.png", + "format": "RGBA8888", + "size": { + "w": 84, + "h": 84 + }, + "scale": 0.333, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 96, + "h": 96 + }, + "spriteSourceSize": { + "x": 6, + "y": 20, + "w": 84, + "h": 56 + }, + "frame": { + "x": 0, + "y": 0, + "w": 84, + "h": 56 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:0226ae22b7a4822d78e38df4af1f59a7:01ce69442faf54e54474cd349cad2f7d:f9a0366e304d666e4262fa0af369d1f4$" + } +} diff --git a/public/images/pokemon/back/966-caph-starmobile.png b/public/images/pokemon/back/966-caph-starmobile.png new file mode 100644 index 00000000000..d1e67365454 Binary files /dev/null and b/public/images/pokemon/back/966-caph-starmobile.png differ diff --git a/public/images/pokemon/back/966-navi-starmobile.json b/public/images/pokemon/back/966-navi-starmobile.json new file mode 100644 index 00000000000..99059aa6edb --- /dev/null +++ b/public/images/pokemon/back/966-navi-starmobile.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "966-navi-starmobile.png", + "format": "RGBA8888", + "size": { + "w": 84, + "h": 84 + }, + "scale": 0.333, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 96, + "h": 96 + }, + "spriteSourceSize": { + "x": 6, + "y": 20, + "w": 84, + "h": 56 + }, + "frame": { + "x": 0, + "y": 0, + "w": 84, + "h": 56 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:0226ae22b7a4822d78e38df4af1f59a7:01ce69442faf54e54474cd349cad2f7d:f9a0366e304d666e4262fa0af369d1f4$" + } +} diff --git a/public/images/pokemon/back/966-navi-starmobile.png b/public/images/pokemon/back/966-navi-starmobile.png new file mode 100644 index 00000000000..d1e67365454 Binary files /dev/null and b/public/images/pokemon/back/966-navi-starmobile.png differ diff --git a/public/images/pokemon/back/966-ruchbah-starmobile.json b/public/images/pokemon/back/966-ruchbah-starmobile.json new file mode 100644 index 00000000000..b3bb8463eac --- /dev/null +++ b/public/images/pokemon/back/966-ruchbah-starmobile.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "966-ruchbah-starmobile.png", + "format": "RGBA8888", + "size": { + "w": 84, + "h": 84 + }, + "scale": 0.333, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 96, + "h": 96 + }, + "spriteSourceSize": { + "x": 6, + "y": 20, + "w": 84, + "h": 56 + }, + "frame": { + "x": 0, + "y": 0, + "w": 84, + "h": 56 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:0226ae22b7a4822d78e38df4af1f59a7:01ce69442faf54e54474cd349cad2f7d:f9a0366e304d666e4262fa0af369d1f4$" + } +} diff --git a/public/images/pokemon/back/966-ruchbah-starmobile.png b/public/images/pokemon/back/966-ruchbah-starmobile.png new file mode 100644 index 00000000000..d1e67365454 Binary files /dev/null and b/public/images/pokemon/back/966-ruchbah-starmobile.png differ diff --git a/public/images/pokemon/back/966-schedar-starmobile.json b/public/images/pokemon/back/966-schedar-starmobile.json new file mode 100644 index 00000000000..9832835b3ce --- /dev/null +++ b/public/images/pokemon/back/966-schedar-starmobile.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "966-schedar-starmobile.png", + "format": "RGBA8888", + "size": { + "w": 84, + "h": 84 + }, + "scale": 0.333, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 96, + "h": 96 + }, + "spriteSourceSize": { + "x": 6, + "y": 20, + "w": 84, + "h": 56 + }, + "frame": { + "x": 0, + "y": 0, + "w": 84, + "h": 56 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:0226ae22b7a4822d78e38df4af1f59a7:01ce69442faf54e54474cd349cad2f7d:f9a0366e304d666e4262fa0af369d1f4$" + } +} diff --git a/public/images/pokemon/back/966-schedar-starmobile.png b/public/images/pokemon/back/966-schedar-starmobile.png new file mode 100644 index 00000000000..d1e67365454 Binary files /dev/null and b/public/images/pokemon/back/966-schedar-starmobile.png differ diff --git a/public/images/pokemon/back/966-segin-starmobile.json b/public/images/pokemon/back/966-segin-starmobile.json new file mode 100644 index 00000000000..75bd4d8f304 --- /dev/null +++ b/public/images/pokemon/back/966-segin-starmobile.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "966-segin-starmobile.png", + "format": "RGBA8888", + "size": { + "w": 84, + "h": 84 + }, + "scale": 0.333, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 96, + "h": 96 + }, + "spriteSourceSize": { + "x": 6, + "y": 20, + "w": 84, + "h": 56 + }, + "frame": { + "x": 0, + "y": 0, + "w": 84, + "h": 56 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:0226ae22b7a4822d78e38df4af1f59a7:01ce69442faf54e54474cd349cad2f7d:f9a0366e304d666e4262fa0af369d1f4$" + } +} diff --git a/public/images/pokemon/back/966-segin-starmobile.png b/public/images/pokemon/back/966-segin-starmobile.png new file mode 100644 index 00000000000..d1e67365454 Binary files /dev/null and b/public/images/pokemon/back/966-segin-starmobile.png differ diff --git a/public/images/pokemon/back/shiny/966-caph-starmobile.json b/public/images/pokemon/back/shiny/966-caph-starmobile.json new file mode 100644 index 00000000000..d71eccd11d7 --- /dev/null +++ b/public/images/pokemon/back/shiny/966-caph-starmobile.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "966-caph-starmobile.png", + "format": "RGBA8888", + "size": { + "w": 84, + "h": 84 + }, + "scale": 0.333, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 96, + "h": 96 + }, + "spriteSourceSize": { + "x": 6, + "y": 20, + "w": 84, + "h": 56 + }, + "frame": { + "x": 0, + "y": 0, + "w": 84, + "h": 56 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:0226ae22b7a4822d78e38df4af1f59a7:01ce69442faf54e54474cd349cad2f7d:f9a0366e304d666e4262fa0af369d1f4$" + } +} diff --git a/public/images/pokemon/back/shiny/966-caph-starmobile.png b/public/images/pokemon/back/shiny/966-caph-starmobile.png new file mode 100644 index 00000000000..64e72d6793f Binary files /dev/null and b/public/images/pokemon/back/shiny/966-caph-starmobile.png differ diff --git a/public/images/pokemon/back/shiny/966-navi-starmobile.json b/public/images/pokemon/back/shiny/966-navi-starmobile.json new file mode 100644 index 00000000000..99059aa6edb --- /dev/null +++ b/public/images/pokemon/back/shiny/966-navi-starmobile.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "966-navi-starmobile.png", + "format": "RGBA8888", + "size": { + "w": 84, + "h": 84 + }, + "scale": 0.333, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 96, + "h": 96 + }, + "spriteSourceSize": { + "x": 6, + "y": 20, + "w": 84, + "h": 56 + }, + "frame": { + "x": 0, + "y": 0, + "w": 84, + "h": 56 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:0226ae22b7a4822d78e38df4af1f59a7:01ce69442faf54e54474cd349cad2f7d:f9a0366e304d666e4262fa0af369d1f4$" + } +} diff --git a/public/images/pokemon/back/shiny/966-navi-starmobile.png b/public/images/pokemon/back/shiny/966-navi-starmobile.png new file mode 100644 index 00000000000..64e72d6793f Binary files /dev/null and b/public/images/pokemon/back/shiny/966-navi-starmobile.png differ diff --git a/public/images/pokemon/back/shiny/966-ruchbah-starmobile.json b/public/images/pokemon/back/shiny/966-ruchbah-starmobile.json new file mode 100644 index 00000000000..b3bb8463eac --- /dev/null +++ b/public/images/pokemon/back/shiny/966-ruchbah-starmobile.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "966-ruchbah-starmobile.png", + "format": "RGBA8888", + "size": { + "w": 84, + "h": 84 + }, + "scale": 0.333, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 96, + "h": 96 + }, + "spriteSourceSize": { + "x": 6, + "y": 20, + "w": 84, + "h": 56 + }, + "frame": { + "x": 0, + "y": 0, + "w": 84, + "h": 56 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:0226ae22b7a4822d78e38df4af1f59a7:01ce69442faf54e54474cd349cad2f7d:f9a0366e304d666e4262fa0af369d1f4$" + } +} diff --git a/public/images/pokemon/back/shiny/966-ruchbah-starmobile.png b/public/images/pokemon/back/shiny/966-ruchbah-starmobile.png new file mode 100644 index 00000000000..64e72d6793f Binary files /dev/null and b/public/images/pokemon/back/shiny/966-ruchbah-starmobile.png differ diff --git a/public/images/pokemon/back/shiny/966-schedar-starmobile.json b/public/images/pokemon/back/shiny/966-schedar-starmobile.json new file mode 100644 index 00000000000..9832835b3ce --- /dev/null +++ b/public/images/pokemon/back/shiny/966-schedar-starmobile.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "966-schedar-starmobile.png", + "format": "RGBA8888", + "size": { + "w": 84, + "h": 84 + }, + "scale": 0.333, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 96, + "h": 96 + }, + "spriteSourceSize": { + "x": 6, + "y": 20, + "w": 84, + "h": 56 + }, + "frame": { + "x": 0, + "y": 0, + "w": 84, + "h": 56 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:0226ae22b7a4822d78e38df4af1f59a7:01ce69442faf54e54474cd349cad2f7d:f9a0366e304d666e4262fa0af369d1f4$" + } +} diff --git a/public/images/pokemon/back/shiny/966-schedar-starmobile.png b/public/images/pokemon/back/shiny/966-schedar-starmobile.png new file mode 100644 index 00000000000..64e72d6793f Binary files /dev/null and b/public/images/pokemon/back/shiny/966-schedar-starmobile.png differ diff --git a/public/images/pokemon/back/shiny/966-segin-starmobile.json b/public/images/pokemon/back/shiny/966-segin-starmobile.json new file mode 100644 index 00000000000..75bd4d8f304 --- /dev/null +++ b/public/images/pokemon/back/shiny/966-segin-starmobile.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "966-segin-starmobile.png", + "format": "RGBA8888", + "size": { + "w": 84, + "h": 84 + }, + "scale": 0.333, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 96, + "h": 96 + }, + "spriteSourceSize": { + "x": 6, + "y": 20, + "w": 84, + "h": 56 + }, + "frame": { + "x": 0, + "y": 0, + "w": 84, + "h": 56 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:0226ae22b7a4822d78e38df4af1f59a7:01ce69442faf54e54474cd349cad2f7d:f9a0366e304d666e4262fa0af369d1f4$" + } +} diff --git a/public/images/pokemon/back/shiny/966-segin-starmobile.png b/public/images/pokemon/back/shiny/966-segin-starmobile.png new file mode 100644 index 00000000000..64e72d6793f Binary files /dev/null and b/public/images/pokemon/back/shiny/966-segin-starmobile.png differ diff --git a/public/images/pokemon/exp/back/745.png b/public/images/pokemon/exp/back/745.png index 46a354be8a4..f4949135164 100644 Binary files a/public/images/pokemon/exp/back/745.png and b/public/images/pokemon/exp/back/745.png differ diff --git a/public/images/pokemon/exp/back/shiny/745.json b/public/images/pokemon/exp/back/shiny/745.json index 8e83c592ce4..4867604448d 100644 --- a/public/images/pokemon/exp/back/shiny/745.json +++ b/public/images/pokemon/exp/back/shiny/745.json @@ -1,440 +1,230 @@ -{ - "textures": [ - { - "image": "745.png", - "format": "RGBA8888", - "size": { - "w": 300, - "h": 300 - }, - "scale": 1, - "frames": [ - { - "filename": "0005.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 61, - "h": 71 - }, - "spriteSourceSize": { - "x": 1, - "y": 4, - "w": 60, - "h": 67 - }, - "frame": { - "x": 0, - "y": 0, - "w": 60, - "h": 67 - } - }, - { - "filename": "0006.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 61, - "h": 71 - }, - "spriteSourceSize": { - "x": 1, - "y": 4, - "w": 60, - "h": 67 - }, - "frame": { - "x": 60, - "y": 0, - "w": 60, - "h": 67 - } - }, - { - "filename": "0015.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 61, - "h": 71 - }, - "spriteSourceSize": { - "x": 1, - "y": 4, - "w": 60, - "h": 67 - }, - "frame": { - "x": 120, - "y": 0, - "w": 60, - "h": 67 - } - }, - { - "filename": "0016.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 61, - "h": 71 - }, - "spriteSourceSize": { - "x": 1, - "y": 4, - "w": 60, - "h": 67 - }, - "frame": { - "x": 180, - "y": 0, - "w": 60, - "h": 67 - } - }, - { - "filename": "0004.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 61, - "h": 71 - }, - "spriteSourceSize": { - "x": 1, - "y": 3, - "w": 60, - "h": 68 - }, - "frame": { - "x": 240, - "y": 0, - "w": 60, - "h": 68 - } - }, - { - "filename": "0008.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 61, - "h": 71 - }, - "spriteSourceSize": { - "x": 0, - "y": 3, - "w": 61, - "h": 68 - }, - "frame": { - "x": 0, - "y": 67, - "w": 61, - "h": 68 - } - }, - { - "filename": "0014.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 61, - "h": 71 - }, - "spriteSourceSize": { - "x": 1, - "y": 3, - "w": 60, - "h": 68 - }, - "frame": { - "x": 61, - "y": 67, - "w": 60, - "h": 68 - } - }, - { - "filename": "0018.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 61, - "h": 71 - }, - "spriteSourceSize": { - "x": 0, - "y": 3, - "w": 61, - "h": 68 - }, - "frame": { - "x": 121, - "y": 67, - "w": 61, - "h": 68 - } - }, - { - "filename": "0007.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 61, - "h": 71 - }, - "spriteSourceSize": { - "x": 1, - "y": 2, - "w": 60, - "h": 69 - }, - "frame": { - "x": 182, - "y": 68, - "w": 60, - "h": 69 - } - }, - { - "filename": "0017.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 61, - "h": 71 - }, - "spriteSourceSize": { - "x": 1, - "y": 2, - "w": 60, - "h": 69 - }, - "frame": { - "x": 0, - "y": 135, - "w": 60, - "h": 69 - } - }, - { - "filename": "0003.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 61, - "h": 71 - }, - "spriteSourceSize": { - "x": 0, - "y": 1, - "w": 61, - "h": 70 - }, - "frame": { - "x": 60, - "y": 135, - "w": 61, - "h": 70 - } - }, - { - "filename": "0013.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 61, - "h": 71 - }, - "spriteSourceSize": { - "x": 0, - "y": 1, - "w": 61, - "h": 70 - }, - "frame": { - "x": 121, - "y": 135, - "w": 61, - "h": 70 - } - }, - { - "filename": "0001.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 61, - "h": 71 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 61, - "h": 71 - }, - "frame": { - "x": 182, - "y": 137, - "w": 61, - "h": 71 - } - }, - { - "filename": "0011.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 61, - "h": 71 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 61, - "h": 71 - }, - "frame": { - "x": 182, - "y": 137, - "w": 61, - "h": 71 - } - }, - { - "filename": "0002.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 61, - "h": 71 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 61, - "h": 71 - }, - "frame": { - "x": 0, - "y": 205, - "w": 61, - "h": 71 - } - }, - { - "filename": "0009.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 61, - "h": 71 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 61, - "h": 71 - }, - "frame": { - "x": 61, - "y": 205, - "w": 61, - "h": 71 - } - }, - { - "filename": "0019.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 61, - "h": 71 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 61, - "h": 71 - }, - "frame": { - "x": 61, - "y": 205, - "w": 61, - "h": 71 - } - }, - { - "filename": "0010.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 61, - "h": 71 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 61, - "h": 71 - }, - "frame": { - "x": 122, - "y": 208, - "w": 61, - "h": 71 - } - }, - { - "filename": "0020.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 61, - "h": 71 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 61, - "h": 71 - }, - "frame": { - "x": 122, - "y": 208, - "w": 61, - "h": 71 - } - }, - { - "filename": "0012.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 61, - "h": 71 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 61, - "h": 71 - }, - "frame": { - "x": 183, - "y": 208, - "w": 61, - "h": 71 - } - } - ] - } - ], - "meta": { - "app": "https://www.codeandweb.com/texturepacker", - "version": "3.0", - "smartupdate": "$TexturePacker:SmartUpdate:8d47c2cedd75d15c81c3aa0a0b14133c:28c19026319cfbbb59916e3d1b92f732:f9304907e03a5223c5bc78c934419106$" - } -} +{ + "textures": [ + { + "image": "745.png", + "format": "RGBA8888", + "size": { + "w": 181, + "h": 181 + }, + "scale": 1, + "frames": [ + { + "filename": "0004.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 72, + "h": 61 + }, + "spriteSourceSize": { + "x": 1, + "y": 0, + "w": 71, + "h": 61 + }, + "frame": { + "x": 0, + "y": 0, + "w": 71, + "h": 61 + } + }, + { + "filename": "0008.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 72, + "h": 61 + }, + "spriteSourceSize": { + "x": 1, + "y": 0, + "w": 71, + "h": 61 + }, + "frame": { + "x": 0, + "y": 0, + "w": 71, + "h": 61 + } + }, + { + "filename": "0005.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 72, + "h": 61 + }, + "spriteSourceSize": { + "x": 1, + "y": 1, + "w": 71, + "h": 60 + }, + "frame": { + "x": 71, + "y": 0, + "w": 71, + "h": 60 + } + }, + { + "filename": "0007.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 72, + "h": 61 + }, + "spriteSourceSize": { + "x": 1, + "y": 1, + "w": 71, + "h": 60 + }, + "frame": { + "x": 71, + "y": 0, + "w": 71, + "h": 60 + } + }, + { + "filename": "0006.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 72, + "h": 61 + }, + "spriteSourceSize": { + "x": 1, + "y": 2, + "w": 71, + "h": 59 + }, + "frame": { + "x": 71, + "y": 60, + "w": 71, + "h": 59 + } + }, + { + "filename": "0003.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 72, + "h": 61 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 70, + "h": 61 + }, + "frame": { + "x": 0, + "y": 61, + "w": 70, + "h": 61 + } + }, + { + "filename": "0009.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 72, + "h": 61 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 70, + "h": 61 + }, + "frame": { + "x": 0, + "y": 61, + "w": 70, + "h": 61 + } + }, + { + "filename": "0001.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 72, + "h": 61 + }, + "spriteSourceSize": { + "x": 1, + "y": 2, + "w": 68, + "h": 59 + }, + "frame": { + "x": 0, + "y": 122, + "w": 68, + "h": 59 + } + }, + { + "filename": "0002.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 72, + "h": 61 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 69, + "h": 61 + }, + "frame": { + "x": 70, + "y": 119, + "w": 69, + "h": 61 + } + }, + { + "filename": "0010.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 72, + "h": 61 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 69, + "h": 61 + }, + "frame": { + "x": 70, + "y": 119, + "w": 69, + "h": 61 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:9bdd7250af45db121574c90e718874a8:ca85d052f16849220d83acd876b20b8b:f9304907e03a5223c5bc78c934419106$" + } +} diff --git a/public/images/pokemon/exp/back/shiny/745.png b/public/images/pokemon/exp/back/shiny/745.png index 5eb15a8cf49..49f2d0569af 100644 Binary files a/public/images/pokemon/exp/back/shiny/745.png and b/public/images/pokemon/exp/back/shiny/745.png differ diff --git a/public/images/pokemon/exp/shiny/745.json b/public/images/pokemon/exp/shiny/745.json index 6cabccff28d..d0989a1ccd3 100644 --- a/public/images/pokemon/exp/shiny/745.json +++ b/public/images/pokemon/exp/shiny/745.json @@ -1,167 +1,524 @@ -{ - "textures": [ - { - "image": "745.png", - "format": "RGBA8888", - "size": { - "w": 189, - "h": 189 - }, - "scale": 1, - "frames": [ - { - "filename": "0006.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 66, - "h": 58 - }, - "spriteSourceSize": { - "x": 1, - "y": 0, - "w": 65, - "h": 58 - }, - "frame": { - "x": 0, - "y": 0, - "w": 65, - "h": 58 - } - }, - { - "filename": "0005.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 66, - "h": 58 - }, - "spriteSourceSize": { - "x": 0, - "y": 1, - "w": 66, - "h": 57 - }, - "frame": { - "x": 65, - "y": 0, - "w": 66, - "h": 57 - } - }, - { - "filename": "0007.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 66, - "h": 58 - }, - "spriteSourceSize": { - "x": 2, - "y": 0, - "w": 64, - "h": 58 - }, - "frame": { - "x": 65, - "y": 57, - "w": 64, - "h": 58 - } - }, - { - "filename": "0003.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 66, - "h": 58 - }, - "spriteSourceSize": { - "x": 2, - "y": 1, - "w": 64, - "h": 57 - }, - "frame": { - "x": 0, - "y": 58, - "w": 64, - "h": 57 - } - }, - { - "filename": "0004.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 66, - "h": 58 - }, - "spriteSourceSize": { - "x": 1, - "y": 2, - "w": 65, - "h": 56 - }, - "frame": { - "x": 0, - "y": 115, - "w": 65, - "h": 56 - } - }, - { - "filename": "0001.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 66, - "h": 58 - }, - "spriteSourceSize": { - "x": 3, - "y": 0, - "w": 62, - "h": 58 - }, - "frame": { - "x": 65, - "y": 115, - "w": 62, - "h": 58 - } - }, - { - "filename": "0002.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 66, - "h": 58 - }, - "spriteSourceSize": { - "x": 3, - "y": 0, - "w": 62, - "h": 58 - }, - "frame": { - "x": 127, - "y": 115, - "w": 62, - "h": 58 - } - } - ] - } - ], - "meta": { - "app": "https://www.codeandweb.com/texturepacker", - "version": "3.0", - "smartupdate": "$TexturePacker:SmartUpdate:1b95a218abc87c12576165b943d3cb77:4d796dc75302ca2e18ce15e67dcf3f0f:f9304907e03a5223c5bc78c934419106$" - } -} +{ + "textures": [ + { + "image": "745.png", + "format": "RGBA8888", + "size": { + "w": 286, + "h": 286 + }, + "scale": 1, + "frames": [ + { + "filename": "0007.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 60, + "h": 58 + }, + "spriteSourceSize": { + "x": 0, + "y": 3, + "w": 60, + "h": 55 + }, + "frame": { + "x": 0, + "y": 0, + "w": 60, + "h": 55 + } + }, + { + "filename": "0008.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 60, + "h": 58 + }, + "spriteSourceSize": { + "x": 0, + "y": 3, + "w": 60, + "h": 55 + }, + "frame": { + "x": 60, + "y": 0, + "w": 60, + "h": 55 + } + }, + { + "filename": "0019.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 60, + "h": 58 + }, + "spriteSourceSize": { + "x": 0, + "y": 3, + "w": 60, + "h": 55 + }, + "frame": { + "x": 120, + "y": 0, + "w": 60, + "h": 55 + } + }, + { + "filename": "0020.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 60, + "h": 58 + }, + "spriteSourceSize": { + "x": 0, + "y": 3, + "w": 60, + "h": 55 + }, + "frame": { + "x": 180, + "y": 0, + "w": 60, + "h": 55 + } + }, + { + "filename": "0005.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 60, + "h": 58 + }, + "spriteSourceSize": { + "x": 0, + "y": 1, + "w": 60, + "h": 57 + }, + "frame": { + "x": 0, + "y": 55, + "w": 60, + "h": 57 + } + }, + { + "filename": "0006.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 60, + "h": 58 + }, + "spriteSourceSize": { + "x": 0, + "y": 1, + "w": 60, + "h": 57 + }, + "frame": { + "x": 60, + "y": 55, + "w": 60, + "h": 57 + } + }, + { + "filename": "0009.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 60, + "h": 58 + }, + "spriteSourceSize": { + "x": 0, + "y": 1, + "w": 60, + "h": 57 + }, + "frame": { + "x": 120, + "y": 55, + "w": 60, + "h": 57 + } + }, + { + "filename": "0010.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 60, + "h": 58 + }, + "spriteSourceSize": { + "x": 0, + "y": 1, + "w": 60, + "h": 57 + }, + "frame": { + "x": 180, + "y": 55, + "w": 60, + "h": 57 + } + }, + { + "filename": "0022.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 60, + "h": 58 + }, + "spriteSourceSize": { + "x": 0, + "y": 1, + "w": 60, + "h": 57 + }, + "frame": { + "x": 180, + "y": 55, + "w": 60, + "h": 57 + } + }, + { + "filename": "0017.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 60, + "h": 58 + }, + "spriteSourceSize": { + "x": 0, + "y": 1, + "w": 60, + "h": 57 + }, + "frame": { + "x": 0, + "y": 112, + "w": 60, + "h": 57 + } + }, + { + "filename": "0018.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 60, + "h": 58 + }, + "spriteSourceSize": { + "x": 0, + "y": 1, + "w": 60, + "h": 57 + }, + "frame": { + "x": 60, + "y": 112, + "w": 60, + "h": 57 + } + }, + { + "filename": "0021.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 60, + "h": 58 + }, + "spriteSourceSize": { + "x": 0, + "y": 1, + "w": 60, + "h": 57 + }, + "frame": { + "x": 120, + "y": 112, + "w": 60, + "h": 57 + } + }, + { + "filename": "0001.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 60, + "h": 58 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 60, + "h": 58 + }, + "frame": { + "x": 180, + "y": 112, + "w": 60, + "h": 58 + } + }, + { + "filename": "0013.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 60, + "h": 58 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 60, + "h": 58 + }, + "frame": { + "x": 180, + "y": 112, + "w": 60, + "h": 58 + } + }, + { + "filename": "0002.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 60, + "h": 58 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 60, + "h": 58 + }, + "frame": { + "x": 0, + "y": 169, + "w": 60, + "h": 58 + } + }, + { + "filename": "0014.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 60, + "h": 58 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 60, + "h": 58 + }, + "frame": { + "x": 0, + "y": 169, + "w": 60, + "h": 58 + } + }, + { + "filename": "0003.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 60, + "h": 58 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 60, + "h": 58 + }, + "frame": { + "x": 60, + "y": 169, + "w": 60, + "h": 58 + } + }, + { + "filename": "0004.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 60, + "h": 58 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 60, + "h": 58 + }, + "frame": { + "x": 120, + "y": 169, + "w": 60, + "h": 58 + } + }, + { + "filename": "0011.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 60, + "h": 58 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 60, + "h": 58 + }, + "frame": { + "x": 180, + "y": 170, + "w": 60, + "h": 58 + } + }, + { + "filename": "0012.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 60, + "h": 58 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 60, + "h": 58 + }, + "frame": { + "x": 0, + "y": 227, + "w": 60, + "h": 58 + } + }, + { + "filename": "0024.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 60, + "h": 58 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 60, + "h": 58 + }, + "frame": { + "x": 0, + "y": 227, + "w": 60, + "h": 58 + } + }, + { + "filename": "0015.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 60, + "h": 58 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 60, + "h": 58 + }, + "frame": { + "x": 60, + "y": 227, + "w": 60, + "h": 58 + } + }, + { + "filename": "0016.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 60, + "h": 58 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 60, + "h": 58 + }, + "frame": { + "x": 120, + "y": 227, + "w": 60, + "h": 58 + } + }, + { + "filename": "0023.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 60, + "h": 58 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 60, + "h": 58 + }, + "frame": { + "x": 180, + "y": 228, + "w": 60, + "h": 58 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:d67741bfb78b7ff0c920c5395dd91fc2:e78172ef76e3b6327173461a595a8a6b:f9304907e03a5223c5bc78c934419106$" + } +} diff --git a/public/images/pokemon/exp/shiny/745.png b/public/images/pokemon/exp/shiny/745.png index 7679c44ba13..c3256cf3f64 100644 Binary files a/public/images/pokemon/exp/shiny/745.png and b/public/images/pokemon/exp/shiny/745.png differ diff --git a/public/images/pokemon/icons/9/966-caph-starmobile.png b/public/images/pokemon/icons/9/966-caph-starmobile.png new file mode 100644 index 00000000000..fba351495bd Binary files /dev/null and b/public/images/pokemon/icons/9/966-caph-starmobile.png differ diff --git a/public/images/pokemon/icons/9/966-navi-starmobile.png b/public/images/pokemon/icons/9/966-navi-starmobile.png new file mode 100644 index 00000000000..fba351495bd Binary files /dev/null and b/public/images/pokemon/icons/9/966-navi-starmobile.png differ diff --git a/public/images/pokemon/icons/9/966-ruchbah-starmobile.png b/public/images/pokemon/icons/9/966-ruchbah-starmobile.png new file mode 100644 index 00000000000..fba351495bd Binary files /dev/null and b/public/images/pokemon/icons/9/966-ruchbah-starmobile.png differ diff --git a/public/images/pokemon/icons/9/966-schedar-starmobile.png b/public/images/pokemon/icons/9/966-schedar-starmobile.png new file mode 100644 index 00000000000..fba351495bd Binary files /dev/null and b/public/images/pokemon/icons/9/966-schedar-starmobile.png differ diff --git a/public/images/pokemon/icons/9/966-segin-starmobile.png b/public/images/pokemon/icons/9/966-segin-starmobile.png new file mode 100644 index 00000000000..fba351495bd Binary files /dev/null and b/public/images/pokemon/icons/9/966-segin-starmobile.png differ diff --git a/public/images/pokemon/icons/9/966s-caph-starmobile.png b/public/images/pokemon/icons/9/966s-caph-starmobile.png new file mode 100644 index 00000000000..e54e5c90e4a Binary files /dev/null and b/public/images/pokemon/icons/9/966s-caph-starmobile.png differ diff --git a/public/images/pokemon/icons/9/966s-navi-starmobile.png b/public/images/pokemon/icons/9/966s-navi-starmobile.png new file mode 100644 index 00000000000..e54e5c90e4a Binary files /dev/null and b/public/images/pokemon/icons/9/966s-navi-starmobile.png differ diff --git a/public/images/pokemon/icons/9/966s-ruchbah-starmobile.png b/public/images/pokemon/icons/9/966s-ruchbah-starmobile.png new file mode 100644 index 00000000000..e54e5c90e4a Binary files /dev/null and b/public/images/pokemon/icons/9/966s-ruchbah-starmobile.png differ diff --git a/public/images/pokemon/icons/9/966s-schedar-starmobile.png b/public/images/pokemon/icons/9/966s-schedar-starmobile.png new file mode 100644 index 00000000000..e54e5c90e4a Binary files /dev/null and b/public/images/pokemon/icons/9/966s-schedar-starmobile.png differ diff --git a/public/images/pokemon/icons/9/966s-segin-starmobile.png b/public/images/pokemon/icons/9/966s-segin-starmobile.png new file mode 100644 index 00000000000..e54e5c90e4a Binary files /dev/null and b/public/images/pokemon/icons/9/966s-segin-starmobile.png differ diff --git a/public/images/pokemon/shiny/966-caph-starmobile.json b/public/images/pokemon/shiny/966-caph-starmobile.json new file mode 100644 index 00000000000..96c5aada282 --- /dev/null +++ b/public/images/pokemon/shiny/966-caph-starmobile.json @@ -0,0 +1,19 @@ +{ "frames": [ + { + "filename": "0001.png", + "frame": { "x": 0, "y": 0, "w": 94, "h": 94 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 26, "y": 0, "w": 94, "h": 94 }, + "sourceSize": { "w": 120, "h": 94 } + } + ], + "meta": { + "app": "https://www.aseprite.org/", + "version": "1.3.8.1-x64", + "image": "966-caph-starmobile.png", + "format": "RGBA8888", + "size": { "w": 94, "h": 94 }, + "scale": "1" + } +} diff --git a/public/images/pokemon/shiny/966-caph-starmobile.png b/public/images/pokemon/shiny/966-caph-starmobile.png new file mode 100644 index 00000000000..6107a426ff6 Binary files /dev/null and b/public/images/pokemon/shiny/966-caph-starmobile.png differ diff --git a/public/images/pokemon/shiny/966-navi-starmobile.json b/public/images/pokemon/shiny/966-navi-starmobile.json new file mode 100644 index 00000000000..6a39310af00 --- /dev/null +++ b/public/images/pokemon/shiny/966-navi-starmobile.json @@ -0,0 +1,19 @@ +{ "frames": [ + { + "filename": "0001.png", + "frame": { "x": 0, "y": 0, "w": 94, "h": 94 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 26, "y": 0, "w": 94, "h": 94 }, + "sourceSize": { "w": 120, "h": 94 } + } + ], + "meta": { + "app": "https://www.aseprite.org/", + "version": "1.3.8.1-x64", + "image": "966-navi-starmobile.png", + "format": "RGBA8888", + "size": { "w": 94, "h": 94 }, + "scale": "1" + } +} diff --git a/public/images/pokemon/shiny/966-navi-starmobile.png b/public/images/pokemon/shiny/966-navi-starmobile.png new file mode 100644 index 00000000000..74999d4fa13 Binary files /dev/null and b/public/images/pokemon/shiny/966-navi-starmobile.png differ diff --git a/public/images/pokemon/shiny/966-ruchbah-starmobile.json b/public/images/pokemon/shiny/966-ruchbah-starmobile.json new file mode 100644 index 00000000000..c75a5630f45 --- /dev/null +++ b/public/images/pokemon/shiny/966-ruchbah-starmobile.json @@ -0,0 +1,19 @@ +{ "frames": [ + { + "filename": "0001.png", + "frame": { "x": 0, "y": 0, "w": 94, "h": 94 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 26, "y": 0, "w": 94, "h": 94 }, + "sourceSize": { "w": 120, "h": 94 } + } + ], + "meta": { + "app": "https://www.aseprite.org/", + "version": "1.3.8.1-x64", + "image": "966-ruchbah-starmobile.png", + "format": "RGBA8888", + "size": { "w": 94, "h": 94 }, + "scale": "1" + } +} diff --git a/public/images/pokemon/shiny/966-ruchbah-starmobile.png b/public/images/pokemon/shiny/966-ruchbah-starmobile.png new file mode 100644 index 00000000000..9de01ac6a73 Binary files /dev/null and b/public/images/pokemon/shiny/966-ruchbah-starmobile.png differ diff --git a/public/images/pokemon/shiny/966-schedar-starmobile.json b/public/images/pokemon/shiny/966-schedar-starmobile.json new file mode 100644 index 00000000000..59f77f3c975 --- /dev/null +++ b/public/images/pokemon/shiny/966-schedar-starmobile.json @@ -0,0 +1,19 @@ +{ "frames": [ + { + "filename": "0001.png", + "frame": { "x": 0, "y": 0, "w": 94, "h": 94 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 26, "y": 0, "w": 94, "h": 94 }, + "sourceSize": { "w": 120, "h": 94 } + } + ], + "meta": { + "app": "https://www.aseprite.org/", + "version": "1.3.8.1-x64", + "image": "966-schedar-starmobile.png", + "format": "RGBA8888", + "size": { "w": 94, "h": 94 }, + "scale": "1" + } +} diff --git a/public/images/pokemon/shiny/966-schedar-starmobile.png b/public/images/pokemon/shiny/966-schedar-starmobile.png new file mode 100644 index 00000000000..79033bb123c Binary files /dev/null and b/public/images/pokemon/shiny/966-schedar-starmobile.png differ diff --git a/public/images/pokemon/shiny/966-segin-starmobile.json b/public/images/pokemon/shiny/966-segin-starmobile.json new file mode 100644 index 00000000000..98b3938643b --- /dev/null +++ b/public/images/pokemon/shiny/966-segin-starmobile.json @@ -0,0 +1,19 @@ +{ "frames": [ + { + "filename": "0001.png", + "frame": { "x": 0, "y": 0, "w": 94, "h": 94 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 26, "y": 0, "w": 94, "h": 94 }, + "sourceSize": { "w": 120, "h": 94 } + } + ], + "meta": { + "app": "https://www.aseprite.org/", + "version": "1.3.8.1-x64", + "image": "966-segin-starmobile.png", + "format": "RGBA8888", + "size": { "w": 94, "h": 94 }, + "scale": "1" + } +} diff --git a/public/images/pokemon/shiny/966-segin-starmobile.png b/public/images/pokemon/shiny/966-segin-starmobile.png new file mode 100644 index 00000000000..f4cab89a203 Binary files /dev/null and b/public/images/pokemon/shiny/966-segin-starmobile.png differ diff --git a/public/images/pokemon_icons_9.json b/public/images/pokemon_icons_9.json index 26e28eedae0..01994a41a02 100644 --- a/public/images/pokemon_icons_9.json +++ b/public/images/pokemon_icons_9.json @@ -4,8 +4,8 @@ "image": "pokemon_icons_9.png", "format": "RGBA8888", "size": { - "w": 252, - "h": 591 + "w": 255, + "h": 646 }, "scale": 1, "frames": [ @@ -382,7 +382,7 @@ }, "frame": { "x": 0, - "y": 270, + "y": 300, "w": 30, "h": 30 } @@ -429,27 +429,6 @@ "h": 27 } }, - { - "filename": "8901", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 5, - "y": 1, - "w": 30, - "h": 28 - }, - "frame": { - "x": 222, - "y": 0, - "w": 30, - "h": 28 - } - }, { "filename": "1020", "rotated": false, @@ -528,12 +507,33 @@ "h": 26 }, "frame": { - "x": 126, - "y": 28, + "x": 222, + "y": 0, "w": 32, "h": 26 } }, + { + "filename": "8901", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 5, + "y": 1, + "w": 30, + "h": 28 + }, + "frame": { + "x": 0, + "y": 330, + "w": 30, + "h": 28 + } + }, { "filename": "8901s", "rotated": false, @@ -550,7 +550,7 @@ }, "frame": { "x": 0, - "y": 300, + "y": 358, "w": 30, "h": 28 } @@ -571,7 +571,7 @@ }, "frame": { "x": 0, - "y": 328, + "y": 386, "w": 27, "h": 30 } @@ -592,7 +592,7 @@ }, "frame": { "x": 0, - "y": 358, + "y": 416, "w": 27, "h": 30 } @@ -611,6 +611,27 @@ "w": 32, "h": 25 }, + "frame": { + "x": 126, + "y": 28, + "w": 32, + "h": 25 + } + }, + { + "filename": "984s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 4, + "y": 3, + "w": 32, + "h": 25 + }, "frame": { "x": 158, "y": 27, @@ -619,7 +640,7 @@ } }, { - "filename": "984s", + "filename": "1014", "rotated": false, "trimmed": true, "sourceSize": { @@ -640,7 +661,7 @@ } }, { - "filename": "992", + "filename": "1014s", "rotated": false, "trimmed": true, "sourceSize": { @@ -648,16 +669,16 @@ "h": 30 }, "spriteSourceSize": { - "x": 6, - "y": 2, - "w": 30, - "h": 26 + "x": 4, + "y": 3, + "w": 32, + "h": 25 }, "frame": { "x": 222, - "y": 28, - "w": 30, - "h": 26 + "y": 26, + "w": 32, + "h": 25 } }, { @@ -676,7 +697,7 @@ }, "frame": { "x": 0, - "y": 388, + "y": 446, "w": 27, "h": 29 } @@ -697,7 +718,7 @@ }, "frame": { "x": 0, - "y": 417, + "y": 475, "w": 27, "h": 29 } @@ -718,7 +739,7 @@ }, "frame": { "x": 0, - "y": 446, + "y": 504, "w": 29, "h": 28 } @@ -739,7 +760,7 @@ }, "frame": { "x": 0, - "y": 474, + "y": 532, "w": 29, "h": 28 } @@ -760,7 +781,7 @@ }, "frame": { "x": 0, - "y": 502, + "y": 560, "w": 29, "h": 27 } @@ -781,7 +802,7 @@ }, "frame": { "x": 0, - "y": 529, + "y": 587, "w": 29, "h": 27 } @@ -802,7 +823,7 @@ }, "frame": { "x": 0, - "y": 556, + "y": 614, "w": 28, "h": 28 } @@ -828,90 +849,6 @@ "h": 21 } }, - { - "filename": "975s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 4, - "y": 7, - "w": 32, - "h": 21 - }, - "frame": { - "x": 126, - "y": 54, - "w": 32, - "h": 21 - } - }, - { - "filename": "1014", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 32, - "h": 25 - }, - "frame": { - "x": 158, - "y": 52, - "w": 32, - "h": 25 - } - }, - { - "filename": "1014s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 32, - "h": 25 - }, - "frame": { - "x": 190, - "y": 52, - "w": 32, - "h": 25 - } - }, - { - "filename": "992s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 6, - "y": 2, - "w": 30, - "h": 26 - }, - "frame": { - "x": 222, - "y": 54, - "w": 30, - "h": 26 - } - }, { "filename": "1024-terastal", "rotated": false, @@ -926,11 +863,95 @@ "w": 32, "h": 22 }, + "frame": { + "x": 126, + "y": 53, + "w": 32, + "h": 22 + } + }, + { + "filename": "1025", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 4, + "y": 4, + "w": 32, + "h": 24 + }, + "frame": { + "x": 158, + "y": 52, + "w": 32, + "h": 24 + } + }, + { + "filename": "1025s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 4, + "y": 4, + "w": 32, + "h": 24 + }, + "frame": { + "x": 190, + "y": 52, + "w": 32, + "h": 24 + } + }, + { + "filename": "992", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 6, + "y": 2, + "w": 30, + "h": 26 + }, + "frame": { + "x": 222, + "y": 51, + "w": 30, + "h": 26 + } + }, + { + "filename": "975s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 4, + "y": 7, + "w": 32, + "h": 21 + }, "frame": { "x": 93, "y": 75, "w": 32, - "h": 22 + "h": 21 } }, { @@ -955,7 +976,7 @@ } }, { - "filename": "1025", + "filename": "992s", "rotated": false, "trimmed": true, "sourceSize": { @@ -963,37 +984,16 @@ "h": 30 }, "spriteSourceSize": { - "x": 4, - "y": 4, - "w": 32, - "h": 24 + "x": 6, + "y": 2, + "w": 30, + "h": 26 }, "frame": { "x": 157, - "y": 77, - "w": 32, - "h": 24 - } - }, - { - "filename": "1025s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 4, - "y": 4, - "w": 32, - "h": 24 - }, - "frame": { - "x": 189, - "y": 77, - "w": 32, - "h": 24 + "y": 76, + "w": 30, + "h": 26 } }, { @@ -1011,8 +1011,29 @@ "h": 25 }, "frame": { - "x": 221, - "y": 80, + "x": 187, + "y": 76, + "w": 30, + "h": 25 + } + }, + { + "filename": "993s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 6, + "y": 2, + "w": 30, + "h": 25 + }, + "frame": { + "x": 217, + "y": 77, "w": 30, "h": 25 } @@ -1033,7 +1054,7 @@ }, "frame": { "x": 27, - "y": 328, + "y": 386, "w": 23, "h": 30 } @@ -1054,7 +1075,7 @@ }, "frame": { "x": 27, - "y": 358, + "y": 416, "w": 23, "h": 30 } @@ -1075,7 +1096,7 @@ }, "frame": { "x": 27, - "y": 388, + "y": 446, "w": 25, "h": 30 } @@ -1096,7 +1117,7 @@ }, "frame": { "x": 27, - "y": 418, + "y": 476, "w": 25, "h": 28 } @@ -1117,7 +1138,7 @@ }, "frame": { "x": 29, - "y": 446, + "y": 504, "w": 25, "h": 30 } @@ -1138,7 +1159,7 @@ }, "frame": { "x": 29, - "y": 476, + "y": 534, "w": 25, "h": 28 } @@ -1159,7 +1180,7 @@ }, "frame": { "x": 29, - "y": 504, + "y": 562, "w": 26, "h": 28 } @@ -1180,7 +1201,7 @@ }, "frame": { "x": 29, - "y": 532, + "y": 590, "w": 27, "h": 26 } @@ -1201,32 +1222,11 @@ }, "frame": { "x": 28, - "y": 558, + "y": 616, "w": 28, "h": 28 } }, - { - "filename": "993s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 6, - "y": 2, - "w": 30, - "h": 25 - }, - "frame": { - "x": 62, - "y": 86, - "w": 30, - "h": 25 - } - }, { "filename": "924", "rotated": false, @@ -1242,8 +1242,8 @@ "h": 20 }, "frame": { - "x": 92, - "y": 97, + "x": 62, + "y": 86, "w": 29, "h": 20 } @@ -1263,8 +1263,8 @@ "h": 20 }, "frame": { - "x": 121, - "y": 97, + "x": 61, + "y": 106, "w": 29, "h": 20 } @@ -1284,8 +1284,8 @@ "h": 22 }, "frame": { - "x": 150, - "y": 101, + "x": 91, + "y": 96, "w": 29, "h": 22 } @@ -1305,33 +1305,12 @@ "h": 22 }, "frame": { - "x": 179, - "y": 101, + "x": 120, + "y": 97, "w": 29, "h": 22 } }, - { - "filename": "935", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 13, - "y": 7, - "w": 13, - "h": 21 - }, - "frame": { - "x": 208, - "y": 101, - "w": 13, - "h": 21 - } - }, { "filename": "925-three", "rotated": false, @@ -1347,8 +1326,8 @@ "h": 20 }, "frame": { - "x": 221, - "y": 105, + "x": 149, + "y": 102, "w": 29, "h": 20 } @@ -1368,54 +1347,12 @@ "h": 20 }, "frame": { - "x": 61, - "y": 111, + "x": 90, + "y": 118, "w": 29, "h": 20 } }, - { - "filename": "976", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 5, - "y": 10, - "w": 29, - "h": 18 - }, - "frame": { - "x": 90, - "y": 117, - "w": 29, - "h": 18 - } - }, - { - "filename": "976s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 5, - "y": 10, - "w": 29, - "h": 18 - }, - "frame": { - "x": 119, - "y": 117, - "w": 29, - "h": 18 - } - }, { "filename": "1022", "rotated": false, @@ -1431,8 +1368,8 @@ "h": 25 }, "frame": { - "x": 148, - "y": 123, + "x": 119, + "y": 119, "w": 29, "h": 25 } @@ -1452,12 +1389,33 @@ "h": 25 }, "frame": { - "x": 177, - "y": 123, + "x": 148, + "y": 122, "w": 29, "h": 25 } }, + { + "filename": "976", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 5, + "y": 10, + "w": 29, + "h": 18 + }, + "frame": { + "x": 31, + "y": 118, + "w": 29, + "h": 18 + } + }, { "filename": "8128-blaze", "rotated": false, @@ -1473,54 +1431,12 @@ "h": 27 }, "frame": { - "x": 206, - "y": 125, + "x": 30, + "y": 136, "w": 29, "h": 27 } }, - { - "filename": "913", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 11, - "y": 5, - "w": 17, - "h": 23 - }, - "frame": { - "x": 235, - "y": 125, - "w": 17, - "h": 23 - } - }, - { - "filename": "913s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 11, - "y": 5, - "w": 17, - "h": 23 - }, - "frame": { - "x": 235, - "y": 148, - "w": 17, - "h": 23 - } - }, { "filename": "8128s-blaze", "rotated": false, @@ -1536,8 +1452,8 @@ "h": 27 }, "frame": { - "x": 31, - "y": 118, + "x": 30, + "y": 163, "w": 29, "h": 27 } @@ -1558,7 +1474,7 @@ }, "frame": { "x": 30, - "y": 145, + "y": 190, "w": 27, "h": 28 } @@ -1579,7 +1495,7 @@ }, "frame": { "x": 30, - "y": 173, + "y": 218, "w": 27, "h": 28 } @@ -1600,7 +1516,7 @@ }, "frame": { "x": 30, - "y": 201, + "y": 246, "w": 26, "h": 28 } @@ -1621,7 +1537,7 @@ }, "frame": { "x": 30, - "y": 229, + "y": 274, "w": 27, "h": 26 } @@ -1642,7 +1558,7 @@ }, "frame": { "x": 30, - "y": 255, + "y": 300, "w": 25, "h": 27 } @@ -1663,13 +1579,13 @@ }, "frame": { "x": 30, - "y": 282, + "y": 327, "w": 25, "h": 27 } }, { - "filename": "916", + "filename": "964-hero", "rotated": false, "trimmed": true, "sourceSize": { @@ -1677,20 +1593,20 @@ "h": 30 }, "spriteSourceSize": { - "x": 7, - "y": 9, - "w": 25, - "h": 19 + "x": 9, + "y": 0, + "w": 22, + "h": 29 }, "frame": { "x": 30, - "y": 309, - "w": 25, - "h": 19 + "y": 354, + "w": 22, + "h": 29 } }, { - "filename": "911", + "filename": "976s", "rotated": false, "trimmed": true, "sourceSize": { @@ -1698,16 +1614,16 @@ "h": 30 }, "spriteSourceSize": { - "x": 6, - "y": 5, - "w": 28, - "h": 23 + "x": 5, + "y": 10, + "w": 29, + "h": 18 }, "frame": { "x": 60, - "y": 131, - "w": 28, - "h": 23 + "y": 126, + "w": 29, + "h": 18 } }, { @@ -1724,9 +1640,51 @@ "w": 28, "h": 24 }, + "frame": { + "x": 59, + "y": 144, + "w": 28, + "h": 24 + } + }, + { + "filename": "911", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 6, + "y": 5, + "w": 28, + "h": 23 + }, + "frame": { + "x": 59, + "y": 168, + "w": 28, + "h": 23 + } + }, + { + "filename": "8128s-aqua", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 10, + "y": 5, + "w": 28, + "h": 24 + }, "frame": { "x": 57, - "y": 154, + "y": 191, "w": 28, "h": 24 } @@ -1747,13 +1705,13 @@ }, "frame": { "x": 57, - "y": 178, + "y": 215, "w": 28, "h": 23 } }, { - "filename": "968", + "filename": "950", "rotated": false, "trimmed": true, "sourceSize": { @@ -1761,37 +1719,58 @@ "h": 30 }, "spriteSourceSize": { - "x": 8, - "y": 0, - "w": 23, - "h": 28 - }, - "frame": { - "x": 56, - "y": 201, - "w": 23, - "h": 28 - } - }, - { - "filename": "964-hero", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 9, - "y": 0, - "w": 22, - "h": 29 + "x": 6, + "y": 11, + "w": 28, + "h": 17 }, "frame": { "x": 57, - "y": 229, - "w": 22, - "h": 29 + "y": 238, + "w": 28, + "h": 17 + } + }, + { + "filename": "916", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 7, + "y": 9, + "w": 25, + "h": 19 + }, + "frame": { + "x": 56, + "y": 255, + "w": 25, + "h": 19 + } + }, + { + "filename": "998", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 7, + "y": 2, + "w": 25, + "h": 26 + }, + "frame": { + "x": 57, + "y": 274, + "w": 25, + "h": 26 } }, { @@ -1810,13 +1789,55 @@ }, "frame": { "x": 55, - "y": 258, + "y": 300, "w": 22, "h": 29 } }, { - "filename": "999", + "filename": "914", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 8, + "y": 3, + "w": 24, + "h": 25 + }, + "frame": { + "x": 55, + "y": 329, + "w": 24, + "h": 25 + } + }, + { + "filename": "968", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 8, + "y": 0, + "w": 23, + "h": 28 + }, + "frame": { + "x": 52, + "y": 354, + "w": 23, + "h": 28 + } + }, + { + "filename": "954", "rotated": false, "trimmed": true, "sourceSize": { @@ -1826,16 +1847,100 @@ "spriteSourceSize": { "x": 9, "y": 0, - "w": 22, + "w": 21, "h": 29 }, "frame": { - "x": 55, - "y": 287, - "w": 22, + "x": 77, + "y": 300, + "w": 21, "h": 29 } }, + { + "filename": "908", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 10, + "y": 2, + "w": 20, + "h": 26 + }, + "frame": { + "x": 79, + "y": 329, + "w": 20, + "h": 26 + } + }, + { + "filename": "1016", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 8, + "y": 1, + "w": 24, + "h": 27 + }, + "frame": { + "x": 75, + "y": 355, + "w": 24, + "h": 27 + } + }, + { + "filename": "950s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 6, + "y": 11, + "w": 28, + "h": 17 + }, + "frame": { + "x": 89, + "y": 138, + "w": 28, + "h": 17 + } + }, + { + "filename": "968s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 8, + "y": 0, + "w": 23, + "h": 28 + }, + "frame": { + "x": 87, + "y": 155, + "w": 23, + "h": 28 + } + }, { "filename": "990", "rotated": false, @@ -1851,8 +1956,8 @@ "h": 23 }, "frame": { - "x": 88, - "y": 135, + "x": 117, + "y": 144, "w": 28, "h": 23 } @@ -1872,33 +1977,12 @@ "h": 23 }, "frame": { - "x": 116, - "y": 135, + "x": 145, + "y": 147, "w": 28, "h": 23 } }, - { - "filename": "8128s-aqua", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 10, - "y": 5, - "w": 28, - "h": 24 - }, - "frame": { - "x": 85, - "y": 158, - "w": 28, - "h": 24 - } - }, { "filename": "972", "rotated": false, @@ -1914,8 +1998,8 @@ "h": 22 }, "frame": { - "x": 113, - "y": 158, + "x": 110, + "y": 167, "w": 27, "h": 22 } @@ -1935,14 +2019,14 @@ "h": 22 }, "frame": { - "x": 85, - "y": 182, + "x": 137, + "y": 170, "w": 27, "h": 22 } }, { - "filename": "968s", + "filename": "947", "rotated": false, "trimmed": true, "sourceSize": { @@ -1951,36 +2035,15 @@ }, "spriteSourceSize": { "x": 8, - "y": 0, + "y": 6, "w": 23, - "h": 28 + "h": 22 }, "frame": { - "x": 79, - "y": 204, + "x": 87, + "y": 183, "w": 23, - "h": 28 - } - }, - { - "filename": "998", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 7, - "y": 2, - "w": 25, - "h": 26 - }, - "frame": { - "x": 79, - "y": 232, - "w": 25, - "h": 26 + "h": 22 } }, { @@ -1998,14 +2061,14 @@ "h": 26 }, "frame": { - "x": 77, - "y": 258, + "x": 85, + "y": 205, "w": 25, "h": 26 } }, { - "filename": "999s", + "filename": "985", "rotated": false, "trimmed": true, "sourceSize": { @@ -2014,120 +2077,15 @@ }, "spriteSourceSize": { "x": 9, - "y": 0, - "w": 22, - "h": 29 + "y": 7, + "w": 27, + "h": 21 }, "frame": { - "x": 77, - "y": 284, - "w": 22, - "h": 29 - } - }, - { - "filename": "936", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 10, - "y": 0, - "w": 20, - "h": 28 - }, - "frame": { - "x": 102, - "y": 204, - "w": 20, - "h": 28 - } - }, - { - "filename": "908", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 10, - "y": 2, - "w": 20, - "h": 26 - }, - "frame": { - "x": 104, - "y": 232, - "w": 20, - "h": 26 - } - }, - { - "filename": "982", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 10, - "y": 2, - "w": 22, - "h": 26 - }, - "frame": { - "x": 102, - "y": 258, - "w": 22, - "h": 26 - } - }, - { - "filename": "1016", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 8, - "y": 1, - "w": 24, - "h": 27 - }, - "frame": { - "x": 99, - "y": 284, - "w": 24, - "h": 27 - } - }, - { - "filename": "1017-hearthflame-mask", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 7, - "y": 4, - "w": 25, - "h": 24 - }, - "frame": { - "x": 113, - "y": 180, - "w": 25, - "h": 24 + "x": 110, + "y": 189, + "w": 27, + "h": 21 } }, { @@ -2145,77 +2103,14 @@ "h": 24 }, "frame": { - "x": 113, - "y": 180, + "x": 85, + "y": 231, "w": 25, "h": 24 } }, { - "filename": "1017s-hearthflame-mask", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 7, - "y": 4, - "w": 25, - "h": 24 - }, - "frame": { - "x": 113, - "y": 180, - "w": 25, - "h": 24 - } - }, - { - "filename": "1017s-hearthflame-mask-tera", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 7, - "y": 4, - "w": 25, - "h": 24 - }, - "frame": { - "x": 113, - "y": 180, - "w": 25, - "h": 24 - } - }, - { - "filename": "936s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 10, - "y": 0, - "w": 20, - "h": 28 - }, - "frame": { - "x": 122, - "y": 204, - "w": 20, - "h": 28 - } - }, - { - "filename": "954", + "filename": "999", "rotated": false, "trimmed": true, "sourceSize": { @@ -2225,18 +2120,18 @@ "spriteSourceSize": { "x": 9, "y": 0, - "w": 21, + "w": 22, "h": 29 }, "frame": { - "x": 124, - "y": 232, - "w": 21, + "x": 110, + "y": 210, + "w": 22, "h": 29 } }, { - "filename": "914", + "filename": "916s", "rotated": false, "trimmed": true, "sourceSize": { @@ -2244,57 +2139,57 @@ "h": 30 }, "spriteSourceSize": { - "x": 8, - "y": 3, - "w": 24, - "h": 25 - }, - "frame": { - "x": 124, - "y": 261, - "w": 24, - "h": 25 - } - }, - { - "filename": "914s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 8, - "y": 3, - "w": 24, - "h": 25 - }, - "frame": { - "x": 123, - "y": 286, - "w": 24, - "h": 25 - } - }, - { - "filename": "916-female", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 6, - "y": 7, + "x": 7, + "y": 9, "w": 25, + "h": 19 + }, + "frame": { + "x": 81, + "y": 255, + "w": 25, + "h": 19 + } + }, + { + "filename": "982", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 10, + "y": 2, + "w": 22, + "h": 26 + }, + "frame": { + "x": 82, + "y": 274, + "w": 22, + "h": 26 + } + }, + { + "filename": "985s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 9, + "y": 7, + "w": 27, "h": 21 }, "frame": { - "x": 55, - "y": 316, - "w": 25, + "x": 137, + "y": 192, + "w": 27, "h": 21 } }, @@ -2313,14 +2208,182 @@ "h": 27 }, "frame": { - "x": 50, - "y": 337, + "x": 132, + "y": 213, "w": 24, "h": 27 } }, { - "filename": "987", + "filename": "917", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 9, + "y": 6, + "w": 22, + "h": 22 + }, + "frame": { + "x": 110, + "y": 239, + "w": 22, + "h": 22 + } + }, + { + "filename": "914s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 8, + "y": 3, + "w": 24, + "h": 25 + }, + "frame": { + "x": 132, + "y": 240, + "w": 24, + "h": 25 + } + }, + { + "filename": "956", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 7, + "y": 6, + "w": 26, + "h": 22 + }, + "frame": { + "x": 106, + "y": 261, + "w": 26, + "h": 22 + } + }, + { + "filename": "916-female", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 6, + "y": 7, + "w": 25, + "h": 21 + }, + "frame": { + "x": 132, + "y": 265, + "w": 25, + "h": 21 + } + }, + { + "filename": "1002", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 6, + "y": 7, + "w": 27, + "h": 21 + }, + "frame": { + "x": 104, + "y": 283, + "w": 27, + "h": 21 + } + }, + { + "filename": "956s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 7, + "y": 6, + "w": 26, + "h": 22 + }, + "frame": { + "x": 131, + "y": 286, + "w": 26, + "h": 22 + } + }, + { + "filename": "1017-hearthflame-mask", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 7, + "y": 4, + "w": 25, + "h": 24 + }, + "frame": { + "x": 98, + "y": 304, + "w": 25, + "h": 24 + } + }, + { + "filename": "999s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 9, + "y": 0, + "w": 22, + "h": 29 + }, + "frame": { + "x": 99, + "y": 328, + "w": 22, + "h": 29 + } + }, + { + "filename": "982s", "rotated": false, "trimmed": true, "sourceSize": { @@ -2329,15 +2392,36 @@ }, "spriteSourceSize": { "x": 10, - "y": 4, - "w": 24, - "h": 24 + "y": 2, + "w": 22, + "h": 26 }, "frame": { - "x": 50, - "y": 364, - "w": 24, - "h": 24 + "x": 99, + "y": 357, + "w": 22, + "h": 26 + } + }, + { + "filename": "1002s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 6, + "y": 7, + "w": 27, + "h": 21 + }, + "frame": { + "x": 123, + "y": 308, + "w": 27, + "h": 21 } }, { @@ -2355,8 +2439,8 @@ "h": 29 }, "frame": { - "x": 52, - "y": 388, + "x": 121, + "y": 329, "w": 21, "h": 29 } @@ -2376,14 +2460,14 @@ "h": 27 }, "frame": { - "x": 52, - "y": 417, + "x": 121, + "y": 358, "w": 22, "h": 27 } }, { - "filename": "1023", + "filename": "936", "rotated": false, "trimmed": true, "sourceSize": { @@ -2392,19 +2476,19 @@ }, "spriteSourceSize": { "x": 10, - "y": 1, + "y": 0, "w": 20, "h": 28 }, "frame": { - "x": 54, - "y": 444, + "x": 164, + "y": 170, "w": 20, "h": 28 } }, { - "filename": "1023s", + "filename": "913", "rotated": false, "trimmed": true, "sourceSize": { @@ -2412,583 +2496,16 @@ "h": 30 }, "spriteSourceSize": { - "x": 10, - "y": 1, - "w": 20, - "h": 28 + "x": 11, + "y": 5, + "w": 17, + "h": 23 }, "frame": { - "x": 54, - "y": 472, - "w": 20, - "h": 28 - } - }, - { - "filename": "1000s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 9, - "y": 1, - "w": 22, - "h": 27 - }, - "frame": { - "x": 55, - "y": 500, - "w": 22, - "h": 27 - } - }, - { - "filename": "1006", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 10, - "y": 1, - "w": 22, - "h": 27 - }, - "frame": { - "x": 56, - "y": 527, - "w": 22, - "h": 27 - } - }, - { - "filename": "1006s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 10, - "y": 1, - "w": 22, - "h": 27 - }, - "frame": { - "x": 56, - "y": 554, - "w": 22, - "h": 27 - } - }, - { - "filename": "908s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 10, - "y": 2, - "w": 20, - "h": 26 - }, - "frame": { - "x": 80, - "y": 313, - "w": 20, - "h": 26 - } - }, - { - "filename": "956", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 7, - "y": 6, - "w": 26, - "h": 22 - }, - "frame": { - "x": 100, - "y": 311, - "w": 26, - "h": 22 - } - }, - { - "filename": "917", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 9, - "y": 6, - "w": 22, - "h": 22 - }, - "frame": { - "x": 126, - "y": 311, - "w": 22, - "h": 22 - } - }, - { - "filename": "956s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 7, - "y": 6, - "w": 26, - "h": 22 - }, - "frame": { - "x": 74, - "y": 339, - "w": 26, - "h": 22 - } - }, - { - "filename": "982s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 10, - "y": 2, - "w": 22, - "h": 26 - }, - "frame": { - "x": 100, - "y": 333, - "w": 22, - "h": 26 - } - }, - { - "filename": "987s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 10, - "y": 4, - "w": 24, - "h": 24 - }, - "frame": { - "x": 74, - "y": 361, - "w": 24, - "h": 24 - } - }, - { - "filename": "1012-artisan", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 7, - "y": 6, - "w": 26, - "h": 22 - }, - "frame": { - "x": 122, - "y": 333, - "w": 26, - "h": 22 - } - }, - { - "filename": "1012-counterfeit", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 7, - "y": 6, - "w": 26, - "h": 22 - }, - "frame": { - "x": 122, - "y": 333, - "w": 26, - "h": 22 - } - }, - { - "filename": "950", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 6, - "y": 11, - "w": 28, - "h": 17 - }, - "frame": { - "x": 144, - "y": 148, - "w": 28, - "h": 17 - } - }, - { - "filename": "950s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 6, - "y": 11, - "w": 28, - "h": 17 - }, - "frame": { - "x": 172, - "y": 148, - "w": 28, - "h": 17 - } - }, - { - "filename": "985", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 9, - "y": 7, - "w": 27, - "h": 21 - }, - "frame": { - "x": 140, - "y": 165, - "w": 27, - "h": 21 - } - }, - { - "filename": "985s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 9, - "y": 7, - "w": 27, - "h": 21 - }, - "frame": { - "x": 167, - "y": 165, - "w": 27, - "h": 21 - } - }, - { - "filename": "953", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 8, - "y": 12, - "w": 24, - "h": 16 - }, - "frame": { - "x": 138, - "y": 186, - "w": 24, - "h": 16 - } - }, - { - "filename": "1010", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 9, - "y": 2, - "w": 21, - "h": 26 - }, - "frame": { - "x": 142, - "y": 202, - "w": 21, - "h": 26 - } - }, - { - "filename": "953s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 8, - "y": 12, - "w": 24, - "h": 16 - }, - "frame": { - "x": 162, - "y": 186, - "w": 24, - "h": 16 - } - }, - { - "filename": "1010s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 9, - "y": 2, - "w": 21, - "h": 26 - }, - "frame": { - "x": 163, - "y": 202, - "w": 21, - "h": 26 - } - }, - { - "filename": "1002", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 6, - "y": 7, - "w": 27, - "h": 21 - }, - "frame": { - "x": 145, - "y": 228, - "w": 27, - "h": 21 - } - }, - { - "filename": "1017-cornerstone-mask", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 8, - "y": 4, - "w": 24, - "h": 24 - }, - "frame": { - "x": 148, - "y": 249, - "w": 24, - "h": 24 - } - }, - { - "filename": "1017-cornerstone-mask-tera", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 8, - "y": 4, - "w": 24, - "h": 24 - }, - "frame": { - "x": 148, - "y": 249, - "w": 24, - "h": 24 - } - }, - { - "filename": "1017s-cornerstone-mask", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 8, - "y": 4, - "w": 24, - "h": 24 - }, - "frame": { - "x": 148, - "y": 249, - "w": 24, - "h": 24 - } - }, - { - "filename": "1017s-cornerstone-mask-tera", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 8, - "y": 4, - "w": 24, - "h": 24 - }, - "frame": { - "x": 148, - "y": 249, - "w": 24, - "h": 24 - } - }, - { - "filename": "973", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 12, - "y": 2, - "w": 15, - "h": 26 - }, - "frame": { - "x": 172, - "y": 228, - "w": 15, - "h": 26 - } - }, - { - "filename": "916s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 7, - "y": 9, - "w": 25, - "h": 19 - }, - "frame": { - "x": 148, - "y": 273, - "w": 25, - "h": 19 - } - }, - { - "filename": "997", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 9, - "y": 9, - "w": 21, - "h": 19 - }, - "frame": { - "x": 147, - "y": 292, - "w": 21, - "h": 19 + "x": 173, + "y": 147, + "w": 17, + "h": 23 } }, { @@ -3006,428 +2523,8 @@ "h": 25 }, "frame": { - "x": 148, - "y": 311, - "w": 20, - "h": 25 - } - }, - { - "filename": "910", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 9, - "y": 7, - "w": 21, - "h": 21 - }, - "frame": { - "x": 148, - "y": 336, - "w": 21, - "h": 21 - } - }, - { - "filename": "906", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 11, - "y": 9, - "w": 18, - "h": 19 - }, - "frame": { - "x": 172, - "y": 254, - "w": 18, - "h": 19 - } - }, - { - "filename": "906s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 11, - "y": 9, - "w": 18, - "h": 19 - }, - "frame": { - "x": 173, - "y": 273, - "w": 18, - "h": 19 - } - }, - { - "filename": "1017-teal-mask", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 8, - "y": 4, - "w": 24, - "h": 24 - }, - "frame": { - "x": 168, - "y": 292, - "w": 24, - "h": 24 - } - }, - { - "filename": "1017-teal-mask-tera", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 8, - "y": 4, - "w": 24, - "h": 24 - }, - "frame": { - "x": 168, - "y": 292, - "w": 24, - "h": 24 - } - }, - { - "filename": "1017s-teal-mask", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 8, - "y": 4, - "w": 24, - "h": 24 - }, - "frame": { - "x": 168, - "y": 292, - "w": 24, - "h": 24 - } - }, - { - "filename": "1017s-teal-mask-tera", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 8, - "y": 4, - "w": 24, - "h": 24 - }, - "frame": { - "x": 168, - "y": 292, - "w": 24, - "h": 24 - } - }, - { - "filename": "943", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 8, - "y": 8, - "w": 24, - "h": 20 - }, - "frame": { - "x": 168, - "y": 316, - "w": 24, - "h": 20 - } - }, - { - "filename": "916s-female", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 6, - "y": 7, - "w": 25, - "h": 21 - }, - "frame": { - "x": 169, - "y": 336, - "w": 25, - "h": 21 - } - }, - { - "filename": "1002s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 6, - "y": 7, - "w": 27, - "h": 21 - }, - "frame": { - "x": 200, - "y": 152, - "w": 27, - "h": 21 - } - }, - { - "filename": "945", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 7, - "y": 6, - "w": 25, - "h": 22 - }, - "frame": { - "x": 227, - "y": 171, - "w": 25, - "h": 22 - } - }, - { - "filename": "1012s-artisan", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 7, - "y": 6, - "w": 26, - "h": 22 - }, - "frame": { - "x": 194, - "y": 173, - "w": 26, - "h": 22 - } - }, - { - "filename": "1012s-counterfeit", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 7, - "y": 6, - "w": 26, - "h": 22 - }, - "frame": { - "x": 194, - "y": 173, - "w": 26, - "h": 22 - } - }, - { - "filename": "973s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 12, - "y": 2, - "w": 15, - "h": 26 - }, - "frame": { - "x": 184, - "y": 202, - "w": 15, - "h": 26 - } - }, - { - "filename": "918", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 9, - "y": 4, - "w": 22, - "h": 24 - }, - "frame": { - "x": 199, - "y": 195, - "w": 22, - "h": 24 - } - }, - { - "filename": "1017-wellspring-mask", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 8, - "y": 4, - "w": 24, - "h": 24 - }, - "frame": { - "x": 221, - "y": 193, - "w": 24, - "h": 24 - } - }, - { - "filename": "1017-wellspring-mask-tera", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 8, - "y": 4, - "w": 24, - "h": 24 - }, - "frame": { - "x": 221, - "y": 193, - "w": 24, - "h": 24 - } - }, - { - "filename": "1017s-wellspring-mask", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 8, - "y": 4, - "w": 24, - "h": 24 - }, - "frame": { - "x": 221, - "y": 193, - "w": 24, - "h": 24 - } - }, - { - "filename": "1017s-wellspring-mask-tera", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 8, - "y": 4, - "w": 24, - "h": 24 - }, - "frame": { - "x": 221, - "y": 193, - "w": 24, - "h": 24 - } - }, - { - "filename": "949s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 10, - "y": 3, - "w": 20, - "h": 25 - }, - "frame": { - "x": 187, - "y": 228, + "x": 177, + "y": 122, "w": 20, "h": 25 } @@ -3447,138 +2544,12 @@ "h": 20 }, "frame": { - "x": 190, - "y": 253, + "x": 178, + "y": 102, "w": 20, "h": 20 } }, - { - "filename": "967", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 10, - "y": 9, - "w": 20, - "h": 19 - }, - "frame": { - "x": 191, - "y": 273, - "w": 20, - "h": 19 - } - }, - { - "filename": "962", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 10, - "y": 3, - "w": 19, - "h": 25 - }, - "frame": { - "x": 192, - "y": 292, - "w": 19, - "h": 25 - } - }, - { - "filename": "967s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 10, - "y": 9, - "w": 20, - "h": 19 - }, - "frame": { - "x": 192, - "y": 317, - "w": 20, - "h": 19 - } - }, - { - "filename": "910s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 9, - "y": 7, - "w": 21, - "h": 21 - }, - "frame": { - "x": 194, - "y": 336, - "w": 21, - "h": 21 - } - }, - { - "filename": "962s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 10, - "y": 3, - "w": 19, - "h": 25 - }, - "frame": { - "x": 207, - "y": 219, - "w": 19, - "h": 25 - } - }, - { - "filename": "918s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 9, - "y": 4, - "w": 22, - "h": 24 - }, - "frame": { - "x": 226, - "y": 217, - "w": 22, - "h": 24 - } - }, { "filename": "923", "rotated": false, @@ -3594,77 +2565,14 @@ "h": 24 }, "frame": { - "x": 210, - "y": 244, + "x": 198, + "y": 101, "w": 19, "h": 24 } }, { - "filename": "961", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 8, - "y": 5, - "w": 23, - "h": 23 - }, - "frame": { - "x": 229, - "y": 241, - "w": 23, - "h": 23 - } - }, - { - "filename": "961s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 8, - "y": 5, - "w": 23, - "h": 23 - }, - "frame": { - "x": 229, - "y": 264, - "w": 23, - "h": 23 - } - }, - { - "filename": "909", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 11, - "y": 9, - "w": 18, - "h": 19 - }, - "frame": { - "x": 211, - "y": 268, - "w": 18, - "h": 19 - } - }, - { - "filename": "945s", + "filename": "1012-artisan", "rotated": false, "trimmed": true, "sourceSize": { @@ -3674,16 +2582,58 @@ "spriteSourceSize": { "x": 7, "y": 6, - "w": 25, + "w": 26, "h": 22 }, "frame": { - "x": 211, - "y": 287, - "w": 25, + "x": 217, + "y": 102, + "w": 26, "h": 22 } }, + { + "filename": "908s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 10, + "y": 2, + "w": 20, + "h": 26 + }, + "frame": { + "x": 197, + "y": 125, + "w": 20, + "h": 26 + } + }, + { + "filename": "1000s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 9, + "y": 1, + "w": 22, + "h": 27 + }, + "frame": { + "x": 217, + "y": 124, + "w": 22, + "h": 27 + } + }, { "filename": "948", "rotated": false, @@ -3699,8 +2649,8 @@ "h": 21 }, "frame": { - "x": 236, - "y": 287, + "x": 239, + "y": 124, "w": 16, "h": 21 } @@ -3720,14 +2670,35 @@ "h": 21 }, "frame": { - "x": 236, - "y": 308, + "x": 239, + "y": 145, "w": 16, "h": 21 } }, { - "filename": "927", + "filename": "1012-counterfeit", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 7, + "y": 6, + "w": 26, + "h": 22 + }, + "frame": { + "x": 190, + "y": 151, + "w": 26, + "h": 22 + } + }, + { + "filename": "947s", "rotated": false, "trimmed": true, "sourceSize": { @@ -3736,19 +2707,82 @@ }, "spriteSourceSize": { "x": 8, - "y": 7, - "w": 24, - "h": 21 + "y": 6, + "w": 23, + "h": 22 }, "frame": { - "x": 212, - "y": 309, - "w": 24, - "h": 21 + "x": 216, + "y": 151, + "w": 23, + "h": 22 } }, { - "filename": "920", + "filename": "951", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 12, + "y": 8, + "w": 16, + "h": 20 + }, + "frame": { + "x": 239, + "y": 166, + "w": 16, + "h": 20 + } + }, + { + "filename": "1017s-hearthflame-mask-tera", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 7, + "y": 4, + "w": 25, + "h": 24 + }, + "frame": { + "x": 184, + "y": 173, + "w": 25, + "h": 24 + } + }, + { + "filename": "1017s-hearthflame-mask", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 7, + "y": 4, + "w": 25, + "h": 24 + }, + "frame": { + "x": 209, + "y": 173, + "w": 25, + "h": 24 + } + }, + { + "filename": "1010", "rotated": false, "trimmed": true, "sourceSize": { @@ -3757,19 +2791,19 @@ }, "spriteSourceSize": { "x": 9, - "y": 5, - "w": 22, - "h": 23 + "y": 2, + "w": 21, + "h": 26 }, "frame": { - "x": 215, - "y": 330, - "w": 22, - "h": 23 + "x": 234, + "y": 186, + "w": 21, + "h": 26 } }, { - "filename": "912", + "filename": "936s", "rotated": false, "trimmed": true, "sourceSize": { @@ -3777,20 +2811,20 @@ "h": 30 }, "spriteSourceSize": { - "x": 13, - "y": 9, - "w": 15, - "h": 19 + "x": 10, + "y": 0, + "w": 20, + "h": 28 }, "frame": { - "x": 237, - "y": 329, - "w": 15, - "h": 19 + "x": 156, + "y": 213, + "w": 20, + "h": 28 } }, { - "filename": "912s", + "filename": "963", "rotated": false, "trimmed": true, "sourceSize": { @@ -3798,16 +2832,79 @@ "h": 30 }, "spriteSourceSize": { - "x": 13, - "y": 9, - "w": 15, - "h": 19 + "x": 9, + "y": 13, + "w": 22, + "h": 15 }, "frame": { - "x": 237, - "y": 348, - "w": 15, - "h": 19 + "x": 164, + "y": 198, + "w": 22, + "h": 15 + } + }, + { + "filename": "918", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 9, + "y": 4, + "w": 22, + "h": 24 + }, + "frame": { + "x": 156, + "y": 241, + "w": 22, + "h": 24 + } + }, + { + "filename": "1006", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 10, + "y": 1, + "w": 22, + "h": 27 + }, + "frame": { + "x": 157, + "y": 265, + "w": 22, + "h": 27 + } + }, + { + "filename": "1012s-artisan", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 7, + "y": 6, + "w": 26, + "h": 22 + }, + "frame": { + "x": 186, + "y": 197, + "w": 26, + "h": 22 } }, { @@ -3825,14 +2922,14 @@ "h": 22 }, "frame": { - "x": 215, - "y": 353, + "x": 212, + "y": 197, "w": 22, "h": 22 } }, { - "filename": "960", + "filename": "1010s", "rotated": false, "trimmed": true, "sourceSize": { @@ -3840,16 +2937,268 @@ "h": 30 }, "spriteSourceSize": { - "x": 12, - "y": 10, - "w": 15, - "h": 18 + "x": 9, + "y": 2, + "w": 21, + "h": 26 }, "frame": { - "x": 237, - "y": 367, - "w": 15, - "h": 18 + "x": 234, + "y": 212, + "w": 21, + "h": 26 + } + }, + { + "filename": "1012s-counterfeit", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 7, + "y": 6, + "w": 26, + "h": 22 + }, + "frame": { + "x": 176, + "y": 219, + "w": 26, + "h": 22 + } + }, + { + "filename": "987", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 10, + "y": 4, + "w": 24, + "h": 24 + }, + "frame": { + "x": 178, + "y": 241, + "w": 24, + "h": 24 + } + }, + { + "filename": "1006s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 10, + "y": 1, + "w": 22, + "h": 27 + }, + "frame": { + "x": 202, + "y": 219, + "w": 22, + "h": 27 + } + }, + { + "filename": "987s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 10, + "y": 4, + "w": 24, + "h": 24 + }, + "frame": { + "x": 179, + "y": 265, + "w": 24, + "h": 24 + } + }, + { + "filename": "997", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 9, + "y": 9, + "w": 21, + "h": 19 + }, + "frame": { + "x": 202, + "y": 246, + "w": 21, + "h": 19 + } + }, + { + "filename": "918s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 9, + "y": 4, + "w": 22, + "h": 24 + }, + "frame": { + "x": 203, + "y": 265, + "w": 22, + "h": 24 + } + }, + { + "filename": "916s-female", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 6, + "y": 7, + "w": 25, + "h": 21 + }, + "frame": { + "x": 157, + "y": 292, + "w": 25, + "h": 21 + } + }, + { + "filename": "1017-cornerstone-mask-tera", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 8, + "y": 4, + "w": 24, + "h": 24 + }, + "frame": { + "x": 182, + "y": 289, + "w": 24, + "h": 24 + } + }, + { + "filename": "923s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 10, + "y": 4, + "w": 19, + "h": 24 + }, + "frame": { + "x": 206, + "y": 289, + "w": 19, + "h": 24 + } + }, + { + "filename": "1023", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 10, + "y": 1, + "w": 20, + "h": 28 + }, + "frame": { + "x": 142, + "y": 329, + "w": 20, + "h": 28 + } + }, + { + "filename": "1023s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 10, + "y": 1, + "w": 20, + "h": 28 + }, + "frame": { + "x": 143, + "y": 357, + "w": 20, + "h": 28 + } + }, + { + "filename": "953", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 8, + "y": 12, + "w": 24, + "h": 16 + }, + "frame": { + "x": 150, + "y": 313, + "w": 24, + "h": 16 } }, { @@ -3867,8 +3216,8 @@ "h": 21 }, "frame": { - "x": 122, - "y": 355, + "x": 174, + "y": 313, "w": 25, "h": 21 } @@ -3888,12 +3237,264 @@ "h": 21 }, "frame": { - "x": 147, - "y": 357, + "x": 199, + "y": 313, "w": 25, "h": 21 } }, + { + "filename": "945", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 7, + "y": 6, + "w": 25, + "h": 22 + }, + "frame": { + "x": 162, + "y": 334, + "w": 25, + "h": 22 + } + }, + { + "filename": "945s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 7, + "y": 6, + "w": 25, + "h": 22 + }, + "frame": { + "x": 187, + "y": 334, + "w": 25, + "h": 22 + } + }, + { + "filename": "1017-cornerstone-mask", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 8, + "y": 4, + "w": 24, + "h": 24 + }, + "frame": { + "x": 163, + "y": 356, + "w": 24, + "h": 24 + } + }, + { + "filename": "1017-teal-mask-tera", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 8, + "y": 4, + "w": 24, + "h": 24 + }, + "frame": { + "x": 187, + "y": 356, + "w": 24, + "h": 24 + } + }, + { + "filename": "907", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 11, + "y": 6, + "w": 17, + "h": 22 + }, + "frame": { + "x": 212, + "y": 334, + "w": 17, + "h": 22 + } + }, + { + "filename": "949s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 10, + "y": 3, + "w": 20, + "h": 25 + }, + "frame": { + "x": 211, + "y": 356, + "w": 20, + "h": 25 + } + }, + { + "filename": "1017-teal-mask", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 8, + "y": 4, + "w": 24, + "h": 24 + }, + "frame": { + "x": 231, + "y": 238, + "w": 24, + "h": 24 + } + }, + { + "filename": "1017-wellspring-mask-tera", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 8, + "y": 4, + "w": 24, + "h": 24 + }, + "frame": { + "x": 231, + "y": 262, + "w": 24, + "h": 24 + } + }, + { + "filename": "1017-wellspring-mask", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 8, + "y": 4, + "w": 24, + "h": 24 + }, + "frame": { + "x": 231, + "y": 286, + "w": 24, + "h": 24 + } + }, + { + "filename": "1017s-cornerstone-mask-tera", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 8, + "y": 4, + "w": 24, + "h": 24 + }, + "frame": { + "x": 231, + "y": 310, + "w": 24, + "h": 24 + } + }, + { + "filename": "1017s-cornerstone-mask", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 8, + "y": 4, + "w": 24, + "h": 24 + }, + "frame": { + "x": 231, + "y": 334, + "w": 24, + "h": 24 + } + }, + { + "filename": "1017s-teal-mask-tera", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 8, + "y": 4, + "w": 24, + "h": 24 + }, + "frame": { + "x": 231, + "y": 358, + "w": 24, + "h": 24 + } + }, { "filename": "952", "rotated": false, @@ -3909,182 +3510,14 @@ "h": 22 }, "frame": { - "x": 172, - "y": 357, + "x": 163, + "y": 380, "w": 25, "h": 22 } }, { - "filename": "909s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 11, - "y": 9, - "w": 18, - "h": 19 - }, - "frame": { - "x": 197, - "y": 357, - "w": 18, - "h": 19 - } - }, - { - "filename": "920s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 9, - "y": 5, - "w": 22, - "h": 23 - }, - "frame": { - "x": 100, - "y": 359, - "w": 22, - "h": 23 - } - }, - { - "filename": "952s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 7, - "y": 6, - "w": 25, - "h": 22 - }, - "frame": { - "x": 122, - "y": 376, - "w": 25, - "h": 22 - } - }, - { - "filename": "966", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 7, - "y": 7, - "w": 25, - "h": 21 - }, - "frame": { - "x": 147, - "y": 378, - "w": 25, - "h": 21 - } - }, - { - "filename": "966s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 7, - "y": 7, - "w": 25, - "h": 21 - }, - "frame": { - "x": 172, - "y": 379, - "w": 25, - "h": 21 - } - }, - { - "filename": "923s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 10, - "y": 4, - "w": 19, - "h": 24 - }, - "frame": { - "x": 197, - "y": 376, - "w": 19, - "h": 24 - } - }, - { - "filename": "941", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 9, - "y": 7, - "w": 21, - "h": 21 - }, - "frame": { - "x": 216, - "y": 375, - "w": 21, - "h": 21 - } - }, - { - "filename": "960s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 12, - "y": 10, - "w": 15, - "h": 18 - }, - "frame": { - "x": 237, - "y": 385, - "w": 15, - "h": 18 - } - }, - { - "filename": "927s", + "filename": "961", "rotated": false, "trimmed": true, "sourceSize": { @@ -4093,122 +3526,17 @@ }, "spriteSourceSize": { "x": 8, - "y": 7, - "w": 24, - "h": 21 - }, - "frame": { - "x": 98, - "y": 382, - "w": 24, - "h": 21 - } - }, - { - "filename": "943s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 8, - "y": 8, - "w": 24, - "h": 20 - }, - "frame": { - "x": 74, - "y": 385, - "w": 24, - "h": 20 - } - }, - { - "filename": "986", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 9, "y": 5, "w": 23, "h": 23 }, "frame": { - "x": 74, - "y": 405, + "x": 188, + "y": 380, "w": 23, "h": 23 } }, - { - "filename": "986s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 9, - "y": 5, - "w": 23, - "h": 23 - }, - "frame": { - "x": 74, - "y": 428, - "w": 23, - "h": 23 - } - }, - { - "filename": "947", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 8, - "y": 6, - "w": 23, - "h": 22 - }, - "frame": { - "x": 74, - "y": 451, - "w": 23, - "h": 22 - } - }, - { - "filename": "947s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 8, - "y": 6, - "w": 23, - "h": 22 - }, - "frame": { - "x": 74, - "y": 473, - "w": 23, - "h": 22 - } - }, { "filename": "991", "rotated": false, @@ -4224,12 +3552,453 @@ "h": 24 }, "frame": { - "x": 77, - "y": 495, + "x": 211, + "y": 381, "w": 20, "h": 24 } }, + { + "filename": "1017s-teal-mask", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 8, + "y": 4, + "w": 24, + "h": 24 + }, + "frame": { + "x": 231, + "y": 382, + "w": 24, + "h": 24 + } + }, + { + "filename": "952s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 7, + "y": 6, + "w": 25, + "h": 22 + }, + "frame": { + "x": 52, + "y": 382, + "w": 25, + "h": 22 + } + }, + { + "filename": "1017s-wellspring-mask-tera", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 8, + "y": 4, + "w": 24, + "h": 24 + }, + "frame": { + "x": 50, + "y": 404, + "w": 24, + "h": 24 + } + }, + { + "filename": "920", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 9, + "y": 5, + "w": 22, + "h": 23 + }, + "frame": { + "x": 77, + "y": 382, + "w": 22, + "h": 23 + } + }, + { + "filename": "920s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 9, + "y": 5, + "w": 22, + "h": 23 + }, + "frame": { + "x": 99, + "y": 383, + "w": 22, + "h": 23 + } + }, + { + "filename": "966-caph-starmobile", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 7, + "y": 7, + "w": 25, + "h": 21 + }, + "frame": { + "x": 121, + "y": 385, + "w": 25, + "h": 21 + } + }, + { + "filename": "907s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 11, + "y": 6, + "w": 17, + "h": 22 + }, + "frame": { + "x": 146, + "y": 385, + "w": 17, + "h": 22 + } + }, + { + "filename": "966-navi-starmobile", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 7, + "y": 7, + "w": 25, + "h": 21 + }, + "frame": { + "x": 74, + "y": 405, + "w": 25, + "h": 21 + } + }, + { + "filename": "966-ruchbah-starmobile", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 7, + "y": 7, + "w": 25, + "h": 21 + }, + "frame": { + "x": 99, + "y": 406, + "w": 25, + "h": 21 + } + }, + { + "filename": "910", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 9, + "y": 7, + "w": 21, + "h": 21 + }, + "frame": { + "x": 124, + "y": 406, + "w": 21, + "h": 21 + } + }, + { + "filename": "953s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 8, + "y": 12, + "w": 24, + "h": 16 + }, + "frame": { + "x": 50, + "y": 428, + "w": 24, + "h": 16 + } + }, + { + "filename": "966-schedar-starmobile", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 7, + "y": 7, + "w": 25, + "h": 21 + }, + "frame": { + "x": 74, + "y": 426, + "w": 25, + "h": 21 + } + }, + { + "filename": "966-segin-starmobile", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 7, + "y": 7, + "w": 25, + "h": 21 + }, + "frame": { + "x": 99, + "y": 427, + "w": 25, + "h": 21 + } + }, + { + "filename": "910s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 9, + "y": 7, + "w": 21, + "h": 21 + }, + "frame": { + "x": 124, + "y": 427, + "w": 21, + "h": 21 + } + }, + { + "filename": "962", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 10, + "y": 3, + "w": 19, + "h": 25 + }, + "frame": { + "x": 145, + "y": 407, + "w": 19, + "h": 25 + } + }, + { + "filename": "1017s-wellspring-mask", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 8, + "y": 4, + "w": 24, + "h": 24 + }, + "frame": { + "x": 164, + "y": 402, + "w": 24, + "h": 24 + } + }, + { + "filename": "961s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 8, + "y": 5, + "w": 23, + "h": 23 + }, + "frame": { + "x": 188, + "y": 403, + "w": 23, + "h": 23 + } + }, + { + "filename": "939", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 10, + "y": 7, + "w": 20, + "h": 21 + }, + "frame": { + "x": 211, + "y": 405, + "w": 20, + "h": 21 + } + }, + { + "filename": "927", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 8, + "y": 7, + "w": 24, + "h": 21 + }, + "frame": { + "x": 231, + "y": 406, + "w": 24, + "h": 21 + } + }, + { + "filename": "962s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 10, + "y": 3, + "w": 19, + "h": 25 + }, + "frame": { + "x": 52, + "y": 444, + "w": 19, + "h": 25 + } + }, + { + "filename": "966", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 7, + "y": 7, + "w": 25, + "h": 21 + }, + "frame": { + "x": 71, + "y": 447, + "w": 25, + "h": 21 + } + }, { "filename": "991s", "rotated": false, @@ -4245,12 +4014,117 @@ "h": 24 }, "frame": { - "x": 78, - "y": 519, + "x": 52, + "y": 469, "w": 20, "h": 24 } }, + { + "filename": "986", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 9, + "y": 5, + "w": 23, + "h": 23 + }, + "frame": { + "x": 72, + "y": 468, + "w": 23, + "h": 23 + } + }, + { + "filename": "966s-caph-starmobile", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 7, + "y": 7, + "w": 25, + "h": 21 + }, + "frame": { + "x": 96, + "y": 448, + "w": 25, + "h": 21 + } + }, + { + "filename": "927s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 8, + "y": 7, + "w": 24, + "h": 21 + }, + "frame": { + "x": 121, + "y": 448, + "w": 24, + "h": 21 + } + }, + { + "filename": "966s-navi-starmobile", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 7, + "y": 7, + "w": 25, + "h": 21 + }, + "frame": { + "x": 95, + "y": 469, + "w": 25, + "h": 21 + } + }, + { + "filename": "966s-ruchbah-starmobile", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 7, + "y": 7, + "w": 25, + "h": 21 + }, + "frame": { + "x": 120, + "y": 469, + "w": 25, + "h": 21 + } + }, { "filename": "1013-masterpiece", "rotated": false, @@ -4266,8 +4140,8 @@ "h": 24 }, "frame": { - "x": 78, - "y": 543, + "x": 145, + "y": 432, "w": 20, "h": 24 } @@ -4287,14 +4161,14 @@ "h": 24 }, "frame": { - "x": 78, - "y": 543, + "x": 145, + "y": 456, "w": 20, "h": 24 } }, { - "filename": "1013s-masterpiece", + "filename": "966s-schedar-starmobile", "rotated": false, "trimmed": true, "sourceSize": { @@ -4302,62 +4176,20 @@ "h": 30 }, "spriteSourceSize": { - "x": 10, - "y": 4, - "w": 20, - "h": 24 - }, - "frame": { - "x": 78, - "y": 567, - "w": 20, - "h": 24 - } - }, - { - "filename": "1013s-unremarkable", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 10, - "y": 4, - "w": 20, - "h": 24 - }, - "frame": { - "x": 78, - "y": 567, - "w": 20, - "h": 24 - } - }, - { - "filename": "941s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 9, + "x": 7, "y": 7, - "w": 21, + "w": 25, "h": 21 }, "frame": { - "x": 216, - "y": 396, - "w": 21, + "x": 165, + "y": 426, + "w": 25, "h": 21 } }, { - "filename": "996", + "filename": "966s-segin-starmobile", "rotated": false, "trimmed": true, "sourceSize": { @@ -4365,16 +4197,37 @@ "h": 30 }, "spriteSourceSize": { - "x": 12, - "y": 12, - "w": 15, - "h": 16 + "x": 7, + "y": 7, + "w": 25, + "h": 21 }, "frame": { - "x": 237, - "y": 403, - "w": 15, - "h": 16 + "x": 165, + "y": 447, + "w": 25, + "h": 21 + } + }, + { + "filename": "966s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 7, + "y": 7, + "w": 25, + "h": 21 + }, + "frame": { + "x": 190, + "y": 426, + "w": 25, + "h": 21 } }, { @@ -4392,12 +4245,33 @@ "h": 21 }, "frame": { - "x": 122, - "y": 398, + "x": 190, + "y": 447, "w": 25, "h": 21 } }, + { + "filename": "973", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 12, + "y": 2, + "w": 15, + "h": 26 + }, + "frame": { + "x": 215, + "y": 426, + "w": 15, + "h": 26 + } + }, { "filename": "8128s", "rotated": false, @@ -4413,14 +4287,14 @@ "h": 21 }, "frame": { - "x": 147, - "y": 399, + "x": 230, + "y": 427, "w": 25, "h": 21 } }, { - "filename": "1024", + "filename": "943", "rotated": false, "trimmed": true, "sourceSize": { @@ -4434,8 +4308,29 @@ "h": 20 }, "frame": { - "x": 172, - "y": 400, + "x": 165, + "y": 468, + "w": 24, + "h": 20 + } + }, + { + "filename": "943s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 8, + "y": 8, + "w": 24, + "h": 20 + }, + "frame": { + "x": 189, + "y": 468, "w": 24, "h": 20 } @@ -4455,12 +4350,33 @@ "h": 20 }, "frame": { - "x": 196, - "y": 400, + "x": 145, + "y": 480, "w": 20, "h": 20 } }, + { + "filename": "1024", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 8, + "y": 8, + "w": 24, + "h": 20 + }, + "frame": { + "x": 165, + "y": 488, + "w": 24, + "h": 20 + } + }, { "filename": "1024s", "rotated": false, @@ -4476,476 +4392,14 @@ "h": 20 }, "frame": { - "x": 98, - "y": 403, + "x": 189, + "y": 488, "w": 24, "h": 20 } }, { - "filename": "939", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 10, - "y": 7, - "w": 20, - "h": 21 - }, - "frame": { - "x": 97, - "y": 423, - "w": 20, - "h": 21 - } - }, - { - "filename": "939s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 10, - "y": 7, - "w": 20, - "h": 21 - }, - "frame": { - "x": 97, - "y": 444, - "w": 20, - "h": 21 - } - }, - { - "filename": "971", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 10, - "y": 7, - "w": 20, - "h": 21 - }, - "frame": { - "x": 97, - "y": 465, - "w": 20, - "h": 21 - } - }, - { - "filename": "971s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 10, - "y": 7, - "w": 20, - "h": 21 - }, - "frame": { - "x": 97, - "y": 486, - "w": 20, - "h": 21 - } - }, - { - "filename": "969", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 11, - "y": 16, - "w": 18, - "h": 12 - }, - "frame": { - "x": 97, - "y": 507, - "w": 18, - "h": 12 - } - }, - { - "filename": "907", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 11, - "y": 6, - "w": 17, - "h": 22 - }, - "frame": { - "x": 98, - "y": 519, - "w": 17, - "h": 22 - } - }, - { - "filename": "907s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 11, - "y": 6, - "w": 17, - "h": 22 - }, - "frame": { - "x": 98, - "y": 541, - "w": 17, - "h": 22 - } - }, - { - "filename": "929", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 11, - "y": 6, - "w": 17, - "h": 22 - }, - "frame": { - "x": 98, - "y": 563, - "w": 17, - "h": 22 - } - }, - { - "filename": "929s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 11, - "y": 6, - "w": 17, - "h": 22 - }, - "frame": { - "x": 117, - "y": 423, - "w": 17, - "h": 22 - } - }, - { - "filename": "1011", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 11, - "y": 5, - "w": 19, - "h": 22 - }, - "frame": { - "x": 117, - "y": 445, - "w": 19, - "h": 22 - } - }, - { - "filename": "1011s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 11, - "y": 5, - "w": 19, - "h": 22 - }, - "frame": { - "x": 117, - "y": 467, - "w": 19, - "h": 22 - } - }, - { - "filename": "931-white-plumage", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 10, - "y": 8, - "w": 20, - "h": 20 - }, - "frame": { - "x": 117, - "y": 489, - "w": 20, - "h": 20 - } - }, - { - "filename": "1004", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 9, - "y": 8, - "w": 21, - "h": 20 - }, - "frame": { - "x": 115, - "y": 509, - "w": 21, - "h": 20 - } - }, - { - "filename": "1004s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 9, - "y": 8, - "w": 21, - "h": 20 - }, - "frame": { - "x": 115, - "y": 529, - "w": 21, - "h": 20 - } - }, - { - "filename": "931-yellow-plumage", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 10, - "y": 8, - "w": 20, - "h": 20 - }, - "frame": { - "x": 115, - "y": 549, - "w": 20, - "h": 20 - } - }, - { - "filename": "931s-blue-plumage", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 10, - "y": 8, - "w": 20, - "h": 20 - }, - "frame": { - "x": 115, - "y": 569, - "w": 20, - "h": 20 - } - }, - { - "filename": "935s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 13, - "y": 7, - "w": 13, - "h": 21 - }, - "frame": { - "x": 134, - "y": 419, - "w": 13, - "h": 21 - } - }, - { - "filename": "931s-green-plumage", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 10, - "y": 8, - "w": 20, - "h": 20 - }, - "frame": { - "x": 147, - "y": 420, - "w": 20, - "h": 20 - } - }, - { - "filename": "931s-white-plumage", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 10, - "y": 8, - "w": 20, - "h": 20 - }, - "frame": { - "x": 167, - "y": 420, - "w": 20, - "h": 20 - } - }, - { - "filename": "931s-yellow-plumage", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 10, - "y": 8, - "w": 20, - "h": 20 - }, - "frame": { - "x": 187, - "y": 420, - "w": 20, - "h": 20 - } - }, - { - "filename": "997s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 9, - "y": 9, - "w": 21, - "h": 19 - }, - "frame": { - "x": 136, - "y": 440, - "w": 21, - "h": 19 - } - }, - { - "filename": "1015", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 11, - "y": 7, - "w": 17, - "h": 21 - }, - "frame": { - "x": 136, - "y": 459, - "w": 17, - "h": 21 - } - }, - { - "filename": "932", + "filename": "906", "rotated": false, "trimmed": true, "sourceSize": { @@ -4959,222 +4413,12 @@ "h": 19 }, "frame": { - "x": 157, - "y": 440, + "x": 215, + "y": 452, "w": 18, "h": 19 } }, - { - "filename": "1015s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 11, - "y": 7, - "w": 17, - "h": 21 - }, - "frame": { - "x": 153, - "y": 459, - "w": 17, - "h": 21 - } - }, - { - "filename": "932s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 11, - "y": 9, - "w": 18, - "h": 19 - }, - "frame": { - "x": 175, - "y": 440, - "w": 18, - "h": 19 - } - }, - { - "filename": "958", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 11, - "y": 8, - "w": 17, - "h": 20 - }, - "frame": { - "x": 170, - "y": 459, - "w": 17, - "h": 20 - } - }, - { - "filename": "922", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 12, - "y": 9, - "w": 17, - "h": 19 - }, - "frame": { - "x": 193, - "y": 440, - "w": 17, - "h": 19 - } - }, - { - "filename": "958s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 11, - "y": 8, - "w": 17, - "h": 20 - }, - "frame": { - "x": 187, - "y": 459, - "w": 17, - "h": 20 - } - }, - { - "filename": "951", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 12, - "y": 8, - "w": 16, - "h": 20 - }, - "frame": { - "x": 207, - "y": 420, - "w": 16, - "h": 20 - } - }, - { - "filename": "922s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 12, - "y": 9, - "w": 17, - "h": 19 - }, - "frame": { - "x": 210, - "y": 440, - "w": 17, - "h": 19 - } - }, - { - "filename": "963", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 9, - "y": 13, - "w": 22, - "h": 15 - }, - "frame": { - "x": 204, - "y": 459, - "w": 22, - "h": 15 - } - }, - { - "filename": "999-roaming", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 13, - "y": 10, - "w": 14, - "h": 18 - }, - "frame": { - "x": 223, - "y": 417, - "w": 14, - "h": 18 - } - }, - { - "filename": "996s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 12, - "y": 12, - "w": 15, - "h": 16 - }, - "frame": { - "x": 237, - "y": 419, - "w": 15, - "h": 16 - } - }, { "filename": "963s", "rotated": false, @@ -5190,8 +4434,8 @@ "h": 15 }, "frame": { - "x": 227, - "y": 435, + "x": 233, + "y": 448, "w": 22, "h": 15 } @@ -5211,12 +4455,33 @@ "h": 15 }, "frame": { - "x": 227, - "y": 450, + "x": 233, + "y": 463, "w": 22, "h": 15 } }, + { + "filename": "1013s-masterpiece", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 10, + "y": 4, + "w": 20, + "h": 24 + }, + "frame": { + "x": 213, + "y": 471, + "w": 20, + "h": 24 + } + }, { "filename": "964s-zero", "rotated": false, @@ -5232,8 +4497,8 @@ "h": 15 }, "frame": { - "x": 226, - "y": 465, + "x": 233, + "y": 478, "w": 22, "h": 15 } @@ -5253,12 +4518,33 @@ "h": 15 }, "frame": { - "x": 204, - "y": 474, + "x": 233, + "y": 493, "w": 22, "h": 15 } }, + { + "filename": "931-white-plumage", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 10, + "y": 8, + "w": 20, + "h": 20 + }, + "frame": { + "x": 213, + "y": 495, + "w": 20, + "h": 20 + } + }, { "filename": "974s", "rotated": false, @@ -5274,12 +4560,390 @@ "h": 15 }, "frame": { - "x": 226, - "y": 480, + "x": 233, + "y": 508, "w": 22, "h": 15 } }, + { + "filename": "1013s-unremarkable", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 10, + "y": 4, + "w": 20, + "h": 24 + }, + "frame": { + "x": 54, + "y": 493, + "w": 20, + "h": 24 + } + }, + { + "filename": "986s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 9, + "y": 5, + "w": 23, + "h": 23 + }, + "frame": { + "x": 54, + "y": 517, + "w": 23, + "h": 23 + } + }, + { + "filename": "941", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 9, + "y": 7, + "w": 21, + "h": 21 + }, + "frame": { + "x": 54, + "y": 540, + "w": 21, + "h": 21 + } + }, + { + "filename": "973s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 12, + "y": 2, + "w": 15, + "h": 26 + }, + "frame": { + "x": 74, + "y": 491, + "w": 15, + "h": 26 + } + }, + { + "filename": "939s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 10, + "y": 7, + "w": 20, + "h": 21 + }, + "frame": { + "x": 55, + "y": 561, + "w": 20, + "h": 21 + } + }, + { + "filename": "913s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 11, + "y": 5, + "w": 17, + "h": 23 + }, + "frame": { + "x": 77, + "y": 517, + "w": 17, + "h": 23 + } + }, + { + "filename": "1011", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 11, + "y": 5, + "w": 19, + "h": 22 + }, + "frame": { + "x": 75, + "y": 540, + "w": 19, + "h": 22 + } + }, + { + "filename": "931-yellow-plumage", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 10, + "y": 8, + "w": 20, + "h": 20 + }, + "frame": { + "x": 75, + "y": 562, + "w": 20, + "h": 20 + } + }, + { + "filename": "941s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 9, + "y": 7, + "w": 21, + "h": 21 + }, + "frame": { + "x": 56, + "y": 582, + "w": 21, + "h": 21 + } + }, + { + "filename": "971", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 10, + "y": 7, + "w": 20, + "h": 21 + }, + "frame": { + "x": 56, + "y": 603, + "w": 20, + "h": 21 + } + }, + { + "filename": "1011s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 11, + "y": 5, + "w": 19, + "h": 22 + }, + "frame": { + "x": 56, + "y": 624, + "w": 19, + "h": 22 + } + }, + { + "filename": "929", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 11, + "y": 6, + "w": 17, + "h": 22 + }, + "frame": { + "x": 75, + "y": 624, + "w": 17, + "h": 22 + } + }, + { + "filename": "971s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 10, + "y": 7, + "w": 20, + "h": 21 + }, + "frame": { + "x": 77, + "y": 582, + "w": 20, + "h": 21 + } + }, + { + "filename": "1015", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 11, + "y": 7, + "w": 17, + "h": 21 + }, + "frame": { + "x": 76, + "y": 603, + "w": 17, + "h": 21 + } + }, + { + "filename": "929s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 11, + "y": 6, + "w": 17, + "h": 22 + }, + "frame": { + "x": 92, + "y": 624, + "w": 17, + "h": 22 + } + }, + { + "filename": "1015s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 11, + "y": 7, + "w": 17, + "h": 21 + }, + "frame": { + "x": 93, + "y": 603, + "w": 17, + "h": 21 + } + }, + { + "filename": "935", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 13, + "y": 7, + "w": 13, + "h": 21 + }, + "frame": { + "x": 89, + "y": 491, + "w": 13, + "h": 21 + } + }, + { + "filename": "1004", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 9, + "y": 8, + "w": 21, + "h": 20 + }, + "frame": { + "x": 102, + "y": 490, + "w": 21, + "h": 20 + } + }, { "filename": "980", "rotated": false, @@ -5295,12 +4959,33 @@ "h": 15 }, "frame": { - "x": 137, - "y": 480, + "x": 123, + "y": 490, "w": 22, "h": 15 } }, + { + "filename": "931s-blue-plumage", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 10, + "y": 8, + "w": 20, + "h": 20 + }, + "frame": { + "x": 145, + "y": 500, + "w": 20, + "h": 20 + } + }, { "filename": "980s", "rotated": false, @@ -5316,12 +5001,285 @@ "h": 15 }, "frame": { - "x": 137, - "y": 495, + "x": 123, + "y": 505, "w": 22, "h": 15 } }, + { + "filename": "997s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 9, + "y": 9, + "w": 21, + "h": 19 + }, + "frame": { + "x": 165, + "y": 508, + "w": 21, + "h": 19 + } + }, + { + "filename": "1004s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 9, + "y": 8, + "w": 21, + "h": 20 + }, + "frame": { + "x": 186, + "y": 508, + "w": 21, + "h": 20 + } + }, + { + "filename": "931s-green-plumage", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 10, + "y": 8, + "w": 20, + "h": 20 + }, + "frame": { + "x": 207, + "y": 515, + "w": 20, + "h": 20 + } + }, + { + "filename": "931s-white-plumage", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 10, + "y": 8, + "w": 20, + "h": 20 + }, + "frame": { + "x": 227, + "y": 523, + "w": 20, + "h": 20 + } + }, + { + "filename": "935s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 13, + "y": 7, + "w": 13, + "h": 21 + }, + "frame": { + "x": 94, + "y": 512, + "w": 13, + "h": 21 + } + }, + { + "filename": "951s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 12, + "y": 8, + "w": 16, + "h": 20 + }, + "frame": { + "x": 107, + "y": 510, + "w": 16, + "h": 20 + } + }, + { + "filename": "931s-yellow-plumage", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 10, + "y": 8, + "w": 20, + "h": 20 + }, + "frame": { + "x": 94, + "y": 533, + "w": 20, + "h": 20 + } + }, + { + "filename": "967", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 10, + "y": 9, + "w": 20, + "h": 19 + }, + "frame": { + "x": 123, + "y": 520, + "w": 20, + "h": 19 + } + }, + { + "filename": "967s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 10, + "y": 9, + "w": 20, + "h": 19 + }, + "frame": { + "x": 143, + "y": 520, + "w": 20, + "h": 19 + } + }, + { + "filename": "970", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 10, + "y": 12, + "w": 20, + "h": 16 + }, + "frame": { + "x": 163, + "y": 527, + "w": 20, + "h": 16 + } + }, + { + "filename": "970s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 10, + "y": 12, + "w": 20, + "h": 16 + }, + "frame": { + "x": 183, + "y": 528, + "w": 20, + "h": 16 + } + }, + { + "filename": "906s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 11, + "y": 9, + "w": 18, + "h": 19 + }, + "frame": { + "x": 95, + "y": 553, + "w": 18, + "h": 19 + } + }, + { + "filename": "958", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 11, + "y": 8, + "w": 17, + "h": 20 + }, + "frame": { + "x": 97, + "y": 572, + "w": 17, + "h": 20 + } + }, { "filename": "915", "rotated": false, @@ -5337,8 +5295,8 @@ "h": 17 }, "frame": { - "x": 136, - "y": 510, + "x": 114, + "y": 539, "w": 19, "h": 17 } @@ -5358,96 +5316,12 @@ "h": 17 }, "frame": { - "x": 136, - "y": 527, + "x": 133, + "y": 539, "w": 19, "h": 17 } }, - { - "filename": "951s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 12, - "y": 8, - "w": 16, - "h": 20 - }, - "frame": { - "x": 159, - "y": 480, - "w": 16, - "h": 20 - } - }, - { - "filename": "970", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 10, - "y": 12, - "w": 20, - "h": 16 - }, - "frame": { - "x": 175, - "y": 479, - "w": 20, - "h": 16 - } - }, - { - "filename": "919", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 12, - "y": 13, - "w": 16, - "h": 15 - }, - "frame": { - "x": 159, - "y": 500, - "w": 16, - "h": 15 - } - }, - { - "filename": "970s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 10, - "y": 12, - "w": 20, - "h": 16 - }, - "frame": { - "x": 175, - "y": 495, - "w": 20, - "h": 16 - } - }, { "filename": "946", "rotated": false, @@ -5463,12 +5337,96 @@ "h": 16 }, "frame": { - "x": 155, - "y": 515, + "x": 113, + "y": 556, "w": 19, "h": 16 } }, + { + "filename": "909", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 11, + "y": 9, + "w": 18, + "h": 19 + }, + "frame": { + "x": 114, + "y": 572, + "w": 18, + "h": 19 + } + }, + { + "filename": "909s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 11, + "y": 9, + "w": 18, + "h": 19 + }, + "frame": { + "x": 132, + "y": 556, + "w": 18, + "h": 19 + } + }, + { + "filename": "932", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 11, + "y": 9, + "w": 18, + "h": 19 + }, + "frame": { + "x": 132, + "y": 575, + "w": 18, + "h": 19 + } + }, + { + "filename": "938", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 14, + "y": 12, + "w": 11, + "h": 16 + }, + "frame": { + "x": 152, + "y": 539, + "w": 11, + "h": 16 + } + }, { "filename": "942", "rotated": false, @@ -5484,8 +5442,8 @@ "h": 15 }, "frame": { - "x": 155, - "y": 531, + "x": 163, + "y": 543, "w": 19, "h": 15 } @@ -5505,14 +5463,14 @@ "h": 15 }, "frame": { - "x": 136, + "x": 182, "y": 544, "w": 19, "h": 15 } }, { - "filename": "946s", + "filename": "932s", "rotated": false, "trimmed": true, "sourceSize": { @@ -5520,58 +5478,16 @@ "h": 30 }, "spriteSourceSize": { - "x": 10, - "y": 12, - "w": 19, - "h": 16 + "x": 11, + "y": 9, + "w": 18, + "h": 19 }, "frame": { - "x": 135, - "y": 559, - "w": 19, - "h": 16 - } - }, - { - "filename": "965", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 10, - "y": 12, - "w": 19, - "h": 16 - }, - "frame": { - "x": 135, - "y": 575, - "w": 19, - "h": 16 - } - }, - { - "filename": "965s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 10, - "y": 12, - "w": 19, - "h": 16 - }, - "frame": { - "x": 155, - "y": 546, - "w": 19, - "h": 16 + "x": 150, + "y": 558, + "w": 18, + "h": 19 } }, { @@ -5589,12 +5505,327 @@ "h": 17 }, "frame": { - "x": 154, - "y": 562, + "x": 150, + "y": 577, "w": 18, "h": 17 } }, + { + "filename": "999-roaming", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 13, + "y": 10, + "w": 14, + "h": 18 + }, + "frame": { + "x": 168, + "y": 558, + "w": 14, + "h": 18 + } + }, + { + "filename": "946s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 10, + "y": 12, + "w": 19, + "h": 16 + }, + "frame": { + "x": 182, + "y": 559, + "w": 19, + "h": 16 + } + }, + { + "filename": "912", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 13, + "y": 9, + "w": 15, + "h": 19 + }, + "frame": { + "x": 168, + "y": 576, + "w": 15, + "h": 19 + } + }, + { + "filename": "958s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 11, + "y": 8, + "w": 17, + "h": 20 + }, + "frame": { + "x": 183, + "y": 575, + "w": 17, + "h": 20 + } + }, + { + "filename": "965", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 10, + "y": 12, + "w": 19, + "h": 16 + }, + "frame": { + "x": 203, + "y": 535, + "w": 19, + "h": 16 + } + }, + { + "filename": "922", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 12, + "y": 9, + "w": 17, + "h": 19 + }, + "frame": { + "x": 201, + "y": 551, + "w": 17, + "h": 19 + } + }, + { + "filename": "912s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 13, + "y": 9, + "w": 15, + "h": 19 + }, + "frame": { + "x": 218, + "y": 551, + "w": 15, + "h": 19 + } + }, + { + "filename": "922s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 12, + "y": 9, + "w": 17, + "h": 19 + }, + "frame": { + "x": 233, + "y": 543, + "w": 17, + "h": 19 + } + }, + { + "filename": "926", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 11, + "y": 13, + "w": 17, + "h": 15 + }, + "frame": { + "x": 233, + "y": 562, + "w": 17, + "h": 15 + } + }, + { + "filename": "965s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 10, + "y": 12, + "w": 19, + "h": 16 + }, + "frame": { + "x": 201, + "y": 570, + "w": 19, + "h": 16 + } + }, + { + "filename": "928", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 13, + "y": 10, + "w": 13, + "h": 18 + }, + "frame": { + "x": 220, + "y": 570, + "w": 13, + "h": 18 + } + }, + { + "filename": "926s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 11, + "y": 13, + "w": 17, + "h": 15 + }, + "frame": { + "x": 233, + "y": 577, + "w": 17, + "h": 15 + } + }, + { + "filename": "969", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 11, + "y": 16, + "w": 18, + "h": 12 + }, + "frame": { + "x": 200, + "y": 586, + "w": 18, + "h": 12 + } + }, + { + "filename": "960", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 12, + "y": 10, + "w": 15, + "h": 18 + }, + "frame": { + "x": 218, + "y": 588, + "w": 15, + "h": 18 + } + }, + { + "filename": "940", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 11, + "y": 14, + "w": 17, + "h": 14 + }, + "frame": { + "x": 233, + "y": 592, + "w": 17, + "h": 14 + } + }, { "filename": "969s", "rotated": false, @@ -5610,8 +5841,8 @@ "h": 12 }, "frame": { - "x": 154, - "y": 579, + "x": 114, + "y": 591, "w": 18, "h": 12 } @@ -5631,35 +5862,14 @@ "h": 17 }, "frame": { - "x": 195, - "y": 489, + "x": 110, + "y": 603, "w": 18, "h": 17 } }, { - "filename": "928", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 13, - "y": 10, - "w": 13, - "h": 18 - }, - "frame": { - "x": 213, - "y": 489, - "w": 13, - "h": 18 - } - }, - { - "filename": "926", + "filename": "940s", "rotated": false, "trimmed": true, "sourceSize": { @@ -5668,57 +5878,15 @@ }, "spriteSourceSize": { "x": 11, - "y": 13, + "y": 14, "w": 17, - "h": 15 + "h": 14 }, "frame": { - "x": 226, - "y": 495, + "x": 132, + "y": 594, "w": 17, - "h": 15 - } - }, - { - "filename": "926s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 11, - "y": 13, - "w": 17, - "h": 15 - }, - "frame": { - "x": 195, - "y": 506, - "w": 17, - "h": 15 - } - }, - { - "filename": "999s-roaming", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 13, - "y": 10, - "w": 14, - "h": 18 - }, - "frame": { - "x": 212, - "y": 507, - "w": 14, - "h": 18 + "h": 14 } }, { @@ -5736,14 +5904,14 @@ "h": 16 }, "frame": { - "x": 226, - "y": 510, + "x": 149, + "y": 594, "w": 17, "h": 16 } }, { - "filename": "940", + "filename": "955s", "rotated": false, "trimmed": true, "sourceSize": { @@ -5752,15 +5920,15 @@ }, "spriteSourceSize": { "x": 11, - "y": 14, + "y": 12, "w": 17, - "h": 14 + "h": 16 }, "frame": { - "x": 175, - "y": 511, + "x": 166, + "y": 595, "w": 17, - "h": 14 + "h": 16 } }, { @@ -5778,12 +5946,54 @@ "h": 18 }, "frame": { - "x": 174, - "y": 525, + "x": 183, + "y": 595, "w": 16, "h": 18 } }, + { + "filename": "919", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 12, + "y": 13, + "w": 16, + "h": 15 + }, + "frame": { + "x": 199, + "y": 598, + "w": 16, + "h": 15 + } + }, + { + "filename": "919s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 12, + "y": 13, + "w": 16, + "h": 15 + }, + "frame": { + "x": 128, + "y": 608, + "w": 16, + "h": 15 + } + }, { "filename": "921s", "rotated": false, @@ -5799,12 +6009,54 @@ "h": 18 }, "frame": { - "x": 174, - "y": 543, + "x": 144, + "y": 610, "w": 16, "h": 18 } }, + { + "filename": "960s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 12, + "y": 10, + "w": 15, + "h": 18 + }, + "frame": { + "x": 109, + "y": 628, + "w": 15, + "h": 18 + } + }, + { + "filename": "999s-roaming", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 13, + "y": 10, + "w": 14, + "h": 18 + }, + "frame": { + "x": 124, + "y": 628, + "w": 14, + "h": 18 + } + }, { "filename": "928s", "rotated": false, @@ -5820,8 +6072,8 @@ "h": 18 }, "frame": { - "x": 172, - "y": 562, + "x": 138, + "y": 628, "w": 13, "h": 18 } @@ -5841,8 +6093,8 @@ "h": 17 }, "frame": { - "x": 185, - "y": 561, + "x": 160, + "y": 611, "w": 16, "h": 17 } @@ -5862,138 +6114,12 @@ "h": 17 }, "frame": { - "x": 190, - "y": 525, + "x": 151, + "y": 628, "w": 16, "h": 17 } }, - { - "filename": "955s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 11, - "y": 12, - "w": 17, - "h": 16 - }, - "frame": { - "x": 190, - "y": 542, - "w": 17, - "h": 16 - } - }, - { - "filename": "919s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 12, - "y": 13, - "w": 16, - "h": 15 - }, - "frame": { - "x": 206, - "y": 525, - "w": 16, - "h": 15 - } - }, - { - "filename": "944", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 12, - "y": 13, - "w": 16, - "h": 15 - }, - "frame": { - "x": 207, - "y": 540, - "w": 16, - "h": 15 - } - }, - { - "filename": "940s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 11, - "y": 14, - "w": 17, - "h": 14 - }, - "frame": { - "x": 222, - "y": 526, - "w": 17, - "h": 14 - } - }, - { - "filename": "944s", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 12, - "y": 13, - "w": 16, - "h": 15 - }, - "frame": { - "x": 223, - "y": 540, - "w": 16, - "h": 15 - } - }, - { - "filename": "938", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 40, - "h": 30 - }, - "spriteSourceSize": { - "x": 14, - "y": 12, - "w": 11, - "h": 16 - }, - "frame": { - "x": 239, - "y": 526, - "w": 11, - "h": 16 - } - }, { "filename": "938s", "rotated": false, @@ -6009,12 +6135,96 @@ "h": 16 }, "frame": { - "x": 239, - "y": 542, + "x": 167, + "y": 628, "w": 11, "h": 16 } }, + { + "filename": "944", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 12, + "y": 13, + "w": 16, + "h": 15 + }, + "frame": { + "x": 176, + "y": 613, + "w": 16, + "h": 15 + } + }, + { + "filename": "944s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 12, + "y": 13, + "w": 16, + "h": 15 + }, + "frame": { + "x": 192, + "y": 613, + "w": 16, + "h": 15 + } + }, + { + "filename": "996", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 12, + "y": 12, + "w": 15, + "h": 16 + }, + "frame": { + "x": 178, + "y": 628, + "w": 15, + "h": 16 + } + }, + { + "filename": "996s", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 12, + "y": 12, + "w": 15, + "h": 16 + }, + "frame": { + "x": 193, + "y": 628, + "w": 15, + "h": 16 + } + }, { "filename": "978-curly", "rotated": false, @@ -6030,8 +6240,8 @@ "h": 14 }, "frame": { - "x": 207, - "y": 555, + "x": 208, + "y": 613, "w": 15, "h": 14 } @@ -6051,8 +6261,8 @@ "h": 14 }, "frame": { - "x": 222, - "y": 555, + "x": 208, + "y": 627, "w": 15, "h": 14 } @@ -6072,8 +6282,8 @@ "h": 14 }, "frame": { - "x": 237, - "y": 558, + "x": 223, + "y": 606, "w": 15, "h": 14 } @@ -6093,8 +6303,8 @@ "h": 14 }, "frame": { - "x": 201, - "y": 569, + "x": 223, + "y": 620, "w": 15, "h": 14 } @@ -6114,8 +6324,8 @@ "h": 14 }, "frame": { - "x": 216, - "y": 569, + "x": 238, + "y": 606, "w": 15, "h": 14 } @@ -6135,8 +6345,8 @@ "h": 14 }, "frame": { - "x": 231, - "y": 572, + "x": 238, + "y": 620, "w": 15, "h": 14 } @@ -6147,6 +6357,6 @@ "meta": { "app": "https://www.codeandweb.com/texturepacker", "version": "3.0", - "smartupdate": "$TexturePacker:SmartUpdate:d412b44b05c0ac7988fc321b8a4eb571:51dd93a83920102d7a1b879808f62790:6fb417eff82c0971c86b4818772ba292$" + "smartupdate": "$TexturePacker:SmartUpdate:b180859bc4c006d56ee5322ca73fa54e:212d282258f5086ad99b3b2b95f7ec1a:6fb417eff82c0971c86b4818772ba292$" } } diff --git a/public/images/pokemon_icons_9.png b/public/images/pokemon_icons_9.png index 6123a15cbe9..2985fb800d6 100644 Binary files a/public/images/pokemon_icons_9.png and b/public/images/pokemon_icons_9.png differ diff --git a/public/images/statuses_ca_ES.json b/public/images/statuses_ca_ES.json new file mode 100644 index 00000000000..be1b78e0e41 --- /dev/null +++ b/public/images/statuses_ca_ES.json @@ -0,0 +1,188 @@ +{ + "textures": [ + { + "image": "statuses_ca_ES.png", + "format": "RGBA8888", + "size": { + "w": 22, + "h": 64 + }, + "scale": 1, + "frames": [ + { + "filename": "pokerus", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 22, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 22, + "h": 8 + }, + "frame": { + "x": 0, + "y": 0, + "w": 22, + "h": 8 + } + }, + { + "filename": "burn", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 8, + "w": 20, + "h": 8 + } + }, + { + "filename": "faint", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 16, + "w": 20, + "h": 8 + } + }, + { + "filename": "freeze", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 24, + "w": 20, + "h": 8 + } + }, + { + "filename": "paralysis", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 32, + "w": 20, + "h": 8 + } + }, + { + "filename": "poison", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 40, + "w": 20, + "h": 8 + } + }, + { + "filename": "sleep", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 48, + "w": 20, + "h": 8 + } + }, + { + "filename": "toxic", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 56, + "w": 20, + "h": 8 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:37686e85605d17b806f22d43081c1139:70535ffee63ba61b3397d8470c2c8982:e6649238c018d3630e55681417c698ca$" + } +} diff --git a/public/images/statuses_ca_ES.png b/public/images/statuses_ca_ES.png new file mode 100644 index 00000000000..d372b989be9 Binary files /dev/null and b/public/images/statuses_ca_ES.png differ diff --git a/public/images/statuses_de.json b/public/images/statuses_de.json new file mode 100644 index 00000000000..90840b8eeb1 --- /dev/null +++ b/public/images/statuses_de.json @@ -0,0 +1,188 @@ +{ + "textures": [ + { + "image": "statuses_de.png", + "format": "RGBA8888", + "size": { + "w": 22, + "h": 64 + }, + "scale": 1, + "frames": [ + { + "filename": "pokerus", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 22, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 22, + "h": 8 + }, + "frame": { + "x": 0, + "y": 0, + "w": 22, + "h": 8 + } + }, + { + "filename": "burn", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 8, + "w": 20, + "h": 8 + } + }, + { + "filename": "faint", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 16, + "w": 20, + "h": 8 + } + }, + { + "filename": "freeze", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 24, + "w": 20, + "h": 8 + } + }, + { + "filename": "paralysis", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 32, + "w": 20, + "h": 8 + } + }, + { + "filename": "poison", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 40, + "w": 20, + "h": 8 + } + }, + { + "filename": "sleep", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 48, + "w": 20, + "h": 8 + } + }, + { + "filename": "toxic", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 56, + "w": 20, + "h": 8 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:37686e85605d17b806f22d43081c1139:70535ffee63ba61b3397d8470c2c8982:e6649238c018d3630e55681417c698ca$" + } +} diff --git a/public/images/statuses_de.png b/public/images/statuses_de.png new file mode 100644 index 00000000000..ab85384d591 Binary files /dev/null and b/public/images/statuses_de.png differ diff --git a/public/images/statuses_es.json b/public/images/statuses_es.json new file mode 100644 index 00000000000..4b44aa117e4 --- /dev/null +++ b/public/images/statuses_es.json @@ -0,0 +1,188 @@ +{ + "textures": [ + { + "image": "statuses_es.png", + "format": "RGBA8888", + "size": { + "w": 22, + "h": 64 + }, + "scale": 1, + "frames": [ + { + "filename": "pokerus", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 22, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 22, + "h": 8 + }, + "frame": { + "x": 0, + "y": 0, + "w": 22, + "h": 8 + } + }, + { + "filename": "burn", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 8, + "w": 20, + "h": 8 + } + }, + { + "filename": "faint", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 16, + "w": 20, + "h": 8 + } + }, + { + "filename": "freeze", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 24, + "w": 20, + "h": 8 + } + }, + { + "filename": "paralysis", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 32, + "w": 20, + "h": 8 + } + }, + { + "filename": "poison", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 40, + "w": 20, + "h": 8 + } + }, + { + "filename": "sleep", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 48, + "w": 20, + "h": 8 + } + }, + { + "filename": "toxic", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 56, + "w": 20, + "h": 8 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:37686e85605d17b806f22d43081c1139:70535ffee63ba61b3397d8470c2c8982:e6649238c018d3630e55681417c698ca$" + } +} diff --git a/public/images/statuses_es.png b/public/images/statuses_es.png new file mode 100644 index 00000000000..d372b989be9 Binary files /dev/null and b/public/images/statuses_es.png differ diff --git a/public/images/statuses_fr.json b/public/images/statuses_fr.json new file mode 100644 index 00000000000..78f78a0856c --- /dev/null +++ b/public/images/statuses_fr.json @@ -0,0 +1,188 @@ +{ + "textures": [ + { + "image": "statuses_fr.png", + "format": "RGBA8888", + "size": { + "w": 22, + "h": 64 + }, + "scale": 1, + "frames": [ + { + "filename": "pokerus", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 22, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 22, + "h": 8 + }, + "frame": { + "x": 0, + "y": 0, + "w": 22, + "h": 8 + } + }, + { + "filename": "burn", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 8, + "w": 20, + "h": 8 + } + }, + { + "filename": "faint", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 16, + "w": 20, + "h": 8 + } + }, + { + "filename": "freeze", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 24, + "w": 20, + "h": 8 + } + }, + { + "filename": "paralysis", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 32, + "w": 20, + "h": 8 + } + }, + { + "filename": "poison", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 40, + "w": 20, + "h": 8 + } + }, + { + "filename": "sleep", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 48, + "w": 20, + "h": 8 + } + }, + { + "filename": "toxic", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 56, + "w": 20, + "h": 8 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:37686e85605d17b806f22d43081c1139:70535ffee63ba61b3397d8470c2c8982:e6649238c018d3630e55681417c698ca$" + } +} diff --git a/public/images/statuses_fr.png b/public/images/statuses_fr.png new file mode 100644 index 00000000000..95989cd5d97 Binary files /dev/null and b/public/images/statuses_fr.png differ diff --git a/public/images/statuses_it.json b/public/images/statuses_it.json new file mode 100644 index 00000000000..76fc9ae8b4b --- /dev/null +++ b/public/images/statuses_it.json @@ -0,0 +1,188 @@ +{ + "textures": [ + { + "image": "statuses_it.png", + "format": "RGBA8888", + "size": { + "w": 22, + "h": 64 + }, + "scale": 1, + "frames": [ + { + "filename": "pokerus", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 22, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 22, + "h": 8 + }, + "frame": { + "x": 0, + "y": 0, + "w": 22, + "h": 8 + } + }, + { + "filename": "burn", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 8, + "w": 20, + "h": 8 + } + }, + { + "filename": "faint", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 16, + "w": 20, + "h": 8 + } + }, + { + "filename": "freeze", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 24, + "w": 20, + "h": 8 + } + }, + { + "filename": "paralysis", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 32, + "w": 20, + "h": 8 + } + }, + { + "filename": "poison", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 40, + "w": 20, + "h": 8 + } + }, + { + "filename": "sleep", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 48, + "w": 20, + "h": 8 + } + }, + { + "filename": "toxic", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 56, + "w": 20, + "h": 8 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:37686e85605d17b806f22d43081c1139:70535ffee63ba61b3397d8470c2c8982:e6649238c018d3630e55681417c698ca$" + } +} diff --git a/public/images/statuses_it.png b/public/images/statuses_it.png new file mode 100644 index 00000000000..d372b989be9 Binary files /dev/null and b/public/images/statuses_it.png differ diff --git a/public/images/statuses_ja.json b/public/images/statuses_ja.json new file mode 100644 index 00000000000..8de633e8e43 --- /dev/null +++ b/public/images/statuses_ja.json @@ -0,0 +1,188 @@ +{ + "textures": [ + { + "image": "statuses_ja.png", + "format": "RGBA8888", + "size": { + "w": 22, + "h": 64 + }, + "scale": 1, + "frames": [ + { + "filename": "pokerus", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 22, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 22, + "h": 8 + }, + "frame": { + "x": 0, + "y": 0, + "w": 22, + "h": 8 + } + }, + { + "filename": "burn", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 8, + "w": 20, + "h": 8 + } + }, + { + "filename": "faint", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 16, + "w": 20, + "h": 8 + } + }, + { + "filename": "freeze", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 24, + "w": 20, + "h": 8 + } + }, + { + "filename": "paralysis", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 32, + "w": 20, + "h": 8 + } + }, + { + "filename": "poison", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 40, + "w": 20, + "h": 8 + } + }, + { + "filename": "sleep", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 48, + "w": 20, + "h": 8 + } + }, + { + "filename": "toxic", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 56, + "w": 20, + "h": 8 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:37686e85605d17b806f22d43081c1139:70535ffee63ba61b3397d8470c2c8982:e6649238c018d3630e55681417c698ca$" + } +} diff --git a/public/images/statuses_ja.png b/public/images/statuses_ja.png new file mode 100644 index 00000000000..305fbe9168c Binary files /dev/null and b/public/images/statuses_ja.png differ diff --git a/public/images/statuses_ko.json b/public/images/statuses_ko.json new file mode 100644 index 00000000000..7e1e2dd6cda --- /dev/null +++ b/public/images/statuses_ko.json @@ -0,0 +1,188 @@ +{ + "textures": [ + { + "image": "statuses_ko.png", + "format": "RGBA8888", + "size": { + "w": 22, + "h": 64 + }, + "scale": 1, + "frames": [ + { + "filename": "pokerus", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 22, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 22, + "h": 8 + }, + "frame": { + "x": 0, + "y": 0, + "w": 22, + "h": 8 + } + }, + { + "filename": "burn", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 8, + "w": 20, + "h": 8 + } + }, + { + "filename": "faint", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 16, + "w": 20, + "h": 8 + } + }, + { + "filename": "freeze", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 24, + "w": 20, + "h": 8 + } + }, + { + "filename": "paralysis", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 32, + "w": 20, + "h": 8 + } + }, + { + "filename": "poison", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 40, + "w": 20, + "h": 8 + } + }, + { + "filename": "sleep", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 48, + "w": 20, + "h": 8 + } + }, + { + "filename": "toxic", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 56, + "w": 20, + "h": 8 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:37686e85605d17b806f22d43081c1139:70535ffee63ba61b3397d8470c2c8982:e6649238c018d3630e55681417c698ca$" + } +} diff --git a/public/images/statuses_ko.png b/public/images/statuses_ko.png new file mode 100644 index 00000000000..d372b989be9 Binary files /dev/null and b/public/images/statuses_ko.png differ diff --git a/public/images/statuses_pt_BR.json b/public/images/statuses_pt_BR.json new file mode 100644 index 00000000000..b607991af8f --- /dev/null +++ b/public/images/statuses_pt_BR.json @@ -0,0 +1,188 @@ +{ + "textures": [ + { + "image": "statuses_pt_BR.png", + "format": "RGBA8888", + "size": { + "w": 22, + "h": 64 + }, + "scale": 1, + "frames": [ + { + "filename": "pokerus", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 22, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 22, + "h": 8 + }, + "frame": { + "x": 0, + "y": 0, + "w": 22, + "h": 8 + } + }, + { + "filename": "burn", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 8, + "w": 20, + "h": 8 + } + }, + { + "filename": "faint", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 16, + "w": 20, + "h": 8 + } + }, + { + "filename": "freeze", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 24, + "w": 20, + "h": 8 + } + }, + { + "filename": "paralysis", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 32, + "w": 20, + "h": 8 + } + }, + { + "filename": "poison", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 40, + "w": 20, + "h": 8 + } + }, + { + "filename": "sleep", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 48, + "w": 20, + "h": 8 + } + }, + { + "filename": "toxic", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 56, + "w": 20, + "h": 8 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:37686e85605d17b806f22d43081c1139:70535ffee63ba61b3397d8470c2c8982:e6649238c018d3630e55681417c698ca$" + } +} diff --git a/public/images/statuses_pt_BR.png b/public/images/statuses_pt_BR.png new file mode 100644 index 00000000000..3073540e8a2 Binary files /dev/null and b/public/images/statuses_pt_BR.png differ diff --git a/public/images/statuses_zh_CN.json b/public/images/statuses_zh_CN.json new file mode 100644 index 00000000000..28760650ecd --- /dev/null +++ b/public/images/statuses_zh_CN.json @@ -0,0 +1,188 @@ +{ + "textures": [ + { + "image": "statuses_zh_CN.png", + "format": "RGBA8888", + "size": { + "w": 22, + "h": 64 + }, + "scale": 1, + "frames": [ + { + "filename": "pokerus", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 22, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 22, + "h": 8 + }, + "frame": { + "x": 0, + "y": 0, + "w": 22, + "h": 8 + } + }, + { + "filename": "burn", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 8, + "w": 20, + "h": 8 + } + }, + { + "filename": "faint", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 16, + "w": 20, + "h": 8 + } + }, + { + "filename": "freeze", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 24, + "w": 20, + "h": 8 + } + }, + { + "filename": "paralysis", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 32, + "w": 20, + "h": 8 + } + }, + { + "filename": "poison", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 40, + "w": 20, + "h": 8 + } + }, + { + "filename": "sleep", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 48, + "w": 20, + "h": 8 + } + }, + { + "filename": "toxic", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 56, + "w": 20, + "h": 8 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:37686e85605d17b806f22d43081c1139:70535ffee63ba61b3397d8470c2c8982:e6649238c018d3630e55681417c698ca$" + } +} diff --git a/public/images/statuses_zh_CN.png b/public/images/statuses_zh_CN.png new file mode 100644 index 00000000000..d372b989be9 Binary files /dev/null and b/public/images/statuses_zh_CN.png differ diff --git a/public/images/statuses_zh_TW.json b/public/images/statuses_zh_TW.json new file mode 100644 index 00000000000..bf05b2ab0d5 --- /dev/null +++ b/public/images/statuses_zh_TW.json @@ -0,0 +1,188 @@ +{ + "textures": [ + { + "image": "statuses.png", + "format": "RGBA8888", + "size": { + "w": 22, + "h": 64 + }, + "scale": 1, + "frames": [ + { + "filename": "pokerus", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 22, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 22, + "h": 8 + }, + "frame": { + "x": 0, + "y": 0, + "w": 22, + "h": 8 + } + }, + { + "filename": "burn", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 8, + "w": 20, + "h": 8 + } + }, + { + "filename": "faint", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 16, + "w": 20, + "h": 8 + } + }, + { + "filename": "freeze", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 24, + "w": 20, + "h": 8 + } + }, + { + "filename": "paralysis", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 32, + "w": 20, + "h": 8 + } + }, + { + "filename": "poison", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 40, + "w": 20, + "h": 8 + } + }, + { + "filename": "sleep", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 48, + "w": 20, + "h": 8 + } + }, + { + "filename": "toxic", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 56, + "w": 20, + "h": 8 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:37686e85605d17b806f22d43081c1139:70535ffee63ba61b3397d8470c2c8982:e6649238c018d3630e55681417c698ca$" + } +} diff --git a/public/images/statuses_zh_TW.png b/public/images/statuses_zh_TW.png new file mode 100644 index 00000000000..d372b989be9 Binary files /dev/null and b/public/images/statuses_zh_TW.png differ diff --git a/public/images/trainer/atticus.json b/public/images/trainer/atticus.json new file mode 100644 index 00000000000..95621998bf2 --- /dev/null +++ b/public/images/trainer/atticus.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "atticus.png", + "format": "RGBA8888", + "size": { + "w": 46, + "h": 46 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 21, + "y": 33, + "w": 43, + "h": 46 + }, + "frame": { + "x": 0, + "y": 0, + "w": 43, + "h": 46 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:6dcd7c3d3982793cbca0d6fcd1f9260e:19c44634629fadd9d039d23dc71ec987:d26ede35f15aa571d5a7a2dd2fb868e1$" + } +} diff --git a/public/images/trainer/atticus.png b/public/images/trainer/atticus.png new file mode 100644 index 00000000000..e3e7e870f2b Binary files /dev/null and b/public/images/trainer/atticus.png differ diff --git a/public/images/trainer/buck.json b/public/images/trainer/buck.json new file mode 100644 index 00000000000..d2d215f716a --- /dev/null +++ b/public/images/trainer/buck.json @@ -0,0 +1,524 @@ +{ + "textures": [ + { + "image": "buck.png", + "format": "RGBA8888", + "size": { + "w": 120, + "h": 78 + }, + "scale": 1, + "frames": [ + { + "filename": "0002.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 33, + "y": 4, + "w": 35, + "h": 76 + }, + "frame": { + "x": 1, + "y": 1, + "w": 35, + "h": 76 + } + }, + { + "filename": "0003.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 33, + "y": 4, + "w": 35, + "h": 76 + }, + "frame": { + "x": 1, + "y": 1, + "w": 35, + "h": 76 + } + }, + { + "filename": "0006.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 33, + "y": 4, + "w": 35, + "h": 76 + }, + "frame": { + "x": 1, + "y": 1, + "w": 35, + "h": 76 + } + }, + { + "filename": "0007.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 33, + "y": 4, + "w": 35, + "h": 76 + }, + "frame": { + "x": 1, + "y": 1, + "w": 35, + "h": 76 + } + }, + { + "filename": "0010.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 33, + "y": 4, + "w": 35, + "h": 76 + }, + "frame": { + "x": 1, + "y": 1, + "w": 35, + "h": 76 + } + }, + { + "filename": "0011.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 33, + "y": 4, + "w": 35, + "h": 76 + }, + "frame": { + "x": 1, + "y": 1, + "w": 35, + "h": 76 + } + }, + { + "filename": "0014.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 33, + "y": 4, + "w": 35, + "h": 76 + }, + "frame": { + "x": 1, + "y": 1, + "w": 35, + "h": 76 + } + }, + { + "filename": "0015.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 33, + "y": 4, + "w": 35, + "h": 76 + }, + "frame": { + "x": 1, + "y": 1, + "w": 35, + "h": 76 + } + }, + { + "filename": "0018.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 18, + "y": 8, + "w": 44, + "h": 72 + }, + "frame": { + "x": 38, + "y": 1, + "w": 44, + "h": 72 + } + }, + { + "filename": "0019.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 18, + "y": 8, + "w": 44, + "h": 72 + }, + "frame": { + "x": 38, + "y": 1, + "w": 44, + "h": 72 + } + }, + { + "filename": "0020.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 15, + "y": 8, + "w": 44, + "h": 72 + }, + "frame": { + "x": 38, + "y": 1, + "w": 44, + "h": 72 + } + }, + { + "filename": "0021.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 15, + "y": 8, + "w": 44, + "h": 72 + }, + "frame": { + "x": 38, + "y": 1, + "w": 44, + "h": 72 + } + }, + { + "filename": "0022.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 8, + "w": 44, + "h": 72 + }, + "frame": { + "x": 38, + "y": 1, + "w": 44, + "h": 72 + } + }, + { + "filename": "0023.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 8, + "w": 44, + "h": 72 + }, + "frame": { + "x": 38, + "y": 1, + "w": 44, + "h": 72 + } + }, + { + "filename": "0000.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 34, + "y": 5, + "w": 35, + "h": 75 + }, + "frame": { + "x": 84, + "y": 1, + "w": 35, + "h": 75 + } + }, + { + "filename": "0001.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 34, + "y": 5, + "w": 35, + "h": 75 + }, + "frame": { + "x": 84, + "y": 1, + "w": 35, + "h": 75 + } + }, + { + "filename": "0004.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 34, + "y": 5, + "w": 35, + "h": 75 + }, + "frame": { + "x": 84, + "y": 1, + "w": 35, + "h": 75 + } + }, + { + "filename": "0005.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 34, + "y": 5, + "w": 35, + "h": 75 + }, + "frame": { + "x": 84, + "y": 1, + "w": 35, + "h": 75 + } + }, + { + "filename": "0008.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 34, + "y": 5, + "w": 35, + "h": 75 + }, + "frame": { + "x": 84, + "y": 1, + "w": 35, + "h": 75 + } + }, + { + "filename": "0009.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 34, + "y": 5, + "w": 35, + "h": 75 + }, + "frame": { + "x": 84, + "y": 1, + "w": 35, + "h": 75 + } + }, + { + "filename": "0012.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 34, + "y": 5, + "w": 35, + "h": 75 + }, + "frame": { + "x": 84, + "y": 1, + "w": 35, + "h": 75 + } + }, + { + "filename": "0013.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 34, + "y": 5, + "w": 35, + "h": 75 + }, + "frame": { + "x": 84, + "y": 1, + "w": 35, + "h": 75 + } + }, + { + "filename": "0016.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 34, + "y": 5, + "w": 35, + "h": 75 + }, + "frame": { + "x": 84, + "y": 1, + "w": 35, + "h": 75 + } + }, + { + "filename": "0017.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 34, + "y": 5, + "w": 35, + "h": 75 + }, + "frame": { + "x": 84, + "y": 1, + "w": 35, + "h": 75 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:033f3d363b4192f64c92e02c19622c15:0d06141bef5af87ef82da967253207cb:3347efe478119141b0e3e6eccdecd0f5$" + } +} diff --git a/public/images/trainer/buck.png b/public/images/trainer/buck.png new file mode 100644 index 00000000000..2384fb42a33 Binary files /dev/null and b/public/images/trainer/buck.png differ diff --git a/public/images/trainer/bug_type_superfan.json b/public/images/trainer/bug_type_superfan.json new file mode 100644 index 00000000000..74dca3583d5 --- /dev/null +++ b/public/images/trainer/bug_type_superfan.json @@ -0,0 +1,1469 @@ +{ + "textures": [ + { + "image": "bug_type_superfan.png", + "format": "RGBA8888", + "size": { + "w": 224, + "h": 224 + }, + "scale": 1, + "frames": [ + { + "filename": "0009.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 5, + "y": 1, + "w": 52, + "h": 85 + }, + "frame": { + "x": 1, + "y": 1, + "w": 52, + "h": 85 + } + }, + { + "filename": "0010.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 5, + "y": 1, + "w": 52, + "h": 85 + }, + "frame": { + "x": 1, + "y": 1, + "w": 52, + "h": 85 + } + }, + { + "filename": "0011.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 9, + "y": 11, + "w": 60, + "h": 75 + }, + "frame": { + "x": 55, + "y": 1, + "w": 60, + "h": 75 + } + }, + { + "filename": "0001.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0026.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0027.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0028.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0029.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0030.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0031.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0032.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0033.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0034.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0035.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0036.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0037.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0038.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0039.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0040.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0041.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0042.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0043.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0044.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0045.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0046.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0047.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0048.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0049.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0050.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0051.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0052.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0053.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0054.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0055.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0056.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0057.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0058.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0059.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0060.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0061.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0062.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0063.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0064.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0065.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0066.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0067.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0068.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0069.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0002.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 10, + "y": 0, + "w": 46, + "h": 86 + }, + "frame": { + "x": 1, + "y": 88, + "w": 46, + "h": 86 + } + }, + { + "filename": "0003.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 10, + "y": 0, + "w": 46, + "h": 86 + }, + "frame": { + "x": 1, + "y": 88, + "w": 46, + "h": 86 + } + }, + { + "filename": "0004.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 10, + "y": 0, + "w": 46, + "h": 86 + }, + "frame": { + "x": 1, + "y": 88, + "w": 46, + "h": 86 + } + }, + { + "filename": "0005.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 10, + "y": 0, + "w": 46, + "h": 86 + }, + "frame": { + "x": 1, + "y": 88, + "w": 46, + "h": 86 + } + }, + { + "filename": "0006.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 10, + "y": 0, + "w": 46, + "h": 86 + }, + "frame": { + "x": 1, + "y": 88, + "w": 46, + "h": 86 + } + }, + { + "filename": "0007.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 10, + "y": 0, + "w": 46, + "h": 86 + }, + "frame": { + "x": 1, + "y": 88, + "w": 46, + "h": 86 + } + }, + { + "filename": "0008.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 10, + "y": 0, + "w": 46, + "h": 86 + }, + "frame": { + "x": 1, + "y": 88, + "w": 46, + "h": 86 + } + }, + { + "filename": "0012.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 8, + "y": 19, + "w": 66, + "h": 67 + }, + "frame": { + "x": 49, + "y": 88, + "w": 66, + "h": 67 + } + }, + { + "filename": "0013.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 8, + "y": 19, + "w": 66, + "h": 67 + }, + "frame": { + "x": 49, + "y": 88, + "w": 66, + "h": 67 + } + }, + { + "filename": "0014.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 8, + "y": 20, + "w": 65, + "h": 66 + }, + "frame": { + "x": 49, + "y": 157, + "w": 65, + "h": 66 + } + }, + { + "filename": "0015.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 8, + "y": 20, + "w": 65, + "h": 66 + }, + "frame": { + "x": 49, + "y": 157, + "w": 65, + "h": 66 + } + }, + { + "filename": "0016.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 8, + "y": 20, + "w": 65, + "h": 66 + }, + "frame": { + "x": 116, + "y": 157, + "w": 65, + "h": 66 + } + }, + { + "filename": "0017.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 8, + "y": 20, + "w": 65, + "h": 66 + }, + "frame": { + "x": 116, + "y": 157, + "w": 65, + "h": 66 + } + }, + { + "filename": "0018.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 8, + "y": 20, + "w": 65, + "h": 66 + }, + "frame": { + "x": 116, + "y": 157, + "w": 65, + "h": 66 + } + }, + { + "filename": "0019.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 8, + "y": 20, + "w": 65, + "h": 66 + }, + "frame": { + "x": 116, + "y": 157, + "w": 65, + "h": 66 + } + }, + { + "filename": "0020.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 8, + "y": 20, + "w": 65, + "h": 66 + }, + "frame": { + "x": 116, + "y": 157, + "w": 65, + "h": 66 + } + }, + { + "filename": "0021.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 8, + "y": 20, + "w": 65, + "h": 66 + }, + "frame": { + "x": 116, + "y": 157, + "w": 65, + "h": 66 + } + }, + { + "filename": "0022.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 8, + "y": 20, + "w": 65, + "h": 66 + }, + "frame": { + "x": 116, + "y": 157, + "w": 65, + "h": 66 + } + }, + { + "filename": "0023.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 8, + "y": 20, + "w": 65, + "h": 66 + }, + "frame": { + "x": 116, + "y": 157, + "w": 65, + "h": 66 + } + }, + { + "filename": "0024.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 7, + "y": 19, + "w": 65, + "h": 67 + }, + "frame": { + "x": 117, + "y": 71, + "w": 65, + "h": 67 + } + }, + { + "filename": "0025.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 7, + "y": 19, + "w": 65, + "h": 67 + }, + "frame": { + "x": 117, + "y": 71, + "w": 65, + "h": 67 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:442c13442d70348845d7f5fcdfc121b3:3b8402aa64ee8990e64c7f03ffffbc55:568199339797fd79d11ae8d741953c1c$" + } +} diff --git a/public/images/trainer/bug_type_superfan.png b/public/images/trainer/bug_type_superfan.png new file mode 100644 index 00000000000..59316fe6ed8 Binary files /dev/null and b/public/images/trainer/bug_type_superfan.png differ diff --git a/public/images/trainer/cheryl.json b/public/images/trainer/cheryl.json new file mode 100644 index 00000000000..4cac665a588 --- /dev/null +++ b/public/images/trainer/cheryl.json @@ -0,0 +1,398 @@ +{ + "textures": [ + { + "image": "cheryl.png", + "format": "RGBA8888", + "size": { + "w": 154, + "h": 83 + }, + "scale": 1, + "frames": [ + { + "filename": "0006.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 81 + }, + "spriteSourceSize": { + "x": 25, + "y": 0, + "w": 41, + "h": 81 + }, + "frame": { + "x": 1, + "y": 1, + "w": 41, + "h": 81 + } + }, + { + "filename": "0007.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 81 + }, + "spriteSourceSize": { + "x": 25, + "y": 0, + "w": 41, + "h": 81 + }, + "frame": { + "x": 1, + "y": 1, + "w": 41, + "h": 81 + } + }, + { + "filename": "0008.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 81 + }, + "spriteSourceSize": { + "x": 26, + "y": 0, + "w": 41, + "h": 81 + }, + "frame": { + "x": 1, + "y": 1, + "w": 41, + "h": 81 + } + }, + { + "filename": "0009.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 81 + }, + "spriteSourceSize": { + "x": 26, + "y": 0, + "w": 41, + "h": 81 + }, + "frame": { + "x": 1, + "y": 1, + "w": 41, + "h": 81 + } + }, + { + "filename": "0010.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 81 + }, + "spriteSourceSize": { + "x": 27, + "y": 0, + "w": 41, + "h": 81 + }, + "frame": { + "x": 44, + "y": 1, + "w": 41, + "h": 81 + } + }, + { + "filename": "0011.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 81 + }, + "spriteSourceSize": { + "x": 27, + "y": 0, + "w": 41, + "h": 81 + }, + "frame": { + "x": 44, + "y": 1, + "w": 41, + "h": 81 + } + }, + { + "filename": "0012.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 81 + }, + "spriteSourceSize": { + "x": 24, + "y": 0, + "w": 41, + "h": 81 + }, + "frame": { + "x": 44, + "y": 1, + "w": 41, + "h": 81 + } + }, + { + "filename": "0013.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 81 + }, + "spriteSourceSize": { + "x": 24, + "y": 0, + "w": 41, + "h": 81 + }, + "frame": { + "x": 44, + "y": 1, + "w": 41, + "h": 81 + } + }, + { + "filename": "0014.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 81 + }, + "spriteSourceSize": { + "x": 27, + "y": 0, + "w": 33, + "h": 81 + }, + "frame": { + "x": 87, + "y": 1, + "w": 33, + "h": 81 + } + }, + { + "filename": "0015.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 81 + }, + "spriteSourceSize": { + "x": 27, + "y": 0, + "w": 33, + "h": 81 + }, + "frame": { + "x": 87, + "y": 1, + "w": 33, + "h": 81 + } + }, + { + "filename": "0016.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 81 + }, + "spriteSourceSize": { + "x": 26, + "y": 0, + "w": 33, + "h": 81 + }, + "frame": { + "x": 87, + "y": 1, + "w": 33, + "h": 81 + } + }, + { + "filename": "0017.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 81 + }, + "spriteSourceSize": { + "x": 26, + "y": 0, + "w": 33, + "h": 81 + }, + "frame": { + "x": 87, + "y": 1, + "w": 33, + "h": 81 + } + }, + { + "filename": "0000.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 81 + }, + "spriteSourceSize": { + "x": 20, + "y": 0, + "w": 31, + "h": 81 + }, + "frame": { + "x": 122, + "y": 1, + "w": 31, + "h": 81 + } + }, + { + "filename": "0001.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 81 + }, + "spriteSourceSize": { + "x": 20, + "y": 0, + "w": 31, + "h": 81 + }, + "frame": { + "x": 122, + "y": 1, + "w": 31, + "h": 81 + } + }, + { + "filename": "0002.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 81 + }, + "spriteSourceSize": { + "x": 20, + "y": 0, + "w": 31, + "h": 81 + }, + "frame": { + "x": 122, + "y": 1, + "w": 31, + "h": 81 + } + }, + { + "filename": "0003.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 81 + }, + "spriteSourceSize": { + "x": 20, + "y": 0, + "w": 31, + "h": 81 + }, + "frame": { + "x": 122, + "y": 1, + "w": 31, + "h": 81 + } + }, + { + "filename": "0004.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 81 + }, + "spriteSourceSize": { + "x": 21, + "y": 0, + "w": 31, + "h": 81 + }, + "frame": { + "x": 122, + "y": 1, + "w": 31, + "h": 81 + } + }, + { + "filename": "0005.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 81 + }, + "spriteSourceSize": { + "x": 21, + "y": 0, + "w": 31, + "h": 81 + }, + "frame": { + "x": 122, + "y": 1, + "w": 31, + "h": 81 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:dfcf7aedbd588c4e42427a2e17c171bf:206549943a0e3325d20a017ef01eefee:a233cd27590422717866c66e366b68fb$" + } +} diff --git a/public/images/trainer/cheryl.png b/public/images/trainer/cheryl.png new file mode 100644 index 00000000000..c46505f6b25 Binary files /dev/null and b/public/images/trainer/cheryl.png differ diff --git a/public/images/trainer/eri.json b/public/images/trainer/eri.json new file mode 100644 index 00000000000..fd4daf60437 --- /dev/null +++ b/public/images/trainer/eri.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "eri.png", + "format": "RGBA8888", + "size": { + "w": 74, + "h": 74 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 15, + "y": 5, + "w": 45, + "h": 74 + }, + "frame": { + "x": 0, + "y": 0, + "w": 45, + "h": 74 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:59594ac27e74ec85e2949d12ff680dc2:d65b6b00858ac47b26ef8393a8fa6795:d7f4cd3ff755f8074c14d3006b0c8301$" + } +} diff --git a/public/images/trainer/eri.png b/public/images/trainer/eri.png new file mode 100644 index 00000000000..0c9bdf7b47b Binary files /dev/null and b/public/images/trainer/eri.png differ diff --git a/public/images/trainer/expert_pokemon_breeder.json b/public/images/trainer/expert_pokemon_breeder.json new file mode 100644 index 00000000000..cd6ecffb267 --- /dev/null +++ b/public/images/trainer/expert_pokemon_breeder.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "expert_pokemon_breeder.png", + "format": "RGBA8888", + "size": { + "w": 39, + "h": 75 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 21, + "y": 3, + "w": 39, + "h": 75 + }, + "frame": { + "x": 0, + "y": 0, + "w": 39, + "h": 75 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:cb681265d8dca038a518ab14076fd140:18ff41b1ef6967682643a11695926e58:c59ea3971195f5a395b75223a77d9068$" + } +} diff --git a/public/images/trainer/expert_pokemon_breeder.png b/public/images/trainer/expert_pokemon_breeder.png new file mode 100644 index 00000000000..0625f5255c3 Binary files /dev/null and b/public/images/trainer/expert_pokemon_breeder.png differ diff --git a/public/images/trainer/giacomo.json b/public/images/trainer/giacomo.json new file mode 100644 index 00000000000..5eeb2cd685b --- /dev/null +++ b/public/images/trainer/giacomo.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "giacomo.png", + "format": "RGBA8888", + "size": { + "w": 75, + "h": 75 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 23, + "y": 4, + "w": 37, + "h": 75 + }, + "frame": { + "x": 0, + "y": 0, + "w": 37, + "h": 75 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:8c4e7da48e5667abc6d364330268c092:0fa43e58d8a746d3b86cb2dd763719f4:8603cc19e888c8c8de62177f4011577c$" + } +} diff --git a/public/images/trainer/giacomo.png b/public/images/trainer/giacomo.png new file mode 100644 index 00000000000..275f47fad3c Binary files /dev/null and b/public/images/trainer/giacomo.png differ diff --git a/public/images/trainer/marley.json b/public/images/trainer/marley.json new file mode 100644 index 00000000000..92d9f1449e5 --- /dev/null +++ b/public/images/trainer/marley.json @@ -0,0 +1,83 @@ +{ "frames": [ + { + "filename": "0000.png", + "frame": { "x": 0, "y": 0, "w": 31, "h": 77 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 26, "y": 2, "w": 31, "h": 77 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0001.png", + "frame": { "x": 0, "y": 0, "w": 31, "h": 77 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 26, "y": 2, "w": 31, "h": 77 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0002.png", + "frame": { "x": 0, "y": 0, "w": 31, "h": 77 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 26, "y": 2, "w": 31, "h": 77 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0003.png", + "frame": { "x": 0, "y": 0, "w": 31, "h": 77 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 26, "y": 2, "w": 31, "h": 77 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0004.png", + "frame": { "x": 32, "y": 0, "w": 28, "h": 78 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 28, "y": 1, "w": 28, "h": 78 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0005.png", + "frame": { "x": 32, "y": 0, "w": 28, "h": 78 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 28, "y": 1, "w": 28, "h": 78 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0006.png", + "frame": { "x": 0, "y": 78, "w": 31, "h": 77 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 28, "y": 2, "w": 31, "h": 77 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0007.png", + "frame": { "x": 0, "y": 78, "w": 31, "h": 77 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 28, "y": 2, "w": 31, "h": 77 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + } + ], + "meta": { + "app": "https://www.pngprite.org/", + "version": "1.3.7-x64", + "image": "marley.png", + "format": "I8", + "size": { "w": 60, "h": 155 }, + "scale": "1" + } +} diff --git a/public/images/trainer/marley.png b/public/images/trainer/marley.png new file mode 100644 index 00000000000..8e78e11e8ad Binary files /dev/null and b/public/images/trainer/marley.png differ diff --git a/public/images/trainer/mela.json b/public/images/trainer/mela.json new file mode 100644 index 00000000000..c9db18acc5a --- /dev/null +++ b/public/images/trainer/mela.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "mela.png", + "format": "RGBA8888", + "size": { + "w": 78, + "h": 78 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 18, + "y": 1, + "w": 46, + "h": 78 + }, + "frame": { + "x": 0, + "y": 0, + "w": 46, + "h": 78 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:e26d8c926c54c848cef673b3f59f35e7:ff040c2cebb1a92d2ef61dc91c018390:68668cf06383ff459cccaafb6bf56215$" + } +} diff --git a/public/images/trainer/mela.png b/public/images/trainer/mela.png new file mode 100644 index 00000000000..fbb08ed69cf Binary files /dev/null and b/public/images/trainer/mela.png differ diff --git a/public/images/trainer/mira.json b/public/images/trainer/mira.json new file mode 100644 index 00000000000..7bd29f53475 --- /dev/null +++ b/public/images/trainer/mira.json @@ -0,0 +1,209 @@ +{ "frames": [ + { + "filename": "0000.png", + "frame": { "x": 53, "y": 0, "w": 44, "h": 65 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 23, "y": 14, "w": 44, "h": 65 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0001.png", + "frame": { "x": 53, "y": 0, "w": 44, "h": 65 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 23, "y": 14, "w": 44, "h": 65 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0002.png", + "frame": { "x": 53, "y": 0, "w": 44, "h": 65 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 22, "y": 13, "w": 44, "h": 65 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0003.png", + "frame": { "x": 53, "y": 0, "w": 44, "h": 65 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 21, "y": 11, "w": 44, "h": 65 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0004.png", + "frame": { "x": 0, "y": 0, "w": 53, "h": 63 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 21, "y": 11, "w": 53, "h": 63 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0005.png", + "frame": { "x": 0, "y": 0, "w": 53, "h": 63 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 21, "y": 12, "w": 53, "h": 63 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0006.png", + "frame": { "x": 0, "y": 63, "w": 44, "h": 65 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 13, "y": 11, "w": 44, "h": 65 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0007.png", + "frame": { "x": 0, "y": 63, "w": 44, "h": 65 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 12, "y": 13, "w": 44, "h": 65 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0008.png", + "frame": { "x": 0, "y": 63, "w": 44, "h": 65 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 11, "y": 14, "w": 44, "h": 65 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0009.png", + "frame": { "x": 0, "y": 63, "w": 44, "h": 65 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 12, "y": 13, "w": 44, "h": 65 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0010.png", + "frame": { "x": 0, "y": 63, "w": 44, "h": 65 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 13, "y": 11, "w": 44, "h": 65 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0011.png", + "frame": { "x": 0, "y": 0, "w": 53, "h": 63 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 21, "y": 11, "w": 53, "h": 63 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0012.png", + "frame": { "x": 0, "y": 0, "w": 53, "h": 63 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 21, "y": 12, "w": 53, "h": 63 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0013.png", + "frame": { "x": 53, "y": 0, "w": 44, "h": 65 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 21, "y": 11, "w": 44, "h": 65 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0014.png", + "frame": { "x": 53, "y": 0, "w": 44, "h": 65 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 22, "y": 13, "w": 44, "h": 65 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0015.png", + "frame": { "x": 53, "y": 0, "w": 44, "h": 65 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 23, "y": 14, "w": 44, "h": 65 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0016.png", + "frame": { "x": 53, "y": 0, "w": 44, "h": 65 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 22, "y": 13, "w": 44, "h": 65 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0017.png", + "frame": { "x": 53, "y": 0, "w": 44, "h": 65 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 21, "y": 11, "w": 44, "h": 65 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0018.png", + "frame": { "x": 0, "y": 0, "w": 53, "h": 63 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 21, "y": 11, "w": 53, "h": 63 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0019.png", + "frame": { "x": 0, "y": 0, "w": 53, "h": 63 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 21, "y": 12, "w": 53, "h": 63 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0020.png", + "frame": { "x": 0, "y": 63, "w": 44, "h": 65 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 13, "y": 11, "w": 44, "h": 65 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0021.png", + "frame": { "x": 0, "y": 63, "w": 44, "h": 65 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 12, "y": 13, "w": 44, "h": 65 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + } + ], + "meta": { + "app": "https://www.aseprite.org/", + "version": "1.3.7-x64", + "image": "mira.png", + "format": "I8", + "size": { "w": 97, "h": 128 }, + "scale": "1" + } +} diff --git a/public/images/trainer/mira.png b/public/images/trainer/mira.png new file mode 100644 index 00000000000..5c1afe5d241 Binary files /dev/null and b/public/images/trainer/mira.png differ diff --git a/public/images/trainer/ortega.json b/public/images/trainer/ortega.json new file mode 100644 index 00000000000..53bab5dba40 --- /dev/null +++ b/public/images/trainer/ortega.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "ortega.png", + "format": "RGBA8888", + "size": { + "w": 69, + "h": 69 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 8, + "y": 10, + "w": 53, + "h": 69 + }, + "frame": { + "x": 0, + "y": 0, + "w": 53, + "h": 69 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:c6ff92d90ed884222095de81d1db9166:a91cf3c83a063f549c52afb42f7ba3b0:c3f9fcec121c8bc93f2b230b20b79c57$" + } +} diff --git a/public/images/trainer/ortega.png b/public/images/trainer/ortega.png new file mode 100644 index 00000000000..7f694c6ded6 Binary files /dev/null and b/public/images/trainer/ortega.png differ diff --git a/public/images/trainer/penny.json b/public/images/trainer/penny.json new file mode 100644 index 00000000000..da64efffa3b --- /dev/null +++ b/public/images/trainer/penny.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "penny.png", + "format": "RGBA8888", + "size": { + "w": 75, + "h": 75 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 24, + "y": 4, + "w": 34, + "h": 75 + }, + "frame": { + "x": 0, + "y": 0, + "w": 34, + "h": 75 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:54f184bf1995a94a78aff33c9a851e6b:a6c9b3fe428b0cd0344b5cf14b999f36:cf221da9747cb8cb356053d3042d8d22$" + } +} diff --git a/public/images/trainer/penny.png b/public/images/trainer/penny.png new file mode 100644 index 00000000000..0e36760e21b Binary files /dev/null and b/public/images/trainer/penny.png differ diff --git a/public/images/trainer/riley.json b/public/images/trainer/riley.json new file mode 100644 index 00000000000..f0f84a909db --- /dev/null +++ b/public/images/trainer/riley.json @@ -0,0 +1,209 @@ +{ "frames": [ + { + "filename": "0000.png", + "frame": { "x": 0, "y": 0, "w": 55, "h": 80 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 11, "y": 0, "w": 55, "h": 80 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0001.png", + "frame": { "x": 0, "y": 0, "w": 55, "h": 80 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 11, "y": 0, "w": 55, "h": 80 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0002.png", + "frame": { "x": 0, "y": 0, "w": 55, "h": 80 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 11, "y": 0, "w": 55, "h": 80 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0003.png", + "frame": { "x": 0, "y": 0, "w": 55, "h": 80 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 11, "y": 0, "w": 55, "h": 80 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0004.png", + "frame": { "x": 55, "y": 80, "w": 37, "h": 80 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 31, "y": 0, "w": 37, "h": 80 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0005.png", + "frame": { "x": 55, "y": 80, "w": 37, "h": 80 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 31, "y": 0, "w": 37, "h": 80 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0006.png", + "frame": { "x": 55, "y": 80, "w": 37, "h": 80 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 30, "y": 0, "w": 37, "h": 80 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0007.png", + "frame": { "x": 55, "y": 80, "w": 37, "h": 80 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 30, "y": 0, "w": 37, "h": 80 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0008.png", + "frame": { "x": 55, "y": 80, "w": 37, "h": 80 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 28, "y": 0, "w": 37, "h": 80 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0009.png", + "frame": { "x": 55, "y": 80, "w": 37, "h": 80 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 28, "y": 0, "w": 37, "h": 80 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0010.png", + "frame": { "x": 0, "y": 0, "w": 55, "h": 80 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 10, "y": 0, "w": 55, "h": 80 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0011.png", + "frame": { "x": 0, "y": 0, "w": 55, "h": 80 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 10, "y": 0, "w": 55, "h": 80 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0012.png", + "frame": { "x": 0, "y": 0, "w": 55, "h": 80 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 11, "y": 0, "w": 55, "h": 80 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0013.png", + "frame": { "x": 0, "y": 0, "w": 55, "h": 80 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 11, "y": 0, "w": 55, "h": 80 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0014.png", + "frame": { "x": 55, "y": 0, "w": 55, "h": 80 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 12, "y": 0, "w": 55, "h": 80 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0015.png", + "frame": { "x": 55, "y": 0, "w": 55, "h": 80 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 12, "y": 0, "w": 55, "h": 80 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0016.png", + "frame": { "x": 0, "y": 80, "w": 55, "h": 80 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 12, "y": 0, "w": 55, "h": 80 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0017.png", + "frame": { "x": 0, "y": 80, "w": 55, "h": 80 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 12, "y": 0, "w": 55, "h": 80 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0018.png", + "frame": { "x": 55, "y": 0, "w": 55, "h": 80 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 12, "y": 0, "w": 55, "h": 80 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0019.png", + "frame": { "x": 55, "y": 0, "w": 55, "h": 80 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 12, "y": 0, "w": 55, "h": 80 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0020.png", + "frame": { "x": 0, "y": 0, "w": 55, "h": 80 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 12, "y": 0, "w": 55, "h": 80 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0021.png", + "frame": { "x": 0, "y": 0, "w": 55, "h": 80 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 12, "y": 0, "w": 55, "h": 80 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + } + ], + "meta": { + "app": "https://www.aseprite.org/", + "version": "1.3.7-x64", + "image": "riley.png", + "format": "I8", + "size": { "w": 110, "h": 160 }, + "scale": "1" + } +} diff --git a/public/images/trainer/riley.png b/public/images/trainer/riley.png new file mode 100644 index 00000000000..a9f0e3b53a9 Binary files /dev/null and b/public/images/trainer/riley.png differ diff --git a/public/images/trainer/star_grunt_f.json b/public/images/trainer/star_grunt_f.json new file mode 100644 index 00000000000..e26477e3512 --- /dev/null +++ b/public/images/trainer/star_grunt_f.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "star_grunt_f.png", + "format": "RGBA8888", + "size": { + "w": 68, + "h": 68 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 24, + "y": 11, + "w": 30, + "h": 68 + }, + "frame": { + "x": 0, + "y": 0, + "w": 30, + "h": 68 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:b542a1bdd6995584fc776f75d578b434:f03fddece4494ab59698002fe6671972:c6f0e54e24ec5ffaa711700431b1955e$" + } +} diff --git a/public/images/trainer/star_grunt_f.png b/public/images/trainer/star_grunt_f.png new file mode 100644 index 00000000000..6eb63ae1e03 Binary files /dev/null and b/public/images/trainer/star_grunt_f.png differ diff --git a/public/images/trainer/star_grunt_m.json b/public/images/trainer/star_grunt_m.json new file mode 100644 index 00000000000..bf49e3027e6 --- /dev/null +++ b/public/images/trainer/star_grunt_m.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "star_grunt_m.png", + "format": "RGBA8888", + "size": { + "w": 70, + "h": 70 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 24, + "y": 9, + "w": 31, + "h": 70 + }, + "frame": { + "x": 0, + "y": 0, + "w": 31, + "h": 70 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:abc4b0424c37fd55a2bf2e9f5142adce:41a140aa68a1eda61d9a00cab4e07721:a0796711f9e0333796b6629cd43ff8e8$" + } +} diff --git a/public/images/trainer/star_grunt_m.png b/public/images/trainer/star_grunt_m.png new file mode 100644 index 00000000000..a69359eda8e Binary files /dev/null and b/public/images/trainer/star_grunt_m.png differ diff --git a/public/images/trainer/vicky.json b/public/images/trainer/vicky.json new file mode 100644 index 00000000000..c19cf11622d --- /dev/null +++ b/public/images/trainer/vicky.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "vicky.png", + "format": "RGBA8888", + "size": { + "w": 52, + "h": 53 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 27, + "w": 52, + "h": 53 + }, + "frame": { + "x": 0, + "y": 0, + "w": 52, + "h": 53 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:bf9d2d417a1982282dd711456ac71206:101e07828e3d6e2a2a7a80aebfa802ad:cabe44a4410c334298b1984a219f8160$" + } +} diff --git a/public/images/trainer/vicky.png b/public/images/trainer/vicky.png new file mode 100644 index 00000000000..3e2d6c13696 Binary files /dev/null and b/public/images/trainer/vicky.png differ diff --git a/public/images/trainer/victor.json b/public/images/trainer/victor.json new file mode 100644 index 00000000000..5afa9704567 --- /dev/null +++ b/public/images/trainer/victor.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "victor.png", + "format": "RGBA8888", + "size": { + "w": 55, + "h": 53 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 12, + "y": 27, + "w": 55, + "h": 53 + }, + "frame": { + "x": 0, + "y": 0, + "w": 55, + "h": 53 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:64eff0f697754cdf9552b46342c9292a:611e0e2cacbd90c1229ce5443b2414f0:0cc0f5a2c1b2eedb46dd8318e8feb1d8$" + } +} diff --git a/public/images/trainer/victor.png b/public/images/trainer/victor.png new file mode 100644 index 00000000000..3ffddea24bb Binary files /dev/null and b/public/images/trainer/victor.png differ diff --git a/public/images/trainer/victoria.json b/public/images/trainer/victoria.json new file mode 100644 index 00000000000..7917113621a --- /dev/null +++ b/public/images/trainer/victoria.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "victoria.png", + "format": "RGBA8888", + "size": { + "w": 52, + "h": 54 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 14, + "y": 26, + "w": 52, + "h": 54 + }, + "frame": { + "x": 0, + "y": 0, + "w": 52, + "h": 54 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:4dafeae3674d63b12cc4d8044f67b5a3:7834687d784c31169256927f419c7958:cf0eb39e0a3f2e42f23ca29747d73c40$" + } +} diff --git a/public/images/trainer/victoria.png b/public/images/trainer/victoria.png new file mode 100644 index 00000000000..e2874f266ad Binary files /dev/null and b/public/images/trainer/victoria.png differ diff --git a/public/images/trainer/vito.json b/public/images/trainer/vito.json new file mode 100644 index 00000000000..61dcf7af0ef --- /dev/null +++ b/public/images/trainer/vito.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "vito.png", + "format": "RGBA8888", + "size": { + "w": 41, + "h": 78 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 20, + "y": 2, + "w": 41, + "h": 78 + }, + "frame": { + "x": 0, + "y": 0, + "w": 41, + "h": 78 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:cb988be58fcd5381174e9d120b051e38:4d4723dbbcd9713ee0ed3c2d84ef4bfb:1c7723b536b218346e3138016d865ce9$" + } +} diff --git a/public/images/trainer/vito.png b/public/images/trainer/vito.png new file mode 100644 index 00000000000..a7c6c0444f4 Binary files /dev/null and b/public/images/trainer/vito.png differ diff --git a/public/images/trainer/vivi.json b/public/images/trainer/vivi.json new file mode 100644 index 00000000000..b36ebcd7c0c --- /dev/null +++ b/public/images/trainer/vivi.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "vivi.png", + "format": "RGBA8888", + "size": { + "w": 48, + "h": 69 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 11, + "w": 48, + "h": 69 + }, + "frame": { + "x": 0, + "y": 0, + "w": 48, + "h": 69 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:0a51b4df0b2ed0fed7e3bdb5dffd9e28:af1f3b1480023b3e3761c49e49faf5f1:4fc6bf2bec74c4bb8809df38231deb01$" + } +} diff --git a/public/images/trainer/vivi.png b/public/images/trainer/vivi.png new file mode 100644 index 00000000000..cd97e676cfb Binary files /dev/null and b/public/images/trainer/vivi.png differ diff --git a/src/battle-scene.ts b/src/battle-scene.ts index c9c8d6b788a..f55f1658648 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -1,42 +1,41 @@ import Phaser from "phaser"; import UI from "./ui/ui"; -import Pokemon, { PlayerPokemon, EnemyPokemon } from "./field/pokemon"; -import PokemonSpecies, { PokemonSpeciesFilter, allSpecies, getPokemonSpecies } from "./data/pokemon-species"; -import { Constructor } from "#app/utils"; +import Pokemon, { EnemyPokemon, PlayerPokemon } from "./field/pokemon"; +import PokemonSpecies, { allSpecies, getPokemonSpecies, PokemonSpeciesFilter } from "./data/pokemon-species"; +import { Constructor, isNullOrUndefined } from "#app/utils"; import * as Utils from "./utils"; -import { Modifier, ModifierBar, ConsumablePokemonModifier, ConsumableModifier, PokemonHpRestoreModifier, TurnHeldItemTransferModifier, HealingBoosterModifier, PersistentModifier, PokemonHeldItemModifier, ModifierPredicate, DoubleBattleChanceBoosterModifier, FusePokemonModifier, PokemonFormChangeItemModifier, TerastallizeModifier, overrideModifiers, overrideHeldItems } from "./modifier/modifier"; +import { ConsumableModifier, ConsumablePokemonModifier, DoubleBattleChanceBoosterModifier, ExpBalanceModifier, ExpShareModifier, FusePokemonModifier, HealingBoosterModifier, Modifier, ModifierBar, ModifierPredicate, MultipleParticipantExpBonusModifier, overrideHeldItems, overrideModifiers, PersistentModifier, PokemonExpBoosterModifier, PokemonFormChangeItemModifier, PokemonHeldItemModifier, PokemonHpRestoreModifier, PokemonIncrementingStatModifier, TerastallizeModifier, TurnHeldItemTransferModifier } from "./modifier/modifier"; import { PokeballType } from "./data/pokeball"; import { initCommonAnims, initMoveAnim, loadCommonAnimAssets, loadMoveAnimAssets, populateAnims } from "./data/battle-anims"; import { Phase } from "./phase"; import { initGameSpeed } from "./system/game-speed"; import { Arena, ArenaBase } from "./field/arena"; import { GameData } from "./system/game-data"; -import { TextStyle, addTextObject, getTextColor } from "./ui/text"; +import { addTextObject, getTextColor, TextStyle } from "./ui/text"; import { allMoves } from "./data/move"; -import { ModifierPoolType, getDefaultModifierTypeForTier, getEnemyModifierTypesForWave, getLuckString, getLuckTextTint, getModifierPoolForType, getModifierType, getPartyLuckValue, modifierTypes } from "./modifier/modifier-type"; +import { getDefaultModifierTypeForTier, getEnemyModifierTypesForWave, getLuckString, getLuckTextTint, getModifierPoolForType, getModifierType, getPartyLuckValue, ModifierPoolType, modifierTypes, PokemonHeldItemModifierType } from "./modifier/modifier-type"; import AbilityBar from "./ui/ability-bar"; -import { BlockItemTheftAbAttr, DoubleBattleChanceAbAttr, ChangeMovePriorityAbAttr, PostBattleInitAbAttr, applyAbAttrs, applyPostBattleInitAbAttrs } from "./data/ability"; -import { allAbilities } from "./data/ability"; +import { allAbilities, applyAbAttrs, applyPostBattleInitAbAttrs, BlockItemTheftAbAttr, ChangeMovePriorityAbAttr, DoubleBattleChanceAbAttr, PostBattleInitAbAttr } from "./data/ability"; import Battle, { BattleType, FixedBattleConfig } from "./battle"; import { GameMode, GameModes, getGameMode } from "./game-mode"; import FieldSpritePipeline from "./pipelines/field-sprite"; import SpritePipeline from "./pipelines/sprite"; import PartyExpBar from "./ui/party-exp-bar"; -import { TrainerSlot, trainerConfigs } from "./data/trainer-config"; +import { trainerConfigs, TrainerSlot } from "./data/trainer-config"; import Trainer, { TrainerVariant } from "./field/trainer"; import TrainerData from "./system/trainer-data"; import SoundFade from "phaser3-rex-plugins/plugins/soundfade"; import { pokemonPrevolutions } from "./data/pokemon-evolutions"; import PokeballTray from "./ui/pokeball-tray"; import InvertPostFX from "./pipelines/invert"; -import { Achv, ModifierAchv, MoneyAchv, achvs } from "./system/achv"; +import { Achv, achvs, ModifierAchv, MoneyAchv } from "./system/achv"; import { Voucher, vouchers } from "./system/voucher"; import { Gender } from "./data/gender"; import UIPlugin from "phaser3-rex-plugins/templates/ui/ui-plugin"; import { addUiThemeOverrides } from "./ui/ui-theme"; import PokemonData from "./system/pokemon-data"; import { Nature } from "./data/nature"; -import { SpeciesFormChangeManualTrigger, SpeciesFormChangeTimeOfDayTrigger, SpeciesFormChangeTrigger, pokemonFormChanges, FormChangeItem, SpeciesFormChange } from "./data/pokemon-forms"; +import { FormChangeItem, pokemonFormChanges, SpeciesFormChange, SpeciesFormChangeManualTrigger, SpeciesFormChangeTimeOfDayTrigger, SpeciesFormChangeTrigger } from "./data/pokemon-forms"; import { FormChangePhase } from "./phases/form-change-phase"; import { getTypeRgb } from "./data/type"; import PokemonSpriteSparkleHandler from "./field/pokemon-sprite-sparkle-handler"; @@ -64,6 +63,7 @@ import { PlayerGender } from "#enums/player-gender"; import { Species } from "#enums/species"; import { UiTheme } from "#enums/ui-theme"; import { TimedEventManager } from "#app/timed-event-manager"; +import { PokemonAnimType } from "#enums/pokemon-anim-type"; import i18next from "i18next"; import { TrainerType } from "#enums/trainer-type"; import { battleSpecDialogue } from "./data/dialogue"; @@ -74,6 +74,7 @@ import { MessagePhase } from "./phases/message-phase"; import { MovePhase } from "./phases/move-phase"; import { NewBiomeEncounterPhase } from "./phases/new-biome-encounter-phase"; import { NextEncounterPhase } from "./phases/next-encounter-phase"; +import { PokemonAnimPhase } from "./phases/pokemon-anim-phase"; import { QuietFormChangePhase } from "./phases/quiet-form-change-phase"; import { ReturnPhase } from "./phases/return-phase"; import { SelectBiomePhase } from "./phases/select-biome-phase"; @@ -84,6 +85,16 @@ import { TitlePhase } from "./phases/title-phase"; import { ToggleDoublePositionPhase } from "./phases/toggle-double-position-phase"; import { TurnInitPhase } from "./phases/turn-init-phase"; import { ShopCursorTarget } from "./enums/shop-cursor-target"; +import MysteryEncounter from "./data/mystery-encounters/mystery-encounter"; +import { allMysteryEncounters, ANTI_VARIANCE_WEIGHT_MODIFIER, AVERAGE_ENCOUNTERS_PER_RUN_TARGET, BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT, MYSTERY_ENCOUNTER_SPAWN_MAX_WEIGHT, mysteryEncountersByBiome, WEIGHT_INCREMENT_ON_SPAWN_MISS } from "./data/mystery-encounters/mystery-encounters"; +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"; +import HeldModifierConfig from "#app/interfaces/held-modifier-config"; +import { ExpPhase } from "#app/phases/exp-phase"; +import { ShowPartyExpBarPhase } from "#app/phases/show-party-exp-bar-phase"; +import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; +import { ExpGainsSpeed } from "./enums/exp-gains-speed"; export const bypassLogin = import.meta.env.VITE_BYPASS_LOGIN === "1"; @@ -159,7 +170,7 @@ export default class BattleScene extends SceneBase { public experimentalSprites: boolean = false; public musicPreference: integer = 0; public moveAnimations: boolean = true; - public expGainsSpeed: integer = 0; + public expGainsSpeed: ExpGainsSpeed = ExpGainsSpeed.DEFAULT; public skipSeenDialogues: boolean = false; /** * Determines if the egg hatching animation should be skipped @@ -244,6 +255,10 @@ export default class BattleScene extends SceneBase { public money: integer; public pokemonInfoContainer: PokemonInfoContainer; private party: PlayerPokemon[]; + /** Session save data that pertains to Mystery Encounters */ + public mysteryEncounterSaveData: MysteryEncounterSaveData = new MysteryEncounterSaveData(); + /** If the previous wave was a MysteryEncounter, tracks the object with this variable. Mostly used for visual object cleanup */ + public lastMysteryEncounter?: MysteryEncounter; /** Combined Biome and Wave count text */ private biomeWaveText: Phaser.GameObjects.Text; private moneyText: Phaser.GameObjects.Text; @@ -756,6 +771,14 @@ export default class BattleScene extends SceneBase { return this.getPlayerField().find(p => p.isActive()); } + /** + * Finds the first {@linkcode Pokemon.isActive() | active PlayerPokemon} that isn't also currently switching out + * @returns Either the first {@linkcode PlayerPokemon} satisfying, or undefined if no player pokemon on the field satisfy + */ + getNonSwitchedPlayerPokemon(): PlayerPokemon | undefined { + return this.getPlayerField().find(p => p.isActive() && p.switchOutStatus === false); + } + /** * Returns an array of PlayerPokemon of length 1 or 2 depending on if double battles or not * @returns array of {@linkcode PlayerPokemon} @@ -773,6 +796,14 @@ export default class BattleScene extends SceneBase { return this.getEnemyField().find(p => p.isActive()); } + /** + * Finds the first {@linkcode Pokemon.isActive() | active EnemyPokemon} pokemon from the enemy that isn't also currently switching out + * @returns Either the first {@linkcode EnemyPokemon} satisfying, or undefined if no player pokemon on the field satisfy + */ + getNonSwitchedEnemyPokemon(): EnemyPokemon | undefined { + return this.getEnemyField().find(p => p.isActive() && p.switchOutStatus === false); + } + /** * Returns an array of EnemyPokemon of length 1 or 2 depending on if double battles or not * @returns array of {@linkcode EnemyPokemon} @@ -882,6 +913,26 @@ export default class BattleScene extends SceneBase { return pokemon; } + /** + * Removes a {@linkcode PlayerPokemon} from the party, and clears modifiers for that Pokemon's id + * Useful for MEs/Challenges that remove Pokemon from the player party temporarily or permanently + * @param pokemon + * @param destroy Default true. If true, will destroy the {@linkcode PlayerPokemon} after removing + */ + removePokemonFromPlayerParty(pokemon: PlayerPokemon, destroy: boolean = true) { + if (!pokemon) { + return; + } + + const partyIndex = this.party.indexOf(pokemon); + this.party.splice(partyIndex, 1); + if (destroy) { + this.field.remove(pokemon, true); + pokemon.destroy(); + } + this.updateModifiers(true); + } + addPokemonIcon(pokemon: Pokemon, x: number, y: number, originX: number = 0.5, originY: number = 0.5, ignoreOverride: boolean = false): Phaser.GameObjects.Container { const container = this.add.container(x, y); container.setName(`${pokemon.name}-icon`); @@ -1019,6 +1070,11 @@ export default class BattleScene extends SceneBase { p.destroy(); } + // If this is a ME, clear any residual visual sprites before reloading + if (this.currentBattle?.mysteryEncounter?.introVisuals) { + this.field.remove(this.currentBattle.mysteryEncounter?.introVisuals, true); + } + //@ts-ignore - allowing `null` for currentBattle causes a lot of trouble this.currentBattle = null; // TODO: resolve ts-ignore @@ -1049,6 +1105,8 @@ export default class BattleScene extends SceneBase { this.trainer.setPosition(406, 186); this.trainer.setVisible(true); + this.mysteryEncounterSaveData = new MysteryEncounterSaveData(); + this.updateGameInfo(); if (reloadI18n) { @@ -1084,7 +1142,14 @@ export default class BattleScene extends SceneBase { } } - newBattle(waveIndex?: integer, battleType?: BattleType, trainerData?: TrainerData, double?: boolean): Battle | null { + getDoubleBattleChance(newWaveIndex: number, playerField: PlayerPokemon[]) { + const doubleChance = new Utils.IntegerHolder(newWaveIndex % 10 === 0 ? 32 : 8); + this.applyModifiers(DoubleBattleChanceBoosterModifier, true, doubleChance); + playerField.forEach(p => applyAbAttrs(DoubleBattleChanceAbAttr, p, null, false, doubleChance)); + return Math.max(doubleChance.value, 1); + } + + newBattle(waveIndex?: integer, battleType?: BattleType, trainerData?: TrainerData, double?: boolean, mysteryEncounterType?: MysteryEncounterType): Battle | null { const _startingWave = Overrides.STARTING_WAVE_OVERRIDE || startingWave; const newWaveIndex = waveIndex || ((this.currentBattle?.waveIndex || (_startingWave - 1)) + 1); let newDouble: boolean | undefined; @@ -1133,14 +1198,41 @@ export default class BattleScene extends SceneBase { newTrainer = trainerData !== undefined ? trainerData.toTrainer(this) : new Trainer(this, trainerType, variant); this.field.add(newTrainer); } + + // Check for mystery encounter + // Can only occur in place of a standard (non-boss) wild battle, waves 10-180 + const [lowestMysteryEncounterWave, highestMysteryEncounterWave] = this.gameMode.getMysteryEncounterLegalWaves(); + if (this.gameMode.hasMysteryEncounters && newBattleType === BattleType.WILD && !this.gameMode.isBoss(newWaveIndex) && newWaveIndex < highestMysteryEncounterWave && newWaveIndex > lowestMysteryEncounterWave) { + const roll = Utils.randSeedInt(MYSTERY_ENCOUNTER_SPAWN_MAX_WEIGHT); + + // Base spawn weight is BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT/256, and increases by WEIGHT_INCREMENT_ON_SPAWN_MISS/256 for each missed attempt at spawning an encounter on a valid floor + const sessionEncounterRate = this.mysteryEncounterSaveData.encounterSpawnChance; + const encounteredEvents = this.mysteryEncounterSaveData.encounteredEvents; + + // If total number of encounters is lower than expected for the run, slightly favor a new encounter spawn (reverse as well) + // Reduces occurrence of runs with total encounters significantly different from AVERAGE_ENCOUNTERS_PER_RUN_TARGET + const expectedEncountersByFloor = AVERAGE_ENCOUNTERS_PER_RUN_TARGET / (highestMysteryEncounterWave - lowestMysteryEncounterWave) * (newWaveIndex - lowestMysteryEncounterWave); + const currentRunDiffFromAvg = expectedEncountersByFloor - encounteredEvents.length; + const favoredEncounterRate = sessionEncounterRate + currentRunDiffFromAvg * ANTI_VARIANCE_WEIGHT_MODIFIER; + + const successRate = isNullOrUndefined(Overrides.MYSTERY_ENCOUNTER_RATE_OVERRIDE) ? favoredEncounterRate : Overrides.MYSTERY_ENCOUNTER_RATE_OVERRIDE!; + + // If the most recent ME was 3 or fewer waves ago, can never spawn a ME + const canSpawn = encounteredEvents.length === 0 || (newWaveIndex - encounteredEvents[encounteredEvents.length - 1].waveIndex) > 3 || !isNullOrUndefined(Overrides.MYSTERY_ENCOUNTER_RATE_OVERRIDE); + + if (canSpawn && roll < successRate) { + newBattleType = BattleType.MYSTERY_ENCOUNTER; + // Reset base spawn weight + this.mysteryEncounterSaveData.encounterSpawnChance = BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT; + } else { + this.mysteryEncounterSaveData.encounterSpawnChance = sessionEncounterRate + WEIGHT_INCREMENT_ON_SPAWN_MISS; + } + } } if (double === undefined && newWaveIndex > 1) { if (newBattleType === BattleType.WILD && !this.gameMode.isWaveFinal(newWaveIndex)) { - const doubleChance = new Utils.IntegerHolder(newWaveIndex % 10 === 0 ? 32 : 8); - this.applyModifiers(DoubleBattleChanceBoosterModifier, true, doubleChance); - playerField.forEach(p => applyAbAttrs(DoubleBattleChanceAbAttr, p, null, false, doubleChance)); - newDouble = !Utils.randSeedInt(doubleChance.value); + newDouble = !Utils.randSeedInt(this.getDoubleBattleChance(newWaveIndex, playerField)); } else if (newBattleType === BattleType.TRAINER) { newDouble = newTrainer?.variant === TrainerVariant.DOUBLE; } @@ -1165,12 +1257,21 @@ export default class BattleScene extends SceneBase { const maxExpLevel = this.getMaxExpLevel(); this.lastEnemyTrainer = lastBattle?.trainer ?? null; + this.lastMysteryEncounter = lastBattle?.mysteryEncounter; this.executeWithSeedOffset(() => { this.currentBattle = new Battle(this.gameMode, newWaveIndex, newBattleType, newTrainer, newDouble); }, newWaveIndex << 3, this.waveSeed); this.currentBattle.incrementTurn(this); + if (newBattleType === BattleType.MYSTERY_ENCOUNTER) { + // Disable double battle on mystery encounters (it may be re-enabled as part of encounter) + this.currentBattle.double = false; + this.executeWithSeedOffset(() => { + this.currentBattle.mysteryEncounter = this.getMysteryEncounter(mysteryEncounterType); + }, this.currentBattle.waveIndex << 4); + } + //this.pushPhase(new TrainerMessageTestPhase(this, TrainerType.RIVAL, TrainerType.RIVAL_2, TrainerType.RIVAL_3, TrainerType.RIVAL_4, TrainerType.RIVAL_5, TrainerType.RIVAL_6)); if (!waveIndex && lastBattle) { @@ -1179,7 +1280,7 @@ export default class BattleScene extends SceneBase { const isEndlessFifthWave = this.gameMode.hasShortBiomes && (lastBattle.waveIndex % 5) === 0; const isWaveIndexMultipleOfFiftyMinusOne = (lastBattle.waveIndex % 50) === 49; const isNewBiome = isWaveIndexMultipleOfTen || isEndlessFifthWave || (isEndlessOrDaily && isWaveIndexMultipleOfFiftyMinusOne); - const resetArenaState = isNewBiome || this.currentBattle.battleType === BattleType.TRAINER || this.currentBattle.battleSpec === BattleSpec.FINAL_BOSS; + const resetArenaState = isNewBiome || [BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER].includes(this.currentBattle.battleType) || this.currentBattle.battleSpec === BattleSpec.FINAL_BOSS; this.getEnemyParty().forEach(enemyPokemon => enemyPokemon.destroy()); this.trySpreadPokerus(); if (!isNewBiome && (newWaveIndex % 10) === 5) { @@ -1187,14 +1288,21 @@ export default class BattleScene extends SceneBase { } if (resetArenaState) { this.arena.resetArenaEffects(); - playerField.forEach((_, p) => this.pushPhase(new ReturnPhase(this, p))); + + playerField.forEach((pokemon, p) => { + if (pokemon.isOnField()) { + this.pushPhase(new ReturnPhase(this, p)); + } + }); for (const pokemon of this.getParty()) { pokemon.resetBattleData(); applyPostBattleInitAbAttrs(PostBattleInitAbAttr, pokemon); } - this.pushPhase(new ShowTrainerPhase(this)); + if (!this.trainer.visible) { + this.pushPhase(new ShowTrainerPhase(this)); + } } for (const pokemon of this.getParty()) { @@ -1288,7 +1396,6 @@ export default class BattleScene extends SceneBase { case Species.ZARUDE: case Species.SQUAWKABILLY: case Species.TATSUGIRI: - case Species.GIMMIGHOUL: case Species.PALDEA_TAUROS: return Utils.randSeedInt(species.forms.length); case Species.PIKACHU: @@ -1314,6 +1421,13 @@ export default class BattleScene extends SceneBase { return 1; } return 0; + case Species.GIMMIGHOUL: + // Chest form can only be found in Mysterious Chest Encounter, if this is a game mode with MEs + if (this.gameMode.hasMysteryEncounters) { + return 1; // Wandering form + } else { + return Utils.randSeedInt(species.forms.length); + } } if (ignoreArena) { @@ -1813,6 +1927,19 @@ export default class BattleScene extends SceneBase { return false; } + /** + * Fades out current track for `delay` ms, then fades in new track. + * @param newBgmKey + * @param destroy + * @param delay + */ + fadeAndSwitchBgm(newBgmKey: string, destroy: boolean = false, delay: number = 2000) { + this.fadeOutBgm(delay, destroy); + this.time.delayedCall(delay, () => { + this.playBgm(newBgmKey); + }); + } + playSound(sound: string | AnySound, config?: object): AnySound { const key = typeof sound === "string" ? sound : sound.key; config = config ?? {}; @@ -2037,12 +2164,16 @@ export default class BattleScene extends SceneBase { return 20.87; case "battle_macro_grunt": // SWSH Trainer Battle return 11.56; + case "battle_star_grunt": //SV Team Star Battle + return 133.362; case "battle_galactic_admin": //BDSP Team Galactic Admin Battle return 11.997; case "battle_skull_admin": //SM Team Skull Admin Battle return 15.463; case "battle_oleana": //SWSH Oleana Battle return 14.110; + case "battle_star_admin": //SV Team Star Boss Battle + return 9.493; case "battle_rocket_boss": //USUM Giovanni Battle return 9.115; case "battle_aqua_magma_boss": //ORAS Archie & Maxie Battle @@ -2059,6 +2190,18 @@ export default class BattleScene extends SceneBase { return 13.13; case "battle_macro_boss": //SWSH Rose Battle return 11.42; + case "battle_star_boss": //SV Cassiopeia Battle + return 25.764; + case "mystery_encounter_gen_5_gts": // BW GTS + return 8.52; + case "mystery_encounter_gen_6_gts": // XY GTS + return 9.24; + case "mystery_encounter_fun_and_games": // EoS Guildmaster Wigglytuff + return 4.78; + case "mystery_encounter_weird_dream": // EoS Temporal Spire + return 41.42; + case "mystery_encounter_delibirdy": // Firel Delibirdy + return 82.28; } return 0; @@ -2483,7 +2626,7 @@ export default class BattleScene extends SceneBase { }); } - generateEnemyModifiers(): Promise { + generateEnemyModifiers(heldModifiersConfigs?: HeldModifierConfig[][]): Promise { return new Promise(resolve => { if (this.currentBattle.battleSpec === BattleSpec.FINAL_BOSS) { return resolve(); @@ -2505,29 +2648,46 @@ export default class BattleScene extends SceneBase { } party.forEach((enemyPokemon: EnemyPokemon, i: integer) => { - const isBoss = enemyPokemon.isBoss() || (this.currentBattle.battleType === BattleType.TRAINER && !!this.currentBattle.trainer?.config.isBoss); - let upgradeChance = 32; - if (isBoss) { - upgradeChance /= 2; - } - if (isFinalBoss) { - upgradeChance /= 8; - } - const modifierChance = this.gameMode.getEnemyModifierChance(isBoss); - let pokemonModifierChance = modifierChance; - if (this.currentBattle.battleType === BattleType.TRAINER && this.currentBattle.trainer) - pokemonModifierChance = Math.ceil(pokemonModifierChance * this.currentBattle.trainer.getPartyMemberModifierChanceMultiplier(i)); // eslint-disable-line - let count = 0; - for (let c = 0; c < chances; c++) { - if (!Utils.randSeedInt(modifierChance)) { - count++; + if (heldModifiersConfigs && i < heldModifiersConfigs.length && heldModifiersConfigs[i]) { + heldModifiersConfigs[i].forEach(mt => { + let modifier: PokemonHeldItemModifier; + if (mt.modifier instanceof PokemonHeldItemModifierType) { + modifier = mt.modifier.newModifier(enemyPokemon); + } else { + modifier = mt.modifier as PokemonHeldItemModifier; + modifier.pokemonId = enemyPokemon.id; + } + const stackCount = mt.stackCount ?? 1; + modifier.stackCount = stackCount; + modifier.isTransferable = mt.isTransferable ?? modifier.isTransferable; + this.addEnemyModifier(modifier, true); + }); + } else { + const isBoss = enemyPokemon.isBoss() || (this.currentBattle.battleType === BattleType.TRAINER && !!this.currentBattle.trainer?.config.isBoss); + let upgradeChance = 32; + if (isBoss) { + upgradeChance /= 2; } + if (isFinalBoss) { + upgradeChance /= 8; + } + const modifierChance = this.gameMode.getEnemyModifierChance(isBoss); + let pokemonModifierChance = modifierChance; + if (this.currentBattle.battleType === BattleType.TRAINER && this.currentBattle.trainer) + pokemonModifierChance = Math.ceil(pokemonModifierChance * this.currentBattle.trainer.getPartyMemberModifierChanceMultiplier(i)); // eslint-disable-line + let count = 0; + for (let c = 0; c < chances; c++) { + if (!Utils.randSeedInt(modifierChance)) { + count++; + } + } + if (isBoss) { + count = Math.max(count, Math.floor(chances / 2)); + } + getEnemyModifierTypesForWave(difficultyWaveIndex, count, [ enemyPokemon ], this.currentBattle.battleType === BattleType.TRAINER ? ModifierPoolType.TRAINER : ModifierPoolType.WILD, upgradeChance) + .map(mt => mt.newModifier(enemyPokemon).add(this.enemyModifiers, false, this)); } - if (isBoss) { - count = Math.max(count, Math.floor(chances / 2)); - } - getEnemyModifierTypesForWave(difficultyWaveIndex, count, [ enemyPokemon ], this.currentBattle.battleType === BattleType.TRAINER ? ModifierPoolType.TRAINER : ModifierPoolType.WILD, upgradeChance) - .map(mt => mt.newModifier(enemyPokemon).add(this.enemyModifiers, false, this)); + return true; }); this.updateModifiers(false).then(() => resolve()); }); @@ -2721,6 +2881,16 @@ export default class BattleScene extends SceneBase { return false; } + triggerPokemonBattleAnim(pokemon: Pokemon, battleAnimType: PokemonAnimType, fieldAssets?: Phaser.GameObjects.Sprite[], delayed: boolean = false): boolean { + const phase: Phase = new PokemonAnimPhase(this, battleAnimType, pokemon, fieldAssets); + if (delayed) { + this.pushPhase(phase); + } else { + this.unshiftPhase(phase); + } + return true; + } + validateAchvs(achvType: Constructor, ...args: unknown[]): void { const filteredAchvs = Object.values(achvs).filter(a => a instanceof achvType); for (const achv of filteredAchvs) { @@ -2825,4 +2995,226 @@ export default class BattleScene extends SceneBase { this.shiftPhase(); } + + /** + * Updates Exp and level values for Player's party, adding new level up phases as required + * @param expValue raw value of exp to split among participants, OR the base multiplier to use with waveIndex + * @param pokemonDefeated If true, will increment Macho Brace stacks and give the party Pokemon friendship increases + * @param useWaveIndexMultiplier Default false. If true, will multiply expValue by a scaling waveIndex multiplier. Not needed if expValue is already scaled by level/wave + * @param pokemonParticipantIds Participants. If none are defined, no exp will be given. To spread evenly among the party, should pass all ids of party members. + */ + applyPartyExp(expValue: number, pokemonDefeated: boolean, useWaveIndexMultiplier?: boolean, pokemonParticipantIds?: Set): void { + const participantIds = pokemonParticipantIds ?? this.currentBattle.playerParticipantIds; + const party = this.getParty(); + const expShareModifier = this.findModifier(m => m instanceof ExpShareModifier) as ExpShareModifier; + const expBalanceModifier = this.findModifier(m => m instanceof ExpBalanceModifier) as ExpBalanceModifier; + const multipleParticipantExpBonusModifier = this.findModifier(m => m instanceof MultipleParticipantExpBonusModifier) as MultipleParticipantExpBonusModifier; + const nonFaintedPartyMembers = party.filter(p => p.hp); + const expPartyMembers = nonFaintedPartyMembers.filter(p => p.level < this.getMaxExpLevel()); + const partyMemberExp: number[] = []; + // EXP value calculation is based off Pokemon.getExpValue + if (useWaveIndexMultiplier) { + expValue = Math.floor(expValue * this.currentBattle.waveIndex / 5 + 1); + } + + if (participantIds.size > 0) { + if (this.currentBattle.battleType === BattleType.TRAINER || this.currentBattle.mysteryEncounter?.encounterMode === MysteryEncounterMode.TRAINER_BATTLE) { + expValue = Math.floor(expValue * 1.5); + } else if (this.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER && this.currentBattle.mysteryEncounter) { + expValue = Math.floor(expValue * this.currentBattle.mysteryEncounter.expMultiplier); + } + for (const partyMember of nonFaintedPartyMembers) { + const pId = partyMember.id; + const participated = participantIds.has(pId); + if (participated && pokemonDefeated) { + partyMember.addFriendship(2); + const machoBraceModifier = partyMember.getHeldItems().find(m => m instanceof PokemonIncrementingStatModifier); + if (machoBraceModifier && machoBraceModifier.stackCount < machoBraceModifier.getMaxStackCount(this)) { + machoBraceModifier.stackCount++; + this.updateModifiers(true, true); + partyMember.updateInfo(); + } + } + if (!expPartyMembers.includes(partyMember)) { + continue; + } + if (!participated && !expShareModifier) { + partyMemberExp.push(0); + continue; + } + let expMultiplier = 0; + if (participated) { + expMultiplier += (1 / participantIds.size); + if (participantIds.size > 1 && multipleParticipantExpBonusModifier) { + expMultiplier += multipleParticipantExpBonusModifier.getStackCount() * 0.2; + } + } else if (expShareModifier) { + expMultiplier += (expShareModifier.getStackCount() * 0.2) / participantIds.size; + } + if (partyMember.pokerus) { + expMultiplier *= 1.5; + } + if (Overrides.XP_MULTIPLIER_OVERRIDE !== null) { + expMultiplier = Overrides.XP_MULTIPLIER_OVERRIDE; + } + const pokemonExp = new Utils.NumberHolder(expValue * expMultiplier); + this.applyModifiers(PokemonExpBoosterModifier, true, partyMember, pokemonExp); + partyMemberExp.push(Math.floor(pokemonExp.value)); + } + + if (expBalanceModifier) { + let totalLevel = 0; + let totalExp = 0; + expPartyMembers.forEach((expPartyMember, epm) => { + totalExp += partyMemberExp[epm]; + totalLevel += expPartyMember.level; + }); + + const medianLevel = Math.floor(totalLevel / expPartyMembers.length); + + const recipientExpPartyMemberIndexes: number[] = []; + expPartyMembers.forEach((expPartyMember, epm) => { + if (expPartyMember.level <= medianLevel) { + recipientExpPartyMemberIndexes.push(epm); + } + }); + + const splitExp = Math.floor(totalExp / recipientExpPartyMemberIndexes.length); + + expPartyMembers.forEach((_partyMember, pm) => { + partyMemberExp[pm] = Phaser.Math.Linear(partyMemberExp[pm], recipientExpPartyMemberIndexes.indexOf(pm) > -1 ? splitExp : 0, 0.2 * expBalanceModifier.getStackCount()); + }); + } + + for (let pm = 0; pm < expPartyMembers.length; pm++) { + const exp = partyMemberExp[pm]; + + if (exp) { + const partyMemberIndex = party.indexOf(expPartyMembers[pm]); + this.unshiftPhase(expPartyMembers[pm].isOnField() ? new ExpPhase(this, partyMemberIndex, exp) : new ShowPartyExpBarPhase(this, partyMemberIndex, exp)); + } + } + } + } + + /** + * Loads or generates a mystery encounter + * @param encounterType used to load session encounter when restarting game, etc. + * @returns + */ + getMysteryEncounter(encounterType?: MysteryEncounterType): MysteryEncounter { + // Loading override or session encounter + let encounter: MysteryEncounter | null; + if (!isNullOrUndefined(Overrides.MYSTERY_ENCOUNTER_OVERRIDE) && allMysteryEncounters.hasOwnProperty(Overrides.MYSTERY_ENCOUNTER_OVERRIDE!)) { + encounter = allMysteryEncounters[Overrides.MYSTERY_ENCOUNTER_OVERRIDE!]; + } else { + encounter = !isNullOrUndefined(encounterType) ? allMysteryEncounters[encounterType!] : null; + } + + // Check for queued encounters first + if (!encounter && this.mysteryEncounterSaveData?.queuedEncounters && this.mysteryEncounterSaveData.queuedEncounters.length > 0) { + let i = 0; + while (i < this.mysteryEncounterSaveData.queuedEncounters.length && !!encounter) { + const candidate = this.mysteryEncounterSaveData.queuedEncounters[i]; + const forcedChance = candidate.spawnPercent; + if (Utils.randSeedInt(100) < forcedChance) { + encounter = allMysteryEncounters[candidate.type]; + } + + i++; + } + } + + if (encounter) { + encounter = new MysteryEncounter(encounter); + encounter.populateDialogueTokensFromRequirements(this); + return encounter; + } + + // See Enum values for base tier weights + const tierWeights = [MysteryEncounterTier.COMMON, MysteryEncounterTier.GREAT, MysteryEncounterTier.ULTRA, MysteryEncounterTier.ROGUE]; + + // Adjust tier weights by previously encountered events to lower odds of only Common/Great in run + this.mysteryEncounterSaveData.encounteredEvents.forEach(seenEncounterData => { + if (seenEncounterData.tier === MysteryEncounterTier.COMMON) { + tierWeights[0] = tierWeights[0] - 6; + } else if (seenEncounterData.tier === MysteryEncounterTier.GREAT) { + tierWeights[1] = tierWeights[1] - 4; + } + }); + + const totalWeight = tierWeights.reduce((a, b) => a + b); + const tierValue = Utils.randSeedInt(totalWeight); + const commonThreshold = totalWeight - tierWeights[0]; + const greatThreshold = totalWeight - tierWeights[0] - tierWeights[1]; + const ultraThreshold = totalWeight - tierWeights[0] - tierWeights[1] - tierWeights[2]; + let tier: MysteryEncounterTier | null = tierValue > commonThreshold ? MysteryEncounterTier.COMMON : tierValue > greatThreshold ? MysteryEncounterTier.GREAT : tierValue > ultraThreshold ? MysteryEncounterTier.ULTRA : MysteryEncounterTier.ROGUE; + + if (!isNullOrUndefined(Overrides.MYSTERY_ENCOUNTER_TIER_OVERRIDE)) { + tier = Overrides.MYSTERY_ENCOUNTER_TIER_OVERRIDE!; + } + + let availableEncounters: MysteryEncounter[] = []; + // New encounter should never be the same as the most recent encounter + const previousEncounter = this.mysteryEncounterSaveData.encounteredEvents.length > 0 ? this.mysteryEncounterSaveData.encounteredEvents[this.mysteryEncounterSaveData.encounteredEvents.length - 1].type : null; + const biomeMysteryEncounters = mysteryEncountersByBiome.get(this.arena.biomeType) ?? []; + // If no valid encounters exist at tier, checks next tier down, continuing until there are some encounters available + while (availableEncounters.length === 0 && tier !== null) { + availableEncounters = biomeMysteryEncounters + .filter((encounterType) => { + const encounterCandidate = allMysteryEncounters[encounterType]; + if (!encounterCandidate) { + return false; + } + if (encounterCandidate.encounterTier !== tier) { // Encounter is in tier + return false; + } + const disallowedGameModes = encounterCandidate.disallowedGameModes; + if (disallowedGameModes && disallowedGameModes.length > 0 + && disallowedGameModes.includes(this.gameMode.modeId)) { // Encounter is enabled for game mode + return false; + } + if (this.gameMode.modeId === GameModes.CHALLENGE) { // Encounter is enabled for challenges + const disallowedChallenges = encounterCandidate.disallowedChallenges; + if (disallowedChallenges && disallowedChallenges.length > 0 && this.gameMode.challenges.some(challenge => disallowedChallenges.includes(challenge.id))) { + return false; + } + } + if (!encounterCandidate.meetsRequirements(this)) { // Meets encounter requirements + return false; + } + if (previousEncounter !== null && encounterType === previousEncounter) { // Previous encounter was not this one + return false; + } + if (this.mysteryEncounterSaveData.encounteredEvents.length > 0 && // Encounter has not exceeded max allowed encounters + (encounterCandidate.maxAllowedEncounters && encounterCandidate.maxAllowedEncounters > 0) + && this.mysteryEncounterSaveData.encounteredEvents.filter(e => e.type === encounterType).length >= encounterCandidate.maxAllowedEncounters) { + return false; + } + return true; + }) + .map((m) => (allMysteryEncounters[m])); + // Decrement tier + if (tier === MysteryEncounterTier.ROGUE) { + tier = MysteryEncounterTier.ULTRA; + } else if (tier === MysteryEncounterTier.ULTRA) { + tier = MysteryEncounterTier.GREAT; + } else if (tier === MysteryEncounterTier.GREAT) { + tier = MysteryEncounterTier.COMMON; + } else { + tier = null; // Ends loop + } + } + + // If absolutely no encounters are available, spawn 0th encounter + if (availableEncounters.length === 0) { + console.log("No Mystery Encounters found, falling back to Mysterious Challengers."); + return allMysteryEncounters[MysteryEncounterType.MYSTERIOUS_CHALLENGERS]; + } + encounter = availableEncounters[Utils.randSeedInt(availableEncounters.length)]; + // New encounter object to not dirty flags + encounter = new MysteryEncounter(encounter); + encounter.populateDialogueTokensFromRequirements(this); + return encounter; + } } diff --git a/src/battle.ts b/src/battle.ts index a3e7b0a4336..d99e1a91c15 100644 --- a/src/battle.ts +++ b/src/battle.ts @@ -14,28 +14,39 @@ import { PlayerGender } from "#enums/player-gender"; import { Species } from "#enums/species"; import { TrainerType } from "#enums/trainer-type"; import i18next from "#app/plugins/i18n"; +import MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; +import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; +import { CustomModifierSettings } from "#app/modifier/modifier-type"; +import { ModifierTier } from "#app/modifier/modifier-tier"; + +export enum ClassicFixedBossWaves { + // TODO: other fixed wave battles should be added here + EVIL_BOSS_1 = 115, + EVIL_BOSS_2 = 165, +} export enum BattleType { - WILD, - TRAINER, - CLEAR + WILD, + TRAINER, + CLEAR, + MYSTERY_ENCOUNTER } export enum BattlerIndex { - ATTACKER = -1, - PLAYER, - PLAYER_2, - ENEMY, - ENEMY_2 + ATTACKER = -1, + PLAYER, + PLAYER_2, + ENEMY, + ENEMY_2 } export interface TurnCommand { - command: Command; - cursor?: number; - move?: QueuedMove; - targets?: BattlerIndex[]; - skip?: boolean; - args?: any[]; + command: Command; + cursor?: number; + move?: QueuedMove; + targets?: BattlerIndex[]; + skip?: boolean; + args?: any[]; } export interface FaintLogEntry { @@ -44,7 +55,7 @@ export interface FaintLogEntry { } interface TurnCommands { - [key: number]: TurnCommand | null + [key: number]: TurnCommand | null } export default class Battle { @@ -77,6 +88,9 @@ export default class Battle { public playerFaintsHistory: FaintLogEntry[] = []; public enemyFaintsHistory: FaintLogEntry[] = []; + /** If the current battle is a Mystery Encounter, this will always be defined */ + public mysteryEncounter?: MysteryEncounter; + private rngCounter: number = 0; constructor(gameMode: GameMode, waveIndex: number, battleType: BattleType, trainer?: Trainer, double?: boolean) { @@ -99,7 +113,7 @@ export default class Battle { this.battleSpec = spec; } - private getLevelForWave(): number { + public getLevelForWave(): number { const levelWaveIndex = this.gameMode.getWaveForDifficulty(this.waveIndex); const baseLevel = 1 + levelWaveIndex / 2 + Math.pow(levelWaveIndex / 25, 2); const bossMultiplier = 1.2; @@ -151,7 +165,7 @@ export default class Battle { } addPostBattleLoot(enemyPokemon: EnemyPokemon): void { - this.postBattleLoot.push(...enemyPokemon.scene.findModifiers(m => m instanceof PokemonHeldItemModifier && m.pokemonId === enemyPokemon.id && m.isTransferrable, false).map(i => { + this.postBattleLoot.push(...enemyPokemon.scene.findModifiers(m => m instanceof PokemonHeldItemModifier && m.pokemonId === enemyPokemon.id && m.isTransferable, false).map(i => { const ret = i as PokemonHeldItemModifier; //@ts-ignore - this is awful to fix/change ret.pokemonId = null; @@ -197,7 +211,11 @@ export default class Battle { getBgmOverride(scene: BattleScene): string | null { const battlers = this.enemyParty.slice(0, this.getBattlerCount()); - if (this.battleType === BattleType.TRAINER) { + if (this.battleType === BattleType.MYSTERY_ENCOUNTER && this.mysteryEncounter?.encounterMode === MysteryEncounterMode.DEFAULT) { + // Music is overridden for MEs during ME onInit() + // Should not use any BGM overrides before swapping from DEFAULT mode + return null; + } else if (this.battleType === BattleType.TRAINER || this.mysteryEncounter?.encounterMode === MysteryEncounterMode.TRAINER_BATTLE) { if (!this.started && this.trainer?.config.encounterBgm && this.trainer?.getEncounterMessages()?.length) { return `encounter_${this.trainer?.getEncounterBgm()}`; } @@ -409,6 +427,7 @@ export class FixedBattleConfig { public getTrainer: GetTrainerFunc; public getEnemyParty: GetEnemyPartyFunc; public seedOffsetWaveIndex: number; + public customModifierRewardSettings?: CustomModifierSettings; setBattleType(battleType: BattleType): FixedBattleConfig { this.battleType = battleType; @@ -434,6 +453,11 @@ export class FixedBattleConfig { this.seedOffsetWaveIndex = seedOffsetWaveIndex; return this; } + + setCustomModifierRewards(customModifierRewardSettings: CustomModifierSettings) { + this.customModifierRewardSettings = customModifierRewardSettings; + return this; + } } @@ -493,29 +517,35 @@ export const classicFixedBattles: FixedBattleConfigs = { [8]: new FixedBattleConfig().setBattleType(BattleType.TRAINER) .setGetTrainerFunc(scene => new Trainer(scene, TrainerType.RIVAL, scene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT)), [25]: new FixedBattleConfig().setBattleType(BattleType.TRAINER) - .setGetTrainerFunc(scene => new Trainer(scene, TrainerType.RIVAL_2, scene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT)), + .setGetTrainerFunc(scene => new Trainer(scene, TrainerType.RIVAL_2, scene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT)) + .setCustomModifierRewards({ guaranteedModifierTiers: [ModifierTier.ULTRA, ModifierTier.GREAT, ModifierTier.GREAT], allowLuckUpgrades: false }), [35]: new FixedBattleConfig().setBattleType(BattleType.TRAINER) - .setGetTrainerFunc(getRandomTrainerFunc([ TrainerType.ROCKET_GRUNT, TrainerType.MAGMA_GRUNT, TrainerType.AQUA_GRUNT, TrainerType.GALACTIC_GRUNT, TrainerType.PLASMA_GRUNT, TrainerType.FLARE_GRUNT, TrainerType.AETHER_GRUNT, TrainerType.SKULL_GRUNT, TrainerType.MACRO_GRUNT ], true)), + .setGetTrainerFunc(getRandomTrainerFunc([ TrainerType.ROCKET_GRUNT, TrainerType.MAGMA_GRUNT, TrainerType.AQUA_GRUNT, TrainerType.GALACTIC_GRUNT, TrainerType.PLASMA_GRUNT, TrainerType.FLARE_GRUNT, TrainerType.AETHER_GRUNT, TrainerType.SKULL_GRUNT, TrainerType.MACRO_GRUNT, TrainerType.STAR_GRUNT ], true)), [55]: new FixedBattleConfig().setBattleType(BattleType.TRAINER) - .setGetTrainerFunc(scene => new Trainer(scene, TrainerType.RIVAL_3, scene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT)), + .setGetTrainerFunc(scene => new Trainer(scene, TrainerType.RIVAL_3, scene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT)) + .setCustomModifierRewards({ guaranteedModifierTiers: [ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.GREAT, ModifierTier.GREAT], allowLuckUpgrades: false }), [62]: new FixedBattleConfig().setBattleType(BattleType.TRAINER).setSeedOffsetWave(35) - .setGetTrainerFunc(getRandomTrainerFunc([ TrainerType.ROCKET_GRUNT, TrainerType.MAGMA_GRUNT, TrainerType.AQUA_GRUNT, TrainerType.GALACTIC_GRUNT, TrainerType.PLASMA_GRUNT, TrainerType.FLARE_GRUNT, TrainerType.AETHER_GRUNT, TrainerType.SKULL_GRUNT, TrainerType.MACRO_GRUNT ], true)), + .setGetTrainerFunc(getRandomTrainerFunc([ TrainerType.ROCKET_GRUNT, TrainerType.MAGMA_GRUNT, TrainerType.AQUA_GRUNT, TrainerType.GALACTIC_GRUNT, TrainerType.PLASMA_GRUNT, TrainerType.FLARE_GRUNT, TrainerType.AETHER_GRUNT, TrainerType.SKULL_GRUNT, TrainerType.MACRO_GRUNT, TrainerType.STAR_GRUNT ], true)), [64]: new FixedBattleConfig().setBattleType(BattleType.TRAINER).setSeedOffsetWave(35) - .setGetTrainerFunc(getRandomTrainerFunc([ TrainerType.ROCKET_GRUNT, TrainerType.MAGMA_GRUNT, TrainerType.AQUA_GRUNT, TrainerType.GALACTIC_GRUNT, TrainerType.PLASMA_GRUNT, TrainerType.FLARE_GRUNT, TrainerType.AETHER_GRUNT, TrainerType.SKULL_GRUNT, TrainerType.MACRO_GRUNT ], true)), + .setGetTrainerFunc(getRandomTrainerFunc([ TrainerType.ROCKET_GRUNT, TrainerType.MAGMA_GRUNT, TrainerType.AQUA_GRUNT, TrainerType.GALACTIC_GRUNT, TrainerType.PLASMA_GRUNT, TrainerType.FLARE_GRUNT, TrainerType.AETHER_GRUNT, TrainerType.SKULL_GRUNT, TrainerType.MACRO_GRUNT, TrainerType.STAR_GRUNT ], true)), [66]: new FixedBattleConfig().setBattleType(BattleType.TRAINER).setSeedOffsetWave(35) - .setGetTrainerFunc(getRandomTrainerFunc([[ TrainerType.ARCHER, TrainerType.ARIANA, TrainerType.PROTON, TrainerType.PETREL ], [ TrainerType.TABITHA, TrainerType.COURTNEY ], [ TrainerType.MATT, TrainerType.SHELLY ], [ TrainerType.JUPITER, TrainerType.MARS, TrainerType.SATURN ], [ TrainerType.ZINZOLIN, TrainerType.ROOD ], [ TrainerType.XEROSIC, TrainerType.BRYONY ], TrainerType.FABA, TrainerType.PLUMERIA, TrainerType.OLEANA ], true)), + .setGetTrainerFunc(getRandomTrainerFunc([[ TrainerType.ARCHER, TrainerType.ARIANA, TrainerType.PROTON, TrainerType.PETREL ], [ TrainerType.TABITHA, TrainerType.COURTNEY ], [ TrainerType.MATT, TrainerType.SHELLY ], [ TrainerType.JUPITER, TrainerType.MARS, TrainerType.SATURN ], [ TrainerType.ZINZOLIN, TrainerType.ROOD ], [ TrainerType.XEROSIC, TrainerType.BRYONY ], TrainerType.FABA, TrainerType.PLUMERIA, TrainerType.OLEANA, [ TrainerType.GIACOMO, TrainerType.MELA, TrainerType.ATTICUS, TrainerType.ORTEGA, TrainerType.ERI ] ], true)), [95]: new FixedBattleConfig().setBattleType(BattleType.TRAINER) - .setGetTrainerFunc(scene => new Trainer(scene, TrainerType.RIVAL_4, scene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT)), + .setGetTrainerFunc(scene => new Trainer(scene, TrainerType.RIVAL_4, scene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT)) + .setCustomModifierRewards({ guaranteedModifierTiers: [ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.ULTRA], allowLuckUpgrades: false }), [112]: new FixedBattleConfig().setBattleType(BattleType.TRAINER).setSeedOffsetWave(35) - .setGetTrainerFunc(getRandomTrainerFunc([ TrainerType.ROCKET_GRUNT, TrainerType.MAGMA_GRUNT, TrainerType.AQUA_GRUNT, TrainerType.GALACTIC_GRUNT, TrainerType.PLASMA_GRUNT, TrainerType.FLARE_GRUNT, TrainerType.AETHER_GRUNT, TrainerType.SKULL_GRUNT, TrainerType.MACRO_GRUNT ], true)), + .setGetTrainerFunc(getRandomTrainerFunc([ TrainerType.ROCKET_GRUNT, TrainerType.MAGMA_GRUNT, TrainerType.AQUA_GRUNT, TrainerType.GALACTIC_GRUNT, TrainerType.PLASMA_GRUNT, TrainerType.FLARE_GRUNT, TrainerType.AETHER_GRUNT, TrainerType.SKULL_GRUNT, TrainerType.MACRO_GRUNT, TrainerType.STAR_GRUNT ], true)), [114]: new FixedBattleConfig().setBattleType(BattleType.TRAINER).setSeedOffsetWave(35) - .setGetTrainerFunc(getRandomTrainerFunc([[ TrainerType.ARCHER, TrainerType.ARIANA, TrainerType.PROTON, TrainerType.PETREL ], [ TrainerType.TABITHA, TrainerType.COURTNEY ], [ TrainerType.MATT, TrainerType.SHELLY ], [ TrainerType.JUPITER, TrainerType.MARS, TrainerType.SATURN ], [ TrainerType.ZINZOLIN, TrainerType.ROOD ], [ TrainerType.XEROSIC, TrainerType.BRYONY ], TrainerType.FABA, TrainerType.PLUMERIA, TrainerType.OLEANA ], true, 1)), - [115]: new FixedBattleConfig().setBattleType(BattleType.TRAINER).setSeedOffsetWave(35) - .setGetTrainerFunc(getRandomTrainerFunc([ TrainerType.ROCKET_BOSS_GIOVANNI_1, TrainerType.MAXIE, TrainerType.ARCHIE, TrainerType.CYRUS, TrainerType.GHETSIS, TrainerType.LYSANDRE, TrainerType.LUSAMINE, TrainerType.GUZMA, TrainerType.ROSE ])), + .setGetTrainerFunc(getRandomTrainerFunc([[ TrainerType.ARCHER, TrainerType.ARIANA, TrainerType.PROTON, TrainerType.PETREL ], [ TrainerType.TABITHA, TrainerType.COURTNEY ], [ TrainerType.MATT, TrainerType.SHELLY ], [ TrainerType.JUPITER, TrainerType.MARS, TrainerType.SATURN ], [ TrainerType.ZINZOLIN, TrainerType.ROOD ], [ TrainerType.XEROSIC, TrainerType.BRYONY ], TrainerType.FABA, TrainerType.PLUMERIA, TrainerType.OLEANA, [ TrainerType.GIACOMO, TrainerType.MELA, TrainerType.ATTICUS, TrainerType.ORTEGA, TrainerType.ERI ] ], true, 1)), + [ClassicFixedBossWaves.EVIL_BOSS_1]: new FixedBattleConfig().setBattleType(BattleType.TRAINER).setSeedOffsetWave(35) + .setGetTrainerFunc(getRandomTrainerFunc([ TrainerType.ROCKET_BOSS_GIOVANNI_1, TrainerType.MAXIE, TrainerType.ARCHIE, TrainerType.CYRUS, TrainerType.GHETSIS, TrainerType.LYSANDRE, TrainerType.LUSAMINE, TrainerType.GUZMA, TrainerType.ROSE, TrainerType.PENNY ])) + .setCustomModifierRewards({ guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ROGUE, ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.ULTRA], allowLuckUpgrades: false }), [145]: new FixedBattleConfig().setBattleType(BattleType.TRAINER) - .setGetTrainerFunc(scene => new Trainer(scene, TrainerType.RIVAL_5, scene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT)), - [165]: new FixedBattleConfig().setBattleType(BattleType.TRAINER).setSeedOffsetWave(35) - .setGetTrainerFunc(getRandomTrainerFunc([ TrainerType.ROCKET_BOSS_GIOVANNI_2, TrainerType.MAXIE_2, TrainerType.ARCHIE_2, TrainerType.CYRUS_2, TrainerType.GHETSIS_2, TrainerType.LYSANDRE_2, TrainerType.LUSAMINE_2, TrainerType.GUZMA_2, TrainerType.ROSE_2 ])), + .setGetTrainerFunc(scene => new Trainer(scene, TrainerType.RIVAL_5, scene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT)) + .setCustomModifierRewards({ guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ROGUE, ModifierTier.ROGUE, ModifierTier.ULTRA, ModifierTier.ULTRA], allowLuckUpgrades: false }), + [ClassicFixedBossWaves.EVIL_BOSS_2]: new FixedBattleConfig().setBattleType(BattleType.TRAINER).setSeedOffsetWave(35) + .setGetTrainerFunc(getRandomTrainerFunc([ TrainerType.ROCKET_BOSS_GIOVANNI_2, TrainerType.MAXIE_2, TrainerType.ARCHIE_2, TrainerType.CYRUS_2, TrainerType.GHETSIS_2, TrainerType.LYSANDRE_2, TrainerType.LUSAMINE_2, TrainerType.GUZMA_2, TrainerType.ROSE_2, TrainerType.PENNY_2 ])) + .setCustomModifierRewards({ guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ROGUE, ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.ULTRA], allowLuckUpgrades: false }), [182]: new FixedBattleConfig().setBattleType(BattleType.TRAINER) .setGetTrainerFunc(getRandomTrainerFunc([ TrainerType.LORELEI, TrainerType.WILL, TrainerType.SIDNEY, TrainerType.AARON, TrainerType.SHAUNTAL, TrainerType.MALVA, [ TrainerType.HALA, TrainerType.MOLAYNE ], TrainerType.MARNIE_ELITE, TrainerType.RIKA, TrainerType.CRISPIN ])), [184]: new FixedBattleConfig().setBattleType(BattleType.TRAINER).setSeedOffsetWave(182) @@ -528,4 +558,5 @@ export const classicFixedBattles: FixedBattleConfigs = { .setGetTrainerFunc(getRandomTrainerFunc([ TrainerType.BLUE, [ TrainerType.RED, TrainerType.LANCE_CHAMPION ], [ TrainerType.STEVEN, TrainerType.WALLACE ], TrainerType.CYNTHIA, [ TrainerType.ALDER, TrainerType.IRIS ], TrainerType.DIANTHA, TrainerType.HAU, TrainerType.LEON, [ TrainerType.GEETA, TrainerType.NEMONA ], TrainerType.KIERAN ])), [195]: new FixedBattleConfig().setBattleType(BattleType.TRAINER) .setGetTrainerFunc(scene => new Trainer(scene, TrainerType.RIVAL_6, scene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT)) + .setCustomModifierRewards({ guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ROGUE, ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.GREAT, ModifierTier.GREAT], allowLuckUpgrades: false }) }; diff --git a/src/constants.ts b/src/constants.ts index a2f7e47b996..0b1261ad814 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1 +1,5 @@ -export const PLAYER_PARTY_MAX_SIZE = 6; +/** The maximum size of the player's party */ +export const PLAYER_PARTY_MAX_SIZE: number = 6; + +/** Whether to use seasonal splash messages in general */ +export const USE_SEASONAL_SPLASH_MESSAGES: boolean = false; diff --git a/src/data/ability.ts b/src/data/ability.ts index 6acf77cfca5..7a8d77cc022 100755 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -165,14 +165,27 @@ export class BlockRecoilDamageAttr extends AbAttr { } } +/** + * Attribute for abilities that increase the chance of a double battle + * occurring. + * @see apply + */ export class DoubleBattleChanceAbAttr extends AbAttr { constructor() { super(false); } - apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean { - const doubleChance = (args[0] as Utils.IntegerHolder); - doubleChance.value = Math.max(doubleChance.value / 2, 1); + /** + * Increases the chance of a double battle occurring + * @param args [0] {@linkcode Utils.NumberHolder} for double battle chance + * @returns true if the ability was applied + */ + apply(_pokemon: Pokemon, _passive: boolean, _simulated: boolean, _cancelled: Utils.BooleanHolder, args: any[]): boolean { + const doubleBattleChance = args[0] as Utils.NumberHolder; + // This is divided because the chance is generated as a number from 0 to doubleBattleChance.value using Utils.randSeedInt + // A double battle will initiate if the generated number is 0 + doubleBattleChance.value = doubleBattleChance.value / 4; + return true; } } @@ -1086,7 +1099,7 @@ export class PostDefendMoveDisableAbAttr extends PostDefendAbAttr { applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean { if (attacker.getTag(BattlerTagType.DISABLED) === null) { - if (move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon) && (this.chance === -1 || pokemon.randSeedInt(100) < this.chance) && !attacker.isMax()) { + if (move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon) && (this.chance === -1 || pokemon.randSeedInt(100) < this.chance)) { if (simulated) { return true; } @@ -1670,7 +1683,7 @@ export class PostAttackStealHeldItemAbAttr extends PostAttackAbAttr { applyPostAttackAfterMoveTypeCheck(pokemon: Pokemon, passive: boolean, simulated: boolean, defender: Pokemon, move: Move, hitResult: HitResult, args: any[]): Promise { return new Promise(resolve => { if (!simulated && hitResult < HitResult.NO_EFFECT && (!this.stealCondition || this.stealCondition(pokemon, defender, move))) { - const heldItems = this.getTargetHeldItems(defender).filter(i => i.isTransferrable); + const heldItems = this.getTargetHeldItems(defender).filter(i => i.isTransferable); if (heldItems.length) { const stolenItem = heldItems[pokemon.randSeedInt(heldItems.length)]; pokemon.scene.tryTransferHeldItemModifier(stolenItem, pokemon, false).then(success => { @@ -1706,6 +1719,10 @@ export class PostAttackApplyStatusEffectAbAttr extends PostAttackAbAttr { } applyPostAttackAfterMoveTypeCheck(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean { + if (pokemon !== attacker && move.hitsSubstitute(attacker, pokemon)) { + return false; + } + /**Status inflicted by abilities post attacking are also considered additional effects.*/ if (!attacker.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && !simulated && pokemon !== attacker && (!this.contactRequired || move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon)) && pokemon.randSeedInt(100) < this.chance && !pokemon.status) { const effect = this.effects.length === 1 ? this.effects[0] : this.effects[pokemon.randSeedInt(this.effects.length)]; @@ -1759,7 +1776,7 @@ export class PostDefendStealHeldItemAbAttr extends PostDefendAbAttr { applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): Promise { return new Promise(resolve => { if (!simulated && hitResult < HitResult.NO_EFFECT && (!this.condition || this.condition(pokemon, attacker, move))) { - const heldItems = this.getTargetHeldItems(attacker).filter(i => i.isTransferrable); + const heldItems = this.getTargetHeldItems(attacker).filter(i => i.isTransferable); if (heldItems.length) { const stolenItem = heldItems[pokemon.randSeedInt(heldItems.length)]; pokemon.scene.tryTransferHeldItemModifier(stolenItem, pokemon, false).then(success => { @@ -2064,6 +2081,10 @@ export class PostSummonStatStageChangeAbAttr extends PostSummonAbAttr { if (this.intimidate) { applyAbAttrs(IntimidateImmunityAbAttr, opponent, cancelled, simulated); applyAbAttrs(PostIntimidateStatStageChangeAbAttr, opponent, cancelled, simulated); + + if (opponent.getTag(BattlerTagType.SUBSTITUTE)) { + cancelled.value = true; + } } if (!cancelled.value) { pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, opponent.getBattlerIndex(), false, this.stats, this.stages)); @@ -2143,7 +2164,6 @@ export class DownloadAbAttr extends PostSummonAbAttr { private enemyCountTally: integer; private stats: BattleStat[]; - // TODO: Implement the Substitute feature(s) once move is implemented. /** * Checks to see if it is the opening turn (starting a new game), if so, Download won't work. This is because Download takes into account * vitamins and items, so it needs to use the Stat and the stat alone. @@ -2618,7 +2638,11 @@ export class PreStatStageChangeAbAttr extends AbAttr { } } +/** + * Protect one or all {@linkcode BattleStat} from reductions caused by other Pokémon's moves and Abilities + */ export class ProtectStatAbAttr extends PreStatStageChangeAbAttr { + /** {@linkcode BattleStat} to protect or `undefined` if **all** {@linkcode BattleStat} are protected */ private protectedStat?: BattleStat; constructor(protectedStat?: BattleStat) { @@ -2627,7 +2651,17 @@ export class ProtectStatAbAttr extends PreStatStageChangeAbAttr { this.protectedStat = protectedStat; } - applyPreStatStageChange(_pokemon: Pokemon, _passive: boolean, simulated: boolean, stat: BattleStat, cancelled: Utils.BooleanHolder, _args: any[]): boolean { + /** + * Apply the {@linkcode ProtectedStatAbAttr} to an interaction + * @param _pokemon + * @param _passive + * @param simulated + * @param stat the {@linkcode BattleStat} being affected + * @param cancelled The {@linkcode Utils.BooleanHolder} that will be set to true if the stat is protected + * @param _args + * @returns true if the stat is protected, false otherwise + */ + applyPreStatStageChange(_pokemon: Pokemon, _passive: boolean, _simulated: boolean, stat: BattleStat, cancelled: Utils.BooleanHolder, _args: any[]): boolean { if (Utils.isNullOrUndefined(this.protectedStat) || stat === this.protectedStat) { cancelled.value = true; return true; @@ -3750,7 +3784,7 @@ export class StatStageChangeMultiplierAbAttr extends AbAttr { this.multiplier = multiplier; } - apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean { + override apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean { (args[0] as Utils.IntegerHolder).value *= this.multiplier; return true; @@ -4781,7 +4815,7 @@ export const allAbilities = [ new Ability(Abilities.NONE, 3) ]; export function initAbilities() { allAbilities.push( new Ability(Abilities.STENCH, 3) - .attr(PostAttackApplyBattlerTagAbAttr, false, (user, target, move) => !move.hasAttr(FlinchAttr) ? 10 : 0, BattlerTagType.FLINCHED), + .attr(PostAttackApplyBattlerTagAbAttr, false, (user, target, move) => !move.hasAttr(FlinchAttr) && !move.hitsSubstitute(user, target) ? 10 : 0, BattlerTagType.FLINCHED), new Ability(Abilities.DRIZZLE, 3) .attr(PostSummonWeatherChangeAbAttr, WeatherType.RAIN) .attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.RAIN), @@ -4810,11 +4844,9 @@ export function initAbilities() { .bypassFaint(), new Ability(Abilities.VOLT_ABSORB, 3) .attr(TypeImmunityHealAbAttr, Type.ELECTRIC) - .partial() // Healing not blocked by Heal Block .ignorable(), new Ability(Abilities.WATER_ABSORB, 3) .attr(TypeImmunityHealAbAttr, Type.WATER) - .partial() // Healing not blocked by Heal Block .ignorable(), new Ability(Abilities.OBLIVIOUS, 3) .attr(BattlerTagImmunityAbAttr, BattlerTagType.INFATUATED) @@ -4927,8 +4959,7 @@ export function initAbilities() { .attr(MoveImmunityAbAttr, (pokemon, attacker, move) => pokemon !== attacker && move.hasFlag(MoveFlags.SOUND_BASED)) .ignorable(), new Ability(Abilities.RAIN_DISH, 3) - .attr(PostWeatherLapseHealAbAttr, 1, WeatherType.RAIN, WeatherType.HEAVY_RAIN) - .partial(), // Healing not blocked by Heal Block + .attr(PostWeatherLapseHealAbAttr, 1, WeatherType.RAIN, WeatherType.HEAVY_RAIN), new Ability(Abilities.SAND_STREAM, 3) .attr(PostSummonWeatherChangeAbAttr, WeatherType.SANDSTORM) .attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.SANDSTORM), @@ -5059,7 +5090,6 @@ export function initAbilities() { .attr(PostWeatherLapseHealAbAttr, 2, WeatherType.RAIN, WeatherType.HEAVY_RAIN) .attr(ReceivedTypeDamageMultiplierAbAttr, Type.FIRE, 1.25) .attr(TypeImmunityHealAbAttr, Type.WATER) - .partial() // Healing not blocked by Heal Block .ignorable(), new Ability(Abilities.DOWNLOAD, 4) .attr(DownloadAbAttr), @@ -5140,8 +5170,7 @@ export function initAbilities() { .ignorable(), new Ability(Abilities.ICE_BODY, 4) .attr(BlockWeatherDamageAttr, WeatherType.HAIL) - .attr(PostWeatherLapseHealAbAttr, 1, WeatherType.HAIL, WeatherType.SNOW) - .partial(), // Healing not blocked by Heal Block + .attr(PostWeatherLapseHealAbAttr, 1, WeatherType.HAIL, WeatherType.SNOW), new Ability(Abilities.SOLID_ROCK, 4) .attr(ReceivedMoveDamageMultiplierAbAttr, (target, user, move) => target.getMoveEffectiveness(user, move) >= 2, 0.75) .ignorable(), @@ -5311,8 +5340,7 @@ export function initAbilities() { .ignorable() .unimplemented(), new Ability(Abilities.CHEEK_POUCH, 6) - .attr(HealFromBerryUseAbAttr, 1/3) - .partial(), // Healing not blocked by Heal Block + .attr(HealFromBerryUseAbAttr, 1/3), new Ability(Abilities.PROTEAN, 6) .attr(PokemonTypeChangeAbAttr), //.condition((p) => !p.summonData?.abilitiesApplied.includes(Abilities.PROTEAN)), //Gen 9 Implementation @@ -5851,6 +5879,6 @@ export function initAbilities() { new Ability(Abilities.POISON_PUPPETEER, 9) .attr(UncopiableAbilityAbAttr) .attr(UnswappableAbilityAbAttr) - .conditionalAttr(pokemon => pokemon.species.speciesId===Species.PECHARUNT, ConfusionOnStatusEffectAbAttr, StatusEffect.POISON, StatusEffect.TOXIC) + .attr(ConfusionOnStatusEffectAbAttr, StatusEffect.POISON, StatusEffect.TOXIC) ); } diff --git a/src/data/battle-anims.ts b/src/data/battle-anims.ts index da4e7f6a33b..eb0dce3bf0c 100644 --- a/src/data/battle-anims.ts +++ b/src/data/battle-anims.ts @@ -6,6 +6,10 @@ import * as Utils from "../utils"; import { BattlerIndex } from "../battle"; import { Element } from "json-stable-stringify"; import { Moves } from "#enums/moves"; +import { SubstituteTag } from "./battler-tags"; +import { isNullOrUndefined } from "../utils"; +import Phaser from "phaser"; +import { EncounterAnim } from "#enums/encounter-anims"; //import fs from 'vite-plugin-fs/browser'; export enum AnimFrameTarget { @@ -303,7 +307,7 @@ abstract class AnimTimedEvent { this.resourceName = resourceName; } - abstract execute(scene: BattleScene, battleAnim: BattleAnim): integer; + abstract execute(scene: BattleScene, battleAnim: BattleAnim, priority?: number): integer; abstract getEventType(): string; } @@ -321,7 +325,7 @@ class AnimTimedSoundEvent extends AnimTimedEvent { } } - execute(scene: BattleScene, battleAnim: BattleAnim): integer { + execute(scene: BattleScene, battleAnim: BattleAnim, priority?: number): integer { const soundConfig = { rate: (this.pitch * 0.01), volume: (this.volume * 0.01) }; if (this.resourceName) { try { @@ -383,7 +387,7 @@ class AnimTimedUpdateBgEvent extends AnimTimedBgEvent { super(frameIndex, resourceName, source); } - execute(scene: BattleScene, moveAnim: MoveAnim): integer { + execute(scene: BattleScene, moveAnim: MoveAnim, priority?: number): integer { const tweenProps = {}; if (this.bgX !== undefined) { tweenProps["x"] = (this.bgX * 0.5) - 320; @@ -413,7 +417,7 @@ class AnimTimedAddBgEvent extends AnimTimedBgEvent { super(frameIndex, resourceName, source); } - execute(scene: BattleScene, moveAnim: MoveAnim): integer { + execute(scene: BattleScene, moveAnim: MoveAnim, priority?: number): integer { if (moveAnim.bgSprite) { moveAnim.bgSprite.destroy(); } @@ -424,8 +428,10 @@ class AnimTimedAddBgEvent extends AnimTimedBgEvent { moveAnim.bgSprite.setScale(1.25); moveAnim.bgSprite.setAlpha(this.opacity / 255); scene.field.add(moveAnim.bgSprite); - const fieldPokemon = scene.getEnemyPokemon() || scene.getPlayerPokemon(); - if (fieldPokemon?.isOnField()) { + const fieldPokemon = scene.getNonSwitchedEnemyPokemon() || scene.getNonSwitchedPlayerPokemon(); + if (!isNullOrUndefined(priority)) { + scene.field.moveTo(moveAnim.bgSprite as Phaser.GameObjects.GameObject, priority!); + } else if (fieldPokemon?.isOnField()) { scene.field.moveBelow(moveAnim.bgSprite as Phaser.GameObjects.GameObject, fieldPokemon); } @@ -445,6 +451,7 @@ class AnimTimedAddBgEvent extends AnimTimedBgEvent { export const moveAnims = new Map(); export const chargeAnims = new Map(); export const commonAnims = new Map(); +export const encounterAnims = new Map(); export function initCommonAnims(scene: BattleScene): Promise { return new Promise(resolve => { @@ -481,14 +488,14 @@ export function initMoveAnim(scene: BattleScene, move: Moves): Promise { } else { moveAnims.set(move, null); const defaultMoveAnim = allMoves[move] instanceof AttackMove ? Moves.TACKLE : allMoves[move] instanceof SelfStatusMove ? Moves.FOCUS_ENERGY : Moves.TAIL_WHIP; - const moveName = Moves[move].toLowerCase().replace(/\_/g, "-"); + const fetchAnimAndResolve = (move: Moves) => { - scene.cachedFetch(`./battle-anims/${moveName}.json`) + scene.cachedFetch(`./battle-anims/${Utils.animationFileName(move)}.json`) .then(response => { const contentType = response.headers.get("content-type"); if (!response.ok || contentType?.indexOf("application/json") === -1) { - console.error(`Could not load animation file for move '${moveName}'`, response.status, response.statusText); - populateMoveAnim(move, moveAnims.get(defaultMoveAnim)); + useDefaultAnim(move, defaultMoveAnim); + logMissingMoveAnim(move, response.status, response.statusText); return resolve(); } return response.json(); @@ -508,6 +515,11 @@ export function initMoveAnim(scene: BattleScene, move: Moves): Promise { } else { resolve(); } + }) + .catch(error => { + useDefaultAnim(move, defaultMoveAnim); + logMissingMoveAnim(move, error); + return resolve(); }); }; fetchAnimAndResolve(move); @@ -515,6 +527,49 @@ export function initMoveAnim(scene: BattleScene, move: Moves): Promise { }); } +/** + * Populates the default animation for the given move. + * + * @param move the move to populate an animation for + * @param defaultMoveAnim the move to use as the default animation + */ +function useDefaultAnim(move: Moves, defaultMoveAnim: Moves) { + populateMoveAnim(move, moveAnims.get(defaultMoveAnim)); +} + +/** + * Helper method for printing a warning to the console when a move animation is missing. + * + * @param move the move to populate an animation for + * @param optionalParams parameters to add to the error logging + * + * @remarks use {@linkcode useDefaultAnim} to use a default animation + */ +function logMissingMoveAnim(move: Moves, ...optionalParams: any[]) { + const moveName = Utils.animationFileName(move); + console.warn(`Could not load animation file for move '${moveName}'`, ...optionalParams); +} + +/** + * Fetches animation configs to be used in a Mystery Encounter + * @param scene + * @param encounterAnim one or more animations to fetch + */ +export async function initEncounterAnims(scene: BattleScene, encounterAnim: EncounterAnim | EncounterAnim[]): Promise { + const anims = Array.isArray(encounterAnim) ? encounterAnim : [encounterAnim]; + const encounterAnimNames = Utils.getEnumKeys(EncounterAnim); + const encounterAnimFetches: Promise>[] = []; + for (const anim of anims) { + if (encounterAnims.has(anim) && !isNullOrUndefined(encounterAnims.get(anim))) { + continue; + } + encounterAnimFetches.push(scene.cachedFetch(`./battle-anims/encounter-${encounterAnimNames[anim].toLowerCase().replace(/\_/g, "-")}.json`) + .then(response => response.json()) + .then(cas => encounterAnims.set(anim, new AnimConfig(cas)))); + } + await Promise.allSettled(encounterAnimFetches); +} + export function initMoveChargeAnim(scene: BattleScene, chargeAnim: ChargeAnim): Promise { return new Promise(resolve => { if (chargeAnims.has(chargeAnim)) { @@ -569,6 +624,16 @@ export function loadCommonAnimAssets(scene: BattleScene, startLoad?: boolean): P }); } +/** + * Loads encounter animation assets to scene + * MUST be called after {@linkcode initEncounterAnims()} to load all required animations properly + * @param scene + * @param startLoad + */ +export async function loadEncounterAnimAssets(scene: BattleScene, startLoad?: boolean): Promise { + await loadAnimAssets(scene, Array.from(encounterAnims.values()), startLoad); +} + export function loadMoveAnimAssets(scene: BattleScene, moveIds: Moves[], startLoad?: boolean): Promise { return new Promise(resolve => { const moveAnimations = moveIds.map(m => moveAnims.get(m) as AnimConfig).flat(); @@ -678,14 +743,21 @@ export abstract class BattleAnim { public target: Pokemon | null; public sprites: Phaser.GameObjects.Sprite[]; public bgSprite: Phaser.GameObjects.TileSprite | Phaser.GameObjects.Rectangle; + /** + * Will attempt to play as much of an animation as possible, even if not all targets are on the field. + * Will also play the animation, even if the user has selected "Move Animations" OFF in Settings. + * Exclusively used by MEs atm, for visual animations at the start of an encounter. + */ + public playRegardlessOfIssues: boolean; private srcLine: number[]; private dstLine: number[]; - constructor(user?: Pokemon, target?: Pokemon) { + constructor(user?: Pokemon, target?: Pokemon, playRegardlessOfIssues: boolean = false) { this.user = user ?? null; this.target = target ?? null; this.sprites = []; + this.playRegardlessOfIssues = playRegardlessOfIssues; } abstract getAnim(): AnimConfig | null; @@ -700,7 +772,7 @@ export abstract class BattleAnim { return false; } - private getGraphicFrameData(scene: BattleScene, frames: AnimFrame[]): Map> { + private getGraphicFrameData(scene: BattleScene, frames: AnimFrame[], onSubstitute?: boolean): Map> { const ret: Map> = new Map([ [AnimFrameTarget.GRAPHIC, new Map() ], [AnimFrameTarget.USER, new Map() ], @@ -711,12 +783,15 @@ 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 userInitialX = user!.x; // TODO: is this bang correct? const userInitialY = user!.y; // TODO: is this bang correct? const userHalfHeight = user!.getSprite().displayHeight! / 2; // TODO: is this bang correct? - const targetInitialX = target!.x; // TODO: is this bang correct? - const targetInitialY = target!.y; // TODO: is this bang correct? - const targetHalfHeight = target!.getSprite().displayHeight! / 2; // TODO: is this bang correct? + + const targetInitialX = targetSubstitute?.sprite?.x ?? target!.x; // TODO: is this bang correct? + const targetInitialY = targetSubstitute?.sprite?.y ?? target!.y; // TODO: is this bang correct? + const targetHalfHeight = (targetSubstitute?.sprite ?? target!.getSprite()).displayHeight! / 2; // TODO: is this bang correct? let g = 0; let u = 0; @@ -754,20 +829,22 @@ export abstract class BattleAnim { return ret; } - play(scene: BattleScene, callback?: Function) { + play(scene: BattleScene, onSubstitute?: boolean, callback?: Function) { const isOppAnim = this.isOppAnim(); const user = !isOppAnim ? this.user! : this.target!; // TODO: are those bangs correct? - const target = !isOppAnim ? this.target : this.user; + const target = !isOppAnim ? this.target! : this.user!; - if (!target?.isOnField()) { + if (!target?.isOnField() && !this.playRegardlessOfIssues) { if (callback) { callback(); } return; } + const targetSubstitute = (!!onSubstitute && user !== target) ? target.getTag(SubstituteTag) : null; + const userSprite = user.getSprite(); - const targetSprite = target.getSprite(); + const targetSprite = targetSubstitute?.sprite ?? target.getSprite(); const spriteCache: SpriteCache = { [AnimFrameTarget.GRAPHIC]: [], @@ -782,16 +859,34 @@ export abstract class BattleAnim { userSprite.setAlpha(1); userSprite.pipelineData["tone"] = [ 0.0, 0.0, 0.0, 0.0 ]; userSprite.setAngle(0); - targetSprite.setPosition(0, 0); - targetSprite.setScale(1); - targetSprite.setAlpha(1); + if (!targetSubstitute) { + targetSprite.setPosition(0, 0); + targetSprite.setScale(1); + targetSprite.setAlpha(1); + } else { + targetSprite.setPosition( + target.x - target.getSubstituteOffset()[0], + target.y - target.getSubstituteOffset()[1] + ); + targetSprite.setScale(target.getSpriteScale() * (target.isPlayer() ? 0.5 : 1)); + targetSprite.setAlpha(1); + } targetSprite.pipelineData["tone"] = [ 0.0, 0.0, 0.0, 0.0 ]; targetSprite.setAngle(0); - if (!this.isHideUser() && userSprite) { - this.user?.getSprite().setVisible(true); // using this.user to fix context loss due to isOppAnim swap (#481) + + /** + * This and `targetSpriteToShow` are used to restore context lost + * from the `isOppAnim` swap. Using these references instead of `this.user` + * and `this.target` prevent the target's Substitute doll from disappearing + * after being the target of an animation. + */ + const userSpriteToShow = !isOppAnim ? userSprite : targetSprite; + const targetSpriteToShow = !isOppAnim ? targetSprite : userSprite; + if (!this.isHideUser() && userSpriteToShow) { + userSpriteToShow.setVisible(true); } - if (!this.isHideTarget() && (targetSprite !== userSprite || !this.isHideUser())) { - this.target?.getSprite().setVisible(true); // using this.target to fix context loss due to isOppAnim swap (#481) + if (!this.isHideTarget() && (targetSpriteToShow !== userSpriteToShow || !this.isHideUser())) { + targetSpriteToShow.setVisible(true); } for (const ms of Object.values(spriteCache).flat()) { if (ms) { @@ -806,7 +901,7 @@ export abstract class BattleAnim { } }; - if (!scene.moveAnimations) { + if (!scene.moveAnimations && !this.playRegardlessOfIssues) { return cleanUpAndComplete(); } @@ -814,8 +909,8 @@ export abstract class BattleAnim { const userInitialX = user.x; const userInitialY = user.y; - const targetInitialX = target.x; - const targetInitialY = target.y; + const targetInitialX = targetSubstitute?.sprite?.x ?? target.x; + const targetInitialY = targetSubstitute?.sprite?.y ?? target.y; this.srcLine = [ userFocusX, userFocusY, targetFocusX, targetFocusY ]; this.dstLine = [ userInitialX, userInitialY, targetInitialX, targetInitialY ]; @@ -833,7 +928,7 @@ export abstract class BattleAnim { } const spriteFrames = anim!.frames[f]; // TODO: is the bang correcT? - const frameData = this.getGraphicFrameData(scene, anim!.frames[f]); // TODO: is the bang correct? + const frameData = this.getGraphicFrameData(scene, anim!.frames[f], onSubstitute); // TODO: is the bang correct? let u = 0; let t = 0; let g = 0; @@ -842,28 +937,40 @@ export abstract class BattleAnim { const isUser = frame.target === AnimFrameTarget.USER; if (isUser && target === user) { continue; + } else if (this.playRegardlessOfIssues && frame.target === AnimFrameTarget.TARGET && !target.isOnField()) { + continue; } const sprites = spriteCache[isUser ? AnimFrameTarget.USER : AnimFrameTarget.TARGET]; const spriteSource = isUser ? userSprite : targetSprite; if ((isUser ? u : t) === sprites.length) { - const sprite = scene.addPokemonSprite(isUser ? user! : target, 0, 0, spriteSource!.texture, spriteSource!.frame.name, true); // TODO: are those bangs correct? - [ "spriteColors", "fusionSpriteColors" ].map(k => sprite.pipelineData[k] = (isUser ? user! : target).getSprite().pipelineData[k]); // TODO: are those bangs correct? - sprite.setPipelineData("spriteKey", (isUser ? user! : target).getBattleSpriteKey()); - sprite.setPipelineData("shiny", (isUser ? user : target).shiny); - sprite.setPipelineData("variant", (isUser ? user : target).variant); - sprite.setPipelineData("ignoreFieldPos", true); - spriteSource.on("animationupdate", (_anim, frame) => sprite.setFrame(frame.textureFrame)); - scene.field.add(sprite); - sprites.push(sprite); + if (isUser || !targetSubstitute) { + const sprite = scene.addPokemonSprite(isUser ? user! : target, 0, 0, spriteSource!.texture, spriteSource!.frame.name, true); // TODO: are those bangs correct? + [ "spriteColors", "fusionSpriteColors" ].map(k => sprite.pipelineData[k] = (isUser ? user! : target).getSprite().pipelineData[k]); // TODO: are those bangs correct? + sprite.setPipelineData("spriteKey", (isUser ? user! : target).getBattleSpriteKey()); + sprite.setPipelineData("shiny", (isUser ? user : target).shiny); + sprite.setPipelineData("variant", (isUser ? user : target).variant); + sprite.setPipelineData("ignoreFieldPos", true); + spriteSource.on("animationupdate", (_anim, frame) => sprite.setFrame(frame.textureFrame)); + scene.field.add(sprite); + sprites.push(sprite); + } else { + const sprite = scene.addFieldSprite(spriteSource.x, spriteSource.y, spriteSource.texture); + spriteSource.on("animationupdate", (_anim, frame) => sprite.setFrame(frame.textureFrame)); + scene.field.add(sprite); + sprites.push(sprite); + } } const spriteIndex = isUser ? u++ : t++; const pokemonSprite = sprites[spriteIndex]; const graphicFrameData = frameData.get(frame.target)!.get(spriteIndex)!; // TODO: are the bangs correct? - pokemonSprite.setPosition(graphicFrameData.x, graphicFrameData.y - ((spriteSource.height / 2) * (spriteSource.parentContainer.scale - 1))); + const spriteSourceScale = (isUser || !targetSubstitute) + ? spriteSource.parentContainer.scale + : target.getSpriteScale() * (target.isPlayer() ? 0.5 : 1); + pokemonSprite.setPosition(graphicFrameData.x, graphicFrameData.y - ((spriteSource.height / 2) * (spriteSourceScale - 1))); pokemonSprite.setAngle(graphicFrameData.angle); - pokemonSprite.setScale(graphicFrameData.scaleX * spriteSource.parentContainer.scale, graphicFrameData.scaleY * spriteSource.parentContainer.scale); + pokemonSprite.setScale(graphicFrameData.scaleX * spriteSourceScale, graphicFrameData.scaleY * spriteSourceScale); pokemonSprite.setData("locked", frame.locked); @@ -887,7 +994,7 @@ export abstract class BattleAnim { const setSpritePriority = (priority: integer) => { switch (priority) { case 0: - scene.field.moveBelow(moveSprite as Phaser.GameObjects.GameObject, scene.getEnemyPokemon() || scene.getPlayerPokemon()!); // TODO: is this bang correct? + scene.field.moveBelow(moveSprite as Phaser.GameObjects.GameObject, scene.getNonSwitchedEnemyPokemon() || scene.getNonSwitchedPlayerPokemon()!); // This bang assumes that if (the EnemyPokemon is undefined, then the PlayerPokemon function must return an object), correct assumption? break; case 1: scene.field.moveTo(moveSprite, scene.field.getAll().length - 1); @@ -983,13 +1090,175 @@ export abstract class BattleAnim { } }); } + + private getGraphicFrameDataWithoutTarget(frames: AnimFrame[], targetInitialX: number, targetInitialY: number): Map> { + const ret: Map> = new Map([ + [AnimFrameTarget.GRAPHIC, new Map() ], + [AnimFrameTarget.USER, new Map() ], + [AnimFrameTarget.TARGET, new Map() ] + ]); + + let g = 0; + let u = 0; + let t = 0; + + for (const frame of frames) { + let { x, y } = frame; + const scaleX = (frame.zoomX / 100) * (!frame.mirror ? 1 : -1); + const scaleY = (frame.zoomY / 100); + x += targetInitialX; + y += targetInitialY; + const angle = -frame.angle; + const key = frame.target === AnimFrameTarget.GRAPHIC ? g++ : frame.target === AnimFrameTarget.USER ? u++ : t++; + ret.get(frame.target)?.set(key, { x: x, y: y, scaleX: scaleX, scaleY: scaleY, angle: angle }); + } + + return ret; + } + + /** + * + * @param scene + * @param targetInitialX + * @param targetInitialY + * @param frameTimeMult + * @param frameTimedEventPriority + * - 0 is behind all other sprites (except BG) + * - 1 on top of player field + * - 3 is on top of both fields + * - 5 is on top of player sprite + * @param callback + */ + playWithoutTargets(scene: BattleScene, targetInitialX: number, targetInitialY: number, frameTimeMult: number, frameTimedEventPriority?: 0 | 1 | 3 | 5, callback?: Function) { + const spriteCache: SpriteCache = { + [AnimFrameTarget.GRAPHIC]: [], + [AnimFrameTarget.USER]: [], + [AnimFrameTarget.TARGET]: [] + }; + + const cleanUpAndComplete = () => { + for (const ms of Object.values(spriteCache).flat()) { + if (ms) { + ms.destroy(); + } + } + if (this.bgSprite) { + this.bgSprite.destroy(); + } + if (callback) { + callback(); + } + }; + + if (!scene.moveAnimations && !this.playRegardlessOfIssues) { + return cleanUpAndComplete(); + } + + const anim = this.getAnim(); + + this.srcLine = [ userFocusX, userFocusY, targetFocusX, targetFocusY ]; + this.dstLine = [ 150, 75, targetInitialX, targetInitialY ]; + + let totalFrames = anim!.frames.length; + let frameCount = 0; + + let existingFieldSprites = scene.field.getAll().slice(0); + + scene.tweens.addCounter({ + duration: Utils.getFrameMs(3) * frameTimeMult, + repeat: anim!.frames.length, + onRepeat: () => { + existingFieldSprites = scene.field.getAll().slice(0); + const spriteFrames = anim!.frames[frameCount]; + const frameData = this.getGraphicFrameDataWithoutTarget(anim!.frames[frameCount], targetInitialX, targetInitialY); + let graphicFrameCount = 0; + for (const frame of spriteFrames) { + if (frame.target !== AnimFrameTarget.GRAPHIC) { + console.log("Encounter animations do not support targets"); + continue; + } + + const sprites = spriteCache[AnimFrameTarget.GRAPHIC]; + if (graphicFrameCount === sprites.length) { + const newSprite: Phaser.GameObjects.Sprite = scene.addFieldSprite(0, 0, anim!.graphic, 1); + sprites.push(newSprite); + scene.field.add(newSprite); + } + + const graphicIndex = graphicFrameCount++; + const moveSprite = sprites[graphicIndex]; + if (!isNullOrUndefined(frame.priority)) { + const setSpritePriority = (priority: integer) => { + if (existingFieldSprites.length > priority) { + // Move to specified priority index + const index = scene.field.getIndex(existingFieldSprites[priority]); + scene.field.moveTo(moveSprite, index); + } else { + // Move to top of scene + scene.field.moveTo(moveSprite, scene.field.getAll().length - 1); + } + }; + setSpritePriority(frame.priority); + } + moveSprite.setFrame(frame.graphicFrame); + + const graphicFrameData = frameData.get(frame.target)?.get(graphicIndex); + if (graphicFrameData) { + moveSprite.setPosition(graphicFrameData.x, graphicFrameData.y); + moveSprite.setAngle(graphicFrameData.angle); + moveSprite.setScale(graphicFrameData.scaleX, graphicFrameData.scaleY); + + moveSprite.setAlpha(frame.opacity / 255); + moveSprite.setVisible(frame.visible); + moveSprite.setBlendMode(frame.blendType === AnimBlendType.NORMAL ? Phaser.BlendModes.NORMAL : frame.blendType === AnimBlendType.ADD ? Phaser.BlendModes.ADD : Phaser.BlendModes.DIFFERENCE); + } + } + if (anim?.frameTimedEvents.get(frameCount)) { + for (const event of anim.frameTimedEvents.get(frameCount)!) { + totalFrames = Math.max((anim.frames.length - frameCount) + event.execute(scene, this, frameTimedEventPriority), totalFrames); + } + } + const targets = Utils.getEnumValues(AnimFrameTarget); + for (const i of targets) { + const count = graphicFrameCount; + if (count < spriteCache[i].length) { + const spritesToRemove = spriteCache[i].slice(count, spriteCache[i].length); + for (const sprite of spritesToRemove) { + if (!sprite.getData("locked") as boolean) { + const spriteCacheIndex = spriteCache[i].indexOf(sprite); + spriteCache[i].splice(spriteCacheIndex, 1); + sprite.destroy(); + } + } + } + } + frameCount++; + totalFrames--; + }, + onComplete: () => { + for (const sprite of Object.values(spriteCache).flat()) { + if (sprite && !sprite.getData("locked")) { + sprite.destroy(); + } + } + if (totalFrames) { + scene.tweens.addCounter({ + duration: Utils.getFrameMs(totalFrames), + onComplete: () => cleanUpAndComplete() + }); + } else { + cleanUpAndComplete(); + } + } + }); + } } export class CommonBattleAnim extends BattleAnim { public commonAnim: CommonAnim | null; - constructor(commonAnim: CommonAnim | null, user: Pokemon, target?: Pokemon) { - super(user, target || user); + constructor(commonAnim: CommonAnim | null, user: Pokemon, target?: Pokemon, playOnEmptyField: boolean = false) { + super(user, target || user, playOnEmptyField); this.commonAnim = commonAnim; } @@ -1006,8 +1275,8 @@ export class CommonBattleAnim extends BattleAnim { export class MoveAnim extends BattleAnim { public move: Moves; - constructor(move: Moves, user: Pokemon, target: BattlerIndex) { - super(user, user.scene.getField()[target]); + constructor(move: Moves, user: Pokemon, target: BattlerIndex, playOnEmptyField: boolean = false) { + super(user, user.scene.getField()[target], playOnEmptyField); this.move = move; } @@ -1051,6 +1320,26 @@ export class MoveChargeAnim extends MoveAnim { } } +export class EncounterBattleAnim extends BattleAnim { + public encounterAnim: EncounterAnim; + public oppAnim: boolean; + + constructor(encounterAnim: EncounterAnim, user: Pokemon, target?: Pokemon, oppAnim?: boolean) { + super(user, target ?? user, true); + + this.encounterAnim = encounterAnim; + this.oppAnim = oppAnim ?? false; + } + + getAnim(): AnimConfig | null { + return encounterAnims.get(this.encounterAnim) ?? null; + } + + isOppAnim(): boolean { + return this.oppAnim; + } +} + export async function populateAnims() { const commonAnimNames = Utils.getEnumKeys(CommonAnim).map(k => k.toLowerCase()); const commonAnimMatchNames = commonAnimNames.map(k => k.replace(/\_/g, "")); diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 4685a4fc7e1..3be6562307b 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -3,9 +3,9 @@ import { getPokemonNameWithAffix } from "../messages"; import Pokemon, { MoveResult, HitResult } from "../field/pokemon"; import { StatusEffect } from "./status-effect"; import * as Utils from "../utils"; -import { ChargeAttr, MoveFlags, allMoves } from "./move"; +import { ChargeAttr, MoveFlags, allMoves, MoveCategory, applyMoveAttrs, StatusCategoryOnAllyAttr, HealOnAllyAttr } from "./move"; import { Type } from "./type"; -import { BlockNonDirectDamageAbAttr, FlinchEffectAbAttr, ReverseDrainAbAttr, applyAbAttrs } from "./ability"; +import { BlockNonDirectDamageAbAttr, FlinchEffectAbAttr, ReverseDrainAbAttr, applyAbAttrs, ProtectStatAbAttr } from "./ability"; import { TerrainType } from "./terrain"; import { WeatherType } from "./weather"; import { allAbilities } from "./ability"; @@ -22,6 +22,7 @@ import { MovePhase } from "#app/phases/move-phase"; import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase"; import { ShowAbilityPhase } from "#app/phases/show-ability-phase"; import { StatStageChangePhase, StatStageChangeCallback } from "#app/phases/stat-stage-change-phase"; +import { PokemonAnimType } from "#app/enums/pokemon-anim-type"; export enum BattlerTagLapseType { FAINT, @@ -30,6 +31,7 @@ export enum BattlerTagLapseType { AFTER_MOVE, MOVE_EFFECT, TURN_END, + HIT, CUSTOM } @@ -139,6 +141,18 @@ export abstract class MoveRestrictionBattlerTag extends BattlerTag { */ abstract isMoveRestricted(move: Moves): boolean; + /** + * Checks if this tag is restricting a move based on a user's decisions during the target selection phase + * + * @param {Moves} move {@linkcode Moves} move ID to check restriction for + * @param {Pokemon} user {@linkcode Pokemon} the user of the above move + * @param {Pokemon} target {@linkcode Pokemon} the target of the above move + * @returns {boolean} `false` unless overridden by the child tag + */ + isMoveTargetRestricted(move: Moves, user: Pokemon, target: Pokemon): boolean { + return false; + } + /** * Gets the text to display when the player attempts to select a move that is restricted by this tag. * @@ -391,9 +405,11 @@ export class BeakBlastChargingTag extends BattlerTag { lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { if (lapseType === BattlerTagLapseType.CUSTOM) { const effectPhase = pokemon.scene.getCurrentPhase(); - if (effectPhase instanceof MoveEffectPhase && effectPhase.move.getMove().hasFlag(MoveFlags.MAKES_CONTACT)) { + if (effectPhase instanceof MoveEffectPhase) { const attacker = effectPhase.getPokemon(); - attacker.trySetStatus(StatusEffect.BURN, true, pokemon); + if (effectPhase.move.getMove().checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon)) { + attacker.trySetStatus(StatusEffect.BURN, true, pokemon); + } } return true; } @@ -451,10 +467,14 @@ export class TrappedTag extends BattlerTag { } canAdd(pokemon: Pokemon): boolean { + const source = pokemon.scene.getPokemonById(this.sourceId!)!; + const move = allMoves[this.sourceMove]; + const isGhost = pokemon.isOfType(Type.GHOST); const isTrapped = pokemon.getTag(TrappedTag); + const hasSubstitute = move.hitsSubstitute(source, pokemon); - return !isTrapped && !isGhost; + return !isTrapped && !isGhost && !hasSubstitute; } onAdd(pokemon: Pokemon): void { @@ -516,10 +536,6 @@ export class FlinchedTag extends BattlerTag { applyAbAttrs(FlinchEffectAbAttr, pokemon, null); } - canAdd(pokemon: Pokemon): boolean { - return !pokemon.isMax(); - } - /** * Cancels the Pokemon's next Move on the turn this tag is applied * @param pokemon The {@linkcode Pokemon} with this tag @@ -870,10 +886,6 @@ export class EncoreTag extends BattlerTag { } canAdd(pokemon: Pokemon): boolean { - if (pokemon.isMax()) { - return false; - } - const lastMoves = pokemon.getLastXMoves(1); if (!lastMoves.length) { return false; @@ -1052,19 +1064,11 @@ export class MinimizeTag extends BattlerTag { super(BattlerTagType.MINIMIZED, BattlerTagLapseType.TURN_END, 1, Moves.MINIMIZE); } - canAdd(pokemon: Pokemon): boolean { - return !pokemon.isMax(); - } - onAdd(pokemon: Pokemon): void { super.onAdd(pokemon); } lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { - //If a pokemon dynamaxes they lose minimized status - if (pokemon.isMax()) { - return false; - } return lapseType !== BattlerTagLapseType.CUSTOM || super.lapse(pokemon, lapseType); } @@ -1121,7 +1125,7 @@ export abstract class DamagingTrapTag extends TrappedTag { } canAdd(pokemon: Pokemon): boolean { - return !pokemon.getTag(TrappedTag); + return !pokemon.getTag(TrappedTag) && !pokemon.getTag(BattlerTagType.SUBSTITUTE); } lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { @@ -2007,7 +2011,6 @@ export class FormBlockDamageTag extends BattlerTag { pokemon.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeManualTrigger); } } - /** Provides the additional weather-based effects of the Ice Face ability */ export class IceFaceBlockDamageTag extends FormBlockDamageTag { constructor(tagType: BattlerTagType) { @@ -2055,7 +2058,6 @@ export class StockpilingTag extends BattlerTag { if (defChange) { this.statChangeCounts[Stat.DEF]++; } - if (spDefChange) { this.statChangeCounts[Stat.SPDEF]++; } @@ -2188,6 +2190,74 @@ export class ExposedTag extends BattlerTag { } } +/** + * Tag that prevents HP recovery from held items and move effects. It also blocks the usage of recovery moves. + * Applied by moves: {@linkcode Moves.HEAL_BLOCK | Heal Block (5 turns)}, {@linkcode Moves.PSYCHIC_NOISE | Psychic Noise (2 turns)} + * + * @extends MoveRestrictionBattlerTag + */ +export class HealBlockTag extends MoveRestrictionBattlerTag { + constructor(turnCount: number, sourceMove: Moves) { + super(BattlerTagType.HEAL_BLOCK, [ BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.TURN_END ], turnCount, sourceMove); + } + + onActivation(pokemon: Pokemon): string { + return i18next.t("battle:battlerTagsHealBlock", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }); + } + + /** + * Checks if a move is disabled under Heal Block + * @param {Moves} move {@linkcode Moves} the move ID + * @returns `true` if the move has a TRIAGE_MOVE flag and is a status move + */ + override isMoveRestricted(move: Moves): boolean { + if (allMoves[move].hasFlag(MoveFlags.TRIAGE_MOVE) && allMoves[move].category === MoveCategory.STATUS) { + return true; + } + return false; + } + + /** + * Checks if a move is disabled under Heal Block because of its choice of target + * Implemented b/c of Pollen Puff + * @param {Moves} move {@linkcode Moves} the move ID + * @param {Pokemon} user {@linkcode Pokemon} the move user + * @param {Pokemon} target {@linkcode Pokemon} the target of the move + * @returns `true` if the move cannot be used because the target is an ally + */ + override isMoveTargetRestricted(move: Moves, user: Pokemon, target: Pokemon) { + const moveCategory = new Utils.IntegerHolder(allMoves[move].category); + applyMoveAttrs(StatusCategoryOnAllyAttr, user, target, allMoves[move], moveCategory); + if (allMoves[move].hasAttr(HealOnAllyAttr) && moveCategory.value === MoveCategory.STATUS ) { + return true; + } + return false; + } + + /** + * Uses DisabledTag's selectionDeniedText() message + */ + override selectionDeniedText(pokemon: Pokemon, move: Moves): string { + return i18next.t("battle:moveDisabled", { moveName: allMoves[move].name }); + } + + /** + * @override + * @param {Pokemon} pokemon {@linkcode Pokemon} attempting to use the restricted move + * @param {Moves} move {@linkcode Moves} ID of the move being interrupted + * @returns {string} text to display when the move is interrupted + */ + override interruptedText(pokemon: Pokemon, move: Moves): string { + return i18next.t("battle:disableInterruptedMove", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), moveName: allMoves[move].name }); + } + + override onRemove(pokemon: Pokemon): void { + super.onRemove(pokemon); + + pokemon.scene.queueMessage(i18next.t("battle:battlerTagsHealBlockOnRemove", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }), null, false, null); + } +} + /** * Tag that doubles the type effectiveness of Fire-type moves. * @extends BattlerTag @@ -2211,6 +2281,132 @@ export class TarShotTag extends BattlerTag { } } +export class SubstituteTag extends BattlerTag { + /** The substitute's remaining HP. If HP is depleted, the Substitute fades. */ + public hp: number; + /** A reference to the sprite representing the Substitute doll */ + public sprite: Phaser.GameObjects.Sprite; + /** Is the source Pokemon "in focus," i.e. is it fully visible on the field? */ + public sourceInFocus: boolean; + + constructor(sourceMove: Moves, sourceId: integer) { + super(BattlerTagType.SUBSTITUTE, [BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.AFTER_MOVE, BattlerTagLapseType.HIT], 0, sourceMove, sourceId, true); + } + + /** Sets the Substitute's HP and queues an on-add battle animation that initializes the Substitute's sprite. */ + onAdd(pokemon: Pokemon): void { + this.hp = Math.floor(pokemon.scene.getPokemonById(this.sourceId!)!.getMaxHp() / 4); + this.sourceInFocus = false; + + // Queue battle animation and message + pokemon.scene.triggerPokemonBattleAnim(pokemon, PokemonAnimType.SUBSTITUTE_ADD); + pokemon.scene.queueMessage(i18next.t("battlerTags:substituteOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }), 1500); + + // Remove any binding effects from the user + pokemon.findAndRemoveTags(tag => tag instanceof DamagingTrapTag); + } + + /** Queues an on-remove battle animation that removes the Substitute's sprite. */ + onRemove(pokemon: Pokemon): void { + // Only play the animation if the cause of removal isn't from the source's own move + if (!this.sourceInFocus) { + pokemon.scene.triggerPokemonBattleAnim(pokemon, PokemonAnimType.SUBSTITUTE_REMOVE, [this.sprite]); + } else { + this.sprite.destroy(); + } + pokemon.scene.queueMessage(i18next.t("battlerTags:substituteOnRemove", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })); + } + + lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { + switch (lapseType) { + case BattlerTagLapseType.PRE_MOVE: + this.onPreMove(pokemon); + break; + case BattlerTagLapseType.AFTER_MOVE: + this.onAfterMove(pokemon); + break; + case BattlerTagLapseType.HIT: + this.onHit(pokemon); + break; + } + return lapseType !== BattlerTagLapseType.CUSTOM; // only remove this tag on custom lapse + } + + /** Triggers an animation that brings the Pokemon into focus before it uses a move */ + onPreMove(pokemon: Pokemon): void { + pokemon.scene.triggerPokemonBattleAnim(pokemon, PokemonAnimType.SUBSTITUTE_PRE_MOVE, [this.sprite]); + this.sourceInFocus = true; + } + + /** Triggers an animation that brings the Pokemon out of focus after it uses a move */ + onAfterMove(pokemon: Pokemon): void { + pokemon.scene.triggerPokemonBattleAnim(pokemon, PokemonAnimType.SUBSTITUTE_POST_MOVE, [this.sprite]); + this.sourceInFocus = false; + } + + /** If the Substitute redirects damage, queue a message to indicate it. */ + onHit(pokemon: Pokemon): void { + const moveEffectPhase = pokemon.scene.getCurrentPhase(); + if (moveEffectPhase instanceof MoveEffectPhase) { + const attacker = moveEffectPhase.getUserPokemon()!; + const move = moveEffectPhase.move.getMove(); + const firstHit = (attacker.turnData.hitCount === attacker.turnData.hitsLeft); + + if (firstHit && move.hitsSubstitute(attacker, pokemon)) { + pokemon.scene.queueMessage(i18next.t("battlerTags:substituteOnHit", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })); + } + } + } + + /** + * When given a battler tag or json representing one, load the data for it. + * @param {BattlerTag | any} source A battler tag + */ + loadTag(source: BattlerTag | any): void { + super.loadTag(source); + this.hp = source.hp; + } +} + +/** + * Tag that adds extra post-summon effects to a battle for a specific Pokemon. + * These post-summon effects are performed through {@linkcode Pokemon.mysteryEncounterBattleEffects}, + * and can be used to unshift special phases, etc. + * Currently used only in MysteryEncounters to provide start of fight stat buffs. + */ +export class MysteryEncounterPostSummonTag extends BattlerTag { + constructor() { + super(BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON, BattlerTagLapseType.CUSTOM, 1); + } + + /** Event when tag is added */ + onAdd(pokemon: Pokemon): void { + super.onAdd(pokemon); + } + + /** Performs post-summon effects through {@linkcode Pokemon.mysteryEncounterBattleEffects} */ + lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { + const ret = super.lapse(pokemon, lapseType); + + if (lapseType === BattlerTagLapseType.CUSTOM) { + const cancelled = new Utils.BooleanHolder(false); + applyAbAttrs(ProtectStatAbAttr, pokemon, cancelled); + if (!cancelled.value) { + if (pokemon.mysteryEncounterBattleEffects) { + pokemon.mysteryEncounterBattleEffects(pokemon); + } + } + } + + return ret; + } + + /** Event when tag is removed */ + onRemove(pokemon: Pokemon): void { + super.onRemove(pokemon); + } +} + /** * Retrieves a {@linkcode BattlerTag} based on the provided tag type, turn count, source move, and source ID. * @@ -2370,6 +2566,12 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source return new ThroatChoppedTag(); case BattlerTagType.GORILLA_TACTICS: return new GorillaTacticsTag(); + case BattlerTagType.SUBSTITUTE: + return new SubstituteTag(sourceMove, sourceId); + case BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON: + return new MysteryEncounterPostSummonTag(); + case BattlerTagType.HEAL_BLOCK: + return new HealBlockTag(turnCount, sourceMove); case BattlerTagType.NONE: default: return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId); diff --git a/src/data/challenge.ts b/src/data/challenge.ts index 1afbfc932dc..f0a928a78fc 100644 --- a/src/data/challenge.ts +++ b/src/data/challenge.ts @@ -447,6 +447,30 @@ export class SingleGenerationChallenge extends Challenge { applyFixedBattle(waveIndex: Number, battleConfig: FixedBattleConfig): boolean { let trainerTypes: TrainerType[] = []; switch (waveIndex) { + case 35: + trainerTypes = [ TrainerType.ROCKET_GRUNT, TrainerType.ROCKET_GRUNT, Utils.randSeedItem([ TrainerType.MAGMA_GRUNT, TrainerType.AQUA_GRUNT ]), TrainerType.GALACTIC_GRUNT, TrainerType.PLASMA_GRUNT, TrainerType.FLARE_GRUNT, Utils.randSeedItem([ TrainerType.AETHER_GRUNT, TrainerType.SKULL_GRUNT ]), TrainerType.MACRO_GRUNT, TrainerType.STAR_GRUNT ]; + break; + case 62: + trainerTypes = [ TrainerType.ROCKET_GRUNT, TrainerType.ROCKET_GRUNT, Utils.randSeedItem([ TrainerType.MAGMA_GRUNT, TrainerType.AQUA_GRUNT ]), TrainerType.GALACTIC_GRUNT, TrainerType.PLASMA_GRUNT, TrainerType.FLARE_GRUNT, Utils.randSeedItem([ TrainerType.AETHER_GRUNT, TrainerType.SKULL_GRUNT ]), TrainerType.MACRO_GRUNT, TrainerType.STAR_GRUNT ]; + break; + case 64: + trainerTypes = [ TrainerType.ROCKET_GRUNT, TrainerType.ROCKET_GRUNT, Utils.randSeedItem([ TrainerType.MAGMA_GRUNT, TrainerType.AQUA_GRUNT ]), TrainerType.GALACTIC_GRUNT, TrainerType.PLASMA_GRUNT, TrainerType.FLARE_GRUNT, Utils.randSeedItem([ TrainerType.AETHER_GRUNT, TrainerType.SKULL_GRUNT ]), TrainerType.MACRO_GRUNT, TrainerType.STAR_GRUNT ]; + break; + case 66: + trainerTypes = [ Utils.randSeedItem([ TrainerType.ARCHER, TrainerType.ARIANA, TrainerType.PROTON, TrainerType.PETREL ]), Utils.randSeedItem([ TrainerType.ARCHER, TrainerType.ARIANA, TrainerType.PROTON, TrainerType.PETREL ]), Utils.randSeedItem([ TrainerType.TABITHA, TrainerType.COURTNEY, TrainerType.MATT, TrainerType.SHELLY ]), Utils.randSeedItem([ TrainerType.JUPITER, TrainerType.MARS, TrainerType.SATURN ]), Utils.randSeedItem([ TrainerType.ZINZOLIN, TrainerType.ROOD ]), Utils.randSeedItem([ TrainerType.XEROSIC, TrainerType.BRYONY ]), Utils.randSeedItem([ TrainerType.FABA, TrainerType.PLUMERIA ]), TrainerType.OLEANA, Utils.randSeedItem([ TrainerType.GIACOMO, TrainerType.MELA, TrainerType.ATTICUS, TrainerType.ORTEGA, TrainerType.ERI ]) ]; + break; + case 112: + trainerTypes = [ TrainerType.ROCKET_GRUNT, TrainerType.ROCKET_GRUNT, Utils.randSeedItem([ TrainerType.MAGMA_GRUNT, TrainerType.AQUA_GRUNT ]), TrainerType.GALACTIC_GRUNT, TrainerType.PLASMA_GRUNT, TrainerType.FLARE_GRUNT, Utils.randSeedItem([ TrainerType.AETHER_GRUNT, TrainerType.SKULL_GRUNT ]), TrainerType.MACRO_GRUNT, TrainerType.STAR_GRUNT ]; + break; + case 114: + trainerTypes = [ Utils.randSeedItem([ TrainerType.ARCHER, TrainerType.ARIANA, TrainerType.PROTON, TrainerType.PETREL ]), Utils.randSeedItem([ TrainerType.ARCHER, TrainerType.ARIANA, TrainerType.PROTON, TrainerType.PETREL ]), Utils.randSeedItem([ TrainerType.TABITHA, TrainerType.COURTNEY, TrainerType.MATT, TrainerType.SHELLY ]), Utils.randSeedItem([ TrainerType.JUPITER, TrainerType.MARS, TrainerType.SATURN ]), Utils.randSeedItem([ TrainerType.ZINZOLIN, TrainerType.ROOD ]), Utils.randSeedItem([ TrainerType.XEROSIC, TrainerType.BRYONY ]), Utils.randSeedItem([ TrainerType.FABA, TrainerType.PLUMERIA ]), TrainerType.OLEANA, Utils.randSeedItem([ TrainerType.GIACOMO, TrainerType.MELA, TrainerType.ATTICUS, TrainerType.ORTEGA, TrainerType.ERI ]) ]; + break; + case 115: + trainerTypes = [ TrainerType.ROCKET_BOSS_GIOVANNI_1, TrainerType.ROCKET_BOSS_GIOVANNI_1, Utils.randSeedItem([ TrainerType.MAXIE, TrainerType.ARCHIE ]), TrainerType.CYRUS, TrainerType.GHETSIS, TrainerType.LYSANDRE, Utils.randSeedItem([ TrainerType.LUSAMINE, TrainerType.GUZMA ]), TrainerType.ROSE, TrainerType.PENNY ]; + break; + case 165: + trainerTypes = [ TrainerType.ROCKET_BOSS_GIOVANNI_2, TrainerType.ROCKET_BOSS_GIOVANNI_2, Utils.randSeedItem([ TrainerType.MAXIE_2, TrainerType.ARCHIE_2 ]), TrainerType.CYRUS_2, TrainerType.GHETSIS_2, TrainerType.LYSANDRE_2, Utils.randSeedItem([ TrainerType.LUSAMINE_2, TrainerType.GUZMA_2 ]), TrainerType.ROSE_2, TrainerType.PENNY_2 ]; + break; case 182: trainerTypes = [ TrainerType.LORELEI, TrainerType.WILL, TrainerType.SIDNEY, TrainerType.AARON, TrainerType.SHAUNTAL, TrainerType.MALVA, Utils.randSeedItem([ TrainerType.HALA, TrainerType.MOLAYNE ]), TrainerType.MARNIE_ELITE, TrainerType.RIKA ]; break; diff --git a/src/data/dialogue.ts b/src/data/dialogue.ts index 355f05523d1..499cd106cf9 100644 --- a/src/data/dialogue.ts +++ b/src/data/dialogue.ts @@ -837,11 +837,15 @@ export const trainerTypeDialogue: TrainerTypeDialogue = { "dialogue:macro_grunt.encounter.1", "dialogue:macro_grunt.encounter.2", "dialogue:macro_grunt.encounter.3", + "dialogue:macro_grunt.encounter.4", + "dialogue:macro_grunt.encounter.5", ], victory: [ "dialogue:macro_grunt.victory.1", "dialogue:macro_grunt.victory.2", "dialogue:macro_grunt.victory.3", + "dialogue:macro_grunt.victory.4", + "dialogue:macro_grunt.victory.5", ] } ], @@ -859,6 +863,84 @@ export const trainerTypeDialogue: TrainerTypeDialogue = { ] } ], + [TrainerType.STAR_GRUNT]: [ + { + encounter: [ + "dialogue:star_grunt.encounter.1", + "dialogue:star_grunt.encounter.2", + "dialogue:star_grunt.encounter.3", + "dialogue:star_grunt.encounter.4", + "dialogue:star_grunt.encounter.5", + ], + victory: [ + "dialogue:star_grunt.victory.1", + "dialogue:star_grunt.victory.2", + "dialogue:star_grunt.victory.3", + "dialogue:star_grunt.victory.4", + "dialogue:star_grunt.victory.5", + ] + } + ], + [TrainerType.GIACOMO]: [ + { + encounter: [ + "dialogue:giacomo.encounter.1", + "dialogue:giacomo.encounter.2", + ], + victory: [ + "dialogue:giacomo.victory.1", + "dialogue:giacomo.victory.2", + ] + } + ], + [TrainerType.MELA]: [ + { + encounter: [ + "dialogue:mela.encounter.1", + "dialogue:mela.encounter.2", + ], + victory: [ + "dialogue:mela.victory.1", + "dialogue:mela.victory.2", + ] + } + ], + [TrainerType.ATTICUS]: [ + { + encounter: [ + "dialogue:atticus.encounter.1", + "dialogue:atticus.encounter.2", + ], + victory: [ + "dialogue:atticus.victory.1", + "dialogue:atticus.victory.2", + ] + } + ], + [TrainerType.ORTEGA]: [ + { + encounter: [ + "dialogue:ortega.encounter.1", + "dialogue:ortega.encounter.2", + ], + victory: [ + "dialogue:ortega.victory.1", + "dialogue:ortega.victory.2", + ] + } + ], + [TrainerType.ERI]: [ + { + encounter: [ + "dialogue:eri.encounter.1", + "dialogue:eri.encounter.2", + ], + victory: [ + "dialogue:eri.victory.1", + "dialogue:eri.victory.2", + ] + } + ], [TrainerType.ROCKET_BOSS_GIOVANNI_1]: [ { encounter: [ @@ -1093,6 +1175,162 @@ export const trainerTypeDialogue: TrainerTypeDialogue = { ] } ], + [TrainerType.PENNY]: [ + { + encounter: [ + "dialogue:star_boss_penny_1.encounter.1" + ], + victory: [ + "dialogue:star_boss_penny_1.victory.1" + ], + defeat: [ + "dialogue:star_boss_penny_1.defeat.1" + ] + } + ], + [TrainerType.PENNY_2]: [ + { + encounter: [ + "dialogue:star_boss_penny_2.encounter.1" + ], + victory: [ + "dialogue:star_boss_penny_2.victory.1" + ], + defeat: [ + "dialogue:star_boss_penny_2.defeat.1" + ] + } + ], + [TrainerType.BUCK]: [ + { + encounter: [ + "dialogue:stat_trainer_buck.encounter.1", + "dialogue:stat_trainer_buck.encounter.2" + ], + victory: [ + "dialogue:stat_trainer_buck.victory.1", + "dialogue:stat_trainer_buck.victory.2" + ], + defeat: [ + "dialogue:stat_trainer_buck.defeat.1", + "dialogue:stat_trainer_buck.defeat.2" + ] + } + ], + [TrainerType.CHERYL]: [ + { + encounter: [ + "dialogue:stat_trainer_cheryl.encounter.1", + "dialogue:stat_trainer_cheryl.encounter.2" + ], + victory: [ + "dialogue:stat_trainer_cheryl.victory.1", + "dialogue:stat_trainer_cheryl.victory.2" + ], + defeat: [ + "dialogue:stat_trainer_cheryl.defeat.1", + "dialogue:stat_trainer_cheryl.defeat.2" + ] + } + ], + [TrainerType.MARLEY]: [ + { + encounter: [ + "dialogue:stat_trainer_marley.encounter.1", + "dialogue:stat_trainer_marley.encounter.2" + ], + victory: [ + "dialogue:stat_trainer_marley.victory.1", + "dialogue:stat_trainer_marley.victory.2" + ], + defeat: [ + "dialogue:stat_trainer_marley.defeat.1", + "dialogue:stat_trainer_marley.defeat.2" + ] + } + ], + [TrainerType.MIRA]: [ + { + encounter: [ + "dialogue:stat_trainer_mira.encounter.1", + "dialogue:stat_trainer_mira.encounter.2" + ], + victory: [ + "dialogue:stat_trainer_mira.victory.1", + "dialogue:stat_trainer_mira.victory.2" + ], + defeat: [ + "dialogue:stat_trainer_mira.defeat.1", + "dialogue:stat_trainer_mira.defeat.2" + ] + } + ], + [TrainerType.RILEY]: [ + { + encounter: [ + "dialogue:stat_trainer_riley.encounter.1", + "dialogue:stat_trainer_riley.encounter.2" + ], + victory: [ + "dialogue:stat_trainer_riley.victory.1", + "dialogue:stat_trainer_riley.victory.2" + ], + defeat: [ + "dialogue:stat_trainer_riley.defeat.1", + "dialogue:stat_trainer_riley.defeat.2" + ] + } + ], + [TrainerType.VICTOR]: [ + { + encounter: [ + "dialogue:winstrates_victor.encounter.1", + ], + victory: [ + "dialogue:winstrates_victor.victory.1" + ], + } + ], + [TrainerType.VICTORIA]: [ + { + encounter: [ + "dialogue:winstrates_victoria.encounter.1", + ], + victory: [ + "dialogue:winstrates_victoria.victory.1" + ], + } + ], + [TrainerType.VIVI]: [ + { + encounter: [ + "dialogue:winstrates_vivi.encounter.1", + ], + victory: [ + "dialogue:winstrates_vivi.victory.1" + ], + } + ], + [TrainerType.VICKY]: [ + { + encounter: [ + "dialogue:winstrates_vicky.encounter.1", + ], + victory: [ + "dialogue:winstrates_vicky.victory.1" + ], + } + ], + [TrainerType.VITO]: [ + { + encounter: [ + "dialogue:winstrates_vito.encounter.1", + ], + victory: [ + "dialogue:winstrates_vito.victory.1" + ], + } + ], [TrainerType.BROCK]: { encounter: [ "dialogue:brock.encounter.1", diff --git a/src/data/egg-moves.ts b/src/data/egg-moves.ts index b516238c46e..3e58f993df2 100644 --- a/src/data/egg-moves.ts +++ b/src/data/egg-moves.ts @@ -264,7 +264,7 @@ export const speciesEggMoves = { [Species.PANPOUR]: [ Moves.NASTY_PLOT, Moves.ENERGY_BALL, Moves.EARTH_POWER, Moves.STEAM_ERUPTION ], [Species.MUNNA]: [ Moves.COSMIC_POWER, Moves.AURA_SPHERE, Moves.EARTH_POWER, Moves.MYSTICAL_POWER ], [Species.PIDOVE]: [ Moves.GUNK_SHOT, Moves.TIDY_UP, Moves.FLOATY_FALL, Moves.TRIPLE_ARROWS ], - [Species.BLITZLE]: [ Moves.HIGH_HORSEPOWER, Moves.THUNDEROUS_KICK, Moves.FLARE_BLITZ, Moves.VOLT_TACKLE ], + [Species.BLITZLE]: [ Moves.HORN_LEECH, Moves.SWORDS_DANCE, Moves.FLARE_BLITZ, Moves.BOLT_STRIKE ], [Species.ROGGENROLA]: [ Moves.BODY_PRESS, Moves.CURSE, Moves.SHORE_UP, Moves.DIAMOND_STORM ], [Species.WOOBAT]: [ Moves.ESPER_WING, Moves.STORED_POWER, Moves.MYSTICAL_FIRE, Moves.OBLIVION_WING ], [Species.DRILBUR]: [ Moves.IRON_HEAD, Moves.MOUNTAIN_GALE, Moves.SHIFT_GEAR, Moves.THOUSAND_ARROWS ], diff --git a/src/data/egg.ts b/src/data/egg.ts index 1cd5c65fc18..b37240a2028 100644 --- a/src/data/egg.ts +++ b/src/data/egg.ts @@ -1,6 +1,6 @@ import BattleScene from "../battle-scene"; import PokemonSpecies, { getPokemonSpecies, speciesStarters } from "./pokemon-species"; -import { VariantTier } from "../enums/variant-tiers"; +import { VariantTier } from "../enums/variant-tier"; import * as Utils from "../utils"; import Overrides from "#app/overrides"; import { pokemonPrevolutions } from "./pokemon-evolutions"; @@ -61,7 +61,10 @@ export interface IEggOptions { /** Defines if the egg will hatch with the hidden ability of this species. * If no hidden ability exist, a random one will get choosen. */ - overrideHiddenAbility?: boolean + overrideHiddenAbility?: boolean, + + /** Can customize the message displayed for where the egg was obtained */ + eggDescriptor?: string; } export class Egg { @@ -83,6 +86,8 @@ export class Egg { private _overrideHiddenAbility: boolean; + private _eggDescriptor?: string; + //// // #endregion //// @@ -173,7 +178,7 @@ export class Egg { // be done because species with no variants get filtered at rollSpecies but if the // species is set via options or the legendary gacha pokemon gets choosen the check never happens if (this._species && !getPokemonSpecies(this._species).hasVariants()) { - this._variantTier = VariantTier.COMMON; + this._variantTier = VariantTier.STANDARD; } // Needs this._tier so it needs to be generated afer the tier override if bought from same species this._eggMoveIndex = eggOptions?.eggMoveIndex ?? this.rollEggMoveIndex(); @@ -191,6 +196,8 @@ export class Egg { } else { // For legacy eggs without scene generateEggProperties(eggOptions); } + + this._eggDescriptor = eggOptions?.eggDescriptor; } //// @@ -292,13 +299,15 @@ export class Egg { public getEggTypeDescriptor(scene: BattleScene): string { switch (this.sourceType) { case EggSourceType.SAME_SPECIES_EGG: - return i18next.t("egg:sameSpeciesEgg", { species: getPokemonSpecies(this._species).getName()}); + return this._eggDescriptor ?? i18next.t("egg:sameSpeciesEgg", { species: getPokemonSpecies(this._species).getName()}); case EggSourceType.GACHA_LEGENDARY: - return `${i18next.t("egg:gachaTypeLegendary")} (${getPokemonSpecies(getLegendaryGachaSpeciesForTimestamp(scene, this.timestamp)).getName()})`; + return this._eggDescriptor ?? `${i18next.t("egg:gachaTypeLegendary")} (${getPokemonSpecies(getLegendaryGachaSpeciesForTimestamp(scene, this.timestamp)).getName()})`; case EggSourceType.GACHA_SHINY: - return i18next.t("egg:gachaTypeShiny"); + return this._eggDescriptor ?? i18next.t("egg:gachaTypeShiny"); case EggSourceType.GACHA_MOVE: - return i18next.t("egg:gachaTypeMove"); + return this._eggDescriptor ?? i18next.t("egg:gachaTypeMove"); + case EggSourceType.EVENT: + return this._eggDescriptor ?? i18next.t("egg:eventType"); default: console.warn("getEggTypeDescriptor case not defined. Returning default empty string"); return ""; @@ -485,12 +494,12 @@ export class Egg { // place but I don't want to touch the pokemon class. private rollVariant(): VariantTier { if (!this.isShiny) { - return VariantTier.COMMON; + return VariantTier.STANDARD; } const rand = Utils.randSeedInt(10); if (rand >= 4) { - return VariantTier.COMMON; // 6/10 + return VariantTier.STANDARD; // 6/10 } else if (rand >= 1) { return VariantTier.RARE; // 3/10 } else { diff --git a/src/data/move.ts b/src/data/move.ts index 650725b311b..86139a22adf 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -1,16 +1,15 @@ -import { ChargeAnim, MoveChargeAnim, initMoveAnim, loadMoveAnimAssets } from "./battle-anims"; -import { EncoreTag, GulpMissileTag, HelpingHandTag, SemiInvulnerableTag, ShellTrapTag, StockpilingTag, TrappedTag, TypeBoostTag } from "./battler-tags"; +import { ChargeAnim, initMoveAnim, loadMoveAnimAssets, MoveChargeAnim } from "./battle-anims"; +import { EncoreTag, GulpMissileTag, HelpingHandTag, SemiInvulnerableTag, ShellTrapTag, StockpilingTag, SubstituteTag, TrappedTag, TypeBoostTag } from "./battler-tags"; import { getPokemonNameWithAffix } from "../messages"; import Pokemon, { AttackMoveResult, EnemyPokemon, HitResult, MoveResult, PlayerPokemon, PokemonMove, TurnMove } from "../field/pokemon"; -import { StatusEffect, getStatusEffectHealText, isNonVolatileStatusEffect, getNonVolatileStatusEffects } from "./status-effect"; +import { getNonVolatileStatusEffects, getStatusEffectHealText, isNonVolatileStatusEffect, StatusEffect } from "./status-effect"; import { getTypeDamageMultiplier, Type } from "./type"; -import { Constructor } from "#app/utils"; +import { Constructor, NumberHolder } from "#app/utils"; import * as Utils from "../utils"; import { WeatherType } from "./weather"; import { ArenaTagSide, ArenaTrapTag, WeakenMoveTypeTag } from "./arena-tag"; -import { UnswappableAbilityAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, BlockRecoilDamageAttr, BlockOneHitKOAbAttr, IgnoreContactAbAttr, MaxMultiHitAbAttr, applyAbAttrs, BlockNonDirectDamageAbAttr, MoveAbilityBypassAbAttr, ReverseDrainAbAttr, FieldPreventExplosiveMovesAbAttr, ForceSwitchOutImmunityAbAttr, BlockItemTheftAbAttr, applyPostAttackAbAttrs, ConfusionOnStatusEffectAbAttr, HealFromBerryUseAbAttr, IgnoreProtectOnContactAbAttr, IgnoreMoveEffectsAbAttr, applyPreDefendAbAttrs, MoveEffectChanceMultiplierAbAttr, WonderSkinAbAttr, applyPreAttackAbAttrs, MoveTypeChangeAbAttr, UserFieldMoveTypePowerBoostAbAttr, FieldMoveTypePowerBoostAbAttr, AllyMoveCategoryPowerBoostAbAttr, VariableMovePowerAbAttr } from "./ability"; -import { allAbilities } from "./ability"; -import { PokemonHeldItemModifier, BerryModifier, PreserveBerryModifier, PokemonMoveAccuracyBoosterModifier, AttackTypeBoosterModifier, PokemonMultiHitModifier } from "../modifier/modifier"; +import { allAbilities, AllyMoveCategoryPowerBoostAbAttr, applyAbAttrs, applyPostAttackAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, BlockItemTheftAbAttr, BlockNonDirectDamageAbAttr, BlockOneHitKOAbAttr, BlockRecoilDamageAttr, ConfusionOnStatusEffectAbAttr, FieldMoveTypePowerBoostAbAttr, FieldPreventExplosiveMovesAbAttr, ForceSwitchOutImmunityAbAttr, HealFromBerryUseAbAttr, IgnoreContactAbAttr, IgnoreMoveEffectsAbAttr, IgnoreProtectOnContactAbAttr, MaxMultiHitAbAttr, MoveAbilityBypassAbAttr, MoveEffectChanceMultiplierAbAttr, MoveTypeChangeAbAttr, ReverseDrainAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, UnswappableAbilityAbAttr, UserFieldMoveTypePowerBoostAbAttr, VariableMovePowerAbAttr, WonderSkinAbAttr } from "./ability"; +import { AttackTypeBoosterModifier, BerryModifier, PokemonHeldItemModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, PreserveBerryModifier } from "../modifier/modifier"; import { BattlerIndex, BattleType } from "../battle"; import { TerrainType } from "./terrain"; import { ModifierPoolType } from "#app/modifier/modifier-type"; @@ -25,7 +24,7 @@ import { Biome } from "#enums/biome"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import { MoveUsedEvent } from "#app/events/battle-scene"; -import { Stat, type BattleStat, type EffectiveStat, BATTLE_STATS, EFFECTIVE_STATS, getStatKey } from "#app/enums/stat"; +import { BATTLE_STATS, type BattleStat, EFFECTIVE_STATS, type EffectiveStat, getStatKey, Stat } from "#app/enums/stat"; import { PartyStatusCurePhase } from "#app/phases/party-status-cure-phase"; import { BattleEndPhase } from "#app/phases/battle-end-phase"; import { MoveEndPhase } from "#app/phases/move-end-phase"; @@ -36,7 +35,6 @@ 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 { NumberHolder } from "#app/utils"; import { GameMode } from "#app/game-mode"; import { applyChallenges, ChallengeType } from "./challenge"; @@ -115,9 +113,11 @@ export enum MoveFlags { TRIAGE_MOVE = 1 << 15, IGNORE_ABILITIES = 1 << 16, /** Enables all hits of a multi-hit move to be accuracy checked individually */ - CHECK_ALL_HITS = 1 << 17, + CHECK_ALL_HITS = 1 << 17, + /** Indicates a move is able to bypass its target's Substitute (if the target has one) */ + IGNORE_SUBSTITUTE = 1 << 18, /** Indicates a move is able to be redirected to allies in a double battle if the attacker faints */ - REDIRECT_COUNTER = 1 << 18, + REDIRECT_COUNTER = 1 << 19, } type MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => boolean; @@ -333,6 +333,22 @@ export default class Move implements Localizable { return false; } + /** + * Checks if the move would hit its target's Substitute instead of the target itself. + * @param user The {@linkcode Pokemon} using this move + * @param target The {@linkcode Pokemon} targeted by this move + * @returns `true` if the move can bypass the target's Substitute; `false` otherwise. + */ + hitsSubstitute(user: Pokemon, target: Pokemon | null): boolean { + if (this.moveTarget === MoveTarget.USER || !target?.getTag(BattlerTagType.SUBSTITUTE)) { + return false; + } + + return !user.hasAbility(Abilities.INFILTRATOR) + && !this.hasFlag(MoveFlags.SOUND_BASED) + && !this.hasFlag(MoveFlags.IGNORE_SUBSTITUTE); + } + /** * Adds a move condition to the move * @param condition {@linkcode MoveCondition} or {@linkcode MoveConditionFunc}, appends to conditions array a new MoveCondition object @@ -576,6 +592,17 @@ export default class Move implements Localizable { return this; } + /** + * Sets the {@linkcode MoveFlags.IGNORE_SUBSTITUTE} flag for the calling Move + * @param ignoresSubstitute The value (boolean) to set the flag to + * example: @see {@linkcode Moves.WHIRLWIND} + * @returns The {@linkcode Move} that called this function + */ + ignoresSubstitute(ignoresSubstitute: boolean = true): this { + this.setFlag(MoveFlags.IGNORE_SUBSTITUTE, ignoresSubstitute); + return this; + } + /** * Sets the {@linkcode MoveFlags.REDIRECT_COUNTER} flag for the calling Move * @param redirectCounter The value (boolean) to set the flag to @@ -598,7 +625,7 @@ 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)) { + if (user.hasAbilityWithAttr(IgnoreContactAbAttr) || this.hitsSubstitute(user, target)) { return false; } break; @@ -612,8 +639,8 @@ export default class Move implements Localizable { } break; case MoveFlags.IGNORE_PROTECT: - if (user.hasAbilityWithAttr(IgnoreProtectOnContactAbAttr) && - this.checkFlag(MoveFlags.MAKES_CONTACT, user, target)) { + if (user.hasAbilityWithAttr(IgnoreProtectOnContactAbAttr) + && this.checkFlag(MoveFlags.MAKES_CONTACT, user, null)) { return true; } break; @@ -784,10 +811,6 @@ export default class Move implements Localizable { power.value *= typeBoost.boostValue; } - if (source.scene.arena.getTerrainType() === TerrainType.GRASSY && target.isGrounded() && this.type === Type.GROUND && this.moveTarget === MoveTarget.ALL_NEAR_OTHERS) { - power.value /= 2; - } - applyMoveAttrs(VariablePowerAttr, source, target, this, power); source.scene.applyModifiers(PokemonMultiHitModifier, source.isPlayer(), source, new Utils.IntegerHolder(0), power); @@ -1446,6 +1469,58 @@ export class HalfSacrificialAttr extends MoveEffectAttr { } } +/** + * Attribute to put in a {@link https://bulbapedia.bulbagarden.net/wiki/Substitute_(doll) | Substitute Doll} + * for the user. + * @extends MoveEffectAttr + * @see {@linkcode apply} + */ +export class AddSubstituteAttr extends MoveEffectAttr { + constructor() { + super(true); + } + + /** + * Removes 1/4 of the user's maximum HP (rounded down) to create a substitute for the user + * @param user the {@linkcode Pokemon} that used the move. + * @param target n/a + * @param move the {@linkcode Move} with this attribute. + * @param args n/a + * @returns true if the attribute successfully applies, false otherwise + */ + apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + if (!super.apply(user, target, move, args)) { + return false; + } + + const hpCost = Math.floor(user.getMaxHp() / 4); + user.damageAndUpdate(hpCost, HitResult.OTHER, false, true, true); + user.addTag(BattlerTagType.SUBSTITUTE, 0, move.id, user.id); + return true; + } + + getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { + if (user.isBoss()) { + return -10; + } + return 5; + } + + getCondition(): MoveConditionFunc { + return (user, target, move) => !user.getTag(SubstituteTag) && user.hp > Math.floor(user.getMaxHp() / 4) && user.getMaxHp() > 1; + } + + getFailedText(user: Pokemon, target: Pokemon, move: Move, cancelled: Utils.BooleanHolder): string | null { + if (user.getTag(SubstituteTag)) { + return i18next.t("moveTriggers:substituteOnOverlap", { pokemonName: getPokemonNameWithAffix(user) }); + } else if (user.hp <= Math.floor(user.getMaxHp() / 4) || user.getMaxHp() === 1) { + return i18next.t("moveTriggers:substituteNotEnoughHp"); + } else { + return i18next.t("battle:attackFailed"); + } + } +} + export enum MultiHitType { _2, _2_TO_5, @@ -1949,6 +2024,10 @@ export class StatusEffectAttr extends MoveEffectAttr { } apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + if (!this.selfTarget && move.hitsSubstitute(user, target)) { + return false; + } + const moveChance = this.getMoveChance(user, target, move, this.selfTarget, true); const statusCheck = moveChance < 0 || moveChance === 100 || user.randSeedInt(100) < moveChance; if (statusCheck) { @@ -2048,11 +2127,14 @@ export class StealHeldItemChanceAttr extends MoveEffectAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise { return new Promise(resolve => { + if (move.hitsSubstitute(user, target)) { + return resolve(false); + } const rand = Phaser.Math.RND.realInRange(0, 1); if (rand >= this.chance) { return resolve(false); } - const heldItems = this.getTargetHeldItems(target).filter(i => i.isTransferrable); + const heldItems = this.getTargetHeldItems(target).filter(i => i.isTransferable); if (heldItems.length) { const poolType = target.isPlayer() ? ModifierPoolType.PLAYER : target.hasTrainer() ? ModifierPoolType.TRAINER : ModifierPoolType.WILD; const highestItemTier = heldItems.map(m => m.type.getOrInferTier(poolType)).reduce((highestTier, tier) => Math.max(tier!, highestTier), 0); // TODO: is the bang after tier correct? @@ -2117,6 +2199,10 @@ export class RemoveHeldItemAttr extends MoveEffectAttr { return false; } + if (move.hitsSubstitute(user, target)) { + return false; + } + const cancelled = new Utils.BooleanHolder(false); applyAbAttrs(BlockItemTheftAbAttr, target, cancelled); // Check for abilities that block item theft @@ -2125,7 +2211,7 @@ export class RemoveHeldItemAttr extends MoveEffectAttr { } // Considers entire transferrable item pool by default (Knock Off). Otherwise berries only if specified (Incinerate). - let heldItems = this.getTargetHeldItems(target).filter(i => i.isTransferrable); + let heldItems = this.getTargetHeldItems(target).filter(i => i.isTransferable); if (this.berriesOnly) { heldItems = heldItems.filter(m => m instanceof BerryModifier && m.pokemonId === target.id, target.isPlayer()); @@ -2236,6 +2322,9 @@ export class StealEatBerryAttr extends EatBerryAttr { * @returns {boolean} true if the function succeeds */ apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + if (move.hitsSubstitute(user, target)) { + return false; + } const cancelled = new Utils.BooleanHolder(false); applyAbAttrs(BlockItemTheftAbAttr, target, cancelled); // check for abilities that block item theft if (cancelled.value === true) { @@ -2286,6 +2375,10 @@ export class HealStatusEffectAttr extends MoveEffectAttr { return false; } + if (!this.selfTarget && move.hitsSubstitute(user, target)) { + return false; + } + // Special edge case for shield dust blocking Sparkling Aria curing burn const moveTargets = getMoveTargets(user, move.id); if (target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && move.id === Moves.SPARKLING_ARIA && moveTargets.targets.length === 1) { @@ -2322,6 +2415,16 @@ export class BypassSleepAttr extends MoveAttr { return false; } + + /** + * Returns arbitrarily high score when Pokemon is asleep, otherwise shouldn't be used + * @param user + * @param target + * @param move + */ + getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer { + return user.status && user.status.effect === StatusEffect.SLEEP ? 200 : -10; + } } /** @@ -2463,7 +2566,7 @@ export class ChargeAttr extends OverrideMoveEffectAttr { const lastMove = user.getLastXMoves().find(() => true); if (!lastMove || lastMove.move !== move.id || (lastMove.result !== MoveResult.OTHER && lastMove.turn !== user.scene.currentBattle.turn)) { (args[0] as Utils.BooleanHolder).value = true; - new MoveChargeAnim(this.chargeAnim, move.id, user).play(user.scene, () => { + new MoveChargeAnim(this.chargeAnim, move.id, user).play(user.scene, false, () => { user.scene.queueMessage(this.chargeText.replace("{TARGET}", getPokemonNameWithAffix(target)).replace("{USER}", getPokemonNameWithAffix(user))); if (this.tagType) { user.addTag(this.tagType, 1, move.id, user.id); @@ -2563,7 +2666,7 @@ export class DelayedAttackAttr extends OverrideMoveEffectAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise { return new Promise(resolve => { if (args.length < 2 || !args[1]) { - new MoveChargeAnim(this.chargeAnim, move.id, user).play(user.scene, () => { + new MoveChargeAnim(this.chargeAnim, move.id, user).play(user.scene, false, () => { (args[0] as Utils.BooleanHolder).value = true; user.scene.queueMessage(this.chargeText.replace("{TARGET}", getPokemonNameWithAffix(target)).replace("{USER}", getPokemonNameWithAffix(user))); user.pushMoveHistory({ move: move.id, targets: [ target.getBattlerIndex() ], result: MoveResult.OTHER }); @@ -2597,6 +2700,10 @@ export class StatStageChangeAttr extends MoveEffectAttr { return false; } + if (!this.selfTarget && move.hitsSubstitute(user, target)) { + return false; + } + const moveChance = this.getMoveChance(user, target, move, this.selfTarget, true); if (moveChance < 0 || moveChance === 100 || user.randSeedInt(100) < moveChance) { const stages = this.getLevels(user); @@ -2793,8 +2900,10 @@ export class ResetStatsAttr extends MoveEffectAttr { activePokemon.forEach(p => promises.push(this.resetStats(p))); target.scene.queueMessage(i18next.t("moveTriggers:statEliminated")); } else { // Affects only the single target when Clear Smog is used - promises.push(this.resetStats(target)); - target.scene.queueMessage(i18next.t("moveTriggers:resetStats", {pokemonName: getPokemonNameWithAffix(target)})); + if (!move.hitsSubstitute(user, target)) { + promises.push(this.resetStats(target)); + target.scene.queueMessage(i18next.t("moveTriggers:resetStats", {pokemonName: getPokemonNameWithAffix(target)})); + } } await Promise.all(promises); @@ -2836,7 +2945,7 @@ export class SwapStatStagesAttr extends MoveEffectAttr { */ apply(user: Pokemon, target: Pokemon, move: Move, args: any []): boolean { if (super.apply(user, target, move, args)) { - for (const s of BATTLE_STATS) { + for (const s of this.stats) { const temp = user.getStatStage(s); user.setStatStage(s, target.getStatStage(s)); target.setStatStage(s, temp); @@ -3738,7 +3847,7 @@ export class StormAccuracyAttr extends VariableAccuracyAttr { * @extends VariableAccuracyAttr * @see {@linkcode apply} */ -export class MinimizeAccuracyAttr extends VariableAccuracyAttr { +export class AlwaysHitMinimizeAttr extends VariableAccuracyAttr { /** * @see {@linkcode apply} * @param user N/A @@ -3872,19 +3981,18 @@ export class StatusCategoryOnAllyAttr extends VariableMoveCategoryAttr { export class ShellSideArmCategoryAttr extends VariableMoveCategoryAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - const category = (args[0] as Utils.IntegerHolder); - const atkRatio = user.getEffectiveStat(Stat.ATK, target, move) / target.getEffectiveStat(Stat.DEF, user, move); - const specialRatio = user.getEffectiveStat(Stat.SPATK, target, move) / target.getEffectiveStat(Stat.SPDEF, user, move); + const category = (args[0] as Utils.NumberHolder); - // Shell Side Arm is much more complicated than it looks, this is a partial implementation to try to achieve something similar to the games - if (atkRatio > specialRatio) { + const predictedPhysDmg = target.getBaseDamage(user, move, MoveCategory.PHYSICAL, true, true); + const predictedSpecDmg = target.getBaseDamage(user, move, MoveCategory.SPECIAL, true, true); + + if (predictedPhysDmg > predictedSpecDmg) { category.value = MoveCategory.PHYSICAL; return true; - } else if (atkRatio === specialRatio && user.randSeedInt(2) === 0) { + } else if (predictedPhysDmg === predictedSpecDmg && user.randSeedInt(2) === 0) { category.value = MoveCategory.PHYSICAL; return true; } - return false; } } @@ -4431,6 +4539,7 @@ export class AddBattlerTagAttr extends MoveEffectAttr { case BattlerTagType.NIGHTMARE: case BattlerTagType.DROWSY: case BattlerTagType.DISABLED: + case BattlerTagType.HEAL_BLOCK: return -5; case BattlerTagType.SEEDED: case BattlerTagType.SALT_CURED: @@ -4621,6 +4730,13 @@ export class FlinchAttr extends AddBattlerTagAttr { constructor() { super(BattlerTagType.FLINCHED, false); } + + apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + if (!move.hitsSubstitute(user, target)) { + return super.apply(user, target, move, args); + } + return false; + } } export class ConfuseAttr extends AddBattlerTagAttr { @@ -4636,7 +4752,10 @@ export class ConfuseAttr extends AddBattlerTagAttr { return false; } - return super.apply(user, target, move, args); + if (!move.hitsSubstitute(user, target)) { + return super.apply(user, target, move, args); + } + return false; } } @@ -4711,7 +4830,39 @@ export class FaintCountdownAttr extends AddBattlerTagAttr { } /** - * Attribute used when a move hits a {@linkcode BattlerTagType} for double damage + * Attribute to remove all Substitutes from the field. + * @extends MoveEffectAttr + * @see {@link https://bulbapedia.bulbagarden.net/wiki/Tidy_Up_(move) | Tidy Up} + * @see {@linkcode SubstituteTag} + */ +export class RemoveAllSubstitutesAttr extends MoveEffectAttr { + constructor() { + super(true); + } + + /** + * Remove's the Substitute Doll effect from all active Pokemon on the field + * @param user {@linkcode Pokemon} the Pokemon using this move + * @param target n/a + * @param move {@linkcode Move} the move applying this effect + * @param args n/a + * @returns `true` if the effect successfully applies + */ + apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + if (!super.apply(user, target, move, args)) { + return false; + } + + user.scene.getField(true).forEach(pokemon => + pokemon.findAndRemoveTags(tag => tag.tagType === BattlerTagType.SUBSTITUTE)); + return true; + } +} + +/** + * Attribute used when a move can deal damage to {@linkcode BattlerTagType} + * Moves that always hit but do not deal double damage: Thunder, Fissure, Sky Uppercut, + * Smack Down, Hurricane, Thousand Arrows * @extends MoveAttr */ export class HitsTagAttr extends MoveAttr { @@ -4720,7 +4871,7 @@ export class HitsTagAttr extends MoveAttr { /** Should this move deal double damage against {@linkcode HitsTagAttr.tagType}? */ public doubleDamage: boolean; - constructor(tagType: BattlerTagType, doubleDamage?: boolean) { + constructor(tagType: BattlerTagType, doubleDamage: boolean = false) { super(); this.tagType = tagType; @@ -4732,6 +4883,17 @@ export class HitsTagAttr extends MoveAttr { } } +/** + * Used for moves that will always hit for a given tag but also doubles damage. + * Moves include: Gust, Stomp, Body Slam, Surf, Earthquake, Magnitude, Twister, + * Whirlpool, Dragon Rush, Heat Crash, Steam Roller, Flying Press + */ +export class HitsTagForDoubleDamageAttr extends HitsTagAttr { + constructor(tagType: BattlerTagType) { + super(tagType, true); + } +} + export class AddArenaTagAttr extends MoveEffectAttr { public tagType: ArenaTagType; public turnCount: integer; @@ -5060,7 +5222,6 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { switchOutTarget.leaveField(false); if (switchOutTarget.hp) { - switchOutTarget.setWildFlee(true); user.scene.queueMessage(i18next.t("moveTriggers:fled", {pokemonName: getPokemonNameWithAffix(switchOutTarget)}), null, true, 500); // in double battles redirect potential moves off fled pokemon @@ -5099,7 +5260,11 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { const switchOutTarget = (this.user ? user : target); const player = switchOutTarget instanceof PlayerPokemon; - if (!this.user && move.category === MoveCategory.STATUS && (target.hasAbilityWithAttr(ForceSwitchOutImmunityAbAttr) || target.isMax())) { + if (!this.user && move.hitsSubstitute(user, target)) { + return false; + } + + if (!this.user && move.category === MoveCategory.STATUS && (target.hasAbilityWithAttr(ForceSwitchOutImmunityAbAttr))) { return false; } @@ -6248,7 +6413,7 @@ export class AttackedByItemAttr extends MoveAttr { */ getCondition(): MoveConditionFunc { return (user: Pokemon, target: Pokemon, move: Move) => { - const heldItems = target.getHeldItems().filter(i => i.isTransferrable); + const heldItems = target.getHeldItems().filter(i => i.isTransferable); if (heldItems.length === 0) { return false; } @@ -6309,8 +6474,6 @@ const failOnGravityCondition: MoveConditionFunc = (user, target, move) => !user. const failOnBossCondition: MoveConditionFunc = (user, target, move) => !target.isBossImmune(); -const failOnMaxCondition: MoveConditionFunc = (user, target, move) => !target.isMax(); - const failIfSingleBattle: MoveConditionFunc = (user, target, move) => user.scene.currentBattle.double; const failIfDampCondition: MoveConditionFunc = (user, target, move) => { @@ -6609,12 +6772,12 @@ export function initMoves() { new AttackMove(Moves.CUT, Type.NORMAL, MoveCategory.PHYSICAL, 50, 95, 30, -1, 0, 1) .slicingMove(), new AttackMove(Moves.GUST, Type.FLYING, MoveCategory.SPECIAL, 40, 100, 35, -1, 0, 1) - .attr(HitsTagAttr, BattlerTagType.FLYING, true) + .attr(HitsTagForDoubleDamageAttr, BattlerTagType.FLYING) .windMove(), new AttackMove(Moves.WING_ATTACK, Type.FLYING, MoveCategory.PHYSICAL, 60, 100, 35, -1, 0, 1), new StatusMove(Moves.WHIRLWIND, Type.NORMAL, -1, 20, -1, -6, 1) .attr(ForceSwitchOutAttr) - .attr(HitsTagAttr, BattlerTagType.FLYING, false) + .ignoresSubstitute() .hidesTarget() .windMove(), new AttackMove(Moves.FLY, Type.FLYING, MoveCategory.PHYSICAL, 90, 95, 15, -1, 0, 1) @@ -6626,8 +6789,8 @@ export function initMoves() { new AttackMove(Moves.SLAM, Type.NORMAL, MoveCategory.PHYSICAL, 80, 75, 20, -1, 0, 1), new AttackMove(Moves.VINE_WHIP, Type.GRASS, MoveCategory.PHYSICAL, 45, 100, 25, -1, 0, 1), new AttackMove(Moves.STOMP, Type.NORMAL, MoveCategory.PHYSICAL, 65, 100, 20, 30, 0, 1) - .attr(MinimizeAccuracyAttr) - .attr(HitsTagAttr, BattlerTagType.MINIMIZED, true) + .attr(AlwaysHitMinimizeAttr) + .attr(HitsTagForDoubleDamageAttr, BattlerTagType.MINIMIZED) .attr(FlinchAttr), new AttackMove(Moves.DOUBLE_KICK, Type.FIGHTING, MoveCategory.PHYSICAL, 30, 100, 30, -1, 0, 1) .attr(MultiHitAttr, MultiHitType._2), @@ -6651,8 +6814,8 @@ export function initMoves() { .attr(OneHitKOAccuracyAttr), new AttackMove(Moves.TACKLE, Type.NORMAL, MoveCategory.PHYSICAL, 40, 100, 35, -1, 0, 1), new AttackMove(Moves.BODY_SLAM, Type.NORMAL, MoveCategory.PHYSICAL, 85, 100, 15, 30, 0, 1) - .attr(MinimizeAccuracyAttr) - .attr(HitsTagAttr, BattlerTagType.MINIMIZED, true) + .attr(AlwaysHitMinimizeAttr) + .attr(HitsTagForDoubleDamageAttr, BattlerTagType.MINIMIZED) .attr(StatusEffectAttr, StatusEffect.PARALYSIS), new AttackMove(Moves.WRAP, Type.NORMAL, MoveCategory.PHYSICAL, 15, 90, 20, -1, 0, 1) .attr(TrapAttr, BattlerTagType.WRAP), @@ -6705,7 +6868,7 @@ export function initMoves() { new StatusMove(Moves.DISABLE, Type.NORMAL, 100, 20, -1, 0, 1) .attr(AddBattlerTagAttr, BattlerTagType.DISABLED, false, true) .condition((user, target, move) => target.getMoveHistory().reverse().find(m => m.move !== Moves.NONE && m.move !== Moves.STRUGGLE && !m.virtual) !== undefined) - .condition(failOnMaxCondition), + .ignoresSubstitute(), new AttackMove(Moves.ACID, Type.POISON, MoveCategory.SPECIAL, 40, 100, 30, 10, 0, 1) .attr(StatStageChangeAttr, [ Stat.SPDEF ], -1) .target(MoveTarget.ALL_NEAR_ENEMIES), @@ -6720,7 +6883,7 @@ export function initMoves() { new AttackMove(Moves.HYDRO_PUMP, Type.WATER, MoveCategory.SPECIAL, 110, 80, 5, -1, 0, 1), new AttackMove(Moves.SURF, Type.WATER, MoveCategory.SPECIAL, 90, 100, 15, -1, 0, 1) .target(MoveTarget.ALL_NEAR_OTHERS) - .attr(HitsTagAttr, BattlerTagType.UNDERWATER, true) + .attr(HitsTagForDoubleDamageAttr, BattlerTagType.UNDERWATER) .attr(GulpMissileTagAttr), new AttackMove(Moves.ICE_BEAM, Type.ICE, MoveCategory.SPECIAL, 90, 100, 10, 10, 0, 1) .attr(StatusEffectAttr, StatusEffect.FREEZE), @@ -6743,8 +6906,7 @@ export function initMoves() { .attr(RecoilAttr) .recklessMove(), new AttackMove(Moves.LOW_KICK, Type.FIGHTING, MoveCategory.PHYSICAL, -1, 100, 20, -1, 0, 1) - .attr(WeightPowerAttr) - .condition(failOnMaxCondition), + .attr(WeightPowerAttr), new AttackMove(Moves.COUNTER, Type.FIGHTING, MoveCategory.PHYSICAL, -1, 100, 20, -1, -5, 1) .attr(CounterDamageAttr, (move: Move) => move.category === MoveCategory.PHYSICAL, 2) .target(MoveTarget.ATTACKER), @@ -6804,17 +6966,18 @@ export function initMoves() { new AttackMove(Moves.THUNDER, Type.ELECTRIC, MoveCategory.SPECIAL, 110, 70, 10, 30, 0, 1) .attr(StatusEffectAttr, StatusEffect.PARALYSIS) .attr(ThunderAccuracyAttr) - .attr(HitsTagAttr, BattlerTagType.FLYING, false), + .attr(HitsTagAttr, BattlerTagType.FLYING), new AttackMove(Moves.ROCK_THROW, Type.ROCK, MoveCategory.PHYSICAL, 50, 90, 15, -1, 0, 1) .makesContact(false), new AttackMove(Moves.EARTHQUAKE, Type.GROUND, MoveCategory.PHYSICAL, 100, 100, 10, -1, 0, 1) - .attr(HitsTagAttr, BattlerTagType.UNDERGROUND, true) + .attr(HitsTagForDoubleDamageAttr, BattlerTagType.UNDERGROUND) + .attr(MovePowerMultiplierAttr, (user, target, move) => user.scene.arena.getTerrainType() === TerrainType.GRASSY && target.isGrounded() ? 0.5 : 1) .makesContact(false) .target(MoveTarget.ALL_NEAR_OTHERS), new AttackMove(Moves.FISSURE, Type.GROUND, MoveCategory.PHYSICAL, 200, 30, 5, -1, 0, 1) .attr(OneHitKOAttr) .attr(OneHitKOAccuracyAttr) - .attr(HitsTagAttr, BattlerTagType.UNDERGROUND, false) + .attr(HitsTagAttr, BattlerTagType.UNDERGROUND) .makesContact(false), new AttackMove(Moves.DIG, Type.GROUND, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 1) .attr(ChargeAttr, ChargeAnim.DIG_CHARGING, i18next.t("moveTriggers:dugAHole", {pokemonName: "{USER}"}), BattlerTagType.UNDERGROUND) @@ -6842,6 +7005,7 @@ export function initMoves() { .attr(LevelDamageAttr), new StatusMove(Moves.MIMIC, Type.NORMAL, -1, 10, -1, 0, 1) .attr(MovesetCopyMoveAttr) + .ignoresSubstitute() .ignoresVirtual(), new StatusMove(Moves.SCREECH, Type.NORMAL, 85, 40, -1, 0, 1) .attr(StatStageChangeAttr, [ Stat.DEF ], -2) @@ -6870,6 +7034,7 @@ export function initMoves() { .attr(AddArenaTagAttr, ArenaTagType.LIGHT_SCREEN, 5, true) .target(MoveTarget.USER_SIDE), new SelfStatusMove(Moves.HAZE, Type.ICE, -1, 30, -1, 0, 1) + .ignoresSubstitute() .attr(ResetStatsAttr, true), new StatusMove(Moves.REFLECT, Type.PSYCHIC, -1, 20, -1, 0, 1) .attr(AddArenaTagAttr, ArenaTagType.REFLECT, 5, true) @@ -7012,14 +7177,14 @@ export function initMoves() { .attr(HighCritAttr) .slicingMove(), new SelfStatusMove(Moves.SUBSTITUTE, Type.NORMAL, -1, 10, -1, 0, 1) - .attr(RecoilAttr) - .unimplemented(), + .attr(AddSubstituteAttr), new AttackMove(Moves.STRUGGLE, Type.NORMAL, MoveCategory.PHYSICAL, 50, -1, 1, -1, 0, 1) .attr(RecoilAttr, true, 0.25, true) .attr(TypelessAttr) .ignoresVirtual() .target(MoveTarget.RANDOM_NEAR_ENEMY), new StatusMove(Moves.SKETCH, Type.NORMAL, -1, 1, -1, 0, 2) + .ignoresSubstitute() .attr(SketchAttr) .ignoresVirtual(), new AttackMove(Moves.TRIPLE_KICK, Type.FIGHTING, MoveCategory.PHYSICAL, 10, 90, 10, -1, 0, 2) @@ -7045,12 +7210,14 @@ export function initMoves() { .soundBased(), new StatusMove(Moves.CURSE, Type.GHOST, -1, 10, -1, 0, 2) .attr(CurseAttr) + .ignoresSubstitute() .ignoresProtect(true) .target(MoveTarget.CURSE), new AttackMove(Moves.FLAIL, Type.NORMAL, MoveCategory.PHYSICAL, -1, 100, 15, -1, 0, 2) .attr(LowHpPowerAttr), new StatusMove(Moves.CONVERSION_2, Type.NORMAL, -1, 30, -1, 0, 2) .attr(ResistLastMoveTypeAttr) + .ignoresSubstitute() .partial(), // Checks the move's original typing and not if its type is changed through some other means new AttackMove(Moves.AEROBLAST, Type.FLYING, MoveCategory.SPECIAL, 100, 95, 5, -1, 0, 2) .windMove() @@ -7062,6 +7229,7 @@ export function initMoves() { new AttackMove(Moves.REVERSAL, Type.FIGHTING, MoveCategory.PHYSICAL, -1, 100, 15, -1, 0, 2) .attr(LowHpPowerAttr), new StatusMove(Moves.SPITE, Type.GHOST, 100, 10, -1, 0, 2) + .ignoresSubstitute() .attr(ReducePpMoveAttr, 4), new AttackMove(Moves.POWDER_SNOW, Type.ICE, MoveCategory.SPECIAL, 40, 100, 25, 10, 0, 2) .attr(StatusEffectAttr, StatusEffect.FREEZE) @@ -7095,7 +7263,8 @@ export function initMoves() { .attr(StatusEffectAttr, StatusEffect.PARALYSIS) .ballBombMove(), new StatusMove(Moves.FORESIGHT, Type.NORMAL, -1, 40, -1, 0, 2) - .attr(ExposedMoveAttr, BattlerTagType.IGNORE_GHOST), + .attr(ExposedMoveAttr, BattlerTagType.IGNORE_GHOST) + .ignoresSubstitute(), new SelfStatusMove(Moves.DESTINY_BOND, Type.GHOST, -1, 5, -1, 0, 2) .ignoresProtect() .attr(DestinyBondAttr) @@ -7164,6 +7333,7 @@ export function initMoves() { .attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1), new StatusMove(Moves.ATTRACT, Type.NORMAL, 100, 15, -1, 0, 2) .attr(AddBattlerTagAttr, BattlerTagType.INFATUATED) + .ignoresSubstitute() .condition((user, target, move) => user.isOppositeGender(target)), new SelfStatusMove(Moves.SLEEP_TALK, Type.NORMAL, -1, 10, -1, 0, 2) .attr(BypassSleepAttr) @@ -7195,7 +7365,8 @@ export function initMoves() { new AttackMove(Moves.MAGNITUDE, Type.GROUND, MoveCategory.PHYSICAL, -1, 100, 30, -1, 0, 2) .attr(PreMoveMessageAttr, magnitudeMessageFunc) .attr(MagnitudePowerAttr) - .attr(HitsTagAttr, BattlerTagType.UNDERGROUND, true) + .attr(MovePowerMultiplierAttr, (user, target, move) => user.scene.arena.getTerrainType() === TerrainType.GRASSY && target.isGrounded() ? 0.5 : 1) + .attr(HitsTagForDoubleDamageAttr, BattlerTagType.UNDERGROUND) .makesContact(false) .target(MoveTarget.ALL_NEAR_OTHERS), new AttackMove(Moves.DYNAMIC_PUNCH, Type.FIGHTING, MoveCategory.PHYSICAL, 100, 50, 5, 100, 0, 2) @@ -7209,6 +7380,7 @@ export function initMoves() { .hidesUser(), new StatusMove(Moves.ENCORE, Type.NORMAL, 100, 5, -1, 0, 2) .attr(AddBattlerTagAttr, BattlerTagType.ENCORE, false, true) + .ignoresSubstitute() .condition((user, target, move) => new EncoreTag(user.id).canAdd(target)), new AttackMove(Moves.PURSUIT, Type.DARK, MoveCategory.PHYSICAL, 40, 100, 20, -1, 0, 2) .partial(), @@ -7250,7 +7422,7 @@ export function initMoves() { new AttackMove(Moves.CROSS_CHOP, Type.FIGHTING, MoveCategory.PHYSICAL, 100, 80, 5, -1, 0, 2) .attr(HighCritAttr), new AttackMove(Moves.TWISTER, Type.DRAGON, MoveCategory.SPECIAL, 40, 100, 20, 20, 0, 2) - .attr(HitsTagAttr, BattlerTagType.FLYING, true) + .attr(HitsTagForDoubleDamageAttr, BattlerTagType.FLYING) .attr(FlinchAttr) .windMove() .target(MoveTarget.ALL_NEAR_ENEMIES), @@ -7267,6 +7439,7 @@ export function initMoves() { .attr(CounterDamageAttr, (move: Move) => move.category === MoveCategory.SPECIAL, 2) .target(MoveTarget.ATTACKER), new StatusMove(Moves.PSYCH_UP, Type.NORMAL, -1, 10, -1, 0, 2) + .ignoresSubstitute() .attr(CopyStatsAttr), new AttackMove(Moves.EXTREME_SPEED, Type.NORMAL, MoveCategory.PHYSICAL, 80, 100, 5, -1, 2, 2), new AttackMove(Moves.ANCIENT_POWER, Type.ROCK, MoveCategory.SPECIAL, 60, 100, 5, 10, 0, 2) @@ -7281,7 +7454,7 @@ export function initMoves() { .attr(StatStageChangeAttr, [ Stat.DEF ], -1), new AttackMove(Moves.WHIRLPOOL, Type.WATER, MoveCategory.SPECIAL, 35, 85, 15, -1, 0, 2) .attr(TrapAttr, BattlerTagType.WHIRLPOOL) - .attr(HitsTagAttr, BattlerTagType.UNDERWATER, true), + .attr(HitsTagForDoubleDamageAttr, BattlerTagType.UNDERWATER), new AttackMove(Moves.BEAT_UP, Type.DARK, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 2) .attr(MultiHitAttr, MultiHitType.BEAT_UP) .attr(BeatUpAttr) @@ -7315,6 +7488,7 @@ export function initMoves() { .attr(WeatherChangeAttr, WeatherType.HAIL) .target(MoveTarget.BOTH_SIDES), new StatusMove(Moves.TORMENT, Type.DARK, 100, 15, -1, 0, 3) + .ignoresSubstitute() .unimplemented(), new StatusMove(Moves.FLATTER, Type.DARK, 100, 15, -1, 0, 3) .attr(StatStageChangeAttr, [ Stat.SPATK ], 1) @@ -7345,13 +7519,16 @@ export function initMoves() { .attr(StatStageChangeAttr, [ Stat.SPDEF ], 1, true) .attr(AddBattlerTagAttr, BattlerTagType.CHARGED, true, false), new StatusMove(Moves.TAUNT, Type.DARK, 100, 20, -1, 0, 3) + .ignoresSubstitute() .unimplemented(), new StatusMove(Moves.HELPING_HAND, Type.NORMAL, -1, 20, -1, 5, 3) .attr(AddBattlerTagAttr, BattlerTagType.HELPING_HAND) + .ignoresSubstitute() .target(MoveTarget.NEAR_ALLY), new StatusMove(Moves.TRICK, Type.PSYCHIC, 100, 10, -1, 0, 3) .unimplemented(), new StatusMove(Moves.ROLE_PLAY, Type.PSYCHIC, -1, 10, -1, 0, 3) + .ignoresSubstitute() .attr(AbilityCopyAttr), new SelfStatusMove(Moves.WISH, Type.NORMAL, -1, 10, -1, 0, 3) .triageMove() @@ -7375,7 +7552,7 @@ export function initMoves() { .attr(AddBattlerTagAttr, BattlerTagType.DROWSY, false, true) .condition((user, target, move) => !target.status && !target.scene.arena.getTagOnSide(ArenaTagType.SAFEGUARD, target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY)), new AttackMove(Moves.KNOCK_OFF, Type.DARK, MoveCategory.PHYSICAL, 65, 100, 20, -1, 0, 3) - .attr(MovePowerMultiplierAttr, (user, target, move) => target.getHeldItems().filter(i => i.isTransferrable).length > 0 ? 1.5 : 1) + .attr(MovePowerMultiplierAttr, (user, target, move) => target.getHeldItems().filter(i => i.isTransferable).length > 0 ? 1.5 : 1) .attr(RemoveHeldItemAttr, false), new AttackMove(Moves.ENDEAVOR, Type.NORMAL, MoveCategory.PHYSICAL, -1, 100, 5, -1, 0, 3) .attr(MatchHpAttr) @@ -7384,8 +7561,10 @@ export function initMoves() { .attr(HpPowerAttr) .target(MoveTarget.ALL_NEAR_ENEMIES), new StatusMove(Moves.SKILL_SWAP, Type.PSYCHIC, -1, 10, -1, 0, 3) + .ignoresSubstitute() .attr(SwitchAbilitiesAttr), new SelfStatusMove(Moves.IMPRISON, Type.PSYCHIC, -1, 10, -1, 0, 3) + .ignoresSubstitute() .unimplemented(), new SelfStatusMove(Moves.REFRESH, Type.NORMAL, -1, 20, -1, 0, 3) .attr(HealStatusEffectAttr, true, StatusEffect.PARALYSIS, StatusEffect.POISON, StatusEffect.TOXIC, StatusEffect.BURN) @@ -7470,7 +7649,8 @@ export function initMoves() { .attr(StatStageChangeAttr, [ Stat.SPATK ], -2, true) .attr(HealStatusEffectAttr, true, StatusEffect.FREEZE), new StatusMove(Moves.ODOR_SLEUTH, Type.NORMAL, -1, 40, -1, 0, 3) - .attr(ExposedMoveAttr, BattlerTagType.IGNORE_GHOST), + .attr(ExposedMoveAttr, BattlerTagType.IGNORE_GHOST) + .ignoresSubstitute(), new AttackMove(Moves.ROCK_TOMB, Type.ROCK, MoveCategory.PHYSICAL, 60, 95, 15, 100, 0, 3) .attr(StatStageChangeAttr, [ Stat.SPD ], -1) .makesContact(false), @@ -7582,7 +7762,8 @@ export function initMoves() { .attr(AddArenaTagAttr, ArenaTagType.GRAVITY, 5) .target(MoveTarget.BOTH_SIDES), new StatusMove(Moves.MIRACLE_EYE, Type.PSYCHIC, -1, 40, -1, 0, 4) - .attr(ExposedMoveAttr, BattlerTagType.IGNORE_DARK), + .attr(ExposedMoveAttr, BattlerTagType.IGNORE_DARK) + .ignoresSubstitute(), new AttackMove(Moves.WAKE_UP_SLAP, Type.FIGHTING, MoveCategory.PHYSICAL, 70, 100, 10, -1, 0, 4) .attr(MovePowerMultiplierAttr, (user, target, move) => targetSleptOrComatoseCondition(user, target, move) ? 2 : 1) .attr(HealStatusEffectAttr, false, StatusEffect.SLEEP), @@ -7646,8 +7827,8 @@ export function initMoves() { .makesContact() .attr(LessPPMorePowerAttr), new StatusMove(Moves.HEAL_BLOCK, Type.PSYCHIC, 100, 15, -1, 0, 4) - .target(MoveTarget.ALL_NEAR_ENEMIES) - .unimplemented(), + .attr(AddBattlerTagAttr, BattlerTagType.HEAL_BLOCK, false, true, 5) + .target(MoveTarget.ALL_NEAR_ENEMIES), new AttackMove(Moves.WRING_OUT, Type.NORMAL, MoveCategory.SPECIAL, -1, 100, 5, -1, 0, 4) .attr(OpponentHighHpPowerAttr, 120) .makesContact(), @@ -7659,6 +7840,7 @@ export function initMoves() { .attr(AddArenaTagAttr, ArenaTagType.NO_CRIT, 5, true, true) .target(MoveTarget.USER_SIDE), new StatusMove(Moves.ME_FIRST, Type.NORMAL, -1, 20, -1, 0, 4) + .ignoresSubstitute() .ignoresVirtual() .target(MoveTarget.NEAR_ENEMY) .unimplemented(), @@ -7666,9 +7848,11 @@ export function initMoves() { .attr(CopyMoveAttr) .ignoresVirtual(), new StatusMove(Moves.POWER_SWAP, Type.PSYCHIC, -1, 10, 100, 0, 4) - .attr(SwapStatStagesAttr, [ Stat.ATK, Stat.SPATK ]), + .attr(SwapStatStagesAttr, [ Stat.ATK, Stat.SPATK ]) + .ignoresSubstitute(), new StatusMove(Moves.GUARD_SWAP, Type.PSYCHIC, -1, 10, 100, 0, 4) - .attr(SwapStatStagesAttr, [ Stat.DEF, Stat.SPDEF ]), + .attr(SwapStatStagesAttr, [ Stat.DEF, Stat.SPDEF ]) + .ignoresSubstitute(), new AttackMove(Moves.PUNISHMENT, Type.DARK, MoveCategory.PHYSICAL, -1, 100, 5, -1, 0, 4) .makesContact(true) .attr(PunishmentPowerAttr), @@ -7682,7 +7866,8 @@ export function initMoves() { .attr(AddArenaTrapTagAttr, ArenaTagType.TOXIC_SPIKES) .target(MoveTarget.ENEMY_SIDE), new StatusMove(Moves.HEART_SWAP, Type.PSYCHIC, -1, 10, -1, 0, 4) - .attr(SwapStatStagesAttr, BATTLE_STATS), + .attr(SwapStatStagesAttr, BATTLE_STATS) + .ignoresSubstitute(), new SelfStatusMove(Moves.AQUA_RING, Type.WATER, -1, 20, -1, 0, 4) .attr(AddBattlerTagAttr, BattlerTagType.AQUA_RING, true, true), new SelfStatusMove(Moves.MAGNET_RISE, Type.ELECTRIC, -1, 10, -1, 0, 4) @@ -7723,8 +7908,8 @@ export function initMoves() { new AttackMove(Moves.DRAGON_PULSE, Type.DRAGON, MoveCategory.SPECIAL, 85, 100, 10, -1, 0, 4) .pulseMove(), new AttackMove(Moves.DRAGON_RUSH, Type.DRAGON, MoveCategory.PHYSICAL, 100, 75, 10, 20, 0, 4) - .attr(MinimizeAccuracyAttr) - .attr(HitsTagAttr, BattlerTagType.MINIMIZED, true) + .attr(AlwaysHitMinimizeAttr) + .attr(HitsTagForDoubleDamageAttr, BattlerTagType.MINIMIZED) .attr(FlinchAttr), new AttackMove(Moves.POWER_GEM, Type.ROCK, MoveCategory.SPECIAL, 80, 100, 20, -1, 0, 4), new AttackMove(Moves.DRAIN_PUNCH, Type.FIGHTING, MoveCategory.PHYSICAL, 75, 100, 10, -1, 0, 4) @@ -7834,8 +8019,7 @@ export function initMoves() { .target(MoveTarget.ENEMY_SIDE), new AttackMove(Moves.GRASS_KNOT, Type.GRASS, MoveCategory.SPECIAL, -1, 100, 20, -1, 0, 4) .attr(WeightPowerAttr) - .makesContact() - .condition(failOnMaxCondition), + .makesContact(), new AttackMove(Moves.CHATTER, Type.FLYING, MoveCategory.SPECIAL, 65, 100, 20, 100, 0, 4) .attr(ConfuseAttr) .soundBased(), @@ -7922,7 +8106,7 @@ export function initMoves() { .attr(AddBattlerTagAttr, BattlerTagType.IGNORE_FLYING, false, false, 1, 1, true) .attr(AddBattlerTagAttr, BattlerTagType.INTERRUPTED) .attr(RemoveBattlerTagAttr, [BattlerTagType.FLYING, BattlerTagType.MAGNET_RISEN]) - .attr(HitsTagAttr, BattlerTagType.FLYING, false) + .attr(HitsTagAttr, BattlerTagType.FLYING) .makesContact(false), new AttackMove(Moves.STORM_THROW, Type.FIGHTING, MoveCategory.PHYSICAL, 60, 100, 10, -1, 0, 5) .attr(CritOnlyAttr), @@ -7935,10 +8119,9 @@ export function initMoves() { .attr(StatStageChangeAttr, [ Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true) .danceMove(), new AttackMove(Moves.HEAVY_SLAM, Type.STEEL, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 5) - .attr(MinimizeAccuracyAttr) + .attr(AlwaysHitMinimizeAttr) .attr(CompareWeightPowerAttr) - .attr(HitsTagAttr, BattlerTagType.MINIMIZED, true) - .condition(failOnMaxCondition), + .attr(HitsTagForDoubleDamageAttr, BattlerTagType.MINIMIZED), new AttackMove(Moves.SYNCHRONOISE, Type.PSYCHIC, MoveCategory.SPECIAL, 120, 100, 10, -1, 0, 5) .target(MoveTarget.ALL_NEAR_OTHERS) .condition(unknownTypeCondition) @@ -7965,6 +8148,7 @@ export function initMoves() { .attr(AbilityGiveAttr), new StatusMove(Moves.AFTER_YOU, Type.NORMAL, -1, 15, -1, 0, 5) .ignoresProtect() + .ignoresSubstitute() .target(MoveTarget.NEAR_OTHER) .condition(failIfSingleBattle) .condition((user, target, move) => !target.turnData.acted) @@ -8007,6 +8191,7 @@ export function initMoves() { .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/ .attr(ChargeAttr, ChargeAnim.SKY_DROP_CHARGING, i18next.t("moveTriggers:tookTargetIntoSky", {pokemonName: "{USER}", targetName: "{TARGET}"}), BattlerTagType.FLYING) // TODO: Add 2nd turn message .condition(failOnGravityCondition) + .condition((user, target, move) => !target.getTag(BattlerTagType.SUBSTITUTE)) .ignoresVirtual(), new SelfStatusMove(Moves.SHIFT_GEAR, Type.STEEL, -1, 10, -1, 0, 5) .attr(StatStageChangeAttr, [ Stat.ATK ], 1, true) @@ -8019,8 +8204,9 @@ export function initMoves() { new StatusMove(Moves.QUASH, Type.DARK, 100, 15, -1, 0, 5) .unimplemented(), new AttackMove(Moves.ACROBATICS, Type.FLYING, MoveCategory.PHYSICAL, 55, 100, 15, -1, 0, 5) - .attr(MovePowerMultiplierAttr, (user, target, move) => Math.max(1, 2 - 0.2 * user.getHeldItems().filter(i => i.isTransferrable).reduce((v, m) => v + m.stackCount, 0))), + .attr(MovePowerMultiplierAttr, (user, target, move) => Math.max(1, 2 - 0.2 * user.getHeldItems().filter(i => i.isTransferable).reduce((v, m) => v + m.stackCount, 0))), new StatusMove(Moves.REFLECT_TYPE, Type.NORMAL, -1, 15, -1, 0, 5) + .ignoresSubstitute() .attr(CopyTypeAttr), new AttackMove(Moves.RETALIATE, Type.NORMAL, MoveCategory.PHYSICAL, 70, 100, 5, -1, 0, 5) .attr(MovePowerMultiplierAttr, (user, target, move) => { @@ -8037,6 +8223,7 @@ export function initMoves() { .attr(SacrificialAttrOnHit), new StatusMove(Moves.BESTOW, Type.NORMAL, -1, 15, -1, 0, 5) .ignoresProtect() + .ignoresSubstitute() .unimplemented(), new AttackMove(Moves.INFERNO, Type.FIRE, MoveCategory.SPECIAL, 100, 50, 5, 100, 0, 5) .attr(StatusEffectAttr, StatusEffect.BURN), @@ -8053,6 +8240,7 @@ export function initMoves() { .target(MoveTarget.ALL_NEAR_ENEMIES), new AttackMove(Moves.BULLDOZE, Type.GROUND, MoveCategory.PHYSICAL, 60, 100, 20, 100, 0, 5) .attr(StatStageChangeAttr, [ Stat.SPD ], -1) + .attr(MovePowerMultiplierAttr, (user, target, move) => user.scene.arena.getTerrainType() === TerrainType.GRASSY && target.isGrounded() ? 0.5 : 1) .makesContact(false) .target(MoveTarget.ALL_NEAR_OTHERS), new AttackMove(Moves.FROST_BREATH, Type.ICE, MoveCategory.SPECIAL, 60, 90, 10, 100, 0, 5) @@ -8084,13 +8272,14 @@ export function initMoves() { .attr(StatStageChangeAttr, [ Stat.DEF ], -1) .slicingMove(), new AttackMove(Moves.HEAT_CRASH, Type.FIRE, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 5) - .attr(MinimizeAccuracyAttr) + .attr(AlwaysHitMinimizeAttr) .attr(CompareWeightPowerAttr) - .attr(HitsTagAttr, BattlerTagType.MINIMIZED, true) - .condition(failOnMaxCondition), + .attr(HitsTagForDoubleDamageAttr, BattlerTagType.MINIMIZED), new AttackMove(Moves.LEAF_TORNADO, Type.GRASS, MoveCategory.SPECIAL, 65, 90, 10, 50, 0, 5) .attr(StatStageChangeAttr, [ Stat.ACC ], -1), new AttackMove(Moves.STEAMROLLER, Type.BUG, MoveCategory.PHYSICAL, 65, 100, 20, 30, 0, 5) + .attr(AlwaysHitMinimizeAttr) + .attr(HitsTagForDoubleDamageAttr, BattlerTagType.MINIMIZED) .attr(FlinchAttr), new SelfStatusMove(Moves.COTTON_GUARD, Type.GRASS, -1, 10, -1, 0, 5) .attr(StatStageChangeAttr, [ Stat.DEF ], 3, true), @@ -8103,7 +8292,7 @@ export function initMoves() { new AttackMove(Moves.HURRICANE, Type.FLYING, MoveCategory.SPECIAL, 110, 70, 10, 30, 0, 5) .attr(ThunderAccuracyAttr) .attr(ConfuseAttr) - .attr(HitsTagAttr, BattlerTagType.FLYING, false) + .attr(HitsTagAttr, BattlerTagType.FLYING) .windMove(), new AttackMove(Moves.HEAD_CHARGE, Type.NORMAL, MoveCategory.PHYSICAL, 120, 100, 15, -1, 0, 5) .attr(RecoilAttr) @@ -8157,9 +8346,9 @@ export function initMoves() { .attr(LastMoveDoublePowerAttr, Moves.FUSION_FLARE) .makesContact(false), new AttackMove(Moves.FLYING_PRESS, Type.FIGHTING, MoveCategory.PHYSICAL, 100, 95, 10, -1, 0, 6) - .attr(MinimizeAccuracyAttr) + .attr(AlwaysHitMinimizeAttr) .attr(FlyingTypeMultiplierAttr) - .attr(HitsTagAttr, BattlerTagType.MINIMIZED, true) + .attr(HitsTagForDoubleDamageAttr, BattlerTagType.MINIMIZED) .condition(failOnGravityCondition), new StatusMove(Moves.MAT_BLOCK, Type.FIGHTING, -1, 10, -1, 0, 6) .target(MoveTarget.USER_SIDE) @@ -8246,13 +8435,15 @@ export function initMoves() { .soundBased() .target(MoveTarget.ALL_NEAR_OTHERS), new StatusMove(Moves.FAIRY_LOCK, Type.FAIRY, -1, 10, -1, 0, 6) + .ignoresSubstitute() .target(MoveTarget.BOTH_SIDES) .unimplemented(), new SelfStatusMove(Moves.KINGS_SHIELD, Type.STEEL, -1, 10, -1, 4, 6) .attr(ProtectAttr, BattlerTagType.KINGS_SHIELD) .condition(failIfLastCondition), new StatusMove(Moves.PLAY_NICE, Type.NORMAL, -1, 20, -1, 0, 6) - .attr(StatStageChangeAttr, [ Stat.ATK ], -1), + .attr(StatStageChangeAttr, [ Stat.ATK ], -1) + .ignoresSubstitute(), new StatusMove(Moves.CONFIDE, Type.NORMAL, -1, 20, -1, 0, 6) .attr(StatStageChangeAttr, [ Stat.SPATK ], -1) .soundBased(), @@ -8265,7 +8456,8 @@ export function initMoves() { .attr(HealStatusEffectAttr, false, StatusEffect.FREEZE) .attr(StatusEffectAttr, StatusEffect.BURN), new AttackMove(Moves.HYPERSPACE_HOLE, Type.PSYCHIC, MoveCategory.SPECIAL, 80, -1, 5, -1, 0, 6) - .ignoresProtect(), + .ignoresProtect() + .ignoresSubstitute(), new AttackMove(Moves.WATER_SHURIKEN, Type.WATER, MoveCategory.SPECIAL, 15, 100, 20, -1, 1, 6) .attr(MultiHitAttr) .attr(WaterShurikenPowerAttr) @@ -8277,6 +8469,7 @@ export function initMoves() { .condition(failIfLastCondition), new StatusMove(Moves.AROMATIC_MIST, Type.FAIRY, -1, 20, -1, 0, 6) .attr(StatStageChangeAttr, [ Stat.SPDEF ], 1) + .ignoresSubstitute() .target(MoveTarget.NEAR_ALLY), new StatusMove(Moves.EERIE_IMPULSE, Type.ELECTRIC, 100, 15, -1, 0, 6) .attr(StatStageChangeAttr, [ Stat.SPATK ], -2), @@ -8284,6 +8477,7 @@ export function initMoves() { .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK, Stat.SPD ], -1, false, (user, target, move) => target.status?.effect === StatusEffect.POISON || target.status?.effect === StatusEffect.TOXIC) .target(MoveTarget.ALL_NEAR_ENEMIES), new StatusMove(Moves.POWDER, Type.BUG, 100, 20, -1, 1, 6) + .ignoresSubstitute() .powderMove() .unimplemented(), new SelfStatusMove(Moves.GEOMANCY, Type.FAIRY, -1, 10, -1, 0, 6) @@ -8292,6 +8486,7 @@ export function initMoves() { .ignoresVirtual(), new StatusMove(Moves.MAGNETIC_FLUX, Type.ELECTRIC, -1, 20, -1, 0, 6) .attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF ], 1, false, (user, target, move) => !![ Abilities.PLUS, Abilities.MINUS].find(a => target.hasAbility(a, false))) + .ignoresSubstitute() .target(MoveTarget.USER_AND_ALLIES) .condition((user, target, move) => !![ user, user.getAlly() ].filter(p => p?.isActive()).find(p => !![ Abilities.PLUS, Abilities.MINUS].find(a => p.hasAbility(a, false)))), new StatusMove(Moves.HAPPY_HOUR, Type.NORMAL, -1, 30, -1, 0, 6) // No animation @@ -8304,6 +8499,7 @@ export function initMoves() { .target(MoveTarget.ALL_NEAR_ENEMIES), new SelfStatusMove(Moves.CELEBRATE, Type.NORMAL, -1, 40, -1, 0, 6), new StatusMove(Moves.HOLD_HANDS, Type.NORMAL, -1, 40, -1, 0, 6) + .ignoresSubstitute() .target(MoveTarget.NEAR_ALLY), new StatusMove(Moves.BABY_DOLL_EYES, Type.FAIRY, 100, 30, -1, 1, 6) .attr(StatStageChangeAttr, [ Stat.ATK ], -1), @@ -8323,8 +8519,8 @@ export function initMoves() { new AttackMove(Moves.THOUSAND_ARROWS, Type.GROUND, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6) .attr(NeutralDamageAgainstFlyingTypeMultiplierAttr) .attr(AddBattlerTagAttr, BattlerTagType.IGNORE_FLYING, false, false, 1, 1, true) - .attr(HitsTagAttr, BattlerTagType.FLYING, false) - .attr(HitsTagAttr, BattlerTagType.MAGNET_RISEN, false) + .attr(HitsTagAttr, BattlerTagType.FLYING) + .attr(HitsTagAttr, BattlerTagType.MAGNET_RISEN) .attr(AddBattlerTagAttr, BattlerTagType.INTERRUPTED) .attr(RemoveBattlerTagAttr, [BattlerTagType.FLYING, BattlerTagType.MAGNET_RISEN]) .makesContact(false) @@ -8349,6 +8545,7 @@ export function initMoves() { .attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF ], -1, true), new AttackMove(Moves.HYPERSPACE_FURY, Type.DARK, MoveCategory.PHYSICAL, 100, -1, 5, -1, 0, 6) .attr(StatStageChangeAttr, [ Stat.DEF ], -1, true) + .ignoresSubstitute() .makesContact(false) .ignoresProtect(), /* Unused */ @@ -8508,6 +8705,7 @@ export function initMoves() { .attr(AddBattlerTagAttr, BattlerTagType.ALWAYS_CRIT, true, false), new StatusMove(Moves.GEAR_UP, Type.STEEL, -1, 20, -1, 0, 7) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, (user, target, move) => !![ Abilities.PLUS, Abilities.MINUS].find(a => target.hasAbility(a, false))) + .ignoresSubstitute() .target(MoveTarget.USER_AND_ALLIES) .condition((user, target, move) => !![ user, user.getAlly() ].filter(p => p?.isActive()).find(p => !![ Abilities.PLUS, Abilities.MINUS].find(a => p.hasAbility(a, false)))), new AttackMove(Moves.THROAT_CHOP, Type.DARK, MoveCategory.PHYSICAL, 80, 100, 15, 100, 0, 7) @@ -8538,7 +8736,8 @@ export function initMoves() { user.scene.queueMessage(i18next.t("moveTriggers:burnedItselfOut", {pokemonName: getPokemonNameWithAffix(user)})); }), new StatusMove(Moves.SPEED_SWAP, Type.PSYCHIC, -1, 10, -1, 0, 7) - .attr(SwapStatAttr, Stat.SPD), + .attr(SwapStatAttr, Stat.SPD) + .ignoresSubstitute(), new AttackMove(Moves.SMART_STRIKE, Type.STEEL, MoveCategory.PHYSICAL, 70, -1, 10, -1, 0, 7), new StatusMove(Moves.PURIFY, Type.POISON, -1, 20, -1, 0, 7) .condition( @@ -8555,6 +8754,7 @@ export function initMoves() { new AttackMove(Moves.TROP_KICK, Type.GRASS, MoveCategory.PHYSICAL, 70, 100, 15, 100, 0, 7) .attr(StatStageChangeAttr, [ Stat.ATK ], -1), new StatusMove(Moves.INSTRUCT, Type.PSYCHIC, -1, 15, -1, 0, 7) + .ignoresSubstitute() .unimplemented(), new AttackMove(Moves.BEAK_BLAST, Type.FLYING, MoveCategory.PHYSICAL, 100, 100, 15, -1, -3, 7) .attr(BeakBlastHeaderAttr) @@ -8577,6 +8777,8 @@ export function initMoves() { .partial() .ignoresVirtual(), new AttackMove(Moves.MALICIOUS_MOONSAULT, Type.DARK, MoveCategory.PHYSICAL, 180, -1, 1, -1, 0, 7) + .attr(AlwaysHitMinimizeAttr) + .attr(HitsTagAttr, BattlerTagType.MINIMIZED, true) .partial() .ignoresVirtual(), new AttackMove(Moves.OCEANIC_OPERETTA, Type.WATER, MoveCategory.SPECIAL, 195, -1, 1, -1, 0, 7) @@ -8622,6 +8824,7 @@ export function initMoves() { new AttackMove(Moves.PRISMATIC_LASER, Type.PSYCHIC, MoveCategory.SPECIAL, 160, 100, 10, -1, 0, 7) .attr(RechargeAttr), new AttackMove(Moves.SPECTRAL_THIEF, Type.GHOST, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 7) + .ignoresSubstitute() .partial(), new AttackMove(Moves.SUNSTEEL_STRIKE, Type.STEEL, MoveCategory.PHYSICAL, 100, 100, 5, -1, 0, 7) .ignoresAbilities() @@ -8870,8 +9073,7 @@ export function initMoves() { new AttackMove(Moves.AURA_WHEEL, Type.ELECTRIC, MoveCategory.PHYSICAL, 110, 100, 10, 100, 0, 8) .attr(StatStageChangeAttr, [ Stat.SPD ], 1, true) .makesContact(false) - .attr(AuraWheelTypeAttr) - .condition((user, target, move) => [user.species.speciesId, user.fusionSpecies?.speciesId].includes(Species.MORPEKO)), // Missing custom fail message + .attr(AuraWheelTypeAttr), new AttackMove(Moves.BREAKING_SWIPE, Type.DRAGON, MoveCategory.PHYSICAL, 60, 100, 15, 100, 0, 8) .target(MoveTarget.ALL_NEAR_ENEMIES) .attr(StatStageChangeAttr, [ Stat.ATK ], -1), @@ -8923,7 +9125,7 @@ export function initMoves() { new AttackMove(Moves.SHELL_SIDE_ARM, Type.POISON, MoveCategory.SPECIAL, 90, 100, 10, 20, 0, 8) .attr(ShellSideArmCategoryAttr) .attr(StatusEffectAttr, StatusEffect.POISON) - .partial(), + .partial(), // Physical version of the move does not make contact new AttackMove(Moves.MISTY_EXPLOSION, Type.FAIRY, MoveCategory.SPECIAL, 100, 100, 5, -1, 0, 8) .attr(SacrificialAttr) .target(MoveTarget.ALL_NEAR_OTHERS) @@ -8931,7 +9133,7 @@ export function initMoves() { .condition(failIfDampCondition) .makesContact(false), new AttackMove(Moves.GRASSY_GLIDE, Type.GRASS, MoveCategory.PHYSICAL, 55, 100, 20, -1, 0, 8) - .attr(IncrementMovePriorityAttr, (user, target, move) =>user.scene.arena.getTerrainType()===TerrainType.GRASSY&&user.isGrounded()), + .attr(IncrementMovePriorityAttr, (user, target, move) => user.scene.arena.getTerrainType() === TerrainType.GRASSY && user.isGrounded()), new AttackMove(Moves.RISING_VOLTAGE, Type.ELECTRIC, MoveCategory.SPECIAL, 70, 100, 20, -1, 0, 8) .attr(MovePowerMultiplierAttr, (user, target, move) => user.scene.arena.getTerrainType() === TerrainType.ELECTRIC && target.isGrounded() ? 2 : 1), new AttackMove(Moves.TERRAIN_PULSE, Type.NORMAL, MoveCategory.SPECIAL, 50, 100, 10, -1, 0, 8) @@ -9289,7 +9491,8 @@ export function initMoves() { .target(MoveTarget.BOTH_SIDES), new SelfStatusMove(Moves.TIDY_UP, Type.NORMAL, -1, 10, -1, 0, 9) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPD ], 1, true, null, true, true) - .attr(RemoveArenaTrapAttr, true), + .attr(RemoveArenaTrapAttr, true) + .attr(RemoveAllSubstitutesAttr), new StatusMove(Moves.SNOWSCAPE, Type.ICE, -1, 10, -1, 0, 9) .attr(WeatherChangeAttr, WeatherType.SNOW) .target(MoveTarget.BOTH_SIDES), @@ -9406,7 +9609,7 @@ export function initMoves() { .recklessMove(), new AttackMove(Moves.PSYCHIC_NOISE, Type.PSYCHIC, MoveCategory.SPECIAL, 75, 100, 10, -1, 0, 9) .soundBased() - .partial(), + .attr(AddBattlerTagAttr, BattlerTagType.HEAL_BLOCK, false, false, 2), new AttackMove(Moves.UPPER_HAND, Type.FIGHTING, MoveCategory.PHYSICAL, 65, 100, 15, 100, 3, 9) .attr(FlinchAttr) .condition((user, target, move) => user.scene.currentBattle.turnCommands[target.getBattlerIndex()]?.command === Command.FIGHT && !target.turnData.acted && allMoves[user.scene.currentBattle.turnCommands[target.getBattlerIndex()]?.move?.move!].category !== MoveCategory.STATUS && allMoves[user.scene.currentBattle.turnCommands[target.getBattlerIndex()]?.move?.move!].priority > 0 ) // TODO: is this bang correct? diff --git a/src/data/mystery-encounters/encounters/a-trainers-test-encounter.ts b/src/data/mystery-encounters/encounters/a-trainers-test-encounter.ts new file mode 100644 index 00000000000..f7666fa1b37 --- /dev/null +++ b/src/data/mystery-encounters/encounters/a-trainers-test-encounter.ts @@ -0,0 +1,185 @@ +import { EnemyPartyConfig, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, setEncounterRewards, transitionMysteryEncounterIntroVisuals, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { trainerConfigs, } from "#app/data/trainer-config"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import BattleScene from "#app/battle-scene"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { TrainerType } from "#enums/trainer-type"; +import { Species } from "#enums/species"; +import { getSpriteKeysFromSpecies } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { randSeedInt } from "#app/utils"; +import i18next from "i18next"; +import { IEggOptions } from "#app/data/egg"; +import { EggSourceType } from "#enums/egg-source-types"; +import { EggTier } from "#enums/egg-type"; +import { PartyHealPhase } from "#app/phases/party-heal-phase"; +import { ModifierTier } from "#app/modifier/modifier-tier"; +import { modifierTypes } from "#app/modifier/modifier-type"; + +/** the i18n namespace for the encounter */ +const namespace = "mysteryEncounter:aTrainersTest"; + +/** + * A Trainer's Test encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3816 | GitHub Issue #3816} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const ATrainersTestEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.A_TRAINERS_TEST) + .withEncounterTier(MysteryEncounterTier.ROGUE) + .withSceneWaveRangeRequirement(100, 180) + .withIntroSpriteConfigs([]) // These are set in onInit() + .withIntroDialogue([ + { + text: `${namespace}.intro`, + }, + ]) + .withAutoHideIntroVisuals(false) + .withOnInit((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + + // Randomly pick from 1 of the 5 stat trainers to spawn + let trainerType: TrainerType; + let spriteKeys; + let trainerNameKey: string; + switch (randSeedInt(5)) { + default: + case 0: + trainerType = TrainerType.BUCK; + spriteKeys = getSpriteKeysFromSpecies(Species.CLAYDOL); + trainerNameKey = "buck"; + break; + case 1: + trainerType = TrainerType.CHERYL; + spriteKeys = getSpriteKeysFromSpecies(Species.BLISSEY); + trainerNameKey = "cheryl"; + break; + case 2: + trainerType = TrainerType.MARLEY; + spriteKeys = getSpriteKeysFromSpecies(Species.ARCANINE); + trainerNameKey = "marley"; + break; + case 3: + trainerType = TrainerType.MIRA; + spriteKeys = getSpriteKeysFromSpecies(Species.ALAKAZAM, false, 1); + trainerNameKey = "mira"; + break; + case 4: + trainerType = TrainerType.RILEY; + spriteKeys = getSpriteKeysFromSpecies(Species.LUCARIO, false, 1); + trainerNameKey = "riley"; + break; + } + + // Dialogue and tokens for trainer + encounter.dialogue.intro = [ + { + speaker: `trainerNames:${trainerNameKey}`, + text: `${namespace}.${trainerNameKey}.intro_dialogue` + } + ]; + encounter.options[0].dialogue!.selected = [ + { + speaker: `trainerNames:${trainerNameKey}`, + text: `${namespace}.${trainerNameKey}.accept` + } + ]; + encounter.options[1].dialogue!.selected = [ + { + speaker: `trainerNames:${trainerNameKey}`, + text: `${namespace}.${trainerNameKey}.decline` + } + ]; + + encounter.setDialogueToken("statTrainerName", i18next.t(`trainerNames:${trainerNameKey}`)); + const eggDescription = i18next.t(`${namespace}.title`) + ":\n" + i18next.t(`trainerNames:${trainerNameKey}`); + encounter.misc = { trainerType, trainerNameKey, trainerEggDescription: eggDescription }; + + // Trainer config + const trainerConfig = trainerConfigs[trainerType].clone(); + const trainerSpriteKey = trainerConfig.getSpriteKey(); + encounter.enemyPartyConfigs.push({ + levelAdditiveModifier: 1, + trainerConfig: trainerConfig + }); + + encounter.spriteConfigs = [ + { + spriteKey: spriteKeys.spriteKey, + fileRoot: spriteKeys.fileRoot, + hasShadow: true, + repeat: true, + isPokemon: true, + x: 22, + y: -2, + yShadow: -2 + }, + { + spriteKey: trainerSpriteKey, + fileRoot: "trainer", + hasShadow: true, + disableAnimation: true, + x: -24, + y: 4, + yShadow: 4 + } + ]; + + return true; + }) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withIntroDialogue() + .withSimpleOption( + { + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip` + }, + async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + // Battle the stat trainer for an Egg and great rewards + const config: EnemyPartyConfig = encounter.enemyPartyConfigs[0]; + + await transitionMysteryEncounterIntroVisuals(scene); + + const eggOptions: IEggOptions = { + scene, + pulled: false, + sourceType: EggSourceType.EVENT, + eggDescriptor: encounter.misc.trainerEggDescription, + tier: EggTier.ULTRA + }; + encounter.setDialogueToken("eggType", i18next.t(`${namespace}.eggTypes.epic`)); + setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.SACRED_ASH], guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ULTRA], fillRemaining: true }, [eggOptions]); + return initBattleWithEnemyConfig(scene, config); + } + ) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip` + }, + async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + // Full heal party + scene.unshiftPhase(new PartyHealPhase(scene, true)); + + const eggOptions: IEggOptions = { + scene, + pulled: false, + sourceType: EggSourceType.EVENT, + eggDescriptor: encounter.misc.trainerEggDescription, + tier: EggTier.GREAT + }; + encounter.setDialogueToken("eggType", i18next.t(`${namespace}.eggTypes.rare`)); + setEncounterRewards(scene, { fillRemaining: false, rerollMultiplier: -1 }, [eggOptions]); + leaveEncounterWithoutBattle(scene); + } + ) + .withOutroDialogue([ + { + text: `${namespace}.outro` + } + ]) + .build(); diff --git a/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts b/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts new file mode 100644 index 00000000000..c9605aadc49 --- /dev/null +++ b/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts @@ -0,0 +1,522 @@ +import { EnemyPartyConfig, generateModifierType, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, setEncounterRewards, transitionMysteryEncounterIntroVisuals, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import Pokemon, { EnemyPokemon, PokemonMove } from "#app/field/pokemon"; +import { BerryModifierType, modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { Species } from "#enums/species"; +import BattleScene from "#app/battle-scene"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { PersistentModifierRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; +import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { BerryModifier } from "#app/modifier/modifier"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { Moves } from "#enums/moves"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { randInt } from "#app/utils"; +import { BattlerIndex } from "#app/battle"; +import { applyModifierTypeToPlayerPokemon, catchPokemon, getHighestLevelPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { TrainerSlot } from "#app/data/trainer-config"; +import { PokeballType } from "#app/data/pokeball"; +import HeldModifierConfig from "#app/interfaces/held-modifier-config"; +import { BerryType } from "#enums/berry-type"; +import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; +import { Stat } from "#enums/stat"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; + +/** the i18n namespace for this encounter */ +const namespace = "mysteryEncounter:absoluteAvarice"; + +/** + * Absolute Avarice encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3805 | GitHub Issue #3805} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const AbsoluteAvariceEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.ABSOLUTE_AVARICE) + .withEncounterTier(MysteryEncounterTier.GREAT) + .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + .withSceneRequirement(new PersistentModifierRequirement("BerryModifier", 4)) // Must have at least 4 berries to spawn + .withIntroSpriteConfigs([ + { + // This sprite has the shadow + spriteKey: "", + fileRoot: "", + species: Species.GREEDENT, + hasShadow: true, + alpha: 0.001, + repeat: true, + x: -5 + }, + { + spriteKey: "", + fileRoot: "", + species: Species.GREEDENT, + hasShadow: false, + repeat: true, + x: -5 + }, + { + spriteKey: "lum_berry", + fileRoot: "items", + isItem: true, + x: 7, + y: -14, + hidden: true, + disableAnimation: true + }, + { + spriteKey: "salac_berry", + fileRoot: "items", + isItem: true, + x: 2, + y: 4, + hidden: true, + disableAnimation: true + }, + { + spriteKey: "lansat_berry", + fileRoot: "items", + isItem: true, + x: 32, + y: 5, + hidden: true, + disableAnimation: true + }, + { + spriteKey: "liechi_berry", + fileRoot: "items", + isItem: true, + x: 6, + y: -5, + hidden: true, + disableAnimation: true + }, + { + spriteKey: "sitrus_berry", + fileRoot: "items", + isItem: true, + x: 7, + y: 8, + hidden: true, + disableAnimation: true + }, + { + spriteKey: "enigma_berry", + fileRoot: "items", + isItem: true, + x: 26, + y: -4, + hidden: true, + disableAnimation: true + }, + { + spriteKey: "leppa_berry", + fileRoot: "items", + isItem: true, + x: 16, + y: -27, + hidden: true, + disableAnimation: true + }, + { + spriteKey: "petaya_berry", + fileRoot: "items", + isItem: true, + x: 30, + y: -17, + hidden: true, + disableAnimation: true + }, + { + spriteKey: "ganlon_berry", + fileRoot: "items", + isItem: true, + x: 16, + y: -11, + hidden: true, + disableAnimation: true + }, + { + spriteKey: "apicot_berry", + fileRoot: "items", + isItem: true, + x: 14, + y: -2, + hidden: true, + disableAnimation: true + }, + { + spriteKey: "starf_berry", + fileRoot: "items", + isItem: true, + x: 18, + y: 9, + hidden: true, + disableAnimation: true + }, + ]) + .withHideWildIntroMessage(true) + .withAutoHideIntroVisuals(false) + .withOnVisualsStart((scene: BattleScene) => { + doGreedentSpriteSteal(scene); + doBerrySpritePile(scene); + + return true; + }) + .withIntroDialogue([ + { + text: `${namespace}.intro`, + } + ]) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withOnInit((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + + scene.loadSe("PRSFX- Bug Bite", "battle_anims", "PRSFX- Bug Bite.wav"); + scene.loadSe("Follow Me", "battle_anims", "Follow Me.mp3"); + + // Get all player berry items, remove from party, and store reference + const berryItems = scene.findModifiers(m => m instanceof BerryModifier) as BerryModifier[]; + + // Sort berries by party member ID to more easily re-add later if necessary + const berryItemsMap = new Map(); + scene.getParty().forEach(pokemon => { + const pokemonBerries = berryItems.filter(b => b.pokemonId === pokemon.id); + if (pokemonBerries?.length > 0) { + berryItemsMap.set(pokemon.id, pokemonBerries); + } + }); + + encounter.misc = { berryItemsMap }; + + // Generates copies of the stolen berries to put on the Greedent + const bossModifierConfigs: HeldModifierConfig[] = []; + berryItems.forEach(berryMod => { + // Can't define stack count on a ModifierType, have to just create separate instances for each stack + // Overflow berries will be "lost" on the boss, but it's un-catchable anyway + for (let i = 0; i < berryMod.stackCount; i++) { + const modifierType = generateModifierType(scene, modifierTypes.BERRY, [berryMod.berryType]) as PokemonHeldItemModifierType; + bossModifierConfigs.push({ modifier: modifierType }); + } + + scene.removeModifier(berryMod); + }); + + // Calculate boss mon + const config: EnemyPartyConfig = { + levelAdditiveModifier: 1, + pokemonConfigs: [ + { + species: getPokemonSpecies(Species.GREEDENT), + isBoss: true, + bossSegments: 3, + moveSet: [Moves.THRASH, Moves.BODY_PRESS, Moves.STUFF_CHEEKS, Moves.SLACK_OFF], + modifierConfigs: bossModifierConfigs, + tags: [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON], + mysteryEncounterBattleEffects: (pokemon: Pokemon) => { + queueEncounterMessage(pokemon.scene, `${namespace}.option.1.boss_enraged`); + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD], 1)); + } + } + ], + }; + + encounter.enemyPartyConfigs = [config]; + encounter.setDialogueToken("greedentName", getPokemonSpecies(Species.GREEDENT).getName()); + + return true; + }) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }) + .withOptionPhase(async (scene: BattleScene) => { + // Pick battle + const encounter = scene.currentBattle.mysteryEncounter!; + + // Provides 1x Reviver Seed to each party member at end of battle + const revSeed = generateModifierType(scene, modifierTypes.REVIVER_SEED); + const givePartyPokemonReviverSeeds = () => { + const party = scene.getParty(); + party.forEach(p => { + if (revSeed) { + const seedModifier = revSeed.newModifier(p); + if (seedModifier) { + encounter.setDialogueToken("foodReward", seedModifier.type.name); + } + scene.addModifier(seedModifier, false, false, false, true); + } + }); + queueEncounterMessage(scene, `${namespace}.option.1.food_stash`); + }; + + setEncounterRewards(scene, { fillRemaining: true }, undefined, givePartyPokemonReviverSeeds); + encounter.startOfBattleEffects.push({ + sourceBattlerIndex: BattlerIndex.ENEMY, + targets: [BattlerIndex.ENEMY], + move: new PokemonMove(Moves.STUFF_CHEEKS), + ignorePp: true + }); + + transitionMysteryEncounterIntroVisuals(scene, true, true, 500); + await initBattleWithEnemyConfig(scene, encounter.enemyPartyConfigs[0]); + }) + .build() + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected`, + }, + ], + }) + .withOptionPhase(async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + const berryMap = encounter.misc.berryItemsMap; + + // Returns 2/5 of the berries stolen to each Pokemon + const party = scene.getParty(); + party.forEach(pokemon => { + const stolenBerries: BerryModifier[] = berryMap.get(pokemon.id); + const berryTypesAsArray: BerryType[] = []; + stolenBerries?.forEach(bMod => berryTypesAsArray.push(...new Array(bMod.stackCount).fill(bMod.berryType))); + const returnedBerryCount = Math.floor((berryTypesAsArray.length ?? 0) * 2 / 5); + + if (returnedBerryCount > 0) { + for (let i = 0; i < returnedBerryCount; i++) { + // Shuffle remaining berry types and pop + Phaser.Math.RND.shuffle(berryTypesAsArray); + const randBerryType = berryTypesAsArray.pop(); + + const berryModType = generateModifierType(scene, modifierTypes.BERRY, [randBerryType]) as BerryModifierType; + applyModifierTypeToPlayerPokemon(scene, pokemon, berryModType); + } + } + }); + await scene.updateModifiers(true); + + transitionMysteryEncounterIntroVisuals(scene, true, true, 500); + leaveEncounterWithoutBattle(scene, true); + }) + .build() + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + selected: [ + { + text: `${namespace}.option.3.selected`, + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene) => { + // Animate berries being eaten + doGreedentEatBerries(scene); + doBerrySpritePile(scene, true); + return true; + }) + .withOptionPhase(async (scene: BattleScene) => { + // Let it have the food + // Greedent joins the team, level equal to 2 below highest party member + const level = getHighestLevelPlayerPokemon(scene, false, true).level - 2; + const greedent = new EnemyPokemon(scene, getPokemonSpecies(Species.GREEDENT), level, TrainerSlot.NONE, false); + greedent.moveset = [new PokemonMove(Moves.THRASH), new PokemonMove(Moves.BODY_PRESS), new PokemonMove(Moves.STUFF_CHEEKS), new PokemonMove(Moves.SLACK_OFF)]; + greedent.passive = true; + + transitionMysteryEncounterIntroVisuals(scene, true, true, 500); + await catchPokemon(scene, greedent, null, PokeballType.POKEBALL, false); + leaveEncounterWithoutBattle(scene, true); + }) + .build() + ) + .build(); + +function doGreedentSpriteSteal(scene: BattleScene) { + const shakeDelay = 50; + const slideDelay = 500; + + const greedentSprites = scene.currentBattle.mysteryEncounter!.introVisuals?.getSpriteAtIndex(1); + + scene.playSound("battle_anims/Follow Me"); + scene.tweens.chain({ + targets: greedentSprites, + tweens: [ + { // Slide Greedent diagonally + duration: slideDelay, + ease: "Cubic.easeOut", + y: "+=75", + x: "-=65", + scale: 1.1 + }, + { // Shake + duration: shakeDelay, + ease: "Cubic.easeOut", + yoyo: true, + x: (randInt(2) > 0 ? "-=" : "+=") + 5, + y: (randInt(2) > 0 ? "-=" : "+=") + 5, + }, + { // Shake + duration: shakeDelay, + ease: "Cubic.easeOut", + yoyo: true, + x: (randInt(2) > 0 ? "-=" : "+=") + 5, + y: (randInt(2) > 0 ? "-=" : "+=") + 5, + }, + { // Shake + duration: shakeDelay, + ease: "Cubic.easeOut", + yoyo: true, + x: (randInt(2) > 0 ? "-=" : "+=") + 5, + y: (randInt(2) > 0 ? "-=" : "+=") + 5, + }, + { // Shake + duration: shakeDelay, + ease: "Cubic.easeOut", + yoyo: true, + x: (randInt(2) > 0 ? "-=" : "+=") + 5, + y: (randInt(2) > 0 ? "-=" : "+=") + 5, + }, + { // Shake + duration: shakeDelay, + ease: "Cubic.easeOut", + yoyo: true, + x: (randInt(2) > 0 ? "-=" : "+=") + 5, + y: (randInt(2) > 0 ? "-=" : "+=") + 5, + }, + { // Shake + duration: shakeDelay, + ease: "Cubic.easeOut", + yoyo: true, + x: (randInt(2) > 0 ? "-=" : "+=") + 5, + y: (randInt(2) > 0 ? "-=" : "+=") + 5, + }, + { // Slide Greedent diagonally + duration: slideDelay, + ease: "Cubic.easeOut", + y: "-=75", + x: "+=65", + scale: 1 + }, + { // Bounce at the end + duration: 300, + ease: "Cubic.easeOut", + yoyo: true, + y: "-=20", + loop: 1, + } + ] + }); +} + +function doGreedentEatBerries(scene: BattleScene) { + const greedentSprites = scene.currentBattle.mysteryEncounter!.introVisuals?.getSpriteAtIndex(1); + let index = 1; + scene.tweens.add({ + targets: greedentSprites, + duration: 150, + ease: "Cubic.easeOut", + yoyo: true, + y: "-=8", + loop: 5, + onStart: () => { + scene.playSound("battle_anims/PRSFX- Bug Bite"); + }, + onLoop: () => { + if (index % 2 === 0) { + scene.playSound("battle_anims/PRSFX- Bug Bite"); + } + index++; + } + }); +} + +/** + * + * @param scene + * @param isEat Default false. Will "create" pile when false, and remove pile when true. + */ +function doBerrySpritePile(scene: BattleScene, isEat: boolean = false) { + const berryAddDelay = 150; + let animationOrder = ["starf", "sitrus", "lansat", "salac", "apicot", "enigma", "liechi", "ganlon", "lum", "petaya", "leppa"]; + if (isEat) { + animationOrder = animationOrder.reverse(); + } + const encounter = scene.currentBattle.mysteryEncounter!; + animationOrder.forEach((berry, i) => { + const introVisualsIndex = encounter.spriteConfigs.findIndex(config => config.spriteKey?.includes(berry)); + let sprite: Phaser.GameObjects.Sprite, tintSprite: Phaser.GameObjects.Sprite; + const sprites = encounter.introVisuals?.getSpriteAtIndex(introVisualsIndex); + if (sprites) { + sprite = sprites[0]; + tintSprite = sprites[1]; + } + scene.time.delayedCall(berryAddDelay * i + 400, () => { + if (sprite) { + sprite.setVisible(!isEat); + } + if (tintSprite) { + tintSprite.setVisible(!isEat); + } + + // Animate Petaya berry falling off the pile + if (berry === "petaya" && sprite && tintSprite && !isEat) { + scene.time.delayedCall(200, () => { + doBerryBounce(scene, [sprite, tintSprite], 30, 500); + }); + } + }); + }); +} + +function doBerryBounce(scene: BattleScene, berrySprites: Phaser.GameObjects.Sprite[], yd: number, baseBounceDuration: number) { + let bouncePower = 1; + let bounceYOffset = yd; + + const doBounce = () => { + scene.tweens.add({ + targets: berrySprites, + y: "+=" + bounceYOffset, + x: { value: "+=" + (bouncePower * bouncePower * 10), ease: "Linear" }, + duration: bouncePower * baseBounceDuration, + ease: "Cubic.easeIn", + onComplete: () => { + bouncePower = bouncePower > 0.01 ? bouncePower * 0.5 : 0; + + if (bouncePower) { + bounceYOffset = bounceYOffset * bouncePower; + + scene.tweens.add({ + targets: berrySprites, + y: "-=" + bounceYOffset, + x: { value: "+=" + (bouncePower * bouncePower * 10), ease: "Linear" }, + duration: bouncePower * baseBounceDuration, + ease: "Cubic.easeOut", + onComplete: () => doBounce() + }); + } + } + }); + }; + + doBounce(); +} 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 new file mode 100644 index 00000000000..62025957abe --- /dev/null +++ b/src/data/mystery-encounters/encounters/an-offer-you-cant-refuse-encounter.ts @@ -0,0 +1,165 @@ +import { leaveEncounterWithoutBattle, setEncounterExp, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { modifierTypes } from "#app/modifier/modifier-type"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { Species } from "#enums/species"; +import BattleScene from "#app/battle-scene"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { AbilityRequirement, CombinationPokemonRequirement, MoveRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; +import { getHighestStatTotalPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { EXTORTION_ABILITIES, EXTORTION_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { ModifierRewardPhase } from "#app/phases/modifier-reward-phase"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; + +/** the i18n namespace for this encounter */ +const namespace = "mysteryEncounter:offerYouCantRefuse"; + +/** + * An Offer You Can't Refuse encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3808 | GitHub Issue #3808} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const AnOfferYouCantRefuseEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE) + .withEncounterTier(MysteryEncounterTier.GREAT) + .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + .withScenePartySizeRequirement(2, 6, true) // Must have at least 2 pokemon in party + .withIntroSpriteConfigs([ + { + spriteKey: Species.LIEPARD.toString(), + fileRoot: "pokemon", + hasShadow: true, + repeat: true, + x: 0, + y: -4, + yShadow: -4 + }, + { + spriteKey: "rich_kid_m", + fileRoot: "trainer", + hasShadow: true, + x: 2, + y: 5, + yShadow: 5 + }, + ]) + .withIntroDialogue([ + { + text: `${namespace}.intro`, + }, + { + text: `${namespace}.intro_dialogue`, + speaker: `${namespace}.speaker`, + }, + ]) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withOnInit((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + const pokemon = getHighestStatTotalPlayerPokemon(scene, true, true); + const price = scene.getWaveMoneyAmount(10); + + encounter.setDialogueToken("strongestPokemon", pokemon.getNameToRender()); + encounter.setDialogueToken("price", price.toString()); + + // Store pokemon and price + encounter.misc = { + pokemon: pokemon, + price: price + }; + + // If player meets the combo OR requirements for option 2, populate the token + const opt2Req = encounter.options[1].primaryPokemonRequirements[0]; + if (opt2Req.meetsRequirement(scene)) { + const abilityToken = encounter.dialogueTokens["option2PrimaryAbility"]; + const moveToken = encounter.dialogueTokens["option2PrimaryMove"]; + if (abilityToken) { + encounter.setDialogueToken("moveOrAbility", abilityToken); + } else if (moveToken) { + encounter.setDialogueToken("moveOrAbility", moveToken); + } + } + + encounter.setDialogueToken("liepardName", getPokemonSpecies(Species.LIEPARD).getName()); + + return true; + }) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + speaker: `${namespace}.speaker`, + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene): Promise => { + const encounter = scene.currentBattle.mysteryEncounter!; + // Update money and remove pokemon from party + updatePlayerMoney(scene, encounter.misc.price); + scene.removePokemonFromPlayerParty(encounter.misc.pokemon); + return true; + }) + .withOptionPhase(async (scene: BattleScene) => { + // Give the player a Shiny charm + scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.SHINY_CHARM)); + leaveEncounterWithoutBattle(scene, true); + }) + .build() + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL) + .withPrimaryPokemonRequirement(new CombinationPokemonRequirement( + new MoveRequirement(EXTORTION_MOVES), + new AbilityRequirement(EXTORTION_ABILITIES)) + ) + .withDialogue({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + disabledButtonTooltip: `${namespace}.option.2.tooltip_disabled`, + selected: [ + { + speaker: `${namespace}.speaker`, + text: `${namespace}.option.2.selected`, + }, + ], + }) + .withOptionPhase(async (scene: BattleScene) => { + // Extort the rich kid for money + const encounter = scene.currentBattle.mysteryEncounter!; + // Update money and remove pokemon from party + updatePlayerMoney(scene, encounter.misc.price); + + setEncounterExp(scene, encounter.options[1].primaryPokemon!.id, getPokemonSpecies(Species.LIEPARD).baseExp, true); + + leaveEncounterWithoutBattle(scene, true); + }) + .build() + ) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + selected: [ + { + speaker: `${namespace}.speaker`, + text: `${namespace}.option.3.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Leave encounter with no rewards or exp + leaveEncounterWithoutBattle(scene, true); + return true; + } + ) + .build(); diff --git a/src/data/mystery-encounters/encounters/berries-abound-encounter.ts b/src/data/mystery-encounters/encounters/berries-abound-encounter.ts new file mode 100644 index 00000000000..9ff223947f5 --- /dev/null +++ b/src/data/mystery-encounters/encounters/berries-abound-encounter.ts @@ -0,0 +1,272 @@ +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { + EnemyPartyConfig, generateModifierType, generateModifierTypeOption, + initBattleWithEnemyConfig, + leaveEncounterWithoutBattle, setEncounterExp, + setEncounterRewards +} from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import Pokemon, { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon"; +import { + BerryModifierType, + getPartyLuckValue, + ModifierPoolType, + ModifierTypeOption, modifierTypes, + regenerateModifierPoolThresholds, +} from "#app/modifier/modifier-type"; +import { randSeedInt } from "#app/utils"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import BattleScene from "#app/battle-scene"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { getPokemonNameWithAffix } from "#app/messages"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { TrainerSlot } from "#app/data/trainer-config"; +import { applyModifierTypeToPlayerPokemon, getEncounterPokemonLevelForWave, getHighestStatPlayerPokemon, getSpriteKeysFromPokemon, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import PokemonData from "#app/system/pokemon-data"; +import { BerryModifier } from "#app/modifier/modifier"; +import i18next from "#app/plugins/i18n"; +import { BerryType } from "#enums/berry-type"; +import { PERMANENT_STATS, Stat } from "#enums/stat"; +import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; + +/** the i18n namespace for the encounter */ +const namespace = "mysteryEncounter:berriesAbound"; + +/** + * Berries Abound encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3810 | GitHub Issue #3810} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const BerriesAboundEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.BERRIES_ABOUND) + .withEncounterTier(MysteryEncounterTier.COMMON) + .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + .withCatchAllowed(true) + .withHideWildIntroMessage(true) + .withIntroSpriteConfigs([]) // Set in onInit() + .withIntroDialogue([ + { + text: `${namespace}.intro`, + }, + ]) + .withOnInit((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + + // Calculate boss mon + const level = getEncounterPokemonLevelForWave(scene, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER); + const bossSpecies = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getParty()), true); + const bossPokemon = new EnemyPokemon(scene, bossSpecies, level, TrainerSlot.NONE, true); + encounter.setDialogueToken("enemyPokemon", getPokemonNameWithAffix(bossPokemon)); + const config: EnemyPartyConfig = { + pokemonConfigs: [{ + level: level, + species: bossSpecies, + dataSource: new PokemonData(bossPokemon), + isBoss: true + }], + }; + encounter.enemyPartyConfigs = [config]; + + // Calculate the number of extra berries that player receives + // 10-40: 2, 40-120: 4, 120-160: 5, 160-180: 7 + const numBerries = + scene.currentBattle.waveIndex > 160 ? 7 + : scene.currentBattle.waveIndex > 120 ? 5 + : scene.currentBattle.waveIndex > 40 ? 4 : 2; + regenerateModifierPoolThresholds(scene.getParty(), ModifierPoolType.PLAYER, 0); + encounter.misc = { numBerries }; + + const { spriteKey, fileRoot } = getSpriteKeysFromPokemon(bossPokemon); + encounter.spriteConfigs = [ + { + spriteKey: "berries_abound_bush", + fileRoot: "mystery-encounters", + x: 25, + y: -6, + yShadow: -7, + disableAnimation: true, + hasShadow: true + }, + { + spriteKey: spriteKey, + fileRoot: fileRoot, + hasShadow: true, + tint: 0.25, + x: -5, + repeat: true, + isPokemon: true + } + ]; + + // Get fastest party pokemon for option 2 + const fastestPokemon = getHighestStatPlayerPokemon(scene, PERMANENT_STATS[Stat.SPD], true, false); + encounter.misc.fastestPokemon = fastestPokemon; + encounter.misc.enemySpeed = bossPokemon.getStat(Stat.SPD); + encounter.setDialogueToken("fastestPokemon", fastestPokemon.getNameToRender()); + + return true; + }) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Pick battle + const encounter = scene.currentBattle.mysteryEncounter!; + const numBerries = encounter.misc.numBerries; + + const doBerryRewards = async () => { + const berryText = numBerries + " " + i18next.t(`${namespace}.berries`); + + scene.playSound("item_fanfare"); + queueEncounterMessage(scene, i18next.t("battle:rewardGain", { modifierName: berryText })); + + // Generate a random berry and give it to the first Pokemon with room for it + for (let i = 0; i < numBerries; i++) { + await tryGiveBerry(scene); + } + }; + + const shopOptions: ModifierTypeOption[] = []; + for (let i = 0; i < 5; i++) { + // Generate shop berries + const mod = generateModifierTypeOption(scene, modifierTypes.BERRY); + if (mod) { + shopOptions.push(mod); + } + } + + setEncounterRewards(scene, { guaranteedModifierTypeOptions: shopOptions, fillRemaining: false }, undefined, doBerryRewards); + await initBattleWithEnemyConfig(scene, scene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0]); + } + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip` + }) + .withOptionPhase(async (scene: BattleScene) => { + // Pick race for berries + const encounter = scene.currentBattle.mysteryEncounter!; + const fastestPokemon: PlayerPokemon = encounter.misc.fastestPokemon; + const enemySpeed: number = encounter.misc.enemySpeed; + const speedDiff = fastestPokemon.getStat(Stat.SPD) / (enemySpeed * 1.1); + const numBerries: number = encounter.misc.numBerries; + + const shopOptions: ModifierTypeOption[] = []; + for (let i = 0; i < 5; i++) { + // Generate shop berries + const mod = generateModifierTypeOption(scene, modifierTypes.BERRY); + if (mod) { + shopOptions.push(mod); + } + } + + if (speedDiff < 1) { + // Caught and attacked by boss, gets +1 to all stats at start of fight + const doBerryRewards = async () => { + const berryText = numBerries + " " + i18next.t(`${namespace}.berries`); + + scene.playSound("item_fanfare"); + queueEncounterMessage(scene, i18next.t("battle:rewardGain", { modifierName: berryText })); + + // Generate a random berry and give it to the first Pokemon with room for it + for (let i = 0; i < numBerries; i++) { + await tryGiveBerry(scene); + } + }; + + const config = scene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0]; + config.pokemonConfigs![0].tags = [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON]; + config.pokemonConfigs![0].mysteryEncounterBattleEffects = (pokemon: Pokemon) => { + queueEncounterMessage(pokemon.scene, `${namespace}.option.2.boss_enraged`); + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD], 1)); + }; + setEncounterRewards(scene, { guaranteedModifierTypeOptions: shopOptions, fillRemaining: false }, undefined, doBerryRewards); + await showEncounterText(scene, `${namespace}.option.2.selected_bad`); + await initBattleWithEnemyConfig(scene, config); + return; + } else { + // Gains 1 berry for every 10% faster the player's pokemon is than the enemy, up to a max of numBerries, minimum of 2 + const numBerriesGrabbed = Math.max(Math.min(Math.round((speedDiff - 1)/0.08), numBerries), 2); + encounter.setDialogueToken("numBerries", String(numBerriesGrabbed)); + const doFasterBerryRewards = async () => { + const berryText = numBerriesGrabbed + " " + i18next.t(`${namespace}.berries`); + + scene.playSound("item_fanfare"); + queueEncounterMessage(scene, i18next.t("battle:rewardGain", { modifierName: berryText })); + + // Generate a random berry and give it to the first Pokemon with room for it (trying to give to fastest first) + for (let i = 0; i < numBerriesGrabbed; i++) { + await tryGiveBerry(scene, fastestPokemon); + } + }; + + setEncounterExp(scene, fastestPokemon.id, encounter.enemyPartyConfigs[0].pokemonConfigs![0].species.baseExp); + setEncounterRewards(scene, { guaranteedModifierTypeOptions: shopOptions, fillRemaining: false }, undefined, doFasterBerryRewards); + await showEncounterText(scene, `${namespace}.option.2.selected`); + leaveEncounterWithoutBattle(scene); + } + }) + .build() + ) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + selected: [ + { + text: `${namespace}.option.3.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Leave encounter with no rewards or exp + leaveEncounterWithoutBattle(scene, true); + return true; + } + ) + .build(); + +async function tryGiveBerry(scene: BattleScene, prioritizedPokemon?: PlayerPokemon) { + const berryType = randSeedInt(Object.keys(BerryType).filter(s => !isNaN(Number(s))).length) as BerryType; + const berry = generateModifierType(scene, modifierTypes.BERRY, [berryType]) as BerryModifierType; + + const party = scene.getParty(); + + // Will try to apply to prioritized pokemon first, then do normal application method if it fails + if (prioritizedPokemon) { + const heldBerriesOfType = scene.findModifier(m => m instanceof BerryModifier + && m.pokemonId === prioritizedPokemon.id && (m as BerryModifier).berryType === berryType, true) as BerryModifier; + + if (!heldBerriesOfType || heldBerriesOfType.getStackCount() < heldBerriesOfType.getMaxStackCount(scene)) { + await applyModifierTypeToPlayerPokemon(scene, prioritizedPokemon, berry); + return; + } + } + + // Iterate over the party until berry was successfully given + for (const pokemon of party) { + const heldBerriesOfType = scene.findModifier(m => m instanceof BerryModifier + && m.pokemonId === pokemon.id && (m as BerryModifier).berryType === berryType, true) as BerryModifier; + + if (!heldBerriesOfType || heldBerriesOfType.getStackCount() < heldBerriesOfType.getMaxStackCount(scene)) { + await applyModifierTypeToPlayerPokemon(scene, pokemon, berry); + return; + } + } +} diff --git a/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts b/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts new file mode 100644 index 00000000000..68840943c49 --- /dev/null +++ b/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts @@ -0,0 +1,662 @@ +import { + EnemyPartyConfig, generateModifierType, + generateModifierTypeOption, + initBattleWithEnemyConfig, + leaveEncounterWithoutBattle, + selectOptionThenPokemon, + selectPokemonForOption, + setEncounterRewards, + transitionMysteryEncounterIntroVisuals, +} from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { + getRandomPartyMemberFunc, + trainerConfigs, + TrainerPartyCompoundTemplate, + TrainerPartyTemplate, + TrainerSlot, +} from "#app/data/trainer-config"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { PartyMemberStrength } from "#enums/party-member-strength"; +import BattleScene from "#app/battle-scene"; +import { isNullOrUndefined, randSeedInt, randSeedShuffle } from "#app/utils"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { TrainerType } from "#enums/trainer-type"; +import { Species } from "#enums/species"; +import Pokemon, { PlayerPokemon, PokemonMove } from "#app/field/pokemon"; +import { getEncounterText, showEncounterDialogue } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { LearnMovePhase } from "#app/phases/learn-move-phase"; +import { Moves } from "#enums/moves"; +import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { + AttackTypeBoosterHeldItemTypeRequirement, + CombinationPokemonRequirement, + HeldItemRequirement, + TypeRequirement +} from "#app/data/mystery-encounters/mystery-encounter-requirements"; +import { Type } from "#app/data/type"; +import { AttackTypeBoosterModifierType, ModifierTypeOption, modifierTypes } from "#app/modifier/modifier-type"; +import { + AttackTypeBoosterModifier, + BypassSpeedChanceModifier, + ContactHeldItemTransferChanceModifier, + PokemonHeldItemModifier +} from "#app/modifier/modifier"; +import i18next from "i18next"; +import MoveInfoOverlay from "#app/ui/move-info-overlay"; +import { allMoves } from "#app/data/move"; +import { ModifierTier } from "#app/modifier/modifier-tier"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; + +/** the i18n namespace for the encounter */ +const namespace = "mysteryEncounter:bugTypeSuperfan"; + +const POOL_1_POKEMON = [ + Species.PARASECT, + Species.VENOMOTH, + Species.LEDIAN, + Species.ARIADOS, + Species.YANMA, + Species.BEAUTIFLY, + Species.DUSTOX, + Species.MASQUERAIN, + Species.NINJASK, + Species.VOLBEAT, + Species.ILLUMISE, + Species.ANORITH, + Species.KRICKETUNE, + Species.WORMADAM, + Species.MOTHIM, + Species.SKORUPI, + Species.JOLTIK, + Species.LARVESTA, + Species.VIVILLON, + Species.CHARJABUG, + Species.RIBOMBEE, + Species.SPIDOPS, + Species.LOKIX +]; + +const POOL_2_POKEMON = [ + Species.SCYTHER, + Species.PINSIR, + Species.HERACROSS, + Species.FORRETRESS, + Species.SCIZOR, + Species.SHUCKLE, + Species.SHEDINJA, + Species.ARMALDO, + Species.VESPIQUEN, + Species.DRAPION, + Species.YANMEGA, + Species.LEAVANNY, + Species.SCOLIPEDE, + Species.CRUSTLE, + Species.ESCAVALIER, + Species.ACCELGOR, + Species.GALVANTULA, + Species.VIKAVOLT, + Species.ARAQUANID, + Species.ORBEETLE, + Species.CENTISKORCH, + Species.FROSMOTH, + Species.KLEAVOR, +]; + +const POOL_3_POKEMON: { species: Species, formIndex?: number }[] = [ + { + species: Species.PINSIR, + formIndex: 1 + }, + { + species: Species.SCIZOR, + formIndex: 1 + }, + { + species: Species.HERACROSS, + formIndex: 1 + }, + { + species: Species.ORBEETLE, + formIndex: 1 + }, + { + species: Species.CENTISKORCH, + formIndex: 1 + }, + { + species: Species.DURANT, + }, + { + species: Species.VOLCARONA, + }, + { + species: Species.GOLISOPOD, + }, +]; + +const POOL_4_POKEMON = [ + Species.GENESECT, + Species.SLITHER_WING, + Species.BUZZWOLE, + Species.PHEROMOSA +]; + +const PHYSICAL_TUTOR_MOVES = [ + Moves.MEGAHORN, + Moves.X_SCISSOR, + Moves.ATTACK_ORDER, + Moves.PIN_MISSILE, + Moves.FIRST_IMPRESSION +]; + +const SPECIAL_TUTOR_MOVES = [ + Moves.SILVER_WIND, + Moves.BUG_BUZZ, + Moves.SIGNAL_BEAM, + Moves.POLLEN_PUFF +]; + +const STATUS_TUTOR_MOVES = [ + Moves.STRING_SHOT, + Moves.STICKY_WEB, + Moves.SILK_TRAP, + Moves.RAGE_POWDER, + Moves.HEAL_ORDER +]; + +const MISC_TUTOR_MOVES = [ + Moves.BUG_BITE, + Moves.LEECH_LIFE, + Moves.DEFEND_ORDER, + Moves.QUIVER_DANCE, + Moves.TAIL_GLOW, + Moves.INFESTATION, + Moves.U_TURN +]; + +/** + * Bug Type Superfan encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3810 | GitHub Issue #3810} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const BugTypeSuperfanEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.BUG_TYPE_SUPERFAN) + .withEncounterTier(MysteryEncounterTier.GREAT) + .withPrimaryPokemonRequirement(new CombinationPokemonRequirement( + // Must have at least 1 Bug type on team, OR have a bug item somewhere on the team + new HeldItemRequirement(["BypassSpeedChanceModifier", "ContactHeldItemTransferChanceModifier"], 1), + new AttackTypeBoosterHeldItemTypeRequirement(Type.BUG, 1), + new TypeRequirement(Type.BUG, false, 1) + )) + .withMaxAllowedEncounters(1) + .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + .withIntroSpriteConfigs([]) // These are set in onInit() + .withAutoHideIntroVisuals(false) + .withIntroDialogue([ + { + text: `${namespace}.intro`, + }, + { + speaker: `${namespace}.speaker`, + text: `${namespace}.intro_dialogue`, + }, + ]) + .withOnInit((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + // Calculates what trainers are available for battle in the encounter + + // Bug type superfan trainer config + const config = getTrainerConfigForWave(scene.currentBattle.waveIndex); + const spriteKey = config.getSpriteKey(); + encounter.enemyPartyConfigs.push({ + trainerConfig: config, + female: true, + }); + + encounter.spriteConfigs = [ + { + spriteKey: spriteKey, + fileRoot: "trainer", + hasShadow: true, + }, + ]; + + const requiredItems = [ + generateModifierType(scene, modifierTypes.QUICK_CLAW), + generateModifierType(scene, modifierTypes.GRIP_CLAW), + generateModifierType(scene, modifierTypes.ATTACK_TYPE_BOOSTER, [Type.BUG]), + ]; + + const requiredItemString = requiredItems.map(m => m?.name ?? "unknown").join("/"); + encounter.setDialogueToken("requiredBugItems", requiredItemString); + + return true; + }) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + speaker: `${namespace}.speaker`, + text: `${namespace}.option.1.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Select battle the bug trainer + const encounter = scene.currentBattle.mysteryEncounter!; + const config: EnemyPartyConfig = encounter.enemyPartyConfigs[0]; + + // Init the moves available for tutor + const moveTutorOptions: PokemonMove[] = []; + moveTutorOptions.push(new PokemonMove(PHYSICAL_TUTOR_MOVES[randSeedInt(PHYSICAL_TUTOR_MOVES.length)])); + moveTutorOptions.push(new PokemonMove(SPECIAL_TUTOR_MOVES[randSeedInt(SPECIAL_TUTOR_MOVES.length)])); + moveTutorOptions.push(new PokemonMove(STATUS_TUTOR_MOVES[randSeedInt(STATUS_TUTOR_MOVES.length)])); + moveTutorOptions.push(new PokemonMove(MISC_TUTOR_MOVES[randSeedInt(MISC_TUTOR_MOVES.length)])); + encounter.misc = { + moveTutorOptions + }; + + // Assigns callback that teaches move before continuing to rewards + encounter.onRewards = doBugTypeMoveTutor; + + setEncounterRewards(scene, { fillRemaining: true }); + await transitionMysteryEncounterIntroVisuals(scene, true, true); + await initBattleWithEnemyConfig(scene, config); + } + ) + .withOption(MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) + .withPrimaryPokemonRequirement(new TypeRequirement(Type.BUG, false, 1)) // Must have 1 Bug type on team + .withDialogue({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + disabledButtonTooltip: `${namespace}.option.2.disabled_tooltip` + }) + .withPreOptionPhase(async (scene: BattleScene) => { + // Player shows off their bug types + const encounter = scene.currentBattle.mysteryEncounter!; + + // Player gets different rewards depending on the number of bug types they have + const numBugTypes = scene.getParty().filter(p => p.isOfType(Type.BUG, true)).length; + const numBugTypesText = i18next.t(`${namespace}.numBugTypes`, { count: numBugTypes }); + encounter.setDialogueToken("numBugTypes", numBugTypesText); + + if (numBugTypes < 2) { + setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.SUPER_LURE, modifierTypes.GREAT_BALL], fillRemaining: false }); + encounter.selectedOption!.dialogue!.selected = [ + { + speaker: `${namespace}.speaker`, + text: `${namespace}.option.2.selected_0_to_1`, + }, + ]; + } else if (numBugTypes < 4) { + setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.QUICK_CLAW, modifierTypes.MAX_LURE, modifierTypes.ULTRA_BALL], fillRemaining: false }); + encounter.selectedOption!.dialogue!.selected = [ + { + speaker: `${namespace}.speaker`, + text: `${namespace}.option.2.selected_2_to_3`, + }, + ]; + } else if (numBugTypes < 6) { + setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.GRIP_CLAW, modifierTypes.MAX_LURE, modifierTypes.ROGUE_BALL], fillRemaining: false }); + encounter.selectedOption!.dialogue!.selected = [ + { + speaker: `${namespace}.speaker`, + text: `${namespace}.option.2.selected_4_to_5`, + }, + ]; + } else { + // If player has any evolution/form change items that are valid for their party, will spawn one of those items in addition to a Master Ball + const modifierOptions: ModifierTypeOption[] = [generateModifierTypeOption(scene, modifierTypes.MASTER_BALL)!, generateModifierTypeOption(scene, modifierTypes.MAX_LURE)!]; + const specialOptions: ModifierTypeOption[] = []; + + const nonRareEvolutionModifier = generateModifierTypeOption(scene, modifierTypes.EVOLUTION_ITEM); + if (nonRareEvolutionModifier) { + specialOptions.push(nonRareEvolutionModifier); + } + const rareEvolutionModifier = generateModifierTypeOption(scene, modifierTypes.RARE_EVOLUTION_ITEM); + if (rareEvolutionModifier) { + specialOptions.push(rareEvolutionModifier); + } + const formChangeModifier = generateModifierTypeOption(scene, modifierTypes.FORM_CHANGE_ITEM); + if (formChangeModifier) { + specialOptions.push(formChangeModifier); + } + if (specialOptions.length > 0) { + modifierOptions.push(specialOptions[randSeedInt(specialOptions.length)]); + } + + setEncounterRewards(scene, { guaranteedModifierTypeOptions: modifierOptions, fillRemaining: false }); + encounter.selectedOption!.dialogue!.selected = [ + { + speaker: `${namespace}.speaker`, + text: `${namespace}.option.2.selected_6`, + }, + ]; + } + }) + .withOptionPhase(async (scene: BattleScene) => { + // Player shows off their bug types + leaveEncounterWithoutBattle(scene); + }) + .build()) + .withOption(MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) + .withPrimaryPokemonRequirement(new CombinationPokemonRequirement( + // Meets one or both of the below reqs + new HeldItemRequirement(["BypassSpeedChanceModifier", "ContactHeldItemTransferChanceModifier"], 1), + new AttackTypeBoosterHeldItemTypeRequirement(Type.BUG, 1) + )) + .withDialogue({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + disabledButtonTooltip: `${namespace}.option.3.disabled_tooltip`, + selected: [ + { + text: `${namespace}.option.3.selected`, + }, + { + speaker: `${namespace}.speaker`, + text: `${namespace}.option.3.selected_dialogue`, + }, + ], + secondOptionPrompt: `${namespace}.option.3.select_prompt`, + }) + .withPreOptionPhase(async (scene: BattleScene): Promise => { + const encounter = scene.currentBattle.mysteryEncounter!; + + const onPokemonSelected = (pokemon: PlayerPokemon) => { + // Get Pokemon held items and filter for valid ones + const validItems = pokemon.getHeldItems().filter(item => { + return (item instanceof BypassSpeedChanceModifier || + item instanceof ContactHeldItemTransferChanceModifier || + (item instanceof AttackTypeBoosterModifier && (item.type as AttackTypeBoosterModifierType).moveType === Type.BUG)) && + item.isTransferable; + }); + + return validItems.map((modifier: PokemonHeldItemModifier) => { + const option: OptionSelectItem = { + label: modifier.type.name, + handler: () => { + // Pokemon and item selected + encounter.setDialogueToken("selectedItem", modifier.type.name); + encounter.misc = { + chosenPokemon: pokemon, + chosenModifier: modifier, + }; + return true; + }, + }; + return option; + }); + }; + + const selectableFilter = (pokemon: Pokemon) => { + // If pokemon has valid item, it can be selected + const hasValidItem = pokemon.getHeldItems().some(item => { + return item instanceof BypassSpeedChanceModifier || + item instanceof ContactHeldItemTransferChanceModifier || + (item instanceof AttackTypeBoosterModifier && (item.type as AttackTypeBoosterModifierType).moveType === Type.BUG); + }); + if (!hasValidItem) { + return getEncounterText(scene, `${namespace}.option.3.invalid_selection`) ?? null; + } + + return null; + }; + + return selectPokemonForOption(scene, onPokemonSelected, undefined, selectableFilter); + }) + .withOptionPhase(async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + const modifier = encounter.misc.chosenModifier; + + // Remove the modifier if its stacks go to 0 + modifier.stackCount -= 1; + if (modifier.stackCount === 0) { + scene.removeModifier(modifier); + } + scene.updateModifiers(true, true); + + const bugNet = generateModifierTypeOption(scene, modifierTypes.MYSTERY_ENCOUNTER_GOLDEN_BUG_NET)!; + bugNet.type.tier = ModifierTier.ROGUE; + + setEncounterRewards(scene, { guaranteedModifierTypeOptions: [bugNet], guaranteedModifierTypeFuncs: [modifierTypes.REVIVER_SEED], fillRemaining: false }); + leaveEncounterWithoutBattle(scene, true); + }) + .build()) + .withOutroDialogue([ + { + text: `${namespace}.outro`, + }, + ]) + .build(); + +function getTrainerConfigForWave(waveIndex: number) { + // Bug type superfan trainer config + const config = trainerConfigs[TrainerType.BUG_TYPE_SUPERFAN].clone(); + config.name = i18next.t("trainerNames:bug_type_superfan"); + + const pool3Copy = POOL_3_POKEMON.slice(0); + randSeedShuffle(pool3Copy); + const pool3Mon = pool3Copy.pop()!; + + if (waveIndex < 30) { + // Use default template (2 AVG) + config + .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.BEEDRILL ], TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.BUTTERFREE ], TrainerSlot.TRAINER, true)); + } else if (waveIndex < 50) { + config + .setPartyTemplates(new TrainerPartyTemplate(3, PartyMemberStrength.AVERAGE)) + .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.BEEDRILL ], TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.BUTTERFREE ], TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(2, getRandomPartyMemberFunc(POOL_1_POKEMON, TrainerSlot.TRAINER, true)); + } else if (waveIndex < 70) { + config + .setPartyTemplates(new TrainerPartyTemplate(4, PartyMemberStrength.AVERAGE)) + .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.BEEDRILL ], TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.BUTTERFREE ], TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(2, getRandomPartyMemberFunc(POOL_1_POKEMON, TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(3, getRandomPartyMemberFunc(POOL_2_POKEMON, TrainerSlot.TRAINER, true)); + } else if (waveIndex < 100) { + config + .setPartyTemplates(new TrainerPartyTemplate(5, PartyMemberStrength.AVERAGE)) + .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.BEEDRILL ], TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.BUTTERFREE ], TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(2, getRandomPartyMemberFunc(POOL_1_POKEMON, TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(3, getRandomPartyMemberFunc(POOL_2_POKEMON, TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(4, getRandomPartyMemberFunc(POOL_2_POKEMON, TrainerSlot.TRAINER, true)); + } else if (waveIndex < 120) { + config + .setPartyTemplates(new TrainerPartyTemplate(5, PartyMemberStrength.AVERAGE)) + .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.BEEDRILL ], TrainerSlot.TRAINER, true, p => { + p.formIndex = 1; + p.generateAndPopulateMoveset(); + p.generateName(); + })) + .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.BUTTERFREE ], TrainerSlot.TRAINER, true, p => { + p.formIndex = 1; + p.generateAndPopulateMoveset(); + p.generateName(); + })) + .setPartyMemberFunc(2, getRandomPartyMemberFunc(POOL_2_POKEMON, TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(3, getRandomPartyMemberFunc(POOL_2_POKEMON, TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(4, getRandomPartyMemberFunc([pool3Mon.species], TrainerSlot.TRAINER, true, p => { + if (!isNullOrUndefined(pool3Mon.formIndex)) { + p.formIndex = pool3Mon.formIndex!; + p.generateAndPopulateMoveset(); + p.generateName(); + } + })); + } else if (waveIndex < 140) { + randSeedShuffle(pool3Copy); + const pool3Mon2 = pool3Copy.pop()!; + config + .setPartyTemplates(new TrainerPartyTemplate(5, PartyMemberStrength.AVERAGE)) + .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.BEEDRILL ], TrainerSlot.TRAINER, true, p => { + p.formIndex = 1; + p.generateAndPopulateMoveset(); + p.generateName(); + })) + .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.BUTTERFREE ], TrainerSlot.TRAINER, true, p => { + p.formIndex = 1; + p.generateAndPopulateMoveset(); + p.generateName(); + })) + .setPartyMemberFunc(2, getRandomPartyMemberFunc(POOL_2_POKEMON, TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(3, getRandomPartyMemberFunc([pool3Mon.species], TrainerSlot.TRAINER, true, p => { + if (!isNullOrUndefined(pool3Mon.formIndex)) { + p.formIndex = pool3Mon.formIndex!; + p.generateAndPopulateMoveset(); + p.generateName(); + } + })) + .setPartyMemberFunc(4, getRandomPartyMemberFunc([pool3Mon2.species], TrainerSlot.TRAINER, true, p => { + if (!isNullOrUndefined(pool3Mon2.formIndex)) { + p.formIndex = pool3Mon2.formIndex!; + p.generateAndPopulateMoveset(); + p.generateName(); + } + })); + } else if (waveIndex < 160) { + config + .setPartyTemplates(new TrainerPartyCompoundTemplate(new TrainerPartyTemplate(4, PartyMemberStrength.AVERAGE), new TrainerPartyTemplate(1, PartyMemberStrength.STRONG))) + .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.BEEDRILL ], TrainerSlot.TRAINER, true, p => { + p.formIndex = 1; + p.generateAndPopulateMoveset(); + p.generateName(); + })) + .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.BUTTERFREE ], TrainerSlot.TRAINER, true, p => { + p.formIndex = 1; + p.generateAndPopulateMoveset(); + p.generateName(); + })) + .setPartyMemberFunc(2, getRandomPartyMemberFunc(POOL_2_POKEMON, TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(3, getRandomPartyMemberFunc([pool3Mon.species], TrainerSlot.TRAINER, true, p => { + if (!isNullOrUndefined(pool3Mon.formIndex)) { + p.formIndex = pool3Mon.formIndex!; + p.generateAndPopulateMoveset(); + p.generateName(); + } + })) + .setPartyMemberFunc(4, getRandomPartyMemberFunc(POOL_4_POKEMON, TrainerSlot.TRAINER, true)); + } else { + config + .setPartyTemplates(new TrainerPartyCompoundTemplate(new TrainerPartyTemplate(4, PartyMemberStrength.AVERAGE), new TrainerPartyTemplate(1, PartyMemberStrength.STRONG))) + .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.BEEDRILL ], TrainerSlot.TRAINER, true, p => { + p.setBoss(true, 2); + p.formIndex = 1; + p.generateAndPopulateMoveset(); + p.generateName(); + })) + .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.BUTTERFREE ], TrainerSlot.TRAINER, true, p => { + p.setBoss(true, 2); + p.formIndex = 1; + p.generateAndPopulateMoveset(); + p.generateName(); + })) + .setPartyMemberFunc(2, getRandomPartyMemberFunc([pool3Mon.species], TrainerSlot.TRAINER, true, p => { + if (!isNullOrUndefined(pool3Mon.formIndex)) { + p.formIndex = pool3Mon.formIndex!; + p.generateAndPopulateMoveset(); + p.generateName(); + } + })) + .setPartyMemberFunc(3, getRandomPartyMemberFunc([pool3Mon.species], TrainerSlot.TRAINER, true, p => { + if (!isNullOrUndefined(pool3Mon.formIndex)) { + p.formIndex = pool3Mon.formIndex!; + p.generateAndPopulateMoveset(); + p.generateName(); + } + })) + .setPartyMemberFunc(4, getRandomPartyMemberFunc(POOL_4_POKEMON, TrainerSlot.TRAINER, true)); + } + + return config; +} + +function doBugTypeMoveTutor(scene: BattleScene): Promise { + return new Promise(async resolve => { + const moveOptions = scene.currentBattle.mysteryEncounter!.misc.moveTutorOptions; + await showEncounterDialogue(scene, `${namespace}.battle_won`, `${namespace}.speaker`); + + const overlayScale = 1; + const moveInfoOverlay = new MoveInfoOverlay(scene, { + delayVisibility: false, + scale: overlayScale, + onSide: true, + right: true, + x: 1, + y: -MoveInfoOverlay.getHeight(overlayScale, true) - 1, + width: (scene.game.canvas.width / 6) - 2, + }); + scene.ui.add(moveInfoOverlay); + + const optionSelectItems = moveOptions.map((move: PokemonMove) => { + const option: OptionSelectItem = { + label: move.getName(), + handler: () => { + moveInfoOverlay.active = false; + moveInfoOverlay.setVisible(false); + return true; + }, + onHover: () => { + moveInfoOverlay.active = true; + moveInfoOverlay.show(allMoves[move.moveId]); + }, + }; + return option; + }); + + const onHoverOverCancel = () => { + moveInfoOverlay.active = false; + moveInfoOverlay.setVisible(false); + }; + + const result = await selectOptionThenPokemon(scene, optionSelectItems, `${namespace}.teach_move_prompt`, undefined, onHoverOverCancel); + // let forceExit = !!result; + if (!result) { + moveInfoOverlay.active = false; + moveInfoOverlay.setVisible(false); + } + + // TODO: add menu to confirm player doesn't want to teach a move + // while (!result && !forceExit) { + // // Didn't teach a move, ask the player to confirm they don't want to teach a move + // await showEncounterDialogue(scene, `${namespace}.confirm_no_teach`, `${namespace}.speaker`); + // const confirm = await new Promise(confirmResolve => { + // scene.ui.setMode(Mode.CONFIRM, () => confirmResolve(true), () => confirmResolve(false)); + // }); + // scene.ui.clearText(); + // await scene.ui.setMode(Mode.MESSAGE); + // if (confirm) { + // // No teach, break out of loop + // forceExit = true; + // } else { + // // Re-show learn menu + // result = await selectOptionThenPokemon(scene, optionSelectItems, `${namespace}.teach_move_prompt`, undefined, onHoverOverCancel); + // if (!result) { + // moveInfoOverlay.active = false; + // moveInfoOverlay.setVisible(false); + // } + // } + // } + + // Option select complete, handle if they are learning a move + if (result && result.selectedOptionIndex < moveOptions.length) { + scene.unshiftPhase(new LearnMovePhase(scene, result.selectedPokemonIndex, moveOptions[result.selectedOptionIndex].moveId)); + } + + // Complete battle and go to rewards + resolve(); + }); +} diff --git a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts new file mode 100644 index 00000000000..e1e681e95dd --- /dev/null +++ b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts @@ -0,0 +1,512 @@ +import { EnemyPartyConfig, generateModifierType, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, loadCustomMovesForEncounter, selectPokemonForOption, setEncounterRewards, transitionMysteryEncounterIntroVisuals } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { trainerConfigs, TrainerPartyCompoundTemplate, TrainerPartyTemplate, } from "#app/data/trainer-config"; +import { ModifierTier } from "#app/modifier/modifier-tier"; +import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { PartyMemberStrength } from "#enums/party-member-strength"; +import BattleScene from "#app/battle-scene"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { Species } from "#enums/species"; +import { TrainerType } from "#enums/trainer-type"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { Abilities } from "#enums/abilities"; +import { applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { Type } from "#app/data/type"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { randSeedInt, randSeedShuffle } from "#app/utils"; +import { showEncounterDialogue, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { Mode } from "#app/ui/ui"; +import i18next from "i18next"; +import { OptionSelectConfig } from "#app/ui/abstact-option-select-ui-handler"; +import { PlayerPokemon, PokemonMove } from "#app/field/pokemon"; +import { Ability } from "#app/data/ability"; +import { BerryModifier } from "#app/modifier/modifier"; +import { BerryType } from "#enums/berry-type"; +import { BattlerIndex } from "#app/battle"; +import { Moves } from "#enums/moves"; +import { EncounterBattleAnim } from "#app/data/battle-anims"; +import { MoveCategory } from "#app/data/move"; +import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; +import { EncounterAnim } from "#enums/encounter-anims"; +import { Challenges } from "#enums/challenges"; + +/** the i18n namespace for the encounter */ +const namespace = "mysteryEncounter:clowningAround"; + +const RANDOM_ABILITY_POOL = [ + Abilities.STURDY, + Abilities.PICKUP, + Abilities.INTIMIDATE, + Abilities.GUTS, + Abilities.DROUGHT, + Abilities.DRIZZLE, + Abilities.SNOW_WARNING, + Abilities.SAND_STREAM, + Abilities.ELECTRIC_SURGE, + Abilities.PSYCHIC_SURGE, + Abilities.GRASSY_SURGE, + Abilities.MISTY_SURGE, + Abilities.MAGICIAN, + Abilities.SHEER_FORCE, + Abilities.PRANKSTER +]; + +/** + * Clowning Around encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3807 | GitHub Issue #3807} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const ClowningAroundEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.CLOWNING_AROUND) + .withEncounterTier(MysteryEncounterTier.ULTRA) + .withDisallowedChallenges(Challenges.SINGLE_TYPE) + .withSceneWaveRangeRequirement(80, CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES[1]) + .withAnimations(EncounterAnim.SMOKESCREEN) + .withAutoHideIntroVisuals(false) + .withIntroSpriteConfigs([ + { + spriteKey: Species.MR_MIME.toString(), + fileRoot: "pokemon", + hasShadow: true, + repeat: true, + x: -25, + tint: 0.3, + y: -3, + yShadow: -3 + }, + { + spriteKey: Species.BLACEPHALON.toString(), + fileRoot: "pokemon/exp", + hasShadow: true, + repeat: true, + x: 25, + tint: 0.3, + y: -3, + yShadow: -3 + }, + { + spriteKey: "harlequin", + fileRoot: "trainer", + hasShadow: true, + x: 0, + y: 2, + yShadow: 2 + }, + ]) + .withIntroDialogue([ + { + text: `${namespace}.intro`, + }, + { + text: `${namespace}.intro_dialogue`, + speaker: `${namespace}.speaker` + }, + ]) + .withOnInit((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + + const clownTrainerType = TrainerType.HARLEQUIN; + const clownConfig = trainerConfigs[clownTrainerType].clone(); + const clownPartyTemplate = new TrainerPartyCompoundTemplate( + new TrainerPartyTemplate(1, PartyMemberStrength.STRONG), + new TrainerPartyTemplate(1, PartyMemberStrength.STRONGER)); + clownConfig.setPartyTemplates(clownPartyTemplate); + clownConfig.setDoubleOnly(); + // @ts-ignore + clownConfig.partyTemplateFunc = null; // Overrides party template func if it exists + + // 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.misc = { ability }; + + encounter.enemyPartyConfigs.push({ + trainerConfig: clownConfig, + pokemonConfigs: [ // Overrides first 2 pokemon to be Mr. Mime and Blacephalon + { + species: getPokemonSpecies(Species.MR_MIME), + isBoss: true, + moveSet: [Moves.TEETER_DANCE, Moves.ALLY_SWITCH, Moves.DAZZLING_GLEAM, Moves.PSYCHIC] + }, + { // Blacephalon has the random ability from pool, and 2 entirely random types to fit with the theme of the encounter + species: getPokemonSpecies(Species.BLACEPHALON), + mysteryEncounterPokemonData: new MysteryEncounterPokemonData({ ability: ability, types: [randSeedInt(18), randSeedInt(18)] }), + isBoss: true, + moveSet: [Moves.TRICK, Moves.HYPNOSIS, Moves.SHADOW_BALL, Moves.MIND_BLOWN] + }, + ], + doubleBattle: true + }); + + // Load animations/sfx for start of fight moves + loadCustomMovesForEncounter(scene, [Moves.ROLE_PLAY, Moves.TAUNT]); + + encounter.setDialogueToken("blacephalonName", getPokemonSpecies(Species.BLACEPHALON).getName()); + + return true; + }) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + speaker: `${namespace}.speaker` + }, + ], + }) + .withOptionPhase(async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + // Spawn battle + const config: EnemyPartyConfig = encounter.enemyPartyConfigs[0]; + + setEncounterRewards(scene, { fillRemaining: true }); + + // TODO: when Magic Room and Wonder Room are implemented, add those to start of battle + encounter.startOfBattleEffects.push( + { // Mr. Mime copies the Blacephalon's random ability + sourceBattlerIndex: BattlerIndex.ENEMY, + targets: [BattlerIndex.ENEMY_2], + move: new PokemonMove(Moves.ROLE_PLAY), + ignorePp: true + }, + { + sourceBattlerIndex: BattlerIndex.ENEMY_2, + targets: [BattlerIndex.PLAYER], + move: new PokemonMove(Moves.TAUNT), + ignorePp: true + }, + { + sourceBattlerIndex: BattlerIndex.ENEMY_2, + targets: [BattlerIndex.PLAYER_2], + move: new PokemonMove(Moves.TAUNT), + ignorePp: true + }); + + await transitionMysteryEncounterIntroVisuals(scene); + await initBattleWithEnemyConfig(scene, config); + }) + .withPostOptionPhase(async (scene: BattleScene): Promise => { + // After the battle, offer the player the opportunity to permanently swap ability + const abilityWasSwapped = await handleSwapAbility(scene); + if (abilityWasSwapped) { + await showEncounterText(scene, `${namespace}.option.1.ability_gained`); + } + + // Play animations once ability swap is complete + // Trainer sprite that is shown at end of battle is not the same as mystery encounter intro visuals + scene.tweens.add({ + targets: scene.currentBattle.trainer, + x: "+=16", + y: "-=16", + alpha: 0, + ease: "Sine.easeInOut", + duration: 250 + }); + const background = new EncounterBattleAnim(EncounterAnim.SMOKESCREEN, scene.getPlayerPokemon()!, scene.getPlayerPokemon()); + background.playWithoutTargets(scene, 230, 40, 2); + return true; + }) + .build() + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected`, + speaker: `${namespace}.speaker` + }, + { + text: `${namespace}.option.2.selected_2`, + }, + { + text: `${namespace}.option.2.selected_3`, + speaker: `${namespace}.speaker` + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene) => { + // Swap player's items on pokemon with the most items + // Item comparisons look at whichever Pokemon has the greatest number of TRANSFERABLE, non-berry items + // So Vitamins, form change items, etc. are not included + const encounter = scene.currentBattle.mysteryEncounter!; + + const party = scene.getParty(); + let mostHeldItemsPokemon = party[0]; + let count = mostHeldItemsPokemon.getHeldItems() + .filter(m => m.isTransferable && !(m instanceof BerryModifier)) + .reduce((v, m) => v + m.stackCount, 0); + + party.forEach(pokemon => { + const nextCount = pokemon.getHeldItems() + .filter(m => m.isTransferable && !(m instanceof BerryModifier)) + .reduce((v, m) => v + m.stackCount, 0); + if (nextCount > count) { + mostHeldItemsPokemon = pokemon; + count = nextCount; + } + }); + + encounter.setDialogueToken("switchPokemon", mostHeldItemsPokemon.getNameToRender()); + + const items = mostHeldItemsPokemon.getHeldItems(); + + // Shuffles Berries (if they have any) + let numBerries = 0; + items.filter(m => m instanceof BerryModifier) + .forEach(m => { + numBerries += m.stackCount; + scene.removeModifier(m); + }); + + generateItemsOfTier(scene, mostHeldItemsPokemon, numBerries, "Berries"); + + // Shuffle Transferable held items in the same tier (only shuffles Ultra and Rogue atm) + let numUltra = 0; + let numRogue = 0; + items.filter(m => m.isTransferable && !(m instanceof BerryModifier)) + .forEach(m => { + const type = m.type.withTierFromPool(); + const tier = type.tier ?? ModifierTier.ULTRA; + if (type.id === "GOLDEN_EGG" || tier === ModifierTier.ROGUE) { + numRogue += m.stackCount; + scene.removeModifier(m); + } else if (type.id === "LUCKY_EGG" || tier === ModifierTier.ULTRA) { + numUltra += m.stackCount; + scene.removeModifier(m); + } + }); + + generateItemsOfTier(scene, mostHeldItemsPokemon, numUltra, ModifierTier.ULTRA); + generateItemsOfTier(scene, mostHeldItemsPokemon, numRogue, ModifierTier.ROGUE); + }) + .withOptionPhase(async (scene: BattleScene) => { + leaveEncounterWithoutBattle(scene, true); + }) + .withPostOptionPhase(async (scene: BattleScene) => { + // Play animations + const background = new EncounterBattleAnim(EncounterAnim.SMOKESCREEN, scene.getPlayerPokemon()!, scene.getPlayerPokemon()); + background.playWithoutTargets(scene, 230, 40, 2); + await transitionMysteryEncounterIntroVisuals(scene, true, true, 200); + }) + .build() + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + selected: [ + { + text: `${namespace}.option.3.selected`, + speaker: `${namespace}.speaker` + }, + { + text: `${namespace}.option.3.selected_2`, + }, + { + text: `${namespace}.option.3.selected_3`, + speaker: `${namespace}.speaker` + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene) => { + // Randomize the second type of all player's pokemon + // If the pokemon does not normally have a second type, it will gain 1 + for (const pokemon of scene.getParty()) { + const originalTypes = pokemon.getTypes(false, false, true); + + // If the Pokemon has non-status moves that don't match the Pokemon's type, prioritizes those as the new type + // Makes the "randomness" of the shuffle slightly less punishing + let priorityTypes = pokemon.moveset + .filter(move => move && !originalTypes.includes(move.getMove().type) && move.getMove().category !== MoveCategory.STATUS) + .map(move => move!.getMove().type); + if (priorityTypes?.length > 0) { + priorityTypes = [...new Set(priorityTypes)]; + randSeedShuffle(priorityTypes); + } + + const newTypes = [originalTypes[0]]; + let secondType: Type | null = null; + while (secondType === null || secondType === newTypes[0] || originalTypes.includes(secondType)) { + if (priorityTypes.length > 0) { + secondType = priorityTypes.pop() ?? null; + } else { + secondType = randSeedInt(18) as Type; + } + } + newTypes.push(secondType); + + // Apply the type changes (to both base and fusion, if pokemon is fused) + if (!pokemon.mysteryEncounterPokemonData) { + pokemon.mysteryEncounterPokemonData = new MysteryEncounterPokemonData(); + } + pokemon.mysteryEncounterPokemonData.types = newTypes; + if (pokemon.isFusion()) { + if (!pokemon.fusionMysteryEncounterPokemonData) { + pokemon.fusionMysteryEncounterPokemonData = new MysteryEncounterPokemonData(); + } + pokemon.fusionMysteryEncounterPokemonData.types = newTypes; + } + } + }) + .withOptionPhase(async (scene: BattleScene) => { + leaveEncounterWithoutBattle(scene, true); + }) + .withPostOptionPhase(async (scene: BattleScene) => { + // Play animations + const background = new EncounterBattleAnim(EncounterAnim.SMOKESCREEN, scene.getPlayerPokemon()!, scene.getPlayerPokemon()); + background.playWithoutTargets(scene, 230, 40, 2); + await transitionMysteryEncounterIntroVisuals(scene, true, true, 200); + }) + .build() + ) + .withOutroDialogue([ + { + text: `${namespace}.outro`, + }, + ]) + .build(); + +async function handleSwapAbility(scene: BattleScene) { + return new Promise(async resolve => { + await showEncounterDialogue(scene, `${namespace}.option.1.apply_ability_dialogue`, `${namespace}.speaker`); + await showEncounterText(scene, `${namespace}.option.1.apply_ability_message`); + + scene.ui.setMode(Mode.MESSAGE).then(() => { + displayYesNoOptions(scene, resolve); + }); + }); +} + +function displayYesNoOptions(scene: BattleScene, resolve) { + showEncounterText(scene, `${namespace}.option.1.ability_prompt`, null, 500, false); + const fullOptions = [ + { + label: i18next.t("menu:yes"), + handler: () => { + onYesAbilitySwap(scene, resolve); + return true; + } + }, + { + label: i18next.t("menu:no"), + handler: () => { + resolve(false); + return true; + } + } + ]; + + const config: OptionSelectConfig = { + options: fullOptions, + maxOptions: 7, + yOffset: 0 + }; + scene.ui.setModeWithoutClear(Mode.OPTION_SELECT, config, null, true); +} + +function onYesAbilitySwap(scene: BattleScene, resolve) { + const onPokemonSelected = (pokemon: PlayerPokemon) => { + // Do ability swap + const encounter = scene.currentBattle.mysteryEncounter!; + if (pokemon.isFusion()) { + if (!pokemon.fusionMysteryEncounterPokemonData) { + pokemon.fusionMysteryEncounterPokemonData = new MysteryEncounterPokemonData(); + } + pokemon.fusionMysteryEncounterPokemonData.ability = encounter.misc.ability; + } else { + if (!pokemon.mysteryEncounterPokemonData) { + pokemon.mysteryEncounterPokemonData = new MysteryEncounterPokemonData(); + } + pokemon.mysteryEncounterPokemonData.ability = encounter.misc.ability; + } + encounter.setDialogueToken("chosenPokemon", pokemon.getNameToRender()); + scene.ui.setMode(Mode.MESSAGE).then(() => resolve(true)); + }; + + const onPokemonNotSelected = () => { + scene.ui.setMode(Mode.MESSAGE).then(() => { + displayYesNoOptions(scene, resolve); + }); + }; + + selectPokemonForOption(scene, onPokemonSelected, onPokemonNotSelected); +} + +function generateItemsOfTier(scene: BattleScene, pokemon: PlayerPokemon, numItems: number, tier: ModifierTier | "Berries") { + // These pools have to be defined at runtime so that modifierTypes exist + // Pools have instances of the modifier type equal to the max stacks that modifier can be applied to any one pokemon + // This is to prevent "over-generating" a random item of a certain type during item swaps + const ultraPool = [ + [modifierTypes.REVIVER_SEED, 1], + [modifierTypes.GOLDEN_PUNCH, 5], + [modifierTypes.ATTACK_TYPE_BOOSTER, 99], + [modifierTypes.QUICK_CLAW, 3], + [modifierTypes.WIDE_LENS, 3] + ]; + + const roguePool = [ + [modifierTypes.LEFTOVERS, 4], + [modifierTypes.SHELL_BELL, 4], + [modifierTypes.SOUL_DEW, 10], + [modifierTypes.SOOTHE_BELL, 3], + [modifierTypes.SCOPE_LENS, 1], + [modifierTypes.BATON, 1], + [modifierTypes.FOCUS_BAND, 5], + [modifierTypes.KINGS_ROCK, 3], + [modifierTypes.GRIP_CLAW, 5] + ]; + + const berryPool = [ + [BerryType.APICOT, 3], + [BerryType.ENIGMA, 2], + [BerryType.GANLON, 3], + [BerryType.LANSAT, 3], + [BerryType.LEPPA, 2], + [BerryType.LIECHI, 3], + [BerryType.LUM, 2], + [BerryType.PETAYA, 3], + [BerryType.SALAC, 2], + [BerryType.SITRUS, 2], + [BerryType.STARF, 3] + ]; + + let pool: any[]; + if (tier === "Berries") { + pool = berryPool; + } else { + pool = tier === ModifierTier.ULTRA ? ultraPool : roguePool; + } + + for (let i = 0; i < numItems; i++) { + const randIndex = randSeedInt(pool.length); + const newItemType = pool[randIndex]; + let newMod; + if (tier === "Berries") { + newMod = generateModifierType(scene, modifierTypes.BERRY, [newItemType[0]]) as PokemonHeldItemModifierType; + } else { + newMod = generateModifierType(scene, newItemType[0]) as PokemonHeldItemModifierType; + } + applyModifierTypeToPlayerPokemon(scene, pokemon, newMod); + // Decrement max stacks and remove from pool if at max + newItemType[1]--; + if (newItemType[1] <= 0) { + pool.splice(randIndex, 1); + } + } +} diff --git a/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts b/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts new file mode 100644 index 00000000000..1ceb14a7372 --- /dev/null +++ b/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts @@ -0,0 +1,328 @@ +import { EnemyPartyConfig, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, selectPokemonForOption, setEncounterRewards } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import Pokemon, { EnemyPokemon, PlayerPokemon, PokemonMove } from "#app/field/pokemon"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { Species } from "#enums/species"; +import BattleScene from "#app/battle-scene"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { Moves } from "#enums/moves"; +import { TrainerSlot } from "#app/data/trainer-config"; +import PokemonData from "#app/system/pokemon-data"; +import { Biome } from "#enums/biome"; +import { EncounterBattleAnim } from "#app/data/battle-anims"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { getEncounterText, queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { MoveRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; +import { DANCING_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups"; +import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; +import { BattlerIndex } from "#app/battle"; +import { catchPokemon, getEncounterPokemonLevelForWave, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { PokeballType } from "#enums/pokeball"; +import { modifierTypes } from "#app/modifier/modifier-type"; +import { LearnMovePhase } from "#app/phases/learn-move-phase"; +import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; +import { Stat } from "#enums/stat"; +import { EncounterAnim } from "#enums/encounter-anims"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; +import i18next from "i18next"; + +/** the i18n namespace for this encounter */ +const namespace = "mysteryEncounter:dancingLessons"; + +// Fire form +const BAILE_STYLE_BIOMES = [ + Biome.VOLCANO, + Biome.BEACH, + Biome.ISLAND, + Biome.WASTELAND, + Biome.MOUNTAIN, + Biome.BADLANDS, + Biome.DESERT +]; + +// Electric form +const POM_POM_STYLE_BIOMES = [ + Biome.CONSTRUCTION_SITE, + Biome.POWER_PLANT, + Biome.FACTORY, + Biome.LABORATORY, + Biome.SLUM, + Biome.METROPOLIS, + Biome.DOJO +]; + +// Psychic form +const PAU_STYLE_BIOMES = [ + Biome.JUNGLE, + Biome.FAIRY_CAVE, + Biome.MEADOW, + Biome.PLAINS, + Biome.GRASS, + Biome.TALL_GRASS, + Biome.FOREST +]; + +// Ghost form +const SENSU_STYLE_BIOMES = [ + Biome.RUINS, + Biome.SWAMP, + Biome.CAVE, + Biome.ABYSS, + Biome.GRAVEYARD, + Biome.LAKE, + Biome.TEMPLE +]; + +/** + * Dancing Lessons encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3823 | GitHub Issue #3823} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const DancingLessonsEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.DANCING_LESSONS) + .withEncounterTier(MysteryEncounterTier.GREAT) + .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + .withIntroSpriteConfigs([]) // Uses a real Pokemon sprite instead of ME Intro Visuals + .withAnimations(EncounterAnim.DANCE) + .withHideWildIntroMessage(true) + .withAutoHideIntroVisuals(false) + .withCatchAllowed(true) + .withOnVisualsStart((scene: BattleScene) => { + const danceAnim = new EncounterBattleAnim(EncounterAnim.DANCE, scene.getEnemyPokemon()!, scene.getParty()[0]); + danceAnim.play(scene); + + return true; + }) + .withIntroDialogue([ + { + text: `${namespace}.intro`, + } + ]) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withOnInit((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + + const species = getPokemonSpecies(Species.ORICORIO); + const level = getEncounterPokemonLevelForWave(scene, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER); + const enemyPokemon = new EnemyPokemon(scene, species, level, TrainerSlot.NONE, false); + if (!enemyPokemon.moveset.some(m => m && m.getMove().id === Moves.REVELATION_DANCE)) { + if (enemyPokemon.moveset.length < 4) { + enemyPokemon.moveset.push(new PokemonMove(Moves.REVELATION_DANCE)); + } else { + enemyPokemon.moveset[0] = new PokemonMove(Moves.REVELATION_DANCE); + } + } + + // Set the form index based on the biome + // Defaults to Baile style if somehow nothing matches + const currentBiome = scene.arena.biomeType; + if (BAILE_STYLE_BIOMES.includes(currentBiome)) { + enemyPokemon.formIndex = 0; + } else if (POM_POM_STYLE_BIOMES.includes(currentBiome)) { + enemyPokemon.formIndex = 1; + } else if (PAU_STYLE_BIOMES.includes(currentBiome)) { + enemyPokemon.formIndex = 2; + } else if (SENSU_STYLE_BIOMES.includes(currentBiome)) { + enemyPokemon.formIndex = 3; + } else { + enemyPokemon.formIndex = 0; + } + + const oricorioData = new PokemonData(enemyPokemon); + const oricorio = scene.addEnemyPokemon(species, level, TrainerSlot.NONE, false, oricorioData); + + // Adds a real Pokemon sprite to the field (required for the animation) + scene.getEnemyParty().forEach(enemyPokemon => { + scene.field.remove(enemyPokemon, true); + }); + scene.currentBattle.enemyParty = [oricorio]; + scene.field.add(oricorio); + // Spawns on offscreen field + oricorio.x -= 300; + encounter.loadAssets.push(oricorio.loadAssets()); + + const config: EnemyPartyConfig = { + pokemonConfigs: [{ + species: species, + dataSource: oricorioData, + isBoss: true, + // Gets +1 to all stats except SPD on battle start + tags: [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON], + mysteryEncounterBattleEffects: (pokemon: Pokemon) => { + queueEncounterMessage(pokemon.scene, `${namespace}.option.1.boss_enraged`); + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF], 1)); + } + }], + }; + encounter.enemyPartyConfigs = [config]; + encounter.misc = { + oricorioData + }; + + encounter.setDialogueToken("oricorioName", getPokemonSpecies(Species.ORICORIO).getName()); + + return true; + }) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }) + .withOptionPhase(async (scene: BattleScene) => { + // Pick battle + const encounter = scene.currentBattle.mysteryEncounter!; + + encounter.startOfBattleEffects.push({ + sourceBattlerIndex: BattlerIndex.ENEMY, + targets: [BattlerIndex.PLAYER], + move: new PokemonMove(Moves.REVELATION_DANCE), + ignorePp: true + }); + + await hideOricorioPokemon(scene); + setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.BATON], fillRemaining: true }); + await initBattleWithEnemyConfig(scene, encounter.enemyPartyConfigs[0]); + }) + .build() + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected`, + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene) => { + // Learn its Dance + const encounter = scene.currentBattle.mysteryEncounter!; + + const onPokemonSelected = (pokemon: PlayerPokemon) => { + encounter.setDialogueToken("selectedPokemon", pokemon.getNameToRender()); + scene.unshiftPhase(new LearnMovePhase(scene, scene.getParty().indexOf(pokemon), Moves.REVELATION_DANCE)); + + // Play animation again to "learn" the dance + const danceAnim = new EncounterBattleAnim(EncounterAnim.DANCE, scene.getEnemyPokemon()!, scene.getPlayerPokemon()); + danceAnim.play(scene); + }; + + return selectPokemonForOption(scene, onPokemonSelected); + }) + .withOptionPhase(async (scene: BattleScene) => { + // Learn its Dance + hideOricorioPokemon(scene); + leaveEncounterWithoutBattle(scene, true); + }) + .build() + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL) + .withPrimaryPokemonRequirement(new MoveRequirement(DANCING_MOVES)) // Will set option3PrimaryName and option3PrimaryMove dialogue tokens automatically + .withDialogue({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + disabledButtonTooltip: `${namespace}.option.3.disabled_tooltip`, + secondOptionPrompt: `${namespace}.option.3.select_prompt`, + selected: [ + { + text: `${namespace}.option.3.selected`, + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene) => { + // Open menu for selecting pokemon with a Dancing move + const encounter = scene.currentBattle.mysteryEncounter!; + const onPokemonSelected = (pokemon: PlayerPokemon) => { + // Return the options for nature selection + return pokemon.moveset + .filter(move => move && DANCING_MOVES.includes(move.getMove().id)) + .map((move: PokemonMove) => { + const option: OptionSelectItem = { + label: move.getName(), + handler: () => { + // Pokemon and second option selected + encounter.setDialogueToken("selectedPokemon", pokemon.getNameToRender()); + encounter.setDialogueToken("selectedMove", move.getName()); + encounter.misc.selectedMove = move; + + return true; + }, + }; + return option; + }); + }; + + // Only challenge legal/unfainted Pokemon that have a Dancing move can be selected + const selectableFilter = (pokemon: Pokemon) => { + // If pokemon meets primary pokemon reqs, it can be selected + if (!pokemon.isAllowedInBattle()) { + return i18next.t("partyUiHandler:cantBeUsed", { pokemonName: pokemon.getNameToRender() }) ?? null; + } + const meetsReqs = encounter.options[2].pokemonMeetsPrimaryRequirements(scene, pokemon); + if (!meetsReqs) { + return getEncounterText(scene, `${namespace}.invalid_selection`) ?? null; + } + + return null; + }; + + return selectPokemonForOption(scene, onPokemonSelected, undefined, selectableFilter); + }) + .withOptionPhase(async (scene: BattleScene) => { + // Show the Oricorio a dance, and recruit it + const encounter = scene.currentBattle.mysteryEncounter!; + const oricorio = encounter.misc.oricorioData.toPokemon(scene); + oricorio.passive = true; + + // Ensure the Oricorio's moveset gains the Dance move the player used + const move = encounter.misc.selectedMove?.getMove().id; + if (!oricorio.moveset.some(m => m.getMove().id === move)) { + if (oricorio.moveset.length < 4) { + oricorio.moveset.push(new PokemonMove(move)); + } else { + oricorio.moveset[3] = new PokemonMove(move); + } + } + + hideOricorioPokemon(scene); + await catchPokemon(scene, oricorio, null, PokeballType.POKEBALL, false); + leaveEncounterWithoutBattle(scene, true); + }) + .build() + ) + .build(); + +function hideOricorioPokemon(scene: BattleScene) { + return new Promise(resolve => { + const oricorioSprite = scene.getEnemyParty()[0]; + scene.tweens.add({ + targets: oricorioSprite, + x: "+=16", + y: "-=16", + alpha: 0, + ease: "Sine.easeInOut", + duration: 750, + onComplete: () => { + scene.field.remove(oricorioSprite, true); + resolve(); + } + }); + }); +} diff --git a/src/data/mystery-encounters/encounters/dark-deal-encounter.ts b/src/data/mystery-encounters/encounters/dark-deal-encounter.ts new file mode 100644 index 00000000000..92009b12958 --- /dev/null +++ b/src/data/mystery-encounters/encounters/dark-deal-encounter.ts @@ -0,0 +1,210 @@ +import { Type } from "#app/data/type"; +import { isNullOrUndefined, randSeedInt } from "#app/utils"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { Species } from "#enums/species"; +import BattleScene from "#app/battle-scene"; +import { modifierTypes } from "#app/modifier/modifier-type"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { EnemyPartyConfig, EnemyPokemonConfig, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, } from "../utils/encounter-phase-utils"; +import { getRandomPlayerPokemon, getRandomSpeciesByStarterTier } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { ModifierRewardPhase } from "#app/phases/modifier-reward-phase"; +import { PokemonFormChangeItemModifier, PokemonHeldItemModifier } from "#app/modifier/modifier"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; + +/** i18n namespace for encounter */ +const namespace = "mysteryEncounter:darkDeal"; + +/** Exclude Ultra Beasts (inludes Cosmog/Solgaleo/Lunala/Necrozma), Paradox (includes Miraidon/Koraidon), Eternatus, and Mythicals */ +const excludedBosses = [ + Species.NECROZMA, + Species.COSMOG, + Species.COSMOEM, + Species.SOLGALEO, + Species.LUNALA, + Species.ETERNATUS, + Species.NIHILEGO, + Species.BUZZWOLE, + Species.PHEROMOSA, + Species.XURKITREE, + Species.CELESTEELA, + Species.KARTANA, + Species.GUZZLORD, + Species.POIPOLE, + Species.NAGANADEL, + Species.STAKATAKA, + Species.BLACEPHALON, + Species.GREAT_TUSK, + Species.SCREAM_TAIL, + Species.BRUTE_BONNET, + Species.FLUTTER_MANE, + Species.SLITHER_WING, + Species.SANDY_SHOCKS, + Species.ROARING_MOON, + Species.KORAIDON, + Species.WALKING_WAKE, + Species.GOUGING_FIRE, + Species.RAGING_BOLT, + Species.IRON_TREADS, + Species.IRON_BUNDLE, + Species.IRON_HANDS, + Species.IRON_JUGULIS, + Species.IRON_MOTH, + Species.IRON_THORNS, + Species.IRON_VALIANT, + Species.MIRAIDON, + Species.IRON_LEAVES, + Species.IRON_BOULDER, + Species.IRON_CROWN, + Species.MEW, + Species.CELEBI, + Species.DEOXYS, + Species.JIRACHI, + Species.DARKRAI, + Species.PHIONE, + Species.MANAPHY, + Species.ARCEUS, + Species.SHAYMIN, + Species.VICTINI, + Species.MELOETTA, + Species.KELDEO, + Species.GENESECT, + Species.DIANCIE, + Species.HOOPA, + Species.VOLCANION, + Species.MAGEARNA, + Species.MARSHADOW, + Species.ZERAORA, + Species.ZARUDE, + Species.MELTAN, + Species.MELMETAL, + Species.PECHARUNT, +]; + +/** + * Dark Deal encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3806 | GitHub Issue #3806} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const DarkDealEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.DARK_DEAL) + .withEncounterTier(MysteryEncounterTier.ROGUE) + .withIntroSpriteConfigs([ + { + spriteKey: "dark_deal_scientist", + fileRoot: "mystery-encounters", + hasShadow: true, + }, + { + spriteKey: "dark_deal_porygon", + fileRoot: "mystery-encounters", + hasShadow: true, + repeat: true, + }, + ]) + .withIntroDialogue([ + { + text: `${namespace}.intro`, + }, + { + speaker: `${namespace}.speaker`, + text: `${namespace}.intro_dialogue`, + }, + ]) + .withSceneWaveRangeRequirement(30, CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES[1]) + .withScenePartySizeRequirement(2, 6, true) // Must have at least 2 pokemon in party + .withCatchAllowed(true) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + speaker: `${namespace}.speaker`, + text: `${namespace}.option.1.selected_dialogue`, + }, + { + text: `${namespace}.option.1.selected_message`, + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene) => { + // Removes random pokemon (including fainted) from party and adds name to dialogue data tokens + // Will never return last battle able mon and instead pick fainted/unable to battle + const removedPokemon = getRandomPlayerPokemon(scene, true, false, true); + // Get all the pokemon's held items + const modifiers = removedPokemon.getHeldItems().filter(m => !(m instanceof PokemonFormChangeItemModifier)); + scene.removePokemonFromPlayerParty(removedPokemon); + + const encounter = scene.currentBattle.mysteryEncounter!; + encounter.setDialogueToken("pokeName", removedPokemon.getNameToRender()); + + // Store removed pokemon types + encounter.misc = { + removedTypes: removedPokemon.getTypes(), + modifiers + }; + }) + .withOptionPhase(async (scene: BattleScene) => { + // Give the player 5 Rogue Balls + const encounter = scene.currentBattle.mysteryEncounter!; + scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.ROGUE_BALL)); + + // Start encounter with random legendary (7-10 starter strength) that has level additive + const bossTypes: Type[] = encounter.misc.removedTypes; + const bossModifiers: PokemonHeldItemModifier[] = encounter.misc.modifiers; + // Starter egg tier, 35/50/10/5 %odds for tiers 6/7/8/9+ + const roll = randSeedInt(100); + const starterTier: number | [number, number] = + roll >= 65 ? 6 : roll >= 15 ? 7 : roll >= 5 ? 8 : [9, 10]; + const bossSpecies = getPokemonSpecies(getRandomSpeciesByStarterTier(starterTier, excludedBosses, bossTypes)); + const pokemonConfig: EnemyPokemonConfig = { + species: bossSpecies, + isBoss: true, + modifierConfigs: bossModifiers.map(m => { + return { + modifier: m + }; + }) + }; + if (!isNullOrUndefined(bossSpecies.forms) && bossSpecies.forms.length > 0) { + pokemonConfig.formIndex = 0; + } + const config: EnemyPartyConfig = { + pokemonConfigs: [pokemonConfig], + }; + return initBattleWithEnemyConfig(scene, config); + }) + .build() + ) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + speaker: `${namespace}.speaker`, + text: `${namespace}.option.2.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Leave encounter with no rewards or exp + leaveEncounterWithoutBattle(scene, true); + return true; + } + ) + .withOutroDialogue([ + { + text: `${namespace}.outro` + } + ]) + .build(); diff --git a/src/data/mystery-encounters/encounters/delibirdy-encounter.ts b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts new file mode 100644 index 00000000000..90ed486efd7 --- /dev/null +++ b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts @@ -0,0 +1,315 @@ +import { generateModifierType, leaveEncounterWithoutBattle, selectPokemonForOption, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import Pokemon, { PlayerPokemon } from "#app/field/pokemon"; +import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { Species } from "#enums/species"; +import BattleScene from "#app/battle-scene"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { CombinationPokemonRequirement, HeldItemRequirement, MoneyRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; +import { getEncounterText, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { BerryModifier, HealingBoosterModifier, HiddenAbilityRateBoosterModifier, LevelIncrementBoosterModifier, PokemonHeldItemModifier, PreserveBerryModifier } from "#app/modifier/modifier"; +import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; +import { applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import i18next from "#app/plugins/i18n"; +import { ModifierRewardPhase } from "#app/phases/modifier-reward-phase"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; + +/** the i18n namespace for this encounter */ +const namespace = "mysteryEncounter:delibirdy"; + +/** Berries only */ +const OPTION_2_ALLOWED_MODIFIERS = ["BerryModifier", "PokemonInstantReviveModifier"]; + +/** Disallowed items are berries, Reviver Seeds, and Vitamins (form change items and fusion items are not PokemonHeldItemModifiers) */ +const OPTION_3_DISALLOWED_MODIFIERS = [ + "BerryModifier", + "PokemonInstantReviveModifier", + "TerastallizeModifier", + "PokemonBaseStatModifier", + "PokemonBaseStatTotalModifier" +]; + +const DELIBIRDY_MONEY_PRICE_MULTIPLIER = 1.5; + +/** + * Delibird-y encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3804 | GitHub Issue #3804} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const DelibirdyEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.DELIBIRDY) + .withEncounterTier(MysteryEncounterTier.GREAT) + .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + .withSceneRequirement(new MoneyRequirement(0, DELIBIRDY_MONEY_PRICE_MULTIPLIER)) // Must have enough money for it to spawn at the very least + .withPrimaryPokemonRequirement(new CombinationPokemonRequirement( // Must also have either option 2 or 3 available to spawn + new HeldItemRequirement(OPTION_2_ALLOWED_MODIFIERS), + new HeldItemRequirement(OPTION_3_DISALLOWED_MODIFIERS, 1, true) + )) + .withIntroSpriteConfigs([ + { + spriteKey: "", + fileRoot: "", + species: Species.DELIBIRD, + hasShadow: true, + repeat: true, + startFrame: 38, + scale: 0.94 + }, + { + spriteKey: "", + fileRoot: "", + species: Species.DELIBIRD, + hasShadow: true, + repeat: true, + scale: 1.06 + }, + { + spriteKey: "", + fileRoot: "", + species: Species.DELIBIRD, + hasShadow: true, + repeat: true, + startFrame: 65, + x: 1, + y: 5, + yShadow: 5 + }, + ]) + .withIntroDialogue([ + { + text: `${namespace}.intro`, + } + ]) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withOutroDialogue([ + { + text: `${namespace}.outro`, + } + ]) + .withOnInit((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + encounter.setDialogueToken("delibirdName", getPokemonSpecies(Species.DELIBIRD).getName()); + + scene.loadBgm("mystery_encounter_delibirdy", "mystery_encounter_delibirdy.mp3"); + return true; + }) + .withOnVisualsStart((scene: BattleScene) => { + scene.fadeAndSwitchBgm("mystery_encounter_delibirdy"); + return true; + }) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) + .withSceneMoneyRequirement(0, DELIBIRDY_MONEY_PRICE_MULTIPLIER) // Must have money to spawn + .withDialogue({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene): Promise => { + const encounter = scene.currentBattle.mysteryEncounter!; + updatePlayerMoney(scene, -(encounter.options[0].requirements[0] as MoneyRequirement).requiredMoney, true, false); + return true; + }) + .withOptionPhase(async (scene: BattleScene) => { + // Give the player an Ability Charm + // Check if the player has max stacks of that item already + const existing = scene.findModifier(m => m instanceof HiddenAbilityRateBoosterModifier) as HiddenAbilityRateBoosterModifier; + + if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) { + // At max stacks, give the first party pokemon a Shell Bell instead + const shellBell = generateModifierType(scene, modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType; + await applyModifierTypeToPlayerPokemon(scene, scene.getParty()[0], shellBell); + scene.playSound("item_fanfare"); + await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, undefined, true); + } else { + scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.ABILITY_CHARM)); + } + + leaveEncounterWithoutBattle(scene, true); + }) + .build() + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) + .withPrimaryPokemonRequirement(new HeldItemRequirement(OPTION_2_ALLOWED_MODIFIERS)) + .withDialogue({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + secondOptionPrompt: `${namespace}.option.2.select_prompt`, + selected: [ + { + text: `${namespace}.option.2.selected`, + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene): Promise => { + const encounter = scene.currentBattle.mysteryEncounter!; + const onPokemonSelected = (pokemon: PlayerPokemon) => { + // Get Pokemon held items and filter for valid ones + const validItems = pokemon.getHeldItems().filter((it) => { + return OPTION_2_ALLOWED_MODIFIERS.some(heldItem => it.constructor.name === heldItem) && it.isTransferable; + }); + + return validItems.map((modifier: PokemonHeldItemModifier) => { + const option: OptionSelectItem = { + label: modifier.type.name, + handler: () => { + // Pokemon and item selected + encounter.setDialogueToken("chosenItem", modifier.type.name); + encounter.misc = { + chosenPokemon: pokemon, + chosenModifier: modifier, + }; + return true; + }, + }; + return option; + }); + }; + + const selectableFilter = (pokemon: Pokemon) => { + // If pokemon has valid item, it can be selected + const meetsReqs = encounter.options[1].pokemonMeetsPrimaryRequirements(scene, pokemon); + if (!meetsReqs) { + return getEncounterText(scene, `${namespace}.invalid_selection`) ?? null; + } + + return null; + }; + + return selectPokemonForOption(scene, onPokemonSelected, undefined, selectableFilter); + }) + .withOptionPhase(async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + const modifier = encounter.misc.chosenModifier; + + // Give the player a Candy Jar if they gave a Berry, and a Healing Charm for Reviver Seed + if (modifier instanceof BerryModifier) { + // Check if the player has max stacks of that Candy Jar already + const existing = scene.findModifier(m => m instanceof LevelIncrementBoosterModifier) as LevelIncrementBoosterModifier; + + if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) { + // At max stacks, give the first party pokemon a Shell Bell instead + const shellBell = generateModifierType(scene, modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType; + await applyModifierTypeToPlayerPokemon(scene, scene.getParty()[0], shellBell); + scene.playSound("item_fanfare"); + await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, undefined, true); + } else { + scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.CANDY_JAR)); + } + } else { + // Check if the player has max stacks of that Healing Charm already + const existing = scene.findModifier(m => m instanceof HealingBoosterModifier) as HealingBoosterModifier; + + if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) { + // At max stacks, give the first party pokemon a Shell Bell instead + const shellBell = generateModifierType(scene, modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType; + await applyModifierTypeToPlayerPokemon(scene, scene.getParty()[0], shellBell); + scene.playSound("item_fanfare"); + await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, undefined, true); + } else { + scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.HEALING_CHARM)); + } + } + + // Remove the modifier if its stacks go to 0 + modifier.stackCount -= 1; + if (modifier.stackCount === 0) { + scene.removeModifier(modifier); + } + + leaveEncounterWithoutBattle(scene, true); + }) + .build() + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) + .withPrimaryPokemonRequirement(new HeldItemRequirement(OPTION_3_DISALLOWED_MODIFIERS, 1, true)) + .withDialogue({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + secondOptionPrompt: `${namespace}.option.3.select_prompt`, + selected: [ + { + text: `${namespace}.option.3.selected`, + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene): Promise => { + const encounter = scene.currentBattle.mysteryEncounter!; + const onPokemonSelected = (pokemon: PlayerPokemon) => { + // Get Pokemon held items and filter for valid ones + const validItems = pokemon.getHeldItems().filter((it) => { + return !OPTION_3_DISALLOWED_MODIFIERS.some(heldItem => it.constructor.name === heldItem) && it.isTransferable; + }); + + return validItems.map((modifier: PokemonHeldItemModifier) => { + const option: OptionSelectItem = { + label: modifier.type.name, + handler: () => { + // Pokemon and item selected + encounter.setDialogueToken("chosenItem", modifier.type.name); + encounter.misc = { + chosenPokemon: pokemon, + chosenModifier: modifier, + }; + return true; + }, + }; + return option; + }); + }; + + const selectableFilter = (pokemon: Pokemon) => { + // If pokemon has valid item, it can be selected + const meetsReqs = encounter.options[2].pokemonMeetsPrimaryRequirements(scene, pokemon); + if (!meetsReqs) { + return getEncounterText(scene, `${namespace}.invalid_selection`) ?? null; + } + + return null; + }; + + return selectPokemonForOption(scene, onPokemonSelected, undefined, selectableFilter); + }) + .withOptionPhase(async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + const modifier = encounter.misc.chosenModifier; + + // Check if the player has max stacks of Berry Pouch already + const existing = scene.findModifier(m => m instanceof PreserveBerryModifier) as PreserveBerryModifier; + + if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) { + // At max stacks, give the first party pokemon a Shell Bell instead + const shellBell = generateModifierType(scene, modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType; + await applyModifierTypeToPlayerPokemon(scene, scene.getParty()[0], shellBell); + scene.playSound("item_fanfare"); + await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, undefined, true); + } else { + scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.BERRY_POUCH)); + } + + // Remove the modifier if its stacks go to 0 + modifier.stackCount -= 1; + if (modifier.stackCount === 0) { + scene.removeModifier(modifier); + } + + leaveEncounterWithoutBattle(scene, true); + }) + .build() + ) + .build(); diff --git a/src/data/mystery-encounters/encounters/department-store-sale-encounter.ts b/src/data/mystery-encounters/encounters/department-store-sale-encounter.ts new file mode 100644 index 00000000000..db25d338a29 --- /dev/null +++ b/src/data/mystery-encounters/encounters/department-store-sale-encounter.ts @@ -0,0 +1,164 @@ +import { + leaveEncounterWithoutBattle, + setEncounterRewards, +} from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { ModifierTypeFunc, modifierTypes } from "#app/modifier/modifier-type"; +import { randSeedInt } from "#app/utils"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { Species } from "#enums/species"; +import BattleScene from "#app/battle-scene"; +import MysteryEncounter, { + MysteryEncounterBuilder, +} from "#app/data/mystery-encounters/mystery-encounter"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; + +/** i18n namespace for encounter */ +const namespace = "mysteryEncounter:departmentStoreSale"; + +/** + * Department Store Sale encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3797 | GitHub Issue #3797} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const DepartmentStoreSaleEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.DEPARTMENT_STORE_SALE) + .withEncounterTier(MysteryEncounterTier.COMMON) + .withSceneWaveRangeRequirement(CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES[0], 100) + .withIntroSpriteConfigs([ + { + spriteKey: "department_store_sale_lady", + fileRoot: "mystery-encounters", + hasShadow: true, + x: -20, + }, + { + spriteKey: "", + fileRoot: "", + species: Species.FURFROU, + hasShadow: true, + repeat: true, + x: 30, + }, + ]) + .withIntroDialogue([ + { + text: `${namespace}.intro`, + }, + { + text: `${namespace}.intro_dialogue`, + speaker: `${namespace}.speaker`, + }, + ]) + .withAutoHideIntroVisuals(false) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + }, + async (scene: BattleScene) => { + // Choose TMs + const modifiers: ModifierTypeFunc[] = []; + let i = 0; + while (i < 4) { + // 2/2/1 weight on TM rarity + const roll = randSeedInt(5); + if (roll < 2) { + modifiers.push(modifierTypes.TM_COMMON); + } else if (roll < 4) { + modifiers.push(modifierTypes.TM_GREAT); + } else { + modifiers.push(modifierTypes.TM_ULTRA); + } + i++; + } + + setEncounterRewards(scene, { guaranteedModifierTypeFuncs: modifiers, fillRemaining: false, }); + leaveEncounterWithoutBattle(scene); + } + ) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + }, + async (scene: BattleScene) => { + // Choose Vitamins + const modifiers: ModifierTypeFunc[] = []; + let i = 0; + while (i < 3) { + // 2/1 weight on base stat booster vs PP Up + const roll = randSeedInt(3); + if (roll === 0) { + modifiers.push(modifierTypes.PP_UP); + } else { + modifiers.push(modifierTypes.BASE_STAT_BOOSTER); + } + i++; + } + + setEncounterRewards(scene, { guaranteedModifierTypeFuncs: modifiers, fillRemaining: false, }); + leaveEncounterWithoutBattle(scene); + } + ) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + }, + async (scene: BattleScene) => { + // Choose X Items + const modifiers: ModifierTypeFunc[] = []; + let i = 0; + while (i < 5) { + // 4/1 weight on base stat booster vs Dire Hit + const roll = randSeedInt(5); + if (roll === 0) { + modifiers.push(modifierTypes.DIRE_HIT); + } else { + modifiers.push(modifierTypes.TEMP_STAT_STAGE_BOOSTER); + } + i++; + } + + setEncounterRewards(scene, { guaranteedModifierTypeFuncs: modifiers, fillRemaining: false, }); + leaveEncounterWithoutBattle(scene); + } + ) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.4.label`, + buttonTooltip: `${namespace}.option.4.tooltip`, + }, + async (scene: BattleScene) => { + // Choose Pokeballs + const modifiers: ModifierTypeFunc[] = []; + let i = 0; + while (i < 4) { + // 10/30/20/5 weight on pokeballs + const roll = randSeedInt(65); + if (roll < 10) { + modifiers.push(modifierTypes.POKEBALL); + } else if (roll < 40) { + modifiers.push(modifierTypes.GREAT_BALL); + } else if (roll < 60) { + modifiers.push(modifierTypes.ULTRA_BALL); + } else { + modifiers.push(modifierTypes.ROGUE_BALL); + } + i++; + } + + setEncounterRewards(scene, { guaranteedModifierTypeFuncs: modifiers, fillRemaining: false, }); + leaveEncounterWithoutBattle(scene); + } + ) + .withOutroDialogue([ + { + text: `${namespace}.outro`, + } + ]) + .build(); diff --git a/src/data/mystery-encounters/encounters/field-trip-encounter.ts b/src/data/mystery-encounters/encounters/field-trip-encounter.ts new file mode 100644 index 00000000000..82f27c5e59b --- /dev/null +++ b/src/data/mystery-encounters/encounters/field-trip-encounter.ts @@ -0,0 +1,235 @@ +import { MoveCategory } from "#app/data/move"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { generateModifierTypeOption, leaveEncounterWithoutBattle, selectPokemonForOption, setEncounterExp, setEncounterRewards } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { PlayerPokemon, PokemonMove } from "#app/field/pokemon"; +import { modifierTypes } from "#app/modifier/modifier-type"; +import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import BattleScene from "#app/battle-scene"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { Stat } from "#enums/stat"; +import i18next from "i18next"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; + +/** i18n namespace for the encounter */ +const namespace = "mysteryEncounter:fieldTrip"; + +/** + * Field Trip encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3794 | GitHub Issue #3794} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const FieldTripEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.FIELD_TRIP) + .withEncounterTier(MysteryEncounterTier.COMMON) + .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + .withIntroSpriteConfigs([ + { + spriteKey: "preschooler_m", + fileRoot: "trainer", + hasShadow: true, + }, + { + spriteKey: "field_trip_teacher", + fileRoot: "mystery-encounters", + hasShadow: true, + }, + { + spriteKey: "preschooler_f", + fileRoot: "trainer", + hasShadow: true, + }, + ]) + .withIntroDialogue([ + { + text: `${namespace}.intro`, + }, + { + text: `${namespace}.intro_dialogue`, + speaker: `${namespace}.speaker`, + }, + ]) + .withAutoHideIntroVisuals(false) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + secondOptionPrompt: `${namespace}.second_option_prompt`, + }) + .withPreOptionPhase(async (scene: BattleScene): Promise => { + const encounter = scene.currentBattle.mysteryEncounter!; + const onPokemonSelected = (pokemon: PlayerPokemon) => { + // Return the options for Pokemon move valid for this option + return pokemon.moveset.map((move: PokemonMove) => { + const option: OptionSelectItem = { + label: move.getName(), + handler: () => { + // Pokemon and move selected + encounter.setDialogueToken("moveCategory", i18next.t(`${namespace}.physical`)); + pokemonAndMoveChosen(scene, pokemon, move, MoveCategory.PHYSICAL); + return true; + }, + }; + return option; + }); + }; + + return selectPokemonForOption(scene, onPokemonSelected); + }) + .withOptionPhase(async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + if (encounter.misc.correctMove) { + const modifiers = [ + generateModifierTypeOption(scene, modifierTypes.TEMP_STAT_STAGE_BOOSTER, [Stat.ATK])!, + generateModifierTypeOption(scene, modifierTypes.TEMP_STAT_STAGE_BOOSTER, [Stat.DEF])!, + generateModifierTypeOption(scene, modifierTypes.TEMP_STAT_STAGE_BOOSTER, [Stat.SPD])!, + generateModifierTypeOption(scene, modifierTypes.DIRE_HIT)!, + generateModifierTypeOption(scene, modifierTypes.RARER_CANDY)!, + ]; + + setEncounterRewards(scene, { guaranteedModifierTypeOptions: modifiers, fillRemaining: false }); + } + + leaveEncounterWithoutBattle(scene, !encounter.misc.correctMove); + }) + .build() + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + secondOptionPrompt: `${namespace}.second_option_prompt`, + }) + .withPreOptionPhase(async (scene: BattleScene): Promise => { + const encounter = scene.currentBattle.mysteryEncounter!; + const onPokemonSelected = (pokemon: PlayerPokemon) => { + // Return the options for Pokemon move valid for this option + return pokemon.moveset.map((move: PokemonMove) => { + const option: OptionSelectItem = { + label: move.getName(), + handler: () => { + // Pokemon and move selected + encounter.setDialogueToken("moveCategory", i18next.t(`${namespace}.special`)); + pokemonAndMoveChosen(scene, pokemon, move, MoveCategory.SPECIAL); + return true; + }, + }; + return option; + }); + }; + + return selectPokemonForOption(scene, onPokemonSelected); + }) + .withOptionPhase(async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + if (encounter.misc.correctMove) { + const modifiers = [ + generateModifierTypeOption(scene, modifierTypes.TEMP_STAT_STAGE_BOOSTER, [Stat.SPATK])!, + generateModifierTypeOption(scene, modifierTypes.TEMP_STAT_STAGE_BOOSTER, [Stat.SPDEF])!, + generateModifierTypeOption(scene, modifierTypes.TEMP_STAT_STAGE_BOOSTER, [Stat.SPD])!, + generateModifierTypeOption(scene, modifierTypes.DIRE_HIT)!, + generateModifierTypeOption(scene, modifierTypes.RARER_CANDY)!, + ]; + + setEncounterRewards(scene, { guaranteedModifierTypeOptions: modifiers, fillRemaining: false }); + } + + leaveEncounterWithoutBattle(scene, !encounter.misc.correctMove); + }) + .build() + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + secondOptionPrompt: `${namespace}.second_option_prompt`, + }) + .withPreOptionPhase(async (scene: BattleScene): Promise => { + const encounter = scene.currentBattle.mysteryEncounter!; + const onPokemonSelected = (pokemon: PlayerPokemon) => { + // Return the options for Pokemon move valid for this option + return pokemon.moveset.map((move: PokemonMove) => { + const option: OptionSelectItem = { + label: move.getName(), + handler: () => { + // Pokemon and move selected + encounter.setDialogueToken("moveCategory", i18next.t(`${namespace}.status`)); + pokemonAndMoveChosen(scene, pokemon, move, MoveCategory.STATUS); + return true; + }, + }; + return option; + }); + }; + + return selectPokemonForOption(scene, onPokemonSelected); + }) + .withOptionPhase(async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + if (encounter.misc.correctMove) { + const modifiers = [ + generateModifierTypeOption(scene, modifierTypes.TEMP_STAT_STAGE_BOOSTER, [Stat.ACC])!, + generateModifierTypeOption(scene, modifierTypes.TEMP_STAT_STAGE_BOOSTER, [Stat.SPD])!, + generateModifierTypeOption(scene, modifierTypes.GREAT_BALL)!, + generateModifierTypeOption(scene, modifierTypes.IV_SCANNER)!, + generateModifierTypeOption(scene, modifierTypes.RARER_CANDY)!, + ]; + + setEncounterRewards(scene, { guaranteedModifierTypeOptions: modifiers, fillRemaining: false }); + } + + leaveEncounterWithoutBattle(scene, !encounter.misc.correctMove); + }) + .build() + ) + .build(); + +function pokemonAndMoveChosen(scene: BattleScene, pokemon: PlayerPokemon, move: PokemonMove, correctMoveCategory: MoveCategory) { + const encounter = scene.currentBattle.mysteryEncounter!; + const correctMove = move.getMove().category === correctMoveCategory; + encounter.setDialogueToken("pokeName", pokemon.getNameToRender()); + encounter.setDialogueToken("move", move.getName()); + if (!correctMove) { + encounter.selectedOption!.dialogue!.selected = [ + { + text: `${namespace}.option.selected`, + }, + { + text: `${namespace}.incorrect`, + speaker: `${namespace}.speaker`, + }, + { + text: `${namespace}.incorrect_exp`, + }, + ]; + setEncounterExp(scene, scene.getParty().map((p) => p.id), 50); + } else { + encounter.selectedOption!.dialogue!.selected = [ + { + text: `${namespace}.option.selected`, + }, + { + text: `${namespace}.correct`, + speaker: `${namespace}.speaker`, + }, + { + text: `${namespace}.correct_exp`, + }, + ]; + setEncounterExp(scene, [pokemon.id], 100); + } + encounter.misc = { + correctMove: correctMove, + }; +} diff --git a/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts new file mode 100644 index 00000000000..4f5430b63d9 --- /dev/null +++ b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts @@ -0,0 +1,255 @@ +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { EnemyPartyConfig, initBattleWithEnemyConfig, loadCustomMovesForEncounter, leaveEncounterWithoutBattle, setEncounterExp, setEncounterRewards, transitionMysteryEncounterIntroVisuals, generateModifierType } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { AttackTypeBoosterModifierType, modifierTypes, } from "#app/modifier/modifier-type"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import BattleScene from "#app/battle-scene"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { TypeRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; +import { Species } from "#enums/species"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { Gender } from "#app/data/gender"; +import { Type } from "#app/data/type"; +import { BattlerIndex } from "#app/battle"; +import { PokemonMove } from "#app/field/pokemon"; +import { Moves } from "#enums/moves"; +import { EncounterBattleAnim } from "#app/data/battle-anims"; +import { WeatherType } from "#app/data/weather"; +import { isNullOrUndefined, randSeedInt } from "#app/utils"; +import { StatusEffect } from "#app/data/status-effect"; +import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { applyDamageToPokemon, applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { EncounterAnim } from "#enums/encounter-anims"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; + +/** the i18n namespace for the encounter */ +const namespace = "mysteryEncounter:fieryFallout"; + +/** + * Damage percentage taken when suffering the heat. + * Can be a number between `0` - `100`. + * The higher the more damage taken (100% = instant KO). + */ +const DAMAGE_PERCENTAGE: number = 20; + +/** + * Fiery Fallout encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3814 | GitHub Issue #3814} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const FieryFalloutEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.FIERY_FALLOUT) + .withEncounterTier(MysteryEncounterTier.COMMON) + .withSceneWaveRangeRequirement(40, CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES[1]) + .withCatchAllowed(true) + .withIntroSpriteConfigs([]) // Set in onInit() + .withAnimations(EncounterAnim.MAGMA_BG, EncounterAnim.MAGMA_SPOUT) + .withAutoHideIntroVisuals(false) + .withIntroDialogue([ + { + text: `${namespace}.intro`, + }, + ]) + .withOnInit((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + + // Calculate boss mons + const volcaronaSpecies = getPokemonSpecies(Species.VOLCARONA); + const config: EnemyPartyConfig = { + pokemonConfigs: [ + { + species: volcaronaSpecies, + isBoss: false, + gender: Gender.MALE + }, + { + species: volcaronaSpecies, + isBoss: false, + gender: Gender.FEMALE + } + ], + doubleBattle: true, + disableSwitch: true + }; + encounter.enemyPartyConfigs = [config]; + + // Load hidden Volcarona sprites + encounter.spriteConfigs = [ + { + spriteKey: "", + fileRoot: "", + species: Species.VOLCARONA, + repeat: true, + hidden: true, + hasShadow: true, + x: -20, + startFrame: 20 + }, + { + spriteKey: "", + fileRoot: "", + species: Species.VOLCARONA, + repeat: true, + hidden: true, + hasShadow: true, + x: 20 + }, + ]; + + // Load animations/sfx for Volcarona moves + loadCustomMovesForEncounter(scene, [Moves.FIRE_SPIN, Moves.QUIVER_DANCE]); + + scene.arena.trySetWeather(WeatherType.SUNNY, true); + + encounter.setDialogueToken("volcaronaName", getPokemonSpecies(Species.VOLCARONA).getName()); + + return true; + }) + .withOnVisualsStart((scene: BattleScene) => { + // Play animations + const background = new EncounterBattleAnim(EncounterAnim.MAGMA_BG, scene.getPlayerPokemon()!, scene.getPlayerPokemon()); + background.playWithoutTargets(scene, 200, 70, 2, 3); + const animation = new EncounterBattleAnim(EncounterAnim.MAGMA_SPOUT, scene.getPlayerPokemon()!, scene.getPlayerPokemon()); + animation.playWithoutTargets(scene, 80, 100, 2); + scene.time.delayedCall(600, () => { + animation.playWithoutTargets(scene, -20, 100, 2); + }); + scene.time.delayedCall(1200, () => { + animation.playWithoutTargets(scene, 140, 150, 2); + }); + + return true; + }) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Pick battle + const encounter = scene.currentBattle.mysteryEncounter!; + setEncounterRewards(scene, { fillRemaining: true }, undefined, () => giveLeadPokemonCharcoal(scene)); + + encounter.startOfBattleEffects.push( + { + sourceBattlerIndex: BattlerIndex.ENEMY, + targets: [BattlerIndex.PLAYER], + move: new PokemonMove(Moves.FIRE_SPIN), + ignorePp: true + }, + { + sourceBattlerIndex: BattlerIndex.ENEMY_2, + targets: [BattlerIndex.PLAYER_2], + move: new PokemonMove(Moves.FIRE_SPIN), + ignorePp: true + }, + { + sourceBattlerIndex: BattlerIndex.ENEMY, + targets: [BattlerIndex.ENEMY], + move: new PokemonMove(Moves.QUIVER_DANCE), + ignorePp: true + }, + { + sourceBattlerIndex: BattlerIndex.ENEMY_2, + targets: [BattlerIndex.ENEMY_2], + move: new PokemonMove(Moves.QUIVER_DANCE), + ignorePp: true + }); + await initBattleWithEnemyConfig(scene, scene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0]); + } + ) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Damage non-fire types and burn 1 random non-fire type member + const encounter = scene.currentBattle.mysteryEncounter!; + const nonFireTypes = scene.getParty().filter((p) => p.isAllowedInBattle() && !p.getTypes().includes(Type.FIRE)); + + for (const pkm of nonFireTypes) { + const percentage = DAMAGE_PERCENTAGE / 100; + const damage = Math.floor(pkm.getMaxHp() * percentage); + applyDamageToPokemon(scene, pkm, damage); + } + + // Burn random member + const burnable = nonFireTypes.filter(p => isNullOrUndefined(p.status) || isNullOrUndefined(p.status!.effect) || p.status?.effect === StatusEffect.NONE); + if (burnable?.length > 0) { + const roll = randSeedInt(burnable.length); + const chosenPokemon = burnable[roll]; + if (chosenPokemon.trySetStatus(StatusEffect.BURN)) { + // Burn applied + encounter.setDialogueToken("burnedPokemon", chosenPokemon.getNameToRender()); + queueEncounterMessage(scene, `${namespace}.option.2.target_burned`); + } + } + + // No rewards + leaveEncounterWithoutBattle(scene, true); + } + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL) + .withPrimaryPokemonRequirement(new TypeRequirement(Type.FIRE, true, 1)) // Will set option3PrimaryName dialogue token automatically + .withSecondaryPokemonRequirement(new TypeRequirement(Type.FIRE, true, 1)) // Will set option3SecondaryName dialogue token automatically + .withDialogue({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + disabledButtonTooltip: `${namespace}.option.3.disabled_tooltip`, + selected: [ + { + text: `${namespace}.option.3.selected`, + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene) => { + transitionMysteryEncounterIntroVisuals(scene, false, false, 2000); + }) + .withOptionPhase(async (scene: BattleScene) => { + // Fire types help calm the Volcarona + const encounter = scene.currentBattle.mysteryEncounter!; + transitionMysteryEncounterIntroVisuals(scene); + setEncounterRewards(scene, + { fillRemaining: true }, + undefined, + () => { + giveLeadPokemonCharcoal(scene); + }); + + const primary = encounter.options[2].primaryPokemon!; + const secondary = encounter.options[2].secondaryPokemon![0]; + + setEncounterExp(scene, [primary.id, secondary.id], getPokemonSpecies(Species.VOLCARONA).baseExp * 2); + leaveEncounterWithoutBattle(scene); + }) + .build() + ) + .build(); + +function giveLeadPokemonCharcoal(scene: BattleScene) { + // Give first party pokemon Charcoal for free at end of battle + const leadPokemon = scene.getParty()?.[0]; + if (leadPokemon) { + const charcoal = generateModifierType(scene, modifierTypes.ATTACK_TYPE_BOOSTER, [Type.FIRE]) as AttackTypeBoosterModifierType; + applyModifierTypeToPlayerPokemon(scene, leadPokemon, charcoal); + scene.currentBattle.mysteryEncounter!.setDialogueToken("leadPokemon", leadPokemon.getNameToRender()); + queueEncounterMessage(scene, `${namespace}.found_charcoal`); + } +} diff --git a/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts b/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts new file mode 100644 index 00000000000..349984f1958 --- /dev/null +++ b/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts @@ -0,0 +1,186 @@ +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { + EnemyPartyConfig, + initBattleWithEnemyConfig, + leaveEncounterWithoutBattle, setEncounterExp, + setEncounterRewards +} from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { STEALING_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups"; +import Pokemon, { EnemyPokemon } from "#app/field/pokemon"; +import { ModifierTier } from "#app/modifier/modifier-tier"; +import { + getPartyLuckValue, + getPlayerModifierTypeOptions, + ModifierPoolType, + ModifierTypeOption, + regenerateModifierPoolThresholds, +} from "#app/modifier/modifier-type"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import BattleScene from "#app/battle-scene"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MoveRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { TrainerSlot } from "#app/data/trainer-config"; +import { getEncounterPokemonLevelForWave, getSpriteKeysFromPokemon, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import PokemonData from "#app/system/pokemon-data"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { randSeedInt } from "#app/utils"; +import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; + +/** the i18n namespace for the encounter */ +const namespace = "mysteryEncounter:fightOrFlight"; + +/** + * Fight or Flight encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3795 | GitHub Issue #3795} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const FightOrFlightEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.FIGHT_OR_FLIGHT) + .withEncounterTier(MysteryEncounterTier.COMMON) + .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + .withCatchAllowed(true) + .withHideWildIntroMessage(true) + .withIntroSpriteConfigs([]) // Set in onInit() + .withIntroDialogue([ + { + text: `${namespace}.intro`, + }, + ]) + .withOnInit((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + + // Calculate boss mon + const level = getEncounterPokemonLevelForWave(scene, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER); + const bossSpecies = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getParty()), true); + const bossPokemon = new EnemyPokemon(scene, bossSpecies, level, TrainerSlot.NONE, true); + encounter.setDialogueToken("enemyPokemon", bossPokemon.getNameToRender()); + const config: EnemyPartyConfig = { + pokemonConfigs: [{ + level: level, + species: bossSpecies, + dataSource: new PokemonData(bossPokemon), + isBoss: true, + tags: [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON], + mysteryEncounterBattleEffects: (pokemon: Pokemon) => { + queueEncounterMessage(pokemon.scene, `${namespace}.option.1.stat_boost`); + // Randomly boost 1 stat 2 stages + // Cannot boost Spd, Acc, or Evasion + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [randSeedInt(4, 1)], 2)); + } + }], + }; + encounter.enemyPartyConfigs = [config]; + + // Calculate item + // Waves 10-40 GREAT, 60-120 ULTRA, 120-160 ROGUE, 160-180 MASTER + const tier = + scene.currentBattle.waveIndex > 160 + ? ModifierTier.MASTER + : scene.currentBattle.waveIndex > 120 + ? ModifierTier.ROGUE + : scene.currentBattle.waveIndex > 40 + ? ModifierTier.ULTRA + : ModifierTier.GREAT; + regenerateModifierPoolThresholds(scene.getParty(), ModifierPoolType.PLAYER, 0); + let item: ModifierTypeOption | null = null; + // TMs and Candy Jar excluded from possible rewards as they're too swingy in value for a singular item reward + while (!item || item.type.id.includes("TM_") || item.type.id === "CANDY_JAR") { + item = getPlayerModifierTypeOptions(1, scene.getParty(), [], { guaranteedModifierTiers: [tier], allowLuckUpgrades: false })[0]; + } + encounter.setDialogueToken("itemName", item.type.name); + encounter.misc = item; + + const { spriteKey, fileRoot } = getSpriteKeysFromPokemon(bossPokemon); + encounter.spriteConfigs = [ + { + spriteKey: item.type.iconImage, + fileRoot: "items", + hasShadow: false, + x: 35, + y: -5, + scale: 0.75, + isItem: true, + disableAnimation: true + }, + { + spriteKey: spriteKey, + fileRoot: fileRoot, + hasShadow: true, + tint: 0.25, + x: -5, + repeat: true, + isPokemon: true + }, + ]; + + return true; + }) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Pick battle + // Pokemon will randomly boost 1 stat by 2 stages + const item = scene.currentBattle.mysteryEncounter!.misc as ModifierTypeOption; + setEncounterRewards(scene, { guaranteedModifierTypeOptions: [item], fillRemaining: false }); + await initBattleWithEnemyConfig(scene, scene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0]); + } + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL) + .withPrimaryPokemonRequirement(new MoveRequirement(STEALING_MOVES)) // Will set option2PrimaryName and option2PrimaryMove dialogue tokens automatically + .withDialogue({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + disabledButtonTooltip: `${namespace}.option.2.disabled_tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected` + } + ] + }) + .withOptionPhase(async (scene: BattleScene) => { + // Pick steal + const encounter = scene.currentBattle.mysteryEncounter!; + const item = scene.currentBattle.mysteryEncounter!.misc as ModifierTypeOption; + setEncounterRewards(scene, { guaranteedModifierTypeOptions: [item], fillRemaining: false }); + + // Use primaryPokemon to execute the thievery + const primaryPokemon = encounter.options[1].primaryPokemon!; + setEncounterExp(scene, primaryPokemon.id, encounter.enemyPartyConfigs[0].pokemonConfigs![0].species.baseExp); + leaveEncounterWithoutBattle(scene); + }) + .build() + ) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + selected: [ + { + text: `${namespace}.option.3.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Leave encounter with no rewards or exp + leaveEncounterWithoutBattle(scene, true); + return true; + } + ) + .build(); diff --git a/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts b/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts new file mode 100644 index 00000000000..a144aa88299 --- /dev/null +++ b/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts @@ -0,0 +1,418 @@ +import { leaveEncounterWithoutBattle, selectPokemonForOption, setEncounterRewards, transitionMysteryEncounterIntroVisuals, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import BattleScene from "#app/battle-scene"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { TrainerSlot } from "#app/data/trainer-config"; +import Pokemon, { FieldPosition, PlayerPokemon } from "#app/field/pokemon"; +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"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { Species } from "#enums/species"; +import i18next from "i18next"; +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 { ShinySparklePhase } from "#app/phases/shiny-sparkle-phase"; +import { SpeciesFormChangeActiveTrigger } from "#app/data/pokemon-forms"; +import { PostSummonPhase } from "#app/phases/post-summon-phase"; +import { modifierTypes } from "#app/modifier/modifier-type"; +import { Nature } from "#enums/nature"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; +import { isPokemonValidForEncounterOptionSelection } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; + +/** the i18n namespace for the encounter */ +const namespace = "mysteryEncounter:funAndGames"; + +/** + * Fun and Games! encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3819 | GitHub Issue #3819} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const FunAndGamesEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.FUN_AND_GAMES) + .withEncounterTier(MysteryEncounterTier.GREAT) + .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + .withSceneRequirement(new MoneyRequirement(0, 1.5)) // Cost equal to 1 Max Potion to play + .withAutoHideIntroVisuals(false) + // Allows using move without a visible enemy pokemon + .withBattleAnimationsWithoutTargets(true) + // The Wobbuffet won't use moves + .withSkipEnemyBattleTurns(true) + // Will skip COMMAND selection menu and go straight to FIGHT (move select) menu + .withSkipToFightInput(true) + .withIntroSpriteConfigs([ + { + spriteKey: "fun_and_games_game", + fileRoot: "mystery-encounters", + hasShadow: false, + x: 0, + y: 6, + }, + { + spriteKey: "fun_and_games_wobbuffet", + fileRoot: "mystery-encounters", + hasShadow: true, + x: -28, + y: 6, + yShadow: 6 + }, + { + spriteKey: "fun_and_games_man", + fileRoot: "mystery-encounters", + hasShadow: true, + x: 40, + y: 6, + yShadow: 6 + }, + ]) + .withIntroDialogue([ + { + speaker: `${namespace}.speaker`, + text: `${namespace}.intro_dialogue`, + }, + ]) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withOnInit((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + scene.loadBgm("mystery_encounter_fun_and_games", "mystery_encounter_fun_and_games.mp3"); + encounter.setDialogueToken("wobbuffetName", getPokemonSpecies(Species.WOBBUFFET).getName()); + return true; + }) + .withOnVisualsStart((scene: BattleScene) => { + scene.fadeAndSwitchBgm("mystery_encounter_fun_and_games"); + return true; + }) + .withOption(MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) + .withSceneRequirement(new MoneyRequirement(0, 1.5)) // Cost equal to 1 Max Potion + .withDialogue({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene) => { + // Select Pokemon for minigame + const encounter = scene.currentBattle.mysteryEncounter!; + const onPokemonSelected = (pokemon: PlayerPokemon) => { + encounter.misc = { + playerPokemon: pokemon, + }; + }; + + // Only Pokemon that are not KOed/legal can be selected + const selectableFilter = (pokemon: Pokemon) => { + return isPokemonValidForEncounterOptionSelection(pokemon, scene, `${namespace}.invalid_selection`); + }; + + return selectPokemonForOption(scene, onPokemonSelected, undefined, selectableFilter); + }) + .withOptionPhase(async (scene: BattleScene) => { + // Start minigame + const encounter = scene.currentBattle.mysteryEncounter!; + encounter.misc.turnsRemaining = 3; + + // Update money + const moneyCost = (encounter.options[0].requirements[0] as MoneyRequirement).requiredMoney; + updatePlayerMoney(scene, -moneyCost, true, false); + await showEncounterText(scene, i18next.t("mysteryEncounterMessages:paid_money", { amount: moneyCost })); + + // Handlers for battle events + encounter.onTurnStart = handleNextTurn; // triggered during TurnInitPhase + encounter.doContinueEncounter = handleLoseMinigame; // triggered during MysteryEncounterRewardsPhase, post VictoryPhase if the player KOs Wobbuffet + + hideShowmanIntroSprite(scene); + await summonPlayerPokemon(scene); + await showWobbuffetHealthBar(scene); + + return true; + }) + .build() + ) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Leave encounter with no rewards or exp + transitionMysteryEncounterIntroVisuals(scene, true, true); + leaveEncounterWithoutBattle(scene, true); + return true; + } + ) + .build(); + +async function summonPlayerPokemon(scene: BattleScene) { + return new Promise(async resolve => { + const encounter = scene.currentBattle.mysteryEncounter!; + + const playerPokemon = encounter.misc.playerPokemon; + // Swaps the chosen Pokemon and the first player's lead Pokemon in the party + const party = scene.getParty(); + const chosenIndex = party.indexOf(playerPokemon); + if (chosenIndex !== 0) { + const leadPokemon = party[0]; + party[0] = playerPokemon; + party[chosenIndex] = leadPokemon; + } + + // Do trainer summon animation + let playerAnimationPromise: Promise | undefined; + scene.ui.showText(i18next.t("battle:playerGo", { pokemonName: getPokemonNameWithAffix(playerPokemon) })); + scene.pbTray.hide(); + scene.trainer.setTexture(`trainer_${scene.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}_back_pb`); + scene.time.delayedCall(562, () => { + scene.trainer.setFrame("2"); + scene.time.delayedCall(64, () => { + scene.trainer.setFrame("3"); + }); + }); + scene.tweens.add({ + targets: scene.trainer, + x: -36, + duration: 1000, + onComplete: () => scene.trainer.setVisible(false) + }); + scene.time.delayedCall(750, () => { + playerAnimationPromise = summonPlayerPokemonAnimation(scene, playerPokemon); + }); + + // Also loads Wobbuffet data + const enemySpecies = getPokemonSpecies(Species.WOBBUFFET); + scene.currentBattle.enemyParty = []; + const wobbuffet = scene.addEnemyPokemon(enemySpecies, encounter.misc.playerPokemon.level, TrainerSlot.NONE, false); + wobbuffet.ivs = [0, 0, 0, 0, 0, 0]; + wobbuffet.setNature(Nature.MILD); + wobbuffet.setAlpha(0); + wobbuffet.setVisible(false); + wobbuffet.calculateStats(); + scene.currentBattle.enemyParty[0] = wobbuffet; + scene.gameData.setPokemonSeen(wobbuffet, true); + await wobbuffet.loadAssets(); + const id = setInterval(checkPlayerAnimationPromise, 500); + async function checkPlayerAnimationPromise() { + if (playerAnimationPromise) { + clearInterval(id); + await playerAnimationPromise; + resolve(); + } + } + }); +} + +function handleLoseMinigame(scene: BattleScene) { + return new Promise(async resolve => { + // Check Wobbuffet is still alive + const wobbuffet = scene.getEnemyPokemon(); + if (!wobbuffet || wobbuffet.isFainted(true) || wobbuffet.hp === 0) { + // Player loses + // End the battle + if (wobbuffet) { + wobbuffet.hideInfo(); + scene.field.remove(wobbuffet); + } + transitionMysteryEncounterIntroVisuals(scene, true, true); + scene.currentBattle.enemyParty = []; + scene.currentBattle.mysteryEncounter!.doContinueEncounter = undefined; + leaveEncounterWithoutBattle(scene, true); + await showEncounterText(scene, `${namespace}.ko`); + const reviveCost = scene.getWaveMoneyAmount(1.5); + updatePlayerMoney(scene, -reviveCost, true, false); + } + + resolve(); + }); +} + +function handleNextTurn(scene: BattleScene) { + const encounter = scene.currentBattle.mysteryEncounter!; + + const wobbuffet = scene.getEnemyPokemon(); + if (!wobbuffet) { + // Should never be triggered, just handling the edge case + handleLoseMinigame(scene); + return true; + } + if (encounter.misc.turnsRemaining <= 0) { + // Check Wobbuffet's health for the actual result + const healthRatio = wobbuffet.hp / wobbuffet.getMaxHp(); + let resultMessageKey: string; + let isHealPhase = false; + if (healthRatio < 0.03) { + // Grand prize + setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.MULTI_LENS], fillRemaining: false }); + resultMessageKey = `${namespace}.best_result`; + } else if (healthRatio < 0.15) { + // 2nd prize + setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.SCOPE_LENS], fillRemaining: false }); + resultMessageKey = `${namespace}.great_result`; + } else if (healthRatio < 0.33) { + // 3rd prize + setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.WIDE_LENS], fillRemaining: false }); + resultMessageKey = `${namespace}.good_result`; + } else { + // No prize + isHealPhase = true; + resultMessageKey = `${namespace}.bad_result`; + } + + // End the battle + wobbuffet.hideInfo(); + scene.field.remove(wobbuffet); + scene.currentBattle.enemyParty = []; + scene.currentBattle.mysteryEncounter!.doContinueEncounter = undefined; + leaveEncounterWithoutBattle(scene, isHealPhase); + // Must end the TurnInit phase prematurely so battle phases aren't added to queue + queueEncounterMessage(scene, `${namespace}.end_game`); + queueEncounterMessage(scene, resultMessageKey); + + // Skip remainder of TurnInitPhase + return true; + } else { + if (encounter.misc.turnsRemaining < 3) { + // Display charging messages on turns that aren't the initial turn + queueEncounterMessage(scene, `${namespace}.charging_continue`); + } + queueEncounterMessage(scene, `${namespace}.turn_remaining_${encounter.misc.turnsRemaining}`); + encounter.misc.turnsRemaining--; + } + + // Don't skip remainder of TurnInitPhase + return false; +} + +async function showWobbuffetHealthBar(scene: BattleScene) { + const wobbuffet = scene.getEnemyPokemon()!; + + scene.add.existing(wobbuffet); + scene.field.add(wobbuffet); + + const playerPokemon = scene.getPlayerPokemon() as Pokemon; + if (playerPokemon?.visible) { + scene.field.moveBelow(wobbuffet, playerPokemon); + } + // Show health bar and trigger cry + wobbuffet.showInfo(); + scene.time.delayedCall(1000, () => { + wobbuffet.cry(); + }); + wobbuffet.resetSummonData(); + + // Track the HP change across turns + scene.currentBattle.mysteryEncounter!.misc.wobbuffetHealth = wobbuffet.hp; +} + +function summonPlayerPokemonAnimation(scene: BattleScene, pokemon: PlayerPokemon): Promise { + return new Promise(resolve => { + const pokeball = scene.addFieldSprite(36, 80, "pb", getPokeballAtlasKey(pokemon.pokeball)); + pokeball.setVisible(false); + pokeball.setOrigin(0.5, 0.625); + scene.field.add(pokeball); + + pokemon.setFieldPosition(FieldPosition.CENTER, 0); + + const fpOffset = pokemon.getFieldPositionOffset(); + + pokeball.setVisible(true); + + scene.tweens.add({ + targets: pokeball, + duration: 650, + x: 100 + fpOffset[0] + }); + + scene.tweens.add({ + targets: pokeball, + duration: 150, + ease: "Cubic.easeOut", + y: 70 + fpOffset[1], + onComplete: () => { + scene.tweens.add({ + targets: pokeball, + duration: 500, + ease: "Cubic.easeIn", + angle: 1440, + y: 132 + fpOffset[1], + onComplete: () => { + scene.playSound("se/pb_rel"); + pokeball.destroy(); + scene.add.existing(pokemon); + scene.field.add(pokemon); + addPokeballOpenParticles(scene, pokemon.x, pokemon.y - 16, pokemon.pokeball); + scene.updateModifiers(true); + scene.updateFieldScale(); + pokemon.showInfo(); + pokemon.playAnim(); + pokemon.setVisible(true); + pokemon.getSprite().setVisible(true); + pokemon.setScale(0.5); + pokemon.tint(getPokeballTintColor(pokemon.pokeball)); + pokemon.untint(250, "Sine.easeIn"); + scene.updateFieldScale(); + scene.tweens.add({ + targets: pokemon, + duration: 250, + ease: "Sine.easeIn", + scale: pokemon.getSpriteScale(), + onComplete: () => { + pokemon.cry(pokemon.getHpRatio() > 0.25 ? undefined : { rate: 0.85 }); + pokemon.getSprite().clearTint(); + pokemon.resetSummonData(); + scene.time.delayedCall(1000, () => { + if (pokemon.isShiny()) { + scene.unshiftPhase(new ShinySparklePhase(scene, pokemon.getBattlerIndex())); + } + + pokemon.resetTurnData(); + + scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true); + scene.pushPhase(new PostSummonPhase(scene, pokemon.getBattlerIndex())); + resolve(); + }); + } + }); + } + }); + } + }); + }); +} + +function hideShowmanIntroSprite(scene: BattleScene) { + const carnivalGame = scene.currentBattle.mysteryEncounter!.introVisuals?.getSpriteAtIndex(0)[0]; + const wobbuffet = scene.currentBattle.mysteryEncounter!.introVisuals?.getSpriteAtIndex(1)[0]; + const showMan = scene.currentBattle.mysteryEncounter!.introVisuals?.getSpriteAtIndex(2)[0]; + + // Hide the showman + scene.tweens.add({ + targets: showMan, + x: "+=16", + y: "-=16", + alpha: 0, + ease: "Sine.easeInOut", + duration: 750 + }); + + // Slide the Wobbuffet and Game over slightly + scene.tweens.add({ + targets: [wobbuffet, carnivalGame], + x: "+=16", + ease: "Sine.easeInOut", + duration: 750 + }); +} diff --git a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts new file mode 100644 index 00000000000..0f9f06c9a68 --- /dev/null +++ b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts @@ -0,0 +1,827 @@ +import { leaveEncounterWithoutBattle, selectPokemonForOption, setEncounterRewards } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { TrainerSlot, } from "#app/data/trainer-config"; +import { ModifierTier } from "#app/modifier/modifier-tier"; +import { getPlayerModifierTypeOptions, ModifierPoolType, ModifierTypeOption, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import BattleScene from "#app/battle-scene"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { Species } from "#enums/species"; +import PokemonSpecies, { allSpecies, getPokemonSpecies } from "#app/data/pokemon-species"; +import { getTypeRgb } from "#app/data/type"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { IntegerHolder, isNullOrUndefined, randInt, randSeedInt, randSeedShuffle } from "#app/utils"; +import Pokemon, { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon"; +import { HiddenAbilityRateBoosterModifier, PokemonFormChangeItemModifier, PokemonHeldItemModifier, SpeciesStatBoosterModifier } from "#app/modifier/modifier"; +import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; +import PokemonData from "#app/system/pokemon-data"; +import i18next from "i18next"; +import { Gender, getGenderSymbol } from "#app/data/gender"; +import { getNatureName } from "#app/data/nature"; +import { getPokeballAtlasKey, getPokeballTintColor, PokeballType } from "#app/data/pokeball"; +import { getEncounterText, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { trainerNamePools } from "#app/data/trainer-names"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; +import { addPokemonDataToDexAndValidateAchievements } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; + +/** the i18n namespace for the encounter */ +const namespace = "mysteryEncounter:globalTradeSystem"; + +const LEGENDARY_TRADE_POOLS = { + 1: [Species.RATTATA, Species.PIDGEY, Species.WEEDLE], + 2: [Species.SENTRET, Species.HOOTHOOT, Species.LEDYBA], + 3: [Species.POOCHYENA, Species.ZIGZAGOON, Species.TAILLOW], + 4: [Species.BIDOOF, Species.STARLY, Species.KRICKETOT], + 5: [Species.PATRAT, Species.PURRLOIN, Species.PIDOVE], + 6: [Species.BUNNELBY, Species.LITLEO, Species.SCATTERBUG], + 7: [Species.PIKIPEK, Species.YUNGOOS, Species.ROCKRUFF], + 8: [Species.SKWOVET, Species.WOOLOO, Species.ROOKIDEE], + 9: [Species.LECHONK, Species.FIDOUGH, Species.TAROUNTULA] +}; + +/** Exclude Paradox mons as they aren't considered legendary/mythical */ +const EXCLUDED_TRADE_SPECIES = [ + Species.GREAT_TUSK, + Species.SCREAM_TAIL, + Species.BRUTE_BONNET, + Species.FLUTTER_MANE, + Species.SLITHER_WING, + Species.SANDY_SHOCKS, + Species.ROARING_MOON, + Species.WALKING_WAKE, + Species.GOUGING_FIRE, + Species.RAGING_BOLT, + Species.IRON_TREADS, + Species.IRON_BUNDLE, + Species.IRON_HANDS, + Species.IRON_JUGULIS, + Species.IRON_MOTH, + Species.IRON_THORNS, + Species.IRON_VALIANT, + Species.IRON_LEAVES, + Species.IRON_BOULDER, + Species.IRON_CROWN +]; + +/** + * Global Trade System encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3812 | GitHub Issue #3812} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const GlobalTradeSystemEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.GLOBAL_TRADE_SYSTEM) + .withEncounterTier(MysteryEncounterTier.COMMON) + .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + .withAutoHideIntroVisuals(false) + .withIntroSpriteConfigs([ + { + spriteKey: "global_trade_system", + fileRoot: "mystery-encounters", + hasShadow: true, + disableAnimation: true, + x: 3, + y: 5, + yShadow: 1 + } + ]) + .withIntroDialogue([ + { + text: `${namespace}.intro`, + } + ]) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withOnInit((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + + // Load bgm + let bgmKey: string; + if (scene.musicPreference === 0) { + bgmKey = "mystery_encounter_gen_5_gts"; + scene.loadBgm(bgmKey, `${bgmKey}.mp3`); + } else { + // Mixed option + bgmKey = "mystery_encounter_gen_6_gts"; + scene.loadBgm(bgmKey, `${bgmKey}.mp3`); + } + + // Load possible trade options + // Maps current party member's id to 3 EnemyPokemon objects + // None of the trade options can be the same species + const tradeOptionsMap: Map = getPokemonTradeOptions(scene); + encounter.misc = { + tradeOptionsMap, + bgmKey + }; + + return true; + }) + .withOnVisualsStart((scene: BattleScene) => { + scene.fadeAndSwitchBgm(scene.currentBattle.mysteryEncounter!.misc.bgmKey); + return true; + }) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withHasDexProgress(true) + .withDialogue({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + secondOptionPrompt: `${namespace}.option.1.trade_options_prompt`, + }) + .withPreOptionPhase(async (scene: BattleScene): Promise => { + const encounter = scene.currentBattle.mysteryEncounter!; + const onPokemonSelected = (pokemon: PlayerPokemon) => { + // Get the trade species options for the selected pokemon + const tradeOptionsMap: Map = encounter.misc.tradeOptionsMap; + const tradeOptions = tradeOptionsMap.get(pokemon.id); + if (!tradeOptions) { + return []; + } + + return tradeOptions.map((tradePokemon: EnemyPokemon) => { + const option: OptionSelectItem = { + label: tradePokemon.getNameToRender(), + handler: () => { + // Pokemon trade selected + encounter.setDialogueToken("tradedPokemon", pokemon.getNameToRender()); + encounter.setDialogueToken("received", tradePokemon.getNameToRender()); + encounter.misc.tradedPokemon = pokemon; + encounter.misc.receivedPokemon = tradePokemon; + return true; + }, + onHover: () => { + const formName = tradePokemon.species.forms?.[pokemon.formIndex]?.formName; + const line1 = i18next.t("pokemonInfoContainer:ability") + " " + tradePokemon.getAbility().name + (tradePokemon.getGender() !== Gender.GENDERLESS ? " | " + i18next.t("pokemonInfoContainer:gender") + " " + getGenderSymbol(tradePokemon.getGender()) : ""); + const line2 = i18next.t("pokemonInfoContainer:nature") + " " + getNatureName(tradePokemon.getNature()) + (formName ? " | " + i18next.t("pokemonInfoContainer:form") + " " + formName : ""); + showEncounterText(scene, `${line1}\n${line2}`, 0, 0, false); + }, + }; + return option; + }); + }; + + return selectPokemonForOption(scene, onPokemonSelected); + }) + .withOptionPhase(async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + const tradedPokemon: PlayerPokemon = encounter.misc.tradedPokemon; + const receivedPokemonData: EnemyPokemon = encounter.misc.receivedPokemon; + const modifiers = tradedPokemon.getHeldItems().filter(m => !(m instanceof PokemonFormChangeItemModifier) && !(m instanceof SpeciesStatBoosterModifier)); + + // Generate a trainer name + const traderName = generateRandomTraderName(); + encounter.setDialogueToken("tradeTrainerName", traderName.trim()); + + // Remove the original party member from party + scene.removePokemonFromPlayerParty(tradedPokemon, false); + + // Set data properly, then generate the new Pokemon's assets + receivedPokemonData.passive = tradedPokemon.passive; + // Pokeball to Ultra ball, randomly + receivedPokemonData.pokeball = randInt(4) as PokeballType; + const dataSource = new PokemonData(receivedPokemonData); + const newPlayerPokemon = scene.addPlayerPokemon(receivedPokemonData.species, receivedPokemonData.level, dataSource.abilityIndex, dataSource.formIndex, dataSource.gender, dataSource.shiny, dataSource.variant, dataSource.ivs, dataSource.nature, dataSource); + scene.getParty().push(newPlayerPokemon); + await newPlayerPokemon.loadAssets(); + + for (const mod of modifiers) { + mod.pokemonId = newPlayerPokemon.id; + scene.addModifier(mod, true, false, false, true); + } + + // Show the trade animation + await showTradeBackground(scene); + await doPokemonTradeSequence(scene, tradedPokemon, newPlayerPokemon); + await showEncounterText(scene, `${namespace}.trade_received`, null, 0, true, 4000); + scene.playBgm(encounter.misc.bgmKey); + await addPokemonDataToDexAndValidateAchievements(scene, newPlayerPokemon); + await hideTradeBackground(scene); + tradedPokemon.destroy(); + + leaveEncounterWithoutBattle(scene, true); + }) + .build() + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withHasDexProgress(true) + .withDialogue({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + }) + .withPreOptionPhase(async (scene: BattleScene): Promise => { + const encounter = scene.currentBattle.mysteryEncounter!; + const onPokemonSelected = (pokemon: PlayerPokemon) => { + // Randomly generate a Wonder Trade pokemon + const randomTradeOption = generateTradeOption(scene.getParty().map(p => p.species)); + const tradePokemon = new EnemyPokemon(scene, randomTradeOption, pokemon.level, TrainerSlot.NONE, false); + // Extra shiny roll at 1/128 odds (boosted by events and charms) + if (!tradePokemon.shiny) { + // 512/65536 -> 1/128 + tradePokemon.trySetShinySeed(512, true); + } + + // Extra HA roll at base 1/64 odds (boosted by events and charms) + if (pokemon.species.abilityHidden) { + const hiddenIndex = pokemon.species.ability2 ? 2 : 1; + if (pokemon.abilityIndex < hiddenIndex) { + const hiddenAbilityChance = new IntegerHolder(64); + scene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance); + + const hasHiddenAbility = !randSeedInt(hiddenAbilityChance.value); + + if (hasHiddenAbility) { + pokemon.abilityIndex = hiddenIndex; + } + } + } + + encounter.setDialogueToken("tradedPokemon", pokemon.getNameToRender()); + encounter.setDialogueToken("received", tradePokemon.getNameToRender()); + encounter.misc.tradedPokemon = pokemon; + encounter.misc.receivedPokemon = tradePokemon; + }; + + return selectPokemonForOption(scene, onPokemonSelected); + }) + .withOptionPhase(async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + const tradedPokemon: PlayerPokemon = encounter.misc.tradedPokemon; + const receivedPokemonData: EnemyPokemon = encounter.misc.receivedPokemon; + const modifiers = tradedPokemon.getHeldItems().filter(m => !(m instanceof PokemonFormChangeItemModifier) && !(m instanceof SpeciesStatBoosterModifier)); + + // Generate a trainer name + const traderName = generateRandomTraderName(); + encounter.setDialogueToken("tradeTrainerName", traderName.trim()); + + // Remove the original party member from party + scene.removePokemonFromPlayerParty(tradedPokemon, false); + + // Set data properly, then generate the new Pokemon's assets + receivedPokemonData.passive = tradedPokemon.passive; + receivedPokemonData.pokeball = randInt(4) as PokeballType; + const dataSource = new PokemonData(receivedPokemonData); + const newPlayerPokemon = scene.addPlayerPokemon(receivedPokemonData.species, receivedPokemonData.level, dataSource.abilityIndex, dataSource.formIndex, dataSource.gender, dataSource.shiny, dataSource.variant, dataSource.ivs, dataSource.nature, dataSource); + scene.getParty().push(newPlayerPokemon); + await newPlayerPokemon.loadAssets(); + + for (const mod of modifiers) { + mod.pokemonId = newPlayerPokemon.id; + scene.addModifier(mod, true, false, false, true); + } + + // Show the trade animation + await showTradeBackground(scene); + await doPokemonTradeSequence(scene, tradedPokemon, newPlayerPokemon); + await showEncounterText(scene, `${namespace}.trade_received`, null, 0, true, 4000); + scene.playBgm(encounter.misc.bgmKey); + await addPokemonDataToDexAndValidateAchievements(scene, newPlayerPokemon); + await hideTradeBackground(scene); + tradedPokemon.destroy(); + + leaveEncounterWithoutBattle(scene, true); + }) + .build() + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + secondOptionPrompt: `${namespace}.option.3.trade_options_prompt`, + }) + .withPreOptionPhase(async (scene: BattleScene): Promise => { + const encounter = scene.currentBattle.mysteryEncounter!; + const onPokemonSelected = (pokemon: PlayerPokemon) => { + // Get Pokemon held items and filter for valid ones + const validItems = pokemon.getHeldItems().filter((it) => { + return it.isTransferable; + }); + + return validItems.map((modifier: PokemonHeldItemModifier) => { + const option: OptionSelectItem = { + label: modifier.type.name, + handler: () => { + // Pokemon and item selected + encounter.setDialogueToken("chosenItem", modifier.type.name); + encounter.misc.chosenModifier = modifier; + return true; + }, + }; + return option; + }); + }; + + const selectableFilter = (pokemon: Pokemon) => { + // If pokemon has items to trade + const meetsReqs = pokemon.getHeldItems().filter((it) => { + return it.isTransferable; + }).length > 0; + if (!meetsReqs) { + return getEncounterText(scene, `${namespace}.option.3.invalid_selection`) ?? null; + } + + return null; + }; + + return selectPokemonForOption(scene, onPokemonSelected, undefined, selectableFilter); + }) + .withOptionPhase(async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + const modifier = encounter.misc.chosenModifier; + + // Check tier of the traded item, the received item will be one tier up + const type = modifier.type.withTierFromPool(); + let tier = type.tier ?? ModifierTier.GREAT; + // Eggs and White Herb are not in the pool + if (type.id === "WHITE_HERB") { + tier = ModifierTier.GREAT; + } else if (type.id === "LUCKY_EGG") { + tier = ModifierTier.ULTRA; + } else if (type.id === "GOLDEN_EGG") { + tier = ModifierTier.ROGUE; + } + // Increment tier by 1 + if (tier < ModifierTier.MASTER) { + tier++; + } + + regenerateModifierPoolThresholds(scene.getParty(), ModifierPoolType.PLAYER, 0); + let item: ModifierTypeOption | null = null; + // TMs excluded from possible rewards + while (!item || item.type.id.includes("TM_")) { + item = getPlayerModifierTypeOptions(1, scene.getParty(), [], { guaranteedModifierTiers: [tier], allowLuckUpgrades: false })[0]; + } + + encounter.setDialogueToken("itemName", item.type.name); + setEncounterRewards(scene, { guaranteedModifierTypeOptions: [item], fillRemaining: false }); + + // Remove the chosen modifier if its stacks go to 0 + modifier.stackCount -= 1; + if (modifier.stackCount === 0) { + scene.removeModifier(modifier); + } + scene.updateModifiers(true, true); + + // Generate a trainer name + const traderName = generateRandomTraderName(); + encounter.setDialogueToken("tradeTrainerName", traderName.trim()); + await showEncounterText(scene, `${namespace}.item_trade_selected`); + leaveEncounterWithoutBattle(scene); + }) + .build() + ) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.4.label`, + buttonTooltip: `${namespace}.option.4.tooltip`, + selected: [ + { + text: `${namespace}.option.4.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Leave encounter with no rewards or exp + leaveEncounterWithoutBattle(scene, true); + return true; + } + ) + .build(); + +function getPokemonTradeOptions(scene: BattleScene): Map { + const tradeOptionsMap: Map = new Map(); + // Starts by filtering out any current party members as valid resulting species + const alreadyUsedSpecies: PokemonSpecies[] = scene.getParty().map(p => p.species); + + scene.getParty().forEach(pokemon => { + // If the party member is legendary/mythical, the only trade options available are always pulled from generation-specific legendary trade pools + if (pokemon.species.legendary || pokemon.species.subLegendary || pokemon.species.mythical) { + const generation = pokemon.species.generation; + const tradeOptions: EnemyPokemon[] = LEGENDARY_TRADE_POOLS[generation].map(s => { + const pokemonSpecies = getPokemonSpecies(s); + return new EnemyPokemon(scene, pokemonSpecies, 5, TrainerSlot.NONE, false); + }); + tradeOptionsMap.set(pokemon.id, tradeOptions); + } else { + const originalBst = pokemon.calculateBaseStats().reduce((a, b) => a + b, 0); + + const tradeOptions: PokemonSpecies[] = []; + for (let i = 0; i < 3; i++) { + const speciesTradeOption = generateTradeOption(alreadyUsedSpecies, originalBst); + alreadyUsedSpecies.push(speciesTradeOption); + tradeOptions.push(speciesTradeOption); + } + + // Add trade options to map + tradeOptionsMap.set(pokemon.id, tradeOptions.map(s => { + return new EnemyPokemon(scene, s, pokemon.level, TrainerSlot.NONE, false); + })); + } + }); + + return tradeOptionsMap; +} + +function generateTradeOption(alreadyUsedSpecies: PokemonSpecies[], originalBst?: number): PokemonSpecies { + let newSpecies: PokemonSpecies | undefined; + let bstCap = 9999; + let bstMin = 0; + if (originalBst) { + bstCap = originalBst + 100; + bstMin = originalBst - 100; + } + while (isNullOrUndefined(newSpecies)) { + // Get all non-legendary species that fall within the Bst range requirements + let validSpecies = allSpecies + .filter(s => { + const isLegendaryOrMythical = s.legendary || s.subLegendary || s.mythical; + const speciesBst = s.getBaseStatTotal(); + const bstInRange = speciesBst >= bstMin && speciesBst <= bstCap; + return !isLegendaryOrMythical && bstInRange && !EXCLUDED_TRADE_SPECIES.includes(s.speciesId); + }); + + // There must be at least 20 species available before it will choose one + if (validSpecies?.length > 20) { + validSpecies = randSeedShuffle(validSpecies); + newSpecies = validSpecies.pop(); + while (isNullOrUndefined(newSpecies) || alreadyUsedSpecies.includes(newSpecies!)) { + newSpecies = validSpecies.pop(); + } + } else { + // Expands search range until at least 20 are in the pool + bstMin -= 10; + bstCap += 10; + } + } + + return newSpecies!; +} + +function showTradeBackground(scene: BattleScene) { + return new Promise(resolve => { + const tradeContainer = scene.add.container(0, -scene.game.canvas.height / 6); + tradeContainer.setName("Trade Background"); + + const flyByStaticBg = scene.add.rectangle(0, 0, scene.game.canvas.width / 6, scene.game.canvas.height / 6, 0); + flyByStaticBg.setName("Black Background"); + flyByStaticBg.setOrigin(0, 0); + flyByStaticBg.setVisible(false); + tradeContainer.add(flyByStaticBg); + + const tradeBaseBg = scene.add.image(0, 0, "default_bg"); + tradeBaseBg.setName("Trade Background Image"); + tradeBaseBg.setOrigin(0, 0); + tradeContainer.add(tradeBaseBg); + + scene.fieldUI.add(tradeContainer); + scene.fieldUI.bringToTop(tradeContainer); + tradeContainer.setVisible(true); + tradeContainer.alpha = 0; + + scene.tweens.add({ + targets: tradeContainer, + alpha: 1, + duration: 500, + ease: "Sine.easeInOut", + onComplete: () => { + resolve(); + } + }); + }); +} + +function hideTradeBackground(scene: BattleScene) { + return new Promise(resolve => { + const transformationContainer = scene.fieldUI.getByName("Trade Background"); + + scene.tweens.add({ + targets: transformationContainer, + alpha: 0, + duration: 1000, + ease: "Sine.easeInOut", + onComplete: () => { + scene.fieldUI.remove(transformationContainer, true); + resolve(); + } + }); + }); +} + +/** + * Initiates an "evolution-like" animation to transform a previousPokemon (presumably from the player's party) into a new one, not necessarily an evolution species. + * @param scene + * @param tradedPokemon + * @param receivedPokemon + */ +function doPokemonTradeSequence(scene: BattleScene, tradedPokemon: PlayerPokemon, receivedPokemon: PlayerPokemon) { + return new Promise(resolve => { + const tradeContainer = scene.fieldUI.getByName("Trade Background") as Phaser.GameObjects.Container; + const tradeBaseBg = tradeContainer.getByName("Trade Background Image") as Phaser.GameObjects.Image; + + let tradedPokemonSprite: Phaser.GameObjects.Sprite; + let tradedPokemonTintSprite: Phaser.GameObjects.Sprite; + let receivedPokemonSprite: Phaser.GameObjects.Sprite; + let receivedPokemonTintSprite: Phaser.GameObjects.Sprite; + + const getPokemonSprite = () => { + const ret = scene.addPokemonSprite(tradedPokemon, tradeBaseBg.displayWidth / 2, tradeBaseBg.displayHeight / 2, "pkmn__sub"); + ret.setPipeline(scene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], ignoreTimeTint: true }); + return ret; + }; + + tradeContainer.add((tradedPokemonSprite = getPokemonSprite())); + tradeContainer.add((tradedPokemonTintSprite = getPokemonSprite())); + tradeContainer.add((receivedPokemonSprite = getPokemonSprite())); + tradeContainer.add((receivedPokemonTintSprite = getPokemonSprite())); + + tradedPokemonSprite.setAlpha(0); + tradedPokemonTintSprite.setAlpha(0); + tradedPokemonTintSprite.setTintFill(getPokeballTintColor(tradedPokemon.pokeball)); + receivedPokemonSprite.setVisible(false); + receivedPokemonTintSprite.setVisible(false); + receivedPokemonTintSprite.setTintFill(getPokeballTintColor(receivedPokemon.pokeball)); + + [ tradedPokemonSprite, tradedPokemonTintSprite ].map(sprite => { + sprite.play(tradedPokemon.getSpriteKey(true)); + sprite.setPipeline(scene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], hasShadow: false, teraColor: getTypeRgb(tradedPokemon.getTeraType()) }); + sprite.setPipelineData("ignoreTimeTint", true); + sprite.setPipelineData("spriteKey", tradedPokemon.getSpriteKey()); + sprite.setPipelineData("shiny", tradedPokemon.shiny); + sprite.setPipelineData("variant", tradedPokemon.variant); + [ "spriteColors", "fusionSpriteColors" ].map(k => { + if (tradedPokemon.summonData?.speciesForm) { + k += "Base"; + } + sprite.pipelineData[k] = tradedPokemon.getSprite().pipelineData[k]; + }); + }); + + [ receivedPokemonSprite, receivedPokemonTintSprite ].map(sprite => { + sprite.play(receivedPokemon.getSpriteKey(true)); + sprite.setPipeline(scene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], hasShadow: false, teraColor: getTypeRgb(tradedPokemon.getTeraType()) }); + sprite.setPipelineData("ignoreTimeTint", true); + sprite.setPipelineData("spriteKey", receivedPokemon.getSpriteKey()); + sprite.setPipelineData("shiny", receivedPokemon.shiny); + sprite.setPipelineData("variant", receivedPokemon.variant); + [ "spriteColors", "fusionSpriteColors" ].map(k => { + if (receivedPokemon.summonData?.speciesForm) { + k += "Base"; + } + sprite.pipelineData[k] = receivedPokemon.getSprite().pipelineData[k]; + }); + }); + + // Traded pokemon pokeball + const tradedPbAtlasKey = getPokeballAtlasKey(tradedPokemon.pokeball); + const tradedPokeball: Phaser.GameObjects.Sprite = scene.add.sprite(tradeBaseBg.displayWidth / 2, tradeBaseBg.displayHeight / 2, "pb", tradedPbAtlasKey); + tradedPokeball.setVisible(false); + tradeContainer.add(tradedPokeball); + + // Received pokemon pokeball + const receivedPbAtlasKey = getPokeballAtlasKey(receivedPokemon.pokeball); + const receivedPokeball: Phaser.GameObjects.Sprite = scene.add.sprite(tradeBaseBg.displayWidth / 2, tradeBaseBg.displayHeight / 2, "pb", receivedPbAtlasKey); + receivedPokeball.setVisible(false); + tradeContainer.add(receivedPokeball); + + scene.tweens.add({ + targets: tradedPokemonSprite, + alpha: 1, + ease: "Cubic.easeInOut", + duration: 500, + onComplete: async () => { + scene.fadeOutBgm(1000, false); + await showEncounterText(scene, `${namespace}.pokemon_trade_selected`); + tradedPokemon.cry(); + scene.playBgm("evolution"); + await showEncounterText(scene, `${namespace}.pokemon_trade_goodbye`); + + tradedPokeball.setAlpha(0); + tradedPokeball.setVisible(true); + scene.tweens.add({ + targets: tradedPokeball, + alpha: 1, + ease: "Cubic.easeInOut", + duration: 250, + onComplete: () => { + tradedPokeball.setTexture("pb", `${tradedPbAtlasKey}_opening`); + scene.time.delayedCall(17, () => tradedPokeball.setTexture("pb", `${tradedPbAtlasKey}_open`)); + scene.playSound("se/pb_rel"); + tradedPokemonTintSprite.setVisible(true); + + // TODO: need to add particles to fieldUI instead of field + // addPokeballOpenParticles(scene, tradedPokemon.x, tradedPokemon.y, tradedPokemon.pokeball); + + scene.tweens.add({ + targets: [tradedPokemonTintSprite, tradedPokemonSprite], + duration: 500, + ease: "Sine.easeIn", + scale: 0.25, + onComplete: () => { + tradedPokemonSprite.setVisible(false); + tradedPokeball.setTexture("pb", `${tradedPbAtlasKey}_opening`); + tradedPokemonTintSprite.setVisible(false); + scene.playSound("se/pb_catch"); + scene.time.delayedCall(17, () => tradedPokeball.setTexture("pb", `${tradedPbAtlasKey}`)); + + scene.tweens.add({ + targets: tradedPokeball, + y: "+=10", + duration: 200, + delay: 250, + ease: "Cubic.easeIn", + onComplete: () => { + scene.playSound("se/pb_bounce_1"); + + scene.tweens.add({ + targets: tradedPokeball, + y: "-=100", + duration: 200, + delay: 1000, + ease: "Cubic.easeInOut", + onStart: () => { + scene.playSound("se/pb_throw"); + }, + onComplete: async () => { + await doPokemonTradeFlyBySequence(scene, tradedPokemonSprite, receivedPokemonSprite); + await doTradeReceivedSequence(scene, receivedPokemon, receivedPokemonSprite, receivedPokemonTintSprite, receivedPokeball, receivedPbAtlasKey); + resolve(); + } + }); + } + }); + } + }); + } + }); + } + }); + }); +} + +function doPokemonTradeFlyBySequence(scene: BattleScene, tradedPokemonSprite: Phaser.GameObjects.Sprite, receivedPokemonSprite: Phaser.GameObjects.Sprite) { + return new Promise(resolve => { + const tradeContainer = scene.fieldUI.getByName("Trade Background") as Phaser.GameObjects.Container; + const tradeBaseBg = tradeContainer.getByName("Trade Background Image") as Phaser.GameObjects.Image; + const flyByStaticBg = tradeContainer.getByName("Black Background") as Phaser.GameObjects.Rectangle; + flyByStaticBg.setVisible(true); + tradeContainer.bringToTop(tradedPokemonSprite); + tradeContainer.bringToTop(receivedPokemonSprite); + + tradedPokemonSprite.x = tradeBaseBg.displayWidth / 4; + tradedPokemonSprite.y = 200; + tradedPokemonSprite.scale = 1; + tradedPokemonSprite.setVisible(true); + receivedPokemonSprite.x = tradeBaseBg.displayWidth * 3 / 4; + receivedPokemonSprite.y = -200; + receivedPokemonSprite.scale = 1; + receivedPokemonSprite.setVisible(true); + + const FADE_DELAY = 300; + const ANIM_DELAY = 750; + const BASE_ANIM_DURATION = 1000; + + // Fade out trade background + scene.tweens.add({ + targets: tradeBaseBg, + alpha: 0, + ease: "Cubic.easeInOut", + duration: FADE_DELAY, + onComplete: () => { + scene.tweens.add({ + targets: [receivedPokemonSprite, tradedPokemonSprite], + y: tradeBaseBg.displayWidth / 2 - 100, + ease: "Cubic.easeInOut", + duration: BASE_ANIM_DURATION * 3, + onComplete: () => { + scene.tweens.add({ + targets: receivedPokemonSprite, + x: tradeBaseBg.displayWidth / 4, + ease: "Cubic.easeInOut", + duration: BASE_ANIM_DURATION / 2, + delay: ANIM_DELAY + }); + scene.tweens.add({ + targets: tradedPokemonSprite, + x: tradeBaseBg.displayWidth * 3 / 4, + ease: "Cubic.easeInOut", + duration: BASE_ANIM_DURATION / 2, + delay: ANIM_DELAY, + onComplete: () => { + scene.tweens.add({ + targets: receivedPokemonSprite, + y: "+=200", + ease: "Cubic.easeInOut", + duration: BASE_ANIM_DURATION * 2, + delay: ANIM_DELAY, + }); + scene.tweens.add({ + targets: tradedPokemonSprite, + y: "-=200", + ease: "Cubic.easeInOut", + duration: BASE_ANIM_DURATION * 2, + delay: ANIM_DELAY, + onComplete: () => { + scene.tweens.add({ + targets: tradeBaseBg, + alpha: 1, + ease: "Cubic.easeInOut", + duration: FADE_DELAY, + onComplete: () => { + resolve(); + } + }); + } + }); + } + }); + } + }); + } + }); + }); +} + +function doTradeReceivedSequence(scene: BattleScene, receivedPokemon: PlayerPokemon, receivedPokemonSprite: Phaser.GameObjects.Sprite, receivedPokemonTintSprite: Phaser.GameObjects.Sprite, receivedPokeballSprite: Phaser.GameObjects.Sprite, receivedPbAtlasKey: string) { + return new Promise(resolve => { + const tradeContainer = scene.fieldUI.getByName("Trade Background") as Phaser.GameObjects.Container; + const tradeBaseBg = tradeContainer.getByName("Trade Background Image") as Phaser.GameObjects.Image; + + receivedPokemonSprite.setVisible(false); + receivedPokemonSprite.x = tradeBaseBg.displayWidth / 2; + receivedPokemonSprite.y = tradeBaseBg.displayHeight / 2; + receivedPokemonTintSprite.setVisible(false); + receivedPokemonTintSprite.x = tradeBaseBg.displayWidth / 2; + receivedPokemonTintSprite.y = tradeBaseBg.displayHeight / 2; + + receivedPokeballSprite.setVisible(true); + receivedPokeballSprite.x = tradeBaseBg.displayWidth / 2; + receivedPokeballSprite.y = tradeBaseBg.displayHeight / 2 - 100; + + const BASE_ANIM_DURATION = 1000; + + // Pokeball falls to the screen + scene.playSound("se/pb_throw"); + scene.tweens.add({ + targets: receivedPokeballSprite, + y: "+=100", + ease: "Cubic.easeInOut", + duration: BASE_ANIM_DURATION, + onComplete: () => { + scene.playSound("se/pb_bounce_1"); + scene.time.delayedCall(100, () => scene.playSound("se/pb_bounce_1")); + + scene.time.delayedCall(2000, () => { + scene.playSound("se/pb_rel"); + scene.fadeOutBgm(500, false); + receivedPokemon.cry(); + receivedPokemonTintSprite.scale = 0.25; + receivedPokemonTintSprite.alpha = 1; + receivedPokemonSprite.setVisible(true); + receivedPokemonSprite.scale = 0.25; + receivedPokemonTintSprite.alpha = 1; + receivedPokemonTintSprite.setVisible(true); + receivedPokeballSprite.setTexture("pb", `${receivedPbAtlasKey}_opening`); + scene.time.delayedCall(17, () => receivedPokeballSprite.setTexture("pb", `${receivedPbAtlasKey}_open`)); + scene.tweens.add({ + targets: receivedPokemonSprite, + duration: 250, + ease: "Sine.easeOut", + scale: 1 + }); + scene.tweens.add({ + targets: receivedPokemonTintSprite, + duration: 250, + ease: "Sine.easeOut", + scale: 1, + alpha: 0, + onComplete: () => { + receivedPokeballSprite.destroy(); + scene.time.delayedCall(2000, () => resolve()); + } + }); + }); + } + }); + }); +} + +function generateRandomTraderName() { + const length = Object.keys(trainerNamePools).length; + // +1 avoids TrainerType.UNKNOWN + let trainerTypePool = trainerNamePools[randInt(length) + 1]; + while (!trainerTypePool) { + trainerTypePool = trainerNamePools[randInt(length) + 1]; + } + // Some trainers have 2 gendered pools, some do not + const genderedPool = trainerTypePool[randInt(trainerTypePool.length)]; + const trainerNameString = genderedPool instanceof Array ? genderedPool[randInt(genderedPool.length)] : genderedPool; + // Some names have an '&' symbol and need to be trimmed to a single name instead of a double name + const trainerNames = trainerNameString.split(" & "); + return trainerNames[randInt(trainerNames.length)]; +} diff --git a/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts b/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts new file mode 100644 index 00000000000..02426c2cab6 --- /dev/null +++ b/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts @@ -0,0 +1,143 @@ +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { Moves } from "#app/enums/moves"; +import { Species } from "#app/enums/species"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import BattleScene from "#app/battle-scene"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { leaveEncounterWithoutBattle, setEncounterExp } from "../utils/encounter-phase-utils"; +import { applyDamageToPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; +import {PokemonMove} from "#app/field/pokemon"; + +const OPTION_1_REQUIRED_MOVE = Moves.SURF; +const OPTION_2_REQUIRED_MOVE = Moves.FLY; +/** + * Damage percentage taken when wandering aimlessly. + * Can be a number between `0` - `100`. + * The higher the more damage taken (100% = instant KO). + */ +const DAMAGE_PERCENTAGE: number = 25; +/** The i18n namespace for the encounter */ +const namespace = "mysteryEncounter:lostAtSea"; + +/** + * Lost at sea encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3793 | GitHub Issue #3793} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const LostAtSeaEncounter: MysteryEncounter = MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.LOST_AT_SEA) + .withEncounterTier(MysteryEncounterTier.COMMON) + .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + .withIntroSpriteConfigs([ + { + spriteKey: "lost_at_sea_buoy", + fileRoot: "mystery-encounters", + hasShadow: false, + x: 20, + y: 3, + }, + ]) + .withIntroDialogue([{ text: `${namespace}.intro` }]) + .withOnInit((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + + encounter.setDialogueToken("damagePercentage", String(DAMAGE_PERCENTAGE)); + encounter.setDialogueToken("option1RequiredMove", new PokemonMove(OPTION_1_REQUIRED_MOVE).getName()); + encounter.setDialogueToken("option2RequiredMove", new PokemonMove(OPTION_2_REQUIRED_MOVE).getName()); + + return true; + }) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withOption( + // Option 1: Use a (non fainted) pokemon that can learn Surf to guide you back/ + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) + .withPokemonCanLearnMoveRequirement(OPTION_1_REQUIRED_MOVE) + .withDialogue({ + buttonLabel: `${namespace}.option.1.label`, + disabledButtonLabel: `${namespace}.option.1.label_disabled`, + buttonTooltip: `${namespace}.option.1.tooltip`, + disabledButtonTooltip: `${namespace}.option.1.tooltip_disabled`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }) + .withOptionPhase(async (scene: BattleScene) => handlePokemonGuidingYouPhase(scene)) + .build() + ) + .withOption( + //Option 2: Use a (non fainted) pokemon that can learn fly to guide you back. + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) + .withPokemonCanLearnMoveRequirement(OPTION_2_REQUIRED_MOVE) + .withDialogue({ + buttonLabel: `${namespace}.option.2.label`, + disabledButtonLabel: `${namespace}.option.2.label_disabled`, + buttonTooltip: `${namespace}.option.2.tooltip`, + disabledButtonTooltip: `${namespace}.option.2.tooltip_disabled`, + selected: [ + { + text: `${namespace}.option.2.selected`, + }, + ], + }) + .withOptionPhase(async (scene: BattleScene) => handlePokemonGuidingYouPhase(scene)) + .build() + ) + .withSimpleOption( + // Option 3: Wander aimlessly + { + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + selected: [ + { + text: `${namespace}.option.3.selected`, + }, + ], + }, + async (scene: BattleScene) => { + const allowedPokemon = scene.getParty().filter((p) => p.isAllowedInBattle()); + + for (const pkm of allowedPokemon) { + const percentage = DAMAGE_PERCENTAGE / 100; + const damage = Math.floor(pkm.getMaxHp() * percentage); + applyDamageToPokemon(scene, pkm, damage); + } + + leaveEncounterWithoutBattle(scene); + + return true; + } + ) + .withOutroDialogue([ + { + text: `${namespace}.outro`, + }, + ]) + .build(); + +/** + * Generic handler for using a guiding pokemon to guide you back. + * + * @param scene Battle scene + */ +async function handlePokemonGuidingYouPhase(scene: BattleScene) { + const laprasSpecies = getPokemonSpecies(Species.LAPRAS); + const { mysteryEncounter } = scene.currentBattle; + + if (mysteryEncounter?.selectedOption?.primaryPokemon?.id) { + setEncounterExp(scene, mysteryEncounter.selectedOption.primaryPokemon.id, laprasSpecies.baseExp, true); + } else { + console.warn("Lost at sea: No guide pokemon found but pokemon guides player. huh!?"); + } + + leaveEncounterWithoutBattle(scene); + return true; +} diff --git a/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts b/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts new file mode 100644 index 00000000000..ac257a8975f --- /dev/null +++ b/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts @@ -0,0 +1,214 @@ +import { + EnemyPartyConfig, + initBattleWithEnemyConfig, + setEncounterRewards, +} from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { + trainerConfigs, + TrainerPartyCompoundTemplate, + TrainerPartyTemplate, + trainerPartyTemplates, +} from "#app/data/trainer-config"; +import { ModifierTier } from "#app/modifier/modifier-tier"; +import { modifierTypes } from "#app/modifier/modifier-type"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { PartyMemberStrength } from "#enums/party-member-strength"; +import BattleScene from "#app/battle-scene"; +import * as Utils from "#app/utils"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; + +/** the i18n namespace for the encounter */ +const namespace = "mysteryEncounter:mysteriousChallengers"; + +/** + * Mysterious Challengers encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3801 | GitHub Issue #3801} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const MysteriousChallengersEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.MYSTERIOUS_CHALLENGERS) + .withEncounterTier(MysteryEncounterTier.GREAT) + .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + .withIntroSpriteConfigs([]) // These are set in onInit() + .withIntroDialogue([ + { + text: `${namespace}.intro`, + }, + ]) + .withOnInit((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + // Calculates what trainers are available for battle in the encounter + + // Normal difficulty trainer is randomly pulled from biome + const normalTrainerType = scene.arena.randomTrainerType(scene.currentBattle.waveIndex); + const normalConfig = trainerConfigs[normalTrainerType].clone(); + let female = false; + if (normalConfig.hasGenders) { + female = !!Utils.randSeedInt(2); + } + const normalSpriteKey = normalConfig.getSpriteKey(female, normalConfig.doubleOnly); + encounter.enemyPartyConfigs.push({ + trainerConfig: normalConfig, + female: female, + }); + + // Hard difficulty trainer is another random trainer, but with AVERAGE_BALANCED config + // Number of mons is based off wave: 1-20 is 2, 20-40 is 3, etc. capping at 6 after wave 100 + const hardTrainerType = scene.arena.randomTrainerType(scene.currentBattle.waveIndex); + const hardTemplate = new TrainerPartyCompoundTemplate( + new TrainerPartyTemplate(1, PartyMemberStrength.STRONGER, false, true), + new TrainerPartyTemplate( + Math.min(Math.ceil(scene.currentBattle.waveIndex / 20), 5), + PartyMemberStrength.AVERAGE, + false, + true + ) + ); + const hardConfig = trainerConfigs[hardTrainerType].clone(); + hardConfig.setPartyTemplates(hardTemplate); + female = false; + if (hardConfig.hasGenders) { + female = !!Utils.randSeedInt(2); + } + const hardSpriteKey = hardConfig.getSpriteKey(female, hardConfig.doubleOnly); + encounter.enemyPartyConfigs.push({ + trainerConfig: hardConfig, + levelAdditiveModifier: 1, + female: female, + }); + + // Brutal trainer is pulled from pool of boss trainers (gym leaders) for the biome + // They are given an E4 template team, so will be stronger than usual boss encounter and always have 6 mons + const brutalTrainerType = scene.arena.randomTrainerType( + scene.currentBattle.waveIndex, + true + ); + const e4Template = trainerPartyTemplates.ELITE_FOUR; + const brutalConfig = trainerConfigs[brutalTrainerType].clone(); + brutalConfig.title = trainerConfigs[brutalTrainerType].title; + brutalConfig.setPartyTemplates(e4Template); + // @ts-ignore + brutalConfig.partyTemplateFunc = null; // Overrides gym leader party template func + female = false; + if (brutalConfig.hasGenders) { + female = !!Utils.randSeedInt(2); + } + const brutalSpriteKey = brutalConfig.getSpriteKey(female, brutalConfig.doubleOnly); + encounter.enemyPartyConfigs.push({ + trainerConfig: brutalConfig, + levelAdditiveModifier: 1.5, + female: female, + }); + + encounter.spriteConfigs = [ + { + spriteKey: normalSpriteKey, + fileRoot: "trainer", + hasShadow: true, + tint: 1, + }, + { + spriteKey: hardSpriteKey, + fileRoot: "trainer", + hasShadow: true, + tint: 1, + }, + { + spriteKey: brutalSpriteKey, + fileRoot: "trainer", + hasShadow: true, + tint: 1, + }, + ]; + + return true; + }) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.selected`, + }, + ], + }, + async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + // Spawn standard trainer battle with memory mushroom reward + const config: EnemyPartyConfig = encounter.enemyPartyConfigs[0]; + + setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.TM_COMMON, modifierTypes.TM_GREAT, modifierTypes.MEMORY_MUSHROOM], fillRemaining: true }); + + // Seed offsets to remove possibility of different trainers having exact same teams + let ret; + scene.executeWithSeedOffset(() => { + ret = initBattleWithEnemyConfig(scene, config); + }, scene.currentBattle.waveIndex * 10); + return ret; + } + ) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + text: `${namespace}.option.selected`, + }, + ], + }, + async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + // Spawn hard fight + const config: EnemyPartyConfig = encounter.enemyPartyConfigs[1]; + + setEncounterRewards(scene, { guaranteedModifierTiers: [ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.GREAT, ModifierTier.GREAT], fillRemaining: true }); + + // Seed offsets to remove possibility of different trainers having exact same teams + let ret; + scene.executeWithSeedOffset(() => { + ret = initBattleWithEnemyConfig(scene, config); + }, scene.currentBattle.waveIndex * 100); + return ret; + } + ) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + selected: [ + { + text: `${namespace}.option.selected`, + }, + ], + }, + async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + // Spawn brutal fight + const config: EnemyPartyConfig = encounter.enemyPartyConfigs[2]; + + // To avoid player level snowballing from picking this option + encounter.expMultiplier = 0.9; + + setEncounterRewards(scene, { guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ROGUE, ModifierTier.ULTRA, ModifierTier.GREAT], fillRemaining: true }); + + // Seed offsets to remove possibility of different trainers having exact same teams + let ret; + scene.executeWithSeedOffset(() => { + ret = initBattleWithEnemyConfig(scene, config); + }, scene.currentBattle.waveIndex * 1000); + return ret; + } + ) + .withOutroDialogue([ + { + text: `${namespace}.outro`, + }, + ]) + .build(); diff --git a/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts b/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts new file mode 100644 index 00000000000..b5da4340dea --- /dev/null +++ b/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts @@ -0,0 +1,204 @@ +import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { EnemyPartyConfig, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, setEncounterRewards, transitionMysteryEncounterIntroVisuals } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { getHighestLevelPlayerPokemon, koPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { ModifierTier } from "#app/modifier/modifier-tier"; +import { randSeedInt } from "#app/utils"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import BattleScene from "#app/battle-scene"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { Species } from "#enums/species"; +import { Moves } from "#enums/moves"; +import { GameOverPhase } from "#app/phases/game-over-phase"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; + +/** i18n namespace for encounter */ +const namespace = "mysteryEncounter:mysteriousChest"; + +const RAND_LENGTH = 100; +const COMMON_REWARDS_WEIGHT = 20; // 20% +const ULTRA_REWARDS_WEIGHT = 50; // 30% +const ROGUE_REWARDS_WEIGHT = 60; // 10% +const MASTER_REWARDS_WEIGHT = 65; // 5% + +/** + * Mysterious Chest encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3796 | GitHub Issue #3796} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const MysteriousChestEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.MYSTERIOUS_CHEST) + .withEncounterTier(MysteryEncounterTier.COMMON) + .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + .withAutoHideIntroVisuals(false) + .withCatchAllowed(true) + .withIntroSpriteConfigs([ + { + spriteKey: "mysterious_chest_blue", + fileRoot: "mystery-encounters", + hasShadow: true, + y: 8, + yShadow: 6, + alpha: 1, + disableAnimation: true, // Re-enabled after option select + }, + { + spriteKey: "mysterious_chest_red", + fileRoot: "mystery-encounters", + hasShadow: false, + y: 8, + yShadow: 6, + alpha: 0, + disableAnimation: true, // Re-enabled after option select + } + ]) + .withIntroDialogue([ + { + text: `${namespace}.intro`, + } + ]) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withOnInit((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + + // Calculate boss mon + const config: EnemyPartyConfig = { + levelAdditiveModifier: 0.5, + disableSwitch: true, + pokemonConfigs: [ + { + species: getPokemonSpecies(Species.GIMMIGHOUL), + formIndex: 0, + isBoss: true, + moveSet: [Moves.NASTY_PLOT, Moves.SHADOW_BALL, Moves.POWER_GEM, Moves.THIEF] + } + ], + }; + + encounter.enemyPartyConfigs = [config]; + + encounter.setDialogueToken("gimmighoulName", getPokemonSpecies(Species.GIMMIGHOUL).getName()); + + return true; + }) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene) => { + // Play animation + const encounter = scene.currentBattle.mysteryEncounter!; + const introVisuals = encounter.introVisuals!; + + // Determine roll first + const roll = randSeedInt(RAND_LENGTH); + encounter.misc = { + roll + }; + + if (roll >= MASTER_REWARDS_WEIGHT) { + // Chest is springing trap, change to red chest sprite + const blueChestSprites = introVisuals.getSpriteAtIndex(0); + const redChestSprites = introVisuals.getSpriteAtIndex(1); + redChestSprites[0].setAlpha(1); + blueChestSprites[0].setAlpha(0.001); + } + introVisuals.spriteConfigs[0].disableAnimation = false; + introVisuals.spriteConfigs[1].disableAnimation = false; + introVisuals.playAnim(); + }) + .withOptionPhase(async (scene: BattleScene) => { + // Open the chest + const encounter = scene.currentBattle.mysteryEncounter!; + const roll = encounter.misc.roll; + if (roll < COMMON_REWARDS_WEIGHT) { + // Choose between 2 COMMON / 2 GREAT tier items (20%) + setEncounterRewards(scene, { + guaranteedModifierTiers: [ + ModifierTier.COMMON, + ModifierTier.COMMON, + ModifierTier.GREAT, + ModifierTier.GREAT, + ], + }); + // Display result message then proceed to rewards + queueEncounterMessage(scene, `${namespace}.option.1.normal`); + leaveEncounterWithoutBattle(scene); + } else if (roll < ULTRA_REWARDS_WEIGHT) { + // Choose between 3 ULTRA tier items (30%) + setEncounterRewards(scene, { + guaranteedModifierTiers: [ + ModifierTier.ULTRA, + ModifierTier.ULTRA, + ModifierTier.ULTRA, + ], + }); + // Display result message then proceed to rewards + queueEncounterMessage(scene, `${namespace}.option.1.good`); + leaveEncounterWithoutBattle(scene); + } else if (roll < ROGUE_REWARDS_WEIGHT) { + // Choose between 2 ROGUE tier items (10%) + setEncounterRewards(scene, { guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ROGUE] }); + // Display result message then proceed to rewards + queueEncounterMessage(scene, `${namespace}.option.1.great`); + leaveEncounterWithoutBattle(scene); + } else if (roll < MASTER_REWARDS_WEIGHT) { + // Choose 1 MASTER tier item (5%) + setEncounterRewards(scene, { guaranteedModifierTiers: [ModifierTier.MASTER] }); + // Display result message then proceed to rewards + queueEncounterMessage(scene, `${namespace}.option.1.amazing`); + leaveEncounterWithoutBattle(scene); + } else { + // Your highest level unfainted Pokemon gets OHKO. Start battle against a Gimmighoul (35%) + const highestLevelPokemon = getHighestLevelPlayerPokemon(scene, true, false); + koPlayerPokemon(scene, highestLevelPokemon); + + encounter.setDialogueToken("pokeName", highestLevelPokemon.getNameToRender()); + await showEncounterText(scene, `${namespace}.option.1.bad`); + + // Handle game over edge case + const allowedPokemon = scene.getParty().filter(p => p.isAllowedInBattle()); + if (allowedPokemon.length === 0) { + // If there are no longer any legal pokemon in the party, game over. + scene.clearPhaseQueue(); + scene.unshiftPhase(new GameOverPhase(scene)); + } else { + // Show which Pokemon was KOed, then start battle against Gimmighoul + transitionMysteryEncounterIntroVisuals(scene, true, true, 500); + setEncounterRewards(scene, { fillRemaining: true }); + await initBattleWithEnemyConfig(scene, encounter.enemyPartyConfigs[0]); + } + } + }) + .build() + ) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Leave encounter with no rewards or exp + leaveEncounterWithoutBattle(scene, true); + return true; + } + ) + .build(); diff --git a/src/data/mystery-encounters/encounters/part-timer-encounter.ts b/src/data/mystery-encounters/encounters/part-timer-encounter.ts new file mode 100644 index 00000000000..4c31e83facb --- /dev/null +++ b/src/data/mystery-encounters/encounters/part-timer-encounter.ts @@ -0,0 +1,333 @@ +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { leaveEncounterWithoutBattle, selectPokemonForOption, setEncounterExp, setEncounterRewards, transitionMysteryEncounterIntroVisuals, updatePlayerMoney } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import BattleScene from "#app/battle-scene"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MoveRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { Stat } from "#enums/stat"; +import { CHARMING_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups"; +import { showEncounterDialogue, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import i18next from "i18next"; +import Pokemon, { PlayerPokemon } from "#app/field/pokemon"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; +import { isPokemonValidForEncounterOptionSelection } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; + +/** the i18n namespace for the encounter */ +const namespace = "mysteryEncounter:partTimer"; + +/** + * Part Timer encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3813 | GitHub Issue #3813} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const PartTimerEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.PART_TIMER) + .withEncounterTier(MysteryEncounterTier.COMMON) + .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + .withIntroSpriteConfigs([ + { + spriteKey: "part_timer_crate", + fileRoot: "mystery-encounters", + hasShadow: false, + y: 6, + x: 15 + }, + { + spriteKey: "worker_f", + fileRoot: "trainer", + hasShadow: true, + x: -18, + y: 4 + } + ]) + .withAutoHideIntroVisuals(false) + .withIntroDialogue([ + { + text: `${namespace}.intro`, + }, + { + speaker: `${namespace}.speaker`, + text: `${namespace}.intro_dialogue`, + }, + ]) + .withOnInit((scene: BattleScene) => { + // Load sfx + scene.loadSe("PRSFX- Horn Drill1", "battle_anims", "PRSFX- Horn Drill1.wav"); + scene.loadSe("PRSFX- Horn Drill3", "battle_anims", "PRSFX- Horn Drill3.wav"); + scene.loadSe("PRSFX- Guillotine2", "battle_anims", "PRSFX- Guillotine2.wav"); + scene.loadSe("PRSFX- Heavy Slam2", "battle_anims", "PRSFX- Heavy Slam2.wav"); + + scene.loadSe("PRSFX- Agility", "battle_anims", "PRSFX- Agility.wav"); + scene.loadSe("PRSFX- Extremespeed1", "battle_anims", "PRSFX- Extremespeed1.wav"); + scene.loadSe("PRSFX- Accelerock1", "battle_anims", "PRSFX- Accelerock1.wav"); + + scene.loadSe("PRSFX- Captivate", "battle_anims", "PRSFX- Captivate.wav"); + scene.loadSe("PRSFX- Attract2", "battle_anims", "PRSFX- Attract2.wav"); + scene.loadSe("PRSFX- Aurora Veil2", "battle_anims", "PRSFX- Aurora Veil2.wav"); + + return true; + }) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withOption(MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected` + } + ] + }) + .withPreOptionPhase(async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + + const onPokemonSelected = (pokemon: PlayerPokemon) => { + encounter.setDialogueToken("selectedPokemon", pokemon.getNameToRender()); + + // Calculate the "baseline" stat value (90 base stat, 16 IVs, neutral nature, same level as pokemon) to compare + // Resulting money is 2.5 * (% difference from baseline), with minimum of 1 and maximum of 4. + // Calculation from Pokemon.calculateStats + const baselineValue = Math.floor(((2 * 90 + 16) * pokemon.level) * 0.01) + 5; + const percentDiff = (pokemon.getStat(Stat.SPD) - baselineValue) / baselineValue; + const moneyMultiplier = Math.min(Math.max(2.5 * (1+ percentDiff), 1), 4); + + encounter.misc = { + moneyMultiplier + }; + + // Reduce all PP to 2 (if they started at greater than 2) + pokemon.moveset.forEach(move => { + if (move) { + const newPpUsed = move.getMovePp() - 2; + move.ppUsed = move.ppUsed < newPpUsed ? newPpUsed : move.ppUsed; + } + }); + + setEncounterExp(scene, pokemon.id, 100); + + // Hide intro visuals + transitionMysteryEncounterIntroVisuals(scene, true, false); + // Play sfx for "working" + doDeliverySfx(scene); + }; + + // Only Pokemon non-KOd pokemon can be selected + const selectableFilter = (pokemon: Pokemon) => { + return isPokemonValidForEncounterOptionSelection(pokemon, scene, `${namespace}.invalid_selection`); + }; + + return selectPokemonForOption(scene, onPokemonSelected, undefined, selectableFilter); + }) + .withOptionPhase(async (scene: BattleScene) => { + // Pick Deliveries + // Bring visuals back in + await transitionMysteryEncounterIntroVisuals(scene, false, false); + + const moneyMultiplier = scene.currentBattle.mysteryEncounter!.misc.moneyMultiplier; + + // Give money and do dialogue + if (moneyMultiplier > 2.5) { + await showEncounterDialogue(scene, `${namespace}.job_complete_good`, `${namespace}.speaker`); + } else { + await showEncounterDialogue(scene, `${namespace}.job_complete_bad`, `${namespace}.speaker`); + } + const moneyChange = scene.getWaveMoneyAmount(moneyMultiplier); + updatePlayerMoney(scene, moneyChange, true, false); + await showEncounterText(scene, i18next.t("mysteryEncounterMessages:receive_money", { amount: moneyChange })); + await showEncounterText(scene, `${namespace}.pokemon_tired`); + + setEncounterRewards(scene, { fillRemaining: true }); + leaveEncounterWithoutBattle(scene); + }) + .build() + ) + .withOption(MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected` + } + ] + }) + .withPreOptionPhase(async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + + const onPokemonSelected = (pokemon: PlayerPokemon) => { + encounter.setDialogueToken("selectedPokemon", pokemon.getNameToRender()); + + // Calculate the "baseline" stat value (75 base stat, 16 IVs, neutral nature, same level as pokemon) to compare + // Resulting money is 2.5 * (% difference from baseline), with minimum of 1 and maximum of 4. + // Calculation from Pokemon.calculateStats + const baselineHp = Math.floor(((2 * 75 + 16) * pokemon.level) * 0.01) + pokemon.level + 10; + const baselineAtkDef = Math.floor(((2 * 75 + 16) * pokemon.level) * 0.01) + 5; + const baselineValue = baselineHp + 1.5 * (baselineAtkDef * 2); + const strongestValue = pokemon.getStat(Stat.HP) + 1.5 * (pokemon.getStat(Stat.ATK) + pokemon.getStat(Stat.DEF)); + const percentDiff = (strongestValue - baselineValue) / baselineValue; + const moneyMultiplier = Math.min(Math.max(2.5 * (1 + percentDiff), 1), 4); + + encounter.misc = { + moneyMultiplier + }; + + // Reduce all PP to 2 (if they started at greater than 2) + pokemon.moveset.forEach(move => { + if (move) { + const newPpUsed = move.getMovePp() - 2; + move.ppUsed = move.ppUsed < newPpUsed ? newPpUsed : move.ppUsed; + } + }); + + setEncounterExp(scene, pokemon.id, 100); + + // Hide intro visuals + transitionMysteryEncounterIntroVisuals(scene, true, false); + // Play sfx for "working" + doStrongWorkSfx(scene); + }; + + // Only Pokemon non-KOd pokemon can be selected + const selectableFilter = (pokemon: Pokemon) => { + return isPokemonValidForEncounterOptionSelection(pokemon, scene, `${namespace}.invalid_selection`); + }; + + return selectPokemonForOption(scene, onPokemonSelected, undefined, selectableFilter); + }) + .withOptionPhase(async (scene: BattleScene) => { + // Pick Move Warehouse items + // Bring visuals back in + await transitionMysteryEncounterIntroVisuals(scene, false, false); + + const moneyMultiplier = scene.currentBattle.mysteryEncounter!.misc.moneyMultiplier; + + // Give money and do dialogue + if (moneyMultiplier > 2.5) { + await showEncounterDialogue(scene, `${namespace}.job_complete_good`, `${namespace}.speaker`); + } else { + await showEncounterDialogue(scene, `${namespace}.job_complete_bad`, `${namespace}.speaker`); + } + const moneyChange = scene.getWaveMoneyAmount(moneyMultiplier); + updatePlayerMoney(scene, moneyChange, true, false); + await showEncounterText(scene, i18next.t("mysteryEncounterMessages:receive_money", { amount: moneyChange })); + await showEncounterText(scene, `${namespace}.pokemon_tired`); + + setEncounterRewards(scene, { fillRemaining: true }); + leaveEncounterWithoutBattle(scene); + }) + .build() + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL) + .withPrimaryPokemonRequirement(new MoveRequirement(CHARMING_MOVES)) // Will set option3PrimaryName and option3PrimaryMove dialogue tokens automatically + .withDialogue({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + disabledButtonTooltip: `${namespace}.option.3.disabled_tooltip`, + selected: [ + { + text: `${namespace}.option.3.selected`, + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + const selectedPokemon = encounter.selectedOption?.primaryPokemon!; + encounter.setDialogueToken("selectedPokemon", selectedPokemon.getNameToRender()); + + // Reduce all PP to 2 (if they started at greater than 2) + selectedPokemon.moveset.forEach(move => { + if (move) { + const newPpUsed = move.getMovePp() - 2; + move.ppUsed = move.ppUsed < newPpUsed ? newPpUsed : move.ppUsed; + } + }); + + setEncounterExp(scene, selectedPokemon.id, 100); + + // Hide intro visuals + transitionMysteryEncounterIntroVisuals(scene, true, false); + // Play sfx for "working" + doSalesSfx(scene); + return true; + }) + .withOptionPhase(async (scene: BattleScene) => { + // Assist with Sales + // Bring visuals back in + await transitionMysteryEncounterIntroVisuals(scene, false, false); + + // Give money and do dialogue + await showEncounterDialogue(scene, `${namespace}.job_complete_good`, `${namespace}.speaker`); + const moneyChange = scene.getWaveMoneyAmount(2.5); + updatePlayerMoney(scene, moneyChange, true, false); + await showEncounterText(scene, i18next.t("mysteryEncounterMessages:receive_money", { amount: moneyChange })); + await showEncounterText(scene, `${namespace}.pokemon_tired`); + + setEncounterRewards(scene, { fillRemaining: true }); + leaveEncounterWithoutBattle(scene); + }) + .build() + ) + .withOutroDialogue([ + { + speaker: `${namespace}.speaker`, + text: `${namespace}.outro`, + } + ]) + .build(); + +function doStrongWorkSfx(scene: BattleScene) { + scene.playSound("battle_anims/PRSFX- Horn Drill1"); + scene.playSound("battle_anims/PRSFX- Horn Drill1"); + + scene.time.delayedCall(1000, () => { + scene.playSound("battle_anims/PRSFX- Guillotine2"); + }); + + scene.time.delayedCall(2000, () => { + scene.playSound("battle_anims/PRSFX- Heavy Slam2"); + }); + + scene.time.delayedCall(2500, () => { + scene.playSound("battle_anims/PRSFX- Guillotine2"); + }); +} + +function doDeliverySfx(scene: BattleScene) { + scene.playSound("battle_anims/PRSFX- Accelerock1"); + + scene.time.delayedCall(1500, () => { + scene.playSound("battle_anims/PRSFX- Extremespeed1"); + }); + + scene.time.delayedCall(2000, () => { + scene.playSound("battle_anims/PRSFX- Extremespeed1"); + }); + + scene.time.delayedCall(2250, () => { + scene.playSound("battle_anims/PRSFX- Agility"); + }); +} + +function doSalesSfx(scene: BattleScene) { + scene.playSound("battle_anims/PRSFX- Captivate"); + + scene.time.delayedCall(1500, () => { + scene.playSound("battle_anims/PRSFX- Attract2"); + }); + + scene.time.delayedCall(2000, () => { + scene.playSound("battle_anims/PRSFX- Aurora Veil2"); + }); + + scene.time.delayedCall(3000, () => { + scene.playSound("battle_anims/PRSFX- Attract2"); + }); +} diff --git a/src/data/mystery-encounters/encounters/safari-zone-encounter.ts b/src/data/mystery-encounters/encounters/safari-zone-encounter.ts new file mode 100644 index 00000000000..97aedc4f826 --- /dev/null +++ b/src/data/mystery-encounters/encounters/safari-zone-encounter.ts @@ -0,0 +1,518 @@ +import { initSubsequentOptionSelect, leaveEncounterWithoutBattle, transitionMysteryEncounterIntroVisuals, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import BattleScene from "#app/battle-scene"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import MysteryEncounterOption, { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { TrainerSlot } from "#app/data/trainer-config"; +import { HiddenAbilityRateBoosterModifier, IvScannerModifier } from "#app/modifier/modifier"; +import { EnemyPokemon } from "#app/field/pokemon"; +import { PokeballType } from "#app/data/pokeball"; +import { PlayerGender } from "#enums/player-gender"; +import { IntegerHolder, randSeedInt } from "#app/utils"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { MoneyRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; +import { doPlayerFlee, doPokemonFlee, getRandomSpeciesByStarterTier, trainerThrowPokeball } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { getEncounterText, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { getPokemonNameWithAffix } from "#app/messages"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { ScanIvsPhase } from "#app/phases/scan-ivs-phase"; +import { SummonPhase } from "#app/phases/summon-phase"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; + +/** the i18n namespace for the encounter */ +const namespace = "mysteryEncounter:safariZone"; + +const TRAINER_THROW_ANIMATION_TIMES = [512, 184, 768]; + +const SAFARI_MONEY_MULTIPLIER = 2; + +/** + * Safari Zone encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3800 | GitHub Issue #3800} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const SafariZoneEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.SAFARI_ZONE) + .withEncounterTier(MysteryEncounterTier.GREAT) + .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + .withSceneRequirement(new MoneyRequirement(0, SAFARI_MONEY_MULTIPLIER)) // Cost equal to 1 Max Revive + .withAutoHideIntroVisuals(false) + .withIntroSpriteConfigs([ + { + spriteKey: "safari_zone", + fileRoot: "mystery-encounters", + hasShadow: false, + x: 4, + y: 6 + }, + ]) + .withIntroDialogue([ + { + text: `${namespace}.intro`, + }, + ]) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withOption(MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) + .withSceneRequirement(new MoneyRequirement(0, SAFARI_MONEY_MULTIPLIER)) // Cost equal to 1 Max Revive + .withDialogue({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }) + .withOptionPhase(async (scene: BattleScene) => { + // Start safari encounter + const encounter = scene.currentBattle.mysteryEncounter!; + encounter.continuousEncounter = true; + encounter.misc = { + safariPokemonRemaining: 3 + }; + updatePlayerMoney(scene, -(encounter.options[0].requirements[0] as MoneyRequirement).requiredMoney); + // Load bait/mud assets + scene.loadSe("PRSFX- Bug Bite", "battle_anims", "PRSFX- Bug Bite.wav"); + scene.loadSe("PRSFX- Sludge Bomb2", "battle_anims", "PRSFX- Sludge Bomb2.wav"); + scene.loadSe("PRSFX- Taunt2", "battle_anims", "PRSFX- Taunt2.wav"); + scene.loadAtlas("safari_zone_bait", "mystery-encounters"); + scene.loadAtlas("safari_zone_mud", "mystery-encounters"); + // Clear enemy party + scene.currentBattle.enemyParty = []; + await transitionMysteryEncounterIntroVisuals(scene); + await summonSafariPokemon(scene); + initSubsequentOptionSelect(scene, { overrideOptions: safariZoneGameOptions, hideDescription: true }); + return true; + }) + .build() + ) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Leave encounter with no rewards or exp + leaveEncounterWithoutBattle(scene, true); + return true; + } + ) + .build(); + +/** + * SAFARI ZONE MINIGAME OPTIONS + * + * Catch and flee rate stages are calculated in the same way stat changes are (they range from -6/+6) + * https://bulbapedia.bulbagarden.net/wiki/Catch_rate#Great_Marsh_and_Johto_Safari_Zone + * + * Catch Rate calculation: + * catchRate = speciesCatchRate [1 to 255] * catchStageMultiplier [2/8 to 8/2] * ballCatchRate [1.5] + * + * Flee calculation: + * The harder a species is to catch, the higher its flee rate is + * (Caps at 50% base chance to flee for the hardest to catch Pokemon, before factoring in flee stage) + * fleeRate = ((255^2 - speciesCatchRate^2) / 255 / 2) [0 to 127.5] * fleeStageMultiplier [2/8 to 8/2] + * Flee chance = fleeRate / 255 + */ +const safariZoneGameOptions: MysteryEncounterOption[] = [ + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.safari.1.label`, + buttonTooltip: `${namespace}.safari.1.tooltip`, + selected: [ + { + text: `${namespace}.safari.1.selected`, + } + ], + }) + .withOptionPhase(async (scene: BattleScene) => { + // Throw a ball option + const encounter = scene.currentBattle.mysteryEncounter!; + const pokemon = encounter.misc.pokemon; + const catchResult = await throwPokeball(scene, pokemon); + + if (catchResult) { + // You caught pokemon + // Check how many safari pokemon left + if (encounter.misc.safariPokemonRemaining > 0) { + await summonSafariPokemon(scene); + initSubsequentOptionSelect(scene, { overrideOptions: safariZoneGameOptions, startingCursorIndex: 0, hideDescription: true }); + } else { + // End safari mode + encounter.continuousEncounter = false; + leaveEncounterWithoutBattle(scene, true); + } + } else { + // Pokemon catch failed, end turn + await doEndTurn(scene, 0); + } + return true; + }) + .build(), + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.safari.2.label`, + buttonTooltip: `${namespace}.safari.2.tooltip`, + selected: [ + { + text: `${namespace}.safari.2.selected`, + }, + ], + }) + .withOptionPhase(async (scene: BattleScene) => { + // Throw bait option + const pokemon = scene.currentBattle.mysteryEncounter!.misc.pokemon; + await throwBait(scene, pokemon); + + // 100% chance to increase catch stage +2 + tryChangeCatchStage(scene, 2); + // 80% chance to increase flee stage +1 + const fleeChangeResult = tryChangeFleeStage(scene, 1, 8); + if (!fleeChangeResult) { + await showEncounterText(scene, getEncounterText(scene, `${namespace}.safari.busy_eating`) ?? "", null, 1000, false ); + } else { + await showEncounterText(scene, getEncounterText(scene, `${namespace}.safari.eating`) ?? "", null, 1000, false); + } + + await doEndTurn(scene, 1); + return true; + }) + .build(), + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.safari.3.label`, + buttonTooltip: `${namespace}.safari.3.tooltip`, + selected: [ + { + text: `${namespace}.safari.3.selected`, + }, + ], + }) + .withOptionPhase(async (scene: BattleScene) => { + // Throw mud option + const pokemon = scene.currentBattle.mysteryEncounter!.misc.pokemon; + await throwMud(scene, pokemon); + // 100% chance to decrease flee stage -2 + tryChangeFleeStage(scene, -2); + // 80% chance to decrease catch stage -1 + const catchChangeResult = tryChangeCatchStage(scene, -1, 8); + if (!catchChangeResult) { + await showEncounterText(scene, getEncounterText(scene, `${namespace}.safari.beside_itself_angry`) ?? "", null, 1000, false ); + } else { + await showEncounterText(scene, getEncounterText(scene, `${namespace}.safari.angry`) ?? "", null, 1000, false ); + } + + await doEndTurn(scene, 2); + return true; + }) + .build(), + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.safari.4.label`, + buttonTooltip: `${namespace}.safari.4.tooltip`, + }) + .withOptionPhase(async (scene: BattleScene) => { + // Flee option + const encounter = scene.currentBattle.mysteryEncounter!; + const pokemon = encounter.misc.pokemon; + await doPlayerFlee(scene, pokemon); + // Check how many safari pokemon left + if (encounter.misc.safariPokemonRemaining > 0) { + await summonSafariPokemon(scene); + initSubsequentOptionSelect(scene, { overrideOptions: safariZoneGameOptions, startingCursorIndex: 3, hideDescription: true }); + } else { + // End safari mode + encounter.continuousEncounter = false; + leaveEncounterWithoutBattle(scene, true); + } + return true; + }) + .build() +]; + +async function summonSafariPokemon(scene: BattleScene) { + const encounter = scene.currentBattle.mysteryEncounter!; + // Message pokemon remaining + encounter.setDialogueToken("remainingCount", encounter.misc.safariPokemonRemaining); + scene.queueMessage(getEncounterText(scene, `${namespace}.safari.remaining_count`) ?? "", null, true); + + // Generate pokemon using safariPokemonRemaining so they are always the same pokemon no matter how many turns are taken + // Safari pokemon roll twice on shiny and HA chances, but are otherwise normal + let enemySpecies; + let pokemon; + scene.executeWithSeedOffset(() => { + enemySpecies = getPokemonSpecies(getRandomSpeciesByStarterTier([0, 5], undefined, undefined, false, false, false)); + const level = scene.currentBattle.getLevelForWave(); + enemySpecies = getPokemonSpecies(enemySpecies.getWildSpeciesForLevel(level, true, false, scene.gameMode)); + pokemon = scene.addEnemyPokemon(enemySpecies, level, TrainerSlot.NONE, false); + + // Roll shiny twice + if (!pokemon.shiny) { + pokemon.trySetShinySeed(); + } + + // Roll HA twice + if (pokemon.species.abilityHidden) { + const hiddenIndex = pokemon.species.ability2 ? 2 : 1; + if (pokemon.abilityIndex < hiddenIndex) { + const hiddenAbilityChance = new IntegerHolder(256); + scene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance); + + const hasHiddenAbility = !randSeedInt(hiddenAbilityChance.value); + + if (hasHiddenAbility) { + pokemon.abilityIndex = hiddenIndex; + } + } + } + + pokemon.calculateStats(); + + scene.currentBattle.enemyParty.unshift(pokemon); + }, scene.currentBattle.waveIndex * 1000 * encounter.misc.safariPokemonRemaining); + + scene.gameData.setPokemonSeen(pokemon, true); + await pokemon.loadAssets(); + + // Reset safari catch and flee rates + encounter.misc.catchStage = 0; + encounter.misc.fleeStage = 0; + encounter.misc.pokemon = pokemon; + encounter.misc.safariPokemonRemaining -= 1; + + scene.unshiftPhase(new SummonPhase(scene, 0, false)); + + encounter.setDialogueToken("pokemonName", getPokemonNameWithAffix(pokemon)); + showEncounterText(scene, getEncounterText(scene, "battle:singleWildAppeared") ?? "", null, 1500, false) + .then(() => { + const ivScannerModifier = scene.findModifier(m => m instanceof IvScannerModifier); + if (ivScannerModifier) { + scene.pushPhase(new ScanIvsPhase(scene, pokemon.getBattlerIndex(), Math.min(ivScannerModifier.getStackCount() * 2, 6))); + } + }); +} + +function throwPokeball(scene: BattleScene, pokemon: EnemyPokemon): Promise { + const baseCatchRate = pokemon.species.catchRate; + // Catch stage ranges from -6 to +6 (like stat boost stages) + const safariCatchStage = scene.currentBattle.mysteryEncounter!.misc.catchStage; + // Catch modifier ranges from 2/8 (-6 stage) to 8/2 (+6) + const safariModifier = (2 + Math.min(Math.max(safariCatchStage, 0), 6)) / (2 - Math.max(Math.min(safariCatchStage, 0), -6)); + // Catch rate same as safari ball + const pokeballMultiplier = 1.5; + const catchRate = Math.round(baseCatchRate * pokeballMultiplier * safariModifier); + const ballTwitchRate = Math.round(1048560 / Math.sqrt(Math.sqrt(16711680 / catchRate))); + return trainerThrowPokeball(scene, pokemon, PokeballType.POKEBALL, ballTwitchRate); +} + +async function throwBait(scene: BattleScene, pokemon: EnemyPokemon): Promise { + const originalY: number = pokemon.y; + + const fpOffset = pokemon.getFieldPositionOffset(); + const bait: Phaser.GameObjects.Sprite = scene.addFieldSprite(16 + 75, 80 + 25, "safari_zone_bait", "0001.png"); + bait.setOrigin(0.5, 0.625); + scene.field.add(bait); + + return new Promise(resolve => { + scene.trainer.setTexture(`trainer_${scene.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}_back_pb`); + scene.time.delayedCall(TRAINER_THROW_ANIMATION_TIMES[0], () => { + scene.playSound("se/pb_throw"); + + // Trainer throw frames + scene.trainer.setFrame("2"); + scene.time.delayedCall(TRAINER_THROW_ANIMATION_TIMES[1], () => { + scene.trainer.setFrame("3"); + scene.time.delayedCall(TRAINER_THROW_ANIMATION_TIMES[2], () => { + scene.trainer.setTexture(`trainer_${scene.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}_back`); + }); + }); + + // Pokeball move and catch logic + scene.tweens.add({ + targets: bait, + x: { value: 210 + fpOffset[0], ease: "Linear" }, + y: { value: 55 + fpOffset[1], ease: "Cubic.easeOut" }, + duration: 500, + onComplete: () => { + + let index = 1; + scene.time.delayedCall(768, () => { + scene.tweens.add({ + targets: pokemon, + duration: 150, + ease: "Cubic.easeOut", + yoyo: true, + y: originalY - 5, + loop: 6, + onStart: () => { + scene.playSound("battle_anims/PRSFX- Bug Bite"); + bait.setFrame("0002.png"); + }, + onLoop: () => { + if (index % 2 === 0) { + scene.playSound("battle_anims/PRSFX- Bug Bite"); + } + if (index === 4) { + bait.setFrame("0003.png"); + } + index++; + }, + onComplete: () => { + scene.time.delayedCall(256, () => { + bait.destroy(); + resolve(true); + }); + } + }); + }); + } + }); + }); + }); +} + +async function throwMud(scene: BattleScene, pokemon: EnemyPokemon): Promise { + const originalY: number = pokemon.y; + + const fpOffset = pokemon.getFieldPositionOffset(); + const mud: Phaser.GameObjects.Sprite = scene.addFieldSprite(16 + 75, 80 + 35, "safari_zone_mud", "0001.png"); + mud.setOrigin(0.5, 0.625); + scene.field.add(mud); + + return new Promise(resolve => { + scene.trainer.setTexture(`trainer_${scene.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}_back_pb`); + scene.time.delayedCall(TRAINER_THROW_ANIMATION_TIMES[0], () => { + scene.playSound("se/pb_throw"); + + // Trainer throw frames + scene.trainer.setFrame("2"); + scene.time.delayedCall(TRAINER_THROW_ANIMATION_TIMES[1], () => { + scene.trainer.setFrame("3"); + scene.time.delayedCall(TRAINER_THROW_ANIMATION_TIMES[2], () => { + scene.trainer.setTexture(`trainer_${scene.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}_back`); + }); + }); + + // Mud throw and splat + scene.tweens.add({ + targets: mud, + x: { value: 230 + fpOffset[0], ease: "Linear" }, + y: { value: 55 + fpOffset[1], ease: "Cubic.easeOut" }, + duration: 500, + onComplete: () => { + // Mud frame 2 + scene.playSound("battle_anims/PRSFX- Sludge Bomb2"); + mud.setFrame("0002.png"); + // Mud splat + scene.time.delayedCall(200, () => { + mud.setFrame("0003.png"); + scene.time.delayedCall(400, () => { + mud.setFrame("0004.png"); + }); + }); + + // Fade mud then angry animation + scene.tweens.add({ + targets: mud, + alpha: 0, + ease: "Cubic.easeIn", + duration: 1000, + onComplete: () => { + mud.destroy(); + scene.tweens.add({ + targets: pokemon, + duration: 300, + ease: "Cubic.easeOut", + yoyo: true, + y: originalY - 20, + loop: 1, + onStart: () => { + scene.playSound("battle_anims/PRSFX- Taunt2"); + }, + onLoop: () => { + scene.playSound("battle_anims/PRSFX- Taunt2"); + }, + onComplete: () => { + resolve(true); + } + }); + } + }); + } + }); + }); + }); +} + +function isPokemonFlee(pokemon: EnemyPokemon, fleeStage: number): boolean { + const speciesCatchRate = pokemon.species.catchRate; + const fleeModifier = (2 + Math.min(Math.max(fleeStage, 0), 6)) / (2 - Math.max(Math.min(fleeStage, 0), -6)); + const fleeRate = (255 * 255 - speciesCatchRate * speciesCatchRate) / 255 / 2 * fleeModifier; + console.log("Flee rate: " + fleeRate); + const roll = randSeedInt(256); + console.log("Roll: " + roll); + return roll < fleeRate; +} + +function tryChangeFleeStage(scene: BattleScene, change: number, chance?: number): boolean { + if (chance && randSeedInt(10) >= chance) { + return false; + } + const currentFleeStage = scene.currentBattle.mysteryEncounter!.misc.fleeStage ?? 0; + scene.currentBattle.mysteryEncounter!.misc.fleeStage = Math.min(Math.max(currentFleeStage + change, -6), 6); + return true; +} + +function tryChangeCatchStage(scene: BattleScene, change: number, chance?: number): boolean { + if (chance && randSeedInt(10) >= chance) { + return false; + } + const currentCatchStage = scene.currentBattle.mysteryEncounter!.misc.catchStage ?? 0; + scene.currentBattle.mysteryEncounter!.misc.catchStage = Math.min(Math.max(currentCatchStage + change, -6), 6); + return true; +} + +async function doEndTurn(scene: BattleScene, cursorIndex: number) { + // First cleanup and destroy old Pokemon objects that were left in the enemyParty + // They are left in enemyParty temporarily so that VictoryPhase properly handles EXP + const party = scene.getEnemyParty(); + if (party.length > 1) { + for (let i = 1; i < party.length; i++) { + party[i].destroy(); + } + scene.currentBattle.enemyParty = party.slice(0, 1); + } + + const encounter = scene.currentBattle.mysteryEncounter!; + const pokemon = encounter.misc.pokemon; + const isFlee = isPokemonFlee(pokemon, encounter.misc.fleeStage); + if (isFlee) { + // Pokemon flees! + await doPokemonFlee(scene, pokemon); + // Check how many safari pokemon left + if (encounter.misc.safariPokemonRemaining > 0) { + await summonSafariPokemon(scene); + initSubsequentOptionSelect(scene, { overrideOptions: safariZoneGameOptions, startingCursorIndex: cursorIndex, hideDescription: true }); + } else { + // End safari mode + encounter.continuousEncounter = false; + leaveEncounterWithoutBattle(scene, true); + } + } else { + scene.queueMessage(getEncounterText(scene, `${namespace}.safari.watching`) ?? "", 0, null, 1000); + initSubsequentOptionSelect(scene, { overrideOptions: safariZoneGameOptions, startingCursorIndex: cursorIndex, hideDescription: true }); + } +} diff --git a/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts b/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts new file mode 100644 index 00000000000..d57a47cb689 --- /dev/null +++ b/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts @@ -0,0 +1,227 @@ +import { generateModifierType, leaveEncounterWithoutBattle, selectPokemonForOption, setEncounterExp, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import Pokemon, { PlayerPokemon } from "#app/field/pokemon"; +import { modifierTypes } from "#app/modifier/modifier-type"; +import { randSeedInt } from "#app/utils"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { Species } from "#enums/species"; +import BattleScene from "#app/battle-scene"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { MoneyRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; +import { getEncounterText, queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { applyDamageToPokemon, applyModifierTypeToPlayerPokemon, isPokemonValidForEncounterOptionSelection } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { Nature } from "#enums/nature"; +import { getNatureName } from "#app/data/nature"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; +import i18next from "i18next"; + +/** the i18n namespace for this encounter */ +const namespace = "mysteryEncounter:shadyVitaminDealer"; + +const VITAMIN_DEALER_CHEAP_PRICE_MULTIPLIER = 1.5; +const VITAMIN_DEALER_EXPENSIVE_PRICE_MULTIPLIER = 3.5; + +/** + * Shady Vitamin Dealer encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3798 | GitHub Issue #3798} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const ShadyVitaminDealerEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.SHADY_VITAMIN_DEALER) + .withEncounterTier(MysteryEncounterTier.COMMON) + .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + .withSceneRequirement(new MoneyRequirement(0, VITAMIN_DEALER_CHEAP_PRICE_MULTIPLIER)) // Must have the money for at least the cheap deal + .withPrimaryPokemonHealthRatioRequirement([0.51, 1]) // At least 1 Pokemon must have above half HP + .withIntroSpriteConfigs([ + { + spriteKey: Species.KROOKODILE.toString(), + fileRoot: "pokemon", + hasShadow: true, + repeat: true, + x: 12, + y: -5, + yShadow: -5 + }, + { + spriteKey: "shady_vitamin_dealer", + fileRoot: "mystery-encounters", + hasShadow: true, + x: -12, + y: 3, + yShadow: 3 + }, + ]) + .withIntroDialogue([ + { + text: `${namespace}.intro`, + }, + { + text: `${namespace}.intro_dialogue`, + speaker: `${namespace}.speaker`, + }, + ]) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) + .withSceneMoneyRequirement(0, VITAMIN_DEALER_CHEAP_PRICE_MULTIPLIER) + .withDialogue({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.selected`, + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene): Promise => { + const encounter = scene.currentBattle.mysteryEncounter!; + const onPokemonSelected = (pokemon: PlayerPokemon) => { + // Update money + updatePlayerMoney(scene, -(encounter.options[0].requirements[0] as MoneyRequirement).requiredMoney); + // Calculate modifiers and dialogue tokens + const modifiers = [ + generateModifierType(scene, modifierTypes.BASE_STAT_BOOSTER)!, + generateModifierType(scene, modifierTypes.BASE_STAT_BOOSTER)!, + ]; + encounter.setDialogueToken("boost1", modifiers[0].name); + encounter.setDialogueToken("boost2", modifiers[1].name); + encounter.misc = { + chosenPokemon: pokemon, + modifiers: modifiers, + }; + }; + + // Only Pokemon that can gain benefits are above half HP with no status + const selectableFilter = (pokemon: Pokemon) => { + // If pokemon meets primary pokemon reqs, it can be selected + if (!pokemon.isAllowed()) { + return i18next.t("partyUiHandler:cantBeUsed", { pokemonName: pokemon.getNameToRender() }) ?? null; + } + if (!encounter.pokemonMeetsPrimaryRequirements(scene, pokemon)) { + return getEncounterText(scene, `${namespace}.invalid_selection`) ?? null; + } + + return null; + }; + + return selectPokemonForOption(scene, onPokemonSelected, undefined, selectableFilter); + }) + .withOptionPhase(async (scene: BattleScene) => { + // Choose Cheap Option + const encounter = scene.currentBattle.mysteryEncounter!; + const chosenPokemon = encounter.misc.chosenPokemon; + const modifiers = encounter.misc.modifiers; + + for (const modType of modifiers) { + await applyModifierTypeToPlayerPokemon(scene, chosenPokemon, modType); + } + + leaveEncounterWithoutBattle(scene, true); + }) + .withPostOptionPhase(async (scene: BattleScene) => { + // Damage and status applied after dealer leaves (to make thematic sense) + const encounter = scene.currentBattle.mysteryEncounter!; + const chosenPokemon = encounter.misc.chosenPokemon as PlayerPokemon; + + // Pokemon takes half max HP damage and nature is randomized (does not update dex) + applyDamageToPokemon(scene, chosenPokemon, Math.floor(chosenPokemon.getMaxHp() / 2)); + + const currentNature = chosenPokemon.nature; + let newNature = randSeedInt(25) as Nature; + while (newNature === currentNature) { + newNature = randSeedInt(25) as Nature; + } + + chosenPokemon.nature = newNature; + encounter.setDialogueToken("newNature", getNatureName(newNature)); + queueEncounterMessage(scene, `${namespace}.cheap_side_effects`); + setEncounterExp(scene, [chosenPokemon.id], 100); + chosenPokemon.updateInfo(); + }) + .build() + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) + .withSceneMoneyRequirement(0, VITAMIN_DEALER_EXPENSIVE_PRICE_MULTIPLIER) + .withDialogue({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + text: `${namespace}.option.selected`, + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene): Promise => { + const encounter = scene.currentBattle.mysteryEncounter!; + const onPokemonSelected = (pokemon: PlayerPokemon) => { + // Update money + updatePlayerMoney(scene, -(encounter.options[1].requirements[0] as MoneyRequirement).requiredMoney); + // Calculate modifiers and dialogue tokens + const modifiers = [ + generateModifierType(scene, modifierTypes.BASE_STAT_BOOSTER)!, + generateModifierType(scene, modifierTypes.BASE_STAT_BOOSTER)!, + ]; + encounter.setDialogueToken("boost1", modifiers[0].name); + encounter.setDialogueToken("boost2", modifiers[1].name); + encounter.misc = { + chosenPokemon: pokemon, + modifiers: modifiers, + }; + }; + + // Only Pokemon that can gain benefits are unfainted + const selectableFilter = (pokemon: Pokemon) => { + return isPokemonValidForEncounterOptionSelection(pokemon, scene, `${namespace}.invalid_selection`); + }; + + return selectPokemonForOption(scene, onPokemonSelected, undefined, selectableFilter); + }) + .withOptionPhase(async (scene: BattleScene) => { + // Choose Expensive Option + const encounter = scene.currentBattle.mysteryEncounter!; + const chosenPokemon = encounter.misc.chosenPokemon; + const modifiers = encounter.misc.modifiers; + + for (const modType of modifiers) { + await applyModifierTypeToPlayerPokemon(scene, chosenPokemon, modType); + } + + leaveEncounterWithoutBattle(scene, true); + }) + .withPostOptionPhase(async (scene: BattleScene) => { + // Status applied after dealer leaves (to make thematic sense) + const encounter = scene.currentBattle.mysteryEncounter!; + const chosenPokemon = encounter.misc.chosenPokemon; + + queueEncounterMessage(scene, `${namespace}.no_bad_effects`); + setEncounterExp(scene, [chosenPokemon.id], 100); + + chosenPokemon.updateInfo(); + }) + .build() + ) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + selected: [ + { + text: `${namespace}.option.3.selected`, + speaker: `${namespace}.speaker` + } + ] + }, + async (scene: BattleScene) => { + // Leave encounter with no rewards or exp + leaveEncounterWithoutBattle(scene, true); + return true; + } + ) + .build(); diff --git a/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts b/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts new file mode 100644 index 00000000000..bfccc46ee0f --- /dev/null +++ b/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts @@ -0,0 +1,165 @@ +import { STEALING_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups"; +import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { Species } from "#enums/species"; +import BattleScene from "#app/battle-scene"; +import { StatusEffect } from "#app/data/status-effect"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { MoveRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; +import { EnemyPartyConfig, EnemyPokemonConfig, generateModifierType, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, loadCustomMovesForEncounter, setEncounterExp, setEncounterRewards, } from "../utils/encounter-phase-utils"; +import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { Moves } from "#enums/moves"; +import { BattlerIndex } from "#app/battle"; +import { AiType, PokemonMove } from "#app/field/pokemon"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { PartyHealPhase } from "#app/phases/party-heal-phase"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; +import { BerryType } from "#enums/berry-type"; +import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data"; + +/** i18n namespace for the encounter */ +const namespace = "mysteryEncounter:slumberingSnorlax"; + +/** + * Sleeping Snorlax encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3815 | GitHub Issue #3815} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const SlumberingSnorlaxEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.SLUMBERING_SNORLAX) + .withEncounterTier(MysteryEncounterTier.GREAT) + .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + .withCatchAllowed(true) + .withHideWildIntroMessage(true) + .withIntroSpriteConfigs([ + { + spriteKey: Species.SNORLAX.toString(), + fileRoot: "pokemon", + hasShadow: true, + tint: 0.25, + scale: 1.25, + repeat: true, + y: 5, + }, + ]) + .withIntroDialogue([ + { + text: `${namespace}.intro`, + }, + ]) + .withOnInit((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + console.log(encounter); + + // Calculate boss mon + const bossSpecies = getPokemonSpecies(Species.SNORLAX); + const pokemonConfig: EnemyPokemonConfig = { + species: bossSpecies, + isBoss: true, + status: [StatusEffect.SLEEP, 5], // Extra turns on timer for Snorlax's start of fight moves + moveSet: [Moves.REST, Moves.SLEEP_TALK, Moves.CRUNCH, Moves.GIGA_IMPACT], + modifierConfigs: [ + { + modifier: generateModifierType(scene, modifierTypes.BERRY, [BerryType.SITRUS]) as PokemonHeldItemModifierType, + stackCount: 2 + }, + { + modifier: generateModifierType(scene, modifierTypes.BERRY, [BerryType.ENIGMA]) as PokemonHeldItemModifierType, + stackCount: 2 + }, + ], + mysteryEncounterPokemonData: new MysteryEncounterPokemonData({ spriteScale: 1.25 }), + aiType: AiType.SMART // Required to ensure Snorlax uses Sleep Talk while it is asleep + }; + const config: EnemyPartyConfig = { + levelAdditiveModifier: 0.5, + pokemonConfigs: [pokemonConfig], + }; + encounter.enemyPartyConfigs = [config]; + + // Load animations/sfx for Snorlax fight start moves + loadCustomMovesForEncounter(scene, [Moves.SNORE]); + + encounter.setDialogueToken("snorlaxName", getPokemonSpecies(Species.SNORLAX).getName()); + + return true; + }) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Pick battle + const encounter = scene.currentBattle.mysteryEncounter!; + setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.LEFTOVERS], fillRemaining: true}); + encounter.startOfBattleEffects.push( + { + sourceBattlerIndex: BattlerIndex.ENEMY, + targets: [BattlerIndex.PLAYER], + move: new PokemonMove(Moves.SNORE), + ignorePp: true + }, + { + sourceBattlerIndex: BattlerIndex.ENEMY, + targets: [BattlerIndex.PLAYER], + move: new PokemonMove(Moves.SNORE), + ignorePp: true + }); + await initBattleWithEnemyConfig(scene, encounter.enemyPartyConfigs[0]); + } + ) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Fall asleep waiting for Snorlax + // Full heal party + scene.unshiftPhase(new PartyHealPhase(scene, true)); + queueEncounterMessage(scene, `${namespace}.option.2.rest_result`); + leaveEncounterWithoutBattle(scene); + } + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL) + .withPrimaryPokemonRequirement(new MoveRequirement(STEALING_MOVES)) + .withDialogue({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + disabledButtonTooltip: `${namespace}.option.3.disabled_tooltip`, + selected: [ + { + text: `${namespace}.option.3.selected` + } + ] + }) + .withOptionPhase(async (scene: BattleScene) => { + // Steal the Snorlax's Leftovers + const instance = scene.currentBattle.mysteryEncounter!; + setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.LEFTOVERS], fillRemaining: false }); + // Snorlax exp to Pokemon that did the stealing + setEncounterExp(scene, instance.primaryPokemon!.id, getPokemonSpecies(Species.SNORLAX).baseExp); + leaveEncounterWithoutBattle(scene); + }) + .build() + ) + .build(); diff --git a/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts b/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts new file mode 100644 index 00000000000..c35817255e0 --- /dev/null +++ b/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts @@ -0,0 +1,244 @@ +import { EnemyPartyConfig, generateModifierTypeOption, initBattleWithEnemyConfig, setEncounterExp, setEncounterRewards, transitionMysteryEncounterIntroVisuals, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { randSeedInt } from "#app/utils"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import BattleScene from "#app/battle-scene"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MoneyRequirement, WaveModulusRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; +import Pokemon, { EnemyPokemon } from "#app/field/pokemon"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import PokemonData from "#app/system/pokemon-data"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { Biome } from "#enums/biome"; +import { getBiomeKey } from "#app/field/arena"; +import { Type } from "#app/data/type"; +import { getPartyLuckValue, modifierTypes } from "#app/modifier/modifier-type"; +import { TrainerSlot } from "#app/data/trainer-config"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { getPokemonNameWithAffix } from "#app/messages"; +import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; +import { Stat } from "#enums/stat"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; +import { getEncounterPokemonLevelForWave, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; + +/** the i18n namespace for this encounter */ +const namespace = "mysteryEncounter:teleportingHijinks"; + +const MONEY_COST_MULTIPLIER = 1.75; +const BIOME_CANDIDATES = [Biome.SPACE, Biome.FAIRY_CAVE, Biome.LABORATORY, Biome.ISLAND, Biome.WASTELAND, Biome.DOJO]; +const MACHINE_INTERFACING_TYPES = [Type.ELECTRIC, Type.STEEL]; + +/** + * Teleporting Hijinks encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3817 | GitHub Issue #3817} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const TeleportingHijinksEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.TELEPORTING_HIJINKS) + .withEncounterTier(MysteryEncounterTier.COMMON) + .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + .withSceneRequirement(new WaveModulusRequirement([1, 2, 3], 10)) // Must be in first 3 waves after boss wave + .withSceneRequirement(new MoneyRequirement(undefined, MONEY_COST_MULTIPLIER)) // Must be able to pay teleport cost + .withAutoHideIntroVisuals(false) + .withCatchAllowed(true) + .withIntroSpriteConfigs([ + { + spriteKey: "teleporting_hijinks_teleporter", + fileRoot: "mystery-encounters", + hasShadow: true, + x: 4, + y: 4, + yShadow: 1 + } + ]) + .withIntroDialogue([ + { + text: `${namespace}.intro`, + } + ]) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withOnInit((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + const price = scene.getWaveMoneyAmount(MONEY_COST_MULTIPLIER); + encounter.setDialogueToken("price", price.toString()); + encounter.misc = { + price + }; + + return true; + }) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) + .withSceneMoneyRequirement(undefined, MONEY_COST_MULTIPLIER) // Must be able to pay teleport cost + .withDialogue({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + } + ], + }) + .withPreOptionPhase(async (scene: BattleScene) => { + // Update money + updatePlayerMoney(scene, -scene.currentBattle.mysteryEncounter!.misc.price, true, false); + }) + .withOptionPhase(async (scene: BattleScene) => { + const config: EnemyPartyConfig = await doBiomeTransitionDialogueAndBattleInit(scene); + setEncounterRewards(scene, { fillRemaining: true }); + await initBattleWithEnemyConfig(scene, config); + }) + .build() + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL) + .withPokemonTypeRequirement(MACHINE_INTERFACING_TYPES, true, 1) // Must have Steel or Electric type + .withDialogue({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + disabledButtonTooltip: `${namespace}.option.2.disabled_tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected`, + } + ], + }) + .withOptionPhase(async (scene: BattleScene) => { + const config: EnemyPartyConfig = await doBiomeTransitionDialogueAndBattleInit(scene); + setEncounterRewards(scene, { fillRemaining: true }); + setEncounterExp(scene, scene.currentBattle.mysteryEncounter!.selectedOption!.primaryPokemon!.id, 100); + await initBattleWithEnemyConfig(scene, config); + }) + .build() + ) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + selected: [ + { + text: `${namespace}.option.3.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Inspect the Machine + const encounter = scene.currentBattle.mysteryEncounter!; + + // Init enemy + const level = getEncounterPokemonLevelForWave(scene, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER); + const bossSpecies = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getParty()), true); + const bossPokemon = new EnemyPokemon(scene, bossSpecies, level, TrainerSlot.NONE, true); + encounter.setDialogueToken("enemyPokemon", getPokemonNameWithAffix(bossPokemon)); + const config: EnemyPartyConfig = { + pokemonConfigs: [{ + level: level, + species: bossSpecies, + dataSource: new PokemonData(bossPokemon), + isBoss: true, + }], + }; + + const magnet = generateModifierTypeOption(scene, modifierTypes.ATTACK_TYPE_BOOSTER, [Type.STEEL])!; + const metalCoat = generateModifierTypeOption(scene, modifierTypes.ATTACK_TYPE_BOOSTER, [Type.ELECTRIC])!; + setEncounterRewards(scene, { guaranteedModifierTypeOptions: [magnet, metalCoat], fillRemaining: true }); + transitionMysteryEncounterIntroVisuals(scene, true, true); + await initBattleWithEnemyConfig(scene, config); + } + ) + .build(); + +async function doBiomeTransitionDialogueAndBattleInit(scene: BattleScene) { + const encounter = scene.currentBattle.mysteryEncounter!; + + // Calculate new biome (cannot be current biome) + const filteredBiomes = BIOME_CANDIDATES.filter(b => scene.arena.biomeType !== b); + const newBiome = filteredBiomes[randSeedInt(filteredBiomes.length)]; + + // Show dialogue and transition biome + await showEncounterText(scene, `${namespace}.transport`); + await Promise.all([animateBiomeChange(scene, newBiome), transitionMysteryEncounterIntroVisuals(scene)]); + scene.playBgm(); + await showEncounterText(scene, `${namespace}.attacked`); + + // Init enemy + const level = getEncounterPokemonLevelForWave(scene, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER); + const bossSpecies = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getParty()), true); + const bossPokemon = new EnemyPokemon(scene, bossSpecies, level, TrainerSlot.NONE, true); + encounter.setDialogueToken("enemyPokemon", getPokemonNameWithAffix(bossPokemon)); + + // Defense/Spd buffs below wave 50, Atk/Def/Spd buffs otherwise + const statChangesForBattle: (Stat.ATK | Stat.DEF | Stat.SPATK | Stat.SPDEF | Stat.SPD | Stat.ACC | Stat.EVA)[] = scene.currentBattle.waveIndex < 50 ? + [Stat.DEF, Stat.SPDEF, Stat.SPD] : + [Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD]; + + const config: EnemyPartyConfig = { + pokemonConfigs: [{ + level: level, + species: bossSpecies, + dataSource: new PokemonData(bossPokemon), + isBoss: true, + tags: [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON], + mysteryEncounterBattleEffects: (pokemon: Pokemon) => { + queueEncounterMessage(pokemon.scene, `${namespace}.boss_enraged`); + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, statChangesForBattle, 1)); + } + }], + }; + + return config; +} + +async function animateBiomeChange(scene: BattleScene, nextBiome: Biome) { + return new Promise(resolve => { + scene.tweens.add({ + targets: [scene.arenaEnemy, scene.lastEnemyTrainer], + x: "+=300", + duration: 2000, + onComplete: () => { + scene.newArena(nextBiome); + + const biomeKey = getBiomeKey(nextBiome); + const bgTexture = `${biomeKey}_bg`; + scene.arenaBgTransition.setTexture(bgTexture); + scene.arenaBgTransition.setAlpha(0); + scene.arenaBgTransition.setVisible(true); + scene.arenaPlayerTransition.setBiome(nextBiome); + scene.arenaPlayerTransition.setAlpha(0); + scene.arenaPlayerTransition.setVisible(true); + + scene.tweens.add({ + targets: [scene.arenaPlayer, scene.arenaBgTransition, scene.arenaPlayerTransition], + duration: 1000, + ease: "Sine.easeInOut", + alpha: (target: any) => target === scene.arenaPlayer ? 0 : 1, + onComplete: () => { + scene.arenaBg.setTexture(bgTexture); + scene.arenaPlayer.setBiome(nextBiome); + scene.arenaPlayer.setAlpha(1); + scene.arenaEnemy.setBiome(nextBiome); + scene.arenaEnemy.setAlpha(1); + scene.arenaNextEnemy.setBiome(nextBiome); + scene.arenaBgTransition.setVisible(false); + scene.arenaPlayerTransition.setVisible(false); + if (scene.lastEnemyTrainer) { + scene.lastEnemyTrainer.destroy(); + } + + resolve(); + + scene.tweens.add({ + targets: scene.arenaEnemy, + x: "-=300", + }); + } + }); + } + }); + }); +} 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 new file mode 100644 index 00000000000..91aeea79111 --- /dev/null +++ b/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts @@ -0,0 +1,616 @@ +import { EnemyPartyConfig, generateModifierType, handleMysteryEncounterBattleFailed, initBattleWithEnemyConfig, setEncounterRewards, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { trainerConfigs } from "#app/data/trainer-config"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import BattleScene from "#app/battle-scene"; +import { randSeedShuffle } from "#app/utils"; +import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; +import { Biome } from "#enums/biome"; +import { TrainerType } from "#enums/trainer-type"; +import i18next from "i18next"; +import { Species } from "#enums/species"; +import { getPokemonSpecies, speciesStarters } from "#app/data/pokemon-species"; +import { Nature } from "#enums/nature"; +import { Moves } from "#enums/moves"; +import { PlayerPokemon } from "#app/field/pokemon"; +import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { IEggOptions } from "#app/data/egg"; +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 { achvs } from "#app/system/achv"; +import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; +import { Type } from "#app/data/type"; +import { getPokeballTintColor } from "#app/data/pokeball"; + +/** the i18n namespace for the encounter */ +const namespace = "mysteryEncounter:expertPokemonBreeder"; + +const trainerNameKey = "trainerNames:expert_pokemon_breeder"; + +const FIRST_STAGE_EVOLUTION_WAVE = 45; +const SECOND_STAGE_EVOLUTION_WAVE = 60; +const FINAL_STAGE_EVOLUTION_WAVE = 75; + +const FRIENDSHIP_ADDED = 20; + +class BreederSpeciesEvolution { + species: Species; + evolution: number; + + constructor(species: Species, evolution: number) { + this.species = species; + this.evolution = evolution; + } +} + +const POOL_1_POKEMON: (Species | BreederSpeciesEvolution)[][] = [ + [Species.MUNCHLAX, new BreederSpeciesEvolution(Species.SNORLAX, SECOND_STAGE_EVOLUTION_WAVE)], + [Species.HAPPINY, new BreederSpeciesEvolution(Species.CHANSEY, FIRST_STAGE_EVOLUTION_WAVE), new BreederSpeciesEvolution(Species.BLISSEY, FINAL_STAGE_EVOLUTION_WAVE)], + [Species.MAGBY, new BreederSpeciesEvolution(Species.MAGMAR, FIRST_STAGE_EVOLUTION_WAVE), new BreederSpeciesEvolution(Species.MAGMORTAR, FINAL_STAGE_EVOLUTION_WAVE)], + [Species.ELEKID, new BreederSpeciesEvolution(Species.ELECTABUZZ, FIRST_STAGE_EVOLUTION_WAVE), new BreederSpeciesEvolution(Species.ELECTIVIRE, FINAL_STAGE_EVOLUTION_WAVE)], + [Species.RIOLU, new BreederSpeciesEvolution(Species.LUCARIO, SECOND_STAGE_EVOLUTION_WAVE)], + [Species.BUDEW, new BreederSpeciesEvolution(Species.ROSELIA, FIRST_STAGE_EVOLUTION_WAVE), new BreederSpeciesEvolution(Species.ROSERADE, FINAL_STAGE_EVOLUTION_WAVE)], + [Species.TOXEL, new BreederSpeciesEvolution(Species.TOXTRICITY, SECOND_STAGE_EVOLUTION_WAVE)], + [Species.MIME_JR, new BreederSpeciesEvolution(Species.GALAR_MR_MIME, FIRST_STAGE_EVOLUTION_WAVE), new BreederSpeciesEvolution(Species.MR_RIME, FINAL_STAGE_EVOLUTION_WAVE)] +]; + +const POOL_2_POKEMON: (Species | BreederSpeciesEvolution)[][] = [ + [Species.PICHU, new BreederSpeciesEvolution(Species.PIKACHU, FIRST_STAGE_EVOLUTION_WAVE), new BreederSpeciesEvolution(Species.RAICHU, FINAL_STAGE_EVOLUTION_WAVE)], + [Species.PICHU, new BreederSpeciesEvolution(Species.PIKACHU, FIRST_STAGE_EVOLUTION_WAVE), new BreederSpeciesEvolution(Species.ALOLA_RAICHU, FINAL_STAGE_EVOLUTION_WAVE)], + [Species.JYNX], + [Species.TYROGUE, new BreederSpeciesEvolution(Species.HITMONLEE, SECOND_STAGE_EVOLUTION_WAVE)], + [Species.TYROGUE, new BreederSpeciesEvolution(Species.HITMONCHAN, SECOND_STAGE_EVOLUTION_WAVE)], + [Species.TYROGUE, new BreederSpeciesEvolution(Species.HITMONTOP, SECOND_STAGE_EVOLUTION_WAVE)], + [Species.IGGLYBUFF, new BreederSpeciesEvolution(Species.JIGGLYPUFF, FIRST_STAGE_EVOLUTION_WAVE), new BreederSpeciesEvolution(Species.WIGGLYTUFF, FINAL_STAGE_EVOLUTION_WAVE)], + [Species.AZURILL, new BreederSpeciesEvolution(Species.MARILL, FIRST_STAGE_EVOLUTION_WAVE), new BreederSpeciesEvolution(Species.AZUMARILL, FINAL_STAGE_EVOLUTION_WAVE)], + [Species.WYNAUT, new BreederSpeciesEvolution(Species.WOBBUFFET, SECOND_STAGE_EVOLUTION_WAVE)], + [Species.CHINGLING, new BreederSpeciesEvolution(Species.CHIMECHO, SECOND_STAGE_EVOLUTION_WAVE)], + [Species.BONSLY, new BreederSpeciesEvolution(Species.SUDOWOODO, SECOND_STAGE_EVOLUTION_WAVE)], + [Species.MANTYKE, new BreederSpeciesEvolution(Species.MANTINE, SECOND_STAGE_EVOLUTION_WAVE)] +]; + +/** + * The Expert Pokémon Breeder encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3818 | GitHub Issue #3818} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const TheExpertPokemonBreederEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER) + .withEncounterTier(MysteryEncounterTier.ULTRA) + .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + .withScenePartySizeRequirement(4, 6, true) // Must have at least 4 legal pokemon in party + .withIntroSpriteConfigs([]) // These are set in onInit() + .withIntroDialogue([ + { + text: `${namespace}.intro`, + }, + { + speaker: trainerNameKey, + text: `${namespace}.intro_dialogue`, + }, + ]) + .withOnInit((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + const waveIndex = scene.currentBattle.waveIndex; + // Calculates what trainers are available for battle in the encounter + + // If player is in space biome, uses special "Space" version of the trainer + encounter.enemyPartyConfigs = [ + getPartyConfig(scene) + ]; + + const cleffaSpecies = waveIndex < FIRST_STAGE_EVOLUTION_WAVE ? Species.CLEFFA : waveIndex < FINAL_STAGE_EVOLUTION_WAVE ? Species.CLEFAIRY : Species.CLEFABLE; + encounter.spriteConfigs = [ + { + spriteKey: cleffaSpecies.toString(), + fileRoot: "pokemon", + hasShadow: true, + repeat: true, + x: 14, + y: -2, + yShadow: -2 + }, + { + spriteKey: "expert_pokemon_breeder", + fileRoot: "trainer", + hasShadow: true, + x: -14, + y: 4, + yShadow: 2 + }, + ]; + + // Determine the 3 pokemon the player can battle with + let partyCopy = scene.getParty().slice(0); + partyCopy = partyCopy + .filter(p => p.isAllowedInBattle()) + .sort((a, b) => a.friendship - b.friendship); + + const pokemon1 = partyCopy[0]; + const pokemon2 = partyCopy[1]; + const pokemon3 = partyCopy[2]; + encounter.setDialogueToken("pokemon1Name", pokemon1.getNameToRender()); + encounter.setDialogueToken("pokemon2Name", pokemon2.getNameToRender()); + encounter.setDialogueToken("pokemon3Name", pokemon3.getNameToRender()); + + // Dialogue and egg calcs for Pokemon 1 + const [pokemon1CommonEggs, pokemon1RareEggs] = calculateEggRewardsForPokemon(pokemon1); + let pokemon1Tooltip = getEncounterText(scene, `${namespace}.option.1.tooltip_base`)!; + if (pokemon1RareEggs > 0) { + const eggsText = i18next.t(`${namespace}.numEggs`, { count: pokemon1RareEggs, rarity: i18next.t("egg:greatTier") }); + pokemon1Tooltip += i18next.t(`${namespace}.eggs_tooltip`, { eggs: eggsText }); + encounter.setDialogueToken("pokemon1RareEggs", eggsText); + } + if (pokemon1CommonEggs > 0) { + const eggsText = i18next.t(`${namespace}.numEggs`, { count: pokemon1CommonEggs, rarity: i18next.t("egg:defaultTier") }); + pokemon1Tooltip += i18next.t(`${namespace}.eggs_tooltip`, { eggs: eggsText }); + encounter.setDialogueToken("pokemon1CommonEggs", eggsText); + } + encounter.options[0].dialogue!.buttonTooltip = pokemon1Tooltip; + + // Dialogue and egg calcs for Pokemon 2 + const [pokemon2CommonEggs, pokemon2RareEggs] = calculateEggRewardsForPokemon(pokemon2); + let pokemon2Tooltip = getEncounterText(scene, `${namespace}.option.2.tooltip_base`)!; + if (pokemon2RareEggs > 0) { + const eggsText = i18next.t(`${namespace}.numEggs`, { count: pokemon2RareEggs, rarity: i18next.t("egg:greatTier") }); + pokemon2Tooltip += i18next.t(`${namespace}.eggs_tooltip`, { eggs: eggsText }); + encounter.setDialogueToken("pokemon2RareEggs", eggsText); + } + if (pokemon2CommonEggs > 0) { + const eggsText = i18next.t(`${namespace}.numEggs`, { count: pokemon2CommonEggs, rarity: i18next.t("egg:defaultTier") }); + pokemon2Tooltip += i18next.t(`${namespace}.eggs_tooltip`, { eggs: eggsText }); + encounter.setDialogueToken("pokemon1CommonEggs", eggsText); + } + encounter.options[1].dialogue!.buttonTooltip = pokemon2Tooltip; + + // Dialogue and egg calcs for Pokemon 3 + const [pokemon3CommonEggs, pokemon3RareEggs] = calculateEggRewardsForPokemon(pokemon3); + let pokemon3Tooltip = getEncounterText(scene, `${namespace}.option.3.tooltip_base`)!; + if (pokemon3RareEggs > 0) { + const eggsText = i18next.t(`${namespace}.numEggs`, { count: pokemon3RareEggs, rarity: i18next.t("egg:greatTier") }); + pokemon3Tooltip += i18next.t(`${namespace}.eggs_tooltip`, { eggs: eggsText }); + encounter.setDialogueToken("pokemon3RareEggs", eggsText); + } + if (pokemon3CommonEggs > 0) { + const eggsText = i18next.t(`${namespace}.numEggs`, { count: pokemon3CommonEggs, rarity: i18next.t("egg:defaultTier") }); + pokemon3Tooltip += i18next.t(`${namespace}.eggs_tooltip`, { eggs: eggsText }); + encounter.setDialogueToken("pokemon3CommonEggs", eggsText); + } + encounter.options[2].dialogue!.buttonTooltip = pokemon3Tooltip; + + encounter.misc = { + pokemon1, + pokemon1CommonEggs, + pokemon1RareEggs, + pokemon2, + pokemon2CommonEggs, + pokemon2RareEggs, + pokemon3, + pokemon3CommonEggs, + pokemon3RareEggs + }; + + return true; + }) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.option.1.label`, + selected: [ + { + speaker: trainerNameKey, + text: `${namespace}.option.selected`, + }, + ], + }) + .withOptionPhase(async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + // Spawn battle with first pokemon + const config: EnemyPartyConfig = encounter.enemyPartyConfigs[0]; + + const { pokemon1, pokemon1CommonEggs, pokemon1RareEggs } = encounter.misc; + encounter.misc.chosenPokemon = pokemon1; + encounter.setDialogueToken("chosenPokemon", pokemon1.getNameToRender()); + const eggOptions = getEggOptions(scene, pokemon1CommonEggs, pokemon1RareEggs); + setEncounterRewards(scene, { fillRemaining: true }, eggOptions); + + // Remove all Pokemon from the party except the chosen Pokemon + removePokemonFromPartyAndStoreHeldItems(scene, encounter, pokemon1); + + // Configure outro dialogue for egg rewards + encounter.dialogue.outro = [ + { + speaker: trainerNameKey, + text: `${namespace}.outro`, + }, + ]; + if (encounter.dialogueTokens.hasOwnProperty("pokemon1CommonEggs")) { + encounter.dialogue.outro.push({ + text: i18next.t(`${namespace}.gained_eggs`, { numEggs: encounter.dialogueTokens["pokemon1CommonEggs"] }), + }); + } + if (encounter.dialogueTokens.hasOwnProperty("pokemon1RareEggs")) { + encounter.dialogue.outro.push({ + text: i18next.t(`${namespace}.gained_eggs`, { numEggs: encounter.dialogueTokens["pokemon1RareEggs"] }), + }); + } + + encounter.onGameOver = onGameOver; + initBattleWithEnemyConfig(scene, config); + }) + .withPostOptionPhase(async (scene: BattleScene) => { + await doPostEncounterCleanup(scene); + }) + .build() + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.option.2.label`, + selected: [ + { + speaker: trainerNameKey, + text: `${namespace}.option.selected`, + }, + ], + }) + .withOptionPhase(async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + // Spawn battle with second pokemon + const config: EnemyPartyConfig = encounter.enemyPartyConfigs[0]; + + const { pokemon2, pokemon2CommonEggs, pokemon2RareEggs } = encounter.misc; + encounter.misc.chosenPokemon = pokemon2; + encounter.setDialogueToken("chosenPokemon", pokemon2.getNameToRender()); + const eggOptions = getEggOptions(scene, pokemon2CommonEggs, pokemon2RareEggs); + setEncounterRewards(scene, { fillRemaining: true }, eggOptions); + + // Remove all Pokemon from the party except the chosen Pokemon + removePokemonFromPartyAndStoreHeldItems(scene, encounter, pokemon2); + + // Configure outro dialogue for egg rewards + encounter.dialogue.outro = [ + { + speaker: trainerNameKey, + text: `${namespace}.outro`, + }, + ]; + if (encounter.dialogueTokens.hasOwnProperty("pokemon2CommonEggs")) { + encounter.dialogue.outro.push({ + text: i18next.t(`${namespace}.gained_eggs`, { numEggs: encounter.dialogueTokens["pokemon2CommonEggs"] }), + }); + } + if (encounter.dialogueTokens.hasOwnProperty("pokemon2RareEggs")) { + encounter.dialogue.outro.push({ + text: i18next.t(`${namespace}.gained_eggs`, { numEggs: encounter.dialogueTokens["pokemon2RareEggs"] }), + }); + } + + encounter.onGameOver = onGameOver; + initBattleWithEnemyConfig(scene, config); + }) + .withPostOptionPhase(async (scene: BattleScene) => { + await doPostEncounterCleanup(scene); + }) + .build() + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.option.3.label`, + selected: [ + { + speaker: trainerNameKey, + text: `${namespace}.option.selected`, + }, + ], + }) + .withOptionPhase(async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + // Spawn battle with third pokemon + const config: EnemyPartyConfig = encounter.enemyPartyConfigs[0]; + + const { pokemon3, pokemon3CommonEggs, pokemon3RareEggs } = encounter.misc; + encounter.misc.chosenPokemon = pokemon3; + encounter.setDialogueToken("chosenPokemon", pokemon3.getNameToRender()); + const eggOptions = getEggOptions(scene, pokemon3CommonEggs, pokemon3RareEggs); + setEncounterRewards(scene, { fillRemaining: true }, eggOptions); + + // Remove all Pokemon from the party except the chosen Pokemon + removePokemonFromPartyAndStoreHeldItems(scene, encounter, pokemon3); + + // Configure outro dialogue for egg rewards + encounter.dialogue.outro = [ + { + speaker: trainerNameKey, + text: `${namespace}.outro`, + }, + ]; + if (encounter.dialogueTokens.hasOwnProperty("pokemon3CommonEggs")) { + encounter.dialogue.outro.push({ + text: i18next.t(`${namespace}.gained_eggs`, { numEggs: encounter.dialogueTokens["pokemon3CommonEggs"] }), + }); + } + if (encounter.dialogueTokens.hasOwnProperty("pokemon3RareEggs")) { + encounter.dialogue.outro.push({ + text: i18next.t(`${namespace}.gained_eggs`, { numEggs: encounter.dialogueTokens["pokemon3RareEggs"] }), + }); + } + + encounter.onGameOver = onGameOver; + initBattleWithEnemyConfig(scene, config); + }) + .withPostOptionPhase(async (scene: BattleScene) => { + await doPostEncounterCleanup(scene); + }) + .build() + ) + .withOutroDialogue([ + { + speaker: trainerNameKey, + text: `${namespace}.outro`, + }, + ]) + .build(); + +function getPartyConfig(scene: BattleScene): EnemyPartyConfig { + // Bug type superfan trainer config + const waveIndex = scene.currentBattle.waveIndex; + const breederConfig = trainerConfigs[TrainerType.EXPERT_POKEMON_BREEDER].clone(); + breederConfig.name = i18next.t(trainerNameKey); + + // First mon is *always* this special cleffa + const cleffaSpecies = waveIndex < FIRST_STAGE_EVOLUTION_WAVE ? Species.CLEFFA : waveIndex < FINAL_STAGE_EVOLUTION_WAVE ? Species.CLEFAIRY : Species.CLEFABLE; + const baseConfig: EnemyPartyConfig = { + trainerType: TrainerType.EXPERT_POKEMON_BREEDER, + pokemonConfigs: [ + { + nickname: i18next.t(`${namespace}.cleffa_1_nickname`, { speciesName: getPokemonSpecies(cleffaSpecies).getName() }), + species: getPokemonSpecies(cleffaSpecies), + isBoss: false, + abilityIndex: 1, // Magic Guard + shiny: false, + nature: Nature.ADAMANT, + moveSet: [Moves.METEOR_MASH, Moves.FIRE_PUNCH, Moves.ICE_PUNCH, Moves.THUNDER_PUNCH], + ivs: [31, 31, 31, 31, 31, 31], + modifierConfigs: [ + { + modifier: generateModifierType(scene, modifierTypes.TERA_SHARD, [Type.STEEL]) as PokemonHeldItemModifierType, + } + ] + } + ] + }; + + if (scene.arena.biomeType === Biome.SPACE) { + // All 3 members always Cleffa line, but different configs + baseConfig.pokemonConfigs!.push({ + nickname: i18next.t(`${namespace}.cleffa_2_nickname`, { speciesName: getPokemonSpecies(cleffaSpecies).getName() }), + species: getPokemonSpecies(cleffaSpecies), + isBoss: false, + abilityIndex: 1, // Magic Guard + shiny: true, + variant: 1, + nature: Nature.MODEST, + moveSet: [Moves.MOONBLAST, Moves.MYSTICAL_FIRE, Moves.ICE_BEAM, Moves.THUNDERBOLT], + ivs: [31, 31, 31, 31, 31, 31] + }, + { + nickname: i18next.t(`${namespace}.cleffa_3_nickname`, { speciesName: getPokemonSpecies(cleffaSpecies).getName() }), + species: getPokemonSpecies(cleffaSpecies), + isBoss: false, + abilityIndex: 2, // Friend Guard / Unaware + shiny: true, + variant: 2, + nature: Nature.BOLD, + moveSet: [Moves.TRI_ATTACK, Moves.STORED_POWER, Moves.TAKE_HEART, Moves.MOONLIGHT], + ivs: [31, 31, 31, 31, 31, 31] + }); + } else { + // Second member from pool 1 + const pool1Species = getSpeciesFromPool(POOL_1_POKEMON, waveIndex); + // Third member from pool 2 + const pool2Species = getSpeciesFromPool(POOL_2_POKEMON, waveIndex); + + baseConfig.pokemonConfigs!.push({ + species: getPokemonSpecies(pool1Species), + isBoss: false, + ivs: [31, 31, 31, 31, 31, 31] + }, + { + species: getPokemonSpecies(pool2Species), + isBoss: false, + ivs: [31, 31, 31, 31, 31, 31] + }); + } + + return baseConfig; +} + +function getSpeciesFromPool(speciesPool: (Species | BreederSpeciesEvolution)[][], waveIndex: number): Species { + const poolCopy = speciesPool.slice(0); + randSeedShuffle(poolCopy); + const speciesEvolutions = poolCopy.pop()!.slice(0); + let speciesObject = speciesEvolutions.pop()!; + while (speciesObject instanceof BreederSpeciesEvolution && speciesObject.evolution > waveIndex) { + speciesObject = speciesEvolutions.pop()!; + } + return speciesObject instanceof BreederSpeciesEvolution ? speciesObject.species : speciesObject; +} + +function calculateEggRewardsForPokemon(pokemon: PlayerPokemon): [number, number] { + const bst = pokemon.calculateBaseStats().reduce((a, b) => a + b, 0); + // 1 point for every 20 points below 680 BST the pokemon is, (max 18, min 1) + const pointsFromBst = Math.min(Math.max(Math.floor((680 - bst) / 20), 1), 18); + + const rootSpecies = pokemon.species.getRootSpeciesId(true); + let pointsFromStarterTier = 0; + // 2 points for every 1 below 7 that the pokemon's starter tier is (max 12, min 0) + if (speciesStarters.hasOwnProperty(rootSpecies)) { + const starterTier = speciesStarters[rootSpecies]; + pointsFromStarterTier = Math.min(Math.max(Math.floor(7 - starterTier) * 2, 0), 12); + } + + // Maximum of 30 points + const totalPoints = Math.min(pointsFromStarterTier + pointsFromBst, 30); + + // 1 Rare egg for every 6 points + const numRares = Math.floor(totalPoints / 6); + // 1 Common egg for every point leftover + const numCommons = totalPoints % 6; + + return [numCommons, numRares]; +} + +function getEggOptions(scene: BattleScene, commonEggs: number, rareEggs: number) { + const eggDescription = i18next.t(`${namespace}.title`) + ":\n" + i18next.t(trainerNameKey); + const eggOptions: IEggOptions[] = []; + + if (commonEggs > 0) { + for (let i = 0; i < commonEggs; i++) { + eggOptions.push({ + scene, + pulled: false, + sourceType: EggSourceType.EVENT, + eggDescriptor: eggDescription, + tier: EggTier.COMMON + }); + } + } + if (rareEggs > 0) { + for (let i = 0; i < rareEggs; i++) { + eggOptions.push({ + scene, + pulled: false, + sourceType: EggSourceType.EVENT, + eggDescriptor: eggDescription, + tier: EggTier.GREAT + }); + } + } + + return eggOptions; +} + +function removePokemonFromPartyAndStoreHeldItems(scene: BattleScene, encounter: MysteryEncounter, chosenPokemon: PlayerPokemon) { + const party = scene.getParty(); + const chosenIndex = party.indexOf(chosenPokemon); + party[chosenIndex] = party[0]; + party[0] = chosenPokemon; + encounter.misc.originalParty = scene.getParty().slice(1); + encounter.misc.originalPartyHeldItems = encounter.misc.originalParty + .map(p => p.getHeldItems()); + scene["party"] = [ + chosenPokemon + ]; +} + +function checkAchievement(scene: BattleScene) { + if (scene.arena.biomeType === Biome.SPACE) { + scene.validateAchv(achvs.BREEDERS_IN_SPACE); + } +} + +async function restorePartyAndHeldItems(scene: BattleScene) { + const encounter = scene.currentBattle.mysteryEncounter!; + // Restore original party + scene.getParty().push(...encounter.misc.originalParty); + + // Restore held items + const originalHeldItems = encounter.misc.originalPartyHeldItems; + originalHeldItems.forEach(pokemonHeldItemsList => { + pokemonHeldItemsList.forEach(heldItem => { + scene.addModifier(heldItem, true, false, false, true); + }); + }); + await scene.updateModifiers(true); +} + +function onGameOver(scene: BattleScene) { + const encounter = scene.currentBattle.mysteryEncounter!; + + encounter.dialogue.outro = [ + { + speaker: trainerNameKey, + text: `${namespace}.outro_failed`, + }, + ]; + + // Restore original party, player loses all friendship with chosen mon (it remains fainted) + restorePartyAndHeldItems(scene); + const chosenPokemon = encounter.misc.chosenPokemon; + chosenPokemon.friendship = 0; + + // Clear all rewards that would have been earned + encounter.doEncounterRewards = undefined; + + // Set flag that encounter was failed + encounter.misc.encounterFailed = true; + + // Revert BGM + scene.playBgm(scene.arena.bgm); + + // Return enemy Pokemon + const pokemon = scene.getEnemyPokemon(); + if (pokemon) { + scene.playSound("se/pb_rel"); + pokemon.hideInfo(); + pokemon.tint(getPokeballTintColor(pokemon.pokeball), 1, 250, "Sine.easeIn"); + scene.tweens.add({ + targets: pokemon, + duration: 250, + ease: "Sine.easeIn", + scale: 0.5, + onComplete: () => { + scene.field.remove(pokemon, true); + } + }); + } + + // Show the enemy trainer + scene.time.delayedCall(250, () => { + const sprites = scene.currentBattle.trainer?.getSprites(); + const tintSprites = scene.currentBattle.trainer?.getTintSprites(); + if (sprites && tintSprites) { + for (let i = 0; i < sprites.length; i++) { + sprites[i].setVisible(true); + tintSprites[i].setVisible(true); + sprites[i].clearTint(); + tintSprites[i].clearTint(); + } + } + scene.tweens.add({ + targets: scene.currentBattle.trainer, + x: "-=16", + y: "+=16", + alpha: 1, + ease: "Sine.easeInOut", + duration: 750 + }); + }); + + + handleMysteryEncounterBattleFailed(scene, true); + + return false; +} + +async function doPostEncounterCleanup(scene: BattleScene) { + const encounter = scene.currentBattle.mysteryEncounter!; + if (!encounter.misc.encounterFailed) { + // Give achievement if in Space biome + checkAchievement(scene); + // Give 20 friendship to the chosen pokemon + encounter.misc.chosenPokemon.addFriendship(FRIENDSHIP_ADDED); + await restorePartyAndHeldItems(scene); + } +} diff --git a/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts b/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts new file mode 100644 index 00000000000..ba6a628f51e --- /dev/null +++ b/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts @@ -0,0 +1,163 @@ +import { leaveEncounterWithoutBattle, transitionMysteryEncounterIntroVisuals, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { isNullOrUndefined, randSeedInt } from "#app/utils"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import BattleScene from "#app/battle-scene"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MoneyRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; +import { catchPokemon, getRandomSpeciesByStarterTier, getSpriteKeysFromPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { getPokemonSpecies, speciesStarters } from "#app/data/pokemon-species"; +import { Species } from "#enums/species"; +import { PokeballType } from "#app/data/pokeball"; +import { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { showEncounterDialogue } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import PokemonData from "#app/system/pokemon-data"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; +import { Abilities } from "#enums/abilities"; + +/** the i18n namespace for this encounter */ +const namespace = "mysteryEncounter:pokemonSalesman"; + +const MAX_POKEMON_PRICE_MULTIPLIER = 4; + +/** Odds of shiny magikarp will be 1/value */ +const SHINY_MAGIKARP_WEIGHT = 100; + +/** + * Pokemon Salesman encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3799 | GitHub Issue #3799} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const ThePokemonSalesmanEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.THE_POKEMON_SALESMAN) + .withEncounterTier(MysteryEncounterTier.ULTRA) + .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + .withSceneRequirement(new MoneyRequirement(undefined, MAX_POKEMON_PRICE_MULTIPLIER)) // Some costs may not be as significant, this is the max you'd pay + .withAutoHideIntroVisuals(false) + .withIntroSpriteConfigs([ + { + spriteKey: "pokemon_salesman", + fileRoot: "mystery-encounters", + hasShadow: true + } + ]) + .withIntroDialogue([ + { + text: `${namespace}.intro`, + }, + { + text: `${namespace}.intro_dialogue`, + speaker: `${namespace}.speaker`, + }, + ]) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withOnInit((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + + let species = getPokemonSpecies(getRandomSpeciesByStarterTier([0, 5], undefined, undefined, false, false, false)); + const tries = 0; + + // Reroll any species that don't have HAs + while ((isNullOrUndefined(species.abilityHidden) || species.abilityHidden === Abilities.NONE) && tries < 5) { + species = getPokemonSpecies(getRandomSpeciesByStarterTier([0, 5], undefined, undefined, false, false, false)); + } + + let pokemon: PlayerPokemon; + if (randSeedInt(SHINY_MAGIKARP_WEIGHT) === 0 || isNullOrUndefined(species.abilityHidden) || species.abilityHidden === Abilities.NONE) { + // If no HA mon found or you roll 1%, give shiny Magikarp + species = getPokemonSpecies(Species.MAGIKARP); + const hiddenIndex = species.ability2 ? 2 : 1; + pokemon = new PlayerPokemon(scene, species, 5, hiddenIndex, species.formIndex, undefined, true); + } else { + const hiddenIndex = species.ability2 ? 2 : 1; + pokemon = new PlayerPokemon(scene, species, 5, hiddenIndex, species.formIndex); + } + pokemon.generateAndPopulateMoveset(); + + const { spriteKey, fileRoot } = getSpriteKeysFromPokemon(pokemon); + encounter.spriteConfigs.push({ + spriteKey: spriteKey, + fileRoot: fileRoot, + hasShadow: true, + repeat: true, + isPokemon: true + }); + + const starterTier = speciesStarters[species.speciesId]; + // Prices decrease by starter tier less than 5, but only reduces cost by half at max + let priceMultiplier = MAX_POKEMON_PRICE_MULTIPLIER * (Math.max(starterTier, 2.5) / 5); + if (pokemon.shiny) { + // Always max price for shiny (flip HA back to normal), and add special messaging + priceMultiplier = MAX_POKEMON_PRICE_MULTIPLIER; + pokemon.abilityIndex = 0; + encounter.dialogue.encounterOptionsDialogue!.description = `${namespace}.description_shiny`; + encounter.options[0].dialogue!.buttonTooltip = `${namespace}.option.1.tooltip_shiny`; + } + const price = scene.getWaveMoneyAmount(priceMultiplier); + encounter.setDialogueToken("purchasePokemon", pokemon.getNameToRender()); + encounter.setDialogueToken("price", price.toString()); + encounter.misc = { + price: price, + pokemon: pokemon + }; + + pokemon.calculateStats(); + + return true; + }) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) + .withHasDexProgress(true) + .withSceneMoneyRequirement(undefined, MAX_POKEMON_PRICE_MULTIPLIER) // Wave scaling money multiplier of 2 + .withDialogue({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected_message`, + } + ], + }) + .withOptionPhase(async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + const price = encounter.misc.price; + const purchasedPokemon = encounter.misc.pokemon as PlayerPokemon; + + // Update money + updatePlayerMoney(scene, -price, true, false); + + // Show dialogue + await showEncounterDialogue(scene, `${namespace}.option.1.selected_dialogue`, `${namespace}.speaker`); + await transitionMysteryEncounterIntroVisuals(scene); + + // "Catch" purchased pokemon + const data = new PokemonData(purchasedPokemon); + data.player = false; + await catchPokemon(scene, data.toPokemon(scene) as EnemyPokemon, null, PokeballType.POKEBALL, true, true); + + leaveEncounterWithoutBattle(scene, true); + }) + .build() + ) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Leave encounter with no rewards or exp + leaveEncounterWithoutBattle(scene, true); + return true; + } + ) + .build(); diff --git a/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts b/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts new file mode 100644 index 00000000000..55cb10644e8 --- /dev/null +++ b/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts @@ -0,0 +1,206 @@ +import { EnemyPartyConfig, initBattleWithEnemyConfig, loadCustomMovesForEncounter, leaveEncounterWithoutBattle, setEncounterRewards, transitionMysteryEncounterIntroVisuals, generateModifierType } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { modifierTypes, PokemonHeldItemModifierType, } from "#app/modifier/modifier-type"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import BattleScene from "#app/battle-scene"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { Species } from "#enums/species"; +import { Nature } from "#app/data/nature"; +import Pokemon, { PokemonMove } from "#app/field/pokemon"; +import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { modifyPlayerPokemonBST } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { Moves } from "#enums/moves"; +import { BattlerIndex } from "#app/battle"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { BerryType } from "#enums/berry-type"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data"; +import { Stat } from "#enums/stat"; +import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; + +/** the i18n namespace for the encounter */ +const namespace = "mysteryEncounter:theStrongStuff"; + +// Halved for HP stat +const HIGH_BST_REDUCTION_VALUE = 15; +const BST_INCREASE_VALUE = 10; + +/** + * The Strong Stuff encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3803 | GitHub Issue #3803} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const TheStrongStuffEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.THE_STRONG_STUFF) + .withEncounterTier(MysteryEncounterTier.GREAT) + .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + .withScenePartySizeRequirement(3, 6) // Must have at least 3 pokemon in party + .withMaxAllowedEncounters(1) + .withHideWildIntroMessage(true) + .withAutoHideIntroVisuals(false) + .withIntroSpriteConfigs([ + { + spriteKey: "berry_juice", + fileRoot: "items", + hasShadow: true, + isItem: true, + scale: 1.25, + x: -15, + y: 3, + disableAnimation: true + }, + { + spriteKey: Species.SHUCKLE.toString(), + fileRoot: "pokemon", + hasShadow: true, + repeat: true, + scale: 1.25, + x: 20, + y: 10, + yShadow: 7 + }, + ]) // Set in onInit() + .withIntroDialogue([ + { + text: `${namespace}.intro`, + }, + ]) + .withOnInit((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + + // Calculate boss mon + const config: EnemyPartyConfig = { + levelAdditiveModifier: 1, + disableSwitch: true, + pokemonConfigs: [ + { + species: getPokemonSpecies(Species.SHUCKLE), + isBoss: true, + bossSegments: 5, + mysteryEncounterPokemonData: new MysteryEncounterPokemonData({ spriteScale: 1.25 }), + nature: Nature.BOLD, + moveSet: [Moves.INFESTATION, Moves.SALT_CURE, Moves.GASTRO_ACID, Moves.HEAL_ORDER], + modifierConfigs: [ + { + modifier: generateModifierType(scene, modifierTypes.BERRY, [BerryType.SITRUS]) as PokemonHeldItemModifierType + }, + { + modifier: generateModifierType(scene, modifierTypes.BERRY, [BerryType.ENIGMA]) as PokemonHeldItemModifierType + }, + { + modifier: generateModifierType(scene, modifierTypes.BERRY, [BerryType.APICOT]) as PokemonHeldItemModifierType + }, + { + modifier: generateModifierType(scene, modifierTypes.BERRY, [BerryType.GANLON]) as PokemonHeldItemModifierType + }, + { + modifier: generateModifierType(scene, modifierTypes.BERRY, [BerryType.LUM]) as PokemonHeldItemModifierType, + stackCount: 2 + } + ], + tags: [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON], + mysteryEncounterBattleEffects: (pokemon: Pokemon) => { + queueEncounterMessage(pokemon.scene, `${namespace}.option.2.stat_boost`); + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [Stat.DEF, Stat.SPDEF], 2)); + } + } + ], + }; + + encounter.enemyPartyConfigs = [config]; + + loadCustomMovesForEncounter(scene, [Moves.GASTRO_ACID, Moves.STEALTH_ROCK]); + + encounter.setDialogueToken("shuckleName", getPokemonSpecies(Species.SHUCKLE).getName()); + + return true; + }) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected` + } + ] + }, + async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + // Do blackout and hide intro visuals during blackout + scene.time.delayedCall(750, () => { + transitionMysteryEncounterIntroVisuals(scene, true, true, 50); + }); + + // -15 to all base stats of highest BST (halved for HP), +10 to all base stats of rest of party (halved for HP) + // Sort party by bst + const sortedParty = scene.getParty().slice(0) + .sort((pokemon1, pokemon2) => { + const pokemon1Bst = pokemon1.calculateBaseStats().reduce((a, b) => a + b, 0); + const pokemon2Bst = pokemon2.calculateBaseStats().reduce((a, b) => a + b, 0); + return pokemon2Bst - pokemon1Bst; + }); + + sortedParty.forEach((pokemon, index) => { + if (index < 2) { + // -15 to the two highest BST mons + modifyPlayerPokemonBST(pokemon, -HIGH_BST_REDUCTION_VALUE); + encounter.setDialogueToken("highBstPokemon" + (index + 1), pokemon.getNameToRender()); + } else { + // +10 for the rest + modifyPlayerPokemonBST(pokemon, BST_INCREASE_VALUE); + } + }); + + encounter.setDialogueToken("reductionValue", HIGH_BST_REDUCTION_VALUE.toString()); + encounter.setDialogueToken("increaseValue", BST_INCREASE_VALUE.toString()); + await showEncounterText(scene, `${namespace}.option.1.selected_2`, null, undefined, true); + + encounter.dialogue.outro = [ + { + text: `${namespace}.outro`, + } + ]; + setEncounterRewards(scene, { fillRemaining: true }); + leaveEncounterWithoutBattle(scene, true); + return true; + } + ) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Pick battle + const encounter = scene.currentBattle.mysteryEncounter!; + setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.SOUL_DEW], fillRemaining: true }); + encounter.startOfBattleEffects.push( + { + sourceBattlerIndex: BattlerIndex.ENEMY, + targets: [BattlerIndex.PLAYER], + move: new PokemonMove(Moves.GASTRO_ACID), + ignorePp: true + }, + { + sourceBattlerIndex: BattlerIndex.ENEMY, + targets: [BattlerIndex.PLAYER], + move: new PokemonMove(Moves.STEALTH_ROCK), + ignorePp: true + }); + + encounter.dialogue.outro = []; + transitionMysteryEncounterIntroVisuals(scene, true, true, 500); + await initBattleWithEnemyConfig(scene, encounter.enemyPartyConfigs[0]); + } + ) + .build(); diff --git a/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts b/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts new file mode 100644 index 00000000000..60061efbc7a --- /dev/null +++ b/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts @@ -0,0 +1,510 @@ +import { EnemyPartyConfig, generateModifierType, generateModifierTypeOption, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, setEncounterRewards, transitionMysteryEncounterIntroVisuals, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import BattleScene from "#app/battle-scene"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { TrainerType } from "#enums/trainer-type"; +import { Species } from "#enums/species"; +import { Abilities } from "#enums/abilities"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { Moves } from "#enums/moves"; +import { Nature } from "#enums/nature"; +import { Type } from "#app/data/type"; +import { BerryType } from "#enums/berry-type"; +import { Stat } from "#enums/stat"; +import { SpeciesFormChangeManualTrigger } from "#app/data/pokemon-forms"; +import { applyPostBattleInitAbAttrs, PostBattleInitAbAttr } from "#app/data/ability"; +import { showEncounterDialogue, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; +import { PartyHealPhase } from "#app/phases/party-heal-phase"; +import { ShowTrainerPhase } from "#app/phases/show-trainer-phase"; +import { ReturnPhase } from "#app/phases/return-phase"; +import i18next from "i18next"; +import { ModifierTier } from "#app/modifier/modifier-tier"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; + +/** the i18n namespace for the encounter */ +const namespace = "mysteryEncounter:theWinstrateChallenge"; + +/** + * The Winstrate Challenge encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3821 | GitHub Issue #3821} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const TheWinstrateChallengeEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.THE_WINSTRATE_CHALLENGE) + .withEncounterTier(MysteryEncounterTier.ROGUE) + .withSceneWaveRangeRequirement(100, CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES[1]) + .withIntroSpriteConfigs([ + { + spriteKey: "vito", + fileRoot: "trainer", + hasShadow: false, + x: 16, + y: -4 + }, + { + spriteKey: "vivi", + fileRoot: "trainer", + hasShadow: false, + x: -14, + y: -4 + }, + { + spriteKey: "victor", + fileRoot: "trainer", + hasShadow: true, + x: -32 + }, + { + spriteKey: "victoria", + fileRoot: "trainer", + hasShadow: true, + x: 40, + }, + { + spriteKey: "vicky", + fileRoot: "trainer", + hasShadow: true, + x: 3, + y: 5, + yShadow: 5 + }, + ]) + .withIntroDialogue([ + { + text: `${namespace}.intro`, + }, + { + speaker: `${namespace}.speaker`, + text: `${namespace}.intro_dialogue`, + }, + ]) + .withAutoHideIntroVisuals(false) + .withOnInit((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + + // Loaded back to front for pop() operations + encounter.enemyPartyConfigs.push(getVitoTrainerConfig(scene)); + encounter.enemyPartyConfigs.push(getVickyTrainerConfig(scene)); + encounter.enemyPartyConfigs.push(getViviTrainerConfig(scene)); + encounter.enemyPartyConfigs.push(getVictoriaTrainerConfig(scene)); + encounter.enemyPartyConfigs.push(getVictorTrainerConfig(scene)); + + return true; + }) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + speaker: `${namespace}.speaker`, + text: `${namespace}.option.1.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Spawn 5 trainer battles back to back with Macho Brace in rewards + scene.currentBattle.mysteryEncounter!.doContinueEncounter = (scene: BattleScene) => { + return endTrainerBattleAndShowDialogue(scene); + }; + await transitionMysteryEncounterIntroVisuals(scene, true, false); + await spawnNextTrainerOrEndEncounter(scene); + } + ) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + speaker: `${namespace}.speaker`, + text: `${namespace}.option.2.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Refuse the challenge, they full heal the party and give the player a Rarer Candy + scene.unshiftPhase(new PartyHealPhase(scene, true)); + setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.RARER_CANDY], fillRemaining: false }); + leaveEncounterWithoutBattle(scene); + } + ) + .build(); + +async function spawnNextTrainerOrEndEncounter(scene: BattleScene) { + const encounter = scene.currentBattle.mysteryEncounter!; + const nextConfig = encounter.enemyPartyConfigs.pop(); + if (!nextConfig) { + await transitionMysteryEncounterIntroVisuals(scene, false, false); + await showEncounterDialogue(scene, `${namespace}.victory`, `${namespace}.speaker`); + + // Give 10x Voucher + const newModifier = modifierTypes.VOUCHER_PREMIUM().newModifier(); + await scene.addModifier(newModifier); + scene.playSound("item_fanfare"); + await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: newModifier?.type.name })); + + await showEncounterDialogue(scene, `${namespace}.victory_2`, `${namespace}.speaker`); + scene.ui.clearText(); // Clears "Winstrate" title from screen as rewards get animated in + const machoBrace = generateModifierTypeOption(scene, modifierTypes.MYSTERY_ENCOUNTER_MACHO_BRACE)!; + machoBrace.type.tier = ModifierTier.MASTER; + setEncounterRewards(scene, { guaranteedModifierTypeOptions: [machoBrace], fillRemaining: false }); + encounter.doContinueEncounter = undefined; + leaveEncounterWithoutBattle(scene, false, MysteryEncounterMode.NO_BATTLE); + } else { + await initBattleWithEnemyConfig(scene, nextConfig); + } +} + +function endTrainerBattleAndShowDialogue(scene: BattleScene): Promise { + return new Promise(async resolve => { + if (scene.currentBattle.mysteryEncounter!.enemyPartyConfigs.length === 0) { + // Battle is over + const trainer = scene.currentBattle.trainer; + if (trainer) { + scene.tweens.add({ + targets: trainer, + x: "+=16", + y: "-=16", + alpha: 0, + ease: "Sine.easeInOut", + duration: 750, + onComplete: () => { + scene.field.remove(trainer, true); + } + }); + } + + await spawnNextTrainerOrEndEncounter(scene); + resolve(); // Wait for all dialogue/post battle stuff to complete before resolving + } else { + scene.arena.resetArenaEffects(); + const playerField = scene.getPlayerField(); + playerField.forEach((_, p) => scene.unshiftPhase(new ReturnPhase(scene, p))); + + for (const pokemon of scene.getParty()) { + // Only trigger form change when Eiscue is in Noice form + // Hardcoded Eiscue for now in case it is fused with another pokemon + if (pokemon.species.speciesId === Species.EISCUE && pokemon.hasAbility(Abilities.ICE_FACE) && pokemon.formIndex === 1) { + scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeManualTrigger); + } + + pokemon.resetBattleData(); + applyPostBattleInitAbAttrs(PostBattleInitAbAttr, pokemon); + } + + scene.unshiftPhase(new ShowTrainerPhase(scene)); + // Hide the trainer and init next battle + const trainer = scene.currentBattle.trainer; + // Unassign previous trainer from battle so it isn't destroyed before animation completes + scene.currentBattle.trainer = null; + await spawnNextTrainerOrEndEncounter(scene); + if (trainer) { + scene.tweens.add({ + targets: trainer, + x: "+=16", + y: "-=16", + alpha: 0, + ease: "Sine.easeInOut", + duration: 750, + onComplete: () => { + scene.field.remove(trainer, true); + resolve(); + } + }); + } + } + }); +} + +function getVictorTrainerConfig(scene: BattleScene): EnemyPartyConfig { + return { + trainerType: TrainerType.VICTOR, + pokemonConfigs: [ + { + species: getPokemonSpecies(Species.SWELLOW), + isBoss: false, + abilityIndex: 0, // Guts + nature: Nature.ADAMANT, + moveSet: [Moves.FACADE, Moves.BRAVE_BIRD, Moves.PROTECT, Moves.QUICK_ATTACK], + modifierConfigs: [ + { + modifier: generateModifierType(scene, modifierTypes.FLAME_ORB) as PokemonHeldItemModifierType, + isTransferable: false + }, + { + modifier: generateModifierType(scene, modifierTypes.FOCUS_BAND) as PokemonHeldItemModifierType, + stackCount: 2, + isTransferable: false + }, + ] + }, + { + species: getPokemonSpecies(Species.OBSTAGOON), + isBoss: false, + abilityIndex: 1, // Guts + nature: Nature.ADAMANT, + moveSet: [Moves.FACADE, Moves.OBSTRUCT, Moves.NIGHT_SLASH, Moves.FIRE_PUNCH], + modifierConfigs: [ + { + modifier: generateModifierType(scene, modifierTypes.FLAME_ORB) as PokemonHeldItemModifierType, + isTransferable: false + }, + { + modifier: generateModifierType(scene, modifierTypes.LEFTOVERS) as PokemonHeldItemModifierType, + stackCount: 2, + isTransferable: false + } + ] + } + ] + }; +} + +function getVictoriaTrainerConfig(scene: BattleScene): EnemyPartyConfig { + return { + trainerType: TrainerType.VICTORIA, + pokemonConfigs: [ + { + species: getPokemonSpecies(Species.ROSERADE), + isBoss: false, + abilityIndex: 0, // Natural Cure + nature: Nature.CALM, + moveSet: [Moves.SYNTHESIS, Moves.SLUDGE_BOMB, Moves.GIGA_DRAIN, Moves.SLEEP_POWDER], + modifierConfigs: [ + { + modifier: generateModifierType(scene, modifierTypes.SOUL_DEW) as PokemonHeldItemModifierType, + isTransferable: false + }, + { + modifier: generateModifierType(scene, modifierTypes.QUICK_CLAW) as PokemonHeldItemModifierType, + stackCount: 2, + isTransferable: false + } + ] + }, + { + species: getPokemonSpecies(Species.GARDEVOIR), + isBoss: false, + formIndex: 1, + nature: Nature.TIMID, + moveSet: [Moves.PSYSHOCK, Moves.MOONBLAST, Moves.SHADOW_BALL, Moves.WILL_O_WISP], + modifierConfigs: [ + { + modifier: generateModifierType(scene, modifierTypes.ATTACK_TYPE_BOOSTER, [Type.PSYCHIC]) as PokemonHeldItemModifierType, + stackCount: 1, + isTransferable: false + }, + { + modifier: generateModifierType(scene, modifierTypes.ATTACK_TYPE_BOOSTER, [Type.FAIRY]) as PokemonHeldItemModifierType, + stackCount: 1, + isTransferable: false + } + ] + } + ] + }; +} + +function getViviTrainerConfig(scene: BattleScene): EnemyPartyConfig { + return { + trainerType: TrainerType.VIVI, + pokemonConfigs: [ + { + species: getPokemonSpecies(Species.SEAKING), + isBoss: false, + abilityIndex: 3, // Lightning Rod + nature: Nature.ADAMANT, + moveSet: [Moves.WATERFALL, Moves.MEGAHORN, Moves.KNOCK_OFF, Moves.REST], + modifierConfigs: [ + { + modifier: generateModifierType(scene, modifierTypes.BERRY, [BerryType.LUM]) as PokemonHeldItemModifierType, + stackCount: 2, + isTransferable: false + }, + { + modifier: generateModifierType(scene, modifierTypes.BASE_STAT_BOOSTER, [Stat.HP]) as PokemonHeldItemModifierType, + stackCount: 4, + isTransferable: false + } + ] + }, + { + species: getPokemonSpecies(Species.BRELOOM), + isBoss: false, + abilityIndex: 1, // Poison Heal + nature: Nature.JOLLY, + moveSet: [Moves.SPORE, Moves.SWORDS_DANCE, Moves.SEED_BOMB, Moves.DRAIN_PUNCH], + modifierConfigs: [ + { + modifier: generateModifierType(scene, modifierTypes.BASE_STAT_BOOSTER, [Stat.HP]) as PokemonHeldItemModifierType, + stackCount: 4, + isTransferable: false + }, + { + modifier: generateModifierType(scene, modifierTypes.TOXIC_ORB) as PokemonHeldItemModifierType, + isTransferable: false + } + ] + }, + { + species: getPokemonSpecies(Species.CAMERUPT), + isBoss: false, + formIndex: 1, + nature: Nature.CALM, + moveSet: [Moves.EARTH_POWER, Moves.FIRE_BLAST, Moves.YAWN, Moves.PROTECT], + modifierConfigs: [ + { + modifier: generateModifierType(scene, modifierTypes.QUICK_CLAW) as PokemonHeldItemModifierType, + stackCount: 3, + isTransferable: false + }, + ] + } + ] + }; +} + +function getVickyTrainerConfig(scene: BattleScene): EnemyPartyConfig { + return { + trainerType: TrainerType.VICKY, + pokemonConfigs: [ + { + species: getPokemonSpecies(Species.MEDICHAM), + isBoss: false, + formIndex: 1, + nature: Nature.IMPISH, + moveSet: [Moves.AXE_KICK, Moves.ICE_PUNCH, Moves.ZEN_HEADBUTT, Moves.BULLET_PUNCH], + modifierConfigs: [ + { + modifier: generateModifierType(scene, modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType, + isTransferable: false + } + ] + } + ] + }; +} + +function getVitoTrainerConfig(scene: BattleScene): EnemyPartyConfig { + return { + trainerType: TrainerType.VITO, + pokemonConfigs: [ + { + species: getPokemonSpecies(Species.HISUI_ELECTRODE), + isBoss: false, + abilityIndex: 0, // Soundproof + nature: Nature.MODEST, + moveSet: [Moves.THUNDERBOLT, Moves.GIGA_DRAIN, Moves.FOUL_PLAY, Moves.THUNDER_WAVE], + modifierConfigs: [ + { + modifier: generateModifierType(scene, modifierTypes.BASE_STAT_BOOSTER, [Stat.SPD]) as PokemonHeldItemModifierType, + stackCount: 2, + isTransferable: false + } + ] + }, + { + species: getPokemonSpecies(Species.SWALOT), + isBoss: false, + abilityIndex: 2, // Gluttony + nature: Nature.QUIET, + moveSet: [Moves.SLUDGE_BOMB, Moves.GIGA_DRAIN, Moves.ICE_BEAM, Moves.EARTHQUAKE], + modifierConfigs: [ + { + modifier: generateModifierType(scene, modifierTypes.BERRY, [BerryType.SITRUS]) as PokemonHeldItemModifierType, + stackCount: 2, + }, + { + modifier: generateModifierType(scene, modifierTypes.BERRY, [BerryType.APICOT]) as PokemonHeldItemModifierType, + stackCount: 2, + }, + { + modifier: generateModifierType(scene, modifierTypes.BERRY, [BerryType.GANLON]) as PokemonHeldItemModifierType, + stackCount: 2, + }, + { + modifier: generateModifierType(scene, modifierTypes.BERRY, [BerryType.STARF]) as PokemonHeldItemModifierType, + stackCount: 2, + }, + { + modifier: generateModifierType(scene, modifierTypes.BERRY, [BerryType.SALAC]) as PokemonHeldItemModifierType, + stackCount: 2, + }, + { + modifier: generateModifierType(scene, modifierTypes.BERRY, [BerryType.LUM]) as PokemonHeldItemModifierType, + stackCount: 2, + }, + { + modifier: generateModifierType(scene, modifierTypes.BERRY, [BerryType.LANSAT]) as PokemonHeldItemModifierType, + stackCount: 2, + }, + { + modifier: generateModifierType(scene, modifierTypes.BERRY, [BerryType.LIECHI]) as PokemonHeldItemModifierType, + stackCount: 2, + }, + { + modifier: generateModifierType(scene, modifierTypes.BERRY, [BerryType.PETAYA]) as PokemonHeldItemModifierType, + stackCount: 2, + }, + { + modifier: generateModifierType(scene, modifierTypes.BERRY, [BerryType.ENIGMA]) as PokemonHeldItemModifierType, + stackCount: 2, + }, + { + modifier: generateModifierType(scene, modifierTypes.BERRY, [BerryType.LEPPA]) as PokemonHeldItemModifierType, + stackCount: 2, + } + ] + }, + { + species: getPokemonSpecies(Species.DODRIO), + isBoss: false, + abilityIndex: 2, // Tangled Feet + nature: Nature.JOLLY, + moveSet: [Moves.DRILL_PECK, Moves.QUICK_ATTACK, Moves.THRASH, Moves.KNOCK_OFF], + modifierConfigs: [ + { + modifier: generateModifierType(scene, modifierTypes.KINGS_ROCK) as PokemonHeldItemModifierType, + stackCount: 2, + isTransferable: false + } + ] + }, + { + species: getPokemonSpecies(Species.ALAKAZAM), + isBoss: false, + formIndex: 1, + nature: Nature.BOLD, + moveSet: [Moves.PSYCHIC, Moves.SHADOW_BALL, Moves.FOCUS_BLAST, Moves.THUNDERBOLT], + modifierConfigs: [ + { + modifier: generateModifierType(scene, modifierTypes.WIDE_LENS) as PokemonHeldItemModifierType, + stackCount: 2, + isTransferable: false + }, + ] + }, + { + species: getPokemonSpecies(Species.DARMANITAN), + isBoss: false, + abilityIndex: 0, // Sheer Force + nature: Nature.IMPISH, + moveSet: [Moves.EARTHQUAKE, Moves.U_TURN, Moves.FLARE_BLITZ, Moves.ROCK_SLIDE], + modifierConfigs: [ + { + modifier: generateModifierType(scene, modifierTypes.QUICK_CLAW) as PokemonHeldItemModifierType, + stackCount: 2, + isTransferable: false + }, + ] + } + ] + }; +} diff --git a/src/data/mystery-encounters/encounters/training-session-encounter.ts b/src/data/mystery-encounters/encounters/training-session-encounter.ts new file mode 100644 index 00000000000..cdf1cef540c --- /dev/null +++ b/src/data/mystery-encounters/encounters/training-session-encounter.ts @@ -0,0 +1,418 @@ +import { Ability, allAbilities } from "#app/data/ability"; +import { EnemyPartyConfig, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, selectPokemonForOption, setEncounterRewards, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { getNatureName, Nature } from "#app/data/nature"; +import { speciesStarters } from "#app/data/pokemon-species"; +import Pokemon, { PlayerPokemon } from "#app/field/pokemon"; +import { PokemonHeldItemModifier } from "#app/modifier/modifier"; +import { AbilityAttr } from "#app/system/game-data"; +import PokemonData from "#app/system/pokemon-data"; +import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; +import { isNullOrUndefined, randSeedShuffle } from "#app/utils"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import BattleScene from "#app/battle-scene"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import HeldModifierConfig from "#app/interfaces/held-modifier-config"; +import i18next from "i18next"; +import { getStatKey } from "#enums/stat"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; +import { isPokemonValidForEncounterOptionSelection } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; + +/** The i18n namespace for the encounter */ +const namespace = "mysteryEncounter:trainingSession"; + +/** + * Training Session encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3802 | GitHub Issue #3802} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const TrainingSessionEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.TRAINING_SESSION) + .withEncounterTier(MysteryEncounterTier.ULTRA) + .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + .withScenePartySizeRequirement(2, 6, true) // Must have at least 2 unfainted pokemon in party + .withFleeAllowed(false) + .withHideWildIntroMessage(true) + .withIntroSpriteConfigs([ + { + spriteKey: "training_session_gear", + fileRoot: "mystery-encounters", + hasShadow: true, + y: 6, + x: 5, + yShadow: -2 + }, + ]) + .withIntroDialogue([ + { + text: `${namespace}.intro`, + } + ]) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withHasDexProgress(true) + .withDialogue({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.selected`, + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene): Promise => { + const encounter = scene.currentBattle.mysteryEncounter!; + const onPokemonSelected = (pokemon: PlayerPokemon) => { + encounter.misc = { + playerPokemon: pokemon, + }; + }; + + // Only Pokemon that are not KOed/legal can be trained + const selectableFilter = (pokemon: Pokemon) => { + return isPokemonValidForEncounterOptionSelection(pokemon, scene, `${namespace}.invalid_selection`); + }; + + return selectPokemonForOption(scene, onPokemonSelected, undefined, selectableFilter); + }) + .withOptionPhase(async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + const playerPokemon: PlayerPokemon = encounter.misc.playerPokemon; + + // Spawn light training session with chosen pokemon + // Every 50 waves, add +1 boss segment, capping at 5 + const segments = Math.min( + 2 + Math.floor(scene.currentBattle.waveIndex / 50), + 5 + ); + const modifiers = new ModifiersHolder(); + const config = getEnemyConfig(scene, playerPokemon, segments, modifiers); + scene.removePokemonFromPlayerParty(playerPokemon, false); + + const onBeforeRewardsPhase = () => { + encounter.setDialogueToken("stat1", "-"); + encounter.setDialogueToken("stat2", "-"); + // Add the pokemon back to party with IV boost + const ivIndexes: any[] = []; + playerPokemon.ivs.forEach((iv, index) => { + if (iv < 31) { + ivIndexes.push({ iv: iv, index: index }); + } + }); + + // Improves 2 random non-maxed IVs + // +10 if IV is < 10, +5 if between 10-20, and +3 if > 20 + // A 0-4 starting IV will cap in 6 encounters (assuming you always rolled that IV) + // 5-14 starting IV caps in 5 encounters + // 15-19 starting IV caps in 4 encounters + // 20-24 starting IV caps in 3 encounters + // 25-27 starting IV caps in 2 encounters + let improvedCount = 0; + while (ivIndexes.length > 0 && improvedCount < 2) { + randSeedShuffle(ivIndexes); + const ivToChange = ivIndexes.pop(); + let newVal = ivToChange.iv; + if (improvedCount === 0) { + encounter.setDialogueToken( + "stat1", + i18next.t(getStatKey(ivToChange.index)) ?? "" + ); + } else { + encounter.setDialogueToken( + "stat2", + i18next.t(getStatKey(ivToChange.index)) ?? "" + ); + } + + // Corrects required encounter breakpoints to be continuous for all IV values + if (ivToChange.iv <= 21 && ivToChange.iv - (1 % 5) === 0) { + newVal += 1; + } + + newVal += ivToChange.iv <= 10 ? 10 : ivToChange.iv <= 20 ? 5 : 3; + newVal = Math.min(newVal, 31); + playerPokemon.ivs[ivToChange.index] = newVal; + improvedCount++; + } + + if (improvedCount > 0) { + playerPokemon.calculateStats(); + scene.gameData.updateSpeciesDexIvs( + playerPokemon.species.getRootSpeciesId(true), + playerPokemon.ivs + ); + scene.gameData.setPokemonCaught(playerPokemon, false); + } + + // Add pokemon and mods back + scene.getParty().push(playerPokemon); + for (const mod of modifiers.value) { + mod.pokemonId = playerPokemon.id; + scene.addModifier(mod, true, false, false, true); + } + scene.updateModifiers(true); + queueEncounterMessage(scene, `${namespace}.option.1.finished`); + }; + + setEncounterRewards(scene, { fillRemaining: true }, undefined, onBeforeRewardsPhase); + + return initBattleWithEnemyConfig(scene, config); + }) + .build() + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withHasDexProgress(true) + .withDialogue({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + secondOptionPrompt: `${namespace}.option.2.select_prompt`, + selected: [ + { + text: `${namespace}.option.selected`, + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene): Promise => { + // Open menu for selecting pokemon and Nature + const encounter = scene.currentBattle.mysteryEncounter!; + const natures = new Array(25).fill(null).map((val, i) => i as Nature); + const onPokemonSelected = (pokemon: PlayerPokemon) => { + // Return the options for nature selection + return natures.map((nature: Nature) => { + const option: OptionSelectItem = { + label: getNatureName(nature, true, true, true, scene.uiTheme), + handler: () => { + // Pokemon and second option selected + encounter.setDialogueToken("nature", getNatureName(nature)); + encounter.misc = { + playerPokemon: pokemon, + chosenNature: nature, + }; + return true; + }, + }; + return option; + }); + }; + + // Only Pokemon that are not KOed/legal can be trained + const selectableFilter = (pokemon: Pokemon) => { + return isPokemonValidForEncounterOptionSelection(pokemon, scene, `${namespace}.invalid_selection`); + }; + + return selectPokemonForOption(scene, onPokemonSelected, undefined, selectableFilter); + }) + .withOptionPhase(async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + const playerPokemon: PlayerPokemon = encounter.misc.playerPokemon; + + // Spawn medium training session with chosen pokemon + // Every 40 waves, add +1 boss segment, capping at 6 + const segments = Math.min(2 + Math.floor(scene.currentBattle.waveIndex / 40), 6); + const modifiers = new ModifiersHolder(); + const config = getEnemyConfig(scene, playerPokemon, segments, modifiers); + scene.removePokemonFromPlayerParty(playerPokemon, false); + + const onBeforeRewardsPhase = () => { + queueEncounterMessage(scene, `${namespace}.option.2.finished`); + // Add the pokemon back to party with Nature change + playerPokemon.setNature(encounter.misc.chosenNature); + scene.gameData.setPokemonCaught(playerPokemon, false); + + // Add pokemon and modifiers back + scene.getParty().push(playerPokemon); + for (const mod of modifiers.value) { + mod.pokemonId = playerPokemon.id; + scene.addModifier(mod, true, false, false, true); + } + scene.updateModifiers(true); + }; + + setEncounterRewards(scene, { fillRemaining: true }, undefined, onBeforeRewardsPhase); + + return initBattleWithEnemyConfig(scene, config); + }) + .build() + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withHasDexProgress(true) + .withDialogue({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + secondOptionPrompt: `${namespace}.option.3.select_prompt`, + selected: [ + { + text: `${namespace}.option.selected`, + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene): Promise => { + // Open menu for selecting pokemon and ability to learn + const encounter = scene.currentBattle.mysteryEncounter!; + const onPokemonSelected = (pokemon: PlayerPokemon) => { + // Return the options for ability selection + const speciesForm = !!pokemon.getFusionSpeciesForm() + ? pokemon.getFusionSpeciesForm() + : pokemon.getSpeciesForm(); + const abilityCount = speciesForm.getAbilityCount(); + const abilities: Ability[] = new Array(abilityCount) + .fill(null) + .map((val, i) => allAbilities[speciesForm.getAbility(i)]); + + const optionSelectItems: OptionSelectItem[] = []; + abilities.forEach((ability: Ability, index) => { + if (!optionSelectItems.some(o => o.label === ability.name)) { + const option: OptionSelectItem = { + label: ability.name, + handler: () => { + // Pokemon and ability selected + encounter.setDialogueToken("ability", ability.name); + encounter.misc = { + playerPokemon: pokemon, + abilityIndex: index, + }; + return true; + }, + onHover: () => { + showEncounterText(scene, ability.description, 0, 0, false); + }, + }; + optionSelectItems.push(option); + } + }); + + return optionSelectItems; + }; + + // Only Pokemon that are not KOed/legal can be trained + const selectableFilter = (pokemon: Pokemon) => { + return isPokemonValidForEncounterOptionSelection(pokemon, scene, `${namespace}.invalid_selection`); + }; + + return selectPokemonForOption(scene, onPokemonSelected, undefined, selectableFilter); + }) + .withOptionPhase(async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + const playerPokemon: PlayerPokemon = encounter.misc.playerPokemon; + + // Spawn hard training session with chosen pokemon + // Every 30 waves, add +1 boss segment, capping at 6 + // Also starts with +1 to all stats + const segments = Math.min(2 + Math.floor(scene.currentBattle.waveIndex / 30), 6); + const modifiers = new ModifiersHolder(); + const config = getEnemyConfig(scene, playerPokemon, segments, modifiers); + config.pokemonConfigs![0].tags = [ + BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON, + ]; + scene.removePokemonFromPlayerParty(playerPokemon, false); + + const onBeforeRewardsPhase = () => { + queueEncounterMessage(scene, `${namespace}.option.3.finished`); + // Add the pokemon back to party with ability change + const abilityIndex = encounter.misc.abilityIndex; + if (!!playerPokemon.getFusionSpeciesForm()) { + playerPokemon.fusionAbilityIndex = abilityIndex; + if (!isNullOrUndefined(playerPokemon.fusionSpecies?.speciesId) && speciesStarters.hasOwnProperty(playerPokemon.fusionSpecies!.speciesId)) { + scene.gameData.starterData[playerPokemon.fusionSpecies!.speciesId] + .abilityAttr |= + abilityIndex !== 1 || playerPokemon.fusionSpecies!.ability2 + ? Math.pow(2, playerPokemon.fusionAbilityIndex) + : AbilityAttr.ABILITY_HIDDEN; + } + } else { + playerPokemon.abilityIndex = abilityIndex; + if ( + speciesStarters.hasOwnProperty(playerPokemon.species.speciesId) + ) { + scene.gameData.starterData[ + playerPokemon.species.speciesId + ].abilityAttr |= + abilityIndex !== 1 || playerPokemon.species.ability2 + ? Math.pow(2, playerPokemon.abilityIndex) + : AbilityAttr.ABILITY_HIDDEN; + } + } + + playerPokemon.getAbility(); + playerPokemon.calculateStats(); + scene.gameData.setPokemonCaught(playerPokemon, false); + + // Add pokemon and mods back + scene.getParty().push(playerPokemon); + for (const mod of modifiers.value) { + mod.pokemonId = playerPokemon.id; + scene.addModifier(mod, true, false, false, true); + } + scene.updateModifiers(true); + }; + + setEncounterRewards(scene, { fillRemaining: true }, undefined, onBeforeRewardsPhase); + + return initBattleWithEnemyConfig(scene, config); + }) + .build() + ) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.4.label`, + buttonTooltip: `${namespace}.option.4.tooltip`, + selected: [ + { + text: `${namespace}.option.4.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Leave encounter with no rewards or exp + leaveEncounterWithoutBattle(scene, true); + return true; + } + ) + .build(); + +function getEnemyConfig(scene: BattleScene, playerPokemon: PlayerPokemon, segments: number, modifiers: ModifiersHolder): EnemyPartyConfig { + playerPokemon.resetSummonData(); + + // Passes modifiers by reference + modifiers.value = playerPokemon.getHeldItems(); + const modifierConfigs = modifiers.value.map((mod) => { + return { + modifier: mod.clone(), + isTransferable: false, + stackCount: mod.stackCount + }; + }) as HeldModifierConfig[]; + + const data = new PokemonData(playerPokemon); + return { + pokemonConfigs: [ + { + species: playerPokemon.species, + isBoss: true, + bossSegments: segments, + formIndex: playerPokemon.formIndex, + level: playerPokemon.level, + dataSource: data, + modifierConfigs: modifierConfigs, + }, + ], + }; +} + +class ModifiersHolder { + public value: PokemonHeldItemModifier[] = []; + + constructor() {} +} diff --git a/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts b/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts new file mode 100644 index 00000000000..d295c8ab548 --- /dev/null +++ b/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts @@ -0,0 +1,222 @@ +import { EnemyPartyConfig, EnemyPokemonConfig, generateModifierType, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, loadCustomMovesForEncounter, setEncounterRewards, transitionMysteryEncounterIntroVisuals, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import BattleScene from "#app/battle-scene"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { Species } from "#enums/species"; +import { HitHealModifier, PokemonHeldItemModifier, TurnHealModifier } from "#app/modifier/modifier"; +import { applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import i18next from "#app/plugins/i18n"; +import { ModifierTier } from "#app/modifier/modifier-tier"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { Moves } from "#enums/moves"; +import { BattlerIndex } from "#app/battle"; +import { PokemonMove } from "#app/field/pokemon"; +import { ModifierRewardPhase } from "#app/phases/modifier-reward-phase"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; + +/** the i18n namespace for this encounter */ +const namespace = "mysteryEncounter:trashToTreasure"; + +const SOUND_EFFECT_WAIT_TIME = 700; + +/** + * Trash to Treasure encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3809 | GitHub Issue #3809} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const TrashToTreasureEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.TRASH_TO_TREASURE) + .withEncounterTier(MysteryEncounterTier.ULTRA) + .withSceneWaveRangeRequirement(60, CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES[1]) + .withMaxAllowedEncounters(1) + .withIntroSpriteConfigs([ + { + spriteKey: Species.GARBODOR.toString() + "-gigantamax", + fileRoot: "pokemon", + hasShadow: false, + disableAnimation: true, + scale: 1.5, + y: 8, + tint: 0.4 + } + ]) + .withAutoHideIntroVisuals(false) + .withIntroDialogue([ + { + text: `${namespace}.intro`, + }, + ]) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withOnInit((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + + // Calculate boss mon + const bossSpecies = getPokemonSpecies(Species.GARBODOR); + const pokemonConfig: EnemyPokemonConfig = { + species: bossSpecies, + isBoss: true, + formIndex: 1, // Gmax + bossSegmentModifier: 1, // +1 Segment from normal + moveSet: [Moves.PAYBACK, Moves.GUNK_SHOT, Moves.STOMPING_TANTRUM, Moves.DRAIN_PUNCH] + }; + const config: EnemyPartyConfig = { + levelAdditiveModifier: 1, + pokemonConfigs: [pokemonConfig], + disableSwitch: true + }; + encounter.enemyPartyConfigs = [config]; + + // Load animations/sfx for Garbodor fight start moves + loadCustomMovesForEncounter(scene, [Moves.TOXIC, Moves.AMNESIA]); + + scene.loadSe("PRSFX- Dig2", "battle_anims", "PRSFX- Dig2.wav"); + scene.loadSe("PRSFX- Venom Drench", "battle_anims", "PRSFX- Venom Drench.wav"); + + return true; + }) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene) => { + // Play Dig2 and then Venom Drench sfx + doGarbageDig(scene); + }) + .withOptionPhase(async (scene: BattleScene) => { + // Gain 2 Leftovers and 2 Shell Bell + transitionMysteryEncounterIntroVisuals(scene); + await tryApplyDigRewardItems(scene); + + // Give the player the Black Sludge curse + scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.MYSTERY_ENCOUNTER_BLACK_SLUDGE)); + leaveEncounterWithoutBattle(scene, true); + }) + .build() + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected`, + }, + ], + }) + .withOptionPhase(async (scene: BattleScene) => { + // Investigate garbage, battle Gmax Garbodor + scene.setFieldScale(0.75); + await showEncounterText(scene, `${namespace}.option.2.selected_2`); + transitionMysteryEncounterIntroVisuals(scene); + + const encounter = scene.currentBattle.mysteryEncounter!; + + setEncounterRewards(scene, { guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ROGUE, ModifierTier.ULTRA, ModifierTier.GREAT], fillRemaining: true }); + encounter.startOfBattleEffects.push( + { + sourceBattlerIndex: BattlerIndex.ENEMY, + targets: [BattlerIndex.PLAYER], + move: new PokemonMove(Moves.TOXIC), + ignorePp: true + }, + { + sourceBattlerIndex: BattlerIndex.ENEMY, + targets: [BattlerIndex.ENEMY], + move: new PokemonMove(Moves.AMNESIA), + ignorePp: true + }); + await initBattleWithEnemyConfig(scene, encounter.enemyPartyConfigs[0]); + }) + .build() + ) + .build(); + +async function tryApplyDigRewardItems(scene: BattleScene) { + const shellBell = generateModifierType(scene, modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType; + const leftovers = generateModifierType(scene, modifierTypes.LEFTOVERS) as PokemonHeldItemModifierType; + + const party = scene.getParty(); + + // Iterate over the party until an item was successfully given + // First leftovers + for (const pokemon of party) { + const heldItems = scene.findModifiers(m => m instanceof PokemonHeldItemModifier + && m.pokemonId === pokemon.id, true) as PokemonHeldItemModifier[]; + const existingLeftovers = heldItems.find(m => m instanceof TurnHealModifier) as TurnHealModifier; + + if (!existingLeftovers || existingLeftovers.getStackCount() < existingLeftovers.getMaxStackCount(scene)) { + await applyModifierTypeToPlayerPokemon(scene, pokemon, leftovers); + break; + } + } + + // Second leftovers + for (const pokemon of party) { + const heldItems = scene.findModifiers(m => m instanceof PokemonHeldItemModifier + && m.pokemonId === pokemon.id, true) as PokemonHeldItemModifier[]; + const existingLeftovers = heldItems.find(m => m instanceof TurnHealModifier) as TurnHealModifier; + + if (!existingLeftovers || existingLeftovers.getStackCount() < existingLeftovers.getMaxStackCount(scene)) { + await applyModifierTypeToPlayerPokemon(scene, pokemon, leftovers); + break; + } + } + + scene.playSound("item_fanfare"); + await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: "2 " + leftovers.name }), null, undefined, true); + + // First Shell bell + for (const pokemon of party) { + const heldItems = scene.findModifiers(m => m instanceof PokemonHeldItemModifier + && m.pokemonId === pokemon.id, true) as PokemonHeldItemModifier[]; + const existingShellBell = heldItems.find(m => m instanceof HitHealModifier) as HitHealModifier; + + if (!existingShellBell || existingShellBell.getStackCount() < existingShellBell.getMaxStackCount(scene)) { + await applyModifierTypeToPlayerPokemon(scene, pokemon, shellBell); + break; + } + } + + // Second Shell bell + for (const pokemon of party) { + const heldItems = scene.findModifiers(m => m instanceof PokemonHeldItemModifier + && m.pokemonId === pokemon.id, true) as PokemonHeldItemModifier[]; + const existingShellBell = heldItems.find(m => m instanceof HitHealModifier) as HitHealModifier; + + if (!existingShellBell || existingShellBell.getStackCount() < existingShellBell.getMaxStackCount(scene)) { + await applyModifierTypeToPlayerPokemon(scene, pokemon, shellBell); + break; + } + } + + scene.playSound("item_fanfare"); + await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: "2 " + shellBell.name }), null, undefined, true); +} + +async function doGarbageDig(scene: BattleScene) { + scene.playSound("battle_anims/PRSFX- Dig2"); + scene.time.delayedCall(SOUND_EFFECT_WAIT_TIME, () => { + scene.playSound("battle_anims/PRSFX- Dig2"); + scene.playSound("battle_anims/PRSFX- Venom Drench", { volume: 2 }); + }); + scene.time.delayedCall(SOUND_EFFECT_WAIT_TIME * 2, () => { + scene.playSound("battle_anims/PRSFX- Dig2"); + }); +} diff --git a/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts b/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts new file mode 100644 index 00000000000..4f8a43ce364 --- /dev/null +++ b/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts @@ -0,0 +1,268 @@ +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { EnemyPartyConfig, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, setEncounterExp, setEncounterRewards } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { CHARMING_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups"; +import Pokemon, { EnemyPokemon, PokemonMove } from "#app/field/pokemon"; +import { getPartyLuckValue } from "#app/modifier/modifier-type"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import BattleScene from "#app/battle-scene"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MoveRequirement, PersistentModifierRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { TrainerSlot } from "#app/data/trainer-config"; +import { catchPokemon, getHighestLevelPlayerPokemon, getSpriteKeysFromPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import PokemonData from "#app/system/pokemon-data"; +import { speciesEggMoves } from "#app/data/egg-moves"; +import { isNullOrUndefined, randSeedInt } from "#app/utils"; +import { Moves } from "#enums/moves"; +import { BattlerIndex } from "#app/battle"; +import { SelfStatusMove } from "#app/data/move"; +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 { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; +import { Stat } from "#enums/stat"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; + +/** the i18n namespace for the encounter */ +const namespace = "mysteryEncounter:uncommonBreed"; + +/** + * Uncommon Breed encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3811 | GitHub Issue #3811} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const UncommonBreedEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.UNCOMMON_BREED) + .withEncounterTier(MysteryEncounterTier.COMMON) + .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + .withCatchAllowed(true) + .withHideWildIntroMessage(true) + .withIntroSpriteConfigs([]) // Set in onInit() + .withIntroDialogue([ + { + text: `${namespace}.intro`, + }, + ]) + .withOnInit((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + + // Calculate boss mon + // Level equal to 2 below highest party member + const level = getHighestLevelPlayerPokemon(scene, false, true).level - 2; + const species = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getParty()), true); + const pokemon = new EnemyPokemon(scene, species, level, TrainerSlot.NONE, true); + const speciesRootForm = pokemon.species.getRootSpeciesId(); + + // Pokemon will always have one of its egg moves in its moveset + if (speciesEggMoves.hasOwnProperty(speciesRootForm)) { + const eggMoves: Moves[] = speciesEggMoves[speciesRootForm]; + const eggMoveIndex = randSeedInt(4); + const randomEggMove: Moves = eggMoves[eggMoveIndex]; + encounter.misc = { + eggMove: randomEggMove + }; + if (pokemon.moveset.length < 4) { + pokemon.moveset.push(new PokemonMove(randomEggMove)); + } else { + pokemon.moveset[0] = new PokemonMove(randomEggMove); + } + } + + encounter.misc.pokemon = pokemon; + + const config: EnemyPartyConfig = { + pokemonConfigs: [{ + level: level, + species: species, + dataSource: new PokemonData(pokemon), + isBoss: false, + tags: [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON], + mysteryEncounterBattleEffects: (pokemon: Pokemon) => { + queueEncounterMessage(pokemon.scene, `${namespace}.option.1.stat_boost`); + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD], 1)); + } + }], + }; + encounter.enemyPartyConfigs = [config]; + + const { spriteKey, fileRoot } = getSpriteKeysFromPokemon(pokemon); + encounter.spriteConfigs = [ + { + spriteKey: spriteKey, + fileRoot: fileRoot, + hasShadow: true, + x: -5, + repeat: true, + isPokemon: true + }, + ]; + + encounter.setDialogueToken("enemyPokemon", pokemon.getNameToRender()); + scene.loadSe("PRSFX- Spotlight2", "battle_anims", "PRSFX- Spotlight2.wav"); + return true; + }) + .withOnVisualsStart((scene: BattleScene) => { + // Animate the pokemon + const encounter = scene.currentBattle.mysteryEncounter!; + const pokemonSprite = encounter.introVisuals!.getSprites(); + + scene.tweens.add({ // Bounce at the end + targets: pokemonSprite, + duration: 300, + ease: "Cubic.easeOut", + yoyo: true, + y: "-=20", + loop: 1, + }); + + scene.time.delayedCall(500, () => scene.playSound("battle_anims/PRSFX- Spotlight2")); + return true; + }) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Pick battle + const encounter = scene.currentBattle.mysteryEncounter!; + + const eggMove = encounter.misc.eggMove; + if (!isNullOrUndefined(eggMove)) { + // 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; + + encounter.startOfBattleEffects.push( + { + sourceBattlerIndex: BattlerIndex.ENEMY, + targets: [target], + move: pokemonMove, + ignorePp: true + }); + } + + setEncounterRewards(scene, { fillRemaining: true }); + await initBattleWithEnemyConfig(scene, encounter.enemyPartyConfigs[0]); + } + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL) + .withSceneRequirement(new PersistentModifierRequirement("BerryModifier", 4)) // Will set option2PrimaryName and option2PrimaryMove dialogue tokens automatically + .withDialogue({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + disabledButtonTooltip: `${namespace}.option.2.disabled_tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected` + } + ] + }) + .withOptionPhase(async (scene: BattleScene) => { + // Give it some food + + // Remove 4 random berries from player's party + // Get all player berry items, remove from party, and store reference + const berryItems: BerryModifier[]= scene.findModifiers(m => m instanceof BerryModifier) as BerryModifier[]; + for (let i = 0; i < 4; i++) { + const index = randSeedInt(berryItems.length); + const randBerry = berryItems[index]; + randBerry.stackCount--; + if (randBerry.stackCount === 0) { + scene.removeModifier(randBerry); + berryItems.splice(index, 1); + } + } + await scene.updateModifiers(true, true); + + // Pokemon joins the team, with 2 egg moves + const encounter = scene.currentBattle.mysteryEncounter!; + const pokemon = encounter.misc.pokemon; + + // Give 1 additional egg move + const previousEggMove = encounter.misc.eggMove; + const speciesRootForm = pokemon.species.getRootSpeciesId(); + if (speciesEggMoves.hasOwnProperty(speciesRootForm)) { + const eggMoves: Moves[] = speciesEggMoves[speciesRootForm]; + let randomEggMove: Moves = eggMoves[randSeedInt(4)]; + while (randomEggMove === previousEggMove) { + randomEggMove = eggMoves[randSeedInt(4)]; + } + if (pokemon.moveset.length < 4) { + pokemon.moveset.push(new PokemonMove(randomEggMove)); + } else { + pokemon.moveset[1] = new PokemonMove(randomEggMove); + } + } + + await catchPokemon(scene, pokemon, null, PokeballType.POKEBALL, false); + setEncounterRewards(scene, { fillRemaining: true }); + leaveEncounterWithoutBattle(scene); + }) + .build() + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL) + .withPrimaryPokemonRequirement(new MoveRequirement(CHARMING_MOVES)) // Will set option2PrimaryName and option2PrimaryMove dialogue tokens automatically + .withDialogue({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + disabledButtonTooltip: `${namespace}.option.3.disabled_tooltip`, + selected: [ + { + text: `${namespace}.option.3.selected` + } + ] + }) + .withOptionPhase(async (scene: BattleScene) => { + // Attract the pokemon with a move + // Pokemon joins the team, with 2 egg moves and IVs rolled an additional time + const encounter = scene.currentBattle.mysteryEncounter!; + const pokemon = encounter.misc.pokemon; + + // Give 1 additional egg move + const previousEggMove = encounter.misc.eggMove; + const speciesRootForm = pokemon.species.getRootSpeciesId(); + if (speciesEggMoves.hasOwnProperty(speciesRootForm)) { + const eggMoves: Moves[] = speciesEggMoves[speciesRootForm]; + let randomEggMove: Moves = eggMoves[randSeedInt(4)]; + while (randomEggMove === previousEggMove) { + randomEggMove = eggMoves[randSeedInt(4)]; + } + if (pokemon.moveset.length < 4) { + pokemon.moveset.push(new PokemonMove(randomEggMove)); + } else { + pokemon.moveset[1] = new PokemonMove(randomEggMove); + } + } + + // Roll IVs a second time + pokemon.ivs = pokemon.ivs.map(iv => { + const newValue = randSeedInt(31); + return newValue > iv ? newValue : iv; + }); + + await catchPokemon(scene, pokemon, null, PokeballType.POKEBALL, false); + if (encounter.selectedOption?.primaryPokemon?.id) { + setEncounterExp(scene, encounter.selectedOption.primaryPokemon.id, pokemon.getExpValue(), false); + } + setEncounterRewards(scene, { fillRemaining: true }); + leaveEncounterWithoutBattle(scene); + }) + .build() + ) + .build(); diff --git a/src/data/mystery-encounters/encounters/weird-dream-encounter.ts b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts new file mode 100644 index 00000000000..0b3b4434278 --- /dev/null +++ b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts @@ -0,0 +1,568 @@ +import { Type } from "#app/data/type"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { Species } from "#enums/species"; +import BattleScene from "#app/battle-scene"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { leaveEncounterWithoutBattle, setEncounterRewards, } from "../utils/encounter-phase-utils"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { PlayerPokemon, PokemonMove } from "#app/field/pokemon"; +import { IntegerHolder, isNullOrUndefined, randSeedInt, randSeedShuffle } from "#app/utils"; +import PokemonSpecies, { allSpecies, getPokemonSpecies } from "#app/data/pokemon-species"; +import { HiddenAbilityRateBoosterModifier, PokemonFormChangeItemModifier, PokemonHeldItemModifier } from "#app/modifier/modifier"; +import { achvs } from "#app/system/achv"; +import { speciesEggMoves } from "#app/data/egg-moves"; +import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data"; +import { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { modifierTypes } from "#app/modifier/modifier-type"; +import i18next from "#app/plugins/i18n"; +import { doPokemonTransformationSequence, TransformationScreenPosition } from "#app/data/mystery-encounters/utils/encounter-transformation-sequence"; +import { getLevelTotalExp } from "#app/data/exp"; +import { Stat } from "#enums/stat"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; +import { Challenges } from "#enums/challenges"; + +/** i18n namespace for encounter */ +const namespace = "mysteryEncounter:weirdDream"; + +/** Exclude Ultra Beasts, Paradox, Eternatus, and all legendary/mythical/trio pokemon that are below 570 BST */ +const EXCLUDED_TRANSFORMATION_SPECIES = [ + Species.ETERNATUS, + /** UBs */ + Species.NIHILEGO, + Species.BUZZWOLE, + Species.PHEROMOSA, + Species.XURKITREE, + Species.CELESTEELA, + Species.KARTANA, + Species.GUZZLORD, + Species.POIPOLE, + Species.NAGANADEL, + Species.STAKATAKA, + Species.BLACEPHALON, + /** Paradox */ + Species.GREAT_TUSK, + Species.SCREAM_TAIL, + Species.BRUTE_BONNET, + Species.FLUTTER_MANE, + Species.SLITHER_WING, + Species.SANDY_SHOCKS, + Species.ROARING_MOON, + Species.WALKING_WAKE, + Species.GOUGING_FIRE, + Species.RAGING_BOLT, + Species.IRON_TREADS, + Species.IRON_BUNDLE, + Species.IRON_HANDS, + Species.IRON_JUGULIS, + Species.IRON_MOTH, + Species.IRON_THORNS, + Species.IRON_VALIANT, + Species.IRON_LEAVES, + Species.IRON_BOULDER, + Species.IRON_CROWN, + /** These are banned so they don't appear in the < 570 BST pool */ + Species.COSMOG, + Species.MELTAN, + Species.KUBFU, + Species.COSMOEM, + Species.POIPOLE, + Species.TERAPAGOS, + Species.TYPE_NULL, + Species.CALYREX, + Species.NAGANADEL, + Species.URSHIFU, + Species.OGERPON, + Species.OKIDOGI, + Species.MUNKIDORI, + Species.FEZANDIPITI, +]; + +const SUPER_LEGENDARY_BST_THRESHOLD = 600; +const NON_LEGENDARY_BST_THRESHOLD = 570; +const GAIN_OLD_GATEAU_ITEM_BST_THRESHOLD = 450; + +/** 0-100 */ +const PERCENT_LEVEL_LOSS_ON_REFUSE = 12.5; + +/** + * Value ranges of the resulting species BST transformations after adding values to original species + * 2 Pokemon in the party use this range + */ +const HIGH_BST_TRANSFORM_BASE_VALUES: [number, number] = [90, 110]; +/** + * Value ranges of the resulting species BST transformations after adding values to original species + * All remaining Pokemon in the party use this range + */ +const STANDARD_BST_TRANSFORM_BASE_VALUES: [number, number] = [40, 50]; + +/** + * Weird Dream encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3822 | GitHub Issue #3822} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const WeirdDreamEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.WEIRD_DREAM) + .withEncounterTier(MysteryEncounterTier.ROGUE) + .withDisallowedChallenges(Challenges.SINGLE_TYPE) + .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + .withIntroSpriteConfigs([ + { + spriteKey: "weird_dream_woman", + fileRoot: "mystery-encounters", + hasShadow: true, + y: 11, + yShadow: 6, + x: 4 + }, + ]) + .withIntroDialogue([ + { + text: `${namespace}.intro`, + }, + { + speaker: `${namespace}.speaker`, + text: `${namespace}.intro_dialogue`, + }, + ]) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withOnInit((scene: BattleScene) => { + scene.loadBgm("mystery_encounter_weird_dream", "mystery_encounter_weird_dream.mp3"); + return true; + }) + .withOnVisualsStart((scene: BattleScene) => { + scene.fadeAndSwitchBgm("mystery_encounter_weird_dream"); + return true; + }) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withHasDexProgress(true) + .withDialogue({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + } + ], + }) + .withPreOptionPhase(async (scene: BattleScene) => { + // Play the animation as the player goes through the dialogue + scene.time.delayedCall(1000, () => { + doShowDreamBackground(scene); + }); + + // Calculate all the newly transformed Pokemon and begin asset load + const teamTransformations = getTeamTransformations(scene); + const loadAssets = teamTransformations.map(t => (t.newPokemon as PlayerPokemon).loadAssets()); + scene.currentBattle.mysteryEncounter!.misc = { + teamTransformations, + loadAssets + }; + }) + .withOptionPhase(async (scene: BattleScene) => { + // Starts cutscene dialogue, but does not await so that cutscene plays as player goes through dialogue + const cutsceneDialoguePromise = showEncounterText(scene, `${namespace}.option.1.cutscene`); + + // Change the entire player's party + // Wait for all new Pokemon assets to be loaded before showing transformation animations + await Promise.all(scene.currentBattle.mysteryEncounter!.misc.loadAssets); + const transformations = scene.currentBattle.mysteryEncounter!.misc.teamTransformations; + + // If there are 1-3 transformations, do them centered back to back + // Otherwise, the first 3 transformations are executed side-by-side, then any remaining 1-3 transformations occur in those same respective positions + if (transformations.length <= 3) { + for (const transformation of transformations) { + const pokemon1 = transformation.previousPokemon; + const pokemon2 = transformation.newPokemon; + + await doPokemonTransformationSequence(scene, pokemon1, pokemon2, TransformationScreenPosition.CENTER); + } + } else { + await doSideBySideTransformations(scene, transformations); + } + + // Make sure player has finished cutscene dialogue + await cutsceneDialoguePromise; + + doHideDreamBackground(scene); + await showEncounterText(scene, `${namespace}.option.1.dream_complete`); + + await doNewTeamPostProcess(scene, transformations); + setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.MEMORY_MUSHROOM, modifierTypes.ROGUE_BALL, modifierTypes.MINT, modifierTypes.MINT]}); + leaveEncounterWithoutBattle(scene, true); + }) + .build() + ) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Reduce party levels by 20% + for (const pokemon of scene.getParty()) { + pokemon.level = Math.max(Math.ceil((100 - PERCENT_LEVEL_LOSS_ON_REFUSE) / 100 * pokemon.level), 1); + pokemon.exp = getLevelTotalExp(pokemon.level, pokemon.species.growthRate); + pokemon.levelExp = 0; + + pokemon.calculateStats(); + pokemon.updateInfo(); + } + + leaveEncounterWithoutBattle(scene, true); + return true; + } + ) + .build(); + +interface PokemonTransformation { + previousPokemon: PlayerPokemon; + newSpecies: PokemonSpecies; + newPokemon: PlayerPokemon; + heldItems: PokemonHeldItemModifier[]; +} + +function getTeamTransformations(scene: BattleScene): PokemonTransformation[] { + const party = scene.getParty(); + // Removes all pokemon from the party + const alreadyUsedSpecies: PokemonSpecies[] = []; + const pokemonTransformations: PokemonTransformation[] = party.map(p => { + return { + previousPokemon: p + } as PokemonTransformation; + }); + + // Only 1 Pokemon can be transformed into BST higher than 600 + let hasPokemonInSuperLegendaryBstThreshold = false; + // Only 1 other Pokemon can be transformed into BST between 570-600 + let hasPokemonInLegendaryBstThreshold = false; + + // First, roll 2 of the party members to new Pokemon at a +90 to +110 BST difference + // Then, roll the remainder of the party members at a +40 to +50 BST difference + const numPokemon = party.length; + for (let i = 0; i < numPokemon; i++) { + const removed = party[randSeedInt(party.length)]; + const index = pokemonTransformations.findIndex(p => p.previousPokemon.id === removed.id); + pokemonTransformations[index].heldItems = removed.getHeldItems().filter(m => !(m instanceof PokemonFormChangeItemModifier)); + scene.removePokemonFromPlayerParty(removed, false); + + const bst = removed.calculateBaseStats().reduce((a, b) => a + b, 0); + let newBstRange: [number, number]; + if (i < 2) { + newBstRange = HIGH_BST_TRANSFORM_BASE_VALUES; + } else { + newBstRange = STANDARD_BST_TRANSFORM_BASE_VALUES; + } + + const newSpecies = getTransformedSpecies(bst, newBstRange, hasPokemonInSuperLegendaryBstThreshold, hasPokemonInLegendaryBstThreshold, alreadyUsedSpecies); + + const newSpeciesBst = newSpecies.getBaseStatTotal(); + if (newSpeciesBst > SUPER_LEGENDARY_BST_THRESHOLD) { + hasPokemonInSuperLegendaryBstThreshold = true; + } + if (newSpeciesBst <= SUPER_LEGENDARY_BST_THRESHOLD && newSpeciesBst >= NON_LEGENDARY_BST_THRESHOLD) { + hasPokemonInLegendaryBstThreshold = true; + } + + + pokemonTransformations[index].newSpecies = newSpecies; + alreadyUsedSpecies.push(newSpecies); + } + + for (const transformation of pokemonTransformations) { + const newAbilityIndex = randSeedInt(transformation.newSpecies.getAbilityCount()); + const newPlayerPokemon = scene.addPlayerPokemon(transformation.newSpecies, transformation.previousPokemon.level, newAbilityIndex, undefined); + transformation.newPokemon = newPlayerPokemon; + scene.getParty().push(newPlayerPokemon); + } + + return pokemonTransformations; +} + +async function doNewTeamPostProcess(scene: BattleScene, transformations: PokemonTransformation[]) { + let atLeastOneNewStarter = false; + for (const transformation of transformations) { + const previousPokemon = transformation.previousPokemon; + const newPokemon = transformation.newPokemon; + const speciesRootForm = newPokemon.species.getRootSpeciesId(); + + // Roll HA a second time + if (newPokemon.species.abilityHidden) { + const hiddenIndex = newPokemon.species.ability2 ? 2 : 1; + if (newPokemon.abilityIndex < hiddenIndex) { + const hiddenAbilityChance = new IntegerHolder(256); + scene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance); + + const hasHiddenAbility = !randSeedInt(hiddenAbilityChance.value); + + if (hasHiddenAbility) { + newPokemon.abilityIndex = hiddenIndex; + } + } + } + + // Roll IVs a second time + newPokemon.ivs = newPokemon.ivs.map(iv => { + const newValue = randSeedInt(31); + return newValue > iv ? newValue : iv; + }); + + // For pokemon at/below 570 BST or any shiny pokemon, unlock it permanently as if you had caught it + if (newPokemon.getSpeciesForm().getBaseStatTotal() <= NON_LEGENDARY_BST_THRESHOLD || newPokemon.isShiny()) { + if (newPokemon.getSpeciesForm().abilityHidden && newPokemon.abilityIndex === newPokemon.getSpeciesForm().getAbilityCount() - 1) { + scene.validateAchv(achvs.HIDDEN_ABILITY); + } + + if (newPokemon.species.subLegendary) { + scene.validateAchv(achvs.CATCH_SUB_LEGENDARY); + } + + if (newPokemon.species.legendary) { + scene.validateAchv(achvs.CATCH_LEGENDARY); + } + + if (newPokemon.species.mythical) { + scene.validateAchv(achvs.CATCH_MYTHICAL); + } + + scene.gameData.updateSpeciesDexIvs(newPokemon.species.getRootSpeciesId(true), newPokemon.ivs); + const newStarterUnlocked = await scene.gameData.setPokemonCaught(newPokemon, true, false, false); + if (newStarterUnlocked) { + atLeastOneNewStarter = true; + await showEncounterText(scene, i18next.t("battle:addedAsAStarter", { pokemonName: getPokemonSpecies(speciesRootForm).getName() })); + } + } + + // If the previous pokemon had pokerus, transfer to new pokemon + newPokemon.pokerus = previousPokemon.pokerus; + + // If the previous pokemon had higher IVs, override to those (after updating dex IVs > prevents perfect 31s on a new unlock) + newPokemon.ivs = newPokemon.ivs.map((iv, index) => { + return previousPokemon.ivs[index] > iv ? previousPokemon.ivs[index] : iv; + }); + + // For pokemon that the player owns (including ones just caught), gain a candy + if (!!scene.gameData.dexData[speciesRootForm].caughtAttr) { + scene.gameData.addStarterCandy(getPokemonSpecies(speciesRootForm), 1); + } + + // Set the moveset of the new pokemon to be the same as previous, but with 1 egg move and 1 (attempted) STAB move of the new species + newPokemon.generateAndPopulateMoveset(); + + // Try to find a favored STAB move + let favoredMove; + for (const move of newPokemon.moveset) { + // Needs to match first type, second type will be replaced + if (move?.getMove().type === newPokemon.getTypes()[0]) { + favoredMove = move; + break; + } + } + // If was unable to find a move, uses first move in moveset (typically a high power STAB move) + favoredMove = favoredMove ?? newPokemon.moveset[0]; + + newPokemon.moveset = previousPokemon.moveset; + let eggMoveIndex: null | number = null; + if (speciesEggMoves.hasOwnProperty(speciesRootForm)) { + const eggMoves = speciesEggMoves[speciesRootForm]; + const randomEggMoveIndex = randSeedInt(4); + const randomEggMove = eggMoves[randomEggMoveIndex]; + if (newPokemon.moveset.length < 4) { + newPokemon.moveset.push(new PokemonMove(randomEggMove)); + } else { + eggMoveIndex = randSeedInt(4); + newPokemon.moveset[eggMoveIndex] = new PokemonMove(randomEggMove); + } + // For pokemon that the player owns (including ones just caught), unlock the egg move + if (!!scene.gameData.dexData[speciesRootForm].caughtAttr) { + await scene.gameData.setEggMoveUnlocked(getPokemonSpecies(speciesRootForm), randomEggMoveIndex, true); + } + } + if (favoredMove) { + let favoredMoveIndex = randSeedInt(4); + while (favoredMoveIndex === eggMoveIndex) { + favoredMoveIndex = randSeedInt(4); + } + + newPokemon.moveset[favoredMoveIndex] = favoredMove; + } + + // Randomize the second type of the pokemon + // If the pokemon does not normally have a second type, it will gain 1 + const newTypes = [newPokemon.getTypes()[0]]; + let newType = randSeedInt(18) as Type; + while (newType === newTypes[0]) { + newType = randSeedInt(18) as Type; + } + newTypes.push(newType); + if (!newPokemon.mysteryEncounterPokemonData) { + newPokemon.mysteryEncounterPokemonData = new MysteryEncounterPokemonData(); + } + newPokemon.mysteryEncounterPokemonData.types = newTypes; + + for (const item of transformation.heldItems) { + item.pokemonId = newPokemon.id; + scene.addModifier(item, false, false, false, true); + } + + // Any pokemon that is at or below 450 BST gets +20 permanent BST to 3 stats: HP (halved, +10), lowest of Atk/SpAtk, and lowest of Def/SpDef + if (newPokemon.getSpeciesForm().getBaseStatTotal() <= GAIN_OLD_GATEAU_ITEM_BST_THRESHOLD) { + const stats: Stat[] = [Stat.HP]; + const baseStats = newPokemon.getSpeciesForm().baseStats.slice(0); + // Attack or SpAtk + stats.push(baseStats[Stat.ATK] < baseStats[Stat.SPATK] ? Stat.ATK : Stat.SPATK); + // Def or SpDef + stats.push(baseStats[Stat.DEF] < baseStats[Stat.SPDEF] ? Stat.DEF : Stat.SPDEF); + // const mod = modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU().newModifier(newPokemon, 20, stats); + const modType = modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU().generateType(scene.getParty(), [20, stats]); + const modifier = modType?.newModifier(newPokemon); + if (modifier) { + scene.addModifier(modifier); + } + } + + // Enable passive if previous had it + newPokemon.passive = previousPokemon.passive; + + newPokemon.calculateStats(); + newPokemon.initBattleInfo(); + } + + // One random pokemon will get its passive unlocked + const passiveDisabledPokemon = scene.getParty().filter(p => !p.passive); + if (passiveDisabledPokemon?.length > 0) { + passiveDisabledPokemon[randSeedInt(passiveDisabledPokemon.length)].passive = true; + } + + // If at least one new starter was unlocked, play 1 fanfare + if (atLeastOneNewStarter) { + scene.playSound("level_up_fanfare"); + } +} + +function getTransformedSpecies(originalBst: number, bstSearchRange: [number, number], hasPokemonBstHigherThan600: boolean, hasPokemonBstBetween570And600: boolean, alreadyUsedSpecies: PokemonSpecies[]): PokemonSpecies { + let newSpecies: PokemonSpecies | undefined; + while (isNullOrUndefined(newSpecies)) { + const bstCap = originalBst + bstSearchRange[1]; + const bstMin = Math.max(originalBst + bstSearchRange[0], 0); + + // Get any/all species that fall within the Bst range requirements + let validSpecies = allSpecies + .filter(s => { + const speciesBst = s.getBaseStatTotal(); + const bstInRange = speciesBst >= bstMin && speciesBst <= bstCap; + // Checks that a Pokemon has not already been added in the +600 or 570-600 slots; + const validBst = (!hasPokemonBstBetween570And600 || (speciesBst < NON_LEGENDARY_BST_THRESHOLD || speciesBst > SUPER_LEGENDARY_BST_THRESHOLD)) && + (!hasPokemonBstHigherThan600 || speciesBst <= SUPER_LEGENDARY_BST_THRESHOLD); + return bstInRange && validBst && !EXCLUDED_TRANSFORMATION_SPECIES.includes(s.speciesId); + }); + + // There must be at least 20 species available before it will choose one + if (validSpecies?.length > 20) { + validSpecies = randSeedShuffle(validSpecies); + newSpecies = validSpecies.pop(); + while (isNullOrUndefined(newSpecies) || alreadyUsedSpecies.includes(newSpecies!)) { + newSpecies = validSpecies.pop(); + } + } else { + // Expands search rand until a Pokemon is found + bstSearchRange[0] -= 10; + bstSearchRange[1] += 10; + } + } + + return newSpecies!; +} + +function doShowDreamBackground(scene: BattleScene) { + const transformationContainer = scene.add.container(0, -scene.game.canvas.height / 6); + transformationContainer.name = "Dream Background"; + + // In case it takes a bit for video to load + const transformationStaticBg = scene.add.rectangle(0, 0, scene.game.canvas.width / 6, scene.game.canvas.height / 6, 0); + transformationStaticBg.setName("Black Background"); + transformationStaticBg.setOrigin(0, 0); + transformationContainer.add(transformationStaticBg); + transformationStaticBg.setVisible(true); + + const transformationVideoBg: Phaser.GameObjects.Video = scene.add.video(0, 0, "evo_bg").stop(); + transformationVideoBg.setLoop(true); + transformationVideoBg.setOrigin(0, 0); + transformationVideoBg.setScale(0.4359673025); + transformationContainer.add(transformationVideoBg); + + scene.fieldUI.add(transformationContainer); + scene.fieldUI.bringToTop(transformationContainer); + transformationVideoBg.play(); + + transformationContainer.setVisible(true); + transformationContainer.alpha = 0; + + scene.tweens.add({ + targets: transformationContainer, + alpha: 1, + duration: 3000, + ease: "Sine.easeInOut" + }); +} + +function doHideDreamBackground(scene: BattleScene) { + const transformationContainer = scene.fieldUI.getByName("Dream Background"); + + scene.tweens.add({ + targets: transformationContainer, + alpha: 0, + duration: 3000, + ease: "Sine.easeInOut", + onComplete: () => { + scene.fieldUI.remove(transformationContainer, true); + } + }); +} + +function doSideBySideTransformations(scene: BattleScene, transformations: PokemonTransformation[]) { + return new Promise(resolve => { + const allTransformationPromises: Promise[] = []; + for (let i = 0; i < 3; i++) { + const delay = i * 4000; + scene.time.delayedCall(delay, () => { + const transformation = transformations[i]; + const pokemon1 = transformation.previousPokemon; + const pokemon2 = transformation.newPokemon; + const screenPosition = i as TransformationScreenPosition; + + const transformationPromise = doPokemonTransformationSequence(scene, pokemon1, pokemon2, screenPosition) + .then(() => { + if (transformations.length > i + 3) { + const nextTransformationAtPosition = transformations[i + 3]; + const nextPokemon1 = nextTransformationAtPosition.previousPokemon; + const nextPokemon2 = nextTransformationAtPosition.newPokemon; + + allTransformationPromises.push(doPokemonTransformationSequence(scene, nextPokemon1, nextPokemon2, screenPosition)); + } + }); + allTransformationPromises.push(transformationPromise); + }); + } + + // Wait for all transformations to be loaded into promise array + const id = setInterval(checkAllPromisesExist, 500); + async function checkAllPromisesExist() { + if (allTransformationPromises.length === transformations.length) { + clearInterval(id); + await Promise.all(allTransformationPromises); + resolve(); + } + } + }); +} diff --git a/src/data/mystery-encounters/mystery-encounter-dialogue.ts b/src/data/mystery-encounters/mystery-encounter-dialogue.ts new file mode 100644 index 00000000000..e0ba8512d34 --- /dev/null +++ b/src/data/mystery-encounters/mystery-encounter-dialogue.ts @@ -0,0 +1,75 @@ +import { TextStyle } from "#app/ui/text"; + +export class TextDisplay { + speaker?: string; + text: string; + style?: TextStyle; +} + +export class OptionTextDisplay { + buttonLabel: string; + buttonTooltip?: string; + disabledButtonLabel?: string; + disabledButtonTooltip?: string; + secondOptionPrompt?: string; + selected?: TextDisplay[]; + style?: TextStyle; +} + +export class EncounterOptionsDialogue { + title?: string; + description?: string; + query?: string; + /** Options array with minimum 2 options */ + options?: [...OptionTextDisplay[]]; +} + +/** + * Example MysteryEncounterDialogue object: + * + { + intro: [ + { + text: "this is a rendered as a message window (no title display)" + }, + { + speaker: "John" + text: "this is a rendered as a dialogue window (title "John" is displayed above text)" + } + ], + encounterOptionsDialogue: { + title: "This is the title displayed at top of encounter description box", + description: "This is the description in the middle of encounter description box", + query: "This is an optional question displayed at the bottom of the description box (keep it short)", + options: [ + { + buttonLabel: "Option #1 button label (keep these short)", + selected: [ // Optional dialogue windows displayed when specific option is selected and before functional logic for the option is executed + { + text: "You chose option #1 message" + }, + { + speaker: "John" + text: "So, you've chosen option #1! It's time to d-d-d-duel!" + } + ] + }, + { + buttonLabel: "Option #2" + } + ], + }, + outro: [ + { + text: "This message will be displayed at the very end of the encounter (i.e. post battle, post reward, etc.)" + } + ], + } + * + */ +export default class MysteryEncounterDialogue { + intro?: TextDisplay[]; + encounterOptionsDialogue?: EncounterOptionsDialogue; + outro?: TextDisplay[]; +} + diff --git a/src/data/mystery-encounters/mystery-encounter-option.ts b/src/data/mystery-encounters/mystery-encounter-option.ts new file mode 100644 index 00000000000..865877445c1 --- /dev/null +++ b/src/data/mystery-encounters/mystery-encounter-option.ts @@ -0,0 +1,303 @@ +import { OptionTextDisplay } from "#app/data/mystery-encounters/mystery-encounter-dialogue"; +import { Moves } from "#app/enums/moves"; +import Pokemon, { PlayerPokemon } from "#app/field/pokemon"; +import BattleScene from "#app/battle-scene"; +import { Type } from "../type"; +import { EncounterPokemonRequirement, EncounterSceneRequirement, MoneyRequirement, TypeRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; +import { CanLearnMoveRequirement, CanLearnMoveRequirementOptions } from "./requirements/can-learn-move-requirement"; +import { isNullOrUndefined, randSeedInt } from "#app/utils"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; + + +export type OptionPhaseCallback = (scene: BattleScene) => Promise; + +/** + * Used by {@linkcode MysteryEncounterOptionBuilder} class to define required/optional properties on the {@linkcode MysteryEncounterOption} class when building. + * + * Should ONLY contain properties that are necessary for {@linkcode MysteryEncounterOption} construction. + * Post-construct and flag data properties are defined in the {@linkcode MysteryEncounterOption} class itself. + */ +export interface IMysteryEncounterOption { + optionMode: MysteryEncounterOptionMode; + hasDexProgress: boolean; + requirements: EncounterSceneRequirement[]; + primaryPokemonRequirements: EncounterPokemonRequirement[]; + secondaryPokemonRequirements: EncounterPokemonRequirement[]; + excludePrimaryFromSecondaryRequirements: boolean; + + dialogue?: OptionTextDisplay; + + onPreOptionPhase?: OptionPhaseCallback; + onOptionPhase: OptionPhaseCallback; + onPostOptionPhase?: OptionPhaseCallback; +} + +export default class MysteryEncounterOption implements IMysteryEncounterOption { + optionMode: MysteryEncounterOptionMode; + hasDexProgress: boolean; + requirements: EncounterSceneRequirement[]; + primaryPokemonRequirements: EncounterPokemonRequirement[]; + secondaryPokemonRequirements: EncounterPokemonRequirement[]; + primaryPokemon?: PlayerPokemon; + secondaryPokemon?: PlayerPokemon[]; + excludePrimaryFromSecondaryRequirements: boolean; + + /** + * Dialogue object containing all the dialogue, messages, tooltips, etc. for this option + * Will be populated on {@linkcode MysteryEncounter} initialization + */ + dialogue?: OptionTextDisplay; + + /** Executes before any following dialogue or business logic from option. Usually this will be for calculating dialogueTokens or performing scene/data updates */ + onPreOptionPhase?: OptionPhaseCallback; + /** Business logic function for option */ + onOptionPhase: OptionPhaseCallback; + /** Executes after the encounter is over. Usually this will be for calculating dialogueTokens or performing data updates */ + onPostOptionPhase?: OptionPhaseCallback; + + constructor(option: IMysteryEncounterOption | null) { + if (!isNullOrUndefined(option)) { + Object.assign(this, option); + } + this.hasDexProgress = this.hasDexProgress ?? false; + this.requirements = this.requirements ?? []; + this.primaryPokemonRequirements = this.primaryPokemonRequirements ?? []; + this.secondaryPokemonRequirements = this.secondaryPokemonRequirements ?? []; + } + + /** + * Returns true if option contains any {@linkcode EncounterRequirement}s, false otherwise. + */ + hasRequirements(): boolean { + return this.requirements.length > 0 || this.primaryPokemonRequirements.length > 0 || this.secondaryPokemonRequirements.length > 0; + } + + /** + * Returns true if all {@linkcode EncounterRequirement}s for the option are met + * @param scene + */ + meetsRequirements(scene: BattleScene): boolean { + return !this.requirements.some(requirement => !requirement.meetsRequirement(scene)) + && this.meetsSupportingRequirementAndSupportingPokemonSelected(scene) + && this.meetsPrimaryRequirementAndPrimaryPokemonSelected(scene); + } + + /** + * Returns true if all PRIMARY {@linkcode EncounterRequirement}s for the option are met + * @param scene + * @param pokemon + */ + pokemonMeetsPrimaryRequirements(scene: BattleScene, pokemon: Pokemon): boolean { + return !this.primaryPokemonRequirements.some(req => !req.queryParty(scene.getParty()).map(p => p.id).includes(pokemon.id)); + } + + /** + * Returns true if all PRIMARY {@linkcode EncounterRequirement}s for the option are met, + * AND there is a valid Pokemon assigned to {@linkcode primaryPokemon}. + * If both {@linkcode primaryPokemonRequirements} and {@linkcode secondaryPokemonRequirements} are defined, + * can cause scenarios where there are not enough Pokemon that are sufficient for all requirements. + * @param scene + */ + meetsPrimaryRequirementAndPrimaryPokemonSelected(scene: BattleScene): boolean { + if (!this.primaryPokemonRequirements || this.primaryPokemonRequirements.length === 0) { + return true; + } + let qualified: PlayerPokemon[] = scene.getParty(); + for (const req of this.primaryPokemonRequirements) { + if (req.meetsRequirement(scene)) { + const queryParty = req.queryParty(scene.getParty()); + qualified = qualified.filter(pkmn => queryParty.includes(pkmn)); + } else { + this.primaryPokemon = undefined; + return false; + } + } + + if (qualified.length === 0) { + return false; + } + + if (this.excludePrimaryFromSecondaryRequirements && this.secondaryPokemon && this.secondaryPokemon.length > 0) { + const truePrimaryPool: PlayerPokemon[] = []; + const overlap: PlayerPokemon[] = []; + for (const qp of qualified) { + if (!this.secondaryPokemon.includes(qp)) { + truePrimaryPool.push(qp); + } else { + overlap.push(qp); + } + + } + if (truePrimaryPool.length > 0) { + // always choose from the non-overlapping pokemon first + this.primaryPokemon = truePrimaryPool[randSeedInt(truePrimaryPool.length)]; + return true; + } else { + // if there are multiple overlapping pokemon, we're okay - just choose one and take it out of the supporting pokemon pool + if (overlap.length > 1 || (this.secondaryPokemon.length - overlap.length >= 1)) { + this.primaryPokemon = overlap[randSeedInt(overlap.length)]; + this.secondaryPokemon = this.secondaryPokemon.filter((supp) => supp !== this.primaryPokemon); + return true; + } + console.log("Mystery Encounter Edge Case: Requirement not met due to primay pokemon overlapping with support pokemon. There's no valid primary pokemon left."); + return false; + } + } else { + // Just pick the first qualifying Pokemon + this.primaryPokemon = qualified[0]; + return true; + } + } + + /** + * Returns true if all SECONDARY {@linkcode EncounterRequirement}s for the option are met, + * AND there is a valid Pokemon assigned to {@linkcode secondaryPokemon} (if applicable). + * If both {@linkcode primaryPokemonRequirements} and {@linkcode secondaryPokemonRequirements} are defined, + * can cause scenarios where there are not enough Pokemon that are sufficient for all requirements. + * @param scene + */ + meetsSupportingRequirementAndSupportingPokemonSelected(scene: BattleScene): boolean { + if (!this.secondaryPokemonRequirements || this.secondaryPokemonRequirements.length === 0) { + this.secondaryPokemon = []; + return true; + } + + let qualified: PlayerPokemon[] = scene.getParty(); + for (const req of this.secondaryPokemonRequirements) { + if (req.meetsRequirement(scene)) { + const queryParty = req.queryParty(scene.getParty()); + qualified = qualified.filter(pkmn => queryParty.includes(pkmn)); + } else { + this.secondaryPokemon = []; + return false; + } + } + this.secondaryPokemon = qualified; + return true; + } +} + +export class MysteryEncounterOptionBuilder implements Partial { + optionMode: MysteryEncounterOptionMode = MysteryEncounterOptionMode.DEFAULT; + requirements: EncounterSceneRequirement[] = []; + primaryPokemonRequirements: EncounterPokemonRequirement[] = []; + secondaryPokemonRequirements: EncounterPokemonRequirement[] = []; + excludePrimaryFromSecondaryRequirements: boolean = false; + isDisabledOnRequirementsNotMet: boolean = true; + hasDexProgress: boolean = false; + dialogue?: OptionTextDisplay; + + static newOptionWithMode(optionMode: MysteryEncounterOptionMode): MysteryEncounterOptionBuilder & Pick { + return Object.assign(new MysteryEncounterOptionBuilder(), { optionMode }); + } + + withHasDexProgress(hasDexProgress: boolean): this & Required> { + return Object.assign(this, { hasDexProgress: hasDexProgress }); + } + + /** + * Adds a {@linkcode EncounterSceneRequirement} to {@linkcode requirements} + * @param requirement + */ + withSceneRequirement(requirement: EncounterSceneRequirement): this & Required> { + if (requirement instanceof EncounterPokemonRequirement) { + Error("Incorrectly added pokemon requirement as scene requirement."); + } + + this.requirements.push(requirement); + return Object.assign(this, { requirements: this.requirements }); + } + + withSceneMoneyRequirement(requiredMoney?: number, scalingMultiplier?: number) { + return this.withSceneRequirement(new MoneyRequirement(requiredMoney, scalingMultiplier)); + } + + /** + * Defines logic that runs immediately when an option is selected, but before the Encounter continues. + * Can determine whether or not the Encounter *should* continue. + * If there are scenarios where the Encounter should NOT continue, should return boolean instead of void. + * @param onPreOptionPhase + */ + withPreOptionPhase(onPreOptionPhase: OptionPhaseCallback): this & Required> { + return Object.assign(this, { onPreOptionPhase: onPreOptionPhase }); + } + + /** + * MUST be defined by every {@linkcode MysteryEncounterOption} + * @param onOptionPhase + */ + withOptionPhase(onOptionPhase: OptionPhaseCallback): this & Required> { + return Object.assign(this, { onOptionPhase: onOptionPhase }); + } + + withPostOptionPhase(onPostOptionPhase: OptionPhaseCallback): this & Required> { + return Object.assign(this, { onPostOptionPhase: onPostOptionPhase }); + } + + /** + * Adds a {@linkcode EncounterPokemonRequirement} to {@linkcode primaryPokemonRequirements} + * @param requirement + */ + withPrimaryPokemonRequirement(requirement: EncounterPokemonRequirement): this & Required> { + if (requirement instanceof EncounterSceneRequirement) { + Error("Incorrectly added scene requirement as pokemon requirement."); + } + + this.primaryPokemonRequirements.push(requirement); + return Object.assign(this, { primaryPokemonRequirements: this.primaryPokemonRequirements }); + } + + /** + * Player is required to have certain type/s of pokemon in his party (with optional min number of pokemons with that type) + * + * @param type the required type/s + * @param excludeFainted whether to exclude fainted pokemon + * @param minNumberOfPokemon number of pokemons to have that type + * @param invertQuery + * @returns + */ + withPokemonTypeRequirement(type: Type | Type[], excludeFainted?: boolean, minNumberOfPokemon?: number, invertQuery?: boolean) { + return this.withPrimaryPokemonRequirement(new TypeRequirement(type, excludeFainted, minNumberOfPokemon, invertQuery)); + } + + /** + * Player is required to have a pokemon that can learn a certain move/moveset + * + * @param move the required move/moves + * @param options see {@linkcode CanLearnMoveRequirementOptions} + * @returns + */ + withPokemonCanLearnMoveRequirement(move: Moves | Moves[], options?: CanLearnMoveRequirementOptions) { + return this.withPrimaryPokemonRequirement(new CanLearnMoveRequirement(move, options)); + } + + /** + * Adds a {@linkcode EncounterPokemonRequirement} to {@linkcode secondaryPokemonRequirements} + * @param requirement + * @param excludePrimaryFromSecondaryRequirements + */ + withSecondaryPokemonRequirement(requirement: EncounterPokemonRequirement, excludePrimaryFromSecondaryRequirements: boolean = true): this & Required> { + if (requirement instanceof EncounterSceneRequirement) { + Error("Incorrectly added scene requirement as pokemon requirement."); + } + + this.secondaryPokemonRequirements.push(requirement); + this.excludePrimaryFromSecondaryRequirements = excludePrimaryFromSecondaryRequirements; + return Object.assign(this, { secondaryPokemonRequirements: this.secondaryPokemonRequirements }); + } + + /** + * Set the full dialogue object to the option. Will override anything already set + * + * @param dialogue see {@linkcode OptionTextDisplay} + * @returns + */ + withDialogue(dialogue: OptionTextDisplay) { + this.dialogue = dialogue; + return this; + } + + build(this: IMysteryEncounterOption) { + return new MysteryEncounterOption(this); + } +} diff --git a/src/data/mystery-encounters/mystery-encounter-pokemon-data.ts b/src/data/mystery-encounters/mystery-encounter-pokemon-data.ts new file mode 100644 index 00000000000..fc6ce313d41 --- /dev/null +++ b/src/data/mystery-encounters/mystery-encounter-pokemon-data.ts @@ -0,0 +1,25 @@ +import { Abilities } from "#enums/abilities"; +import { Type } from "#app/data/type"; +import { isNullOrUndefined } from "#app/utils"; + +/** + * Data that can customize a Pokemon in non-standard ways from its Species + * Currently only used by Mystery Encounters, may need to be renamed if it becomes more widely used + */ +export class MysteryEncounterPokemonData { + public spriteScale: number; + public ability: Abilities | -1; + public passive: Abilities | -1; + public types: Type[]; + + constructor(data?: MysteryEncounterPokemonData | Partial) { + if (!isNullOrUndefined(data)) { + Object.assign(this, data); + } + + this.spriteScale = this.spriteScale ?? -1; + this.ability = this.ability ?? -1; + this.passive = this.passive ?? -1; + this.types = this.types ?? []; + } +} diff --git a/src/data/mystery-encounters/mystery-encounter-requirements.ts b/src/data/mystery-encounters/mystery-encounter-requirements.ts new file mode 100644 index 00000000000..1141b492d42 --- /dev/null +++ b/src/data/mystery-encounters/mystery-encounter-requirements.ts @@ -0,0 +1,1036 @@ +import { PlayerPokemon } from "#app/field/pokemon"; +import BattleScene from "#app/battle-scene"; +import { isNullOrUndefined } from "#app/utils"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { TimeOfDay } from "#enums/time-of-day"; +import { Nature } from "../nature"; +import { EvolutionItem, pokemonEvolutions } from "../pokemon-evolutions"; +import { FormChangeItem, pokemonFormChanges, SpeciesFormChangeItemTrigger } from "../pokemon-forms"; +import { SpeciesFormKey } from "../pokemon-species"; +import { StatusEffect } from "../status-effect"; +import { Type } from "../type"; +import { WeatherType } from "../weather"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { AttackTypeBoosterModifier } from "#app/modifier/modifier"; +import { AttackTypeBoosterModifierType } from "#app/modifier/modifier-type"; + +export interface EncounterRequirement { + meetsRequirement(scene: BattleScene): boolean; // Boolean to see if a requirement is met + getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string]; +} + +export abstract class EncounterSceneRequirement implements EncounterRequirement { + /** + * Returns whether the EncounterSceneRequirement's... requirements, are met by the given scene + * @param partyPokemon + */ + abstract meetsRequirement(scene: BattleScene): boolean; + /** + * Returns a dialogue token key/value pair for a given Requirement. + * Should be overridden by child Requirement classes. + * @param scene + * @param pokemon + */ + abstract getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string]; +} + +export class CombinationSceneRequirement extends EncounterSceneRequirement { + orRequirements: EncounterSceneRequirement[]; + + constructor(... orRequirements: EncounterSceneRequirement[]) { + super(); + this.orRequirements = orRequirements; + } + + override meetsRequirement(scene: BattleScene): boolean { + for (const req of this.orRequirements) { + if (req.meetsRequirement(scene)) { + return true; + } + } + return false; + } + + override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + for (const req of this.orRequirements) { + if (req.meetsRequirement(scene)) { + return req.getDialogueToken(scene, pokemon); + } + } + + return this.orRequirements[0].getDialogueToken(scene, pokemon); + } +} + +export abstract class EncounterPokemonRequirement implements EncounterRequirement { + public minNumberOfPokemon: number; + public invertQuery: boolean; + + /** + * Returns whether the EncounterPokemonRequirement's... requirements, are met by the given scene + * @param partyPokemon + */ + abstract meetsRequirement(scene: BattleScene): boolean; + + /** + * Returns all party members that are compatible with this requirement. For non pokemon related requirements, the entire party is returned. + * @param partyPokemon + */ + abstract queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[]; + + /** + * Returns a dialogue token key/value pair for a given Requirement. + * Should be overridden by child Requirement classes. + * @param scene + * @param pokemon + */ + abstract getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string]; +} + +export class CombinationPokemonRequirement extends EncounterPokemonRequirement { + orRequirements: EncounterPokemonRequirement[]; + + constructor(...orRequirements: EncounterPokemonRequirement[]) { + super(); + this.invertQuery = false; + this.minNumberOfPokemon = 1; + this.orRequirements = orRequirements; + } + + override meetsRequirement(scene: BattleScene): boolean { + for (const req of this.orRequirements) { + if (req.meetsRequirement(scene)) { + return true; + } + } + return false; + } + + override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { + for (const req of this.orRequirements) { + const result = req.queryParty(partyPokemon); + if (result?.length > 0) { + return result; + } + } + + return []; + } + + override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + for (const req of this.orRequirements) { + if (req.meetsRequirement(scene)) { + return req.getDialogueToken(scene, pokemon); + } + } + + return this.orRequirements[0].getDialogueToken(scene, pokemon); + } +} + +export class PreviousEncounterRequirement extends EncounterSceneRequirement { + previousEncounterRequirement: MysteryEncounterType; + + /** + * Used for specifying an encounter that must be seen before this encounter can spawn + * @param previousEncounterRequirement + */ + constructor(previousEncounterRequirement: MysteryEncounterType) { + super(); + this.previousEncounterRequirement = previousEncounterRequirement; + } + + override meetsRequirement(scene: BattleScene): boolean { + return scene.mysteryEncounterSaveData.encounteredEvents.some(e => e.type === this.previousEncounterRequirement); + } + + override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + return ["previousEncounter", scene.mysteryEncounterSaveData.encounteredEvents.find(e => e.type === this.previousEncounterRequirement)?.[0].toString() ?? ""]; + } +} + +export class WaveRangeRequirement extends EncounterSceneRequirement { + waveRange: [number, number]; + + /** + * Used for specifying a unique wave or wave range requirement + * If minWaveIndex and maxWaveIndex are equivalent, will check for exact wave number + * @param waveRange [min, max] + */ + constructor(waveRange: [number, number]) { + super(); + this.waveRange = waveRange; + } + + override meetsRequirement(scene: BattleScene): boolean { + if (!isNullOrUndefined(this.waveRange) && this.waveRange?.[0] <= this.waveRange?.[1]) { + const waveIndex = scene.currentBattle.waveIndex; + if (waveIndex >= 0 && (this.waveRange?.[0] >= 0 && this.waveRange?.[0] > waveIndex) || (this.waveRange?.[1] >= 0 && this.waveRange?.[1] < waveIndex)) { + return false; + } + } + return true; + } + + override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + return ["waveIndex", scene.currentBattle.waveIndex.toString()]; + } +} + +export class WaveModulusRequirement extends EncounterSceneRequirement { + waveModuli: number[]; + modulusValue: number; + + /** + * Used for specifying a modulus requirement on the wave index + * For example, can be used to require the wave index to end with 1, 2, or 3 + * @param waveModuli The allowed modulus results + * @param modulusValue The modulus calculation value + * + * Example: + * new WaveModulusRequirement([1, 2, 3], 10) will check for 1st/2nd/3rd waves that are immediately after a multiple of 10 wave + * So waves 21, 32, 53 all return true. 58, 14, 99 return false. + */ + constructor(waveModuli: number[], modulusValue: number) { + super(); + this.waveModuli = waveModuli; + this.modulusValue = modulusValue; + } + + override meetsRequirement(scene: BattleScene): boolean { + return this.waveModuli.includes(scene.currentBattle.waveIndex % this.modulusValue); + } + + override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + return ["waveIndex", scene.currentBattle.waveIndex.toString()]; + } +} + +export class TimeOfDayRequirement extends EncounterSceneRequirement { + requiredTimeOfDay: TimeOfDay[]; + + constructor(timeOfDay: TimeOfDay | TimeOfDay[]) { + super(); + this.requiredTimeOfDay = Array.isArray(timeOfDay) ? timeOfDay : [timeOfDay]; + } + + override meetsRequirement(scene: BattleScene): boolean { + const timeOfDay = scene.arena?.getTimeOfDay(); + if (!isNullOrUndefined(timeOfDay) && this.requiredTimeOfDay?.length > 0 && !this.requiredTimeOfDay.includes(timeOfDay)) { + return false; + } + + return true; + } + + override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + return ["timeOfDay", TimeOfDay[scene.arena.getTimeOfDay()].toLocaleLowerCase()]; + } +} + +export class WeatherRequirement extends EncounterSceneRequirement { + requiredWeather: WeatherType[]; + + constructor(weather: WeatherType | WeatherType[]) { + super(); + this.requiredWeather = Array.isArray(weather) ? weather : [weather]; + } + + override meetsRequirement(scene: BattleScene): boolean { + const currentWeather = scene.arena.weather?.weatherType; + if (!isNullOrUndefined(currentWeather) && this.requiredWeather?.length > 0 && !this.requiredWeather.includes(currentWeather!)) { + return false; + } + + return true; + } + + override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + const currentWeather = scene.arena.weather?.weatherType; + let token = ""; + if (!isNullOrUndefined(currentWeather)) { + token = WeatherType[currentWeather!].replace("_", " ").toLocaleLowerCase(); + } + return ["weather", token]; + } +} + +export class PartySizeRequirement extends EncounterSceneRequirement { + partySizeRange: [number, number]; + excludeDisallowedPokemon: boolean; + + /** + * Used for specifying a party size requirement + * If min and max are equivalent, will check for exact size + * @param partySizeRange + * @param excludeDisallowedPokemon + */ + constructor(partySizeRange: [number, number], excludeDisallowedPokemon: boolean) { + super(); + this.partySizeRange = partySizeRange; + this.excludeDisallowedPokemon = excludeDisallowedPokemon; + } + + override meetsRequirement(scene: BattleScene): boolean { + if (!isNullOrUndefined(this.partySizeRange) && this.partySizeRange?.[0] <= this.partySizeRange?.[1]) { + const partySize = this.excludeDisallowedPokemon ? scene.getParty().filter(p => p.isAllowedInBattle()).length : scene.getParty().length; + if (partySize >= 0 && (this.partySizeRange?.[0] >= 0 && this.partySizeRange?.[0] > partySize) || (this.partySizeRange?.[1] >= 0 && this.partySizeRange?.[1] < partySize)) { + return false; + } + } + + return true; + } + + override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + return ["partySize", scene.getParty().length.toString()]; + } +} + +export class PersistentModifierRequirement extends EncounterSceneRequirement { + requiredHeldItemModifiers: string[]; + minNumberOfItems: number; + + constructor(heldItem: string | string[], minNumberOfItems: number = 1) { + super(); + this.minNumberOfItems = minNumberOfItems; + this.requiredHeldItemModifiers = Array.isArray(heldItem) ? heldItem : [heldItem]; + } + + override meetsRequirement(scene: BattleScene): boolean { + const partyPokemon = scene.getParty(); + if (isNullOrUndefined(partyPokemon) || this.requiredHeldItemModifiers?.length < 0) { + return false; + } + let modifierCount = 0; + this.requiredHeldItemModifiers.forEach(modifier => { + const matchingMods = scene.findModifiers(m => m.constructor.name === modifier); + if (matchingMods?.length > 0) { + matchingMods.forEach(matchingMod => { + modifierCount += matchingMod.stackCount; + }); + } + }); + + return modifierCount >= this.minNumberOfItems; + } + + override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + return ["requiredItem", this.requiredHeldItemModifiers[0]]; + } +} + +export class MoneyRequirement extends EncounterSceneRequirement { + requiredMoney: number; // Static value + scalingMultiplier: number; // Calculates required money based off wave index + + constructor(requiredMoney?: number, scalingMultiplier?: number) { + super(); + this.requiredMoney = requiredMoney ?? 0; + this.scalingMultiplier = scalingMultiplier ?? 0; + } + + override meetsRequirement(scene: BattleScene): boolean { + const money = scene.money; + if (isNullOrUndefined(money)) { + return false; + } + + if (this.scalingMultiplier > 0) { + this.requiredMoney = scene.getWaveMoneyAmount(this.scalingMultiplier); + } + return !(this.requiredMoney > 0 && this.requiredMoney > money); + } + + override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + const value = this.scalingMultiplier > 0 ? scene.getWaveMoneyAmount(this.scalingMultiplier).toString() : this.requiredMoney.toString(); + return ["money", value]; + } +} + +export class SpeciesRequirement extends EncounterPokemonRequirement { + requiredSpecies: Species[]; + minNumberOfPokemon: number; + invertQuery: boolean; + + constructor(species: Species | Species[], minNumberOfPokemon: number = 1, invertQuery: boolean = false) { + super(); + this.minNumberOfPokemon = minNumberOfPokemon; + this.invertQuery = invertQuery; + this.requiredSpecies = Array.isArray(species) ? species : [species]; + } + + override meetsRequirement(scene: BattleScene): boolean { + const partyPokemon = scene.getParty(); + if (isNullOrUndefined(partyPokemon) || this.requiredSpecies?.length < 0) { + return false; + } + return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; + } + + override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { + if (!this.invertQuery) { + return partyPokemon.filter((pokemon) => this.requiredSpecies.filter((species) => pokemon.species.speciesId === species).length > 0); + } else { + // for an inverted query, we only want to get the pokemon that don't have ANY of the listed speciess + return partyPokemon.filter((pokemon) => this.requiredSpecies.filter((species) => pokemon.species.speciesId === species).length === 0); + } + } + + override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + if (pokemon?.species.speciesId && this.requiredSpecies.includes(pokemon.species.speciesId)) { + return ["species", Species[pokemon.species.speciesId]]; + } + return ["species", ""]; + } +} + + +export class NatureRequirement extends EncounterPokemonRequirement { + requiredNature: Nature[]; + minNumberOfPokemon: number; + invertQuery: boolean; + + constructor(nature: Nature | Nature[], minNumberOfPokemon: number = 1, invertQuery: boolean = false) { + super(); + this.minNumberOfPokemon = minNumberOfPokemon; + this.invertQuery = invertQuery; + this.requiredNature = Array.isArray(nature) ? nature : [nature]; + } + + override meetsRequirement(scene: BattleScene): boolean { + const partyPokemon = scene.getParty(); + if (isNullOrUndefined(partyPokemon) || this.requiredNature?.length < 0) { + return false; + } + return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; + } + + override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { + if (!this.invertQuery) { + return partyPokemon.filter((pokemon) => this.requiredNature.filter((nature) => pokemon.nature === nature).length > 0); + } else { + // for an inverted query, we only want to get the pokemon that don't have ANY of the listed natures + return partyPokemon.filter((pokemon) => this.requiredNature.filter((nature) => pokemon.nature === nature).length === 0); + } + } + + override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + if (!isNullOrUndefined(pokemon?.nature) && this.requiredNature.includes(pokemon!.nature)) { + return ["nature", Nature[pokemon!.nature]]; + } + return ["nature", ""]; + } +} + +export class TypeRequirement extends EncounterPokemonRequirement { + requiredType: Type[]; + excludeFainted: boolean; + minNumberOfPokemon: number; + invertQuery: boolean; + + constructor(type: Type | Type[], excludeFainted: boolean = true, minNumberOfPokemon: number = 1, invertQuery: boolean = false) { + super(); + this.excludeFainted = excludeFainted; + this.minNumberOfPokemon = minNumberOfPokemon; + this.invertQuery = invertQuery; + this.requiredType = Array.isArray(type) ? type : [type]; + } + + override meetsRequirement(scene: BattleScene): boolean { + let partyPokemon = scene.getParty(); + + if (isNullOrUndefined(partyPokemon)) { + return false; + } + + if (this.excludeFainted) { + partyPokemon = partyPokemon.filter((pokemon) => !pokemon.isFainted()); + } + + return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; + } + + override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { + if (!this.invertQuery) { + return partyPokemon.filter((pokemon) => this.requiredType.filter((type) => pokemon.getTypes().includes(type)).length > 0); + } else { + // for an inverted query, we only want to get the pokemon that don't have ANY of the listed types + return partyPokemon.filter((pokemon) => this.requiredType.filter((type) => pokemon.getTypes().includes(type)).length === 0); + } + } + + override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + const includedTypes = this.requiredType.filter((ty) => pokemon?.getTypes().includes(ty)); + if (includedTypes.length > 0) { + return ["type", Type[includedTypes[0]]]; + } + return ["type", ""]; + } +} + + +export class MoveRequirement extends EncounterPokemonRequirement { + requiredMoves: Moves[] = []; + minNumberOfPokemon: number; + invertQuery: boolean; + + constructor(moves: Moves | Moves[], minNumberOfPokemon: number = 1, invertQuery: boolean = false) { + super(); + this.minNumberOfPokemon = minNumberOfPokemon; + this.invertQuery = invertQuery; + this.requiredMoves = Array.isArray(moves) ? moves : [moves]; + } + + override meetsRequirement(scene: BattleScene): boolean { + const partyPokemon = scene.getParty(); + if (isNullOrUndefined(partyPokemon) || this.requiredMoves?.length < 0) { + return false; + } + return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; + } + + override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { + if (!this.invertQuery) { + return partyPokemon.filter((pokemon) => this.requiredMoves.filter((reqMove) => pokemon.moveset.filter((move) => move?.moveId === reqMove).length > 0).length > 0); + } else { + // for an inverted query, we only want to get the pokemon that don't have ANY of the listed moves + return partyPokemon.filter((pokemon) => this.requiredMoves.filter((reqMove) => pokemon.moveset.filter((move) => move?.moveId === reqMove).length === 0).length === 0); + } + } + + override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + const includedMoves = pokemon?.moveset.filter((move) => move?.moveId && this.requiredMoves.includes(move.moveId)); + if (includedMoves && includedMoves.length > 0 && includedMoves[0]) { + return ["move", includedMoves[0].getName()]; + } + return ["move", ""]; + } + +} + +/** + * Find out if Pokemon in the party are able to learn one of many specific moves by TM. + * NOTE: Egg moves are not included as learnable. + * NOTE: If the Pokemon already knows the move, this requirement will fail, since it's not technically learnable. + */ +export class CompatibleMoveRequirement extends EncounterPokemonRequirement { + requiredMoves: Moves[]; + minNumberOfPokemon: number; + invertQuery: boolean; + + constructor(learnableMove: Moves | Moves[], minNumberOfPokemon: number = 1, invertQuery: boolean = false) { + super(); + this.minNumberOfPokemon = minNumberOfPokemon; + this.invertQuery = invertQuery; + this.requiredMoves = Array.isArray(learnableMove) ? learnableMove : [learnableMove]; + } + + override meetsRequirement(scene: BattleScene): boolean { + const partyPokemon = scene.getParty(); + if (isNullOrUndefined(partyPokemon) || this.requiredMoves?.length < 0) { + return false; + } + return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; + } + + override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { + if (!this.invertQuery) { + return partyPokemon.filter((pokemon) => this.requiredMoves.filter((learnableMove) => pokemon.compatibleTms.filter(tm => !pokemon.moveset.find(m => m?.moveId === tm)).includes(learnableMove)).length > 0); + } else { + // for an inverted query, we only want to get the pokemon that don't have ANY of the listed learnableMoves + return partyPokemon.filter((pokemon) => this.requiredMoves.filter((learnableMove) => pokemon.compatibleTms.filter(tm => !pokemon.moveset.find(m => m?.moveId === tm)).includes(learnableMove)).length === 0); + } + } + + override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + const includedCompatMoves = this.requiredMoves.filter((reqMove) => pokemon?.compatibleTms.filter((tm) => !pokemon.moveset.find(m => m?.moveId === tm)).includes(reqMove)); + if (includedCompatMoves.length > 0) { + return ["compatibleMove", Moves[includedCompatMoves[0]]]; + } + return ["compatibleMove", ""]; + } + +} + +export class AbilityRequirement extends EncounterPokemonRequirement { + requiredAbilities: Abilities[]; + minNumberOfPokemon: number; + invertQuery: boolean; + + constructor(abilities: Abilities | Abilities[], minNumberOfPokemon: number = 1, invertQuery: boolean = false) { + super(); + this.minNumberOfPokemon = minNumberOfPokemon; + this.invertQuery = invertQuery; + this.requiredAbilities = Array.isArray(abilities) ? abilities : [abilities]; + } + + override meetsRequirement(scene: BattleScene): boolean { + const partyPokemon = scene.getParty(); + if (isNullOrUndefined(partyPokemon) || this.requiredAbilities?.length < 0) { + return false; + } + return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; + } + + override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { + if (!this.invertQuery) { + return partyPokemon.filter((pokemon) => this.requiredAbilities.some((ability) => pokemon.getAbility().id === ability)); + } else { + // for an inverted query, we only want to get the pokemon that don't have ANY of the listed abilitiess + return partyPokemon.filter((pokemon) => this.requiredAbilities.filter((ability) => pokemon.getAbility().id === ability).length === 0); + } + } + + override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + if (pokemon?.getAbility().id && this.requiredAbilities.some(a => pokemon.getAbility().id === a)) { + return ["ability", pokemon.getAbility().name]; + } + return ["ability", ""]; + } +} + +export class StatusEffectRequirement extends EncounterPokemonRequirement { + requiredStatusEffect: StatusEffect[]; + minNumberOfPokemon: number; + invertQuery: boolean; + + constructor(statusEffect: StatusEffect | StatusEffect[], minNumberOfPokemon: number = 1, invertQuery: boolean = false) { + super(); + this.minNumberOfPokemon = minNumberOfPokemon; + this.invertQuery = invertQuery; + this.requiredStatusEffect = Array.isArray(statusEffect) ? statusEffect : [statusEffect]; + } + + override meetsRequirement(scene: BattleScene): boolean { + const partyPokemon = scene.getParty(); + if (isNullOrUndefined(partyPokemon) || this.requiredStatusEffect?.length < 0) { + return false; + } + const x = this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; + console.log(x); + return x; + } + + override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { + if (!this.invertQuery) { + return partyPokemon.filter((pokemon) => { + return this.requiredStatusEffect.some((statusEffect) => { + if (statusEffect === StatusEffect.NONE) { + // StatusEffect.NONE also checks for null or undefined status + return isNullOrUndefined(pokemon.status) || isNullOrUndefined(pokemon.status!.effect) || pokemon.status?.effect === statusEffect; + } else { + return pokemon.status?.effect === statusEffect; + } + }); + }); + } else { + // for an inverted query, we only want to get the pokemon that don't have ANY of the listed StatusEffects + // return partyPokemon.filter((pokemon) => this.requiredStatusEffect.filter((statusEffect) => pokemon.status?.effect === statusEffect).length === 0); + return partyPokemon.filter((pokemon) => { + return !this.requiredStatusEffect.some((statusEffect) => { + if (statusEffect === StatusEffect.NONE) { + // StatusEffect.NONE also checks for null or undefined status + return isNullOrUndefined(pokemon.status) || isNullOrUndefined(pokemon.status!.effect) || pokemon.status?.effect === statusEffect; + } else { + return pokemon.status?.effect === statusEffect; + } + }); + }); + } + } + + override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + const reqStatus = this.requiredStatusEffect.filter((a) => { + if (a === StatusEffect.NONE) { + return isNullOrUndefined(pokemon?.status) || isNullOrUndefined(pokemon!.status!.effect) || pokemon!.status!.effect === a; + } + return pokemon!.status?.effect === a; + }); + if (reqStatus.length > 0) { + return ["status", StatusEffect[reqStatus[0]]]; + } + return ["status", ""]; + } + +} + +/** + * Finds if there are pokemon that can form change with a given item. + * Notice that we mean specific items, like Charizardite, not the Mega Bracelet. + * If you want to trigger the event based on the form change enabler, use PersistentModifierRequirement. + */ +export class CanFormChangeWithItemRequirement extends EncounterPokemonRequirement { + requiredFormChangeItem: FormChangeItem[]; + minNumberOfPokemon: number; + invertQuery: boolean; + + constructor(formChangeItem: FormChangeItem | FormChangeItem[], minNumberOfPokemon: number = 1, invertQuery: boolean = false) { + super(); + this.minNumberOfPokemon = minNumberOfPokemon; + this.invertQuery = invertQuery; + this.requiredFormChangeItem = Array.isArray(formChangeItem) ? formChangeItem : [formChangeItem]; + } + + override meetsRequirement(scene: BattleScene): boolean { + const partyPokemon = scene.getParty(); + if (isNullOrUndefined(partyPokemon) || this.requiredFormChangeItem?.length < 0) { + return false; + } + return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; + } + + filterByForm(pokemon, formChangeItem) { + if (pokemonFormChanges.hasOwnProperty(pokemon.species.speciesId) + // Get all form changes for this species with an item trigger, including any compound triggers + && pokemonFormChanges[pokemon.species.speciesId].filter(fc => fc.trigger.hasTriggerType(SpeciesFormChangeItemTrigger)) + // Returns true if any form changes match this item + .map(fc => fc.findTrigger(SpeciesFormChangeItemTrigger) as SpeciesFormChangeItemTrigger) + .flat().flatMap(fc => fc.item).includes(formChangeItem)) { + return true; + } else { + return false; + } + } + + override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { + if (!this.invertQuery) { + return partyPokemon.filter((pokemon) => this.requiredFormChangeItem.filter((formChangeItem) => this.filterByForm(pokemon, formChangeItem)).length > 0); + } else { + // for an inverted query, we only want to get the pokemon that don't have ANY of the listed formChangeItems + return partyPokemon.filter((pokemon) => this.requiredFormChangeItem.filter((formChangeItem) => this.filterByForm(pokemon, formChangeItem)).length === 0); + } + } + + override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + const requiredItems = this.requiredFormChangeItem.filter((formChangeItem) => this.filterByForm(pokemon, formChangeItem)); + if (requiredItems.length > 0) { + return ["formChangeItem", FormChangeItem[requiredItems[0]]]; + } + return ["formChangeItem", ""]; + } + +} + +export class CanEvolveWithItemRequirement extends EncounterPokemonRequirement { + requiredEvolutionItem: EvolutionItem[]; + minNumberOfPokemon: number; + invertQuery: boolean; + + constructor(evolutionItems: EvolutionItem | EvolutionItem[], minNumberOfPokemon: number = 1, invertQuery: boolean = false) { + super(); + this.minNumberOfPokemon = minNumberOfPokemon; + this.invertQuery = invertQuery; + this.requiredEvolutionItem = Array.isArray(evolutionItems) ? evolutionItems : [evolutionItems]; + } + + override meetsRequirement(scene: BattleScene): boolean { + const partyPokemon = scene.getParty(); + if (isNullOrUndefined(partyPokemon) || this.requiredEvolutionItem?.length < 0) { + return false; + } + return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; + } + + filterByEvo(pokemon, evolutionItem) { + if (pokemonEvolutions.hasOwnProperty(pokemon.species.speciesId) && pokemonEvolutions[pokemon.species.speciesId].filter(e => e.item === evolutionItem + && (!e.condition || e.condition.predicate(pokemon))).length && (pokemon.getFormKey() !== SpeciesFormKey.GIGANTAMAX)) { + return true; + } else if (pokemon.isFusion() && pokemonEvolutions.hasOwnProperty(pokemon.fusionSpecies.speciesId) && pokemonEvolutions[pokemon.fusionSpecies.speciesId].filter(e => e.item === evolutionItem + && (!e.condition || e.condition.predicate(pokemon))).length && (pokemon.getFusionFormKey() !== SpeciesFormKey.GIGANTAMAX)) { + return true; + } + return false; + } + + override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { + if (!this.invertQuery) { + return partyPokemon.filter((pokemon) => this.requiredEvolutionItem.filter((evolutionItem) => this.filterByEvo(pokemon, evolutionItem)).length > 0); + } else { + // for an inverted query, we only want to get the pokemon that don't have ANY of the listed evolutionItemss + return partyPokemon.filter((pokemon) => this.requiredEvolutionItem.filter((evolutionItems) => this.filterByEvo(pokemon, evolutionItems)).length === 0); + } + } + + override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + const requiredItems = this.requiredEvolutionItem.filter((evoItem) => this.filterByEvo(pokemon, evoItem)); + if (requiredItems.length > 0) { + return ["evolutionItem", EvolutionItem[requiredItems[0]]]; + } + return ["evolutionItem", ""]; + } +} + +export class HeldItemRequirement extends EncounterPokemonRequirement { + requiredHeldItemModifiers: string[]; + minNumberOfPokemon: number; + invertQuery: boolean; + requireTransferable: boolean; + + constructor(heldItem: string | string[], minNumberOfPokemon: number = 1, invertQuery: boolean = false, requireTransferable: boolean = true) { + super(); + this.minNumberOfPokemon = minNumberOfPokemon; + this.invertQuery = invertQuery; + this.requiredHeldItemModifiers = Array.isArray(heldItem) ? heldItem : [heldItem]; + this.requireTransferable = requireTransferable; + } + + override meetsRequirement(scene: BattleScene): boolean { + const partyPokemon = scene.getParty(); + if (isNullOrUndefined(partyPokemon)) { + return false; + } + return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; + } + + override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { + if (!this.invertQuery) { + return partyPokemon.filter((pokemon) => this.requiredHeldItemModifiers.some((heldItem) => { + return pokemon.getHeldItems().some((it) => { + return it.constructor.name === heldItem && (!this.requireTransferable || it.isTransferable); + }); + })); + } else { + // for an inverted query, we only want to get the pokemon that have any held items that are NOT in requiredHeldItemModifiers + // E.g. functions as a blacklist + return partyPokemon.filter((pokemon) => pokemon.getHeldItems().filter((it) => { + return !this.requiredHeldItemModifiers.some(heldItem => it.constructor.name === heldItem) + && (!this.requireTransferable || it.isTransferable); + }).length > 0); + } + } + + override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + const requiredItems = pokemon?.getHeldItems().filter((it) => { + return this.requiredHeldItemModifiers.some(heldItem => it.constructor.name === heldItem) + && (!this.requireTransferable || it.isTransferable); + }); + if (requiredItems && requiredItems.length > 0) { + return ["heldItem", requiredItems[0].type.name]; + } + return ["heldItem", ""]; + } +} + +export class AttackTypeBoosterHeldItemTypeRequirement extends EncounterPokemonRequirement { + requiredHeldItemTypes: Type[]; + minNumberOfPokemon: number; + invertQuery: boolean; + requireTransferable: boolean; + + constructor(heldItemTypes: Type | Type[], minNumberOfPokemon: number = 1, invertQuery: boolean = false, requireTransferable: boolean = true) { + super(); + this.minNumberOfPokemon = minNumberOfPokemon; + this.invertQuery = invertQuery; + this.requiredHeldItemTypes = Array.isArray(heldItemTypes) ? heldItemTypes : [heldItemTypes]; + this.requireTransferable = requireTransferable; + } + + override meetsRequirement(scene: BattleScene): boolean { + const partyPokemon = scene.getParty(); + if (isNullOrUndefined(partyPokemon)) { + return false; + } + return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; + } + + override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { + if (!this.invertQuery) { + return partyPokemon.filter((pokemon) => this.requiredHeldItemTypes.some((heldItemType) => { + return pokemon.getHeldItems().some((it) => { + return it instanceof AttackTypeBoosterModifier + && (it.type as AttackTypeBoosterModifierType).moveType === heldItemType + && (!this.requireTransferable || it.isTransferable); + }); + })); + } else { + // for an inverted query, we only want to get the pokemon that have any held items that are NOT in requiredHeldItemModifiers + // E.g. functions as a blacklist + return partyPokemon.filter((pokemon) => pokemon.getHeldItems().filter((it) => { + return !this.requiredHeldItemTypes.some(heldItemType => + it instanceof AttackTypeBoosterModifier + && (it.type as AttackTypeBoosterModifierType).moveType === heldItemType + && (!this.requireTransferable || it.isTransferable)); + }).length > 0); + } + } + + override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + const requiredItems = pokemon?.getHeldItems().filter((it) => { + return this.requiredHeldItemTypes.some(heldItemType => + it instanceof AttackTypeBoosterModifier + && (it.type as AttackTypeBoosterModifierType).moveType === heldItemType) + && (!this.requireTransferable || it.isTransferable); + }); + if (requiredItems && requiredItems.length > 0) { + return ["heldItem", requiredItems[0].type.name]; + } + return ["heldItem", ""]; + } +} + +export class LevelRequirement extends EncounterPokemonRequirement { + requiredLevelRange: [number, number]; + minNumberOfPokemon: number; + invertQuery: boolean; + + constructor(requiredLevelRange: [number, number], minNumberOfPokemon: number = 1, invertQuery: boolean = false) { + super(); + this.minNumberOfPokemon = minNumberOfPokemon; + this.invertQuery = invertQuery; + this.requiredLevelRange = requiredLevelRange; + } + + override meetsRequirement(scene: BattleScene): boolean { + // Party Pokemon inside required level range + if (!isNullOrUndefined(this.requiredLevelRange) && this.requiredLevelRange[0] <= this.requiredLevelRange[1]) { + const partyPokemon = scene.getParty(); + const pokemonInRange = this.queryParty(partyPokemon); + if (pokemonInRange.length < this.minNumberOfPokemon) { + return false; + } + } + return true; + } + + override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { + if (!this.invertQuery) { + return partyPokemon.filter((pokemon) => pokemon.level >= this.requiredLevelRange[0] && pokemon.level <= this.requiredLevelRange[1]); + } else { + // for an inverted query, we only want to get the pokemon that don't have ANY of the listed requiredLevelRanges + return partyPokemon.filter((pokemon) => pokemon.level < this.requiredLevelRange[0] || pokemon.level > this.requiredLevelRange[1]); + } + } + + override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + return ["level", pokemon?.level.toString() ?? ""]; + } +} + +export class FriendshipRequirement extends EncounterPokemonRequirement { + requiredFriendshipRange: [number, number]; + minNumberOfPokemon: number; + invertQuery: boolean; + + constructor(requiredFriendshipRange: [number, number], minNumberOfPokemon: number = 1, invertQuery: boolean = false) { + super(); + this.minNumberOfPokemon = minNumberOfPokemon; + this.invertQuery = invertQuery; + this.requiredFriendshipRange = requiredFriendshipRange; + } + + override meetsRequirement(scene: BattleScene): boolean { + // Party Pokemon inside required friendship range + if (!isNullOrUndefined(this.requiredFriendshipRange) && this.requiredFriendshipRange[0] <= this.requiredFriendshipRange[1]) { + const partyPokemon = scene.getParty(); + const pokemonInRange = this.queryParty(partyPokemon); + if (pokemonInRange.length < this.minNumberOfPokemon) { + return false; + } + } + return true; + } + + override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { + if (!this.invertQuery) { + return partyPokemon.filter((pokemon) => pokemon.friendship >= this.requiredFriendshipRange[0] && pokemon.friendship <= this.requiredFriendshipRange[1]); + } else { + // for an inverted query, we only want to get the pokemon that don't have ANY of the listed requiredFriendshipRanges + return partyPokemon.filter((pokemon) => pokemon.friendship < this.requiredFriendshipRange[0] || pokemon.friendship > this.requiredFriendshipRange[1]); + } + } + + override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + return ["friendship", pokemon?.friendship.toString() ?? ""]; + } +} + +/** + * .1 -> 10% hp + * .5 -> 50% hp + * 1 -> 100% hp + */ +export class HealthRatioRequirement extends EncounterPokemonRequirement { + requiredHealthRange: [number, number]; + minNumberOfPokemon: number; + invertQuery: boolean; + + constructor(requiredHealthRange: [number, number], minNumberOfPokemon: number = 1, invertQuery: boolean = false) { + super(); + this.minNumberOfPokemon = minNumberOfPokemon; + this.invertQuery = invertQuery; + this.requiredHealthRange = requiredHealthRange; + } + + override meetsRequirement(scene: BattleScene): boolean { + // Party Pokemon's health inside required health range + if (!isNullOrUndefined(this.requiredHealthRange) && this.requiredHealthRange[0] <= this.requiredHealthRange[1]) { + const partyPokemon = scene.getParty(); + const pokemonInRange = this.queryParty(partyPokemon); + if (pokemonInRange.length < this.minNumberOfPokemon) { + return false; + } + } + return true; + } + + override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { + if (!this.invertQuery) { + return partyPokemon.filter((pokemon) => { + return pokemon.getHpRatio() >= this.requiredHealthRange[0] && pokemon.getHpRatio() <= this.requiredHealthRange[1]; + }); + } else { + // for an inverted query, we only want to get the pokemon that don't have ANY of the listed requiredHealthRanges + return partyPokemon.filter((pokemon) => pokemon.getHpRatio() < this.requiredHealthRange[0] || pokemon.getHpRatio() > this.requiredHealthRange[1]); + } + } + + override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + if (!isNullOrUndefined(pokemon?.getHpRatio())) { + return ["healthRatio", Math.floor(pokemon!.getHpRatio() * 100).toString() + "%"]; + } + return ["healthRatio", ""]; + } +} + +export class WeightRequirement extends EncounterPokemonRequirement { + requiredWeightRange: [number, number]; + minNumberOfPokemon: number; + invertQuery: boolean; + + constructor(requiredWeightRange: [number, number], minNumberOfPokemon: number = 1, invertQuery: boolean = false) { + super(); + this.minNumberOfPokemon = minNumberOfPokemon; + this.invertQuery = invertQuery; + this.requiredWeightRange = requiredWeightRange; + } + + override meetsRequirement(scene: BattleScene): boolean { + // Party Pokemon's weight inside required weight range + if (!isNullOrUndefined(this.requiredWeightRange) && this.requiredWeightRange[0] <= this.requiredWeightRange[1]) { + const partyPokemon = scene.getParty(); + const pokemonInRange = this.queryParty(partyPokemon); + if (pokemonInRange.length < this.minNumberOfPokemon) { + return false; + } + } + return true; + } + + override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { + if (!this.invertQuery) { + return partyPokemon.filter((pokemon) => pokemon.getWeight() >= this.requiredWeightRange[0] && pokemon.getWeight() <= this.requiredWeightRange[1]); + } else { + // for an inverted query, we only want to get the pokemon that don't have ANY of the listed requiredWeightRanges + return partyPokemon.filter((pokemon) => pokemon.getWeight() < this.requiredWeightRange[0] || pokemon.getWeight() > this.requiredWeightRange[1]); + } + } + + override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + return ["weight", pokemon?.getWeight().toString() ?? ""]; + } +} + + diff --git a/src/data/mystery-encounters/mystery-encounter-save-data.ts b/src/data/mystery-encounters/mystery-encounter-save-data.ts new file mode 100644 index 00000000000..259fbff7b85 --- /dev/null +++ b/src/data/mystery-encounters/mystery-encounter-save-data.ts @@ -0,0 +1,38 @@ +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT } from "#app/data/mystery-encounters/mystery-encounters"; +import { isNullOrUndefined } from "#app/utils"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; + +export class SeenEncounterData { + type: MysteryEncounterType; + tier: MysteryEncounterTier; + waveIndex: number; + selectedOption: number; + + constructor(type: MysteryEncounterType, tier: MysteryEncounterTier, waveIndex: number, selectedOption?: number) { + this.type = type; + this.tier = tier; + this.waveIndex = waveIndex; + this.selectedOption = selectedOption ?? -1; + } +} + +export interface QueuedEncounter { + type: MysteryEncounterType; + spawnPercent: number; // Out of 100 +} + +export class MysteryEncounterSaveData { + encounteredEvents: SeenEncounterData[] = []; + encounterSpawnChance: number = BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT; + queuedEncounters: QueuedEncounter[] = []; + + constructor(data?: MysteryEncounterSaveData) { + if (!isNullOrUndefined(data)) { + Object.assign(this, data); + } + + this.encounteredEvents = this.encounteredEvents ?? []; + this.queuedEncounters = this.queuedEncounters ?? []; + } +} diff --git a/src/data/mystery-encounters/mystery-encounter.ts b/src/data/mystery-encounters/mystery-encounter.ts new file mode 100644 index 00000000000..da4d29c94d6 --- /dev/null +++ b/src/data/mystery-encounters/mystery-encounter.ts @@ -0,0 +1,997 @@ +import { EnemyPartyConfig } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import Pokemon, { PlayerPokemon, PokemonMove } from "#app/field/pokemon"; +import { capitalizeFirstLetter, isNullOrUndefined } from "#app/utils"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import BattleScene from "#app/battle-scene"; +import MysteryEncounterIntroVisuals, { MysteryEncounterSpriteConfig } from "#app/field/mystery-encounter-intro"; +import * as Utils from "#app/utils"; +import { StatusEffect } from "../status-effect"; +import MysteryEncounterDialogue, { OptionTextDisplay } from "./mystery-encounter-dialogue"; +import MysteryEncounterOption, { MysteryEncounterOptionBuilder, OptionPhaseCallback } from "./mystery-encounter-option"; +import { EncounterPokemonRequirement, EncounterSceneRequirement, HealthRatioRequirement, PartySizeRequirement, StatusEffectRequirement, WaveRangeRequirement } from "./mystery-encounter-requirements"; +import { BattlerIndex } from "#app/battle"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { GameModes } from "#app/game-mode"; +import { EncounterAnim } from "#enums/encounter-anims"; +import { Challenges } from "#enums/challenges"; + +export interface EncounterStartOfBattleEffect { + sourcePokemon?: Pokemon; + sourceBattlerIndex?: BattlerIndex; + targets: BattlerIndex[]; + move: PokemonMove; + ignorePp: boolean; + followUp?: boolean; +} + +const DEFAULT_MAX_ALLOWED_ENCOUNTERS = 2; +const DEFAULT_MAX_ALLOWED_ROGUE_ENCOUNTERS = 1; + +/** + * Used by {@linkcode MysteryEncounterBuilder} class to define required/optional properties on the {@linkcode MysteryEncounter} class when building. + * + * Should ONLY contain properties that are necessary for {@linkcode MysteryEncounter} construction. + * Post-construct and flag data properties are defined in the {@linkcode MysteryEncounter} class itself. + */ +export interface IMysteryEncounter { + encounterType: MysteryEncounterType; + options: [MysteryEncounterOption, MysteryEncounterOption, ...MysteryEncounterOption[]]; + spriteConfigs: MysteryEncounterSpriteConfig[]; + encounterTier: MysteryEncounterTier; + encounterAnimations?: EncounterAnim[]; + disallowedGameModes?: GameModes[]; + disallowedChallenges?: Challenges[]; + hideBattleIntroMessage: boolean; + autoHideIntroVisuals: boolean; + enterIntroVisualsFromRight: boolean; + catchAllowed: boolean; + fleeAllowed: boolean; + continuousEncounter: boolean; + maxAllowedEncounters: number; + hasBattleAnimationsWithoutTargets: boolean; + skipEnemyBattleTurns: boolean; + skipToFightInput: boolean; + + onInit?: (scene: BattleScene) => boolean; + onVisualsStart?: (scene: BattleScene) => boolean; + doEncounterExp?: (scene: BattleScene) => boolean; + doEncounterRewards?: (scene: BattleScene) => boolean; + doContinueEncounter?: (scene: BattleScene) => Promise; + + requirements: EncounterSceneRequirement[]; + primaryPokemonRequirements: EncounterPokemonRequirement[]; + secondaryPokemonRequirements: EncounterPokemonRequirement[]; + excludePrimaryFromSupportRequirements: boolean; + + dialogue: MysteryEncounterDialogue; + enemyPartyConfigs: EnemyPartyConfig[]; + + dialogueTokens: Record; + expMultiplier: number; +} + +/** + * MysteryEncounter class that defines the logic for a single encounter + * These objects will be saved as part of session data any time the player is on a floor with an encounter + * Unless you know what you're doing, you should use MysteryEncounterBuilder to create an instance for this class + */ +export default class MysteryEncounter implements IMysteryEncounter { + // #region Required params + + encounterType: MysteryEncounterType; + options: [MysteryEncounterOption, MysteryEncounterOption, ...MysteryEncounterOption[]]; + spriteConfigs: MysteryEncounterSpriteConfig[]; + + // #region Optional params + + encounterTier: MysteryEncounterTier; + /** + * Custom battle animations that are configured for encounter effects and visuals + * Specify here so that assets are loaded on initialization of encounter + */ + encounterAnimations?: EncounterAnim[]; + /** + * If specified, defines any game modes where the {@linkcode MysteryEncounter} should *NOT* spawn + */ + disallowedGameModes?: GameModes[]; + /** + * If specified, defines any challenges (from Challenge game mode) where the {@linkcode MysteryEncounter} should *NOT* spawn + */ + disallowedChallenges?: Challenges[]; + /** + * If true, hides "A Wild X Appeared" etc. messages + * Default true + */ + hideBattleIntroMessage: boolean; + /** + * If true, when an option is selected the field visuals will fade out automatically + * Default false + */ + autoHideIntroVisuals: boolean; + /** + * Intro visuals on the field will slide in from the right instead of the left + * Default false + */ + enterIntroVisualsFromRight: boolean; + /** + * If true, allows catching a wild pokemon during the encounter + * Default false + */ + catchAllowed: boolean; + /** + * If true, allows fleeing from a wild encounter (trainer battle MEs auto-disable fleeing) + * Default true + */ + fleeAllowed: boolean; + /** + * If true, encounter will continuously run through multiple battles/puzzles/etc. instead of going to next wave + * MUST EVENTUALLY BE DISABLED TO CONTINUE TO NEXT WAVE + * Default false + */ + continuousEncounter: boolean; + /** + * Maximum number of times the encounter can be seen per run + * Rogue tier encounters default to 1, others default to 3 + */ + maxAllowedEncounters: number; + /** + * If true, encounter will not animate the target Pokemon as part of battle animations + * Used for encounters where it is not a "real" battle, but still uses battle animations and commands (see {@linkcode FunAndGamesEncounter} for an example) + */ + hasBattleAnimationsWithoutTargets: boolean; + /** + * If true, will skip enemy pokemon turns during battle for the encounter + * Used for encounters where it is not a "real" battle, but still uses battle animations and commands (see {@linkcode FunAndGamesEncounter} for an example) + */ + skipEnemyBattleTurns: boolean; + /** + * If true, will skip COMMAND input and go straight to FIGHT (move select) input menu + */ + skipToFightInput: boolean; + + // #region Event callback functions + + /** Event when Encounter is first loaded, use it for data conditioning */ + onInit?: (scene: BattleScene) => boolean; + /** Event when battlefield visuals have finished sliding in and the encounter dialogue begins */ + onVisualsStart?: (scene: BattleScene) => boolean; + /** Event triggered prior to {@linkcode CommandPhase}, during {@linkcode TurnInitPhase} */ + onTurnStart?: (scene: BattleScene) => boolean; + /** Event prior to any rewards logic in {@linkcode MysteryEncounterRewardsPhase} */ + onRewards?: (scene: BattleScene) => Promise; + /** Will provide the player party EXP before rewards are displayed for that wave */ + doEncounterExp?: (scene: BattleScene) => boolean; + /** Will provide the player a rewards shop for that wave */ + doEncounterRewards?: (scene: BattleScene) => boolean; + /** Will execute callback during VictoryPhase of a continuousEncounter */ + doContinueEncounter?: (scene: BattleScene) => Promise; + /** + * Can perform special logic when a ME battle is lost, before GameOver/battle retry prompt. + * Should return `true` if it is treated as "real" Game Over, `false` if not. + */ + onGameOver?: (scene: BattleScene) => boolean; + + /** + * Requirements + */ + requirements: EncounterSceneRequirement[]; + /** Primary Pokemon is a single pokemon randomly selected from the party that meet ALL primary pokemon requirements */ + primaryPokemonRequirements: EncounterPokemonRequirement[]; + /** + * Secondary Pokemon are pokemon that meet ALL secondary pokemon requirements + * Note that an individual requirement may require multiple pokemon, but the resulting pokemon after all secondary requirements are met may be lower than expected + * If the primary pokemon and secondary pokemon are the same and ExcludePrimaryFromSupportRequirements flag is true, primary pokemon may be promoted from secondary pool + */ + secondaryPokemonRequirements: EncounterPokemonRequirement[]; + excludePrimaryFromSupportRequirements: boolean; + primaryPokemon?: PlayerPokemon; + secondaryPokemon?: PlayerPokemon[]; + + // #region Post-construct / Auto-populated params + + /** + * Dialogue object containing all the dialogue, messages, tooltips, etc. for an encounter + */ + dialogue: MysteryEncounterDialogue; + /** + * Data used for setting up/initializing enemy party in battles + * Can store multiple configs so that one can be chosen based on option selected + * Should usually be defined in `onInit()` or `onPreOptionPhase()` + */ + enemyPartyConfigs: EnemyPartyConfig[]; + /** + * Object instance containing sprite data for an encounter when it is being spawned + * Otherwise, will be undefined + * You probably shouldn't do anything directly with this unless you have a very specific need + */ + introVisuals?: MysteryEncounterIntroVisuals; + + // #region Flags + + /** + * Can be set for uses programatic dialogue during an encounter (storing the name of one of the party's pokemon, etc.) + * Example use: see MYSTERIOUS_CHEST + */ + dialogueTokens: Record; + /** + * Should be set depending upon option selected as part of an encounter + * For example, if there is no battle as part of the encounter/selected option, should be set to NO_BATTLE + * Defaults to DEFAULT + */ + encounterMode: MysteryEncounterMode; + /** + * Flag for checking if it's the first time a shop is being shown for an encounter. + * Defaults to true so that the first shop does not override the specified rewards. + * Will be set to false after a shop is shown (so can't reroll same rarity items for free) + */ + lockEncounterRewardTiers: boolean; + /** + * Will be set automatically, indicates special moves in startOfBattleEffects are complete (so will not repeat) + */ + startOfBattleEffectsComplete: boolean; + /** + * Will be set by option select handlers automatically, and can be used to refer to which option was chosen by later phases + */ + selectedOption?: MysteryEncounterOption; + /** + * Will be set by option select handlers automatically, and can be used to refer to which option was chosen by later phases + */ + startOfBattleEffects: EncounterStartOfBattleEffect[] = []; + /** + * Can be set higher or lower based on the type of battle or exp gained for an option/encounter + * Defaults to 1 + */ + expMultiplier: number; + /** + * Can add any asset load promises here during onInit() to make sure the scene awaits the loads properly + */ + loadAssets: Promise[]; + /** + * Generic property to set any custom data required for the encounter + * Extremely useful for carrying state/data between onPreOptionPhase/onOptionPhase/onPostOptionPhase + */ + misc?: any; + /** + * Used for keeping RNG consistent on session resets, but increments when cycling through multiple "Encounters" on the same wave + * You should only need to interact via getter/update methods + */ + private seedOffset?: any; + + constructor(encounter: IMysteryEncounter | null) { + if (!isNullOrUndefined(encounter)) { + Object.assign(this, encounter); + } + this.encounterTier = this.encounterTier ?? MysteryEncounterTier.COMMON; + this.dialogue = this.dialogue ?? {}; + this.spriteConfigs = this.spriteConfigs ? [...this.spriteConfigs] : []; + // Default max is 1 for ROGUE encounters, 2 for others + this.maxAllowedEncounters = this.maxAllowedEncounters ?? this.encounterTier === MysteryEncounterTier.ROGUE ? DEFAULT_MAX_ALLOWED_ROGUE_ENCOUNTERS : DEFAULT_MAX_ALLOWED_ENCOUNTERS; + this.encounterMode = MysteryEncounterMode.DEFAULT; + this.requirements = this.requirements ? this.requirements : []; + this.hideBattleIntroMessage = this.hideBattleIntroMessage ?? false; + this.autoHideIntroVisuals = this.autoHideIntroVisuals ?? true; + this.enterIntroVisualsFromRight = this.enterIntroVisualsFromRight ?? false; + this.continuousEncounter = this.continuousEncounter ?? false; + + // Reset any dirty flags or encounter data + this.startOfBattleEffectsComplete = false; + this.lockEncounterRewardTiers = true; + this.dialogueTokens = {}; + this.enemyPartyConfigs = []; + this.startOfBattleEffects = []; + this.introVisuals = undefined; + this.misc = null; + this.expMultiplier = 1; + this.loadAssets = []; + } + + /** + * Checks if the current scene state meets the requirements for the {@linkcode MysteryEncounter} to spawn + * This is used to filter the pool of encounters down to only the ones with all requirements met + * @param scene + * @returns + */ + meetsRequirements(scene: BattleScene): boolean { + const sceneReq = !this.requirements.some(requirement => !requirement.meetsRequirement(scene)); + const secReqs = this.meetsSecondaryRequirementAndSecondaryPokemonSelected(scene); // secondary is checked first to handle cases of primary overlapping with secondary + const priReqs = this.meetsPrimaryRequirementAndPrimaryPokemonSelected(scene); + + return sceneReq && secReqs && priReqs; + } + + /** + * Checks if a specific player pokemon meets all given primary EncounterPokemonRequirements + * Used automatically as part of {@linkcode meetsRequirements}, but can also be used to manually check certain Pokemon where needed + * @param scene + * @param pokemon + */ + pokemonMeetsPrimaryRequirements(scene: BattleScene, pokemon: Pokemon): boolean { + return !this.primaryPokemonRequirements.some(req => !req.queryParty(scene.getParty()).map(p => p.id).includes(pokemon.id)); + } + + /** + * Returns true if all PRIMARY {@linkcode EncounterRequirement}s for the option are met, + * AND there is a valid Pokemon assigned to {@linkcode primaryPokemon}. + * If both {@linkcode primaryPokemonRequirements} and {@linkcode secondaryPokemonRequirements} are defined, + * can cause scenarios where there are not enough Pokemon that are sufficient for all requirements. + * @param scene + */ + private meetsPrimaryRequirementAndPrimaryPokemonSelected(scene: BattleScene): boolean { + if (!this.primaryPokemonRequirements || this.primaryPokemonRequirements.length === 0) { + const activeMon = scene.getParty().filter(p => p.isActive(true)); + if (activeMon.length > 0) { + this.primaryPokemon = activeMon[0]; + } else { + this.primaryPokemon = scene.getParty().filter(p => !p.isFainted())[0]; + } + return true; + } + let qualified: PlayerPokemon[] = scene.getParty(); + for (const req of this.primaryPokemonRequirements) { + if (req.meetsRequirement(scene)) { + qualified = qualified.filter(pkmn => req.queryParty(scene.getParty()).includes(pkmn)); + } else { + this.primaryPokemon = undefined; + return false; + } + } + + if (qualified.length === 0) { + return false; + } + + if (this.excludePrimaryFromSupportRequirements && this.secondaryPokemon && this.secondaryPokemon.length > 0) { + const truePrimaryPool: PlayerPokemon[] = []; + const overlap: PlayerPokemon[] = []; + for (const qp of qualified) { + if (!this.secondaryPokemon.includes(qp)) { + truePrimaryPool.push(qp); + } else { + overlap.push(qp); + } + + } + if (truePrimaryPool.length > 0) { + // Always choose from the non-overlapping pokemon first + this.primaryPokemon = truePrimaryPool[Utils.randSeedInt(truePrimaryPool.length, 0)]; + return true; + } else { + // If there are multiple overlapping pokemon, we're okay - just choose one and take it out of the primary pokemon pool + if (overlap.length > 1 || (this.secondaryPokemon.length - overlap.length >= 1)) { + // is this working? + this.primaryPokemon = overlap[Utils.randSeedInt(overlap.length, 0)]; + this.secondaryPokemon = this.secondaryPokemon.filter((supp) => supp !== this.primaryPokemon); + return true; + } + console.log("Mystery Encounter Edge Case: Requirement not met due to primary pokemon overlapping with secondary pokemon. There's no valid primary pokemon left."); + return false; + } + } else { + // this means we CAN have the same pokemon be a primary and secondary pokemon, so just choose any qualifying one randomly. + this.primaryPokemon = qualified[Utils.randSeedInt(qualified.length, 0)]; + return true; + } + } + + /** + * Returns true if all SECONDARY {@linkcode EncounterRequirement}s for the option are met, + * AND there is a valid Pokemon assigned to {@linkcode secondaryPokemon} (if applicable). + * If both {@linkcode primaryPokemonRequirements} and {@linkcode secondaryPokemonRequirements} are defined, + * can cause scenarios where there are not enough Pokemon that are sufficient for all requirements. + * @param scene + */ + private meetsSecondaryRequirementAndSecondaryPokemonSelected(scene: BattleScene): boolean { + if (!this.secondaryPokemonRequirements || this.secondaryPokemonRequirements.length === 0) { + this.secondaryPokemon = []; + return true; + } + + let qualified: PlayerPokemon[] = scene.getParty(); + for (const req of this.secondaryPokemonRequirements) { + if (req.meetsRequirement(scene)) { + qualified = qualified.filter(pkmn => req.queryParty(scene.getParty()).includes(pkmn)); + } else { + this.secondaryPokemon = []; + return false; + } + } + this.secondaryPokemon = qualified; + return true; + } + + /** + * Initializes encounter intro sprites based on the sprite configs defined in spriteConfigs + * @param scene + */ + initIntroVisuals(scene: BattleScene): void { + this.introVisuals = new MysteryEncounterIntroVisuals(scene, this); + } + + /** + * Auto-pushes dialogue tokens from the encounter (and option) requirements. + * Will use the first support pokemon in list + * For multiple support pokemon in the dialogue token, it will have to be overridden. + */ + populateDialogueTokensFromRequirements(scene: BattleScene): void { + this.meetsRequirements(scene); + if (this.requirements?.length > 0) { + for (const req of this.requirements) { + const dialogueToken = req.getDialogueToken(scene); + if (dialogueToken?.length === 2) { + this.setDialogueToken(...dialogueToken); + } + } + } + if (this.primaryPokemon && this.primaryPokemon.length > 0) { + this.setDialogueToken("primaryName", this.primaryPokemon.getNameToRender()); + for (const req of this.primaryPokemonRequirements) { + if (!req.invertQuery) { + const value = req.getDialogueToken(scene, this.primaryPokemon); + if (value?.length === 2) { + this.setDialogueToken("primary" + capitalizeFirstLetter(value[0]), value[1]); + } + } + } + } + if (this.secondaryPokemonRequirements?.length > 0 && this.secondaryPokemon && this.secondaryPokemon.length > 0) { + this.setDialogueToken("secondaryName", this.secondaryPokemon[0].getNameToRender()); + for (const req of this.secondaryPokemonRequirements) { + if (!req.invertQuery) { + const value = req.getDialogueToken(scene, this.secondaryPokemon[0]); + if (value?.length === 2) { + this.setDialogueToken("primary" + capitalizeFirstLetter(value[0]), value[1]); + } + this.setDialogueToken("secondary" + capitalizeFirstLetter(value[0]), value[1]); + } + } + } + + // Dialogue tokens for options + for (let i = 0; i < this.options.length; i++) { + const opt = this.options[i]; + opt.meetsRequirements(scene); + const j = i + 1; + if (opt.requirements.length > 0) { + for (const req of opt.requirements) { + const dialogueToken = req.getDialogueToken(scene); + if (dialogueToken?.length === 2) { + this.setDialogueToken("option" + j + capitalizeFirstLetter(dialogueToken[0]), dialogueToken[1]); + } + } + } + if (opt.primaryPokemonRequirements.length > 0 && opt.primaryPokemon) { + this.setDialogueToken("option" + j + "PrimaryName", opt.primaryPokemon.getNameToRender()); + for (const req of opt.primaryPokemonRequirements) { + if (!req.invertQuery) { + const value = req.getDialogueToken(scene, opt.primaryPokemon); + if (value?.length === 2) { + this.setDialogueToken("option" + j + "Primary" + capitalizeFirstLetter(value[0]), value[1]); + } + } + } + } + if (opt.secondaryPokemonRequirements?.length > 0 && opt.secondaryPokemon && opt.secondaryPokemon.length > 0) { + this.setDialogueToken("option" + j + "SecondaryName", opt.secondaryPokemon[0].getNameToRender()); + for (const req of opt.secondaryPokemonRequirements) { + if (!req.invertQuery) { + const value = req.getDialogueToken(scene, opt.secondaryPokemon[0]); + if (value?.length === 2) { + this.setDialogueToken("option" + j + "Secondary" + capitalizeFirstLetter(value[0]), value[1]); + } + } + } + } + } + } + + /** + * Used to cache a dialogue token for the encounter. + * Tokens will be auto-injected via the `{{key}}` pattern with `value`, + * when using the {@linkcode showEncounterText} and {@linkcode showEncounterDialogue} helper functions. + * + * @param key + * @param value + */ + setDialogueToken(key: string, value: string): void { + this.dialogueTokens[key] = value; + } + + /** + * If an encounter uses {@linkcode MysteryEncounterMode.continuousEncounter}, + * should rely on this value for seed offset instead of wave index. + * + * This offset is incremented for each new {@linkcode MysteryEncounterPhase} that occurs, + * so multi-encounter RNG will be consistent on resets and not be affected by number of turns, move RNG, etc. + */ + getSeedOffset() { + return this.seedOffset; + } + + /** + * Maintains seed offset for RNG consistency + * Increments if the same {@linkcode MysteryEncounter} has multiple option select cycles + * @param scene + */ + updateSeedOffset(scene: BattleScene) { + const currentOffset = this.seedOffset ?? scene.currentBattle.waveIndex * 1000; + this.seedOffset = currentOffset + 512; + } +} + +/** + * Builder class for creating a MysteryEncounter + * must call `build()` at the end after specifying all params for the MysteryEncounter + */ +export class MysteryEncounterBuilder implements Partial { + options: [MysteryEncounterOption, MysteryEncounterOption, ...MysteryEncounterOption[]]; + enemyPartyConfigs: EnemyPartyConfig[] = []; + + dialogue: MysteryEncounterDialogue = {}; + requirements: EncounterSceneRequirement[] = []; + primaryPokemonRequirements: EncounterPokemonRequirement[] = []; + secondaryPokemonRequirements: EncounterPokemonRequirement[] = []; + excludePrimaryFromSupportRequirements: boolean = true; + dialogueTokens: Record = {}; + + hideBattleIntroMessage: boolean = false; + autoHideIntroVisuals: boolean = true; + enterIntroVisualsFromRight: boolean = false; + continuousEncounter: boolean = false; + catchAllowed: boolean = false; + fleeAllowed: boolean = true; + lockEncounterRewardTiers: boolean = false; + startOfBattleEffectsComplete: boolean = false; + hasBattleAnimationsWithoutTargets: boolean = false; + skipEnemyBattleTurns: boolean = false; + skipToFightInput: boolean = false; + maxAllowedEncounters: number = 3; + expMultiplier: number = 1; + + /** + * REQUIRED + */ + + /** + * @statif Defines the type of encounter which is used as an identifier, should be tied to a unique MysteryEncounterType + * NOTE: if new functions are added to {@linkcode MysteryEncounter} class + * @param encounterType + * @returns this + */ + static withEncounterType(encounterType: MysteryEncounterType): MysteryEncounterBuilder & Pick { + return Object.assign(new MysteryEncounterBuilder(), { encounterType }); + } + + /** + * Defines an option for the encounter. + * Use for complex options. + * There should be at least 2 options defined and no more than 4. + * + * @param option MysteryEncounterOption to add, can use MysteryEncounterOptionBuilder to create instance + * @returns + */ + withOption(option: MysteryEncounterOption): this & Pick { + if (!this.options) { + const options = [option]; + return Object.assign(this, { options }); + } else { + this.options.push(option); + return this; + } + } + + /** + * Defines an option + phasefor the encounter. + * Use for easy/streamlined options. + * There should be at least 2 options defined and no more than 4. + * If complex use {@linkcode MysteryEncounterBuilder.withOption} + * + * @param dialogue {@linkcode OptionTextDisplay} + * @param callback {@linkcode OptionPhaseCallback} + * @returns + */ + withSimpleOption(dialogue: OptionTextDisplay, callback: OptionPhaseCallback): this & Pick { + return this.withOption(MysteryEncounterOptionBuilder.newOptionWithMode(MysteryEncounterOptionMode.DEFAULT).withDialogue(dialogue).withOptionPhase(callback).build()); + } + + /** + * Defines an option + phasefor the encounter. + * Use for easy/streamlined options. + * There should be at least 2 options defined and no more than 4. + * If complex use {@linkcode MysteryEncounterBuilder.withOption} + * + * @param dialogue {@linkcode OptionTextDisplay} + * @param callback {@linkcode OptionPhaseCallback} + * @returns + */ + withSimpleDexProgressOption(dialogue: OptionTextDisplay, callback: OptionPhaseCallback): this & Pick { + return this.withOption(MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withHasDexProgress(true) + .withDialogue(dialogue) + .withOptionPhase(callback).build()); + } + + /** + * Defines the sprites that will be shown on the enemy field when the encounter spawns + * Can be one or more sprites, recommended not to exceed 4 + * @param spriteConfigs + * @returns + */ + withIntroSpriteConfigs(spriteConfigs: MysteryEncounterSpriteConfig[]): this & Pick { + return Object.assign(this, { spriteConfigs: spriteConfigs }); + } + + withIntroDialogue(dialogue: MysteryEncounterDialogue["intro"] = []): this { + this.dialogue = {...this.dialogue, intro: dialogue }; + return this; + } + + withIntro({spriteConfigs, dialogue} : {spriteConfigs: MysteryEncounterSpriteConfig[], dialogue?: MysteryEncounterDialogue["intro"]}) { + return this.withIntroSpriteConfigs(spriteConfigs).withIntroDialogue(dialogue); + } + + /** + * OPTIONAL + */ + + /** + * Sets the rarity tier for an encounter + * If not specified, defaults to COMMON + * Tiers are: + * COMMON 32/64 odds + * GREAT 16/64 odds + * ULTRA 10/64 odds + * ROGUE 6/64 odds + * ULTRA_RARE Not currently used + * @param encounterTier + * @returns + */ + withEncounterTier(encounterTier: MysteryEncounterTier): this & Pick { + return Object.assign(this, { encounterTier: encounterTier }); + } + + /** + * Defines any EncounterAnim animations that are intended to be used during the encounter + * EncounterAnims are custom battle animations (think Ice Beam) that can be played at any point during an encounter or callback + * They just need to be specified here so that resources are loaded on encounter init + * @param encounterAnimations + * @returns + */ + withAnimations(...encounterAnimations: EncounterAnim[]): this & Required> { + const animations = Array.isArray(encounterAnimations) ? encounterAnimations : [encounterAnimations]; + return Object.assign(this, { encounterAnimations: animations }); + } + + /** + * Defines any game modes where the Mystery Encounter should *NOT* spawn + * @returns + * @param disallowedGameModes + */ + withDisallowedGameModes(...disallowedGameModes: GameModes[]): this & Required> { + const gameModes = Array.isArray(disallowedGameModes) ? disallowedGameModes : [disallowedGameModes]; + return Object.assign(this, { disallowedGameModes: gameModes }); + } + + /** + * Defines any challenges (from Challenge game mode) where the Mystery Encounter should *NOT* spawn + * @returns + * @param disallowedChallenges + */ + withDisallowedChallenges(...disallowedChallenges: Challenges[]): this & Required> { + const challenges = Array.isArray(disallowedChallenges) ? disallowedChallenges : [disallowedChallenges]; + return Object.assign(this, { disallowedChallenges: challenges }); + } + + /** + * If true, encounter will continuously run through multiple battles/puzzles/etc. instead of going to next wave + * MUST EVENTUALLY BE DISABLED TO CONTINUE TO NEXT WAVE + * Default false + * @param continuousEncounter + */ + withContinuousEncounter(continuousEncounter: boolean): this & Required> { + return Object.assign(this, { continuousEncounter: continuousEncounter }); + } + + /** + * If true, encounter will not animate the target Pokemon as part of battle animations + * Used for encounters where it is not a "real" battle, but still uses battle animations and commands (see {@linkcode FunAndGamesEncounter} for an example) + * Default false + * @param hasBattleAnimationsWithoutTargets + */ + withBattleAnimationsWithoutTargets(hasBattleAnimationsWithoutTargets: boolean): this & Required> { + return Object.assign(this, { hasBattleAnimationsWithoutTargets }); + } + + /** + * If true, encounter will not animate the target Pokemon as part of battle animations + * Used for encounters where it is not a "real" battle, but still uses battle animations and commands (see {@linkcode FunAndGamesEncounter} for an example) + * Default false + * @param skipEnemyBattleTurns + */ + withSkipEnemyBattleTurns(skipEnemyBattleTurns: boolean): this & Required> { + return Object.assign(this, { skipEnemyBattleTurns }); + } + + /** + * If true, will skip COMMAND input and go straight to FIGHT (move select) input menu + * Default false + * @param skipToFightInput + */ + withSkipToFightInput(skipToFightInput: boolean): this & Required> { + return Object.assign(this, { skipToFightInput }); + } + + /** + * Sets the maximum number of times that an encounter can spawn in a given Classic run + * @param maxAllowedEncounters + * @returns + */ + withMaxAllowedEncounters(maxAllowedEncounters: number): this & Required> { + return Object.assign(this, { maxAllowedEncounters: maxAllowedEncounters }); + } + + /** + * Specifies a requirement for an encounter + * For example, passing requirement as "new WaveCountRequirement([2, 180])" would create a requirement that the encounter can only be spawned between waves 2 and 180 + * Existing Requirement objects are defined in mystery-encounter-requirements.ts, and more can always be created to meet a requirement need + * @param requirement + * @returns + */ + withSceneRequirement(requirement: EncounterSceneRequirement): this & Required> { + if (requirement instanceof EncounterPokemonRequirement) { + Error("Incorrectly added pokemon requirement as scene requirement."); + } + this.requirements.push(requirement); + return this; + } + + /** + * Specifies a wave range requirement for an encounter. + * + * @param min min wave (or exact wave if only min is given) + * @param max optional max wave. If not given, defaults to min => exact wave + * @returns + */ + withSceneWaveRangeRequirement(min: number, max?: number): this & Required> { + return this.withSceneRequirement(new WaveRangeRequirement([min, max ?? min])); + } + + /** + * Specifies a party size requirement for an encounter. + * + * @param min min wave (or exact size if only min is given) + * @param max optional max size. If not given, defaults to min => exact wave + * @param excludeDisallowedPokemon if true, only counts allowed (legal in Challenge/unfainted) mons + * @returns + */ + withScenePartySizeRequirement(min: number, max?: number, excludeDisallowedPokemon: boolean = false): this & Required> { + return this.withSceneRequirement(new PartySizeRequirement([min, max ?? min], excludeDisallowedPokemon)); + } + + /** + * Add a primary pokemon requirement + * + * @param requirement {@linkcode EncounterPokemonRequirement} + * @returns + */ + withPrimaryPokemonRequirement(requirement: EncounterPokemonRequirement): this & Required> { + if (requirement instanceof EncounterSceneRequirement) { + Error("Incorrectly added scene requirement as pokemon requirement."); + } + + this.primaryPokemonRequirements.push(requirement); + return Object.assign(this, { primaryPokemonRequirements: this.primaryPokemonRequirements }); + } + + /** + * Add a primary pokemon status effect requirement + * + * @param statusEffect the status effect/s to check + * @param minNumberOfPokemon minimum number of pokemon to have the effect + * @param invertQuery if true will invert the query + * @returns + */ + withPrimaryPokemonStatusEffectRequirement(statusEffect: StatusEffect | StatusEffect[], minNumberOfPokemon: number = 1, invertQuery: boolean = false): this & Required> { + return this.withPrimaryPokemonRequirement(new StatusEffectRequirement(statusEffect, minNumberOfPokemon, invertQuery)); + } + + /** + * Add a primary pokemon health ratio requirement + * + * @param requiredHealthRange the health range to check + * @param minNumberOfPokemon minimum number of pokemon to have the health range + * @param invertQuery if true will invert the query + * @returns + */ + withPrimaryPokemonHealthRatioRequirement(requiredHealthRange: [number, number], minNumberOfPokemon: number = 1, invertQuery: boolean = false): this & Required> { + return this.withPrimaryPokemonRequirement(new HealthRatioRequirement(requiredHealthRange, minNumberOfPokemon, invertQuery)); + } + + // TODO: Maybe add an optional parameter for excluding primary pokemon from the support cast? + // ex. if your only grass type pokemon, a snivy, is chosen as primary, if the support pokemon requires a grass type, the event won't trigger because + // it's already been + withSecondaryPokemonRequirement(requirement: EncounterPokemonRequirement, excludePrimaryFromSecondaryRequirements: boolean = false): this & Required> { + if (requirement instanceof EncounterSceneRequirement) { + Error("Incorrectly added scene requirement as pokemon requirement."); + } + + this.secondaryPokemonRequirements.push(requirement); + this.excludePrimaryFromSupportRequirements = excludePrimaryFromSecondaryRequirements; + return Object.assign(this, { excludePrimaryFromSecondaryRequirements: this.excludePrimaryFromSupportRequirements, secondaryPokemonRequirements: this.secondaryPokemonRequirements }); + } + + /** + * Can set custom encounter rewards via this callback function + * If rewards are always deterministic for an encounter, this is a good way to set them + * + * NOTE: If rewards are dependent on options selected, runtime data, etc., + * It may be better to programmatically set doEncounterRewards elsewhere. + * There is a helper function in mystery-encounter utils, setEncounterRewards(), which can be called programmatically to set rewards + * @param doEncounterRewards Synchronous callback function to perform during rewards phase of the encounter + * @returns + */ + withRewards(doEncounterRewards: (scene: BattleScene) => boolean): this & Required> { + return Object.assign(this, { doEncounterRewards: doEncounterRewards }); + } + + /** + * Can set custom encounter exp via this callback function + * If exp always deterministic for an encounter, this is a good way to set them + * + * NOTE: If rewards are dependent on options selected, runtime data, etc., + * It may be better to programmatically set doEncounterExp elsewhere. + * There is a helper function in mystery-encounter utils, setEncounterExp(), which can be called programmatically to set rewards + * @param doEncounterExp Synchronous callback function to perform during rewards phase of the encounter + * @returns + */ + withExp(doEncounterExp: (scene: BattleScene) => boolean): this & Required> { + return Object.assign(this, { doEncounterExp: doEncounterExp }); + } + + /** + * Can be used to perform init logic before intro visuals are shown and before the MysteryEncounterPhase begins + * Useful for performing things like procedural generation of intro sprites, etc. + * + * @param onInit Synchronous callback function to perform as soon as the encounter is selected for the next phase + * @returns + */ + withOnInit(onInit: (scene: BattleScene) => boolean): this & Required> { + return Object.assign(this, { onInit }); + } + + /** + * Can be used to perform some extra logic (usually animations) when the enemy field is finished sliding in + * + * @param onVisualsStart Synchronous callback function to perform as soon as the enemy field finishes sliding in + * @returns + */ + withOnVisualsStart(onVisualsStart: (scene: BattleScene) => boolean): this & Required> { + return Object.assign(this, { onVisualsStart: onVisualsStart }); + } + + /** + * Can set whether catching is allowed or not on the encounter + * This flag can also be programmatically set inside option event functions or elsewhere + * @param catchAllowed If `true`, allows enemy pokemon to be caught during the encounter + * @returns + */ + withCatchAllowed(catchAllowed: boolean): this & Required> { + return Object.assign(this, { catchAllowed: catchAllowed }); + } + + /** + * Can set whether fleeing is allowed or not on the encounter + * @param fleeAllowed If `false`, prevents fleeing from a wild battle (trainer battle MEs already have flee disabled) + * @returns + */ + withFleeAllowed(fleeAllowed: boolean): this & Required> { + return Object.assign(this, { fleeAllowed }); + } + + /** + * @param hideBattleIntroMessage If `true`, will not show the trainerAppeared/wildAppeared/bossAppeared message for an encounter + * @returns + */ + withHideWildIntroMessage(hideBattleIntroMessage: boolean): this & Required> { + return Object.assign(this, { hideBattleIntroMessage: hideBattleIntroMessage }); + } + + /** + * @param autoHideIntroVisuals If `false`, will not hide the intro visuals that are displayed at the beginning of encounter + * @returns + */ + withAutoHideIntroVisuals(autoHideIntroVisuals: boolean): this & Required> { + return Object.assign(this, { autoHideIntroVisuals: autoHideIntroVisuals }); + } + + /** + * @param enterIntroVisualsFromRight If `true`, will slide in intro visuals from the right side of the screen. If false, slides in from left, as normal + * Default false + * @returns + */ + withEnterIntroVisualsFromRight(enterIntroVisualsFromRight: boolean): this & Required> { + return Object.assign(this, { enterIntroVisualsFromRight: enterIntroVisualsFromRight }); + } + + /** + * Add a title for the encounter + * + * @param title Title of the encounter + * @returns + */ + withTitle(title: string): this { + const encounterOptionsDialogue = this.dialogue.encounterOptionsDialogue ?? {}; + + this.dialogue = { + ...this.dialogue, + encounterOptionsDialogue: { + ...encounterOptionsDialogue, + title, + } + }; + + return this; + } + + /** + * Add a description of the encounter + * + * @param description Description of the encounter + * @returns + */ + withDescription(description: string): this { + const encounterOptionsDialogue = this.dialogue.encounterOptionsDialogue ?? {}; + + this.dialogue = { + ...this.dialogue, + encounterOptionsDialogue: { + ...encounterOptionsDialogue, + description, + } + }; + + return this; + } + + /** + * Add a query for the encounter + * + * @param query Query to use for the encounter + * @returns + */ + withQuery(query: string): this { + const encounterOptionsDialogue = this.dialogue.encounterOptionsDialogue ?? {}; + + this.dialogue = { + ...this.dialogue, + encounterOptionsDialogue: { + ...encounterOptionsDialogue, + query, + } + }; + + return this; + } + + /** + * Add outro dialogue/s for the encounter + * + * @param dialogue Outro dialogue(s) + * @returns + */ + withOutroDialogue(dialogue: MysteryEncounterDialogue["outro"] = []): this { + this.dialogue = {...this.dialogue, outro: dialogue }; + return this; + } + + /** + * Builds the mystery encounter + * + * @returns + */ + build(this: IMysteryEncounter): MysteryEncounter { + return new MysteryEncounter(this); + } +} diff --git a/src/data/mystery-encounters/mystery-encounters.ts b/src/data/mystery-encounters/mystery-encounters.ts new file mode 100644 index 00000000000..cc2eaf234c4 --- /dev/null +++ b/src/data/mystery-encounters/mystery-encounters.ts @@ -0,0 +1,372 @@ +import { Biome } from "#enums/biome"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { DarkDealEncounter } from "./encounters/dark-deal-encounter"; +import { DepartmentStoreSaleEncounter } from "./encounters/department-store-sale-encounter"; +import { FieldTripEncounter } from "./encounters/field-trip-encounter"; +import { FightOrFlightEncounter } from "./encounters/fight-or-flight-encounter"; +import { LostAtSeaEncounter } from "./encounters/lost-at-sea-encounter"; +import { MysteriousChallengersEncounter } from "./encounters/mysterious-challengers-encounter"; +import { MysteriousChestEncounter } from "./encounters/mysterious-chest-encounter"; +import { ShadyVitaminDealerEncounter } from "./encounters/shady-vitamin-dealer-encounter"; +import { SlumberingSnorlaxEncounter } from "./encounters/slumbering-snorlax-encounter"; +import { TrainingSessionEncounter } from "./encounters/training-session-encounter"; +import MysteryEncounter from "./mystery-encounter"; +import { SafariZoneEncounter } from "#app/data/mystery-encounters/encounters/safari-zone-encounter"; +import { FieryFalloutEncounter } from "#app/data/mystery-encounters/encounters/fiery-fallout-encounter"; +import { TheStrongStuffEncounter } from "#app/data/mystery-encounters/encounters/the-strong-stuff-encounter"; +import { ThePokemonSalesmanEncounter } from "#app/data/mystery-encounters/encounters/the-pokemon-salesman-encounter"; +import { AnOfferYouCantRefuseEncounter } from "#app/data/mystery-encounters/encounters/an-offer-you-cant-refuse-encounter"; +import { DelibirdyEncounter } from "#app/data/mystery-encounters/encounters/delibirdy-encounter"; +import { AbsoluteAvariceEncounter } from "#app/data/mystery-encounters/encounters/absolute-avarice-encounter"; +import { ATrainersTestEncounter } from "#app/data/mystery-encounters/encounters/a-trainers-test-encounter"; +import { TrashToTreasureEncounter } from "#app/data/mystery-encounters/encounters/trash-to-treasure-encounter"; +import { BerriesAboundEncounter } from "#app/data/mystery-encounters/encounters/berries-abound-encounter"; +import { ClowningAroundEncounter } from "#app/data/mystery-encounters/encounters/clowning-around-encounter"; +import { PartTimerEncounter } from "#app/data/mystery-encounters/encounters/part-timer-encounter"; +import { DancingLessonsEncounter } from "#app/data/mystery-encounters/encounters/dancing-lessons-encounter"; +import { WeirdDreamEncounter } from "#app/data/mystery-encounters/encounters/weird-dream-encounter"; +import { TheWinstrateChallengeEncounter } from "#app/data/mystery-encounters/encounters/the-winstrate-challenge-encounter"; +import { TeleportingHijinksEncounter } from "#app/data/mystery-encounters/encounters/teleporting-hijinks-encounter"; +import { BugTypeSuperfanEncounter } from "#app/data/mystery-encounters/encounters/bug-type-superfan-encounter"; +import { FunAndGamesEncounter } from "#app/data/mystery-encounters/encounters/fun-and-games-encounter"; +import { UncommonBreedEncounter } from "#app/data/mystery-encounters/encounters/uncommon-breed-encounter"; +import { GlobalTradeSystemEncounter } from "#app/data/mystery-encounters/encounters/global-trade-system-encounter"; +import { TheExpertPokemonBreederEncounter } from "#app/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter"; + +/** + * 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 = [ + Biome.SEA, + Biome.SEABED, + Biome.BADLANDS, + Biome.DESERT, + Biome.ICE_CAVE, + Biome.VOLCANO, + Biome.WASTELAND, + Biome.ABYSS, + Biome.SPACE, + Biome.END +]; + +export const NON_EXTREME_ENCOUNTER_BIOMES = [ + Biome.TOWN, + Biome.PLAINS, + Biome.GRASS, + Biome.TALL_GRASS, + Biome.METROPOLIS, + Biome.FOREST, + Biome.SWAMP, + Biome.BEACH, + Biome.LAKE, + Biome.MOUNTAIN, + Biome.CAVE, + Biome.MEADOW, + Biome.POWER_PLANT, + Biome.GRAVEYARD, + Biome.DOJO, + Biome.FACTORY, + Biome.RUINS, + Biome.CONSTRUCTION_SITE, + Biome.JUNGLE, + Biome.FAIRY_CAVE, + Biome.TEMPLE, + Biome.SLUM, + Biome.SNOWY_FOREST, + Biome.ISLAND, + Biome.LABORATORY +]; + +/** + * Places where you could very reasonably expect to encounter a single human + * + * Diff from NON_EXTREME_ENCOUNTER_BIOMES: + * + BADLANDS + * + DESERT + * + ICE_CAVE + */ +export const HUMAN_TRANSITABLE_BIOMES = [ + Biome.TOWN, + Biome.PLAINS, + Biome.GRASS, + Biome.TALL_GRASS, + Biome.METROPOLIS, + Biome.FOREST, + Biome.SWAMP, + Biome.BEACH, + Biome.LAKE, + Biome.MOUNTAIN, + Biome.BADLANDS, + Biome.CAVE, + Biome.DESERT, + Biome.ICE_CAVE, + Biome.MEADOW, + Biome.POWER_PLANT, + Biome.GRAVEYARD, + Biome.DOJO, + Biome.FACTORY, + Biome.RUINS, + Biome.CONSTRUCTION_SITE, + Biome.JUNGLE, + Biome.FAIRY_CAVE, + Biome.TEMPLE, + Biome.SLUM, + Biome.SNOWY_FOREST, + Biome.ISLAND, + Biome.LABORATORY +]; + +/** + * Places where you could expect a town or city, some form of large civilization + */ +export const CIVILIZATION_ENCOUNTER_BIOMES = [ + Biome.TOWN, + Biome.PLAINS, + Biome.GRASS, + Biome.TALL_GRASS, + Biome.METROPOLIS, + Biome.BEACH, + Biome.LAKE, + Biome.MEADOW, + Biome.POWER_PLANT, + Biome.GRAVEYARD, + Biome.DOJO, + Biome.FACTORY, + Biome.CONSTRUCTION_SITE, + Biome.SLUM, + Biome.ISLAND +]; + +export const allMysteryEncounters: { [encounterType: number]: MysteryEncounter } = {}; + + +const extremeBiomeEncounters: MysteryEncounterType[] = []; + +const nonExtremeBiomeEncounters: MysteryEncounterType[] = [ + MysteryEncounterType.FIELD_TRIP, + MysteryEncounterType.DANCING_LESSONS, // Is also in BADLANDS, DESERT, VOLCANO, WASTELAND, ABYSS +]; + +const humanTransitableBiomeEncounters: MysteryEncounterType[] = [ + MysteryEncounterType.MYSTERIOUS_CHALLENGERS, + MysteryEncounterType.SHADY_VITAMIN_DEALER, + MysteryEncounterType.THE_POKEMON_SALESMAN, + MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE, + MysteryEncounterType.THE_WINSTRATE_CHALLENGE, + MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER +]; + +const civilizationBiomeEncounters: MysteryEncounterType[] = [ + MysteryEncounterType.DEPARTMENT_STORE_SALE, + MysteryEncounterType.PART_TIMER, + MysteryEncounterType.FUN_AND_GAMES, + MysteryEncounterType.GLOBAL_TRADE_SYSTEM +]; + +/** + * To add an encounter to every biome possible, use this array + */ +const anyBiomeEncounters: MysteryEncounterType[] = [ + MysteryEncounterType.FIGHT_OR_FLIGHT, + MysteryEncounterType.DARK_DEAL, + MysteryEncounterType.MYSTERIOUS_CHEST, + MysteryEncounterType.TRAINING_SESSION, + MysteryEncounterType.DELIBIRDY, + MysteryEncounterType.A_TRAINERS_TEST, + MysteryEncounterType.TRASH_TO_TREASURE, + MysteryEncounterType.BERRIES_ABOUND, + MysteryEncounterType.CLOWNING_AROUND, + MysteryEncounterType.WEIRD_DREAM, + MysteryEncounterType.TELEPORTING_HIJINKS, + MysteryEncounterType.BUG_TYPE_SUPERFAN, + MysteryEncounterType.UNCOMMON_BREED +]; + +/** + * ENCOUNTER BIOME MAPPING + * To add an Encounter to a biome group, instead of cluttering the map, use the biome group arrays above + * + * Adding specific Encounters to the mysteryEncountersByBiome map is for specific cases and special circumstances + * that biome groups do not cover + */ +export const mysteryEncountersByBiome = new Map([ + [Biome.TOWN, []], + [Biome.PLAINS, [ + MysteryEncounterType.SLUMBERING_SNORLAX, + MysteryEncounterType.ABSOLUTE_AVARICE + ]], + [Biome.GRASS, [ + MysteryEncounterType.SLUMBERING_SNORLAX, + MysteryEncounterType.ABSOLUTE_AVARICE + ]], + [Biome.TALL_GRASS, [ + MysteryEncounterType.ABSOLUTE_AVARICE + ]], + [Biome.METROPOLIS, []], + [Biome.FOREST, [ + MysteryEncounterType.SAFARI_ZONE, + MysteryEncounterType.ABSOLUTE_AVARICE + ]], + [Biome.SEA, [ + MysteryEncounterType.LOST_AT_SEA + ]], + [Biome.SWAMP, [ + MysteryEncounterType.SAFARI_ZONE + ]], + [Biome.BEACH, []], + [Biome.LAKE, []], + [Biome.SEABED, []], + [Biome.MOUNTAIN, []], + [Biome.BADLANDS, [ + MysteryEncounterType.DANCING_LESSONS + ]], + [Biome.CAVE, [ + MysteryEncounterType.THE_STRONG_STUFF + ]], + [Biome.DESERT, [ + MysteryEncounterType.DANCING_LESSONS + ]], + [Biome.ICE_CAVE, []], + [Biome.MEADOW, []], + [Biome.POWER_PLANT, []], + [Biome.VOLCANO, [ + MysteryEncounterType.FIERY_FALLOUT, + MysteryEncounterType.DANCING_LESSONS + ]], + [Biome.GRAVEYARD, []], + [Biome.DOJO, []], + [Biome.FACTORY, []], + [Biome.RUINS, []], + [Biome.WASTELAND, [ + MysteryEncounterType.DANCING_LESSONS + ]], + [Biome.ABYSS, [ + MysteryEncounterType.DANCING_LESSONS + ]], + [Biome.SPACE, [ + MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER + ]], + [Biome.CONSTRUCTION_SITE, []], + [Biome.JUNGLE, [ + MysteryEncounterType.SAFARI_ZONE + ]], + [Biome.FAIRY_CAVE, []], + [Biome.TEMPLE, []], + [Biome.SLUM, []], + [Biome.SNOWY_FOREST, []], + [Biome.ISLAND, []], + [Biome.LABORATORY, []] +]); + +export function initMysteryEncounters() { + allMysteryEncounters[MysteryEncounterType.MYSTERIOUS_CHALLENGERS] = MysteriousChallengersEncounter; + allMysteryEncounters[MysteryEncounterType.MYSTERIOUS_CHEST] = MysteriousChestEncounter; + allMysteryEncounters[MysteryEncounterType.DARK_DEAL] = DarkDealEncounter; + allMysteryEncounters[MysteryEncounterType.FIGHT_OR_FLIGHT] = FightOrFlightEncounter; + allMysteryEncounters[MysteryEncounterType.TRAINING_SESSION] = TrainingSessionEncounter; + allMysteryEncounters[MysteryEncounterType.SLUMBERING_SNORLAX] = SlumberingSnorlaxEncounter; + allMysteryEncounters[MysteryEncounterType.DEPARTMENT_STORE_SALE] = DepartmentStoreSaleEncounter; + allMysteryEncounters[MysteryEncounterType.SHADY_VITAMIN_DEALER] = ShadyVitaminDealerEncounter; + allMysteryEncounters[MysteryEncounterType.FIELD_TRIP] = FieldTripEncounter; + allMysteryEncounters[MysteryEncounterType.SAFARI_ZONE] = SafariZoneEncounter; + allMysteryEncounters[MysteryEncounterType.LOST_AT_SEA] = LostAtSeaEncounter; + allMysteryEncounters[MysteryEncounterType.FIERY_FALLOUT] = FieryFalloutEncounter; + allMysteryEncounters[MysteryEncounterType.THE_STRONG_STUFF] = TheStrongStuffEncounter; + allMysteryEncounters[MysteryEncounterType.THE_POKEMON_SALESMAN] = ThePokemonSalesmanEncounter; + allMysteryEncounters[MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE] = AnOfferYouCantRefuseEncounter; + allMysteryEncounters[MysteryEncounterType.DELIBIRDY] = DelibirdyEncounter; + allMysteryEncounters[MysteryEncounterType.ABSOLUTE_AVARICE] = AbsoluteAvariceEncounter; + allMysteryEncounters[MysteryEncounterType.A_TRAINERS_TEST] = ATrainersTestEncounter; + allMysteryEncounters[MysteryEncounterType.TRASH_TO_TREASURE] = TrashToTreasureEncounter; + allMysteryEncounters[MysteryEncounterType.BERRIES_ABOUND] = BerriesAboundEncounter; + allMysteryEncounters[MysteryEncounterType.CLOWNING_AROUND] = ClowningAroundEncounter; + allMysteryEncounters[MysteryEncounterType.PART_TIMER] = PartTimerEncounter; + allMysteryEncounters[MysteryEncounterType.DANCING_LESSONS] = DancingLessonsEncounter; + allMysteryEncounters[MysteryEncounterType.WEIRD_DREAM] = WeirdDreamEncounter; + allMysteryEncounters[MysteryEncounterType.THE_WINSTRATE_CHALLENGE] = TheWinstrateChallengeEncounter; + allMysteryEncounters[MysteryEncounterType.TELEPORTING_HIJINKS] = TeleportingHijinksEncounter; + allMysteryEncounters[MysteryEncounterType.BUG_TYPE_SUPERFAN] = BugTypeSuperfanEncounter; + allMysteryEncounters[MysteryEncounterType.FUN_AND_GAMES] = FunAndGamesEncounter; + allMysteryEncounters[MysteryEncounterType.UNCOMMON_BREED] = UncommonBreedEncounter; + allMysteryEncounters[MysteryEncounterType.GLOBAL_TRADE_SYSTEM] = GlobalTradeSystemEncounter; + allMysteryEncounters[MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER] = TheExpertPokemonBreederEncounter; + + // Add extreme encounters to biome map + extremeBiomeEncounters.forEach(encounter => { + EXTREME_ENCOUNTER_BIOMES.forEach(biome => { + const encountersForBiome = mysteryEncountersByBiome.get(biome); + if (encountersForBiome && !encountersForBiome.includes(encounter)) { + encountersForBiome.push(encounter); + } + }); + }); + // Add non-extreme encounters to biome map + nonExtremeBiomeEncounters.forEach(encounter => { + NON_EXTREME_ENCOUNTER_BIOMES.forEach(biome => { + const encountersForBiome = mysteryEncountersByBiome.get(biome); + if (encountersForBiome && !encountersForBiome.includes(encounter)) { + encountersForBiome.push(encounter); + } + }); + }); + // Add human encounters to biome map + humanTransitableBiomeEncounters.forEach(encounter => { + HUMAN_TRANSITABLE_BIOMES.forEach(biome => { + const encountersForBiome = mysteryEncountersByBiome.get(biome); + if (encountersForBiome && !encountersForBiome.includes(encounter)) { + encountersForBiome.push(encounter); + } + }); + }); + // Add civilization encounters to biome map + civilizationBiomeEncounters.forEach(encounter => { + CIVILIZATION_ENCOUNTER_BIOMES.forEach(biome => { + const encountersForBiome = mysteryEncountersByBiome.get(biome); + if (encountersForBiome && !encountersForBiome.includes(encounter)) { + encountersForBiome.push(encounter); + } + }); + }); + + // Add ANY biome encounters to biome map + mysteryEncountersByBiome.forEach(biomeEncounters => { + anyBiomeEncounters.forEach(encounter => { + if (!biomeEncounters.includes(encounter)) { + biomeEncounters.push(encounter); + } + }); + }); +} diff --git a/src/data/mystery-encounters/requirements/can-learn-move-requirement.ts b/src/data/mystery-encounters/requirements/can-learn-move-requirement.ts new file mode 100644 index 00000000000..a0b4edd4a36 --- /dev/null +++ b/src/data/mystery-encounters/requirements/can-learn-move-requirement.ts @@ -0,0 +1,91 @@ +import BattleScene from "#app/battle-scene"; +import { Moves } from "#app/enums/moves"; +import { PlayerPokemon, PokemonMove } from "#app/field/pokemon"; +import { isNullOrUndefined } from "#app/utils"; +import { EncounterPokemonRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; + +/** + * {@linkcode CanLearnMoveRequirement} options + */ +export interface CanLearnMoveRequirementOptions { + excludeLevelMoves?: boolean; + excludeTmMoves?: boolean; + excludeEggMoves?: boolean; + includeFainted?: boolean; + minNumberOfPokemon?: number; + invertQuery?: boolean; +} + +/** + * Requires that a pokemon can learn a specific move/moveset. + */ +export class CanLearnMoveRequirement extends EncounterPokemonRequirement { + private readonly requiredMoves: Moves[]; + private readonly excludeLevelMoves?: boolean; + private readonly excludeTmMoves?: boolean; + private readonly excludeEggMoves?: boolean; + private readonly includeFainted?: boolean; + + constructor(requiredMoves: Moves | Moves[], options: CanLearnMoveRequirementOptions = {}) { + super(); + this.requiredMoves = Array.isArray(requiredMoves) ? requiredMoves : [requiredMoves]; + + this.excludeLevelMoves = options.excludeLevelMoves ?? false; + this.excludeTmMoves = options.excludeTmMoves ?? false; + this.excludeEggMoves = options.excludeEggMoves ?? false; + this.includeFainted = options.includeFainted ?? false; + this.minNumberOfPokemon = options.minNumberOfPokemon ?? 1; + this.invertQuery = options.invertQuery ?? false; + } + + override meetsRequirement(scene: BattleScene): boolean { + const partyPokemon = scene.getParty().filter((pkm) => (this.includeFainted ? pkm.isAllowed() : pkm.isAllowedInBattle())); + + if (isNullOrUndefined(partyPokemon) || this.requiredMoves?.length < 0) { + return false; + } + + return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; + } + + override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { + if (!this.invertQuery) { + return partyPokemon.filter((pokemon) => + // every required move should be included + this.requiredMoves.every((requiredMove) => this.getAllPokemonMoves(pokemon).includes(requiredMove)) + ); + } else { + return partyPokemon.filter( + (pokemon) => + // none of the "required" moves should be included + !this.requiredMoves.some((requiredMove) => this.getAllPokemonMoves(pokemon).includes(requiredMove)) + ); + } + } + + override getDialogueToken(_scene: BattleScene, _pokemon?: PlayerPokemon): [string, string] { + return ["requiredMoves", this.requiredMoves.map(m => new PokemonMove(m).getName()).join(", ")]; + } + + private getPokemonLevelMoves(pkm: PlayerPokemon): Moves[] { + return pkm.getLevelMoves().map(([_level, move]) => move); + } + + private getAllPokemonMoves(pkm: PlayerPokemon): Moves[] { + const allPokemonMoves: Moves[] = []; + + if (!this.excludeLevelMoves) { + allPokemonMoves.push(...(this.getPokemonLevelMoves(pkm) ?? [])); + } + + if (!this.excludeTmMoves) { + allPokemonMoves.push(...(pkm.compatibleTms ?? [])); + } + + if (!this.excludeEggMoves) { + allPokemonMoves.push(...(pkm.getEggMoves() ?? [])); + } + + return allPokemonMoves; + } +} diff --git a/src/data/mystery-encounters/requirements/requirement-groups.ts b/src/data/mystery-encounters/requirements/requirement-groups.ts new file mode 100644 index 00000000000..63c899fc5e9 --- /dev/null +++ b/src/data/mystery-encounters/requirements/requirement-groups.ts @@ -0,0 +1,120 @@ +import { Moves } from "#enums/moves"; +import { Abilities } from "#enums/abilities"; + +/** + * Moves that "steal" things + */ +export const STEALING_MOVES = [ + Moves.PLUCK, + Moves.COVET, + Moves.KNOCK_OFF, + Moves.THIEF, + Moves.TRICK, + Moves.SWITCHEROO +]; + +/** + * Moves that "charm" someone + */ +export const CHARMING_MOVES = [ + Moves.CHARM, + Moves.FLATTER, + Moves.DRAGON_CHEER, + Moves.ALLURING_VOICE, + Moves.ATTRACT, + Moves.SWEET_SCENT, + Moves.CAPTIVATE, + Moves.AROMATIC_MIST +]; + +/** + * Moves for the Dancer ability + */ +export const DANCING_MOVES = [ + Moves.AQUA_STEP, + Moves.CLANGOROUS_SOUL, + Moves.DRAGON_DANCE, + Moves.FEATHER_DANCE, + Moves.FIERY_DANCE, + Moves.LUNAR_DANCE, + Moves.PETAL_DANCE, + Moves.REVELATION_DANCE, + Moves.QUIVER_DANCE, + Moves.SWORDS_DANCE, + Moves.TEETER_DANCE, + Moves.VICTORY_DANCE +]; + +/** + * Moves that can distract someone/something + */ +export const DISTRACTION_MOVES = [ + Moves.FAKE_OUT, + Moves.FOLLOW_ME, + Moves.TAUNT, + Moves.ROAR, + Moves.TELEPORT, + Moves.CHARM, + Moves.FAKE_TEARS, + Moves.TICKLE, + Moves.CAPTIVATE, + Moves.RAGE_POWDER, + Moves.SUBSTITUTE, + Moves.SHED_TAIL +]; + +/** + * Moves that protect in some way + */ +export const PROTECTING_MOVES = [ + Moves.PROTECT, + Moves.WIDE_GUARD, + Moves.MAX_GUARD, + Moves.SAFEGUARD, + Moves.REFLECT, + Moves.BARRIER, + Moves.QUICK_GUARD, + Moves.FLOWER_SHIELD, + Moves.KINGS_SHIELD, + Moves.CRAFTY_SHIELD, + Moves.SPIKY_SHIELD, + Moves.OBSTRUCT, + Moves.DETECT +]; + +/** + * Moves that (loosely) can be used to trap/rob someone + */ +export const EXTORTION_MOVES = [ + Moves.BIND, + Moves.CLAMP, + Moves.INFESTATION, + Moves.SAND_TOMB, + Moves.SNAP_TRAP, + Moves.THUNDER_CAGE, + Moves.WRAP, + Moves.SPIRIT_SHACKLE, + Moves.MEAN_LOOK, + Moves.JAW_LOCK, + Moves.BLOCK, + Moves.SPIDER_WEB, + Moves.ANCHOR_SHOT, + Moves.OCTOLOCK, + Moves.PURSUIT, + Moves.CONSTRICT, + Moves.BEAT_UP, + Moves.COIL, + Moves.WRING_OUT, + Moves.STRING_SHOT, +]; + +/** + * Abilities that (loosely) can be used to trap/rob someone + */ +export const EXTORTION_ABILITIES = [ + Abilities.INTIMIDATE, + Abilities.ARENA_TRAP, + Abilities.SHADOW_TAG, + Abilities.SUCTION_CUPS, + Abilities.STICKY_HOLD +]; diff --git a/src/data/mystery-encounters/utils/encounter-dialogue-utils.ts b/src/data/mystery-encounters/utils/encounter-dialogue-utils.ts new file mode 100644 index 00000000000..494a45f69f5 --- /dev/null +++ b/src/data/mystery-encounters/utils/encounter-dialogue-utils.ts @@ -0,0 +1,86 @@ +import BattleScene from "#app/battle-scene"; +import { getTextWithColors, TextStyle } from "#app/ui/text"; +import { UiTheme } from "#enums/ui-theme"; +import { isNullOrUndefined } from "#app/utils"; +import i18next from "i18next"; + +/** + * Will inject all relevant dialogue tokens that exist in the {@linkcode BattleScene.currentBattle.mysteryEncounter.dialogueTokens}, into i18n text. + * Also adds BBCodeText fragments for colored text, if applicable + * @param scene + * @param keyOrString + * @param primaryStyle Can define a text style to be applied to the entire string. Must be defined for BBCodeText styles to be applied correctly + * @param uiTheme + */ +export function getEncounterText(scene: BattleScene, keyOrString?: string, primaryStyle?: TextStyle, uiTheme: UiTheme = UiTheme.DEFAULT): string | null { + if (isNullOrUndefined(keyOrString)) { + return null; + } + + let textString: string | null = getTextWithDialogueTokens(scene, keyOrString!); + + // Can only color the text if a Primary Style is defined + // primaryStyle is applied to all text that does not have its own specified style + if (primaryStyle && textString) { + textString = getTextWithColors(textString, primaryStyle, uiTheme); + } + + return textString; +} + +/** + * Helper function to inject {@linkcode BattleScene.currentBattle.mysteryEncounter.dialogueTokens} into a given content string + * @param scene + * @param keyOrString + */ +function getTextWithDialogueTokens(scene: BattleScene, keyOrString: string): string | null { + const tokens = scene.currentBattle?.mysteryEncounter?.dialogueTokens; + + if (i18next.exists(keyOrString, tokens)) { + return i18next.t(keyOrString, tokens) as string; + } + + return keyOrString ?? null; +} + +/** + * Will queue a message in UI with injected encounter data tokens + * @param scene + * @param contentKey + */ +export function queueEncounterMessage(scene: BattleScene, contentKey: string): void { + const text: string | null = getEncounterText(scene, contentKey); + scene.queueMessage(text ?? "", null, true); +} + +/** + * Will display a message in UI with injected encounter data tokens + * @param scene + * @param contentKey + * @param delay + * @param prompt + * @param callbackDelay + * @param promptDelay + */ +export function showEncounterText(scene: BattleScene, contentKey: string, delay: number | null = null, callbackDelay: number = 0, prompt: boolean = true, promptDelay: number | null = null): Promise { + return new Promise(resolve => { + const text: string | null = getEncounterText(scene, contentKey); + scene.ui.showText(text ?? "", delay, () => resolve(), callbackDelay, prompt, promptDelay); + }); +} + +/** + * Will display a dialogue (with speaker title) in UI with injected encounter data tokens + * @param scene + * @param textContentKey + * @param delay + * @param speakerContentKey + * @param callbackDelay + */ +export function showEncounterDialogue(scene: BattleScene, textContentKey: string, speakerContentKey: string, delay: number | null = null, callbackDelay: number = 0): Promise { + return new Promise(resolve => { + const text: string | null = getEncounterText(scene, textContentKey); + const speaker: string | null = getEncounterText(scene, speakerContentKey); + scene.ui.showDialogue(text ?? "", speaker ?? "", delay, () => resolve(), callbackDelay); + }); +} diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts new file mode 100644 index 00000000000..7eb205ce3f7 --- /dev/null +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -0,0 +1,1112 @@ +import Battle, { BattlerIndex, BattleType } from "#app/battle"; +import { biomeLinks, BiomePoolTier } from "#app/data/biomes"; +import 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 { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import Pokemon, { AiType, FieldPosition, PlayerPokemon, PokemonMove, PokemonSummonData } from "#app/field/pokemon"; +import { CustomModifierSettings, ModifierPoolType, ModifierType, ModifierTypeGenerator, ModifierTypeOption, modifierTypes, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type"; +import { MysteryEncounterBattlePhase, MysteryEncounterBattleStartCleanupPhase, MysteryEncounterPhase, MysteryEncounterRewardsPhase } from "#app/phases/mystery-encounter-phases"; +import PokemonData from "#app/system/pokemon-data"; +import { OptionSelectConfig, OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; +import { PartyOption, PartyUiMode, PokemonSelectFilter } from "#app/ui/party-ui-handler"; +import { Mode } from "#app/ui/ui"; +import * as Utils from "#app/utils"; +import { isNullOrUndefined } from "#app/utils"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { Biome } from "#enums/biome"; +import { TrainerType } from "#enums/trainer-type"; +import i18next from "i18next"; +import BattleScene from "#app/battle-scene"; +import Trainer, { TrainerVariant } from "#app/field/trainer"; +import { Gender } from "#app/data/gender"; +import { Nature } from "#app/data/nature"; +import { Moves } from "#enums/moves"; +import { initMoveAnim, loadMoveAnimAssets } from "#app/data/battle-anims"; +import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; +import { Status, StatusEffect } from "#app/data/status-effect"; +import { TrainerConfig, trainerConfigs, TrainerSlot } from "#app/data/trainer-config"; +import PokemonSpecies from "#app/data/pokemon-species"; +import { Egg, IEggOptions } from "#app/data/egg"; +import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data"; +import HeldModifierConfig from "#app/interfaces/held-modifier-config"; +import { MovePhase } from "#app/phases/move-phase"; +import { EggLapsePhase } from "#app/phases/egg-lapse-phase"; +import { TrainerVictoryPhase } from "#app/phases/trainer-victory-phase"; +import { BattleEndPhase } from "#app/phases/battle-end-phase"; +import { GameOverPhase } from "#app/phases/game-over-phase"; +import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; +import { PartyExpPhase } from "#app/phases/party-exp-phase"; +import { Variant } from "#app/data/variant"; + +/** + * Animates exclamation sprite over trainer's head at start of encounter + * @param scene + */ +export function doTrainerExclamation(scene: BattleScene) { + const exclamationSprite = scene.add.sprite(0, 0, "encounter_exclaim"); + exclamationSprite.setName("exclamation"); + scene.field.add(exclamationSprite); + scene.field.moveTo(exclamationSprite, scene.field.getAll().length - 1); + exclamationSprite.setVisible(true); + exclamationSprite.setPosition(110, 68); + scene.tweens.add({ + targets: exclamationSprite, + y: "-=25", + ease: "Cubic.easeOut", + duration: 300, + yoyo: true, + onComplete: () => { + scene.time.delayedCall(800, () => { + scene.field.remove(exclamationSprite, true); + }); + } + }); + + scene.playSound("battle_anims/GEN8- Exclaim", { volume: 0.7 }); +} + +export interface EnemyPokemonConfig { + species: PokemonSpecies; + isBoss: boolean; + nickname?: string; + bossSegments?: number; + bossSegmentModifier?: number; // Additive to the determined segment number + mysteryEncounterPokemonData?: MysteryEncounterPokemonData; + formIndex?: number; + abilityIndex?: number; + level?: number; + gender?: Gender; + passive?: boolean; + moveSet?: Moves[]; + nature?: Nature; + ivs?: [number, number, number, number, number, number]; + shiny?: boolean; + /** Is only checked if Pokemon is shiny */ + variant?: Variant; + /** Can set just the status, or pass a timer on the status turns */ + status?: StatusEffect | [StatusEffect, number]; + mysteryEncounterBattleEffects?: (pokemon: Pokemon) => void; + modifierConfigs?: HeldModifierConfig[]; + tags?: BattlerTagType[]; + dataSource?: PokemonData; + aiType?: AiType; +} + +export interface EnemyPartyConfig { + /** Formula for enemy level: level += waveIndex / 10 * levelAdditiveModifier */ + levelAdditiveModifier?: number; + doubleBattle?: boolean; + /** Generates trainer battle solely off trainer type */ + trainerType?: TrainerType; + /** More customizable option for configuring trainer battle */ + trainerConfig?: TrainerConfig; + pokemonConfigs?: EnemyPokemonConfig[]; + /** `true` for female trainer, false for male */ + female?: boolean; + /** `true` will prevent player from switching */ + disableSwitch?: boolean; + /** `true` or leaving undefined will increment dex seen count for the encounter battle, `false` will not */ + countAsSeen?: boolean; +} + +/** + * Generates an enemy party for a mystery encounter battle + * This will override and replace any standard encounter generation logic + * Useful for tailoring specific battles to mystery encounters + * @param scene Battle Scene + * @param partyConfig Can pass various customizable attributes for the enemy party, see EnemyPartyConfig + */ +export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig: EnemyPartyConfig): Promise { + const loaded: boolean = false; + const loadEnemyAssets: Promise[] = []; + + const battle: Battle = scene.currentBattle; + + let doubleBattle: boolean = partyConfig?.doubleBattle ?? false; + + // Trainer + const trainerType = partyConfig?.trainerType; + const partyTrainerConfig = partyConfig?.trainerConfig; + let trainerConfig: TrainerConfig; + if (!isNullOrUndefined(trainerType) || partyTrainerConfig) { + scene.currentBattle.mysteryEncounter!.encounterMode = MysteryEncounterMode.TRAINER_BATTLE; + if (scene.currentBattle.trainer) { + scene.currentBattle.trainer.setVisible(false); + scene.currentBattle.trainer.destroy(); + } + + trainerConfig = partyConfig?.trainerConfig ? partyConfig?.trainerConfig : trainerConfigs[trainerType!]; + + const doubleTrainer = trainerConfig.doubleOnly || (trainerConfig.hasDouble && !!partyConfig.doubleBattle); + doubleBattle = doubleTrainer; + const trainerFemale = isNullOrUndefined(partyConfig.female) ? !!(Utils.randSeedInt(2)) : partyConfig.female; + const newTrainer = new Trainer(scene, trainerConfig.trainerType, doubleTrainer ? TrainerVariant.DOUBLE : trainerFemale ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT, undefined, undefined, undefined, trainerConfig); + newTrainer.x += 300; + newTrainer.setVisible(false); + scene.field.add(newTrainer); + scene.currentBattle.trainer = newTrainer; + loadEnemyAssets.push(newTrainer.loadAssets()); + + battle.enemyLevels = scene.currentBattle.trainer.getPartyLevels(scene.currentBattle.waveIndex); + } else { + // Wild + scene.currentBattle.mysteryEncounter!.encounterMode = MysteryEncounterMode.WILD_BATTLE; + const numEnemies = partyConfig?.pokemonConfigs && partyConfig.pokemonConfigs.length > 0 ? partyConfig?.pokemonConfigs?.length : doubleBattle ? 2 : 1; + battle.enemyLevels = new Array(numEnemies).fill(null).map(() => scene.currentBattle.getLevelForWave()); + } + + scene.getEnemyParty().forEach(enemyPokemon => { + scene.field.remove(enemyPokemon, true); + }); + battle.enemyParty = []; + battle.double = doubleBattle; + + // ME levels are modified by an additive value that scales with wave index + // Base scaling: Every 10 waves, modifier gets +1 level + // This can be amplified or counteracted by setting levelAdditiveModifier in config + // levelAdditiveModifier value of 0.5 will halve the modifier scaling, 2 will double it, etc. + // Leaving null/undefined will disable level scaling + const mult: number = !isNullOrUndefined(partyConfig.levelAdditiveModifier) ? partyConfig.levelAdditiveModifier! : 0; + const additive = Math.max(Math.round((scene.currentBattle.waveIndex / 10) * mult), 0); + battle.enemyLevels = battle.enemyLevels.map(level => level + additive); + + battle.enemyLevels.forEach((level, e) => { + let enemySpecies; + let dataSource; + let isBoss = false; + if (!loaded) { + if ((!isNullOrUndefined(trainerType) || trainerConfig) && battle.trainer) { + // Allows overriding a trainer's pokemon to use specific species/data + if (partyConfig?.pokemonConfigs && e < partyConfig.pokemonConfigs.length) { + const config = partyConfig.pokemonConfigs[e]; + level = config.level ? config.level : level; + dataSource = config.dataSource; + enemySpecies = config.species; + isBoss = config.isBoss; + battle.enemyParty[e] = scene.addEnemyPokemon(enemySpecies, level, TrainerSlot.TRAINER, isBoss, dataSource); + } else { + battle.enemyParty[e] = battle.trainer.genPartyMember(e); + } + } else { + if (partyConfig?.pokemonConfigs && e < partyConfig.pokemonConfigs.length) { + const config = partyConfig.pokemonConfigs[e]; + level = config.level ? config.level : level; + dataSource = config.dataSource; + enemySpecies = config.species; + isBoss = config.isBoss; + if (isBoss) { + scene.currentBattle.mysteryEncounter!.encounterMode = MysteryEncounterMode.BOSS_BATTLE; + } + } else { + enemySpecies = scene.randomSpecies(battle.waveIndex, level, true); + } + + battle.enemyParty[e] = scene.addEnemyPokemon(enemySpecies, level, TrainerSlot.NONE, isBoss, dataSource); + } + } + + const enemyPokemon = scene.getEnemyParty()[e]; + + // Make sure basic data is clean + enemyPokemon.hp = enemyPokemon.getMaxHp(); + enemyPokemon.status = null; + enemyPokemon.passive = false; + + if (e < (doubleBattle ? 2 : 1)) { + enemyPokemon.setX(-66 + enemyPokemon.getFieldPositionOffset()[0]); + enemyPokemon.resetSummonData(); + } + + if (!loaded && isNullOrUndefined(partyConfig.countAsSeen) || partyConfig.countAsSeen) { + scene.gameData.setPokemonSeen(enemyPokemon, true, !!(trainerType || trainerConfig)); + } + + if (partyConfig?.pokemonConfigs && e < partyConfig.pokemonConfigs.length) { + const config = partyConfig.pokemonConfigs[e]; + + // Set form + if (!isNullOrUndefined(config.nickname)) { + enemyPokemon.nickname = btoa(unescape(encodeURIComponent(config.nickname!))); + } + + // Generate new id, reset status and HP in case using data source + if (config.dataSource) { + enemyPokemon.id = Utils.randSeedInt(4294967296); + } + + // Set form + if (!isNullOrUndefined(config.formIndex)) { + enemyPokemon.formIndex = config.formIndex!; + } + + // Set shiny + if (!isNullOrUndefined(config.shiny)) { + enemyPokemon.shiny = config.shiny!; + } + + // Set Variant + if (enemyPokemon.shiny && !isNullOrUndefined(config.variant)) { + enemyPokemon.variant = config.variant!; + } + + // Set custom mystery encounter data fields (such as sprite scale, custom abilities, types, etc.) + if (!isNullOrUndefined(config.mysteryEncounterPokemonData)) { + enemyPokemon.mysteryEncounterPokemonData = config.mysteryEncounterPokemonData!; + } + + // Set Boss + if (config.isBoss) { + let segments = !isNullOrUndefined(config.bossSegments) ? config.bossSegments! : scene.getEncounterBossSegments(scene.currentBattle.waveIndex, level, enemySpecies, true); + if (!isNullOrUndefined(config.bossSegmentModifier)) { + segments += config.bossSegmentModifier!; + } + enemyPokemon.setBoss(true, segments); + } + + // Set Passive + if (config.passive) { + enemyPokemon.passive = true; + } + + // Set Nature + if (config.nature) { + enemyPokemon.nature = config.nature; + } + + // Set IVs + if (config.ivs) { + enemyPokemon.ivs = config.ivs; + } + + // Set Status + const statusEffects = config.status; + if (statusEffects) { + // Default to cureturn 3 for sleep + const status = Array.isArray(statusEffects) ? statusEffects[0] : statusEffects; + const cureTurn = Array.isArray(statusEffects) ? statusEffects[1] : statusEffects === StatusEffect.SLEEP ? 3 : undefined; + enemyPokemon.status = new Status(status, 0, cureTurn); + } + + // Set summon data fields + if (!enemyPokemon.summonData) { + enemyPokemon.summonData = new PokemonSummonData(); + } + + // Set ability + if (!isNullOrUndefined(config.abilityIndex)) { + enemyPokemon.abilityIndex = config.abilityIndex!; + } + + // Set gender + if (!isNullOrUndefined(config.gender)) { + enemyPokemon.gender = config.gender!; + enemyPokemon.summonData.gender = config.gender!; + } + + // Set AI type + if (!isNullOrUndefined(config.aiType)) { + enemyPokemon.aiType = config.aiType!; + } + + // Set moves + if (config?.moveSet && config.moveSet.length > 0) { + const moves = config.moveSet.map(m => new PokemonMove(m)); + enemyPokemon.moveset = moves; + enemyPokemon.summonData.moveset = moves; + } + + // Set tags + if (config.tags && config.tags.length > 0) { + const tags = config.tags; + tags.forEach(tag => enemyPokemon.addTag(tag)); + } + + // mysteryEncounterBattleEffects will only be used IFF MYSTERY_ENCOUNTER_POST_SUMMON tag is applied + if (config.mysteryEncounterBattleEffects) { + enemyPokemon.mysteryEncounterBattleEffects = config.mysteryEncounterBattleEffects; + } + + // Requires re-priming summon data to update everything properly + enemyPokemon.primeSummonData(enemyPokemon.summonData); + + if (enemyPokemon.isShiny() && !enemyPokemon["shinySparkle"]) { + enemyPokemon.initShinySparkle(); + } + enemyPokemon.initBattleInfo(); + enemyPokemon.getBattleInfo().initInfo(enemyPokemon); + enemyPokemon.generateName(); + } + + loadEnemyAssets.push(enemyPokemon.loadAssets()); + + console.log(enemyPokemon.name, enemyPokemon.species.speciesId, enemyPokemon.stats); + }); + + scene.pushPhase(new MysteryEncounterBattlePhase(scene, partyConfig.disableSwitch)); + + await Promise.all(loadEnemyAssets); + battle.enemyParty.forEach((enemyPokemon_2, e_1) => { + if (e_1 < (doubleBattle ? 2 : 1)) { + enemyPokemon_2.setVisible(false); + if (battle.double) { + enemyPokemon_2.setFieldPosition(e_1 ? FieldPosition.RIGHT : FieldPosition.LEFT); + } + // Spawns at current visible field instead of on "next encounter" field (off screen to the left) + enemyPokemon_2.x += 300; + } + }); + if (!loaded) { + regenerateModifierPoolThresholds(scene.getEnemyField(), battle.battleType === BattleType.TRAINER ? ModifierPoolType.TRAINER : ModifierPoolType.WILD); + const customModifierTypes = partyConfig?.pokemonConfigs + ?.filter(config => config?.modifierConfigs) + .map(config => config.modifierConfigs!); + scene.generateEnemyModifiers(customModifierTypes); + } +} + +/** + * Load special move animations/sfx for hard-coded encounter-specific moves that a pokemon uses at the start of an encounter + * See: [startOfBattleEffects](IMysteryEncounter.startOfBattleEffects) for more details + * + * This promise does not need to be awaited on if called in an encounter onInit (will just load lazily) + * @param scene + * @param moves + */ +export function loadCustomMovesForEncounter(scene: BattleScene, moves: Moves | Moves[]) { + moves = Array.isArray(moves) ? moves : [moves]; + return Promise.all(moves.map(move => initMoveAnim(scene, move))) + .then(() => loadMoveAnimAssets(scene, moves)); +} + +/** + * Will update player money, and animate change (sound optional) + * @param scene + * @param changeValue + * @param playSound + * @param showMessage + */ +export function updatePlayerMoney(scene: BattleScene, changeValue: number, playSound: boolean = true, showMessage: boolean = true) { + scene.money = Math.min(Math.max(scene.money + changeValue, 0), Number.MAX_SAFE_INTEGER); + scene.updateMoneyText(); + scene.animateMoneyChanged(false); + if (playSound) { + scene.playSound("se/buy"); + } + if (showMessage) { + if (changeValue < 0) { + scene.queueMessage(i18next.t("mysteryEncounterMessages:paid_money", { amount: -changeValue }), null, true); + } else { + scene.queueMessage(i18next.t("mysteryEncounterMessages:receive_money", { amount: changeValue }), null, true); + } + } +} + +/** + * Converts modifier bullshit to an actual item + * @param scene Battle Scene + * @param modifier + * @param pregenArgs Can specify BerryType for berries, TM for TMs, AttackBoostType for item, etc. + */ +export function generateModifierType(scene: BattleScene, modifier: () => ModifierType, pregenArgs?: any[]): ModifierType | null { + const modifierId = Object.keys(modifierTypes).find(k => modifierTypes[k] === modifier); + if (!modifierId) { + return null; + } + + let result: ModifierType = modifierTypes[modifierId](); + + // Populates item id and tier (order matters) + result = result + .withIdFromFunc(modifierTypes[modifierId]) + .withTierFromPool(); + + return result instanceof ModifierTypeGenerator ? result.generateType(scene.getParty(), pregenArgs) : result; +} + +/** + * Converts modifier bullshit to an actual item + * @param scene - Battle Scene + * @param modifier + * @param pregenArgs - can specify BerryType for berries, TM for TMs, AttackBoostType for item, etc. + */ +export function generateModifierTypeOption(scene: BattleScene, modifier: () => ModifierType, pregenArgs?: any[]): ModifierTypeOption | null { + const result = generateModifierType(scene, modifier, pregenArgs); + if (result) { + return new ModifierTypeOption(result, 0); + } + return result; +} + +/** + * This function is intended for use inside onPreOptionPhase() of an encounter option + * @param scene + * @param onPokemonSelected - Any logic that needs to be performed when Pokemon is chosen + * If a second option needs to be selected, onPokemonSelected should return a OptionSelectItem[] object + * @param onPokemonNotSelected - Any logic that needs to be performed if no Pokemon is chosen + * @param selectablePokemonFilter + */ +export function selectPokemonForOption(scene: BattleScene, onPokemonSelected: (pokemon: PlayerPokemon) => void | OptionSelectItem[], onPokemonNotSelected?: () => void, selectablePokemonFilter?: PokemonSelectFilter): Promise { + return new Promise(resolve => { + const modeToSetOnExit = scene.ui.getMode(); + + // Open party screen to choose pokemon + scene.ui.setMode(Mode.PARTY, PartyUiMode.SELECT, -1, (slotIndex: number, option: PartyOption) => { + if (slotIndex < scene.getParty().length) { + scene.ui.setMode(modeToSetOnExit).then(() => { + const pokemon = scene.getParty()[slotIndex]; + const secondaryOptions = onPokemonSelected(pokemon); + if (!secondaryOptions) { + scene.currentBattle.mysteryEncounter!.setDialogueToken("selectedPokemon", pokemon.getNameToRender()); + resolve(true); + return; + } + + // There is a second option to choose after selecting the Pokemon + scene.ui.setMode(Mode.MESSAGE).then(() => { + const displayOptions = () => { + // Always appends a cancel option to bottom of options + const fullOptions = secondaryOptions.map(option => { + // Update handler to resolve promise + const onSelect = option.handler; + option.handler = () => { + onSelect(); + scene.currentBattle.mysteryEncounter!.setDialogueToken("selectedPokemon", pokemon.getNameToRender()); + resolve(true); + return true; + }; + return option; + }).concat({ + label: i18next.t("menu:cancel"), + handler: () => { + scene.ui.clearText(); + scene.ui.setMode(modeToSetOnExit); + resolve(false); + return true; + }, + onHover: () => { + showEncounterText(scene, i18next.t("mysteryEncounterMessages:cancel_option"), 0, 0, false); + } + }); + + const config: OptionSelectConfig = { + options: fullOptions, + maxOptions: 7, + yOffset: 0, + supportHover: true + }; + + // Do hover over the starting selection option + if (fullOptions[0].onHover) { + fullOptions[0].onHover(); + } + scene.ui.setModeWithoutClear(Mode.OPTION_SELECT, config, null, true); + }; + + const textPromptKey = scene.currentBattle.mysteryEncounter?.selectedOption?.dialogue?.secondOptionPrompt; + if (!textPromptKey) { + displayOptions(); + } else { + showEncounterText(scene, textPromptKey).then(() => displayOptions()); + } + }); + }); + } else { + scene.ui.setMode(modeToSetOnExit).then(() => { + if (onPokemonNotSelected) { + onPokemonNotSelected(); + } + resolve(false); + }); + } + }, selectablePokemonFilter); + }); +} + +interface PokemonAndOptionSelected { + selectedPokemonIndex: number; + selectedOptionIndex: number; +} + +/** + * This function is intended for use inside onPreOptionPhase() of an encounter option + * @param scene + * If a second option needs to be selected, onPokemonSelected should return a OptionSelectItem[] object + * @param options + * @param optionSelectPromptKey + * @param selectablePokemonFilter + * @param onHoverOverCancelOption + */ +export function selectOptionThenPokemon(scene: BattleScene, options: OptionSelectItem[], optionSelectPromptKey: string, selectablePokemonFilter?: PokemonSelectFilter, onHoverOverCancelOption?: () => void): Promise { + return new Promise(resolve => { + const modeToSetOnExit = scene.ui.getMode(); + + const displayOptions = (config: OptionSelectConfig) => { + scene.ui.setMode(Mode.MESSAGE).then(() => { + if (!optionSelectPromptKey) { + // Do hover over the starting selection option + if (fullOptions[0].onHover) { + fullOptions[0].onHover(); + } + scene.ui.setMode(Mode.OPTION_SELECT, config); + } else { + showEncounterText(scene, optionSelectPromptKey).then(() => { + // Do hover over the starting selection option + if (fullOptions[0].onHover) { + fullOptions[0].onHover(); + } + scene.ui.setMode(Mode.OPTION_SELECT, config); + }); + } + }); + }; + + const selectPokemonAfterOption = (selectedOptionIndex: number) => { + // Open party screen to choose a Pokemon + scene.ui.setMode(Mode.PARTY, PartyUiMode.SELECT, -1, (slotIndex: number, option: PartyOption) => { + if (slotIndex < scene.getParty().length) { + // Pokemon and option selected + scene.ui.setMode(modeToSetOnExit).then(() => { + const result: PokemonAndOptionSelected = { selectedPokemonIndex: slotIndex, selectedOptionIndex: selectedOptionIndex }; + resolve(result); + }); + } else { + // Back to first option select screen + displayOptions(config); + } + }, selectablePokemonFilter); + }; + + // Always appends a cancel option to bottom of options + const fullOptions = options.map((option, index) => { + // Update handler to resolve promise + const onSelect = option.handler; + option.handler = () => { + onSelect(); + selectPokemonAfterOption(index); + return true; + }; + return option; + }).concat({ + label: i18next.t("menu:cancel"), + handler: () => { + scene.ui.clearText(); + scene.ui.setMode(modeToSetOnExit); + resolve(null); + return true; + }, + onHover: () => { + if (onHoverOverCancelOption) { + onHoverOverCancelOption(); + } + showEncounterText(scene, i18next.t("mysteryEncounterMessages:cancel_option"), 0, 0, false); + } + }); + + const config: OptionSelectConfig = { + options: fullOptions, + maxOptions: 7, + yOffset: 0, + supportHover: true + }; + + displayOptions(config); + }); +} + +/** + * Will initialize reward phases to follow the mystery encounter + * Can have shop displayed or skipped + * @param scene - Battle Scene + * @param customShopRewards - adds a shop phase with the specified rewards / reward tiers + * @param eggRewards + * @param preRewardsCallback - can execute an arbitrary callback before the new phases if necessary (useful for updating items/party/injecting new phases before {@linkcode MysteryEncounterRewardsPhase}) + */ +export function setEncounterRewards(scene: BattleScene, customShopRewards?: CustomModifierSettings, eggRewards?: IEggOptions[], preRewardsCallback?: Function) { + scene.currentBattle.mysteryEncounter!.doEncounterRewards = (scene: BattleScene) => { + if (preRewardsCallback) { + preRewardsCallback(); + } + + if (customShopRewards) { + scene.unshiftPhase(new SelectModifierPhase(scene, 0, undefined, customShopRewards)); + } else { + scene.tryRemovePhase(p => p instanceof SelectModifierPhase); + } + + if (eggRewards) { + eggRewards.forEach(eggOptions => { + const egg = new Egg(eggOptions); + egg.addEggToGameData(scene); + }); + } + + return true; + }; +} + +/** + * Will initialize exp phases into the phase queue (these are in addition to any combat or other exp earned) + * Exp Share and Exp Balance will still function as normal + * @param scene - Battle Scene + * @param participantId - id/s of party pokemon that get full exp value. Other party members will receive Exp Share amounts + * @param baseExpValue - gives exp equivalent to a pokemon of the wave index's level. + * Guidelines: + * 36 - Sunkern (lowest in game) + * 62-64 - regional starter base evos + * 100 - Scyther + * 170 - Spiritomb + * 250 - Gengar + * 290 - trio legendaries + * 340 - box legendaries + * 608 - Blissey (highest in game) + * https://bulbapedia.bulbagarden.net/wiki/List_of_Pok%C3%A9mon_by_effort_value_yield_(Generation_IX) + * @param useWaveIndex - set to false when directly passing the the full exp value instead of baseExpValue + */ +export function setEncounterExp(scene: BattleScene, participantId: number | number[], baseExpValue: number, useWaveIndex: boolean = true) { + const participantIds = Array.isArray(participantId) ? participantId : [participantId]; + + scene.currentBattle.mysteryEncounter!.doEncounterExp = (scene: BattleScene) => { + scene.unshiftPhase(new PartyExpPhase(scene, baseExpValue, useWaveIndex, new Set(participantIds))); + + return true; + }; +} + +export class OptionSelectSettings { + hideDescription?: boolean; + slideInDescription?: boolean; + overrideTitle?: string; + overrideDescription?: string; + overrideQuery?: string; + overrideOptions?: MysteryEncounterOption[]; + startingCursorIndex?: number; +} + +/** + * Can be used to queue a new series of Options to select for an Encounter + * MUST be used only in onOptionPhase, will not work in onPreOptionPhase or onPostOptionPhase + * @param scene + * @param optionSelectSettings + */ +export function initSubsequentOptionSelect(scene: BattleScene, optionSelectSettings: OptionSelectSettings) { + scene.pushPhase(new MysteryEncounterPhase(scene, optionSelectSettings)); +} + +/** + * Can be used to exit an encounter without any battles or followup + * Will skip any shops and rewards, and queue the next encounter phase as normal + * @param scene + * @param addHealPhase - when true, will add a shop phase to end of encounter with 0 rewards but healing items are available + * @param encounterMode - Can set custom encounter mode if necessary (may be required for forcing Pokemon to return before next phase) + */ +export function leaveEncounterWithoutBattle(scene: BattleScene, addHealPhase: boolean = false, encounterMode: MysteryEncounterMode = MysteryEncounterMode.NO_BATTLE) { + scene.currentBattle.mysteryEncounter!.encounterMode = encounterMode; + scene.clearPhaseQueue(); + scene.clearPhaseQueueSplice(); + handleMysteryEncounterVictory(scene, addHealPhase); +} + +/** + * + * @param scene + * @param addHealPhase - Adds an empty shop phase to allow player to purchase healing items + * @param doNotContinue - default `false`. If set to true, will not end the battle and continue to next wave + */ +export function handleMysteryEncounterVictory(scene: BattleScene, addHealPhase: boolean = false, doNotContinue: boolean = false) { + const allowedPkm = scene.getParty().filter((pkm) => pkm.isAllowedInBattle()); + + if (allowedPkm.length === 0) { + scene.clearPhaseQueue(); + scene.unshiftPhase(new GameOverPhase(scene)); + return; + } + + // If in repeated encounter variant, do nothing + // Variant must eventually be swapped in order to handle "true" end of the encounter + const encounter = scene.currentBattle.mysteryEncounter!; + if (encounter.continuousEncounter || doNotContinue) { + return; + } else if (encounter.encounterMode === MysteryEncounterMode.NO_BATTLE) { + scene.pushPhase(new MysteryEncounterRewardsPhase(scene, addHealPhase)); + scene.pushPhase(new EggLapsePhase(scene)); + } else if (!scene.getEnemyParty().find(p => encounter.encounterMode !== MysteryEncounterMode.TRAINER_BATTLE ? p.isOnField() : !p?.isFainted(true))) { + scene.pushPhase(new BattleEndPhase(scene)); + if (encounter.encounterMode === MysteryEncounterMode.TRAINER_BATTLE) { + scene.pushPhase(new TrainerVictoryPhase(scene)); + } + if (scene.gameMode.isEndless || !scene.gameMode.isWaveFinal(scene.currentBattle.waveIndex)) { + scene.pushPhase(new MysteryEncounterRewardsPhase(scene, addHealPhase)); + if (!encounter.doContinueEncounter) { + // Only lapse eggs once for multi-battle encounters + scene.pushPhase(new EggLapsePhase(scene)); + } + } + } +} + +/** + * Similar to {@linkcode handleMysteryEncounterVictory}, but for cases where the player lost a battle or failed a challenge + * @param scene + * @param addHealPhase + */ +export function handleMysteryEncounterBattleFailed(scene: BattleScene, addHealPhase: boolean = false, doNotContinue: boolean = false) { + const allowedPkm = scene.getParty().filter((pkm) => pkm.isAllowedInBattle()); + + if (allowedPkm.length === 0) { + scene.clearPhaseQueue(); + scene.unshiftPhase(new GameOverPhase(scene)); + return; + } + + // If in repeated encounter variant, do nothing + // Variant must eventually be swapped in order to handle "true" end of the encounter + const encounter = scene.currentBattle.mysteryEncounter!; + if (encounter.continuousEncounter || doNotContinue) { + return; + } else if (encounter.encounterMode !== MysteryEncounterMode.NO_BATTLE) { + scene.pushPhase(new BattleEndPhase(scene, false)); + } + + scene.pushPhase(new MysteryEncounterRewardsPhase(scene, addHealPhase)); + + if (!encounter.doContinueEncounter) { + // Only lapse eggs once for multi-battle encounters + scene.pushPhase(new EggLapsePhase(scene)); + } +} + +/** + * + * @param scene + * @param hide - If true, performs ease out and hide visuals. If false, eases in visuals. Defaults to true + * @param destroy - If true, will destroy visuals ONLY ON HIDE TRANSITION. Does nothing on show. Defaults to true + * @param duration + */ +export function transitionMysteryEncounterIntroVisuals(scene: BattleScene, hide: boolean = true, destroy: boolean = true, duration: number = 750): Promise { + return new Promise(resolve => { + const introVisuals = scene.currentBattle.mysteryEncounter!.introVisuals; + const enemyPokemon = scene.getEnemyField(); + if (enemyPokemon) { + scene.currentBattle.enemyParty = []; + } + if (introVisuals) { + if (!hide) { + // Make sure visuals are in proper state for showing + introVisuals.setVisible(true); + introVisuals.x = 244; + introVisuals.y = 60; + introVisuals.alpha = 0; + } + + // Transition + scene.tweens.add({ + targets: [introVisuals, enemyPokemon], + x: `${hide? "+" : "-"}=16`, + y: `${hide ? "-" : "+"}=16`, + alpha: hide ? 0 : 1, + ease: "Sine.easeInOut", + duration, + onComplete: () => { + if (hide && destroy) { + scene.field.remove(introVisuals, true); + + enemyPokemon.forEach(pokemon => { + scene.field.remove(pokemon, true); + }); + + scene.currentBattle.mysteryEncounter!.introVisuals = undefined; + } + resolve(true); + } + }); + } else { + resolve(true); + } + }); +} + +/** + * Will queue moves for any pokemon to use before the first CommandPhase of a battle + * Mostly useful for allowing {@linkcode MysteryEncounter} enemies to "cheat" and use moves before the first turn + * @param scene + */ +export function handleMysteryEncounterBattleStartEffects(scene: BattleScene) { + const encounter = scene.currentBattle.mysteryEncounter; + if (scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER && encounter && encounter.encounterMode !== MysteryEncounterMode.NO_BATTLE && !encounter.startOfBattleEffectsComplete) { + const effects = encounter.startOfBattleEffects; + effects.forEach(effect => { + let source; + if (effect.sourcePokemon) { + source = effect.sourcePokemon; + } else if (!isNullOrUndefined(effect.sourceBattlerIndex)) { + if (effect.sourceBattlerIndex === BattlerIndex.ATTACKER) { + source = scene.getEnemyField()[0]; + } else if (effect.sourceBattlerIndex === BattlerIndex.ENEMY) { + source = scene.getEnemyField()[0]; + } else if (effect.sourceBattlerIndex === BattlerIndex.ENEMY_2) { + source = scene.getEnemyField()[1]; + } else if (effect.sourceBattlerIndex === BattlerIndex.PLAYER) { + source = scene.getPlayerField()[0]; + } else if (effect.sourceBattlerIndex === BattlerIndex.PLAYER_2) { + source = scene.getPlayerField()[1]; + } + } else { + source = scene.getEnemyField()[0]; + } + scene.pushPhase(new MovePhase(scene, source, effect.targets, effect.move, effect.followUp, effect.ignorePp)); + }); + + // Pseudo turn end phase to reset flinch states, Endure, etc. + scene.pushPhase(new MysteryEncounterBattleStartCleanupPhase(scene)); + + encounter.startOfBattleEffectsComplete = true; + } +} + +/** + * Can queue extra phases or logic during {@linkcode TurnInitPhase} + * Should mostly just be used for injecting custom phases into the battle system on turn start + * @param scene + * @return boolean - if true, will skip the remainder of the {@linkcode TurnInitPhase} + */ +export function handleMysteryEncounterTurnStartEffects(scene: BattleScene): boolean { + const encounter = scene.currentBattle.mysteryEncounter; + if (scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER && encounter && encounter.onTurnStart) { + return encounter.onTurnStart(scene); + } + + return false; +} + +/** + * TODO: remove once encounter spawn rate is finalized + * Just a helper function to calculate aggregate stats for MEs in a Classic run + * @param scene + * @param baseSpawnWeight + */ +export function calculateMEAggregateStats(scene: BattleScene, baseSpawnWeight: number) { + const numRuns = 1000; + let run = 0; + const biomes = Object.keys(Biome).filter(key => isNaN(Number(key))); + const alwaysPickTheseBiomes = [Biome.ISLAND, Biome.ABYSS, Biome.WASTELAND, Biome.FAIRY_CAVE, Biome.TEMPLE, Biome.LABORATORY, Biome.SPACE, Biome.WASTELAND]; + + const calculateNumEncounters = (): any[] => { + let encounterRate = baseSpawnWeight; // BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT + const numEncounters = [0, 0, 0, 0]; + let mostRecentEncounterWave = 0; + const encountersByBiome = new Map(biomes.map(b => [b, 0])); + const validMEfloorsByBiome = new Map(biomes.map(b => [b, 0])); + let currentBiome = Biome.TOWN; + let currentArena = scene.newArena(currentBiome); + scene.setSeed(Utils.randomString(24)); + scene.resetSeed(); + for (let i = 10; i < 180; i++) { + // Boss + if (i % 10 === 0) { + continue; + } + + // New biome + if (i % 10 === 1) { + if (Array.isArray(biomeLinks[currentBiome])) { + let biomes: Biome[]; + scene.executeWithSeedOffset(() => { + biomes = (biomeLinks[currentBiome] as (Biome | [Biome, number])[]) + .filter(b => { + return !Array.isArray(b) || !Utils.randSeedInt(b[1]); + }) + .map(b => !Array.isArray(b) ? b : b[0]); + }, i * 100); + if (biomes! && biomes.length > 0) { + const specialBiomes = biomes.filter(b => alwaysPickTheseBiomes.includes(b)); + if (specialBiomes.length > 0) { + currentBiome = specialBiomes[Utils.randSeedInt(specialBiomes.length)]; + } else { + currentBiome = biomes[Utils.randSeedInt(biomes.length)]; + } + } + } else if (biomeLinks.hasOwnProperty(currentBiome)) { + currentBiome = (biomeLinks[currentBiome] as Biome); + } else { + if (!(i % 50)) { + currentBiome = Biome.END; + } else { + currentBiome = scene.generateRandomBiome(i); + } + } + + currentArena = scene.newArena(currentBiome); + } + + // Fixed battle + if (scene.gameMode.isFixedBattle(i)) { + continue; + } + + // Trainer + if (scene.gameMode.isWaveTrainer(i, currentArena)) { + continue; + } + + // Otherwise, roll encounter + + const roll = Utils.randSeedInt(256); + validMEfloorsByBiome.set(Biome[currentBiome], (validMEfloorsByBiome.get(Biome[currentBiome]) ?? 0) + 1); + + // If total number of encounters is lower than expected for the run, slightly favor a new encounter + // Do the reverse as well + const expectedEncountersByFloor = AVERAGE_ENCOUNTERS_PER_RUN_TARGET / (180 - 10) * (i - 10); + const currentRunDiffFromAvg = expectedEncountersByFloor - numEncounters.reduce((a, b) => a + b); + const favoredEncounterRate = encounterRate + currentRunDiffFromAvg * 15; + + // If the most recent ME was 3 or fewer waves ago, can never spawn a ME + const canSpawn = (i - mostRecentEncounterWave) > 3; + + if (canSpawn && roll < favoredEncounterRate) { + mostRecentEncounterWave = i; + encounterRate = baseSpawnWeight; + + // Calculate encounter rarity + // Common / Uncommon / Rare / Super Rare (base is out of 128) + const tierWeights = [66, 40, 19, 3]; + + // Adjust tier weights by currently encountered events (pity system that lowers odds of multiple Common/Great) + tierWeights[0] = tierWeights[0] - 6 * numEncounters[0]; + tierWeights[1] = tierWeights[1] - 4 * numEncounters[1]; + + const totalWeight = tierWeights.reduce((a, b) => a + b); + const tierValue = Utils.randSeedInt(totalWeight); + const commonThreshold = totalWeight - tierWeights[0]; // 64 - 32 = 32 + const uncommonThreshold = totalWeight - tierWeights[0] - tierWeights[1]; // 64 - 32 - 16 = 16 + const rareThreshold = totalWeight - tierWeights[0] - tierWeights[1] - tierWeights[2]; // 64 - 32 - 16 - 10 = 6 + + tierValue > commonThreshold ? ++numEncounters[0] : tierValue > uncommonThreshold ? ++numEncounters[1] : tierValue > rareThreshold ? ++numEncounters[2] : ++numEncounters[3]; + encountersByBiome.set(Biome[currentBiome], (encountersByBiome.get(Biome[currentBiome]) ?? 0) + 1); + } else { + encounterRate += WEIGHT_INCREMENT_ON_SPAWN_MISS; + } + } + + return [numEncounters, encountersByBiome, validMEfloorsByBiome]; + }; + + const encounterRuns: number[][] = []; + const encountersByBiomeRuns: Map[] = []; + const validFloorsByBiome: Map[] = []; + while (run < numRuns) { + scene.executeWithSeedOffset(() => { + const [numEncounters, encountersByBiome, validMEfloorsByBiome] = calculateNumEncounters(); + encounterRuns.push(numEncounters); + encountersByBiomeRuns.push(encountersByBiome); + validFloorsByBiome.push(validMEfloorsByBiome); + }, 1000 * run); + run++; + } + + const n = encounterRuns.length; + const totalEncountersInRun = encounterRuns.map(run => run.reduce((a, b) => a + b)); + const totalMean = totalEncountersInRun.reduce((a, b) => a + b) / n; + const totalStd = Math.sqrt(totalEncountersInRun.map(x => Math.pow(x - totalMean, 2)).reduce((a, b) => a + b) / n); + const commonMean = encounterRuns.reduce((a, b) => a + b[0], 0) / n; + const uncommonMean = encounterRuns.reduce((a, b) => a + b[1], 0) / n; + const rareMean = encounterRuns.reduce((a, b) => a + b[2], 0) / n; + const superRareMean = encounterRuns.reduce((a, b) => a + b[3], 0) / n; + + const encountersPerRunPerBiome = encountersByBiomeRuns.reduce((a, b) => { + for (const biome of a.keys()) { + a.set(biome, a.get(biome)! + b.get(biome)!); + } + return a; + }); + const meanEncountersPerRunPerBiome: Map = new Map(); + encountersPerRunPerBiome.forEach((value, key) => { + meanEncountersPerRunPerBiome.set(key, value / n); + }); + + const validMEFloorsPerRunPerBiome = validFloorsByBiome.reduce((a, b) => { + for (const biome of a.keys()) { + a.set(biome, a.get(biome)! + b.get(biome)!); + } + return a; + }); + const meanMEFloorsPerRunPerBiome: Map = new Map(); + validMEFloorsPerRunPerBiome.forEach((value, key) => { + meanMEFloorsPerRunPerBiome.set(key, value / n); + }); + + let stats = `Starting weight: ${baseSpawnWeight}\nAverage MEs per run: ${totalMean}\nStandard Deviation: ${totalStd}\nAvg Commons: ${commonMean}\nAvg Greats: ${uncommonMean}\nAvg Ultras: ${rareMean}\nAvg Rogues: ${superRareMean}\n`; + + const meanEncountersPerRunPerBiomeSorted = [...meanEncountersPerRunPerBiome.entries()].sort((e1, e2) => e2[1] - e1[1]); + meanEncountersPerRunPerBiomeSorted.forEach(value => stats = stats + `${value[0]}: avg valid floors ${meanMEFloorsPerRunPerBiome.get(value[0])}, avg MEs ${value[1]},\n`); + + console.log(stats); +} + + +/** + * TODO: remove once encounter spawn rate is finalized + * Just a helper function to calculate aggregate stats for MEs in a Classic run + * @param scene + * @param luckValue - 0 to 14 + */ +export function calculateRareSpawnAggregateStats(scene: BattleScene, luckValue: number) { + const numRuns = 1000; + let run = 0; + + const calculateNumRareEncounters = (): any[] => { + const bossEncountersByRarity = [0, 0, 0, 0]; + scene.setSeed(Utils.randomString(24)); + scene.resetSeed(); + // There are 12 wild boss floors + for (let i = 0; i < 12; i++) { + // Roll boss tier + // luck influences encounter rarity + let luckModifier = 0; + if (!isNaN(luckValue)) { + luckModifier = luckValue * 0.5; + } + const tierValue = Utils.randSeedInt(64 - luckModifier); + const tier = tierValue >= 20 ? BiomePoolTier.BOSS : tierValue >= 6 ? BiomePoolTier.BOSS_RARE : tierValue >= 1 ? BiomePoolTier.BOSS_SUPER_RARE : BiomePoolTier.BOSS_ULTRA_RARE; + + switch (tier) { + default: + case BiomePoolTier.BOSS: + ++bossEncountersByRarity[0]; + break; + case BiomePoolTier.BOSS_RARE: + ++bossEncountersByRarity[1]; + break; + case BiomePoolTier.BOSS_SUPER_RARE: + ++bossEncountersByRarity[2]; + break; + case BiomePoolTier.BOSS_ULTRA_RARE: + ++bossEncountersByRarity[3]; + break; + } + } + + return bossEncountersByRarity; + }; + + const encounterRuns: number[][] = []; + while (run < numRuns) { + scene.executeWithSeedOffset(() => { + const bossEncountersByRarity = calculateNumRareEncounters(); + encounterRuns.push(bossEncountersByRarity); + }, 1000 * run); + run++; + } + + const n = encounterRuns.length; + // const totalEncountersInRun = encounterRuns.map(run => run.reduce((a, b) => a + b)); + // const totalMean = totalEncountersInRun.reduce((a, b) => a + b) / n; + // const totalStd = Math.sqrt(totalEncountersInRun.map(x => Math.pow(x - totalMean, 2)).reduce((a, b) => a + b) / n); + const commonMean = encounterRuns.reduce((a, b) => a + b[0], 0) / n; + const rareMean = encounterRuns.reduce((a, b) => a + b[1], 0) / n; + const superRareMean = encounterRuns.reduce((a, b) => a + b[2], 0) / n; + const ultraRareMean = encounterRuns.reduce((a, b) => a + b[3], 0) / n; + + const stats = `Avg Commons: ${commonMean}\nAvg Rare: ${rareMean}\nAvg Super Rare: ${superRareMean}\nAvg Ultra Rare: ${ultraRareMean}\n`; + + console.log(stats); +} diff --git a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts new file mode 100644 index 00000000000..5db84186471 --- /dev/null +++ b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts @@ -0,0 +1,831 @@ +import BattleScene from "#app/battle-scene"; +import i18next from "i18next"; +import { isNullOrUndefined, randSeedInt } from "#app/utils"; +import { PokemonHeldItemModifier } from "#app/modifier/modifier"; +import Pokemon, { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon"; +import { doPokeballBounceAnim, getPokeballAtlasKey, getPokeballCatchMultiplier, getPokeballTintColor, PokeballType } from "#app/data/pokeball"; +import { PlayerGender } from "#enums/player-gender"; +import { addPokeballCaptureStars, addPokeballOpenParticles } from "#app/field/anims"; +import { getStatusEffectCatchRateMultiplier, StatusEffect } from "#app/data/status-effect"; +import { achvs } from "#app/system/achv"; +import { Mode } from "#app/ui/ui"; +import { PartyOption, PartyUiMode } from "#app/ui/party-ui-handler"; +import { Species } from "#enums/species"; +import { Type } from "#app/data/type"; +import PokemonSpecies, { getPokemonSpecies, speciesStarters } from "#app/data/pokemon-species"; +import { getEncounterText, queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { getPokemonNameWithAffix } from "#app/messages"; +import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; +import { Gender } from "#app/data/gender"; +import { PermanentStat } from "#enums/stat"; +import { VictoryPhase } from "#app/phases/victory-phase"; +import { SummaryUiMode } from "#app/ui/summary-ui-handler"; + +/** Will give +1 level every 10 waves */ +export const STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER = 1; + +/** + * Gets the sprite key and file root for a given PokemonSpecies (accounts for gender, shiny, variants, forms, and experimental) + * @param species + * @param female + * @param formIndex + * @param shiny + * @param variant + */ +export function getSpriteKeysFromSpecies(species: Species, female?: boolean, formIndex?: number, shiny?: boolean, variant?: number): { spriteKey: string, fileRoot: string } { + const spriteKey = getPokemonSpecies(species).getSpriteKey(female ?? false, formIndex ?? 0, shiny ?? false, variant ?? 0); + const fileRoot = getPokemonSpecies(species).getSpriteAtlasPath(female ?? false, formIndex ?? 0, shiny ?? false, variant ?? 0); + return { spriteKey, fileRoot }; +} + +/** + * Gets the sprite key and file root for a given Pokemon (accounts for gender, shiny, variants, forms, and experimental) + * @param pokemon + */ +export function getSpriteKeysFromPokemon(pokemon: Pokemon): { spriteKey: string, fileRoot: string } { + const spriteKey = pokemon.getSpeciesForm().getSpriteKey(pokemon.getGender() === Gender.FEMALE, pokemon.formIndex, pokemon.shiny, pokemon.variant); + const fileRoot = pokemon.getSpeciesForm().getSpriteAtlasPath(pokemon.getGender() === Gender.FEMALE, pokemon.formIndex, pokemon.shiny, pokemon.variant); + + return { spriteKey, fileRoot }; +} + +/** + * Will never remove the player's last non-fainted Pokemon (if they only have 1) + * Otherwise, picks a Pokemon completely at random and removes from the party + * @param scene + * @param isAllowed Default false. If true, only picks from legal mons. If no legal mons are found (or there is 1, with `doNotReturnLastAllowedMon = true), will return a mon that is not allowed. + * @param isFainted Default false. If true, includes fainted mons. + * @param doNotReturnLastAllowedMon Default false. If true, will never return the last unfainted pokemon in the party. Useful when this function is being used to determine what Pokemon to remove from the party (Don't want to remove last unfainted) + * @returns + */ +export function getRandomPlayerPokemon(scene: BattleScene, isAllowed: boolean = false, isFainted: boolean = false, doNotReturnLastAllowedMon: boolean = false): PlayerPokemon { + const party = scene.getParty(); + let chosenIndex: number; + let chosenPokemon: PlayerPokemon | null = null; + const fullyLegalMons = party.filter(p => (!isAllowed || p.isAllowed()) && (isFainted || !p.isFainted())); + const allowedOnlyMons = party.filter(p => p.isAllowed()); + + if (doNotReturnLastAllowedMon && fullyLegalMons.length === 1) { + // If there is only 1 legal/unfainted mon left, select from fainted legal mons + const faintedLegalMons = party.filter(p => (!isAllowed || p.isAllowed()) && p.isFainted()); + if (faintedLegalMons.length > 0) { + chosenIndex = randSeedInt(faintedLegalMons.length); + chosenPokemon = faintedLegalMons[chosenIndex]; + } + } + if (!chosenPokemon && fullyLegalMons.length > 0) { + chosenIndex = randSeedInt(fullyLegalMons.length); + chosenPokemon = fullyLegalMons[chosenIndex]; + } + if (!chosenPokemon && isAllowed && allowedOnlyMons.length > 0) { + chosenIndex = randSeedInt(allowedOnlyMons.length); + chosenPokemon = allowedOnlyMons[chosenIndex]; + } + if (!chosenPokemon) { + // If no other options worked, returns fully random + chosenIndex = randSeedInt(party.length); + chosenPokemon = party[chosenIndex]; + } + + return chosenPokemon; +} + +/** + * Ties are broken by whatever mon is closer to the front of the party + * @param scene + * @param isAllowed Default false. If true, only picks from legal mons. + * @param isFainted Default false. If true, includes fainted mons. + * @returns + */ +export function getHighestLevelPlayerPokemon(scene: BattleScene, isAllowed: boolean = false, isFainted: boolean = false): PlayerPokemon { + const party = scene.getParty(); + let pokemon: PlayerPokemon | null = null; + + for (const p of party) { + if (isAllowed && !p.isAllowed()) { + continue; + } + if (!isFainted && p.isFainted()) { + continue; + } + + pokemon = pokemon ? pokemon?.level < p?.level ? p : pokemon : p; + } + + return pokemon!; +} + +/** + * Ties are broken by whatever mon is closer to the front of the party + * @param scene + * @param stat Stat to search for + * @param isAllowed Default false. If true, only picks from legal mons. + * @param isFainted Default false. If true, includes fainted mons. + * @returns + */ +export function getHighestStatPlayerPokemon(scene: BattleScene, stat: PermanentStat, isAllowed: boolean = false, isFainted: boolean = false): PlayerPokemon { + const party = scene.getParty(); + let pokemon: PlayerPokemon | null = null; + + for (const p of party) { + if (isAllowed && !p.isAllowed()) { + continue; + } + if (!isFainted && p.isFainted()) { + continue; + } + + pokemon = pokemon ? pokemon.getStat(stat) < p?.getStat(stat) ? p : pokemon : p; + } + + return pokemon!; +} + +/** + * Ties are broken by whatever mon is closer to the front of the party + * @param scene + * @param isAllowed Default false. If true, only picks from legal mons. + * @param isFainted Default false. If true, includes fainted mons. + * @returns + */ +export function getLowestLevelPlayerPokemon(scene: BattleScene, isAllowed: boolean = false, isFainted: boolean = false): PlayerPokemon { + const party = scene.getParty(); + let pokemon: PlayerPokemon | null = null; + + for (const p of party) { + if (isAllowed && !p.isAllowed()) { + continue; + } + if (!isFainted && p.isFainted()) { + continue; + } + + pokemon = pokemon ? pokemon?.level > p?.level ? p : pokemon : p; + } + + return pokemon!; +} + +/** + * Ties are broken by whatever mon is closer to the front of the party + * @param scene + * @param isAllowed Default false. If true, only picks from legal mons. + * @param isFainted Default false. If true, includes fainted mons. + * @returns + */ +export function getHighestStatTotalPlayerPokemon(scene: BattleScene, isAllowed: boolean = false, isFainted: boolean = false): PlayerPokemon { + const party = scene.getParty(); + let pokemon: PlayerPokemon | null = null; + + for (const p of party) { + if (isAllowed && !p.isAllowed()) { + continue; + } + if (!isFainted && p.isFainted()) { + continue; + } + + pokemon = pokemon ? pokemon?.stats.reduce((a, b) => a + b) < p?.stats.reduce((a, b) => a + b) ? p : pokemon : p; + } + + return pokemon!; +} + +/** + * + * NOTE: This returns ANY random species, including those locked behind eggs, etc. + * @param starterTiers + * @param excludedSpecies + * @param types + * @param allowSubLegendary + * @param allowLegendary + * @param allowMythical + * @returns + */ +export function getRandomSpeciesByStarterTier(starterTiers: number | [number, number], excludedSpecies?: Species[], types?: Type[], allowSubLegendary: boolean = true, allowLegendary: boolean = true, allowMythical: boolean = true): Species { + let min = Array.isArray(starterTiers) ? starterTiers[0] : starterTiers; + let max = Array.isArray(starterTiers) ? starterTiers[1] : starterTiers; + + let filteredSpecies: [PokemonSpecies, number][] = Object.keys(speciesStarters) + .map(s => [parseInt(s) as Species, speciesStarters[s] as number]) + .filter(s => { + const pokemonSpecies = getPokemonSpecies(s[0]); + return pokemonSpecies && (!excludedSpecies || !excludedSpecies.includes(s[0]) + && (allowSubLegendary || !pokemonSpecies.subLegendary) + && (allowLegendary || !pokemonSpecies.legendary) + && (allowMythical || !pokemonSpecies.mythical)); + }) + .map(s => [getPokemonSpecies(s[0]), s[1]]); + + if (types && types.length > 0) { + filteredSpecies = filteredSpecies.filter(s => types.includes(s[0].type1) || (!isNullOrUndefined(s[0].type2) && types.includes(s[0].type2!))); + } + + // If no filtered mons exist at specified starter tiers, will expand starter search range until there are + // Starts by decrementing starter tier min until it is 0, then increments tier max up to 10 + let tryFilterStarterTiers: [PokemonSpecies, number][] = filteredSpecies.filter(s => (s[1] >= min && s[1] <= max)); + while (tryFilterStarterTiers.length === 0 && (min !== 0 && max !== 10)) { + if (min > 0) { + min--; + } else { + max++; + } + + tryFilterStarterTiers = filteredSpecies.filter(s => s[1] >= min && s[1] <= max); + } + + if (tryFilterStarterTiers.length > 0) { + const index = randSeedInt(tryFilterStarterTiers.length); + return Phaser.Math.RND.shuffle(tryFilterStarterTiers)[index][0].speciesId; + } + + return Species.BULBASAUR; +} + +/** + * Takes care of handling player pokemon KO (with all its side effects) + * + * @param scene the battle scene + * @param pokemon the player pokemon to KO + */ +export function koPlayerPokemon(scene: BattleScene, pokemon: PlayerPokemon) { + pokemon.hp = 0; + pokemon.trySetStatus(StatusEffect.FAINT); + pokemon.updateInfo(); + queueEncounterMessage(scene, i18next.t("battle:fainted", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })); +} + +/** + * Handles applying hp changes to a player pokemon. + * Takes care of not going below `0`, above max-hp, adding `FNT` status correctly and updating the pokemon info. + * TODO: should we handle special cases like wonder-guard/shedinja? + * @param scene the battle scene + * @param pokemon the player pokemon to apply the hp change to + * @param value the hp change amount. Positive for heal. Negative for damage + * + */ +function applyHpChangeToPokemon(scene: BattleScene, pokemon: PlayerPokemon, value: number) { + const hpChange = Math.round(pokemon.hp + value); + const nextHp = Math.max(Math.min(hpChange, pokemon.getMaxHp()), 0); + if (nextHp === 0) { + koPlayerPokemon(scene, pokemon); + } else { + pokemon.hp = nextHp; + } +} + +/** + * Handles applying damage to a player pokemon + * @param scene the battle scene + * @param pokemon the player pokemon to apply damage to + * @param damage the amount of damage to apply + * @see {@linkcode applyHpChangeToPokemon} + */ +export function applyDamageToPokemon(scene: BattleScene, pokemon: PlayerPokemon, damage: number) { + if (damage <= 0) { + console.warn("Healing pokemon with `applyDamageToPokemon` is not recommended! Please use `applyHealToPokemon` instead."); + } + + applyHpChangeToPokemon(scene, pokemon, -damage); +} + +/** + * Handles applying heal to a player pokemon + * @param scene the battle scene + * @param pokemon the player pokemon to apply heal to + * @param heal the amount of heal to apply + * @see {@linkcode applyHpChangeToPokemon} + */ +export function applyHealToPokemon(scene: BattleScene, pokemon: PlayerPokemon, heal: number) { + if (heal <= 0) { + console.warn("Damaging pokemon with `applyHealToPokemon` is not recommended! Please use `applyDamageToPokemon` instead."); + } + + applyHpChangeToPokemon(scene, pokemon, heal); +} + +/** + * Will modify all of a Pokemon's base stats by a flat value + * Base stats can never go below 1 + * @param pokemon + * @param value + */ +export async function modifyPlayerPokemonBST(pokemon: PlayerPokemon, value: number) { + const modType = modifierTypes.MYSTERY_ENCOUNTER_SHUCKLE_JUICE().generateType(pokemon.scene.getParty(), [value]); + const modifier = modType?.newModifier(pokemon); + if (modifier) { + await pokemon.scene.addModifier(modifier, false, false, false, true); + pokemon.calculateStats(); + } +} + +/** + * Will attempt to add a new modifier to a Pokemon. + * If the Pokemon already has max stacks of that item, it will instead apply 'fallbackModifierType', if specified. + * @param scene + * @param pokemon + * @param modType + * @param fallbackModifierType + */ +export async function applyModifierTypeToPlayerPokemon(scene: BattleScene, pokemon: PlayerPokemon, modType: PokemonHeldItemModifierType, fallbackModifierType?: PokemonHeldItemModifierType) { + // Check if the Pokemon has max stacks of that item already + const modifier = modType.newModifier(pokemon); + const existing = scene.findModifier(m => ( + m instanceof PokemonHeldItemModifier && + m.type.id === modType.id && + m.pokemonId === pokemon.id && + m.matchType(modifier) + )) as PokemonHeldItemModifier; + + // At max stacks + if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) { + if (!fallbackModifierType) { + return; + } + + // Apply fallback + return applyModifierTypeToPlayerPokemon(scene, pokemon, fallbackModifierType); + } + + await scene.addModifier(modifier, false, false, false, true); +} + +/** + * Alternative to using AttemptCapturePhase + * Assumes player sprite is visible on the screen (this is intended for non-combat uses) + * + * Can await returned promise to wait for throw animation completion before continuing + * + * @param scene + * @param pokemon + * @param pokeballType + * @param ballTwitchRate - can pass custom ball catch rates (for special events, like safari) + */ +export function trainerThrowPokeball(scene: BattleScene, pokemon: EnemyPokemon, pokeballType: PokeballType, ballTwitchRate?: number): Promise { + const originalY: number = pokemon.y; + + if (!ballTwitchRate) { + const _3m = 3 * pokemon.getMaxHp(); + const _2h = 2 * pokemon.hp; + const catchRate = pokemon.species.catchRate; + const pokeballMultiplier = getPokeballCatchMultiplier(pokeballType); + const statusMultiplier = pokemon.status ? getStatusEffectCatchRateMultiplier(pokemon.status.effect) : 1; + const x = Math.round((((_3m - _2h) * catchRate * pokeballMultiplier) / _3m) * statusMultiplier); + ballTwitchRate = Math.round(65536 / Math.sqrt(Math.sqrt(255 / x))); + } + + const fpOffset = pokemon.getFieldPositionOffset(); + const pokeballAtlasKey = getPokeballAtlasKey(pokeballType); + const pokeball: Phaser.GameObjects.Sprite = scene.addFieldSprite(16 + 75, 80 + 25, "pb", pokeballAtlasKey); + pokeball.setOrigin(0.5, 0.625); + scene.field.add(pokeball); + + scene.time.delayedCall(300, () => { + scene.field.moveBelow(pokeball as Phaser.GameObjects.GameObject, pokemon); + }); + + return new Promise(resolve => { + scene.trainer.setTexture(`trainer_${scene.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}_back_pb`); + scene.time.delayedCall(512, () => { + scene.playSound("se/pb_throw"); + + // Trainer throw frames + scene.trainer.setFrame("2"); + scene.time.delayedCall(256, () => { + scene.trainer.setFrame("3"); + scene.time.delayedCall(768, () => { + scene.trainer.setTexture(`trainer_${scene.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}_back`); + }); + }); + + // Pokeball move and catch logic + scene.tweens.add({ + targets: pokeball, + x: { value: 236 + fpOffset[0], ease: "Linear" }, + y: { value: 16 + fpOffset[1], ease: "Cubic.easeOut" }, + duration: 500, + onComplete: () => { + pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`); + scene.time.delayedCall(17, () => pokeball.setTexture("pb", `${pokeballAtlasKey}_open`)); + scene.playSound("se/pb_rel"); + pokemon.tint(getPokeballTintColor(pokeballType)); + + addPokeballOpenParticles(scene, pokeball.x, pokeball.y, pokeballType); + + scene.tweens.add({ + targets: pokemon, + duration: 500, + ease: "Sine.easeIn", + scale: 0.25, + y: 20, + onComplete: () => { + pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`); + pokemon.setVisible(false); + scene.playSound("se/pb_catch"); + scene.time.delayedCall(17, () => pokeball.setTexture("pb", `${pokeballAtlasKey}`)); + + const doShake = () => { + let shakeCount = 0; + const pbX = pokeball.x; + const shakeCounter = scene.tweens.addCounter({ + from: 0, + to: 1, + repeat: 4, + yoyo: true, + ease: "Cubic.easeOut", + duration: 250, + repeatDelay: 500, + onUpdate: t => { + if (shakeCount && shakeCount < 4) { + const value = t.getValue(); + const directionMultiplier = shakeCount % 2 === 1 ? 1 : -1; + pokeball.setX(pbX + value * 4 * directionMultiplier); + pokeball.setAngle(value * 27.5 * directionMultiplier); + } + }, + onRepeat: () => { + if (!pokemon.species.isObtainable()) { + shakeCounter.stop(); + failCatch(scene, pokemon, originalY, pokeball, pokeballType).then(() => resolve(false)); + } else if (shakeCount++ < 3) { + if (randSeedInt(65536) < ballTwitchRate) { + scene.playSound("se/pb_move"); + } else { + shakeCounter.stop(); + failCatch(scene, pokemon, originalY, pokeball, pokeballType).then(() => resolve(false)); + } + } else { + scene.playSound("se/pb_lock"); + addPokeballCaptureStars(scene, pokeball); + + const pbTint = scene.add.sprite(pokeball.x, pokeball.y, "pb", "pb"); + pbTint.setOrigin(pokeball.originX, pokeball.originY); + pbTint.setTintFill(0); + pbTint.setAlpha(0); + scene.field.add(pbTint); + scene.tweens.add({ + targets: pbTint, + alpha: 0.375, + duration: 200, + easing: "Sine.easeOut", + onComplete: () => { + scene.tweens.add({ + targets: pbTint, + alpha: 0, + duration: 200, + easing: "Sine.easeIn", + onComplete: () => pbTint.destroy() + }); + } + }); + } + }, + onComplete: () => { + catchPokemon(scene, pokemon, pokeball, pokeballType).then(() => resolve(true)); + } + }); + }; + + scene.time.delayedCall(250, () => doPokeballBounceAnim(scene, pokeball, 16, 72, 350, doShake)); + } + }); + } + }); + }); + }); +} + +/** + * Animates pokeball opening and messages when an attempted catch fails + * @param scene + * @param pokemon + * @param originalY + * @param pokeball + * @param pokeballType + */ +function failCatch(scene: BattleScene, pokemon: EnemyPokemon, originalY: number, pokeball: Phaser.GameObjects.Sprite, pokeballType: PokeballType) { + return new Promise(resolve => { + scene.playSound("se/pb_rel"); + pokemon.setY(originalY); + if (pokemon.status?.effect !== StatusEffect.SLEEP) { + pokemon.cry(pokemon.getHpRatio() > 0.25 ? undefined : { rate: 0.85 }); + } + pokemon.tint(getPokeballTintColor(pokeballType)); + pokemon.setVisible(true); + pokemon.untint(250, "Sine.easeOut"); + + const pokeballAtlasKey = getPokeballAtlasKey(pokeballType); + pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`); + scene.time.delayedCall(17, () => pokeball.setTexture("pb", `${pokeballAtlasKey}_open`)); + + scene.tweens.add({ + targets: pokemon, + duration: 250, + ease: "Sine.easeOut", + scale: 1 + }); + + scene.currentBattle.lastUsedPokeball = pokeballType; + removePb(scene, pokeball); + + scene.ui.showText(i18next.t("battle:pokemonBrokeFree", { pokemonName: pokemon.getNameToRender() }), null, () => resolve(), null, true); + }); +} + +/** + * + * @param scene + * @param pokemon + * @param pokeball + * @param pokeballType + * @param showCatchObtainMessage + * @param isObtain + */ +export async function catchPokemon(scene: BattleScene, pokemon: EnemyPokemon, pokeball: Phaser.GameObjects.Sprite | null, pokeballType: PokeballType, showCatchObtainMessage: boolean = true, isObtain: boolean = false): Promise { + const speciesForm = !pokemon.fusionSpecies ? pokemon.getSpeciesForm() : pokemon.getFusionSpeciesForm(); + + if (speciesForm.abilityHidden && (pokemon.fusionSpecies ? pokemon.fusionAbilityIndex : pokemon.abilityIndex) === speciesForm.getAbilityCount() - 1) { + scene.validateAchv(achvs.HIDDEN_ABILITY); + } + + if (pokemon.species.subLegendary) { + scene.validateAchv(achvs.CATCH_SUB_LEGENDARY); + } + + if (pokemon.species.legendary) { + scene.validateAchv(achvs.CATCH_LEGENDARY); + } + + if (pokemon.species.mythical) { + scene.validateAchv(achvs.CATCH_MYTHICAL); + } + + scene.pokemonInfoContainer.show(pokemon, true); + + scene.gameData.updateSpeciesDexIvs(pokemon.species.getRootSpeciesId(true), pokemon.ivs); + + return new Promise(resolve => { + const doPokemonCatchMenu = () => { + const end = () => { + // Ensure the pokemon is in the enemy party in all situations + if (!scene.getEnemyParty().some(p => p.id === pokemon.id)) { + scene.getEnemyParty().push(pokemon); + } + scene.unshiftPhase(new VictoryPhase(scene, pokemon.id, true)); + scene.pokemonInfoContainer.hide(); + if (pokeball) { + removePb(scene, pokeball); + } + resolve(); + }; + const removePokemon = () => { + if (pokemon) { + scene.field.remove(pokemon, true); + } + }; + const addToParty = (slotIndex?: number) => { + const newPokemon = pokemon.addToParty(pokeballType, slotIndex); + const modifiers = scene.findModifiers(m => m instanceof PokemonHeldItemModifier, false); + if (scene.getParty().filter(p => p.isShiny()).length === 6) { + scene.validateAchv(achvs.SHINY_PARTY); + } + Promise.all(modifiers.map(m => scene.addModifier(m, true))).then(() => { + scene.updateModifiers(true); + removePokemon(); + if (newPokemon) { + newPokemon.loadAssets().then(end); + } else { + end(); + } + }); + }; + Promise.all([pokemon.hideInfo(), scene.gameData.setPokemonCaught(pokemon)]).then(() => { + if (scene.getParty().length === 6) { + const promptRelease = () => { + scene.ui.showText(i18next.t("battle:partyFull", { pokemonName: pokemon.getNameToRender() }), null, () => { + scene.pokemonInfoContainer.makeRoomForConfirmUi(1, true); + scene.ui.setMode(Mode.CONFIRM, () => { + const newPokemon = scene.addPlayerPokemon(pokemon.species, pokemon.level, pokemon.abilityIndex, pokemon.formIndex, pokemon.gender, pokemon.shiny, pokemon.variant, pokemon.ivs, pokemon.nature, pokemon); + scene.ui.setMode(Mode.SUMMARY, newPokemon, 0, SummaryUiMode.DEFAULT, () => { + scene.ui.setMode(Mode.MESSAGE).then(() => { + promptRelease(); + }); + }, false); + }, () => { + scene.ui.setMode(Mode.PARTY, PartyUiMode.RELEASE, 0, (slotIndex: integer, _option: PartyOption) => { + scene.ui.setMode(Mode.MESSAGE).then(() => { + if (slotIndex < 6) { + addToParty(slotIndex); + } else { + promptRelease(); + } + }); + }); + }, () => { + scene.ui.setMode(Mode.MESSAGE).then(() => { + removePokemon(); + end(); + }); + }, "fullParty"); + }); + }; + promptRelease(); + } else { + addToParty(); + } + }); + }; + + if (showCatchObtainMessage) { + scene.ui.showText(i18next.t(isObtain ? "battle:pokemonObtained" : "battle:pokemonCaught", { pokemonName: pokemon.getNameToRender() }), null, doPokemonCatchMenu, 0, true); + } else { + doPokemonCatchMenu(); + } + }); +} + +/** + * Animates pokeball disappearing then destroys the object + * @param scene + * @param pokeball + */ +function removePb(scene: BattleScene, pokeball: Phaser.GameObjects.Sprite) { + if (pokeball) { + scene.tweens.add({ + targets: pokeball, + duration: 250, + delay: 250, + ease: "Sine.easeIn", + alpha: 0, + onComplete: () => { + pokeball.destroy(); + } + }); + } +} + +/** + * Animates a wild pokemon "fleeing", including sfx and messaging + * @param scene + * @param pokemon + */ +export async function doPokemonFlee(scene: BattleScene, pokemon: EnemyPokemon): Promise { + await new Promise(resolve => { + scene.playSound("se/flee"); + // Ease pokemon out + scene.tweens.add({ + targets: pokemon, + x: "+=16", + y: "-=16", + alpha: 0, + duration: 1000, + ease: "Sine.easeIn", + scale: pokemon.getSpriteScale(), + onComplete: () => { + pokemon.setVisible(false); + scene.field.remove(pokemon, true); + showEncounterText(scene, i18next.t("battle:pokemonFled", { pokemonName: pokemon.getNameToRender() }), null, 600, false) + .then(() => { + resolve(); + }); + } + }); + }); +} + +/** + * Handles the player fleeing from a wild pokemon, including sfx and messaging + * @param scene + * @param pokemon + */ +export function doPlayerFlee(scene: BattleScene, pokemon: EnemyPokemon): Promise { + return new Promise(resolve => { + // Ease pokemon out + scene.tweens.add({ + targets: pokemon, + x: "+=16", + y: "-=16", + alpha: 0, + duration: 1000, + ease: "Sine.easeIn", + scale: pokemon.getSpriteScale(), + onComplete: () => { + pokemon.setVisible(false); + scene.field.remove(pokemon, true); + showEncounterText(scene, i18next.t("battle:playerFled", { pokemonName: pokemon.getNameToRender() }), null, 600, false) + .then(() => { + resolve(); + }); + } + }); + }); +} + +/** + * Bug Species and their corresponding weights + */ +const GOLDEN_BUG_NET_SPECIES_POOL: [Species, number][] = [ + [Species.SCYTHER, 40], + [Species.SCIZOR, 40], + [Species.KLEAVOR, 40], + [Species.PINSIR, 40], + [Species.HERACROSS, 40], + [Species.YANMA, 40], + [Species.YANMEGA, 40], + [Species.SHUCKLE, 40], + [Species.ANORITH, 40], + [Species.ARMALDO, 40], + [Species.ESCAVALIER, 40], + [Species.ACCELGOR, 40], + [Species.JOLTIK, 40], + [Species.GALVANTULA, 40], + [Species.DURANT, 40], + [Species.LARVESTA, 40], + [Species.VOLCARONA, 40], + [Species.DEWPIDER, 40], + [Species.ARAQUANID, 40], + [Species.WIMPOD, 40], + [Species.GOLISOPOD, 40], + [Species.SIZZLIPEDE, 40], + [Species.CENTISKORCH, 40], + [Species.NYMBLE, 40], + [Species.LOKIX, 40], + [Species.BUZZWOLE, 1], + [Species.PHEROMOSA, 1], +]; + +/** + * Will randomly return one of the species from GOLDEN_BUG_NET_SPECIES_POOL, based on their weights + */ +export function getGoldenBugNetSpecies(): PokemonSpecies { + const totalWeight = GOLDEN_BUG_NET_SPECIES_POOL.reduce((a, b) => a + b[1], 0); + const roll = randSeedInt(totalWeight); + + let w = 0; + for (const speciesWeightPair of GOLDEN_BUG_NET_SPECIES_POOL) { + w += speciesWeightPair[1]; + if (roll < w) { + return getPokemonSpecies(speciesWeightPair[0]); + } + } + + // Defaults to Scyther + return getPokemonSpecies(Species.SCYTHER); +} + +/** + * Generates a Pokemon level for a given wave, with an option to increase/decrease by a scaling modifier + * @param scene + * @param levelAdditiveModifier Default 0. will add +(1 level / 10 waves * levelAdditiveModifier) to the level calculation + */ +export function getEncounterPokemonLevelForWave(scene: BattleScene, levelAdditiveModifier: number = 0) { + const currentBattle = scene.currentBattle; + // Default to use the first generated level from enemyLevels, or generate a new one if it DNE + const baseLevel = currentBattle.enemyLevels && currentBattle.enemyLevels?.length > 0 ? currentBattle.enemyLevels[0] : currentBattle.getLevelForWave(); + + // Add a level scaling modifier that is (+1 level per 10 waves) * levelAdditiveModifier + return baseLevel + Math.max(Math.round((currentBattle.waveIndex / 10) * levelAdditiveModifier), 0); +} + +export async function addPokemonDataToDexAndValidateAchievements(scene: BattleScene, pokemon: PlayerPokemon) { + const speciesForm = !pokemon.fusionSpecies ? pokemon.getSpeciesForm() : pokemon.getFusionSpeciesForm(); + + if (speciesForm.abilityHidden && (pokemon.fusionSpecies ? pokemon.fusionAbilityIndex : pokemon.abilityIndex) === speciesForm.getAbilityCount() - 1) { + scene.validateAchv(achvs.HIDDEN_ABILITY); + } + + if (pokemon.species.subLegendary) { + scene.validateAchv(achvs.CATCH_SUB_LEGENDARY); + } + + if (pokemon.species.legendary) { + scene.validateAchv(achvs.CATCH_LEGENDARY); + } + + if (pokemon.species.mythical) { + scene.validateAchv(achvs.CATCH_MYTHICAL); + } + + scene.gameData.updateSpeciesDexIvs(pokemon.species.getRootSpeciesId(true), pokemon.ivs); + return scene.gameData.setPokemonCaught(pokemon, true, false, false); +} + +/** + * Checks if a Pokemon is allowed under a challenge, and allowed in battle. + * If both are true, returns `null`. + * If one of them is not true, returns message content that the Pokemon is invalid. + * Typically used for cheecking whether a Pokemon can be selected for a {@linkcode MysteryEncounterOption} + * @param pokemon + * @param scene + * @param invalidSelectionKey + */ +export function isPokemonValidForEncounterOptionSelection(pokemon: Pokemon, scene: BattleScene, invalidSelectionKey: string): string | null { + if (!pokemon.isAllowed()) { + return i18next.t("partyUiHandler:cantBeUsed", { pokemonName: pokemon.getNameToRender() }) ?? null; + } + if (!pokemon.isAllowedInBattle()) { + return getEncounterText(scene, invalidSelectionKey) ?? null; + } + + return null; +} diff --git a/src/data/mystery-encounters/utils/encounter-transformation-sequence.ts b/src/data/mystery-encounters/utils/encounter-transformation-sequence.ts new file mode 100644 index 00000000000..fcadb101817 --- /dev/null +++ b/src/data/mystery-encounters/utils/encounter-transformation-sequence.ts @@ -0,0 +1,392 @@ +import BattleScene from "#app/battle-scene"; +import { PlayerPokemon } from "#app/field/pokemon"; +import { getFrameMs } from "#app/utils"; +import { cos, sin } from "#app/field/anims"; +import { getTypeRgb } from "#app/data/type"; + +export enum TransformationScreenPosition { + CENTER, + LEFT, + RIGHT +} + +/** + * Initiates an "evolution-like" animation to transform a previousPokemon (presumably from the player's party) into a new one, not necessarily an evolution species. + * @param scene + * @param previousPokemon + * @param transformPokemon + * @param screenPosition + */ +export function doPokemonTransformationSequence(scene: BattleScene, previousPokemon: PlayerPokemon, transformPokemon: PlayerPokemon, screenPosition: TransformationScreenPosition) { + return new Promise(resolve => { + const transformationContainer = scene.fieldUI.getByName("Dream Background") as Phaser.GameObjects.Container; + const transformationBaseBg = scene.add.image(0, 0, "default_bg"); + transformationBaseBg.setOrigin(0, 0); + transformationBaseBg.setVisible(false); + transformationContainer.add(transformationBaseBg); + + let pokemonSprite: Phaser.GameObjects.Sprite; + let pokemonTintSprite: Phaser.GameObjects.Sprite; + let pokemonEvoSprite: Phaser.GameObjects.Sprite; + let pokemonEvoTintSprite: Phaser.GameObjects.Sprite; + + const xOffset = screenPosition === TransformationScreenPosition.CENTER ? 0 : + screenPosition === TransformationScreenPosition.RIGHT ? 100 : -100; + // Centered transformations occur at a lower y Position + const yOffset = screenPosition !== TransformationScreenPosition.CENTER ? -15 : 0; + + const getPokemonSprite = () => { + const ret = scene.addPokemonSprite(previousPokemon, transformationBaseBg.displayWidth / 2 + xOffset, transformationBaseBg.displayHeight / 2 + yOffset, "pkmn__sub"); + ret.setPipeline(scene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], ignoreTimeTint: true }); + return ret; + }; + + transformationContainer.add((pokemonSprite = getPokemonSprite())); + transformationContainer.add((pokemonTintSprite = getPokemonSprite())); + transformationContainer.add((pokemonEvoSprite = getPokemonSprite())); + transformationContainer.add((pokemonEvoTintSprite = getPokemonSprite())); + + pokemonSprite.setAlpha(0); + pokemonTintSprite.setAlpha(0); + pokemonTintSprite.setTintFill(0xFFFFFF); + pokemonEvoSprite.setVisible(false); + pokemonEvoTintSprite.setVisible(false); + pokemonEvoTintSprite.setTintFill(0xFFFFFF); + + [ pokemonSprite, pokemonTintSprite, pokemonEvoSprite, pokemonEvoTintSprite ].map(sprite => { + sprite.play(previousPokemon.getSpriteKey(true)); + sprite.setPipeline(scene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], hasShadow: false, teraColor: getTypeRgb(previousPokemon.getTeraType()) }); + sprite.setPipelineData("ignoreTimeTint", true); + sprite.setPipelineData("spriteKey", previousPokemon.getSpriteKey()); + sprite.setPipelineData("shiny", previousPokemon.shiny); + sprite.setPipelineData("variant", previousPokemon.variant); + [ "spriteColors", "fusionSpriteColors" ].map(k => { + if (previousPokemon.summonData?.speciesForm) { + k += "Base"; + } + sprite.pipelineData[k] = previousPokemon.getSprite().pipelineData[k]; + }); + }); + + [ pokemonEvoSprite, pokemonEvoTintSprite ].map(sprite => { + sprite.play(transformPokemon.getSpriteKey(true)); + sprite.setPipelineData("ignoreTimeTint", true); + sprite.setPipelineData("spriteKey", transformPokemon.getSpriteKey()); + sprite.setPipelineData("shiny", transformPokemon.shiny); + sprite.setPipelineData("variant", transformPokemon.variant); + [ "spriteColors", "fusionSpriteColors" ].map(k => { + if (transformPokemon.summonData?.speciesForm) { + k += "Base"; + } + sprite.pipelineData[k] = transformPokemon.getSprite().pipelineData[k]; + }); + }); + + scene.tweens.add({ + targets: pokemonSprite, + alpha: 1, + ease: "Cubic.easeInOut", + duration: 2000, + onComplete: () => { + doSpiralUpward(scene, transformationBaseBg, transformationContainer, xOffset, yOffset); + scene.tweens.addCounter({ + from: 0, + to: 1, + duration: 1000, + onUpdate: t => { + pokemonTintSprite.setAlpha(t.getValue()); + }, + onComplete: () => { + pokemonSprite.setVisible(false); + scene.time.delayedCall(700, () => { + doArcDownward(scene, transformationBaseBg, transformationContainer, xOffset, yOffset); + scene.time.delayedCall(1000, () => { + pokemonEvoTintSprite.setScale(0.25); + pokemonEvoTintSprite.setVisible(true); + doCycle(scene, 1.5, 6, pokemonTintSprite, pokemonEvoTintSprite).then(() => { + pokemonEvoSprite.setVisible(true); + doCircleInward(scene, transformationBaseBg, transformationContainer, xOffset, yOffset); + + scene.time.delayedCall(900, () => { + scene.tweens.add({ + targets: pokemonEvoTintSprite, + alpha: 0, + duration: 1500, + delay: 150, + easing: "Sine.easeIn", + onComplete: () => { + scene.time.delayedCall(3000, () => { + resolve(); + scene.tweens.add({ + targets: pokemonEvoSprite, + alpha: 0, + duration: 2000, + delay: 150, + easing: "Sine.easeIn", + onComplete: () => { + previousPokemon.destroy(); + transformPokemon.setVisible(false); + transformPokemon.setAlpha(1); + } + }); + }); + } + }); + }); + }); + }); + }); + } + }); + } + }); + }); +} + +/** + * Animates particles that "spiral" upwards at start of transform animation + * @param scene + * @param transformationBaseBg + * @param transformationContainer + * @param xOffset + * @param yOffset + */ +function doSpiralUpward(scene: BattleScene, transformationBaseBg: Phaser.GameObjects.Image, transformationContainer: Phaser.GameObjects.Container, xOffset: number, yOffset: number) { + let f = 0; + + scene.tweens.addCounter({ + repeat: 64, + duration: getFrameMs(1), + onRepeat: () => { + if (f < 64) { + if (!(f & 7)) { + for (let i = 0; i < 4; i++) { + doSpiralUpwardParticle(scene, (f & 120) * 2 + i * 64, transformationBaseBg, transformationContainer, xOffset, yOffset); + } + } + f++; + } + } + }); +} + +/** + * Animates particles that arc downwards after the upwards spiral + * @param scene + * @param transformationBaseBg + * @param transformationContainer + * @param xOffset + * @param yOffset + */ +function doArcDownward(scene: BattleScene, transformationBaseBg: Phaser.GameObjects.Image, transformationContainer: Phaser.GameObjects.Container, xOffset: number, yOffset: number) { + let f = 0; + + scene.tweens.addCounter({ + repeat: 96, + duration: getFrameMs(1), + onRepeat: () => { + if (f < 96) { + if (f < 6) { + for (let i = 0; i < 9; i++) { + doArcDownParticle(scene, i * 16, transformationBaseBg, transformationContainer, xOffset, yOffset); + } + } + f++; + } + } + }); +} + +/** + * Animates the transformation between the old pokemon form and new pokemon form + * @param scene + * @param l + * @param lastCycle + * @param pokemonTintSprite + * @param pokemonEvoTintSprite + */ +function doCycle(scene: BattleScene, l: number, lastCycle: number, pokemonTintSprite: Phaser.GameObjects.Sprite, pokemonEvoTintSprite: Phaser.GameObjects.Sprite): Promise { + return new Promise(resolve => { + const isLastCycle = l === lastCycle; + scene.tweens.add({ + targets: pokemonTintSprite, + scale: 0.25, + ease: "Cubic.easeInOut", + duration: 500 / l, + yoyo: !isLastCycle + }); + scene.tweens.add({ + targets: pokemonEvoTintSprite, + scale: 1, + ease: "Cubic.easeInOut", + duration: 500 / l, + yoyo: !isLastCycle, + onComplete: () => { + if (l < lastCycle) { + doCycle(scene, l + 0.5, lastCycle, pokemonTintSprite, pokemonEvoTintSprite).then(success => resolve(success)); + } else { + pokemonTintSprite.setVisible(false); + resolve(true); + } + } + }); + }); +} + +/** + * Animates particles in a circle pattern + * @param scene + * @param transformationBaseBg + * @param transformationContainer + * @param xOffset + * @param yOffset + */ +function doCircleInward(scene: BattleScene, transformationBaseBg: Phaser.GameObjects.Image, transformationContainer: Phaser.GameObjects.Container, xOffset: number, yOffset: number) { + let f = 0; + + scene.tweens.addCounter({ + repeat: 48, + duration: getFrameMs(1), + onRepeat: () => { + if (!f) { + for (let i = 0; i < 16; i++) { + doCircleInwardParticle(scene, i * 16, 4, transformationBaseBg, transformationContainer, xOffset, yOffset); + } + } else if (f === 32) { + for (let i = 0; i < 16; i++) { + doCircleInwardParticle(scene, i * 16, 8, transformationBaseBg, transformationContainer, xOffset, yOffset); + } + } + f++; + } + }); +} + +/** + * Helper function for {@linkcode doSpiralUpward}, handles a single particle + * @param scene + * @param trigIndex + * @param transformationBaseBg + * @param transformationContainer + * @param xOffset + * @param yOffset + */ +function doSpiralUpwardParticle(scene: BattleScene, trigIndex: number, transformationBaseBg: Phaser.GameObjects.Image, transformationContainer: Phaser.GameObjects.Container, xOffset: number, yOffset: number) { + const initialX = transformationBaseBg.displayWidth / 2 + xOffset; + const particle = scene.add.image(initialX, 0, "evo_sparkle"); + transformationContainer.add(particle); + + let f = 0; + let amp = 48; + + const particleTimer = scene.tweens.addCounter({ + repeat: -1, + duration: getFrameMs(1), + onRepeat: () => { + updateParticle(); + } + }); + + const updateParticle = () => { + if (!f || particle.y > 8) { + particle.setPosition(initialX, 88 - (f * f) / 80 + yOffset); + particle.y += sin(trigIndex, amp) / 4; + particle.x += cos(trigIndex, amp); + particle.setScale(1 - (f / 80)); + trigIndex += 4; + if (f & 1) { + amp--; + } + f++; + } else { + particle.destroy(); + particleTimer.remove(); + } + }; + + updateParticle(); +} + +/** + * Helper function for {@linkcode doArcDownward}, handles a single particle + * @param scene + * @param trigIndex + * @param transformationBaseBg + * @param transformationContainer + * @param xOffset + * @param yOffset + */ +function doArcDownParticle(scene: BattleScene, trigIndex: number, transformationBaseBg: Phaser.GameObjects.Image, transformationContainer: Phaser.GameObjects.Container, xOffset: number, yOffset: number) { + const initialX = transformationBaseBg.displayWidth / 2 + xOffset; + const particle = scene.add.image(initialX, 0, "evo_sparkle"); + particle.setScale(0.5); + transformationContainer.add(particle); + + let f = 0; + let amp = 8; + + const particleTimer = scene.tweens.addCounter({ + repeat: -1, + duration: getFrameMs(1), + onRepeat: () => { + updateParticle(); + } + }); + + const updateParticle = () => { + if (!f || particle.y < 88) { + particle.setPosition(initialX, 8 + (f * f) / 5 + yOffset); + particle.y += sin(trigIndex, amp) / 4; + particle.x += cos(trigIndex, amp); + amp = 8 + sin(f * 4, 40); + f++; + } else { + particle.destroy(); + particleTimer.remove(); + } + }; + + updateParticle(); +} + +/** + * Helper function for @{link doCircleInward}, handles a single particle + * @param scene + * @param trigIndex + * @param speed + * @param transformationBaseBg + * @param transformationContainer + * @param xOffset + * @param yOffset + */ +function doCircleInwardParticle(scene: BattleScene, trigIndex: number, speed: number, transformationBaseBg: Phaser.GameObjects.Image, transformationContainer: Phaser.GameObjects.Container, xOffset: number, yOffset: number) { + const initialX = transformationBaseBg.displayWidth / 2 + xOffset; + const initialY = transformationBaseBg.displayHeight / 2 + yOffset; + const particle = scene.add.image(initialX, initialY, "evo_sparkle"); + transformationContainer.add(particle); + + let amp = 120; + + const particleTimer = scene.tweens.addCounter({ + repeat: -1, + duration: getFrameMs(1), + onRepeat: () => { + updateParticle(); + } + }); + + const updateParticle = () => { + if (amp > 8) { + particle.setPosition(initialX, initialY); + particle.y += sin(trigIndex, amp); + particle.x += cos(trigIndex, amp); + amp -= speed; + trigIndex += 4; + } else { + particle.destroy(); + particleTimer.remove(); + } + }; + + updateParticle(); +} diff --git a/src/data/pokemon-evolutions.ts b/src/data/pokemon-evolutions.ts index 6479d620182..f9602d1386a 100644 --- a/src/data/pokemon-evolutions.ts +++ b/src/data/pokemon-evolutions.ts @@ -17,7 +17,8 @@ export enum SpeciesWildEvolutionDelay { SHORT, MEDIUM, LONG, - VERY_LONG + VERY_LONG, + NEVER } export enum EvolutionItem { @@ -39,19 +40,34 @@ export enum EvolutionItem { TART_APPLE, STRAWBERRY_SWEET, UNREMARKABLE_TEACUP, - - CHIPPED_POT = 51, - BLACK_AUGURITE, + UPGRADE, + DUBIOUS_DISC, + DRAGON_SCALE, + PRISM_SCALE, + RAZOR_CLAW, + RAZOR_FANG, + REAPER_CLOTH, + ELECTIRIZER, + MAGMARIZER, + PROTECTOR, + SACHET, + WHIPPED_DREAM, + SYRUPY_APPLE, + CHIPPED_POT, GALARICA_CUFF, GALARICA_WREATH, - PEAT_BLOCK, AUSPICIOUS_ARMOR, MALICIOUS_ARMOR, MASTERPIECE_TEACUP, + SUN_FLUTE, + MOON_FLUTE, + + BLACK_AUGURITE = 51, + PEAT_BLOCK, METAL_ALLOY, SCROLL_OF_DARKNESS, SCROLL_OF_WATERS, - SYRUPY_APPLE + LEADERS_CREST } /** @@ -222,7 +238,7 @@ export const pokemonEvolutions: PokemonEvolutions = { ], [Species.SLOWPOKE]: [ new SpeciesEvolution(Species.SLOWBRO, 37, null, null), - new SpeciesEvolution(Species.SLOWKING, 1, EvolutionItem.LINKING_CORD, new SpeciesEvolutionCondition(p => true /* King's Rock */), SpeciesWildEvolutionDelay.VERY_LONG) + new SpeciesEvolution(Species.SLOWKING, 1, EvolutionItem.LINKING_CORD, null, SpeciesWildEvolutionDelay.VERY_LONG) ], [Species.MAGNEMITE]: [ new SpeciesEvolution(Species.MAGNETON, 30, null, null) @@ -249,8 +265,8 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.ELECTRODE, 30, null, null) ], [Species.CUBONE]: [ - new SpeciesEvolution(Species.ALOLA_MAROWAK, 28, null, new SpeciesEvolutionCondition(p => p.scene.arena.biomeType === Biome.ISLAND || p.scene.arena.biomeType === Biome.BEACH), SpeciesWildEvolutionDelay.MEDIUM), - new SpeciesEvolution(Species.MAROWAK, 28, null, null) + new SpeciesEvolution(Species.ALOLA_MAROWAK, 28, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT)), + new SpeciesEvolution(Species.MAROWAK, 28, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAWN || p.scene.arena.getTimeOfDay() === TimeOfDay.DAY)) ], [Species.TYROGUE]: [ new SpeciesEvolution(Species.HITMONLEE, 20, null, new SpeciesEvolutionCondition(p => p.stats[Stat.ATK] > p.stats[Stat.DEF])), @@ -258,8 +274,8 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.HITMONTOP, 20, null, new SpeciesEvolutionCondition(p => p.stats[Stat.ATK] === p.stats[Stat.DEF])) ], [Species.KOFFING]: [ - new SpeciesEvolution(Species.GALAR_WEEZING, 35, null, new SpeciesEvolutionCondition(p => p.scene.arena.biomeType === Biome.METROPOLIS || p.scene.arena.biomeType === Biome.SLUM), SpeciesWildEvolutionDelay.MEDIUM), - new SpeciesEvolution(Species.WEEZING, 35, null, null) + new SpeciesEvolution(Species.GALAR_WEEZING, 35, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT)), + new SpeciesEvolution(Species.WEEZING, 35, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAWN || p.scene.arena.getTimeOfDay() === TimeOfDay.DAY)) ], [Species.RHYHORN]: [ new SpeciesEvolution(Species.RHYDON, 42, null, null) @@ -304,7 +320,7 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.QUILAVA, 14, null, null) ], [Species.QUILAVA]: [ - new SpeciesEvolution(Species.HISUI_TYPHLOSION, 36, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT), SpeciesWildEvolutionDelay.LONG), + new SpeciesEvolution(Species.HISUI_TYPHLOSION, 36, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT)), new SpeciesEvolution(Species.TYPHLOSION, 36, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAWN || p.scene.arena.getTimeOfDay() === TimeOfDay.DAY)) ], [Species.TOTODILE]: [ @@ -652,7 +668,7 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.DEWOTT, 17, null, null) ], [Species.DEWOTT]: [ - new SpeciesEvolution(Species.HISUI_SAMUROTT, 36, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT), SpeciesWildEvolutionDelay.LONG), + new SpeciesEvolution(Species.HISUI_SAMUROTT, 36, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT)), new SpeciesEvolution(Species.SAMUROTT, 36, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAWN || p.scene.arena.getTimeOfDay() === TimeOfDay.DAY)) ], [Species.PATRAT]: [ @@ -800,10 +816,10 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.BISHARP, 52, null, null) ], [Species.BISHARP]: [ - new SpeciesEvolution(Species.KINGAMBIT, 64, null, null) + new SpeciesEvolution(Species.KINGAMBIT, 1, EvolutionItem.LEADERS_CREST, null, SpeciesWildEvolutionDelay.VERY_LONG) ], [Species.RUFFLET]: [ - new SpeciesEvolution(Species.HISUI_BRAVIARY, 54, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT), SpeciesWildEvolutionDelay.LONG), + new SpeciesEvolution(Species.HISUI_BRAVIARY, 54, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT)), new SpeciesEvolution(Species.BRAVIARY, 54, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAWN || p.scene.arena.getTimeOfDay() === TimeOfDay.DAY)) ], [Species.VULLABY]: [ @@ -883,20 +899,20 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.CLAWITZER, 37, null, null) ], [Species.TYRUNT]: [ - new SpeciesEvolution(Species.TYRANTRUM, 39, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAY)) + new SpeciesEvolution(Species.TYRANTRUM, 39, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAWN || p.scene.arena.getTimeOfDay() === TimeOfDay.DAY)) ], [Species.AMAURA]: [ - new SpeciesEvolution(Species.AURORUS, 39, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT)) + new SpeciesEvolution(Species.AURORUS, 39, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT)) ], [Species.GOOMY]: [ - new SpeciesEvolution(Species.HISUI_SLIGGOO, 40, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT), SpeciesWildEvolutionDelay.LONG), + new SpeciesEvolution(Species.HISUI_SLIGGOO, 40, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT)), new SpeciesEvolution(Species.SLIGGOO, 40, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAWN || p.scene.arena.getTimeOfDay() === TimeOfDay.DAY)) ], [Species.SLIGGOO]: [ new SpeciesEvolution(Species.GOODRA, 50, null, new SpeciesEvolutionCondition(p => [ WeatherType.RAIN, WeatherType.FOG, WeatherType.HEAVY_RAIN ].indexOf(p.scene.arena.weather?.weatherType || WeatherType.NONE) > -1), SpeciesWildEvolutionDelay.LONG) ], [Species.BERGMITE]: [ - new SpeciesEvolution(Species.HISUI_AVALUGG, 37, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT), SpeciesWildEvolutionDelay.LONG), + new SpeciesEvolution(Species.HISUI_AVALUGG, 37, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT)), new SpeciesEvolution(Species.AVALUGG, 37, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAWN || p.scene.arena.getTimeOfDay() === TimeOfDay.DAY)) ], [Species.NOIBAT]: [ @@ -906,7 +922,7 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.DARTRIX, 17, null, null) ], [Species.DARTRIX]: [ - new SpeciesEvolution(Species.HISUI_DECIDUEYE, 36, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT), SpeciesWildEvolutionDelay.LONG), + new SpeciesEvolution(Species.HISUI_DECIDUEYE, 36, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT)), new SpeciesEvolution(Species.DECIDUEYE, 34, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAWN || p.scene.arena.getTimeOfDay() === TimeOfDay.DAY)) ], [Species.LITTEN]: [ @@ -928,7 +944,7 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.TOUCANNON, 28, null, null) ], [Species.YUNGOOS]: [ - new SpeciesEvolution(Species.GUMSHOOS, 20, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAY)) + new SpeciesEvolution(Species.GUMSHOOS, 20, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAWN || p.scene.arena.getTimeOfDay() === TimeOfDay.DAY)) ], [Species.GRUBBIN]: [ new SpeciesEvolution(Species.CHARJABUG, 20, null, null) @@ -946,7 +962,7 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.ARAQUANID, 22, null, null) ], [Species.FOMANTIS]: [ - new SpeciesEvolution(Species.LURANTIS, 34, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAY)) + new SpeciesEvolution(Species.LURANTIS, 34, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAWN || p.scene.arena.getTimeOfDay() === TimeOfDay.DAY)) ], [Species.MORELULL]: [ new SpeciesEvolution(Species.SHIINOTIC, 24, null, null) @@ -973,17 +989,17 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.KOMMO_O, 45, null, null) ], [Species.COSMOG]: [ - new SpeciesEvolution(Species.COSMOEM, 43, null, null) + new SpeciesEvolution(Species.COSMOEM, 23, null, null) ], [Species.COSMOEM]: [ - new SpeciesEvolution(Species.SOLGALEO, 53, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAY)), - new SpeciesEvolution(Species.LUNALA, 53, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT)) + new SpeciesEvolution(Species.SOLGALEO, 53, EvolutionItem.SUN_FLUTE, null, SpeciesWildEvolutionDelay.VERY_LONG), + new SpeciesEvolution(Species.LUNALA, 53, EvolutionItem.MOON_FLUTE, null, SpeciesWildEvolutionDelay.VERY_LONG) ], [Species.MELTAN]: [ new SpeciesEvolution(Species.MELMETAL, 48, null, null) ], [Species.ALOLA_RATTATA]: [ - new SpeciesEvolution(Species.ALOLA_RATICATE, 20, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT)) + new SpeciesEvolution(Species.ALOLA_RATICATE, 20, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT)) ], [Species.ALOLA_DIGLETT]: [ new SpeciesEvolution(Species.ALOLA_DUGTRIO, 26, null, null) @@ -1090,7 +1106,7 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.GALAR_RAPIDASH, 40, null, null) ], [Species.GALAR_FARFETCHD]: [ - new SpeciesEvolution(Species.SIRFETCHD, 30, null, null) + new SpeciesEvolution(Species.SIRFETCHD, 30, null, null, SpeciesWildEvolutionDelay.LONG) ], [Species.GALAR_SLOWPOKE]: [ new SpeciesEvolution(Species.GALAR_SLOWBRO, 1, EvolutionItem.GALARICA_CUFF, null, SpeciesWildEvolutionDelay.VERY_LONG), @@ -1106,7 +1122,7 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.GALAR_LINOONE, 20, null, null) ], [Species.GALAR_LINOONE]: [ - new SpeciesEvolution(Species.OBSTAGOON, 35, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT)) + new SpeciesEvolution(Species.OBSTAGOON, 35, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT)) ], [Species.GALAR_YAMASK]: [ new SpeciesEvolution(Species.RUNERIGUS, 34, null, null) @@ -1214,7 +1230,7 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.GLIMMORA, 35, null, null) ], [Species.GREAVARD]: [ - new SpeciesEvolution(Species.HOUNDSTONE, 30, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT)) + new SpeciesEvolution(Species.HOUNDSTONE, 30, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT)) ], [Species.FRIGIBAX]: [ new SpeciesEvolution(Species.ARCTIBAX, 35, null, null) @@ -1226,8 +1242,8 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.CLODSIRE, 20, null, null) ], [Species.PIKACHU]: [ - new SpeciesFormEvolution(Species.ALOLA_RAICHU, "", "", 1, EvolutionItem.THUNDER_STONE, new SpeciesEvolutionCondition(p => p.scene.arena.biomeType === Biome.ISLAND || p.scene.arena.biomeType === Biome.BEACH), SpeciesWildEvolutionDelay.LONG), - new SpeciesFormEvolution(Species.ALOLA_RAICHU, "partner", "", 1, EvolutionItem.THUNDER_STONE, new SpeciesEvolutionCondition(p => p.scene.arena.biomeType === Biome.ISLAND || p.scene.arena.biomeType === Biome.BEACH), SpeciesWildEvolutionDelay.LONG), + new SpeciesFormEvolution(Species.ALOLA_RAICHU, "", "", 1, EvolutionItem.SHINY_STONE, null, SpeciesWildEvolutionDelay.LONG), + new SpeciesFormEvolution(Species.ALOLA_RAICHU, "partner", "", 1, EvolutionItem.SHINY_STONE, null, SpeciesWildEvolutionDelay.LONG), new SpeciesFormEvolution(Species.RAICHU, "", "", 1, EvolutionItem.THUNDER_STONE, null, SpeciesWildEvolutionDelay.LONG), new SpeciesFormEvolution(Species.RAICHU, "partner", "", 1, EvolutionItem.THUNDER_STONE, null, SpeciesWildEvolutionDelay.LONG) ], @@ -1255,7 +1271,7 @@ export const pokemonEvolutions: PokemonEvolutions = { ], [Species.POLIWHIRL]: [ new SpeciesEvolution(Species.POLIWRATH, 1, EvolutionItem.WATER_STONE, null, SpeciesWildEvolutionDelay.LONG), - new SpeciesEvolution(Species.POLITOED, 1, EvolutionItem.LINKING_CORD, new SpeciesEvolutionCondition(p => true /* King's Rock */), SpeciesWildEvolutionDelay.VERY_LONG) + new SpeciesEvolution(Species.POLITOED, 1, EvolutionItem.LINKING_CORD, null, SpeciesWildEvolutionDelay.VERY_LONG) ], [Species.WEEPINBELL]: [ new SpeciesEvolution(Species.VICTREEBEL, 1, EvolutionItem.LEAF_STONE, null, SpeciesWildEvolutionDelay.LONG) @@ -1267,7 +1283,7 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.CLOYSTER, 1, EvolutionItem.WATER_STONE, null, SpeciesWildEvolutionDelay.LONG) ], [Species.EXEGGCUTE]: [ - new SpeciesEvolution(Species.ALOLA_EXEGGUTOR, 1, EvolutionItem.LEAF_STONE, new SpeciesEvolutionCondition(p => p.scene.arena.biomeType === Biome.ISLAND || p.scene.arena.biomeType === Biome.BEACH), SpeciesWildEvolutionDelay.LONG), + new SpeciesEvolution(Species.ALOLA_EXEGGUTOR, 1, EvolutionItem.SUN_STONE, null, SpeciesWildEvolutionDelay.LONG), new SpeciesEvolution(Species.EXEGGUTOR, 1, EvolutionItem.LEAF_STONE, null, SpeciesWildEvolutionDelay.LONG) ], [Species.TANGELA]: [ @@ -1280,12 +1296,12 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.STARMIE, 1, EvolutionItem.WATER_STONE, null, SpeciesWildEvolutionDelay.LONG) ], [Species.EEVEE]: [ - new SpeciesFormEvolution(Species.SYLVEON, "", "", 1, null, new SpeciesFriendshipEvolutionCondition(70, p => !!p.getMoveset().find(m => m?.getMove().type === Type.FAIRY)), SpeciesWildEvolutionDelay.LONG), - new SpeciesFormEvolution(Species.SYLVEON, "partner", "", 1, null, new SpeciesFriendshipEvolutionCondition(70, p => !!p.getMoveset().find(m => m?.getMove().type === Type.FAIRY)), SpeciesWildEvolutionDelay.LONG), - new SpeciesFormEvolution(Species.ESPEON, "", "", 1, null, new SpeciesFriendshipEvolutionCondition(70, p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAY), SpeciesWildEvolutionDelay.LONG), - new SpeciesFormEvolution(Species.ESPEON, "partner", "", 1, null, new SpeciesFriendshipEvolutionCondition(70, p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAY), SpeciesWildEvolutionDelay.LONG), - new SpeciesFormEvolution(Species.UMBREON, "", "", 1, null, new SpeciesFriendshipEvolutionCondition(70, p => p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT), SpeciesWildEvolutionDelay.LONG), - new SpeciesFormEvolution(Species.UMBREON, "partner", "", 1, null, new SpeciesFriendshipEvolutionCondition(70, p => p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT), SpeciesWildEvolutionDelay.LONG), + new SpeciesFormEvolution(Species.SYLVEON, "", "", 1, null, new SpeciesFriendshipEvolutionCondition(120, p => !!p.getMoveset().find(m => m?.getMove().type === Type.FAIRY)), SpeciesWildEvolutionDelay.LONG), + new SpeciesFormEvolution(Species.SYLVEON, "partner", "", 1, null, new SpeciesFriendshipEvolutionCondition(120, p => !!p.getMoveset().find(m => m?.getMove().type === Type.FAIRY)), SpeciesWildEvolutionDelay.LONG), + new SpeciesFormEvolution(Species.ESPEON, "", "", 1, null, new SpeciesFriendshipEvolutionCondition(120, p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAY), SpeciesWildEvolutionDelay.LONG), + new SpeciesFormEvolution(Species.ESPEON, "partner", "", 1, null, new SpeciesFriendshipEvolutionCondition(120, p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAY), SpeciesWildEvolutionDelay.LONG), + new SpeciesFormEvolution(Species.UMBREON, "", "", 1, null, new SpeciesFriendshipEvolutionCondition(120, p => p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT), SpeciesWildEvolutionDelay.LONG), + new SpeciesFormEvolution(Species.UMBREON, "partner", "", 1, null, new SpeciesFriendshipEvolutionCondition(120, p => p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT), SpeciesWildEvolutionDelay.LONG), new SpeciesFormEvolution(Species.VAPOREON, "", "", 1, EvolutionItem.WATER_STONE, null, SpeciesWildEvolutionDelay.LONG), new SpeciesFormEvolution(Species.VAPOREON, "partner", "", 1, EvolutionItem.WATER_STONE, null, SpeciesWildEvolutionDelay.LONG), new SpeciesFormEvolution(Species.JOLTEON, "", "", 1, EvolutionItem.THUNDER_STONE, null, SpeciesWildEvolutionDelay.LONG), @@ -1329,10 +1345,10 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.DUDUNSPARCE, 32, null, new SpeciesEvolutionCondition(p => p.moveset.filter(m => m?.moveId === Moves.HYPER_DRILL).length > 0), SpeciesWildEvolutionDelay.LONG) ], [Species.GLIGAR]: [ - new SpeciesEvolution(Species.GLISCOR, 1, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT /* Razor fang at night*/), SpeciesWildEvolutionDelay.LONG) + new SpeciesEvolution(Species.GLISCOR, 1, EvolutionItem.RAZOR_FANG, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT /* Razor fang at night*/), SpeciesWildEvolutionDelay.VERY_LONG) ], [Species.SNEASEL]: [ - new SpeciesEvolution(Species.WEAVILE, 1, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT /* Razor claw at night*/), SpeciesWildEvolutionDelay.LONG) + new SpeciesEvolution(Species.WEAVILE, 1, EvolutionItem.RAZOR_CLAW, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT /* Razor claw at night*/), SpeciesWildEvolutionDelay.VERY_LONG) ], [Species.URSARING]: [ new SpeciesEvolution(Species.URSALUNA, 1, EvolutionItem.PEAT_BLOCK, null, SpeciesWildEvolutionDelay.VERY_LONG) //Ursaring does not evolve into Bloodmoon Ursaluna @@ -1362,8 +1378,8 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.SUDOWOODO, 1, null, new SpeciesEvolutionCondition(p => p.moveset.filter(m => m?.moveId === Moves.MIMIC).length > 0), SpeciesWildEvolutionDelay.MEDIUM) ], [Species.MIME_JR]: [ - new SpeciesEvolution(Species.GALAR_MR_MIME, 1, null, new SpeciesEvolutionCondition(p => p.moveset.filter(m => m?.moveId === Moves.MIMIC).length > 0 && (p.scene.arena.biomeType === Biome.ICE_CAVE || p.scene.arena.biomeType === Biome.SNOWY_FOREST)), SpeciesWildEvolutionDelay.MEDIUM), - new SpeciesEvolution(Species.MR_MIME, 1, null, new SpeciesEvolutionCondition(p => p.moveset.filter(m => m?.moveId === Moves.MIMIC).length > 0), SpeciesWildEvolutionDelay.MEDIUM) + new SpeciesEvolution(Species.GALAR_MR_MIME, 1, null, new SpeciesEvolutionCondition(p => p.moveset.filter(m => m?.moveId === Moves.MIMIC).length > 0 && (p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT)), SpeciesWildEvolutionDelay.MEDIUM), + new SpeciesEvolution(Species.MR_MIME, 1, null, new SpeciesEvolutionCondition(p => p.moveset.filter(m => m?.moveId === Moves.MIMIC).length > 0 && (p.scene.arena.getTimeOfDay() === TimeOfDay.DAWN || p.scene.arena.getTimeOfDay() === TimeOfDay.DAY)), SpeciesWildEvolutionDelay.MEDIUM) ], [Species.PANSAGE]: [ new SpeciesEvolution(Species.SIMISAGE, 1, EvolutionItem.LEAF_STONE, null, SpeciesWildEvolutionDelay.LONG) @@ -1381,8 +1397,8 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.WHIMSICOTT, 1, EvolutionItem.SUN_STONE, null, SpeciesWildEvolutionDelay.LONG) ], [Species.PETILIL]: [ - new SpeciesEvolution(Species.HISUI_LILLIGANT, 1, EvolutionItem.SUN_STONE, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT), SpeciesWildEvolutionDelay.VERY_LONG), - new SpeciesEvolution(Species.LILLIGANT, 1, EvolutionItem.SUN_STONE, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAWN || p.scene.arena.getTimeOfDay() === TimeOfDay.DAY), SpeciesWildEvolutionDelay.LONG) + new SpeciesEvolution(Species.HISUI_LILLIGANT, 1, EvolutionItem.SHINY_STONE, null, SpeciesWildEvolutionDelay.LONG), + new SpeciesEvolution(Species.LILLIGANT, 1, EvolutionItem.SUN_STONE, null, SpeciesWildEvolutionDelay.LONG) ], [Species.BASCULIN]: [ new SpeciesFormEvolution(Species.BASCULEGION, "white-striped", "female", 40, null, new SpeciesEvolutionCondition(p => p.gender === Gender.FEMALE, p => p.gender = Gender.FEMALE), SpeciesWildEvolutionDelay.VERY_LONG), @@ -1435,7 +1451,7 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.APPLETUN, 1, EvolutionItem.SWEET_APPLE, null, SpeciesWildEvolutionDelay.LONG) ], [Species.CLOBBOPUS]: [ - new SpeciesEvolution(Species.GRAPPLOCT, 35, null, new SpeciesEvolutionCondition(p => p.moveset.filter(m => m?.moveId === Moves.TAUNT).length > 0), SpeciesWildEvolutionDelay.MEDIUM) + new SpeciesEvolution(Species.GRAPPLOCT, 35, null, new SpeciesEvolutionCondition(p => p.moveset.filter(m => m?.moveId === Moves.TAUNT).length > 0)/*Once Taunt is implemented, change evo level to 1 and delay to LONG*/) ], [Species.SINISTEA]: [ new SpeciesFormEvolution(Species.POLTEAGEIST, "phony", "phony", 1, EvolutionItem.CRACKED_POT, null, SpeciesWildEvolutionDelay.LONG), @@ -1472,7 +1488,7 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.OVERQWIL, 28, null, new SpeciesEvolutionCondition(p => p.moveset.filter(m => m?.moveId === Moves.BARB_BARRAGE).length > 0), SpeciesWildEvolutionDelay.LONG) ], [Species.HISUI_SNEASEL]: [ - new SpeciesEvolution(Species.SNEASLER, 1, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAY /* Razor claw at day*/), SpeciesWildEvolutionDelay.LONG) + new SpeciesEvolution(Species.SNEASLER, 1, EvolutionItem.RAZOR_CLAW, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAWN || p.scene.arena.getTimeOfDay() === TimeOfDay.DAY /* Razor claw at day*/), SpeciesWildEvolutionDelay.VERY_LONG) ], [Species.CHARCADET]: [ new SpeciesEvolution(Species.ARMAROUGE, 1, EvolutionItem.AUSPICIOUS_ARMOR, null, SpeciesWildEvolutionDelay.LONG), @@ -1512,10 +1528,10 @@ export const pokemonEvolutions: PokemonEvolutions = { SpeciesWildEvolutionDelay.VERY_LONG) ], [Species.RHYDON]: [ - new SpeciesEvolution(Species.RHYPERIOR, 1, EvolutionItem.LINKING_CORD, new SpeciesEvolutionCondition(p => true /* Protector */), SpeciesWildEvolutionDelay.VERY_LONG) + new SpeciesEvolution(Species.RHYPERIOR, 1, EvolutionItem.PROTECTOR, null, SpeciesWildEvolutionDelay.VERY_LONG) ], [Species.SEADRA]: [ - new SpeciesEvolution(Species.KINGDRA, 1, EvolutionItem.LINKING_CORD, new SpeciesEvolutionCondition(p => true /* Dragon scale*/), SpeciesWildEvolutionDelay.VERY_LONG) + new SpeciesEvolution(Species.KINGDRA, 1, EvolutionItem.DRAGON_SCALE, null, SpeciesWildEvolutionDelay.VERY_LONG) ], [Species.SCYTHER]: [ new SpeciesEvolution(Species.SCIZOR, 1, EvolutionItem.LINKING_CORD, new SpeciesEvolutionCondition( @@ -1524,22 +1540,22 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.KLEAVOR, 1, EvolutionItem.BLACK_AUGURITE, null, SpeciesWildEvolutionDelay.VERY_LONG) ], [Species.ELECTABUZZ]: [ - new SpeciesEvolution(Species.ELECTIVIRE, 1, EvolutionItem.LINKING_CORD, new SpeciesEvolutionCondition(p => true /* Electirizer*/), SpeciesWildEvolutionDelay.VERY_LONG) + new SpeciesEvolution(Species.ELECTIVIRE, 1, EvolutionItem.ELECTIRIZER, null, SpeciesWildEvolutionDelay.VERY_LONG) ], [Species.MAGMAR]: [ - new SpeciesEvolution(Species.MAGMORTAR, 1, EvolutionItem.LINKING_CORD, new SpeciesEvolutionCondition(p => true /* Magmarizer*/), SpeciesWildEvolutionDelay.VERY_LONG) + new SpeciesEvolution(Species.MAGMORTAR, 1, EvolutionItem.MAGMARIZER, null, SpeciesWildEvolutionDelay.VERY_LONG) ], [Species.PORYGON]: [ - new SpeciesEvolution(Species.PORYGON2, 1, EvolutionItem.LINKING_CORD, new SpeciesEvolutionCondition(p => true /*Upgrade*/), SpeciesWildEvolutionDelay.LONG) + new SpeciesEvolution(Species.PORYGON2, 1, EvolutionItem.UPGRADE, null, SpeciesWildEvolutionDelay.LONG) ], [Species.PORYGON2]: [ - new SpeciesEvolution(Species.PORYGON_Z, 1, EvolutionItem.LINKING_CORD, new SpeciesEvolutionCondition(p => true /* Dubious disc*/), SpeciesWildEvolutionDelay.VERY_LONG) + new SpeciesEvolution(Species.PORYGON_Z, 1, EvolutionItem.DUBIOUS_DISC, null, SpeciesWildEvolutionDelay.VERY_LONG) ], [Species.FEEBAS]: [ - new SpeciesEvolution(Species.MILOTIC, 1, EvolutionItem.LINKING_CORD, new SpeciesEvolutionCondition(p => true /* Prism scale*/), SpeciesWildEvolutionDelay.VERY_LONG) + new SpeciesEvolution(Species.MILOTIC, 1, EvolutionItem.PRISM_SCALE, null, SpeciesWildEvolutionDelay.VERY_LONG) ], [Species.DUSCLOPS]: [ - new SpeciesEvolution(Species.DUSKNOIR, 1, EvolutionItem.LINKING_CORD, new SpeciesEvolutionCondition(p => true /* Reaper cloth*/), SpeciesWildEvolutionDelay.VERY_LONG) + new SpeciesEvolution(Species.DUSKNOIR, 1, EvolutionItem.REAPER_CLOTH, null, SpeciesWildEvolutionDelay.VERY_LONG) ], [Species.CLAMPERL]: [ new SpeciesEvolution(Species.HUNTAIL, 1, EvolutionItem.LINKING_CORD, new SpeciesEvolutionCondition(p => p.gender === Gender.MALE, p => p.gender = Gender.MALE /* Deep Sea Tooth */), SpeciesWildEvolutionDelay.VERY_LONG), @@ -1558,10 +1574,10 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.ACCELGOR, 1, EvolutionItem.LINKING_CORD, new SpeciesEvolutionCondition(p => !!p.scene.gameData.dexData[Species.KARRABLAST].caughtAttr), SpeciesWildEvolutionDelay.VERY_LONG) ], [Species.SPRITZEE]: [ - new SpeciesEvolution(Species.AROMATISSE, 1, EvolutionItem.LINKING_CORD, new SpeciesEvolutionCondition(p => true /*Sachet*/), SpeciesWildEvolutionDelay.VERY_LONG) + new SpeciesEvolution(Species.AROMATISSE, 1, EvolutionItem.SACHET, null, SpeciesWildEvolutionDelay.VERY_LONG) ], [Species.SWIRLIX]: [ - new SpeciesEvolution(Species.SLURPUFF, 1, EvolutionItem.LINKING_CORD, new SpeciesEvolutionCondition(p => true /*Whipped Dream*/), SpeciesWildEvolutionDelay.VERY_LONG) + new SpeciesEvolution(Species.SLURPUFF, 1, EvolutionItem.WHIPPED_DREAM, null, SpeciesWildEvolutionDelay.VERY_LONG) ], [Species.PHANTUMP]: [ new SpeciesEvolution(Species.TREVENANT, 1, EvolutionItem.LINKING_CORD, null, SpeciesWildEvolutionDelay.VERY_LONG) @@ -1576,7 +1592,7 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.ANNIHILAPE, 35, null, new SpeciesEvolutionCondition(p => p.moveset.filter(m => m?.moveId === Moves.RAGE_FIST).length > 0), SpeciesWildEvolutionDelay.VERY_LONG) ], [Species.GOLBAT]: [ - new SpeciesEvolution(Species.CROBAT, 1, null, new SpeciesFriendshipEvolutionCondition(110), SpeciesWildEvolutionDelay.VERY_LONG) + new SpeciesEvolution(Species.CROBAT, 1, null, new SpeciesFriendshipEvolutionCondition(120), SpeciesWildEvolutionDelay.VERY_LONG) ], [Species.CHANSEY]: [ new SpeciesEvolution(Species.BLISSEY, 1, null, new SpeciesFriendshipEvolutionCondition(200), SpeciesWildEvolutionDelay.LONG) @@ -1610,29 +1626,29 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.CHANSEY, 1, null, new SpeciesFriendshipEvolutionCondition(160), SpeciesWildEvolutionDelay.SHORT) ], [Species.MUNCHLAX]: [ - new SpeciesEvolution(Species.SNORLAX, 1, null, new SpeciesFriendshipEvolutionCondition(90), SpeciesWildEvolutionDelay.LONG) + new SpeciesEvolution(Species.SNORLAX, 1, null, new SpeciesFriendshipEvolutionCondition(120), SpeciesWildEvolutionDelay.LONG) ], [Species.RIOLU]: [ - new SpeciesEvolution(Species.LUCARIO, 1, null, new SpeciesFriendshipEvolutionCondition(90, p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAWN || p.scene.arena.getTimeOfDay() === TimeOfDay.DAY), SpeciesWildEvolutionDelay.LONG) + new SpeciesEvolution(Species.LUCARIO, 1, null, new SpeciesFriendshipEvolutionCondition(120, p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAWN || p.scene.arena.getTimeOfDay() === TimeOfDay.DAY), SpeciesWildEvolutionDelay.LONG) ], [Species.WOOBAT]: [ - new SpeciesEvolution(Species.SWOOBAT, 1, null, new SpeciesFriendshipEvolutionCondition(70), SpeciesWildEvolutionDelay.MEDIUM) + new SpeciesEvolution(Species.SWOOBAT, 1, null, new SpeciesFriendshipEvolutionCondition(90), SpeciesWildEvolutionDelay.MEDIUM) ], [Species.SWADLOON]: [ - new SpeciesEvolution(Species.LEAVANNY, 1, null, new SpeciesFriendshipEvolutionCondition(110), SpeciesWildEvolutionDelay.LONG) + new SpeciesEvolution(Species.LEAVANNY, 1, null, new SpeciesFriendshipEvolutionCondition(120), SpeciesWildEvolutionDelay.LONG) ], [Species.TYPE_NULL]: [ - new SpeciesEvolution(Species.SILVALLY, 1, null, new SpeciesFriendshipEvolutionCondition(70), SpeciesWildEvolutionDelay.LONG) + new SpeciesEvolution(Species.SILVALLY, 1, null, new SpeciesFriendshipEvolutionCondition(100), SpeciesWildEvolutionDelay.LONG) ], [Species.ALOLA_MEOWTH]: [ - new SpeciesEvolution(Species.ALOLA_PERSIAN, 1, null, new SpeciesFriendshipEvolutionCondition(70), SpeciesWildEvolutionDelay.LONG) + new SpeciesEvolution(Species.ALOLA_PERSIAN, 1, null, new SpeciesFriendshipEvolutionCondition(120), SpeciesWildEvolutionDelay.LONG) ], [Species.SNOM]: [ new SpeciesEvolution(Species.FROSMOTH, 1, null, new SpeciesFriendshipEvolutionCondition(90, p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT), SpeciesWildEvolutionDelay.MEDIUM) ], [Species.GIMMIGHOUL]: [ - new SpeciesFormEvolution(Species.GHOLDENGO, "chest", "", 1, null, new SpeciesFriendshipEvolutionCondition(70), SpeciesWildEvolutionDelay.VERY_LONG), - new SpeciesFormEvolution(Species.GHOLDENGO, "roaming", "", 1, null, new SpeciesFriendshipEvolutionCondition(70), SpeciesWildEvolutionDelay.VERY_LONG) + new SpeciesFormEvolution(Species.GHOLDENGO, "chest", "", 1, null, new SpeciesEvolutionCondition( p => p.evoCounter > 9 ), SpeciesWildEvolutionDelay.VERY_LONG), + new SpeciesFormEvolution(Species.GHOLDENGO, "roaming", "", 1, null, new SpeciesEvolutionCondition( p => p.evoCounter > 9 ), SpeciesWildEvolutionDelay.VERY_LONG) ] }; diff --git a/src/data/pokemon-forms.ts b/src/data/pokemon-forms.ts index 20703cebe81..a904f497b0f 100644 --- a/src/data/pokemon-forms.ts +++ b/src/data/pokemon-forms.ts @@ -12,6 +12,7 @@ import { TimeOfDay } from "#enums/time-of-day"; import { getPokemonNameWithAffix } from "#app/messages"; import i18next from "i18next"; import { WeatherType } from "./weather"; +import { Challenges } from "#app/enums/challenges"; export enum FormChangeItem { NONE, @@ -345,6 +346,16 @@ export class SpeciesFormChangePostMoveTrigger extends SpeciesFormChangeMoveTrigg } } +export class MeloettaFormChangePostMoveTrigger extends SpeciesFormChangePostMoveTrigger { + override canChange(pokemon: Pokemon): boolean { + if (pokemon.scene.gameMode.hasChallenge(Challenges.SINGLE_TYPE)) { + return false; + } else { + return super.canChange(pokemon); + } + } +} + export class SpeciesDefaultFormMatchTrigger extends SpeciesFormChangeTrigger { private formKey: string; @@ -673,7 +684,7 @@ export const pokemonFormChanges: PokemonFormChanges = { new SpeciesFormChange(Species.GROUDON, "", SpeciesFormKey.PRIMAL, new SpeciesFormChangeItemTrigger(FormChangeItem.RED_ORB)) ], [Species.RAYQUAZA]: [ - new SpeciesFormChange(Species.RAYQUAZA, "", SpeciesFormKey.MEGA, new SpeciesFormChangeCompoundTrigger(new SpeciesFormChangeItemTrigger(FormChangeItem.RAYQUAZITE), new SpeciesFormChangeMoveLearnedTrigger(Moves.DRAGON_ASCENT))) + new SpeciesFormChange(Species.RAYQUAZA, "", SpeciesFormKey.MEGA, new SpeciesFormChangeItemTrigger(FormChangeItem.RAYQUAZITE)) ], [Species.DEOXYS]: [ new SpeciesFormChange(Species.DEOXYS, "normal", "attack", new SpeciesFormChangeItemTrigger(FormChangeItem.SHARP_METEORITE)), @@ -759,9 +770,8 @@ export const pokemonFormChanges: PokemonFormChanges = { new SpeciesFormChange(Species.KELDEO, "resolute", "ordinary", new SpeciesFormChangeMoveLearnedTrigger(Moves.SECRET_SWORD, false)) ], [Species.MELOETTA]: [ - new SpeciesFormChange(Species.MELOETTA, "aria", "pirouette", new SpeciesFormChangePostMoveTrigger(Moves.RELIC_SONG), true), - new SpeciesFormChange(Species.MELOETTA, "pirouette", "aria", new SpeciesFormChangePostMoveTrigger(Moves.RELIC_SONG), true), - new SpeciesFormChange(Species.MELOETTA, "pirouette", "aria", new SpeciesFormChangeActiveTrigger(false), true) + new SpeciesFormChange(Species.MELOETTA, "aria", "pirouette", new MeloettaFormChangePostMoveTrigger(Moves.RELIC_SONG), true), + new SpeciesFormChange(Species.MELOETTA, "pirouette", "aria", new MeloettaFormChangePostMoveTrigger(Moves.RELIC_SONG), true) ], [Species.GENESECT]: [ new SpeciesFormChange(Species.GENESECT, "", "shock", new SpeciesFormChangeItemTrigger(FormChangeItem.SHOCK_DRIVE)), diff --git a/src/data/pokemon-level-moves.ts b/src/data/pokemon-level-moves.ts index 93bd57ae32c..b5608093df2 100644 --- a/src/data/pokemon-level-moves.ts +++ b/src/data/pokemon-level-moves.ts @@ -1609,6 +1609,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 12, Moves.DRAGON_BREATH ], [ 16, Moves.CURSE ], [ 20, Moves.ROCK_SLIDE ], + [ 22, Moves.GYRO_BALL ], //Custom, from USUM [ 24, Moves.SCREECH ], [ 28, Moves.SAND_TOMB ], [ 32, Moves.STEALTH_ROCK ], @@ -2121,7 +2122,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 20, Moves.DOUBLE_HIT ], [ 24, Moves.SLASH ], [ 28, Moves.FOCUS_ENERGY ], - [ 30, Moves.STEEL_WING ], + [ 30, Moves.STEEL_WING ], //Custom [ 32, Moves.AGILITY ], [ 36, Moves.AIR_SLASH ], [ 40, Moves.X_SCISSOR ], @@ -7549,14 +7550,15 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.POUND ], [ 1, Moves.COPYCAT ], [ 1, Moves.BARRIER ], + [ 1, Moves.TICKLE ], //USUM [ 4, Moves.BATON_PASS ], [ 8, Moves.ENCORE ], [ 12, Moves.CONFUSION ], - [ 16, Moves.ROLE_PLAY ], + [ 16, Moves.MIMIC ], //Custom, swapped with Role Play to be closer to USUM [ 20, Moves.PROTECT ], [ 24, Moves.RECYCLE ], [ 28, Moves.PSYBEAM ], - [ 32, Moves.MIMIC ], + [ 32, Moves.ROLE_PLAY ], //Custom, swapped with Mimic [ 36, Moves.LIGHT_SCREEN ], [ 36, Moves.REFLECT ], [ 36, Moves.SAFEGUARD ], @@ -19496,6 +19498,108 @@ export const pokemonFormLevelMoves: PokemonSpeciesFormLevelMoves = { [ 51, Moves.BELCH ], ], }, + [Species.REVAVROOM]: { + 1: [ + [ EVOLVE_MOVE, Moves.WICKED_TORQUE ], + [ EVOLVE_MOVE, Moves.SHIFT_GEAR ], + [ 1, Moves.LICK ], + [ 1, Moves.POISON_GAS ], + [ 1, Moves.MAGNET_RISE ], + [ 4, Moves.SMOG ], + [ 7, Moves.TAUNT ], + [ 10, Moves.ASSURANCE ], + [ 13, Moves.SLUDGE ], + [ 17, Moves.GYRO_BALL ], + [ 21, Moves.HEADBUTT ], + [ 25, Moves.SCREECH ], + [ 28, Moves.IRON_HEAD ], + [ 32, Moves.SWAGGER ], + [ 36, Moves.POISON_JAB ], + [ 46, Moves.UPROAR ], + [ 52, Moves.SPIN_OUT ], + [ 58, Moves.GUNK_SHOT ], + ], + 2: [ + [ EVOLVE_MOVE, Moves.BLAZING_TORQUE ], + [ EVOLVE_MOVE, Moves.SHIFT_GEAR ], + [ 1, Moves.LICK ], + [ 1, Moves.POISON_GAS ], + [ 1, Moves.MAGNET_RISE ], + [ 4, Moves.SMOG ], + [ 7, Moves.TAUNT ], + [ 10, Moves.ASSURANCE ], + [ 13, Moves.SLUDGE ], + [ 17, Moves.GYRO_BALL ], + [ 21, Moves.HEADBUTT ], + [ 25, Moves.SCREECH ], + [ 28, Moves.IRON_HEAD ], + [ 32, Moves.SWAGGER ], + [ 36, Moves.POISON_JAB ], + [ 46, Moves.UPROAR ], + [ 52, Moves.SPIN_OUT ], + [ 58, Moves.GUNK_SHOT ], + ], + 3: [ + [ EVOLVE_MOVE, Moves.NOXIOUS_TORQUE ], + [ EVOLVE_MOVE, Moves.SHIFT_GEAR ], + [ 1, Moves.LICK ], + [ 1, Moves.POISON_GAS ], + [ 1, Moves.MAGNET_RISE ], + [ 4, Moves.SMOG ], + [ 7, Moves.TAUNT ], + [ 10, Moves.ASSURANCE ], + [ 13, Moves.SLUDGE ], + [ 17, Moves.GYRO_BALL ], + [ 21, Moves.HEADBUTT ], + [ 25, Moves.SCREECH ], + [ 28, Moves.IRON_HEAD ], + [ 32, Moves.SWAGGER ], + [ 36, Moves.POISON_JAB ], + [ 46, Moves.UPROAR ], + [ 52, Moves.SPIN_OUT ], + [ 58, Moves.GUNK_SHOT ], + ], + 4: [ + [ EVOLVE_MOVE, Moves.MAGICAL_TORQUE ], + [ EVOLVE_MOVE, Moves.SHIFT_GEAR ], + [ 1, Moves.LICK ], + [ 1, Moves.POISON_GAS ], + [ 1, Moves.MAGNET_RISE ], + [ 4, Moves.SMOG ], + [ 7, Moves.TAUNT ], + [ 10, Moves.ASSURANCE ], + [ 13, Moves.SLUDGE ], + [ 17, Moves.GYRO_BALL ], + [ 21, Moves.HEADBUTT ], + [ 25, Moves.SCREECH ], + [ 28, Moves.IRON_HEAD ], + [ 32, Moves.SWAGGER ], + [ 36, Moves.POISON_JAB ], + [ 46, Moves.UPROAR ], + [ 52, Moves.SPIN_OUT ], + [ 58, Moves.GUNK_SHOT ], + ], + 5: [ + [ EVOLVE_MOVE, Moves.COMBAT_TORQUE ], + [ EVOLVE_MOVE, Moves.SHIFT_GEAR ], + [ 1, Moves.LICK ], + [ 1, Moves.POISON_GAS ], + [ 1, Moves.MAGNET_RISE ], + [ 4, Moves.SMOG ], + [ 7, Moves.TAUNT ], + [ 10, Moves.ASSURANCE ], + [ 13, Moves.SLUDGE ], + [ 17, Moves.GYRO_BALL ], + [ 21, Moves.HEADBUTT ], + [ 25, Moves.SCREECH ], + [ 28, Moves.IRON_HEAD ], + [ 32, Moves.SWAGGER ], + [ 36, Moves.POISON_JAB ], + [ 46, Moves.UPROAR ], + [ 52, Moves.SPIN_OUT ], + [ 58, Moves.GUNK_SHOT ], + ], + }, [Species.PALDEA_TAUROS]: { 1: [ [ 1, Moves.TACKLE ], diff --git a/src/data/pokemon-species.ts b/src/data/pokemon-species.ts index 3645cb03c60..b8ddd826035 100644 --- a/src/data/pokemon-species.ts +++ b/src/data/pokemon-species.ts @@ -243,16 +243,24 @@ export abstract class PokemonSpeciesForm { return false; } + /** + * Gets the BST for the species + * @returns The species' BST. + */ + getBaseStatTotal(): number { + return this.baseStats.reduce((i, n) => n + i); + } + /** * Gets the species' base stat amount for the given stat. * @param stat The desired stat. * @returns The species' base stat amount. */ - getBaseStat(stat: Stat): integer { + getBaseStat(stat: Stat): number { return this.baseStats[stat]; } - getBaseExp(): integer { + getBaseExp(): number { let ret = this.baseExp; switch (this.getFormSpriteKey()) { case SpeciesFormKey.MEGA: @@ -936,7 +944,7 @@ export function initSpecies() { new PokemonSpecies(Species.VENUSAUR, 1, false, false, false, "Seed Pokémon", Type.GRASS, Type.POISON, 2, 100, Abilities.OVERGROW, Abilities.NONE, Abilities.CHLOROPHYLL, 525, 80, 82, 83, 100, 100, 80, 45, 50, 263, GrowthRate.MEDIUM_SLOW, 87.5, true, true, new PokemonForm("Normal", "", Type.GRASS, Type.POISON, 2, 100, Abilities.OVERGROW, Abilities.NONE, Abilities.CHLOROPHYLL, 525, 80, 82, 83, 100, 100, 80, 45, 50, 263, true, null, true), new PokemonForm("Mega", SpeciesFormKey.MEGA, Type.GRASS, Type.POISON, 2.4, 155.5, Abilities.THICK_FAT, Abilities.THICK_FAT, Abilities.THICK_FAT, 625, 80, 100, 123, 122, 120, 80, 45, 50, 263, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.GRASS, Type.POISON, 24, 100, Abilities.CHLOROPHYLL, Abilities.CHLOROPHYLL, Abilities.CHLOROPHYLL, 625, 120, 82, 98, 130, 115, 80, 45, 50, 263, true), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.GRASS, Type.POISON, 24, 999.9, Abilities.CHLOROPHYLL, Abilities.CHLOROPHYLL, Abilities.CHLOROPHYLL, 625, 120, 82, 98, 130, 115, 80, 45, 50, 263, true), ), new PokemonSpecies(Species.CHARMANDER, 1, false, false, false, "Lizard Pokémon", Type.FIRE, null, 0.6, 8.5, Abilities.BLAZE, Abilities.NONE, Abilities.SOLAR_POWER, 309, 39, 52, 43, 60, 50, 65, 45, 50, 62, GrowthRate.MEDIUM_SLOW, 87.5, false), new PokemonSpecies(Species.CHARMELEON, 1, false, false, false, "Flame Pokémon", Type.FIRE, null, 1.1, 19, Abilities.BLAZE, Abilities.NONE, Abilities.SOLAR_POWER, 405, 58, 64, 58, 80, 65, 80, 45, 50, 142, GrowthRate.MEDIUM_SLOW, 87.5, false), @@ -944,20 +952,20 @@ export function initSpecies() { new PokemonForm("Normal", "", Type.FIRE, Type.FLYING, 1.7, 90.5, Abilities.BLAZE, Abilities.NONE, Abilities.SOLAR_POWER, 534, 78, 84, 78, 109, 85, 100, 45, 50, 267, false, null, true), new PokemonForm("Mega X", SpeciesFormKey.MEGA_X, Type.FIRE, Type.DRAGON, 1.7, 110.5, Abilities.TOUGH_CLAWS, Abilities.NONE, Abilities.TOUGH_CLAWS, 634, 78, 130, 111, 130, 85, 100, 45, 50, 267), new PokemonForm("Mega Y", SpeciesFormKey.MEGA_Y, Type.FIRE, Type.FLYING, 1.7, 100.5, Abilities.DROUGHT, Abilities.NONE, Abilities.DROUGHT, 634, 78, 104, 78, 159, 115, 100, 45, 50, 267), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.FIRE, Type.FLYING, 28, 90.5, Abilities.SOLAR_POWER, Abilities.SOLAR_POWER, Abilities.SOLAR_POWER, 634, 118, 84, 93, 139, 110, 100, 45, 50, 267), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.FIRE, Type.FLYING, 28, 999.9, Abilities.BERSERK, Abilities.BERSERK, Abilities.BERSERK, 634, 118, 84, 93, 139, 110, 100, 45, 50, 267), ), new PokemonSpecies(Species.SQUIRTLE, 1, false, false, false, "Tiny Turtle Pokémon", Type.WATER, null, 0.5, 9, Abilities.TORRENT, Abilities.NONE, Abilities.RAIN_DISH, 314, 44, 48, 65, 50, 64, 43, 45, 50, 63, GrowthRate.MEDIUM_SLOW, 87.5, false), new PokemonSpecies(Species.WARTORTLE, 1, false, false, false, "Turtle Pokémon", Type.WATER, null, 1, 22.5, Abilities.TORRENT, Abilities.NONE, Abilities.RAIN_DISH, 405, 59, 63, 80, 65, 80, 58, 45, 50, 142, GrowthRate.MEDIUM_SLOW, 87.5, false), new PokemonSpecies(Species.BLASTOISE, 1, false, false, false, "Shellfish Pokémon", Type.WATER, null, 1.6, 85.5, Abilities.TORRENT, Abilities.NONE, Abilities.RAIN_DISH, 530, 79, 83, 100, 85, 105, 78, 45, 50, 265, GrowthRate.MEDIUM_SLOW, 87.5, false, true, new PokemonForm("Normal", "", Type.WATER, null, 1.6, 85.5, Abilities.TORRENT, Abilities.NONE, Abilities.RAIN_DISH, 530, 79, 83, 100, 85, 105, 78, 45, 50, 265, false, null, true), new PokemonForm("Mega", SpeciesFormKey.MEGA, Type.WATER, null, 1.6, 101.1, Abilities.MEGA_LAUNCHER, Abilities.NONE, Abilities.MEGA_LAUNCHER, 630, 79, 103, 120, 135, 115, 78, 45, 50, 265), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.WATER, Type.STEEL, 25, 85.5, Abilities.SHELL_ARMOR, Abilities.SHELL_ARMOR, Abilities.SHELL_ARMOR, 630, 119, 83, 130, 115, 115, 68, 45, 50, 265), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.WATER, Type.STEEL, 25, 999.9, Abilities.SHELL_ARMOR, Abilities.SHELL_ARMOR, Abilities.SHELL_ARMOR, 630, 119, 83, 130, 115, 115, 68, 45, 50, 265), ), new PokemonSpecies(Species.CATERPIE, 1, false, false, false, "Worm Pokémon", Type.BUG, null, 0.3, 2.9, Abilities.SHIELD_DUST, Abilities.NONE, Abilities.RUN_AWAY, 195, 45, 30, 35, 20, 20, 45, 255, 50, 39, GrowthRate.MEDIUM_FAST, 50, false), new PokemonSpecies(Species.METAPOD, 1, false, false, false, "Cocoon Pokémon", Type.BUG, null, 0.7, 9.9, Abilities.SHED_SKIN, Abilities.NONE, Abilities.SHED_SKIN, 205, 50, 20, 55, 25, 25, 30, 120, 50, 72, GrowthRate.MEDIUM_FAST, 50, false), new PokemonSpecies(Species.BUTTERFREE, 1, false, false, false, "Butterfly Pokémon", Type.BUG, Type.FLYING, 1.1, 32, Abilities.COMPOUND_EYES, Abilities.NONE, Abilities.TINTED_LENS, 395, 60, 45, 50, 90, 80, 70, 45, 50, 198, GrowthRate.MEDIUM_FAST, 50, true, true, new PokemonForm("Normal", "", Type.BUG, Type.FLYING, 1.1, 32, Abilities.COMPOUND_EYES, Abilities.NONE, Abilities.TINTED_LENS, 395, 60, 45, 50, 90, 80, 70, 45, 50, 198, true, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.BUG, Type.FLYING, 17, 32, Abilities.COMPOUND_EYES, Abilities.COMPOUND_EYES, Abilities.COMPOUND_EYES, 495, 85, 35, 80, 120, 90, 85, 45, 50, 198, true), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.BUG, Type.FLYING, 17, 999.9, Abilities.COMPOUND_EYES, Abilities.COMPOUND_EYES, Abilities.COMPOUND_EYES, 495, 85, 35, 80, 120, 90, 85, 45, 50, 198, true), ), new PokemonSpecies(Species.WEEDLE, 1, false, false, false, "Hairy Bug Pokémon", Type.BUG, Type.POISON, 0.3, 3.2, Abilities.SHIELD_DUST, Abilities.NONE, Abilities.RUN_AWAY, 195, 40, 35, 30, 20, 20, 50, 255, 70, 39, GrowthRate.MEDIUM_FAST, 50, false), new PokemonSpecies(Species.KAKUNA, 1, false, false, false, "Cocoon Pokémon", Type.BUG, Type.POISON, 0.6, 10, Abilities.SHED_SKIN, Abilities.NONE, Abilities.SHED_SKIN, 205, 45, 25, 50, 25, 25, 35, 120, 70, 72, GrowthRate.MEDIUM_FAST, 50, false), @@ -986,7 +994,7 @@ export function initSpecies() { new PokemonForm("Cute Cosplay", "cute-cosplay", Type.ELECTRIC, null, 0.4, 6, Abilities.STATIC, Abilities.NONE, Abilities.LIGHTNING_ROD, 430, 45, 80, 50, 75, 60, 120, 190, 50, 112, true, null, true), //Custom new PokemonForm("Smart Cosplay", "smart-cosplay", Type.ELECTRIC, null, 0.4, 6, Abilities.STATIC, Abilities.NONE, Abilities.LIGHTNING_ROD, 430, 45, 80, 50, 75, 60, 120, 190, 50, 112, true, null, true), //Custom new PokemonForm("Tough Cosplay", "tough-cosplay", Type.ELECTRIC, null, 0.4, 6, Abilities.STATIC, Abilities.NONE, Abilities.LIGHTNING_ROD, 430, 45, 80, 50, 75, 60, 120, 190, 50, 112, true, null, true), //Custom - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.ELECTRIC, null, 21, 6, Abilities.LIGHTNING_ROD, Abilities.LIGHTNING_ROD, Abilities.LIGHTNING_ROD, 530, 125, 95, 60, 90, 70, 90, 190, 50, 112), //+100 BST from Partner Form + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.ELECTRIC, null, 21, 999.9, Abilities.LIGHTNING_ROD, Abilities.LIGHTNING_ROD, Abilities.LIGHTNING_ROD, 530, 125, 95, 60, 90, 70, 90, 190, 50, 112), //+100 BST from Partner Form ), new PokemonSpecies(Species.RAICHU, 1, false, false, false, "Mouse Pokémon", Type.ELECTRIC, null, 0.8, 30, Abilities.STATIC, Abilities.NONE, Abilities.LIGHTNING_ROD, 485, 60, 90, 55, 90, 80, 110, 75, 50, 243, GrowthRate.MEDIUM_FAST, 50, true), new PokemonSpecies(Species.SANDSHREW, 1, false, false, false, "Mouse Pokémon", Type.GROUND, null, 0.6, 12, Abilities.SAND_VEIL, Abilities.NONE, Abilities.SAND_RUSH, 300, 50, 75, 85, 20, 30, 40, 255, 50, 60, GrowthRate.MEDIUM_FAST, 50, false), @@ -1016,7 +1024,7 @@ export function initSpecies() { new PokemonSpecies(Species.DUGTRIO, 1, false, false, false, "Mole Pokémon", Type.GROUND, null, 0.7, 33.3, Abilities.SAND_VEIL, Abilities.ARENA_TRAP, Abilities.SAND_FORCE, 425, 35, 100, 50, 50, 70, 120, 50, 50, 149, GrowthRate.MEDIUM_FAST, 50, false), new PokemonSpecies(Species.MEOWTH, 1, false, false, false, "Scratch Cat Pokémon", Type.NORMAL, null, 0.4, 4.2, Abilities.PICKUP, Abilities.TECHNICIAN, Abilities.UNNERVE, 290, 40, 45, 35, 40, 40, 90, 255, 50, 58, GrowthRate.MEDIUM_FAST, 50, false, true, new PokemonForm("Normal", "", Type.NORMAL, null, 0.4, 4.2, Abilities.PICKUP, Abilities.TECHNICIAN, Abilities.UNNERVE, 290, 40, 45, 35, 40, 40, 90, 255, 50, 58, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.NORMAL, null, 33, 4.2, Abilities.TECHNICIAN, Abilities.TECHNICIAN, Abilities.TECHNICIAN, 540, 115, 110, 65, 65, 70, 115, 255, 50, 58), //+100 BST from Persian + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.NORMAL, null, 33, 999.9, Abilities.TECHNICIAN, Abilities.TECHNICIAN, Abilities.TECHNICIAN, 540, 115, 110, 65, 65, 70, 115, 255, 50, 58), //+100 BST from Persian ), new PokemonSpecies(Species.PERSIAN, 1, false, false, false, "Classy Cat Pokémon", Type.NORMAL, null, 1, 32, Abilities.LIMBER, Abilities.TECHNICIAN, Abilities.UNNERVE, 440, 65, 70, 60, 65, 65, 115, 90, 50, 154, GrowthRate.MEDIUM_FAST, 50, false), new PokemonSpecies(Species.PSYDUCK, 1, false, false, false, "Duck Pokémon", Type.WATER, null, 0.8, 19.6, Abilities.DAMP, Abilities.CLOUD_NINE, Abilities.SWIFT_SWIM, 320, 50, 52, 48, 65, 50, 55, 190, 50, 64, GrowthRate.MEDIUM_FAST, 50, false), @@ -1038,7 +1046,7 @@ export function initSpecies() { new PokemonSpecies(Species.MACHOKE, 1, false, false, false, "Superpower Pokémon", Type.FIGHTING, null, 1.5, 70.5, Abilities.GUTS, Abilities.NO_GUARD, Abilities.STEADFAST, 405, 80, 100, 70, 50, 60, 45, 90, 50, 142, GrowthRate.MEDIUM_SLOW, 75, false), new PokemonSpecies(Species.MACHAMP, 1, false, false, false, "Superpower Pokémon", Type.FIGHTING, null, 1.6, 130, Abilities.GUTS, Abilities.NO_GUARD, Abilities.STEADFAST, 505, 90, 130, 80, 65, 85, 55, 45, 50, 253, GrowthRate.MEDIUM_SLOW, 75, false, true, new PokemonForm("Normal", "", Type.FIGHTING, null, 1.6, 130, Abilities.GUTS, Abilities.NO_GUARD, Abilities.STEADFAST, 505, 90, 130, 80, 65, 85, 55, 45, 50, 253, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.FIGHTING, null, 25, 130, Abilities.GUTS, Abilities.GUTS, Abilities.GUTS, 605, 115, 170, 95, 65, 95, 65, 45, 50, 253), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.FIGHTING, null, 25, 999.9, Abilities.GUTS, Abilities.GUTS, Abilities.GUTS, 605, 115, 170, 95, 65, 95, 65, 45, 50, 253), ), new PokemonSpecies(Species.BELLSPROUT, 1, false, false, false, "Flower Pokémon", Type.GRASS, Type.POISON, 0.7, 4, Abilities.CHLOROPHYLL, Abilities.NONE, Abilities.GLUTTONY, 300, 50, 75, 35, 70, 30, 40, 255, 70, 60, GrowthRate.MEDIUM_SLOW, 50, false), new PokemonSpecies(Species.WEEPINBELL, 1, false, false, false, "Flycatcher Pokémon", Type.GRASS, Type.POISON, 1, 6.4, Abilities.CHLOROPHYLL, Abilities.NONE, Abilities.GLUTTONY, 390, 65, 90, 50, 85, 45, 55, 120, 70, 137, GrowthRate.MEDIUM_SLOW, 50, false), @@ -1071,7 +1079,7 @@ export function initSpecies() { new PokemonSpecies(Species.GENGAR, 1, false, false, false, "Shadow Pokémon", Type.GHOST, Type.POISON, 1.5, 40.5, Abilities.CURSED_BODY, Abilities.NONE, Abilities.NONE, 500, 60, 65, 60, 130, 75, 110, 45, 50, 250, GrowthRate.MEDIUM_SLOW, 50, false, true, new PokemonForm("Normal", "", Type.GHOST, Type.POISON, 1.5, 40.5, Abilities.CURSED_BODY, Abilities.NONE, Abilities.NONE, 500, 60, 65, 60, 130, 75, 110, 45, 50, 250, false, null, true), new PokemonForm("Mega", SpeciesFormKey.MEGA, Type.GHOST, Type.POISON, 1.4, 40.5, Abilities.SHADOW_TAG, Abilities.NONE, Abilities.NONE, 600, 60, 65, 80, 170, 95, 130, 45, 50, 250), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.GHOST, Type.POISON, 20, 40.5, Abilities.CURSED_BODY, Abilities.CURSED_BODY, Abilities.CURSED_BODY, 600, 140, 65, 70, 140, 85, 100, 45, 50, 250), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.GHOST, Type.POISON, 20, 999.9, Abilities.CURSED_BODY, Abilities.CURSED_BODY, Abilities.CURSED_BODY, 600, 140, 65, 70, 140, 85, 100, 45, 50, 250), ), new PokemonSpecies(Species.ONIX, 1, false, false, false, "Rock Snake Pokémon", Type.ROCK, Type.GROUND, 8.8, 210, Abilities.ROCK_HEAD, Abilities.STURDY, Abilities.WEAK_ARMOR, 385, 35, 45, 160, 30, 45, 70, 45, 50, 77, GrowthRate.MEDIUM_FAST, 50, false), new PokemonSpecies(Species.DROWZEE, 1, false, false, false, "Hypnosis Pokémon", Type.PSYCHIC, null, 1, 32.4, Abilities.INSOMNIA, Abilities.FOREWARN, Abilities.INNER_FOCUS, 328, 60, 48, 45, 43, 90, 42, 190, 70, 66, GrowthRate.MEDIUM_FAST, 50, false), @@ -1079,7 +1087,7 @@ export function initSpecies() { new PokemonSpecies(Species.KRABBY, 1, false, false, false, "River Crab Pokémon", Type.WATER, null, 0.4, 6.5, Abilities.HYPER_CUTTER, Abilities.SHELL_ARMOR, Abilities.SHEER_FORCE, 325, 30, 105, 90, 25, 25, 50, 225, 50, 65, GrowthRate.MEDIUM_FAST, 50, false), new PokemonSpecies(Species.KINGLER, 1, false, false, false, "Pincer Pokémon", Type.WATER, null, 1.3, 60, Abilities.HYPER_CUTTER, Abilities.SHELL_ARMOR, Abilities.SHEER_FORCE, 475, 55, 130, 115, 50, 50, 75, 60, 50, 166, GrowthRate.MEDIUM_FAST, 50, false, true, new PokemonForm("Normal", "", Type.WATER, null, 1.3, 60, Abilities.HYPER_CUTTER, Abilities.SHELL_ARMOR, Abilities.SHEER_FORCE, 475, 55, 130, 115, 50, 50, 75, 60, 50, 166, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.WATER, null, 19, 60, Abilities.TOUGH_CLAWS, Abilities.TOUGH_CLAWS, Abilities.TOUGH_CLAWS, 575, 90, 155, 140, 50, 80, 70, 60, 50, 166), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.WATER, null, 19, 999.9, Abilities.TOUGH_CLAWS, Abilities.TOUGH_CLAWS, Abilities.TOUGH_CLAWS, 575, 90, 155, 140, 50, 80, 70, 60, 50, 166), ), new PokemonSpecies(Species.VOLTORB, 1, false, false, false, "Ball Pokémon", Type.ELECTRIC, null, 0.5, 10.4, Abilities.SOUNDPROOF, Abilities.STATIC, Abilities.AFTERMATH, 330, 40, 30, 50, 55, 55, 100, 190, 70, 66, GrowthRate.MEDIUM_FAST, null, false), new PokemonSpecies(Species.ELECTRODE, 1, false, false, false, "Ball Pokémon", Type.ELECTRIC, null, 1.2, 66.6, Abilities.SOUNDPROOF, Abilities.STATIC, Abilities.AFTERMATH, 490, 60, 50, 70, 80, 80, 150, 60, 70, 172, GrowthRate.MEDIUM_FAST, null, false), @@ -1123,13 +1131,13 @@ export function initSpecies() { ), new PokemonSpecies(Species.LAPRAS, 1, false, false, false, "Transport Pokémon", Type.WATER, Type.ICE, 2.5, 220, Abilities.WATER_ABSORB, Abilities.SHELL_ARMOR, Abilities.HYDRATION, 535, 130, 85, 80, 85, 95, 60, 45, 50, 187, GrowthRate.SLOW, 50, false, true, new PokemonForm("Normal", "", Type.WATER, Type.ICE, 2.5, 220, Abilities.WATER_ABSORB, Abilities.SHELL_ARMOR, Abilities.HYDRATION, 535, 130, 85, 80, 85, 95, 60, 45, 50, 187, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.WATER, Type.ICE, 24, 220, Abilities.SHELL_ARMOR, Abilities.SHELL_ARMOR, Abilities.SHELL_ARMOR, 635, 170, 85, 95, 115, 110, 60, 45, 50, 187), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.WATER, Type.ICE, 24, 999.9, Abilities.SHELL_ARMOR, Abilities.SHELL_ARMOR, Abilities.SHELL_ARMOR, 635, 170, 85, 95, 115, 110, 60, 45, 50, 187), ), new PokemonSpecies(Species.DITTO, 1, false, false, false, "Transform Pokémon", Type.NORMAL, null, 0.3, 4, Abilities.LIMBER, Abilities.NONE, Abilities.IMPOSTER, 288, 48, 48, 48, 48, 48, 48, 35, 50, 101, GrowthRate.MEDIUM_FAST, null, false), new PokemonSpecies(Species.EEVEE, 1, false, false, false, "Evolution Pokémon", Type.NORMAL, null, 0.3, 6.5, Abilities.RUN_AWAY, Abilities.ADAPTABILITY, Abilities.ANTICIPATION, 325, 55, 55, 50, 45, 65, 55, 45, 50, 65, GrowthRate.MEDIUM_FAST, 87.5, false, true, new PokemonForm("Normal", "", Type.NORMAL, null, 0.3, 6.5, Abilities.RUN_AWAY, Abilities.ADAPTABILITY, Abilities.ANTICIPATION, 325, 55, 55, 50, 45, 65, 55, 45, 50, 65, false, null, true), new PokemonForm("Partner", "partner", Type.NORMAL, null, 0.3, 6.5, Abilities.RUN_AWAY, Abilities.ADAPTABILITY, Abilities.ANTICIPATION, 435, 65, 75, 70, 65, 85, 75, 45, 50, 65, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.NORMAL, null, 18, 6.5, Abilities.PROTEAN, Abilities.PROTEAN, Abilities.PROTEAN, 535, 105, 95, 70, 95, 85, 85, 45, 50, 65), //+100 BST from Partner Form + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.NORMAL, null, 18, 999.9, Abilities.PROTEAN, Abilities.PROTEAN, Abilities.PROTEAN, 535, 110, 90, 70, 95, 85, 85, 45, 50, 65), //+100 BST from Partner Form ), new PokemonSpecies(Species.VAPOREON, 1, false, false, false, "Bubble Jet Pokémon", Type.WATER, null, 1, 29, Abilities.WATER_ABSORB, Abilities.NONE, Abilities.HYDRATION, 525, 130, 65, 60, 110, 95, 65, 45, 50, 184, GrowthRate.MEDIUM_FAST, 87.5, false), new PokemonSpecies(Species.JOLTEON, 1, false, false, false, "Lightning Pokémon", Type.ELECTRIC, null, 0.8, 24.5, Abilities.VOLT_ABSORB, Abilities.NONE, Abilities.QUICK_FEET, 525, 65, 65, 60, 110, 95, 130, 45, 50, 184, GrowthRate.MEDIUM_FAST, 87.5, false), @@ -1145,7 +1153,7 @@ export function initSpecies() { ), new PokemonSpecies(Species.SNORLAX, 1, false, false, false, "Sleeping Pokémon", Type.NORMAL, null, 2.1, 460, Abilities.IMMUNITY, Abilities.THICK_FAT, Abilities.GLUTTONY, 540, 160, 110, 65, 65, 110, 30, 25, 50, 189, GrowthRate.SLOW, 87.5, false, true, new PokemonForm("Normal", "", Type.NORMAL, null, 2.1, 460, Abilities.IMMUNITY, Abilities.THICK_FAT, Abilities.GLUTTONY, 540, 160, 110, 65, 65, 110, 30, 25, 50, 189, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.NORMAL, null, 35, 460, Abilities.HARVEST, Abilities.HARVEST, Abilities.HARVEST, 640, 200, 135, 80, 80, 125, 20, 25, 50, 189), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.NORMAL, null, 35, 999.9, Abilities.HARVEST, Abilities.HARVEST, Abilities.HARVEST, 640, 200, 135, 80, 80, 125, 20, 25, 50, 189), ), new PokemonSpecies(Species.ARTICUNO, 1, true, false, false, "Freeze Pokémon", Type.ICE, Type.FLYING, 1.7, 55.4, Abilities.PRESSURE, Abilities.NONE, Abilities.SNOW_CLOAK, 580, 90, 85, 100, 95, 125, 85, 3, 35, 290, GrowthRate.SLOW, null, false), new PokemonSpecies(Species.ZAPDOS, 1, true, false, false, "Electric Pokémon", Type.ELECTRIC, Type.FLYING, 1.6, 52.6, Abilities.PRESSURE, Abilities.NONE, Abilities.STATIC, 580, 90, 90, 85, 125, 90, 100, 3, 35, 290, GrowthRate.SLOW, null, false), @@ -1785,7 +1793,7 @@ export function initSpecies() { new PokemonSpecies(Species.TRUBBISH, 5, false, false, false, "Trash Bag Pokémon", Type.POISON, null, 0.6, 31, Abilities.STENCH, Abilities.STICKY_HOLD, Abilities.AFTERMATH, 329, 50, 50, 62, 40, 62, 65, 190, 50, 66, GrowthRate.MEDIUM_FAST, 50, false), new PokemonSpecies(Species.GARBODOR, 5, false, false, false, "Trash Heap Pokémon", Type.POISON, null, 1.9, 107.3, Abilities.STENCH, Abilities.WEAK_ARMOR, Abilities.AFTERMATH, 474, 80, 95, 82, 60, 82, 75, 60, 50, 166, GrowthRate.MEDIUM_FAST, 50, false, true, new PokemonForm("Normal", "", Type.POISON, null, 1.9, 107.3, Abilities.STENCH, Abilities.WEAK_ARMOR, Abilities.AFTERMATH, 474, 80, 95, 82, 60, 82, 75, 60, 50, 166, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.POISON, Type.STEEL, 21, 107.3, Abilities.TOXIC_DEBRIS, Abilities.TOXIC_DEBRIS, Abilities.TOXIC_DEBRIS, 574, 135, 125, 102, 57, 102, 53, 60, 50, 166), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.POISON, Type.STEEL, 21, 999.9, Abilities.TOXIC_DEBRIS, Abilities.TOXIC_DEBRIS, Abilities.TOXIC_DEBRIS, 574, 135, 125, 102, 57, 102, 53, 60, 50, 166), ), new PokemonSpecies(Species.ZORUA, 5, false, false, false, "Tricky Fox Pokémon", Type.DARK, null, 0.7, 12.5, Abilities.ILLUSION, Abilities.NONE, Abilities.NONE, 330, 40, 65, 40, 80, 40, 65, 75, 50, 66, GrowthRate.MEDIUM_SLOW, 87.5, false), new PokemonSpecies(Species.ZOROARK, 5, false, false, false, "Illusion Fox Pokémon", Type.DARK, null, 1.6, 81.1, Abilities.ILLUSION, Abilities.NONE, Abilities.NONE, 510, 60, 105, 60, 120, 60, 105, 45, 50, 179, GrowthRate.MEDIUM_SLOW, 87.5, false), @@ -1893,7 +1901,7 @@ export function initSpecies() { ), new PokemonSpecies(Species.MELOETTA, 5, false, false, true, "Melody Pokémon", Type.NORMAL, Type.PSYCHIC, 0.6, 6.5, Abilities.SERENE_GRACE, Abilities.NONE, Abilities.NONE, 600, 100, 77, 77, 128, 128, 90, 3, 100, 270, GrowthRate.SLOW, null, false, true, new PokemonForm("Aria Forme", "aria", Type.NORMAL, Type.PSYCHIC, 0.6, 6.5, Abilities.SERENE_GRACE, Abilities.NONE, Abilities.NONE, 600, 100, 77, 77, 128, 128, 90, 3, 100, 270, false, null, true), - new PokemonForm("Pirouette Forme", "pirouette", Type.NORMAL, Type.FIGHTING, 0.6, 6.5, Abilities.SERENE_GRACE, Abilities.NONE, Abilities.NONE, 600, 100, 128, 90, 77, 77, 128, 3, 100, 270), + new PokemonForm("Pirouette Forme", "pirouette", Type.NORMAL, Type.FIGHTING, 0.6, 6.5, Abilities.SERENE_GRACE, Abilities.NONE, Abilities.NONE, 600, 100, 128, 90, 77, 77, 128, 3, 100, 270, false, null, true), ), new PokemonSpecies(Species.GENESECT, 5, false, false, true, "Paleozoic Pokémon", Type.BUG, Type.STEEL, 1.5, 82.5, Abilities.DOWNLOAD, Abilities.NONE, Abilities.NONE, 600, 71, 120, 95, 120, 95, 99, 3, 0, 300, GrowthRate.SLOW, null, false, true, new PokemonForm("Normal", "", Type.BUG, Type.STEEL, 1.5, 82.5, Abilities.DOWNLOAD, Abilities.NONE, Abilities.NONE, 600, 71, 120, 95, 120, 95, 99, 3, 0, 300, false, null, true), @@ -2259,25 +2267,25 @@ export function initSpecies() { new PokemonSpecies(Species.MELTAN, 7, false, false, true, "Hex Nut Pokémon", Type.STEEL, null, 0.2, 8, Abilities.MAGNET_PULL, Abilities.NONE, Abilities.NONE, 300, 46, 65, 65, 55, 35, 34, 3, 0, 150, GrowthRate.SLOW, null, false), new PokemonSpecies(Species.MELMETAL, 7, false, false, true, "Hex Nut Pokémon", Type.STEEL, null, 2.5, 800, Abilities.IRON_FIST, Abilities.NONE, Abilities.NONE, 600, 135, 143, 143, 80, 65, 34, 3, 0, 300, GrowthRate.SLOW, null, false, true, new PokemonForm("Normal", "", Type.STEEL, null, 2.5, 800, Abilities.IRON_FIST, Abilities.NONE, Abilities.NONE, 600, 135, 143, 143, 80, 65, 34, 3, 0, 300, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.STEEL, null, 25, 800, Abilities.IRON_FIST, Abilities.IRON_FIST, Abilities.IRON_FIST, 700, 175, 165, 155, 85, 75, 45, 3, 0, 300), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.STEEL, null, 25, 999.9, Abilities.IRON_FIST, Abilities.IRON_FIST, Abilities.IRON_FIST, 700, 175, 165, 155, 85, 75, 45, 3, 0, 300), ), new PokemonSpecies(Species.GROOKEY, 8, false, false, false, "Chimp Pokémon", Type.GRASS, null, 0.3, 5, Abilities.OVERGROW, Abilities.NONE, Abilities.GRASSY_SURGE, 310, 50, 65, 50, 40, 40, 65, 45, 50, 62, GrowthRate.MEDIUM_SLOW, 87.5, false), new PokemonSpecies(Species.THWACKEY, 8, false, false, false, "Beat Pokémon", Type.GRASS, null, 0.7, 14, Abilities.OVERGROW, Abilities.NONE, Abilities.GRASSY_SURGE, 420, 70, 85, 70, 55, 60, 80, 45, 50, 147, GrowthRate.MEDIUM_SLOW, 87.5, false), new PokemonSpecies(Species.RILLABOOM, 8, false, false, false, "Drummer Pokémon", Type.GRASS, null, 2.1, 90, Abilities.OVERGROW, Abilities.NONE, Abilities.GRASSY_SURGE, 530, 100, 125, 90, 60, 70, 85, 45, 50, 265, GrowthRate.MEDIUM_SLOW, 87.5, false, true, new PokemonForm("Normal", "", Type.GRASS, null, 2.1, 90, Abilities.OVERGROW, Abilities.NONE, Abilities.GRASSY_SURGE, 530, 100, 125, 90, 60, 70, 85, 45, 50, 265, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.GRASS, null, 28, 90, Abilities.GRASSY_SURGE, Abilities.GRASSY_SURGE, Abilities.GRASSY_SURGE, 630, 125, 150, 105, 85, 85, 80, 45, 50, 265), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.GRASS, null, 28, 999.9, Abilities.GRASSY_SURGE, Abilities.GRASSY_SURGE, Abilities.GRASSY_SURGE, 630, 125, 150, 105, 85, 85, 80, 45, 50, 265), ), new PokemonSpecies(Species.SCORBUNNY, 8, false, false, false, "Rabbit Pokémon", Type.FIRE, null, 0.3, 4.5, Abilities.BLAZE, Abilities.NONE, Abilities.LIBERO, 310, 50, 71, 40, 40, 40, 69, 45, 50, 62, GrowthRate.MEDIUM_SLOW, 87.5, false), new PokemonSpecies(Species.RABOOT, 8, false, false, false, "Rabbit Pokémon", Type.FIRE, null, 0.6, 9, Abilities.BLAZE, Abilities.NONE, Abilities.LIBERO, 420, 65, 86, 60, 55, 60, 94, 45, 50, 147, GrowthRate.MEDIUM_SLOW, 87.5, false), new PokemonSpecies(Species.CINDERACE, 8, false, false, false, "Striker Pokémon", Type.FIRE, null, 1.4, 33, Abilities.BLAZE, Abilities.NONE, Abilities.LIBERO, 530, 80, 116, 75, 65, 75, 119, 45, 50, 265, GrowthRate.MEDIUM_SLOW, 87.5, false, true, new PokemonForm("Normal", "", Type.FIRE, null, 1.4, 33, Abilities.BLAZE, Abilities.NONE, Abilities.LIBERO, 530, 80, 116, 75, 65, 75, 119, 45, 50, 265, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.FIRE, null, 27, 33, Abilities.LIBERO, Abilities.LIBERO, Abilities.LIBERO, 630, 100, 146, 80, 90, 80, 134, 45, 50, 265), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.FIRE, null, 27, 999.9, Abilities.LIBERO, Abilities.LIBERO, Abilities.LIBERO, 630, 100, 146, 80, 90, 80, 134, 45, 50, 265), ), new PokemonSpecies(Species.SOBBLE, 8, false, false, false, "Water Lizard Pokémon", Type.WATER, null, 0.3, 4, Abilities.TORRENT, Abilities.NONE, Abilities.SNIPER, 310, 50, 40, 40, 70, 40, 70, 45, 50, 62, GrowthRate.MEDIUM_SLOW, 87.5, false), new PokemonSpecies(Species.DRIZZILE, 8, false, false, false, "Water Lizard Pokémon", Type.WATER, null, 0.7, 11.5, Abilities.TORRENT, Abilities.NONE, Abilities.SNIPER, 420, 65, 60, 55, 95, 55, 90, 45, 50, 147, GrowthRate.MEDIUM_SLOW, 87.5, false), new PokemonSpecies(Species.INTELEON, 8, false, false, false, "Secret Agent Pokémon", Type.WATER, null, 1.9, 45.2, Abilities.TORRENT, Abilities.NONE, Abilities.SNIPER, 530, 70, 85, 65, 125, 65, 120, 45, 50, 265, GrowthRate.MEDIUM_SLOW, 87.5, false, true, new PokemonForm("Normal", "", Type.WATER, null, 1.9, 45.2, Abilities.TORRENT, Abilities.NONE, Abilities.SNIPER, 530, 70, 85, 65, 125, 65, 120, 45, 50, 265, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.WATER, null, 40, 45.2, Abilities.SNIPER, Abilities.SNIPER, Abilities.SNIPER, 630, 95, 97, 77, 147, 77, 137, 45, 50, 265), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.WATER, null, 40, 999.9, Abilities.SNIPER, Abilities.SNIPER, Abilities.SNIPER, 630, 95, 97, 77, 147, 77, 137, 45, 50, 265), ), new PokemonSpecies(Species.SKWOVET, 8, false, false, false, "Cheeky Pokémon", Type.NORMAL, null, 0.3, 2.5, Abilities.CHEEK_POUCH, Abilities.NONE, Abilities.GLUTTONY, 275, 70, 55, 55, 35, 35, 25, 255, 50, 55, GrowthRate.MEDIUM_FAST, 50, false), new PokemonSpecies(Species.GREEDENT, 8, false, false, false, "Greedy Pokémon", Type.NORMAL, null, 0.6, 6, Abilities.CHEEK_POUCH, Abilities.NONE, Abilities.GLUTTONY, 460, 120, 95, 95, 55, 75, 20, 90, 50, 161, GrowthRate.MEDIUM_FAST, 50, false), @@ -2285,13 +2293,13 @@ export function initSpecies() { new PokemonSpecies(Species.CORVISQUIRE, 8, false, false, false, "Raven Pokémon", Type.FLYING, null, 0.8, 16, Abilities.KEEN_EYE, Abilities.UNNERVE, Abilities.BIG_PECKS, 365, 68, 67, 55, 43, 55, 77, 120, 50, 128, GrowthRate.MEDIUM_SLOW, 50, false), new PokemonSpecies(Species.CORVIKNIGHT, 8, false, false, false, "Raven Pokémon", Type.FLYING, Type.STEEL, 2.2, 75, Abilities.PRESSURE, Abilities.UNNERVE, Abilities.MIRROR_ARMOR, 495, 98, 87, 105, 53, 85, 67, 45, 50, 248, GrowthRate.MEDIUM_SLOW, 50, false, true, new PokemonForm("Normal", "", Type.FLYING, Type.STEEL, 2.2, 75, Abilities.PRESSURE, Abilities.UNNERVE, Abilities.MIRROR_ARMOR, 495, 98, 87, 105, 53, 85, 67, 45, 50, 248, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.FLYING, Type.STEEL, 14, 75, Abilities.MIRROR_ARMOR, Abilities.MIRROR_ARMOR, Abilities.MIRROR_ARMOR, 595, 128, 102, 140, 53, 95, 77, 45, 50, 248), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.FLYING, Type.STEEL, 14, 999.9, Abilities.MIRROR_ARMOR, Abilities.MIRROR_ARMOR, Abilities.MIRROR_ARMOR, 595, 128, 102, 140, 53, 95, 77, 45, 50, 248), ), new PokemonSpecies(Species.BLIPBUG, 8, false, false, false, "Larva Pokémon", Type.BUG, null, 0.4, 8, Abilities.SWARM, Abilities.COMPOUND_EYES, Abilities.TELEPATHY, 180, 25, 20, 20, 25, 45, 45, 255, 50, 36, GrowthRate.MEDIUM_FAST, 50, false), new PokemonSpecies(Species.DOTTLER, 8, false, false, false, "Radome Pokémon", Type.BUG, Type.PSYCHIC, 0.4, 19.5, Abilities.SWARM, Abilities.COMPOUND_EYES, Abilities.TELEPATHY, 335, 50, 35, 80, 50, 90, 30, 120, 50, 117, GrowthRate.MEDIUM_FAST, 50, false), new PokemonSpecies(Species.ORBEETLE, 8, false, false, false, "Seven Spot Pokémon", Type.BUG, Type.PSYCHIC, 0.4, 40.8, Abilities.SWARM, Abilities.FRISK, Abilities.TELEPATHY, 505, 60, 45, 110, 80, 120, 90, 45, 50, 253, GrowthRate.MEDIUM_FAST, 50, false, true, new PokemonForm("Normal", "", Type.BUG, Type.PSYCHIC, 0.4, 40.8, Abilities.SWARM, Abilities.FRISK, Abilities.TELEPATHY, 505, 60, 45, 110, 80, 120, 90, 45, 50, 253, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.BUG, Type.PSYCHIC, 14, 40.8, Abilities.TRACE, Abilities.TRACE, Abilities.TRACE, 605, 90, 45, 130, 110, 140, 90, 45, 50, 253), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.BUG, Type.PSYCHIC, 14, 999.9, Abilities.TRACE, Abilities.TRACE, Abilities.TRACE, 605, 90, 45, 130, 110, 140, 90, 45, 50, 253), ), new PokemonSpecies(Species.NICKIT, 8, false, false, false, "Fox Pokémon", Type.DARK, null, 0.6, 8.9, Abilities.RUN_AWAY, Abilities.UNBURDEN, Abilities.STAKEOUT, 245, 40, 28, 28, 47, 52, 50, 255, 50, 49, GrowthRate.FAST, 50, false), new PokemonSpecies(Species.THIEVUL, 8, false, false, false, "Fox Pokémon", Type.DARK, null, 1.2, 19.9, Abilities.RUN_AWAY, Abilities.UNBURDEN, Abilities.STAKEOUT, 455, 70, 58, 58, 87, 92, 90, 127, 50, 159, GrowthRate.FAST, 50, false), @@ -2302,7 +2310,7 @@ export function initSpecies() { new PokemonSpecies(Species.CHEWTLE, 8, false, false, false, "Snapping Pokémon", Type.WATER, null, 0.3, 8.5, Abilities.STRONG_JAW, Abilities.SHELL_ARMOR, Abilities.SWIFT_SWIM, 284, 50, 64, 50, 38, 38, 44, 255, 50, 57, GrowthRate.MEDIUM_FAST, 50, false), new PokemonSpecies(Species.DREDNAW, 8, false, false, false, "Bite Pokémon", Type.WATER, Type.ROCK, 1, 115.5, Abilities.STRONG_JAW, Abilities.SHELL_ARMOR, Abilities.SWIFT_SWIM, 485, 90, 115, 90, 48, 68, 74, 75, 50, 170, GrowthRate.MEDIUM_FAST, 50, false, true, new PokemonForm("Normal", "", Type.WATER, Type.ROCK, 1, 115.5, Abilities.STRONG_JAW, Abilities.SHELL_ARMOR, Abilities.SWIFT_SWIM, 485, 90, 115, 90, 48, 68, 74, 75, 50, 170, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.WATER, Type.ROCK, 24, 115.5, Abilities.STRONG_JAW, Abilities.STRONG_JAW, Abilities.STRONG_JAW, 585, 115, 145, 115, 43, 83, 84, 75, 50, 170), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.WATER, Type.ROCK, 24, 999.9, Abilities.STRONG_JAW, Abilities.STRONG_JAW, Abilities.STRONG_JAW, 585, 115, 145, 115, 43, 83, 84, 75, 50, 170), ), new PokemonSpecies(Species.YAMPER, 8, false, false, false, "Puppy Pokémon", Type.ELECTRIC, null, 0.3, 13.5, Abilities.BALL_FETCH, Abilities.NONE, Abilities.RATTLED, 270, 59, 45, 50, 40, 50, 26, 255, 50, 54, GrowthRate.FAST, 50, false), new PokemonSpecies(Species.BOLTUND, 8, false, false, false, "Dog Pokémon", Type.ELECTRIC, null, 1, 34, Abilities.STRONG_JAW, Abilities.NONE, Abilities.COMPETITIVE, 490, 69, 90, 60, 90, 60, 121, 45, 50, 172, GrowthRate.FAST, 50, false), @@ -2310,21 +2318,21 @@ export function initSpecies() { new PokemonSpecies(Species.CARKOL, 8, false, false, false, "Coal Pokémon", Type.ROCK, Type.FIRE, 1.1, 78, Abilities.STEAM_ENGINE, Abilities.FLAME_BODY, Abilities.FLASH_FIRE, 410, 80, 60, 90, 60, 70, 50, 120, 50, 144, GrowthRate.MEDIUM_SLOW, 50, false), new PokemonSpecies(Species.COALOSSAL, 8, false, false, false, "Coal Pokémon", Type.ROCK, Type.FIRE, 2.8, 310.5, Abilities.STEAM_ENGINE, Abilities.FLAME_BODY, Abilities.FLASH_FIRE, 510, 110, 80, 120, 80, 90, 30, 45, 50, 255, GrowthRate.MEDIUM_SLOW, 50, false, true, new PokemonForm("Normal", "", Type.ROCK, Type.FIRE, 2.8, 310.5, Abilities.STEAM_ENGINE, Abilities.FLAME_BODY, Abilities.FLASH_FIRE, 510, 110, 80, 120, 80, 90, 30, 45, 50, 255, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.ROCK, Type.FIRE, 42, 310.5, Abilities.STEAM_ENGINE, Abilities.STEAM_ENGINE, Abilities.STEAM_ENGINE, 610, 140, 95, 130, 95, 110, 40, 45, 50, 255), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.ROCK, Type.FIRE, 42, 999.9, Abilities.STEAM_ENGINE, Abilities.STEAM_ENGINE, Abilities.STEAM_ENGINE, 610, 140, 95, 130, 95, 110, 40, 45, 50, 255), ), new PokemonSpecies(Species.APPLIN, 8, false, false, false, "Apple Core Pokémon", Type.GRASS, Type.DRAGON, 0.2, 0.5, Abilities.RIPEN, Abilities.GLUTTONY, Abilities.BULLETPROOF, 260, 40, 40, 80, 40, 40, 20, 255, 50, 52, GrowthRate.ERRATIC, 50, false), new PokemonSpecies(Species.FLAPPLE, 8, false, false, false, "Apple Wing Pokémon", Type.GRASS, Type.DRAGON, 0.3, 1, Abilities.RIPEN, Abilities.GLUTTONY, Abilities.HUSTLE, 485, 70, 110, 80, 95, 60, 70, 45, 50, 170, GrowthRate.ERRATIC, 50, false, true, new PokemonForm("Normal", "", Type.GRASS, Type.DRAGON, 0.3, 1, Abilities.RIPEN, Abilities.GLUTTONY, Abilities.HUSTLE, 485, 70, 110, 80, 95, 60, 70, 45, 50, 170, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.GRASS, Type.DRAGON, 24, 1, Abilities.HUSTLE, Abilities.HUSTLE, Abilities.HUSTLE, 585, 90, 130, 100, 85, 80, 100, 45, 50, 170), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.GRASS, Type.DRAGON, 24, 999.9, Abilities.HUSTLE, Abilities.HUSTLE, Abilities.HUSTLE, 585, 90, 130, 100, 85, 80, 100, 45, 50, 170), ), new PokemonSpecies(Species.APPLETUN, 8, false, false, false, "Apple Nectar Pokémon", Type.GRASS, Type.DRAGON, 0.4, 13, Abilities.RIPEN, Abilities.GLUTTONY, Abilities.THICK_FAT, 485, 110, 85, 80, 100, 80, 30, 45, 50, 170, GrowthRate.ERRATIC, 50, false, true, new PokemonForm("Normal", "", Type.GRASS, Type.DRAGON, 0.4, 13, Abilities.RIPEN, Abilities.GLUTTONY, Abilities.THICK_FAT, 485, 110, 85, 80, 100, 80, 30, 45, 50, 170, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.GRASS, Type.DRAGON, 24, 13, Abilities.THICK_FAT, Abilities.THICK_FAT, Abilities.THICK_FAT, 585, 130, 75, 115, 125, 115, 25, 45, 50, 170), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.GRASS, Type.DRAGON, 24, 999.9, Abilities.THICK_FAT, Abilities.THICK_FAT, Abilities.THICK_FAT, 585, 130, 75, 115, 125, 115, 25, 45, 50, 170), ), new PokemonSpecies(Species.SILICOBRA, 8, false, false, false, "Sand Snake Pokémon", Type.GROUND, null, 2.2, 7.6, Abilities.SAND_SPIT, Abilities.SHED_SKIN, Abilities.SAND_VEIL, 315, 52, 57, 75, 35, 50, 46, 255, 50, 63, GrowthRate.MEDIUM_FAST, 50, false), new PokemonSpecies(Species.SANDACONDA, 8, false, false, false, "Sand Snake Pokémon", Type.GROUND, null, 3.8, 65.5, Abilities.SAND_SPIT, Abilities.SHED_SKIN, Abilities.SAND_VEIL, 510, 72, 107, 125, 65, 70, 71, 120, 50, 179, GrowthRate.MEDIUM_FAST, 50, false, true, new PokemonForm("Normal", "", Type.GROUND, null, 3.8, 65.5, Abilities.SAND_SPIT, Abilities.SHED_SKIN, Abilities.SAND_VEIL, 510, 72, 107, 125, 65, 70, 71, 120, 50, 179, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.GROUND, null, 22, 65.5, Abilities.SAND_SPIT, Abilities.SAND_SPIT, Abilities.SAND_SPIT, 610, 117, 137, 140, 55, 80, 81, 120, 50, 179), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.GROUND, null, 22, 999.9, Abilities.SAND_SPIT, Abilities.SAND_SPIT, Abilities.SAND_SPIT, 610, 117, 137, 140, 55, 80, 81, 120, 50, 179), ), new PokemonSpecies(Species.CRAMORANT, 8, false, false, false, "Gulp Pokémon", Type.FLYING, Type.WATER, 0.8, 18, Abilities.GULP_MISSILE, Abilities.NONE, Abilities.NONE, 475, 70, 85, 55, 85, 95, 85, 45, 50, 166, GrowthRate.MEDIUM_FAST, 50, false, false, new PokemonForm("Normal", "", Type.FLYING, Type.WATER, 0.8, 18, Abilities.GULP_MISSILE, Abilities.NONE, Abilities.NONE, 475, 70, 85, 55, 85, 95, 85, 45, 50, 166, false, null, true), @@ -2337,12 +2345,12 @@ export function initSpecies() { new PokemonSpecies(Species.TOXTRICITY, 8, false, false, false, "Punk Pokémon", Type.ELECTRIC, Type.POISON, 1.6, 40, Abilities.PUNK_ROCK, Abilities.PLUS, Abilities.TECHNICIAN, 502, 75, 98, 70, 114, 70, 75, 45, 50, 176, GrowthRate.MEDIUM_SLOW, 50, false, true, new PokemonForm("Amped Form", "amped", Type.ELECTRIC, Type.POISON, 1.6, 40, Abilities.PUNK_ROCK, Abilities.PLUS, Abilities.TECHNICIAN, 502, 75, 98, 70, 114, 70, 75, 45, 50, 176, false, "", true), new PokemonForm("Low-Key Form", "lowkey", Type.ELECTRIC, Type.POISON, 1.6, 40, Abilities.PUNK_ROCK, Abilities.MINUS, Abilities.TECHNICIAN, 502, 75, 98, 70, 114, 70, 75, 45, 50, 176, false, "lowkey", true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.ELECTRIC, Type.POISON, 24, 40, Abilities.PUNK_ROCK, Abilities.PUNK_ROCK, Abilities.PUNK_ROCK, 602, 114, 98, 82, 144, 82, 82, 45, 50, 176), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.ELECTRIC, Type.POISON, 24, 999.9, Abilities.PUNK_ROCK, Abilities.PUNK_ROCK, Abilities.PUNK_ROCK, 602, 114, 98, 82, 144, 82, 82, 45, 50, 176), ), new PokemonSpecies(Species.SIZZLIPEDE, 8, false, false, false, "Radiator Pokémon", Type.FIRE, Type.BUG, 0.7, 1, Abilities.FLASH_FIRE, Abilities.WHITE_SMOKE, Abilities.FLAME_BODY, 305, 50, 65, 45, 50, 50, 45, 190, 50, 61, GrowthRate.MEDIUM_FAST, 50, false), new PokemonSpecies(Species.CENTISKORCH, 8, false, false, false, "Radiator Pokémon", Type.FIRE, Type.BUG, 3, 120, Abilities.FLASH_FIRE, Abilities.WHITE_SMOKE, Abilities.FLAME_BODY, 525, 100, 115, 65, 90, 90, 65, 75, 50, 184, GrowthRate.MEDIUM_FAST, 50, false, true, new PokemonForm("Normal", "", Type.FIRE, Type.BUG, 3, 120, Abilities.FLASH_FIRE, Abilities.WHITE_SMOKE, Abilities.FLAME_BODY, 525, 100, 115, 65, 90, 90, 65, 75, 50, 184, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.FIRE, Type.BUG, 75, 120, Abilities.FLASH_FIRE, Abilities.FLASH_FIRE, Abilities.FLASH_FIRE, 625, 140, 145, 75, 90, 100, 75, 75, 50, 184), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.FIRE, Type.BUG, 75, 999.9, Abilities.FLASH_FIRE, Abilities.FLASH_FIRE, Abilities.FLASH_FIRE, 625, 140, 145, 75, 90, 100, 75, 75, 50, 184), ), new PokemonSpecies(Species.CLOBBOPUS, 8, false, false, false, "Tantrum Pokémon", Type.FIGHTING, null, 0.6, 4, Abilities.LIMBER, Abilities.NONE, Abilities.TECHNICIAN, 310, 50, 68, 60, 50, 50, 32, 180, 50, 62, GrowthRate.MEDIUM_SLOW, 50, false), new PokemonSpecies(Species.GRAPPLOCT, 8, false, false, false, "Jujitsu Pokémon", Type.FIGHTING, null, 1.6, 39, Abilities.LIMBER, Abilities.NONE, Abilities.TECHNICIAN, 480, 80, 118, 90, 70, 80, 42, 45, 50, 168, GrowthRate.MEDIUM_SLOW, 50, false), @@ -2358,13 +2366,13 @@ export function initSpecies() { new PokemonSpecies(Species.HATTREM, 8, false, false, false, "Serene Pokémon", Type.PSYCHIC, null, 0.6, 4.8, Abilities.HEALER, Abilities.ANTICIPATION, Abilities.MAGIC_BOUNCE, 370, 57, 40, 65, 86, 73, 49, 120, 50, 130, GrowthRate.SLOW, 0, false), new PokemonSpecies(Species.HATTERENE, 8, false, false, false, "Silent Pokémon", Type.PSYCHIC, Type.FAIRY, 2.1, 5.1, Abilities.HEALER, Abilities.ANTICIPATION, Abilities.MAGIC_BOUNCE, 510, 57, 90, 95, 136, 103, 29, 45, 50, 255, GrowthRate.SLOW, 0, false, true, new PokemonForm("Normal", "", Type.PSYCHIC, Type.FAIRY, 2.1, 5.1, Abilities.HEALER, Abilities.ANTICIPATION, Abilities.MAGIC_BOUNCE, 510, 57, 90, 95, 136, 103, 29, 45, 50, 255, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.PSYCHIC, Type.FAIRY, 26, 5.1, Abilities.MAGIC_BOUNCE, Abilities.MAGIC_BOUNCE, Abilities.MAGIC_BOUNCE, 610, 97, 90, 105, 146, 122, 50, 45, 50, 255), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.PSYCHIC, Type.FAIRY, 26, 999.9, Abilities.MAGIC_BOUNCE, Abilities.MAGIC_BOUNCE, Abilities.MAGIC_BOUNCE, 610, 97, 90, 105, 146, 122, 50, 45, 50, 255), ), new PokemonSpecies(Species.IMPIDIMP, 8, false, false, false, "Wily Pokémon", Type.DARK, Type.FAIRY, 0.4, 5.5, Abilities.PRANKSTER, Abilities.FRISK, Abilities.PICKPOCKET, 265, 45, 45, 30, 55, 40, 50, 255, 50, 53, GrowthRate.MEDIUM_FAST, 100, false), new PokemonSpecies(Species.MORGREM, 8, false, false, false, "Devious Pokémon", Type.DARK, Type.FAIRY, 0.8, 12.5, Abilities.PRANKSTER, Abilities.FRISK, Abilities.PICKPOCKET, 370, 65, 60, 45, 75, 55, 70, 120, 50, 130, GrowthRate.MEDIUM_FAST, 100, false), new PokemonSpecies(Species.GRIMMSNARL, 8, false, false, false, "Bulk Up Pokémon", Type.DARK, Type.FAIRY, 1.5, 61, Abilities.PRANKSTER, Abilities.FRISK, Abilities.PICKPOCKET, 510, 95, 120, 65, 95, 75, 60, 45, 50, 255, GrowthRate.MEDIUM_FAST, 100, false, true, new PokemonForm("Normal", "", Type.DARK, Type.FAIRY, 1.5, 61, Abilities.PRANKSTER, Abilities.FRISK, Abilities.PICKPOCKET, 510, 95, 120, 65, 95, 75, 60, 45, 50, 255, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.DARK, Type.FAIRY, 32, 61, Abilities.PRANKSTER, Abilities.PRANKSTER, Abilities.PRANKSTER, 610, 135, 138, 77, 110, 85, 65, 45, 50, 255), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.DARK, Type.FAIRY, 32, 999.9, Abilities.PRANKSTER, Abilities.PRANKSTER, Abilities.PRANKSTER, 610, 135, 138, 77, 110, 85, 65, 45, 50, 255), ), new PokemonSpecies(Species.OBSTAGOON, 8, false, false, false, "Blocking Pokémon", Type.DARK, Type.NORMAL, 1.6, 46, Abilities.RECKLESS, Abilities.GUTS, Abilities.DEFIANT, 520, 93, 90, 101, 60, 81, 95, 45, 50, 260, GrowthRate.MEDIUM_FAST, 50, false), new PokemonSpecies(Species.PERRSERKER, 8, false, false, false, "Viking Pokémon", Type.STEEL, null, 0.8, 28, Abilities.BATTLE_ARMOR, Abilities.TOUGH_CLAWS, Abilities.STEELY_SPIRIT, 440, 70, 110, 100, 50, 60, 50, 90, 50, 154, GrowthRate.MEDIUM_FAST, 50, false), @@ -2383,7 +2391,7 @@ export function initSpecies() { new PokemonForm("Ruby Swirl", "ruby-swirl", Type.FAIRY, null, 0.3, 0.5, Abilities.SWEET_VEIL, Abilities.NONE, Abilities.AROMA_VEIL, 495, 65, 60, 75, 110, 121, 64, 100, 50, 173, false, null, true), new PokemonForm("Caramel Swirl", "caramel-swirl", Type.FAIRY, null, 0.3, 0.5, Abilities.SWEET_VEIL, Abilities.NONE, Abilities.AROMA_VEIL, 495, 65, 60, 75, 110, 121, 64, 100, 50, 173, false, null, true), new PokemonForm("Rainbow Swirl", "rainbow-swirl", Type.FAIRY, null, 0.3, 0.5, Abilities.SWEET_VEIL, Abilities.NONE, Abilities.AROMA_VEIL, 495, 65, 60, 75, 110, 121, 64, 100, 50, 173, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.FAIRY, null, 30, 0.5, Abilities.MISTY_SURGE, Abilities.MISTY_SURGE, Abilities.MISTY_SURGE, 595, 135, 60, 75, 130, 131, 64, 100, 50, 173), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.FAIRY, null, 30, 999.9, Abilities.MISTY_SURGE, Abilities.MISTY_SURGE, Abilities.MISTY_SURGE, 595, 135, 60, 75, 130, 131, 64, 100, 50, 173), ), new PokemonSpecies(Species.FALINKS, 8, false, false, false, "Formation Pokémon", Type.FIGHTING, null, 3, 62, Abilities.BATTLE_ARMOR, Abilities.NONE, Abilities.DEFIANT, 470, 65, 100, 100, 70, 60, 75, 45, 50, 165, GrowthRate.MEDIUM_FAST, null, false), new PokemonSpecies(Species.PINCURCHIN, 8, false, false, false, "Sea Urchin Pokémon", Type.ELECTRIC, null, 0.3, 1, Abilities.LIGHTNING_ROD, Abilities.NONE, Abilities.ELECTRIC_SURGE, 435, 48, 101, 95, 91, 85, 15, 75, 50, 152, GrowthRate.MEDIUM_FAST, 50, false), @@ -2405,7 +2413,7 @@ export function initSpecies() { new PokemonSpecies(Species.CUFANT, 8, false, false, false, "Copperderm Pokémon", Type.STEEL, null, 1.2, 100, Abilities.SHEER_FORCE, Abilities.NONE, Abilities.HEAVY_METAL, 330, 72, 80, 49, 40, 49, 40, 190, 50, 66, GrowthRate.MEDIUM_FAST, 50, false), new PokemonSpecies(Species.COPPERAJAH, 8, false, false, false, "Copperderm Pokémon", Type.STEEL, null, 3, 650, Abilities.SHEER_FORCE, Abilities.NONE, Abilities.HEAVY_METAL, 500, 122, 130, 69, 80, 69, 30, 90, 50, 175, GrowthRate.MEDIUM_FAST, 50, false, true, new PokemonForm("Normal", "", Type.STEEL, null, 3, 650, Abilities.SHEER_FORCE, Abilities.NONE, Abilities.HEAVY_METAL, 500, 122, 130, 69, 80, 69, 30, 90, 50, 175, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.STEEL, Type.GROUND, 23, 650, Abilities.MOLD_BREAKER, Abilities.MOLD_BREAKER, Abilities.MOLD_BREAKER, 600, 167, 155, 89, 80, 89, 20, 90, 50, 175), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.STEEL, Type.GROUND, 23, 999.9, Abilities.MOLD_BREAKER, Abilities.MOLD_BREAKER, Abilities.MOLD_BREAKER, 600, 167, 155, 89, 80, 89, 20, 90, 50, 175), ), new PokemonSpecies(Species.DRACOZOLT, 8, false, false, false, "Fossil Pokémon", Type.ELECTRIC, Type.DRAGON, 1.8, 190, Abilities.VOLT_ABSORB, Abilities.HUSTLE, Abilities.SAND_RUSH, 505, 90, 100, 90, 80, 70, 75, 45, 50, 177, GrowthRate.SLOW, null, false), new PokemonSpecies(Species.ARCTOZOLT, 8, false, false, false, "Fossil Pokémon", Type.ELECTRIC, Type.ICE, 2.3, 150, Abilities.VOLT_ABSORB, Abilities.STATIC, Abilities.SLUSH_RUSH, 505, 90, 100, 90, 90, 80, 55, 45, 50, 177, GrowthRate.SLOW, null, false), @@ -2413,7 +2421,7 @@ export function initSpecies() { new PokemonSpecies(Species.ARCTOVISH, 8, false, false, false, "Fossil Pokémon", Type.WATER, Type.ICE, 2, 175, Abilities.WATER_ABSORB, Abilities.ICE_BODY, Abilities.SLUSH_RUSH, 505, 90, 90, 100, 80, 90, 55, 45, 50, 177, GrowthRate.SLOW, null, false), new PokemonSpecies(Species.DURALUDON, 8, false, false, false, "Alloy Pokémon", Type.STEEL, Type.DRAGON, 1.8, 40, Abilities.LIGHT_METAL, Abilities.HEAVY_METAL, Abilities.STALWART, 535, 70, 95, 115, 120, 50, 85, 45, 50, 187, GrowthRate.MEDIUM_FAST, 50, false, true, new PokemonForm("Normal", "", Type.STEEL, Type.DRAGON, 1.8, 40, Abilities.LIGHT_METAL, Abilities.HEAVY_METAL, Abilities.STALWART, 535, 70, 95, 115, 120, 50, 85, 45, 50, 187, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.STEEL, Type.DRAGON, 43, 40, Abilities.LIGHTNING_ROD, Abilities.LIGHTNING_ROD, Abilities.LIGHTNING_ROD, 635, 100, 105, 119, 166, 57, 88, 45, 50, 187), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.STEEL, Type.DRAGON, 43, 999.9, Abilities.LIGHTNING_ROD, Abilities.LIGHTNING_ROD, Abilities.LIGHTNING_ROD, 635, 100, 110, 120, 175, 60, 70, 45, 50, 187), ), new PokemonSpecies(Species.DREEPY, 8, false, false, false, "Lingering Pokémon", Type.DRAGON, Type.GHOST, 0.5, 2, Abilities.CLEAR_BODY, Abilities.INFILTRATOR, Abilities.CURSED_BODY, 270, 28, 60, 30, 40, 30, 82, 45, 50, 54, GrowthRate.SLOW, 50, false), new PokemonSpecies(Species.DRAKLOAK, 8, false, false, false, "Caretaker Pokémon", Type.DRAGON, Type.GHOST, 1.4, 11, Abilities.CLEAR_BODY, Abilities.INFILTRATOR, Abilities.CURSED_BODY, 410, 68, 80, 50, 60, 50, 102, 45, 50, 144, GrowthRate.SLOW, 50, false), @@ -2428,14 +2436,14 @@ export function initSpecies() { ), new PokemonSpecies(Species.ETERNATUS, 8, false, true, false, "Gigantic Pokémon", Type.POISON, Type.DRAGON, 20, 950, Abilities.PRESSURE, Abilities.NONE, Abilities.NONE, 690, 140, 85, 95, 145, 95, 130, 255, 0, 345, GrowthRate.SLOW, null, false, true, new PokemonForm("Normal", "", Type.POISON, Type.DRAGON, 20, 950, Abilities.PRESSURE, Abilities.NONE, Abilities.NONE, 690, 140, 85, 95, 145, 95, 130, 255, 0, 345, false, null, true), - new PokemonForm("E-Max", "eternamax", Type.POISON, Type.DRAGON, 100, 0, Abilities.PRESSURE, Abilities.NONE, Abilities.NONE, 1125, 255, 115, 250, 125, 250, 130, 255, 0, 345), + new PokemonForm("E-Max", "eternamax", Type.POISON, Type.DRAGON, 100, 999.9, Abilities.PRESSURE, Abilities.NONE, Abilities.NONE, 1125, 255, 115, 250, 125, 250, 130, 255, 0, 345), ), new PokemonSpecies(Species.KUBFU, 8, true, false, false, "Wushu Pokémon", Type.FIGHTING, null, 0.6, 12, Abilities.INNER_FOCUS, Abilities.NONE, Abilities.NONE, 385, 60, 90, 60, 53, 50, 72, 3, 50, 77, GrowthRate.SLOW, 87.5, false), new PokemonSpecies(Species.URSHIFU, 8, true, false, false, "Wushu Pokémon", Type.FIGHTING, Type.DARK, 1.9, 105, Abilities.UNSEEN_FIST, Abilities.NONE, Abilities.NONE, 550, 100, 130, 100, 63, 60, 97, 3, 50, 275, GrowthRate.SLOW, 87.5, false, true, new PokemonForm("Single Strike Style", "single-strike", Type.FIGHTING, Type.DARK, 1.9, 105, Abilities.UNSEEN_FIST, Abilities.NONE, Abilities.NONE, 550, 100, 130, 100, 63, 60, 97, 3, 50, 275, false, "", true), new PokemonForm("Rapid Strike Style", "rapid-strike", Type.FIGHTING, Type.WATER, 1.9, 105, Abilities.UNSEEN_FIST, Abilities.NONE, Abilities.NONE, 550, 100, 130, 100, 63, 60, 97, 3, 50, 275, false, null, true), - new PokemonForm("G-Max Single Strike Style", SpeciesFormKey.GIGANTAMAX_SINGLE, Type.FIGHTING, Type.DARK, 29, 105, Abilities.UNSEEN_FIST, Abilities.NONE, Abilities.NONE, 650, 125, 150, 115, 73, 70, 117, 3, 50, 275), - new PokemonForm("G-Max Rapid Strike Style", SpeciesFormKey.GIGANTAMAX_RAPID, Type.FIGHTING, Type.WATER, 26, 105, Abilities.UNSEEN_FIST, Abilities.NONE, Abilities.NONE, 650, 125, 150, 115, 73, 70, 117, 3, 50, 275), + new PokemonForm("G-Max Single Strike Style", SpeciesFormKey.GIGANTAMAX_SINGLE, Type.FIGHTING, Type.DARK, 29, 999.9, Abilities.UNSEEN_FIST, Abilities.NONE, Abilities.NONE, 650, 125, 150, 115, 73, 70, 117, 3, 50, 275), + new PokemonForm("G-Max Rapid Strike Style", SpeciesFormKey.GIGANTAMAX_RAPID, Type.FIGHTING, Type.WATER, 26, 999.9, Abilities.UNSEEN_FIST, Abilities.NONE, Abilities.NONE, 650, 125, 150, 115, 73, 70, 117, 3, 50, 275), ), new PokemonSpecies(Species.ZARUDE, 8, false, false, true, "Rogue Monkey Pokémon", Type.DARK, Type.GRASS, 1.8, 70, Abilities.LEAF_GUARD, Abilities.NONE, Abilities.NONE, 600, 105, 120, 105, 70, 95, 105, 3, 0, 300, GrowthRate.SLOW, null, false, false, new PokemonForm("Normal", "", Type.DARK, Type.GRASS, 1.8, 70, Abilities.LEAF_GUARD, Abilities.NONE, Abilities.NONE, 600, 105, 120, 105, 70, 95, 105, 3, 0, 300, false, null, true), @@ -2537,7 +2545,14 @@ export function initSpecies() { new PokemonForm("Hero Form", "hero", Type.WATER, null, 1.8, 97.4, Abilities.ZERO_TO_HERO, Abilities.NONE, Abilities.ZERO_TO_HERO, 650, 100, 160, 97, 106, 87, 100, 45, 50, 160), ), new PokemonSpecies(Species.VAROOM, 9, false, false, false, "Single-Cyl Pokémon", Type.STEEL, Type.POISON, 1, 35, Abilities.OVERCOAT, Abilities.NONE, Abilities.SLOW_START, 300, 45, 70, 63, 30, 45, 47, 190, 50, 60, GrowthRate.MEDIUM_FAST, 50, false), - new PokemonSpecies(Species.REVAVROOM, 9, false, false, false, "Multi-Cyl Pokémon", Type.STEEL, Type.POISON, 1.8, 120, Abilities.OVERCOAT, Abilities.NONE, Abilities.FILTER, 500, 80, 119, 90, 54, 67, 90, 75, 50, 175, GrowthRate.MEDIUM_FAST, 50, false), + new PokemonSpecies(Species.REVAVROOM, 9, false, false, false, "Multi-Cyl Pokémon", Type.STEEL, Type.POISON, 1.8, 120, Abilities.OVERCOAT, Abilities.NONE, Abilities.FILTER, 500, 80, 119, 90, 54, 67, 90, 75, 50, 175, GrowthRate.MEDIUM_FAST, 50, false, false, + new PokemonForm("Normal", "", Type.STEEL, Type.POISON, 1.8, 120, Abilities.OVERCOAT, Abilities.NONE, Abilities.FILTER, 500, 80, 119, 90, 54, 67, 90, 75, 50, 175, false, null, true), + new PokemonForm("Segin Starmobile", "segin-starmobile", Type.STEEL, Type.DARK, 1.8, 240, Abilities.INTIMIDATE, Abilities.NONE, Abilities.INTIMIDATE, 600, 120, 129, 100, 59, 77, 115, 75, 50, 175), + new PokemonForm("Schedar Starmobile", "schedar-starmobile", Type.STEEL, Type.FIRE, 1.8, 240, Abilities.SPEED_BOOST, Abilities.NONE, Abilities.SPEED_BOOST, 600, 120, 129, 100, 59, 77, 115, 75, 50, 175), + new PokemonForm("Navi Starmobile", "navi-starmobile", Type.STEEL, Type.POISON, 1.8, 240, Abilities.TOXIC_DEBRIS, Abilities.NONE, Abilities.TOXIC_DEBRIS, 600, 120, 129, 100, 59, 77, 115, 75, 50, 175), + new PokemonForm("Ruchbah Starmobile", "ruchbah-starmobile", Type.STEEL, Type.FAIRY, 1.8, 240, Abilities.MISTY_SURGE, Abilities.NONE, Abilities.MISTY_SURGE, 600, 120, 129, 100, 59, 77, 115, 75, 50, 175), + new PokemonForm("Caph Starmobile", "caph-starmobile", Type.STEEL, Type.FIGHTING, 1.8, 240, Abilities.STAMINA, Abilities.NONE, Abilities.STAMINA, 600, 120, 129, 100, 59, 77, 115, 75, 50, 175), + ), new PokemonSpecies(Species.CYCLIZAR, 9, false, false, false, "Mount Pokémon", Type.DRAGON, Type.NORMAL, 1.6, 63, Abilities.SHED_SKIN, Abilities.NONE, Abilities.REGENERATOR, 501, 70, 95, 65, 85, 65, 121, 190, 50, 175, GrowthRate.MEDIUM_SLOW, 50, false), new PokemonSpecies(Species.ORTHWORM, 9, false, false, false, "Earthworm Pokémon", Type.STEEL, null, 2.5, 310, Abilities.EARTH_EATER, Abilities.NONE, Abilities.SAND_VEIL, 480, 70, 85, 145, 60, 55, 65, 25, 50, 240, GrowthRate.SLOW, 50, false), new PokemonSpecies(Species.GLIMMET, 9, false, false, false, "Ore Pokémon", Type.ROCK, Type.POISON, 0.7, 8, Abilities.TOXIC_DEBRIS, Abilities.NONE, Abilities.CORROSION, 350, 48, 35, 42, 105, 60, 60, 70, 50, 70, GrowthRate.MEDIUM_SLOW, 50, false), @@ -3340,6 +3355,7 @@ export function getStarterValueFriendshipCap(value: integer): integer { } } +export const POKERUS_STARTER_COUNT = 5; //adjust here! /** * Method to get the daily list of starters with Pokerus. * @param scene {@linkcode BattleScene} used as part of RNG @@ -3348,10 +3364,9 @@ export function getStarterValueFriendshipCap(value: integer): integer { export function getPokerusStarters(scene: BattleScene): PokemonSpecies[] { const pokerusStarters: PokemonSpecies[] = []; const date = new Date(); - const starterCount = 3; //for easy future adjustment! date.setUTCHours(0, 0, 0, 0); scene.executeWithSeedOffset(() => { - while (pokerusStarters.length < starterCount) { + while (pokerusStarters.length < POKERUS_STARTER_COUNT) { const randomSpeciesId = parseInt(Utils.randSeedItem(Object.keys(speciesStarters)), 10); const species = getPokemonSpecies(randomSpeciesId); if (!pokerusStarters.includes(species)) { @@ -3388,7 +3403,7 @@ export const starterPassiveAbilities = { [Species.POLIWAG]: Abilities.NO_GUARD, [Species.ABRA]: Abilities.PSYCHIC_SURGE, [Species.MACHOP]: Abilities.QUICK_FEET, - [Species.BELLSPROUT]: Abilities.PROTOSYNTHESIS, + [Species.BELLSPROUT]: Abilities.FLOWER_GIFT, [Species.TENTACOOL]: Abilities.TOXIC_CHAIN, [Species.GEODUDE]: Abilities.DRY_SKIN, [Species.PONYTA]: Abilities.MAGIC_GUARD, @@ -3416,7 +3431,7 @@ export const starterPassiveAbilities = { [Species.STARYU]: Abilities.REGENERATOR, [Species.SCYTHER]: Abilities.TINTED_LENS, [Species.PINSIR]: Abilities.TINTED_LENS, - [Species.TAUROS]: Abilities.SCRAPPY, + [Species.TAUROS]: Abilities.STAMINA, [Species.MAGIKARP]: Abilities.MULTISCALE, [Species.LAPRAS]: Abilities.LIGHTNING_ROD, [Species.DITTO]: Abilities.ADAPTABILITY, @@ -3484,7 +3499,7 @@ export const starterPassiveAbilities = { [Species.LARVITAR]: Abilities.SAND_RUSH, [Species.LUGIA]: Abilities.DELTA_STREAM, [Species.HO_OH]: Abilities.MAGIC_GUARD, - [Species.CELEBI]: Abilities.GRASSY_SURGE, + [Species.CELEBI]: Abilities.PSYCHIC_SURGE, [Species.TREECKO]: Abilities.TINTED_LENS, [Species.TORCHIC]: Abilities.RECKLESS, [Species.MUDKIP]: Abilities.DRIZZLE, @@ -3622,7 +3637,7 @@ export const starterPassiveAbilities = { [Species.PANPOUR]: Abilities.SAP_SIPPER, [Species.MUNNA]: Abilities.NEUTRALIZING_GAS, [Species.PIDOVE]: Abilities.SNIPER, - [Species.BLITZLE]: Abilities.RECKLESS, + [Species.BLITZLE]: Abilities.ELECTRIC_SURGE, [Species.ROGGENROLA]: Abilities.SOLID_ROCK, [Species.WOOBAT]: Abilities.OPPORTUNIST, [Species.DRILBUR]: Abilities.SAND_STREAM, @@ -3822,7 +3837,7 @@ export const starterPassiveAbilities = { [Species.DURALUDON]: Abilities.STEELWORKER, [Species.DREEPY]: Abilities.PARENTAL_BOND, [Species.ZACIAN]: Abilities.UNNERVE, - [Species.ZAMAZENTA]: Abilities.STAMINA, + [Species.ZAMAZENTA]: Abilities.UNNERVE, [Species.ETERNATUS]: Abilities.NEUTRALIZING_GAS, [Species.KUBFU]: Abilities.IRON_FIST, [Species.ZARUDE]: Abilities.TOUGH_CLAWS, @@ -3854,7 +3869,7 @@ export const starterPassiveAbilities = { [Species.KLAWF]: Abilities.WATER_ABSORB, [Species.CAPSAKID]: Abilities.PARENTAL_BOND, [Species.RELLOR]: Abilities.PRANKSTER, - [Species.FLITTLE]: Abilities.MAGIC_BOUNCE, + [Species.FLITTLE]: Abilities.DAZZLING, [Species.TINKATINK]: Abilities.STEELWORKER, [Species.WIGLETT]: Abilities.STURDY, [Species.BOMBIRDIER]: Abilities.UNBURDEN, @@ -3905,7 +3920,7 @@ export const starterPassiveAbilities = { [Species.TERAPAGOS]: Abilities.SOUL_HEART, [Species.PECHARUNT]: Abilities.TOXIC_CHAIN, [Species.ALOLA_RATTATA]: Abilities.ADAPTABILITY, - [Species.ALOLA_SANDSHREW]: Abilities.TOUGH_CLAWS, + [Species.ALOLA_SANDSHREW]: Abilities.ICE_SCALES, [Species.ALOLA_VULPIX]: Abilities.SHEER_FORCE, [Species.ALOLA_DIGLETT]: Abilities.STURDY, [Species.ALOLA_MEOWTH]: Abilities.DARK_AURA, diff --git a/src/data/splash-messages.ts b/src/data/splash-messages.ts index 8e95bba0591..b8069f77737 100644 --- a/src/data/splash-messages.ts +++ b/src/data/splash-messages.ts @@ -1,46 +1,136 @@ -import i18next from "i18next"; +import { USE_SEASONAL_SPLASH_MESSAGES } from "#app/constants"; -export function getBattleCountSplashMessage(): string { - return `{COUNT} ${i18next.t("splashMessages:battlesWon")}`; +//#region Interfaces/Types + +type Month = "01" | "02" | "03" | "04" | "05" | "06" | "07" | "08" | "09" | "10" | "11" | "12"; +type Day = + | Month + | "13" + | "14" + | "15" + | "16" + | "17" + | "18" + | "19" + | "20" + | "21" + | "22" + | "23" + | "24" + | "25" + | "26" + | "27" + | "28" + | "29" + | "30" + | "31"; + +/** + * Represents a season with its {@linkcode name}, + * {@linkcode start} day+month, {@linkcode end} day+month + * and {@linkcode messages}. + */ +interface Season { + /** The name of the season (internal use only) */ + name: string; + /** The start day and month of the season. Format `MM-DD` */ + start: `${Month}-${Day}`; + /** The end day and month of the season. Format `MM-DD` */ + end: `${Month}-${Day}`; + /** Collection of the messages to display (without the `i18next.t()` call!) */ + messages: string[]; } +//#region Constants + +/** The weight multiplier for the battles-won splash message */ +const BATTLES_WON_WEIGHT_MULTIPLIER = 10; +/** The weight multiplier for the seasonal splash messages */ +const SEASONAL_WEIGHT_MULTIPLIER = 10; + +//#region Common Messages + +const commonSplashMessages = [ + ...Array(BATTLES_WON_WEIGHT_MULTIPLIER).fill("battlesWon"), + "joinTheDiscord", + "infiniteLevels", + "everythingStacks", + "optionalSaveScumming", + "biomes", + "openSource", + "playWithSpeed", + "liveBugTesting", + "heavyInfluence", + "pokemonRiskAndPokemonRain", + "nowWithMoreSalt", + "infiniteFusionAtHome", + "brokenEggMoves", + "magnificent", + "mubstitute", + "thatsCrazy", + "oranceJuice", + "questionableBalancing", + "coolShaders", + "aiFree", + "suddenDifficultySpikes", + "basedOnAnUnfinishedFlashGame", + "moreAddictiveThanIntended", + "mostlyConsistentSeeds", + "achievementPointsDontDoAnything", + "youDoNotStartAtLevel", + "dontTalkAboutTheManaphyEggIncident", + "alsoTryPokengine", + "alsoTryEmeraldRogue", + "alsoTryRadicalRed", + "eeveeExpo", + "ynoproject", + "breedersInSpace", +]; + +//#region Seasonal Messages + +const seasonalSplashMessages: Season[] = [ + { + name: "Halloween", + start: "09-15", + end: "10-31", + messages: ["halloween.pumpkaboosAbout", "halloween.mayContainSpiders", "halloween.spookyScaryDuskulls"], + }, + { + name: "XMAS", + start: "12-01", + end: "12-26", + messages: ["xmas.happyHolidays", "xmas.delibirdSeason"], + }, + { + name: "New Year's", + start: "01-01", + end: "01-31", + messages: ["newYears.happyNewYear"], + }, +]; + +//#endregion + export function getSplashMessages(): string[] { - const splashMessages = Array(10).fill(getBattleCountSplashMessage()); - splashMessages.push( - i18next.t("splashMessages:joinTheDiscord"), - i18next.t("splashMessages:infiniteLevels"), - i18next.t("splashMessages:everythingStacks"), - i18next.t("splashMessages:optionalSaveScumming"), - i18next.t("splashMessages:biomes"), - i18next.t("splashMessages:openSource"), - i18next.t("splashMessages:playWithSpeed"), - i18next.t("splashMessages:liveBugTesting"), - i18next.t("splashMessages:heavyInfluence"), - i18next.t("splashMessages:pokemonRiskAndPokemonRain"), - i18next.t("splashMessages:nowWithMoreSalt"), - i18next.t("splashMessages:infiniteFusionAtHome"), - i18next.t("splashMessages:brokenEggMoves"), - i18next.t("splashMessages:magnificent"), - i18next.t("splashMessages:mubstitute"), - i18next.t("splashMessages:thatsCrazy"), - i18next.t("splashMessages:oranceJuice"), - i18next.t("splashMessages:questionableBalancing"), - i18next.t("splashMessages:coolShaders"), - i18next.t("splashMessages:aiFree"), - i18next.t("splashMessages:suddenDifficultySpikes"), - i18next.t("splashMessages:basedOnAnUnfinishedFlashGame"), - i18next.t("splashMessages:moreAddictiveThanIntended"), - i18next.t("splashMessages:mostlyConsistentSeeds"), - i18next.t("splashMessages:achievementPointsDontDoAnything"), - i18next.t("splashMessages:youDoNotStartAtLevel"), - i18next.t("splashMessages:dontTalkAboutTheManaphyEggIncident"), - i18next.t("splashMessages:alsoTryPokengine"), - i18next.t("splashMessages:alsoTryEmeraldRogue"), - i18next.t("splashMessages:alsoTryRadicalRed"), - i18next.t("splashMessages:eeveeExpo"), - i18next.t("splashMessages:ynoproject"), - i18next.t("splashMessages:breedersInSpace"), - ); + const splashMessages: string[] = [...commonSplashMessages]; + console.log("use seasonal splash messages", USE_SEASONAL_SPLASH_MESSAGES); + if (USE_SEASONAL_SPLASH_MESSAGES) { + // add seasonal splash messages if the season is active + for (const { name, start, end, messages } of seasonalSplashMessages) { + const now = new Date(); + const startDate = new Date(`${start}-${now.getFullYear()}`); + const endDate = new Date(`${end}-${now.getFullYear()}`); - return splashMessages; + if (now >= startDate && now <= endDate) { + console.log(`Adding ${messages.length} ${name} splash messages (weight: x${SEASONAL_WEIGHT_MULTIPLIER})`); + messages.forEach((message) => { + const weightedMessage = Array(SEASONAL_WEIGHT_MULTIPLIER).fill(message); + splashMessages.push(...weightedMessage); + }); + } + } + } + + return splashMessages.map((message) => `splashMessages:${message}`); } diff --git a/src/data/trainer-config.ts b/src/data/trainer-config.ts index 07722a5a206..62f9589b7a3 100644 --- a/src/data/trainer-config.ts +++ b/src/data/trainer-config.ts @@ -1,16 +1,16 @@ -import BattleScene, {startingWave} from "../battle-scene"; -import {ModifierTypeFunc, modifierTypes} from "../modifier/modifier-type"; -import {EnemyPokemon} from "../field/pokemon"; +import BattleScene, { startingWave } from "../battle-scene"; +import { ModifierTypeFunc, modifierTypes } from "../modifier/modifier-type"; +import { EnemyPokemon, PokemonMove } from "../field/pokemon"; import * as Utils from "../utils"; -import {PokeballType} from "./pokeball"; -import {pokemonEvolutions, pokemonPrevolutions} from "./pokemon-evolutions"; -import PokemonSpecies, {getPokemonSpecies, PokemonSpeciesFilter} from "./pokemon-species"; -import {tmSpecies} from "./tms"; -import {Type} from "./type"; -import {doubleBattleDialogue} from "./dialogue"; -import {PersistentModifier} from "../modifier/modifier"; -import {TrainerVariant} from "../field/trainer"; -import {getIsInitialized, initI18n} from "#app/plugins/i18n"; +import { PokeballType } from "./pokeball"; +import { pokemonEvolutions, pokemonPrevolutions } from "./pokemon-evolutions"; +import PokemonSpecies, { getPokemonSpecies, PokemonSpeciesFilter } from "./pokemon-species"; +import { tmSpecies } from "./tms"; +import { Type } from "./type"; +import { doubleBattleDialogue } from "./dialogue"; +import { PersistentModifier } from "../modifier/modifier"; +import { TrainerVariant } from "../field/trainer"; +import { getIsInitialized, initI18n } from "#app/plugins/i18n"; import i18next from "i18next"; import {Moves} from "#enums/moves"; import {PartyMemberStrength} from "#enums/party-member-strength"; @@ -255,7 +255,9 @@ export class TrainerConfig { name = i18next.t("trainerNames:rival"); } } + this.name = name; + return this; } @@ -333,6 +335,9 @@ export class TrainerConfig { case TrainerType.ROSE_2: trainerType = TrainerType.ROSE; break; + case TrainerType.PENNY_2: + trainerType = TrainerType.PENNY; + break; case TrainerType.MARNIE_ELITE: trainerType = TrainerType.MARNIE; break; @@ -355,9 +360,9 @@ export class TrainerConfig { /** * Sets the configuration for trainers with genders, including the female name and encounter background music (BGM). - * @param {string} [nameFemale] - The name of the female trainer. If 'Ivy', a localized name will be assigned. - * @param {TrainerType | string} [femaleEncounterBgm] - The encounter BGM for the female trainer, which can be a TrainerType or a string. - * @returns {TrainerConfig} - The updated TrainerConfig instance. + * @param {string} [nameFemale] The name of the female trainer. If 'Ivy', a localized name will be assigned. + * @param {TrainerType | string} [femaleEncounterBgm] The encounter BGM for the female trainer, which can be a TrainerType or a string. + * @returns {TrainerConfig} The updated TrainerConfig instance. **/ setHasGenders(nameFemale?: string, femaleEncounterBgm?: TrainerType | string): TrainerConfig { // If the female name is 'Ivy' (the rival), assign a localized name. @@ -392,9 +397,9 @@ export class TrainerConfig { /** * Sets the configuration for trainers with double battles, including the name of the double trainer and the encounter BGM. - * @param nameDouble - The name of the double trainer (e.g., "Ace Duo" for Trainer Class Doubles or "red_blue_double" for NAMED trainer doubles). - * @param doubleEncounterBgm - The encounter BGM for the double trainer, which can be a TrainerType or a string. - * @returns {TrainerConfig} - The updated TrainerConfig instance. + * @param nameDouble The name of the double trainer (e.g., "Ace Duo" for Trainer Class Doubles or "red_blue_double" for NAMED trainer doubles). + * @param doubleEncounterBgm The encounter BGM for the double trainer, which can be a TrainerType or a string. + * @returns {TrainerConfig} The updated TrainerConfig instance. */ setHasDouble(nameDouble: string, doubleEncounterBgm?: TrainerType | string): TrainerConfig { this.hasDouble = true; @@ -407,8 +412,8 @@ export class TrainerConfig { /** * Sets the trainer type for double battles. - * @param trainerTypeDouble - The TrainerType of the partner in a double battle. - * @returns {TrainerConfig} - The updated TrainerConfig instance. + * @param trainerTypeDouble The TrainerType of the partner in a double battle. + * @returns {TrainerConfig} The updated TrainerConfig instance. */ setDoubleTrainerType(trainerTypeDouble: TrainerType): TrainerConfig { this.trainerTypeDouble = trainerTypeDouble; @@ -432,8 +437,8 @@ export class TrainerConfig { /** * Sets the title for double trainers - * @param titleDouble - the key for the title in the i18n file. (e.g., "champion_double"). - * @returns {TrainerConfig} - The updated TrainerConfig instance. + * @param titleDouble The key for the title in the i18n file. (e.g., "champion_double"). + * @returns {TrainerConfig} The updated TrainerConfig instance. */ setDoubleTitle(titleDouble: string): TrainerConfig { // First check if i18n is initialized @@ -617,6 +622,41 @@ export class TrainerConfig { [TrainerPoolTier.RARE]: [Species.TINKATINK, Species.HISUI_LILLIGANT] }; } + case "star_1": { + return { + [TrainerPoolTier.COMMON]: [ Species.MURKROW, Species.SEEDOT, Species.CACNEA, Species.STUNKY, Species.SANDILE, Species.NYMBLE, Species.MASCHIFF, Species.GALAR_ZIGZAGOON ], + [TrainerPoolTier.UNCOMMON]: [ Species.UMBREON, Species.SNEASEL, Species.CORPHISH, Species.ZORUA, Species.INKAY, Species.BOMBIRDIER ], + [TrainerPoolTier.RARE]: [ Species.DEINO, Species.SPRIGATITO ] + }; + } + case "star_2": { + return { + [TrainerPoolTier.COMMON]: [ Species.GROWLITHE, Species.HOUNDOUR, Species.NUMEL, Species.LITWICK, Species.FLETCHLING, Species.LITLEO, Species.ROLYCOLY, Species.CAPSAKID ], + [TrainerPoolTier.UNCOMMON]: [ Species.PONYTA, Species.FLAREON, Species.MAGBY, Species.TORKOAL, Species.SALANDIT, Species.TURTONATOR ], + [TrainerPoolTier.RARE]: [ Species.LARVESTA, Species.FUECOCO ] + }; + } + case "star_3": { + return { + [TrainerPoolTier.COMMON]: [ Species.ZUBAT, Species.GRIMER, Species.STUNKY, Species.FOONGUS, Species.MAREANIE, Species.TOXEL, Species.SHROODLE, Species.PALDEA_WOOPER ], + [TrainerPoolTier.UNCOMMON]: [ Species.GASTLY, Species.SEVIPER, Species.SKRELP, Species.ALOLA_GRIMER, Species.GALAR_SLOWPOKE, Species.HISUI_QWILFISH ], + [TrainerPoolTier.RARE]: [ Species.BULBASAUR, Species.GLIMMET ] + }; + } + case "star_4": { + return { + [TrainerPoolTier.COMMON]: [ Species.CLEFFA, Species.IGGLYBUFF, Species.AZURILL, Species.COTTONEE, Species.FLABEBE, Species.HATENNA, Species.IMPIDIMP, Species.TINKATINK ], + [TrainerPoolTier.UNCOMMON]: [ Species.TOGEPI, Species.GARDEVOIR, Species.SYLVEON, Species.KLEFKI, Species.MIMIKYU, Species.ALOLA_VULPIX ], + [TrainerPoolTier.RARE]: [ Species.POPPLIO, Species.GALAR_PONYTA ] + }; + } + case "star_5": { + return { + [TrainerPoolTier.COMMON]: [ Species.SHROOMISH, Species.MAKUHITA, Species.MEDITITE, Species.CROAGUNK, Species.SCRAGGY, Species.MIENFOO, Species.PAWMI, Species.PALDEA_TAUROS ], + [TrainerPoolTier.UNCOMMON]: [ Species.RIOLU, Species.TIMBURR, Species.HAWLUCHA, Species.PASSIMIAN, Species.FALINKS, Species.FLAMIGO ], + [TrainerPoolTier.RARE]: [ Species.JANGMO_O, Species.QUAXLY ] + }; + } } console.warn(`Evil team admin for ${team} not found. Returning empty species pools.`); @@ -625,10 +665,10 @@ export class TrainerConfig { /** * Initializes the trainer configuration for an evil team admin. - * @param title - The title of the evil team admin. - * @param poolName - The evil team the admin belongs to. - * @param {Species | Species[]} signatureSpecies - The signature species for the evil team leader. - * @returns {TrainerConfig} - The updated TrainerConfig instance. + * @param title The title of the evil team admin. + * @param poolName The evil team the admin belongs to. + * @param {Species | Species[]} signatureSpecies The signature species for the evil team leader. + * @returns {TrainerConfig} The updated TrainerConfig instance. * **/ initForEvilTeamAdmin(title: string, poolName: string, signatureSpecies: (Species | Species[])[],): TrainerConfig { if (!getIsInitialized()) { @@ -659,12 +699,49 @@ export class TrainerConfig { return this; } + /** + * Initializes the trainer configuration for a Stat Trainer, as part of the Trainer's Test Mystery Encounter. + * @param {Species | Species[]} signatureSpecies The signature species for the Elite Four member. + * @param {Type[]} specialtyTypes The specialty types for the Stat Trainer. + * @param isMale Whether the Elite Four Member is Male or Female (for localization of the title). + * @returns {TrainerConfig} The updated TrainerConfig instance. + **/ + initForStatTrainer(signatureSpecies: (Species | Species[])[], isMale: boolean, ...specialtyTypes: Type[]): TrainerConfig { + if (!getIsInitialized()) { + initI18n(); + } + + this.setPartyTemplates(trainerPartyTemplates.ELITE_FOUR); + + signatureSpecies.forEach((speciesPool, s) => { + if (!Array.isArray(speciesPool)) { + speciesPool = [speciesPool]; + } + this.setPartyMemberFunc(-(s + 1), getRandomPartyMemberFunc(speciesPool)); + }); + if (specialtyTypes.length) { + this.setSpeciesFilter(p => specialtyTypes.find(t => p.isOfType(t)) !== undefined); + this.setSpecialtyTypes(...specialtyTypes); + } + const nameForCall = this.name.toLowerCase().replace(/\s/g, "_"); + this.name = i18next.t(`trainerNames:${nameForCall}`); + this.setMoneyMultiplier(2); + this.setBoss(); + this.setStaticParty(); + + // TODO: replace with more suitable music? + this.setBattleBgm("battle_trainer"); + this.setVictoryBgm("victory_trainer"); + + return this; + } + /** * Initializes the trainer configuration for an evil team leader. Temporarily hardcoding evil leader teams though. - * @param {Species | Species[]} signatureSpecies - The signature species for the evil team leader. - * @param {Type[]} specialtyTypes - The specialty types for the evil team Leader. - * @param boolean whether or not this is the rematch fight - * @returns {TrainerConfig} - The updated TrainerConfig instance. + * @param {Species | Species[]} signatureSpecies The signature species for the evil team leader. + * @param {Type[]} specialtyTypes The specialty types for the evil team Leader. + * @param boolean Whether or not this is the rematch fight + * @returns {TrainerConfig} The updated TrainerConfig instance. * **/ initForEvilTeamLeader(title: string, signatureSpecies: (Species | Species[])[], rematch: boolean = false, ...specialtyTypes: Type[]): TrainerConfig { if (!getIsInitialized()) { @@ -700,10 +777,10 @@ export class TrainerConfig { /** * Initializes the trainer configuration for a Gym Leader. - * @param {Species | Species[]} signatureSpecies - The signature species for the Gym Leader. - * @param {Type[]} specialtyTypes - The specialty types for the Gym Leader. - * @param isMale - Whether the Gym Leader is Male or Not (for localization of the title). - * @returns {TrainerConfig} - The updated TrainerConfig instance. + * @param {Species | Species[]} signatureSpecies The signature species for the Gym Leader. + * @param {Type[]} specialtyTypes The specialty types for the Gym Leader. + * @param isMale Whether the Gym Leader is Male or Not (for localization of the title). + * @returns {TrainerConfig} The updated TrainerConfig instance. * **/ initForGymLeader(signatureSpecies: (Species | Species[])[], isMale: boolean, ...specialtyTypes: Type[]): TrainerConfig { // Check if the internationalization (i18n) system is initialized. @@ -757,10 +834,10 @@ export class TrainerConfig { /** * Initializes the trainer configuration for an Elite Four member. - * @param {Species | Species[]} signatureSpecies - The signature species for the Elite Four member. - * @param {Type[]} specialtyTypes - The specialty types for the Elite Four member. - * @param isMale - Whether the Elite Four Member is Male or Female (for localization of the title). - * @returns {TrainerConfig} - The updated TrainerConfig instance. + * @param {Species | Species[]} signatureSpecies The signature species for the Elite Four member. + * @param {Type[]} specialtyTypes The specialty types for the Elite Four member. + * @param isMale Whether the Elite Four Member is Male or Female (for localization of the title). + * @returns {TrainerConfig} The updated TrainerConfig instance. **/ initForEliteFour(signatureSpecies: (Species | Species[])[], isMale: boolean, ...specialtyTypes: Type[]): TrainerConfig { // Check if the internationalization (i18n) system is initialized. @@ -813,9 +890,9 @@ export class TrainerConfig { /** * Initializes the trainer configuration for a Champion. - * @param {Species | Species[]} signatureSpecies - The signature species for the Champion. - * @param isMale - Whether the Champion is Male or Female (for localization of the title). - * @returns {TrainerConfig} - The updated TrainerConfig instance. + * @param {Species | Species[]} signatureSpecies The signature species for the Champion. + * @param isMale Whether the Champion is Male or Female (for localization of the title). + * @returns {TrainerConfig} The updated TrainerConfig instance. **/ initForChampion(signatureSpecies: (Species | Species[])[], isMale: boolean): TrainerConfig { // Check if the internationalization (i18n) system is initialized. @@ -862,6 +939,20 @@ export class TrainerConfig { return this; } + /** + * Sets a localized name for the trainer. This should only be used for trainers that dont use a "initFor" function and are considered "named" trainers + * @param name - The name of the trainer. + * @returns {TrainerConfig} The updated TrainerConfig instance. + */ + setLocalizedName(name: string): TrainerConfig { + // Check if the internationalization (i18n) system is initialized. + if (!getIsInitialized()) { + initI18n(); + } + this.name = i18next.t(`trainerNames:${name.toLowerCase().replace(/\s/g, "_")}`); + return this; + } + /** * Retrieves the title for the trainer based on the provided trainer slot and variant. * @param {TrainerSlot} trainerSlot - The slot to determine which title to use. Defaults to TrainerSlot.NONE. @@ -956,6 +1047,66 @@ export class TrainerConfig { } }); } + + /** + * Creates a shallow copy of a trainer config so that it can be modified without affecting the {@link trainerConfigs} source map + */ + clone(): TrainerConfig { + let clone = new TrainerConfig(this.trainerType); + clone = this.trainerTypeDouble ? clone.setDoubleTrainerType(this.trainerTypeDouble) : clone; + clone = this.name ? clone.setName(this.name) : clone; + clone = this.hasGenders ? clone.setHasGenders(this.nameFemale, this.femaleEncounterBgm) : clone; + clone = this.hasDouble ? clone.setHasDouble(this.nameDouble, this.doubleEncounterBgm) : clone; + clone = this.title ? clone.setTitle(this.title) : clone; + clone = this.titleDouble ? clone.setDoubleTitle(this.titleDouble) : clone; + clone = this.hasCharSprite ? clone.setHasCharSprite() : clone; + clone = this.doubleOnly ? clone.setDoubleOnly() : clone; + clone = this.moneyMultiplier ? clone.setMoneyMultiplier(this.moneyMultiplier) : clone; + clone = this.isBoss ? clone.setBoss() : clone; + clone = this.hasStaticParty ? clone.setStaticParty() : clone; + clone = this.useSameSeedForAllMembers ? clone.setUseSameSeedForAllMembers() : clone; + clone = this.battleBgm ? clone.setBattleBgm(this.battleBgm) : clone; + clone = this.encounterBgm ? clone.setEncounterBgm(this.encounterBgm) : clone; + clone = this.victoryBgm ? clone.setVictoryBgm(this.victoryBgm) : clone; + clone = this.genModifiersFunc ? clone.setGenModifiersFunc(this.genModifiersFunc) : clone; + + if (this.modifierRewardFuncs) { + // Clones array instead of passing ref + clone.modifierRewardFuncs = this.modifierRewardFuncs.slice(0); + } + + if (this.partyTemplates) { + clone.partyTemplates = this.partyTemplates.slice(0); + } + + clone = this.partyTemplateFunc ? clone.setPartyTemplateFunc(this.partyTemplateFunc) : clone; + + if (this.partyMemberFuncs) { + Object.keys(this.partyMemberFuncs).forEach((index) => { + clone = clone.setPartyMemberFunc(parseInt(index, 10), this.partyMemberFuncs[index]); + }); + } + + clone = this.speciesPools ? clone.setSpeciesPools(this.speciesPools) : clone; + clone = this.speciesFilter ? clone.setSpeciesFilter(this.speciesFilter) : clone; + if (this.specialtyTypes) { + clone.specialtyTypes = this.specialtyTypes.slice(0); + } + + clone.encounterMessages = this.encounterMessages?.slice(0); + clone.victoryMessages = this.victoryMessages?.slice(0); + clone.defeatMessages = this.defeatMessages?.slice(0); + + clone.femaleEncounterMessages = this.femaleEncounterMessages?.slice(0); + clone.femaleVictoryMessages = this.femaleVictoryMessages?.slice(0); + clone.femaleDefeatMessages = this.femaleDefeatMessages?.slice(0); + + clone.doubleEncounterMessages = this.doubleEncounterMessages?.slice(0); + clone.doubleVictoryMessages = this.doubleVictoryMessages?.slice(0); + clone.doubleDefeatMessages = this.doubleDefeatMessages?.slice(0); + + return clone; + } } let t = 0; @@ -992,8 +1143,16 @@ function getGymLeaderPartyTemplate(scene: BattleScene) { return getWavePartyTemplate(scene, trainerPartyTemplates.GYM_LEADER_1, trainerPartyTemplates.GYM_LEADER_2, trainerPartyTemplates.GYM_LEADER_3, trainerPartyTemplates.GYM_LEADER_4, trainerPartyTemplates.GYM_LEADER_5); } -function getRandomPartyMemberFunc(speciesPool: Species[], trainerSlot: TrainerSlot = TrainerSlot.TRAINER, ignoreEvolution: boolean = false, postProcess?: (enemyPokemon: EnemyPokemon) => void): PartyMemberFunc { - return (scene: BattleScene, level: integer, strength: PartyMemberStrength) => { +/** + * Randomly selects one of the `Species` from `speciesPool`, determines its evolution, level, and strength. + * Then adds Pokemon to scene. + * @param speciesPool + * @param trainerSlot + * @param ignoreEvolution + * @param postProcess + */ +export function getRandomPartyMemberFunc(speciesPool: Species[], trainerSlot: TrainerSlot = TrainerSlot.TRAINER, ignoreEvolution: boolean = false, postProcess?: (enemyPokemon: EnemyPokemon) => void) { + return (scene: BattleScene, level: number, strength: PartyMemberStrength) => { let species = Utils.randSeedItem(speciesPool); if (!ignoreEvolution) { species = getPokemonSpecies(species).getTrainerSpeciesForLevel(level, true, strength, scene.currentBattle.waveIndex); @@ -1157,7 +1316,7 @@ export const signatureSpecies: SignatureSpecies = { IRIS: [Species.HAXORUS, Species.RESHIRAM, Species.ARCHEOPS], // Druddigon lead, Gmax Lapras DIANTHA: [Species.HAWLUCHA, Species.XERNEAS, Species.GOODRA], // Gourgeist lead, Mega Gardevoir HAU: [[Species.SOLGALEO, Species.LUNALA], Species.NOIVERN, [Species.DECIDUEYE, Species.INCINEROAR, Species.PRIMARINA], [Species.TAPU_BULU, Species.TAPU_FINI, Species.TAPU_KOKO, Species.TAPU_LELE]], // Alola Raichu lead - LEON: [Species.DRAGAPULT, [Species.ZACIAN, Species.ZAMAZENTA], Species.AEGISLASH], // Rillaboom/Cinderace/Inteleon lead, GMax Charizard + LEON: [Species.DRAGAPULT, Species.ZACIAN, Species.AEGISLASH], // Rillaboom/Cinderace/Inteleon lead, GMax Charizard GEETA: [Species.MIRAIDON, [Species.ESPATHRA, Species.VELUZA], [Species.AVALUGG, Species.HISUI_AVALUGG], Species.KINGAMBIT], // Glimmora lead NEMONA: [Species.KORAIDON, Species.PAWMOT, [Species.DUDUNSPARCE, Species.ORTHWORM], [Species.MEOWSCARADA, Species.SKELEDIRGE, Species.QUAQUAVAL]], // Lycanroc lead KIERAN: [[Species.GRIMMSNARL, Species.INCINEROAR, Species.PORYGON_Z], Species.OGERPON, Species.TERAPAGOS, Species.HYDRAPPLE], // Poliwrath/Politoed lead @@ -1407,6 +1566,38 @@ export const trainerConfigs: TrainerConfigs = { [TrainerPoolTier.SUPER_RARE]: [Species.DURALUDON, Species.DREEPY] }), [TrainerType.OLEANA]: new TrainerConfig(++t).setMoneyMultiplier(1.5).initForEvilTeamAdmin("macro_admin", "macro", [Species.GARBODOR]).setEncounterBgm(TrainerType.PLASMA_GRUNT).setBattleBgm("battle_plasma_grunt").setMixedBattleBgm("battle_oleana").setVictoryBgm("victory_team_plasma").setPartyTemplateFunc(scene => getEvilGruntPartyTemplate(scene)), + [TrainerType.STAR_GRUNT]: new TrainerConfig(++t).setHasGenders("Star Grunt Female").setHasDouble("Star Grunts").setMoneyMultiplier(1.0).setEncounterBgm(TrainerType.PLASMA_GRUNT).setBattleBgm("battle_plasma_grunt").setMixedBattleBgm("battle_star_grunt").setVictoryBgm("victory_team_plasma").setPartyTemplateFunc(scene => getEvilGruntPartyTemplate(scene)) + .setSpeciesPools({ + [TrainerPoolTier.COMMON]: [ Species.DUNSPARCE, Species.HOUNDOUR, Species.AZURILL, Species.GULPIN, Species.FOONGUS, Species.FLETCHLING, Species.LITLEO, Species.FLABEBE, Species.CRABRAWLER, Species.NYMBLE, Species.PAWMI, Species.FIDOUGH, Species.SQUAWKABILLY, Species.MASCHIFF, Species.SHROODLE, Species.KLAWF, Species.WIGLETT, Species.PALDEA_WOOPER ], + [TrainerPoolTier.UNCOMMON]: [ Species.KOFFING, Species.EEVEE, Species.GIRAFARIG, Species.RALTS, Species.TORKOAL, Species.SEVIPER, Species.SCRAGGY, Species.ZORUA, Species.MIMIKYU, Species.IMPIDIMP, Species.FALINKS, Species.CAPSAKID, Species.TINKATINK, Species.BOMBIRDIER, Species.CYCLIZAR, Species.FLAMIGO, Species.PALDEA_TAUROS ], + [TrainerPoolTier.RARE]: [ Species.MANKEY, Species.PAWNIARD, Species.CHARCADET, Species.FLITTLE, Species.VAROOM, Species.ORTHWORM], + [TrainerPoolTier.SUPER_RARE]: [ Species.DONDOZO, Species.GIMMIGHOUL ] + }), + [TrainerType.GIACOMO]: new TrainerConfig(++t).setMoneyMultiplier(1.5).initForEvilTeamAdmin("star_admin", "star_1", [Species.KINGAMBIT]).setEncounterBgm(TrainerType.PLASMA_GRUNT).setBattleBgm("battle_plasma_grunt").setMixedBattleBgm("battle_star_admin").setVictoryBgm("victory_team_plasma").setPartyTemplateFunc(scene => getEvilGruntPartyTemplate(scene)) + .setPartyMemberFunc(3, getRandomPartyMemberFunc([Species.REVAVROOM], TrainerSlot.TRAINER, true, p => { + p.formIndex = 1; // Segin Starmobile + p.moveset = [ new PokemonMove(Moves.WICKED_TORQUE), new PokemonMove(Moves.SPIN_OUT), new PokemonMove(Moves.SHIFT_GEAR), new PokemonMove(Moves.HIGH_HORSEPOWER) ]; + })), + [TrainerType.MELA]: new TrainerConfig(++t).setMoneyMultiplier(1.5).initForEvilTeamAdmin("star_admin", "star_2", [Species.ARMAROUGE]).setEncounterBgm(TrainerType.PLASMA_GRUNT).setBattleBgm("battle_plasma_grunt").setMixedBattleBgm("battle_star_admin").setVictoryBgm("victory_team_plasma").setPartyTemplateFunc(scene => getEvilGruntPartyTemplate(scene)) + .setPartyMemberFunc(3, getRandomPartyMemberFunc([Species.REVAVROOM], TrainerSlot.TRAINER, true, p => { + p.formIndex = 2; // Schedar Starmobile + p.moveset = [ new PokemonMove(Moves.BLAZING_TORQUE), new PokemonMove(Moves.SPIN_OUT), new PokemonMove(Moves.SHIFT_GEAR), new PokemonMove(Moves.HIGH_HORSEPOWER) ]; + })), + [TrainerType.ATTICUS]: new TrainerConfig(++t).setMoneyMultiplier(1.5).initForEvilTeamAdmin("star_admin", "star_3", [Species.REVAVROOM]).setEncounterBgm(TrainerType.PLASMA_GRUNT).setBattleBgm("battle_plasma_grunt").setMixedBattleBgm("battle_star_admin").setVictoryBgm("victory_team_plasma").setPartyTemplateFunc(scene => getEvilGruntPartyTemplate(scene)) + .setPartyMemberFunc(3, getRandomPartyMemberFunc([Species.REVAVROOM], TrainerSlot.TRAINER, true, p => { + p.formIndex = 3; // Navi Starmobile + p.moveset = [ new PokemonMove(Moves.NOXIOUS_TORQUE), new PokemonMove(Moves.SPIN_OUT), new PokemonMove(Moves.SHIFT_GEAR), new PokemonMove(Moves.HIGH_HORSEPOWER) ]; + })), + [TrainerType.ORTEGA]: new TrainerConfig(++t).setMoneyMultiplier(1.5).initForEvilTeamAdmin("star_admin", "star_4", [Species.DACHSBUN]).setEncounterBgm(TrainerType.PLASMA_GRUNT).setBattleBgm("battle_plasma_grunt").setMixedBattleBgm("battle_star_admin").setVictoryBgm("victory_team_plasma").setPartyTemplateFunc(scene => getEvilGruntPartyTemplate(scene)) + .setPartyMemberFunc(3, getRandomPartyMemberFunc([Species.REVAVROOM], TrainerSlot.TRAINER, true, p => { + p.formIndex = 4; // Ruchbah Starmobile + p.moveset = [ new PokemonMove(Moves.MAGICAL_TORQUE), new PokemonMove(Moves.SPIN_OUT), new PokemonMove(Moves.SHIFT_GEAR), new PokemonMove(Moves.HIGH_HORSEPOWER) ]; + })), + [TrainerType.ERI]: new TrainerConfig(++t).setMoneyMultiplier(1.5).initForEvilTeamAdmin("star_admin", "star_5", [Species.ANNIHILAPE]).setEncounterBgm(TrainerType.PLASMA_GRUNT).setBattleBgm("battle_plasma_grunt").setMixedBattleBgm("battle_star_admin").setVictoryBgm("victory_team_plasma").setPartyTemplateFunc(scene => getEvilGruntPartyTemplate(scene)) + .setPartyMemberFunc(3, getRandomPartyMemberFunc([Species.REVAVROOM], TrainerSlot.TRAINER, true, p => { + p.formIndex = 5; // Caph Starmobile + p.moveset = [ new PokemonMove(Moves.COMBAT_TORQUE), new PokemonMove(Moves.SPIN_OUT), new PokemonMove(Moves.SHIFT_GEAR), new PokemonMove(Moves.HIGH_HORSEPOWER) ]; + })), [TrainerType.BROCK]: new TrainerConfig((t = TrainerType.BROCK)).initForGymLeader(signatureSpecies["BROCK"], true, Type.ROCK).setBattleBgm("battle_kanto_gym").setMixedBattleBgm("battle_kanto_gym"), [TrainerType.MISTY]: new TrainerConfig(++t).initForGymLeader(signatureSpecies["MISTY"], false, Type.WATER).setBattleBgm("battle_kanto_gym").setMixedBattleBgm("battle_kanto_gym"), @@ -1530,18 +1721,18 @@ export const trainerConfigs: TrainerConfigs = { p.generateAndPopulateMoveset(); })) .setPartyMemberFunc(1, getRandomPartyMemberFunc([Species.PIDGEOT], TrainerSlot.TRAINER, true, p => { - p.formIndex = 1; + p.formIndex = 1; // Mega Pidgeot p.generateAndPopulateMoveset(); p.generateName(); })), [TrainerType.RED]: new TrainerConfig(++t).initForChampion(signatureSpecies["RED"], true).setBattleBgm("battle_johto_champion").setMixedBattleBgm("battle_johto_champion").setHasDouble("red_blue_double").setDoubleTrainerType(TrainerType.BLUE).setDoubleTitle("champion_double") .setPartyMemberFunc(0, getRandomPartyMemberFunc([Species.PIKACHU], TrainerSlot.TRAINER, true, p => { - p.formIndex = 8; + p.formIndex = 8; // G-Max Pikachu p.generateAndPopulateMoveset(); p.generateName(); })) .setPartyMemberFunc(1, getRandomPartyMemberFunc([Species.VENUSAUR, Species.CHARIZARD, Species.BLASTOISE], TrainerSlot.TRAINER, true, p => { - p.formIndex = 1; + p.formIndex = 1; // Mega Venusaur, Mega Charizard X, or Mega Blastoise p.generateAndPopulateMoveset(); p.generateName(); })), @@ -1550,7 +1741,7 @@ export const trainerConfigs: TrainerConfigs = { p.generateAndPopulateMoveset(); })) .setPartyMemberFunc(1, getRandomPartyMemberFunc([Species.LATIAS, Species.LATIOS], TrainerSlot.TRAINER, true, p => { - p.formIndex = 1; + p.formIndex = 1; // Mega Latias or Mega Latios p.generateAndPopulateMoveset(); p.generateName(); })), @@ -1559,7 +1750,7 @@ export const trainerConfigs: TrainerConfigs = { p.generateAndPopulateMoveset(); })) .setPartyMemberFunc(1, getRandomPartyMemberFunc([Species.METAGROSS], TrainerSlot.TRAINER, true, p => { - p.formIndex = 1; + p.formIndex = 1; // Mega Metagross p.generateAndPopulateMoveset(); p.generateName(); })), @@ -1569,15 +1760,16 @@ export const trainerConfigs: TrainerConfigs = { p.generateAndPopulateMoveset(); })) .setPartyMemberFunc(1, getRandomPartyMemberFunc([Species.SWAMPERT], TrainerSlot.TRAINER, true, p => { - p.formIndex = 1; + p.formIndex = 1; // Mega Swampert p.generateAndPopulateMoveset(); + p.generateName(); })), [TrainerType.CYNTHIA]: new TrainerConfig(++t).initForChampion(signatureSpecies["CYNTHIA"], false).setBattleBgm("battle_sinnoh_champion").setMixedBattleBgm("battle_sinnoh_champion") .setPartyMemberFunc(0, getRandomPartyMemberFunc([Species.SPIRITOMB], TrainerSlot.TRAINER, true, p => { p.generateAndPopulateMoveset(); })) .setPartyMemberFunc(1, getRandomPartyMemberFunc([Species.GARCHOMP], TrainerSlot.TRAINER, true, p => { - p.formIndex = 1; + p.formIndex = 1; // Mega Garchomp p.generateAndPopulateMoveset(); p.generateName(); })), @@ -1590,7 +1782,7 @@ export const trainerConfigs: TrainerConfigs = { p.generateAndPopulateMoveset(); })) .setPartyMemberFunc(1, getRandomPartyMemberFunc([Species.LAPRAS], TrainerSlot.TRAINER, true, p => { - p.formIndex = 1; + p.formIndex = 1; // G-Max Lapras p.generateAndPopulateMoveset(); p.generateName(); })), @@ -1599,7 +1791,7 @@ export const trainerConfigs: TrainerConfigs = { p.generateAndPopulateMoveset(); })) .setPartyMemberFunc(1, getRandomPartyMemberFunc([Species.GARDEVOIR], TrainerSlot.TRAINER, true, p => { - p.formIndex = 1; + p.formIndex = 1; // Mega Gardevoir p.generateAndPopulateMoveset(); p.generateName(); })), @@ -1612,7 +1804,7 @@ export const trainerConfigs: TrainerConfigs = { p.generateAndPopulateMoveset(); })) .setPartyMemberFunc(1, getRandomPartyMemberFunc([Species.CHARIZARD], TrainerSlot.TRAINER, true, p => { - p.formIndex = 3; + p.formIndex = 3; // G-Max Charizard p.generateAndPopulateMoveset(); p.generateName(); })), @@ -1688,7 +1880,7 @@ export const trainerConfigs: TrainerConfigs = { p.pokeball = PokeballType.MASTER_BALL; p.shiny = true; p.variant = 1; - p.formIndex = 1; + p.formIndex = 1; // Mega Rayquaza p.generateName(); })) .setGenModifiersFunc(party => { @@ -1706,7 +1898,7 @@ export const trainerConfigs: TrainerConfigs = { p.setBoss(true, 2); p.generateAndPopulateMoveset(); p.pokeball = PokeballType.ULTRA_BALL; - p.formIndex = 1; + p.formIndex = 1; // Mega Kangaskhan p.generateName(); })), [TrainerType.ROCKET_BOSS_GIOVANNI_2]: new TrainerConfig(++t).setName("Giovanni").initForEvilTeamLeader("Rocket Boss", [], true).setMixedBattleBgm("battle_rocket_boss").setVictoryBgm("victory_team_plasma") @@ -1721,7 +1913,7 @@ export const trainerConfigs: TrainerConfigs = { p.setBoss(true, 2); p.generateAndPopulateMoveset(); p.pokeball = PokeballType.ULTRA_BALL; - p.formIndex = 1; + p.formIndex = 1; // Mega Kangaskhan p.generateName(); })) .setPartyMemberFunc(4, getRandomPartyMemberFunc([Species.GASTRODON, Species.SEISMITOAD])) @@ -1740,7 +1932,7 @@ export const trainerConfigs: TrainerConfigs = { p.setBoss(true, 2); p.generateAndPopulateMoveset(); p.pokeball = PokeballType.ULTRA_BALL; - p.formIndex = 1; + p.formIndex = 1; // Mega Camerupt p.generateName(); })), [TrainerType.MAXIE_2]: new TrainerConfig(++t).setName("Maxie").initForEvilTeamLeader("Magma Boss", [], true).setMixedBattleBgm("battle_aqua_magma_boss").setVictoryBgm("victory_team_plasma") @@ -1751,7 +1943,7 @@ export const trainerConfigs: TrainerConfigs = { })) .setPartyMemberFunc(1, getRandomPartyMemberFunc([Species.TORKOAL, Species.NINETALES], TrainerSlot.TRAINER, true, p => { p.generateAndPopulateMoveset(); - p.abilityIndex = 2; // DROUGHT + p.abilityIndex = 2; // Drought })) .setPartyMemberFunc(2, getRandomPartyMemberFunc([Species.SHIFTRY, Species.SCOVILLAIN], TrainerSlot.TRAINER, true, p => { p.generateAndPopulateMoveset(); @@ -1762,7 +1954,7 @@ export const trainerConfigs: TrainerConfigs = { p.setBoss(true, 2); p.generateAndPopulateMoveset(); p.pokeball = PokeballType.ULTRA_BALL; - p.formIndex = 1; + p.formIndex = 1; // Mega Camerupt p.generateName(); })) .setPartyMemberFunc(5, getRandomPartyMemberFunc([Species.GROUDON], TrainerSlot.TRAINER, true, p => { @@ -1780,7 +1972,7 @@ export const trainerConfigs: TrainerConfigs = { p.setBoss(true, 2); p.generateAndPopulateMoveset(); p.pokeball = PokeballType.ULTRA_BALL; - p.formIndex = 1; + p.formIndex = 1; // Mega Sharpedo p.generateName(); })), [TrainerType.ARCHIE_2]: new TrainerConfig(++t).setName("Archie").initForEvilTeamLeader("Aqua Boss", [], true).setMixedBattleBgm("battle_aqua_magma_boss").setVictoryBgm("victory_team_plasma") @@ -1805,7 +1997,7 @@ export const trainerConfigs: TrainerConfigs = { p.setBoss(true, 2); p.generateAndPopulateMoveset(); p.pokeball = PokeballType.ULTRA_BALL; - p.formIndex = 1; + p.formIndex = 1; // Mega Sharpedo p.generateName(); })) .setPartyMemberFunc(5, getRandomPartyMemberFunc([Species.KYOGRE], TrainerSlot.TRAINER, true, p => { @@ -1821,7 +2013,7 @@ export const trainerConfigs: TrainerConfigs = { .setPartyMemberFunc(4, getRandomPartyMemberFunc([ Species.HOUNDOOM ], TrainerSlot.TRAINER, true, p => { p.generateAndPopulateMoveset(); p.pokeball = PokeballType.ULTRA_BALL; - p.formIndex = 1; + p.formIndex = 1; // Mega Houndoom p.generateName(); })) .setPartyMemberFunc(5, getRandomPartyMemberFunc([Species.WEAVILE], TrainerSlot.TRAINER, true, p => { @@ -1839,7 +2031,7 @@ export const trainerConfigs: TrainerConfigs = { .setPartyMemberFunc(3, getRandomPartyMemberFunc([Species.HOUNDOOM], TrainerSlot.TRAINER, true, p => { p.generateAndPopulateMoveset(); p.pokeball = PokeballType.ULTRA_BALL; - p.formIndex = 1; + p.formIndex = 1; // Mega Houndoom p.generateName(); })) .setPartyMemberFunc(4, getRandomPartyMemberFunc([Species.WEAVILE, Species.SNEASLER], TrainerSlot.TRAINER, true, p => { @@ -1867,8 +2059,8 @@ export const trainerConfigs: TrainerConfigs = { .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.GENESECT ], TrainerSlot.TRAINER, true, p => { p.setBoss(true, 2); p.generateAndPopulateMoveset(); - p.pokeball = PokeballType.MASTER_BALL; - p.formIndex = Utils.randSeedInt(5); + p.pokeball = PokeballType.ULTRA_BALL; + p.formIndex = Utils.randSeedInt(5, 1); // Shock, Burn, Chill, or Douse Drive })) .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.BASCULEGION, Species.JELLICENT ], TrainerSlot.TRAINER, true, p => { p.generateAndPopulateMoveset(); @@ -1900,7 +2092,7 @@ export const trainerConfigs: TrainerConfigs = { p.setBoss(true, 2); p.generateAndPopulateMoveset(); p.pokeball = PokeballType.ULTRA_BALL; - p.formIndex = 1; + p.formIndex = 1; // Mega Gyarados p.generateName(); })), [TrainerType.LYSANDRE_2]: new TrainerConfig(++t).setName("Lysandre").initForEvilTeamLeader("Flare Boss", [], true).setMixedBattleBgm("battle_flare_boss").setVictoryBgm("victory_team_plasma") @@ -1919,7 +2111,7 @@ export const trainerConfigs: TrainerConfigs = { p.setBoss(true, 2); p.generateAndPopulateMoveset(); p.pokeball = PokeballType.ULTRA_BALL; - p.formIndex = 1; + p.formIndex = 1; // Mega Gyardos p.generateName(); })) .setPartyMemberFunc(5, getRandomPartyMemberFunc([Species.YVELTAL], TrainerSlot.TRAINER, true, p => { @@ -1936,25 +2128,24 @@ export const trainerConfigs: TrainerConfigs = { .setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.NIHILEGO ], TrainerSlot.TRAINER, true, p => { p.setBoss(true, 2); p.generateAndPopulateMoveset(); + p.pokeball = PokeballType.ROGUE_BALL; })), [TrainerType.LUSAMINE_2]: new TrainerConfig(++t).setName("Lusamine").initForEvilTeamLeader("Aether Boss", [], true).setMixedBattleBgm("battle_aether_boss").setVictoryBgm("victory_team_plasma") - .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.CLEFABLE ], TrainerSlot.TRAINER, true, p => { + .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.NIHILEGO ], TrainerSlot.TRAINER, true, p => { p.setBoss(true, 2); p.generateAndPopulateMoveset(); + p.pokeball = PokeballType.ROGUE_BALL; })) .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.MILOTIC, Species.PRIMARINA ])) - .setPartyMemberFunc(2, getRandomPartyMemberFunc([ Species.PHEROMOSA ], TrainerSlot.TRAINER, true, p => { - p.generateAndPopulateMoveset(); - p.pokeball = PokeballType.MASTER_BALL; - })) + .setPartyMemberFunc(2, getRandomPartyMemberFunc([ Species.CLEFABLE ])) .setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.STAKATAKA, Species.CELESTEELA, Species.GUZZLORD ], TrainerSlot.TRAINER, true, p => { p.generateAndPopulateMoveset(); - p.pokeball = PokeballType.MASTER_BALL; + p.pokeball = PokeballType.ROGUE_BALL; })) - .setPartyMemberFunc(4, getRandomPartyMemberFunc([ Species.NIHILEGO ], TrainerSlot.TRAINER, true, p => { + .setPartyMemberFunc(4, getRandomPartyMemberFunc([ Species.PHEROMOSA ], TrainerSlot.TRAINER, true, p => { p.setBoss(true, 2); p.generateAndPopulateMoveset(); - p.pokeball = PokeballType.MASTER_BALL; + p.pokeball = PokeballType.ROGUE_BALL; })) .setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.NECROZMA ], TrainerSlot.TRAINER, true, p => { p.setBoss(true, 2); @@ -1968,37 +2159,41 @@ export const trainerConfigs: TrainerConfigs = { .setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.GALVANTULA, Species.VIKAVOLT])) .setPartyMemberFunc(4, getRandomPartyMemberFunc([ Species.PINSIR ], TrainerSlot.TRAINER, true, p => { p.generateAndPopulateMoveset(); - p.formIndex = 1; + p.formIndex = 1; // Mega Pinsir + p.pokeball = PokeballType.ULTRA_BALL; p.generateName(); })) .setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.GOLISOPOD ], TrainerSlot.TRAINER, true, p => { p.setBoss(true, 2); p.generateAndPopulateMoveset(); + p.pokeball = PokeballType.ULTRA_BALL; })), [TrainerType.GUZMA_2]: new TrainerConfig(++t).setName("Guzma").initForEvilTeamLeader("Skull Boss", [], true).setMixedBattleBgm("battle_skull_boss").setVictoryBgm("victory_team_plasma") .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.GOLISOPOD ], TrainerSlot.TRAINER, true, p => { p.setBoss(true, 2); p.generateAndPopulateMoveset(); p.abilityIndex = 2; //Anticipation + p.pokeball = PokeballType.ULTRA_BALL; })) .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.HISUI_SAMUROTT, Species.CRAWDAUNT ], TrainerSlot.TRAINER, true, p => { - p.abilityIndex = 2; //Sharpness, Adaptability + p.abilityIndex = 2; //Sharpness Hisui Samurott, Adaptability Crawdaunt })) .setPartyMemberFunc(2, getRandomPartyMemberFunc([ Species.SCIZOR, Species.KLEAVOR ])) .setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.PINSIR ], TrainerSlot.TRAINER, true, p => { p.generateAndPopulateMoveset(); p.formIndex = 1; p.generateName(); + p.pokeball = PokeballType.ULTRA_BALL; })) .setPartyMemberFunc(4, getRandomPartyMemberFunc([ Species.BUZZWOLE ], TrainerSlot.TRAINER, true, p => { p.setBoss(true, 2); p.generateAndPopulateMoveset(); - p.pokeball = PokeballType.MASTER_BALL; + p.pokeball = PokeballType.ROGUE_BALL; })) .setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.XURKITREE ], TrainerSlot.TRAINER, true, p => { p.setBoss(true, 2); p.generateAndPopulateMoveset(); - p.pokeball = PokeballType.MASTER_BALL; + p.pokeball = PokeballType.ROGUE_BALL; })), [TrainerType.ROSE]: new TrainerConfig(++t).setName("Rose").initForEvilTeamLeader("Macro Boss", []).setMixedBattleBgm("battle_macro_boss").setVictoryBgm("victory_team_plasma") .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.ARCHALUDON ])) @@ -2009,29 +2204,242 @@ export const trainerConfigs: TrainerConfigs = { .setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.COPPERAJAH ], TrainerSlot.TRAINER, true, p => { p.setBoss(true, 2); p.generateAndPopulateMoveset(); - p.formIndex = 1; + p.formIndex = 1; // G-Max Copperajah p.generateName(); + p.pokeball = PokeballType.ULTRA_BALL; })), [TrainerType.ROSE_2]: new TrainerConfig(++t).setName("Rose").initForEvilTeamLeader("Macro Boss", [], true).setMixedBattleBgm("battle_macro_boss").setVictoryBgm("victory_team_plasma") - .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.ARCHALUDON ], TrainerSlot.TRAINER, true, p => { + .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.MELMETAL ], TrainerSlot.TRAINER, true, p => { p.setBoss(true, 2); p.generateAndPopulateMoveset(); + p.pokeball = PokeballType.ULTRA_BALL; })) .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.AEGISLASH, Species.GHOLDENGO ])) .setPartyMemberFunc(2, getRandomPartyMemberFunc([ Species.DRACOVISH, Species.DRACOZOLT ], TrainerSlot.TRAINER, true, p => { p.generateAndPopulateMoveset(); - p.abilityIndex = 1; //Strong Jaw, Hustle + p.abilityIndex = 1; //Strong Jaw Dracovish, Hustle Dracozolt })) - .setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.MELMETAL ])) + .setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.ARCHALUDON ])) .setPartyMemberFunc(4, getRandomPartyMemberFunc([ Species.GALAR_ARTICUNO, Species.GALAR_ZAPDOS, Species.GALAR_MOLTRES ], TrainerSlot.TRAINER, true, p => { p.setBoss(true, 2); p.generateAndPopulateMoveset(); - p.pokeball = PokeballType.MASTER_BALL; + p.pokeball = PokeballType.ULTRA_BALL; })) .setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.COPPERAJAH ], TrainerSlot.TRAINER, true, p => { p.setBoss(true, 2); p.generateAndPopulateMoveset(); - p.formIndex = 1; + p.formIndex = 1; // G-Max Copperajah p.generateName(); + p.pokeball = PokeballType.ULTRA_BALL; })), + [TrainerType.PENNY]: new TrainerConfig(++t).setName("Cassiopeia").initForEvilTeamLeader("Star Boss", []).setMixedBattleBgm("battle_star_boss").setVictoryBgm("victory_team_plasma") + .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.VAPOREON, Species.JOLTEON, Species.FLAREON ])) + .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.ESPEON, Species.UMBREON ], TrainerSlot.TRAINER, true, p => { + p.abilityIndex = 2; // Magic Bounce Espeon, Inner Focus Umbreon + p.generateAndPopulateMoveset(); + })) + .setPartyMemberFunc(2, getRandomPartyMemberFunc([ Species.LEAFEON, Species.GLACEON ])) + .setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.ROTOM ], TrainerSlot.TRAINER, true, p => { + p.generateAndPopulateMoveset(); + p.formIndex = Utils.randSeedInt(5, 1); // Heat, Wash, Frost, Fan, or Mow + })) + .setPartyMemberFunc(4, getRandomPartyMemberFunc([ Species.SYLVEON ], TrainerSlot.TRAINER, true, p => { + p.generateAndPopulateMoveset(); + p.abilityIndex = 2; // Pixilate + })) + .setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.EEVEE ], TrainerSlot.TRAINER, true, p => { + p.setBoss(true, 2); + p.generateAndPopulateMoveset(); + p.formIndex = 2; // G-Max Eevee + p.pokeball = PokeballType.ULTRA_BALL; + p.generateName(); + })) + .setGenModifiersFunc(party => { + const teraPokemon = party[4]; + return [modifierTypes.TERA_SHARD().generateType([], [teraPokemon.species.type1])!.withIdFromFunc(modifierTypes.TERA_SHARD).newModifier(teraPokemon) as PersistentModifier]; //TODO: is the bang correct? + }), + [TrainerType.PENNY_2]: new TrainerConfig(++t).setName("Cassiopeia").initForEvilTeamLeader("Star Boss", [], true).setMixedBattleBgm("battle_star_boss").setVictoryBgm("victory_team_plasma") + .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.REVAVROOM ], TrainerSlot.TRAINER, true, p => { + p.setBoss(true, 2); + p.formIndex = Utils.randSeedInt(5, 1); //Random Starmobile form + p.generateAndPopulateMoveset(); + p.pokeball = PokeballType.ULTRA_BALL; + })) + .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.ENTEI, Species.RAIKOU, Species.SUICUNE ], TrainerSlot.TRAINER, true, p => { + p.generateAndPopulateMoveset(); + p.pokeball = PokeballType.ULTRA_BALL; + })) + .setPartyMemberFunc(2, getRandomPartyMemberFunc([ Species.WALKING_WAKE, Species.GOUGING_FIRE, Species.RAGING_BOLT ])) + .setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.SYLVEON ], TrainerSlot.TRAINER, true, p => { + p.generateAndPopulateMoveset(); + p.abilityIndex = 2; // Pixilate + })) + .setPartyMemberFunc(4, getRandomPartyMemberFunc([ Species.EEVEE ], TrainerSlot.TRAINER, true, p => { + p.setBoss(true, 2); + p.generateAndPopulateMoveset(); + p.formIndex = 2; + p.generateName(); + p.pokeball = PokeballType.ULTRA_BALL; + })) + .setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.ZAMAZENTA ], TrainerSlot.TRAINER, true, p => { + p.setBoss(true, 2); + p.generateAndPopulateMoveset(); + p.pokeball = PokeballType.MASTER_BALL; + })) + .setGenModifiersFunc(party => { + const teraPokemon = party[3]; + return [modifierTypes.TERA_SHARD().generateType([], [teraPokemon.species.type1])!.withIdFromFunc(modifierTypes.TERA_SHARD).newModifier(teraPokemon) as PersistentModifier]; //TODO: is the bang correct? + }), + [TrainerType.BUCK]: new TrainerConfig(++t).setName("Buck").initForStatTrainer([], true) + .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.CLAYDOL ], TrainerSlot.TRAINER, true, p => { + p.setBoss(true, 3); + p.generateAndPopulateMoveset(); + p.pokeball = PokeballType.ULTRA_BALL; + })) + .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.VENUSAUR, Species.COALOSSAL ], TrainerSlot.TRAINER, true, p => { + p.generateAndPopulateMoveset(); + p.pokeball = PokeballType.GREAT_BALL; + if (p.species.speciesId === Species.VENUSAUR) { + p.formIndex = 2; // Gmax + p.abilityIndex = 2; // Venusaur gets Chlorophyll + } else { + p.formIndex = 1; // Gmax + } + p.generateName(); + })) + .setPartyMemberFunc(2, getRandomPartyMemberFunc([ Species.AGGRON ], TrainerSlot.TRAINER, true, p => { + p.generateAndPopulateMoveset(); + p.formIndex = 1; // Mega + p.generateName(); + })) + .setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.TORKOAL ], TrainerSlot.TRAINER, true, p => { + p.generateAndPopulateMoveset(); + p.abilityIndex = 1; // Drought + })) + .setPartyMemberFunc(4, getRandomPartyMemberFunc([ Species.GREAT_TUSK ], TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.HEATRAN ], TrainerSlot.TRAINER, true, p => { + p.setBoss(true, 2); + p.generateAndPopulateMoveset(); + p.pokeball = PokeballType.MASTER_BALL; + })), + [TrainerType.CHERYL]: new TrainerConfig(++t).setName("Cheryl").initForStatTrainer([], false) + .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.BLISSEY ], TrainerSlot.TRAINER, true, p => { + p.setBoss(true, 3); + p.generateAndPopulateMoveset(); + p.pokeball = PokeballType.ULTRA_BALL; + })) + .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.SNORLAX, Species.LAPRAS ], TrainerSlot.TRAINER, true, p => { + p.generateAndPopulateMoveset(); + p.pokeball = PokeballType.GREAT_BALL; + p.formIndex = 1; // Gmax + p.generateName(); + })) + .setPartyMemberFunc(2, getRandomPartyMemberFunc([ Species.AUDINO ], TrainerSlot.TRAINER, true, p => { + p.generateAndPopulateMoveset(); + p.formIndex = 1; // Mega + p.generateName(); + })) + .setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.GOODRA ], TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(4, getRandomPartyMemberFunc([ Species.IRON_HANDS ], TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.CRESSELIA, Species.ENAMORUS ], TrainerSlot.TRAINER, true, p => { + p.setBoss(true, 2); + p.generateAndPopulateMoveset(); + if (p.species.speciesId === Species.ENAMORUS) { + p.formIndex = 1; // Therian + p.generateName(); + } + p.pokeball = PokeballType.MASTER_BALL; + })), + [TrainerType.MARLEY]: new TrainerConfig(++t).setName("Marley").initForStatTrainer([], false) + .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.ARCANINE ], TrainerSlot.TRAINER, true, p => { + p.setBoss(true, 3); + p.generateAndPopulateMoveset(); + p.pokeball = PokeballType.ULTRA_BALL; + })) + .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.CINDERACE, Species.INTELEON ], TrainerSlot.TRAINER, true, p => { + p.generateAndPopulateMoveset(); + p.pokeball = PokeballType.GREAT_BALL; + p.formIndex = 1; // Gmax + p.generateName(); + })) + .setPartyMemberFunc(2, getRandomPartyMemberFunc([ Species.AERODACTYL ], TrainerSlot.TRAINER, true, p => { + p.generateAndPopulateMoveset(); + p.formIndex = 1; // Mega + p.generateName(); + })) + .setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.DRAGAPULT ], TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(4, getRandomPartyMemberFunc([ Species.IRON_BUNDLE ], TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.REGIELEKI ], TrainerSlot.TRAINER, true, p => { + p.setBoss(true, 2); + p.generateAndPopulateMoveset(); + p.pokeball = PokeballType.MASTER_BALL; + })), + [TrainerType.MIRA]: new TrainerConfig(++t).setName("Mira").initForStatTrainer([], false) + .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.ALAKAZAM ], TrainerSlot.TRAINER, true, p => { + p.setBoss(true, 2); + p.generateAndPopulateMoveset(); + p.formIndex = 1; + p.pokeball = PokeballType.ULTRA_BALL; + p.generateName(); + })) + .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.GENGAR, Species.HATTERENE ], TrainerSlot.TRAINER, true, p => { + p.generateAndPopulateMoveset(); + p.pokeball = PokeballType.GREAT_BALL; + p.formIndex = p.species.speciesId === Species.GENGAR ? 2 : 1; // Gmax + p.generateName(); + })) + .setPartyMemberFunc(2, getRandomPartyMemberFunc([ Species.FLUTTER_MANE ], TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.HYDREIGON ], TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(4, getRandomPartyMemberFunc([ Species.MAGNEZONE ], TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.LATIOS, Species.LATIAS ], TrainerSlot.TRAINER, true, p => { + p.setBoss(true, 2); + p.generateAndPopulateMoveset(); + p.pokeball = PokeballType.MASTER_BALL; + })), + [TrainerType.RILEY]: new TrainerConfig(++t).setName("Riley").initForStatTrainer([], true) + .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.LUCARIO ], TrainerSlot.TRAINER, true, p => { + p.setBoss(true, 2); + p.generateAndPopulateMoveset(); + p.formIndex = 1; + p.pokeball = PokeballType.ULTRA_BALL; + p.generateName(); + })) + .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.RILLABOOM, Species.CENTISKORCH ], TrainerSlot.TRAINER, true, p => { + p.generateAndPopulateMoveset(); + p.pokeball = PokeballType.GREAT_BALL; + p.formIndex = 1; // Gmax + p.generateName(); + })) + .setPartyMemberFunc(2, getRandomPartyMemberFunc([ Species.TYRANITAR ], TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.ROARING_MOON ], TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(4, getRandomPartyMemberFunc([ Species.URSALUNA ], TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.REGIGIGAS, Species.LANDORUS ], TrainerSlot.TRAINER, true, p => { + p.setBoss(true, 2); + p.generateAndPopulateMoveset(); + if (p.species.speciesId === Species.LANDORUS) { + p.formIndex = 1; // Therian + p.generateName(); + } + p.pokeball = PokeballType.MASTER_BALL; + })), + [TrainerType.VICTOR]: new TrainerConfig(++t).setTitle("The Winstrates").setLocalizedName("Victor") + .setMoneyMultiplier(1) // The Winstrate trainers have total money multiplier of 6 + .setPartyTemplates(trainerPartyTemplates.ONE_AVG_ONE_STRONG), + [TrainerType.VICTORIA]: new TrainerConfig(++t).setTitle("The Winstrates").setLocalizedName("Victoria") + .setMoneyMultiplier(1) + .setPartyTemplates(trainerPartyTemplates.ONE_AVG_ONE_STRONG), + [TrainerType.VIVI]: new TrainerConfig(++t).setTitle("The Winstrates").setLocalizedName("Vivi") + .setMoneyMultiplier(1) + .setPartyTemplates(trainerPartyTemplates.TWO_AVG_ONE_STRONG), + [TrainerType.VICKY]: new TrainerConfig(++t).setTitle("The Winstrates").setLocalizedName("Vicky") + .setMoneyMultiplier(1) + .setPartyTemplates(trainerPartyTemplates.ONE_AVG), + [TrainerType.VITO]: new TrainerConfig(++t).setTitle("The Winstrates").setLocalizedName("Vito") + .setMoneyMultiplier(2) + .setPartyTemplates(new TrainerPartyCompoundTemplate(new TrainerPartyTemplate(3, PartyMemberStrength.AVERAGE), new TrainerPartyTemplate(2, PartyMemberStrength.STRONG))), + [TrainerType.BUG_TYPE_SUPERFAN]: new TrainerConfig(++t).setMoneyMultiplier(2.25).setEncounterBgm(TrainerType.ACE_TRAINER) + .setPartyTemplates(new TrainerPartyTemplate(2, PartyMemberStrength.AVERAGE)), + [TrainerType.EXPERT_POKEMON_BREEDER]: new TrainerConfig(++t).setMoneyMultiplier(3).setEncounterBgm(TrainerType.ACE_TRAINER).setLocalizedName("Expert Pokemon Breeder") + .setPartyTemplates(new TrainerPartyTemplate(3, PartyMemberStrength.STRONG)) }; + diff --git a/src/enums/battler-tag-type.ts b/src/enums/battler-tag-type.ts index 105f359df76..6cf2d260dcb 100644 --- a/src/enums/battler-tag-type.ts +++ b/src/enums/battler-tag-type.ts @@ -65,6 +65,7 @@ export enum BattlerTagType { RECEIVE_DOUBLE_DAMAGE = "RECEIVE_DOUBLE_DAMAGE", ALWAYS_GET_HIT = "ALWAYS_GET_HIT", DISABLED = "DISABLED", + SUBSTITUTE = "SUBSTITUTE", IGNORE_GHOST = "IGNORE_GHOST", IGNORE_DARK = "IGNORE_DARK", GULP_MISSILE_ARROKUDA = "GULP_MISSILE_ARROKUDA", @@ -78,4 +79,6 @@ export enum BattlerTagType { TAR_SHOT = "TAR_SHOT", BURNED_UP = "BURNED_UP", DOUBLE_SHOCKED = "DOUBLE_SHOCKED", + MYSTERY_ENCOUNTER_POST_SUMMON = "MYSTERY_ENCOUNTER_POST_SUMMON", + HEAL_BLOCK = "HEAL_BLOCK", } diff --git a/src/enums/encounter-anims.ts b/src/enums/encounter-anims.ts new file mode 100644 index 00000000000..bd1461473c9 --- /dev/null +++ b/src/enums/encounter-anims.ts @@ -0,0 +1,11 @@ +/** + * Animations used for Mystery Encounters + * These are custom animations that may or may not work in any other circumstance + * Use at your own risk + */ +export enum EncounterAnim { + MAGMA_BG, + MAGMA_SPOUT, + SMOKESCREEN, + DANCE +} diff --git a/src/enums/exp-gains-speed.ts b/src/enums/exp-gains-speed.ts new file mode 100644 index 00000000000..964c4f99c70 --- /dev/null +++ b/src/enums/exp-gains-speed.ts @@ -0,0 +1,22 @@ +/** + * Defines the speed of gaining experience. + * + * @remarks + * The `expGainSpeed` can have several modes: + * - `0` - Default: The normal speed. + * - `1` - Fast: Fast speed. + * - `2` - Faster: Faster speed. + * - `3` - Skip: Skip gaining exp animation. + * + * @default 0 - Uses the default normal speed. + */ +export enum ExpGainsSpeed { + /** The normal speed. */ + DEFAULT, + /** Fast speed. */ + FAST, + /** Faster speed. */ + FASTER, + /** Skip gaining exp animation. */ + SKIP +} diff --git a/src/enums/mystery-encounter-mode.ts b/src/enums/mystery-encounter-mode.ts new file mode 100644 index 00000000000..f1e98ca5b18 --- /dev/null +++ b/src/enums/mystery-encounter-mode.ts @@ -0,0 +1,12 @@ +export enum MysteryEncounterMode { + /** {@linkcode MysteryEncounter} will always begin in this mode, but will always swap modes when an option is selected */ + DEFAULT, + /** If the {@linkcode MysteryEncounter} battle is a trainer type battle */ + TRAINER_BATTLE, + /** If the {@linkcode MysteryEncounter} battle is a wild type battle */ + WILD_BATTLE, + /** Enables special boss music during encounter */ + BOSS_BATTLE, + /** If there is no battle in the {@linkcode MysteryEncounter} or option selected */ + NO_BATTLE +} diff --git a/src/enums/mystery-encounter-option-mode.ts b/src/enums/mystery-encounter-option-mode.ts new file mode 100644 index 00000000000..a994c30581b --- /dev/null +++ b/src/enums/mystery-encounter-option-mode.ts @@ -0,0 +1,10 @@ +export enum MysteryEncounterOptionMode { + /** Default style */ + DEFAULT, + /** Disabled on requirements not met, default style on requirements met */ + DISABLED_OR_DEFAULT, + /** Default style on requirements not met, special style on requirements met */ + DEFAULT_OR_SPECIAL, + /** Disabled on requirements not met, special style on requirements met */ + DISABLED_OR_SPECIAL +} diff --git a/src/enums/mystery-encounter-tier.ts b/src/enums/mystery-encounter-tier.ts new file mode 100644 index 00000000000..484acc7aba9 --- /dev/null +++ b/src/enums/mystery-encounter-tier.ts @@ -0,0 +1,11 @@ +/** + * Enum values are base spawn weights of each tier. + * The weights aim for 46.25/31.25/18.5/4% spawn ratios, AFTER accounting for anti-variance and pity mechanisms + */ +export enum MysteryEncounterTier { + COMMON = 66, + GREAT = 40, + ULTRA = 19, + ROGUE = 3, + MASTER = 0 // Not currently used +} diff --git a/src/enums/mystery-encounter-type.ts b/src/enums/mystery-encounter-type.ts new file mode 100644 index 00000000000..b973652b113 --- /dev/null +++ b/src/enums/mystery-encounter-type.ts @@ -0,0 +1,33 @@ +export enum MysteryEncounterType { + MYSTERIOUS_CHALLENGERS, + MYSTERIOUS_CHEST, + DARK_DEAL, + FIGHT_OR_FLIGHT, + SLUMBERING_SNORLAX, + TRAINING_SESSION, + DEPARTMENT_STORE_SALE, + SHADY_VITAMIN_DEALER, + FIELD_TRIP, + SAFARI_ZONE, + LOST_AT_SEA, + FIERY_FALLOUT, + THE_STRONG_STUFF, + THE_POKEMON_SALESMAN, + AN_OFFER_YOU_CANT_REFUSE, + DELIBIRDY, + ABSOLUTE_AVARICE, + A_TRAINERS_TEST, + TRASH_TO_TREASURE, + BERRIES_ABOUND, + CLOWNING_AROUND, + PART_TIMER, + DANCING_LESSONS, + WEIRD_DREAM, + THE_WINSTRATE_CHALLENGE, + TELEPORTING_HIJINKS, + BUG_TYPE_SUPERFAN, + FUN_AND_GAMES, + UNCOMMON_BREED, + GLOBAL_TRADE_SYSTEM, + THE_EXPERT_POKEMON_BREEDER +} diff --git a/src/enums/pokemon-anim-type.ts b/src/enums/pokemon-anim-type.ts new file mode 100644 index 00000000000..5a0a0c2f622 --- /dev/null +++ b/src/enums/pokemon-anim-type.ts @@ -0,0 +1,16 @@ +export enum PokemonAnimType { + /** + * Adds a Substitute doll to the field in front of a Pokemon. + * The Pokemon then moves "out of focus" and becomes semi-transparent. + */ + SUBSTITUTE_ADD, + /** Brings a Pokemon with a Substitute "into focus" before using a move. */ + SUBSTITUTE_PRE_MOVE, + /** Brings a Pokemon with a Substitute "out of focus" after using a move. */ + SUBSTITUTE_POST_MOVE, + /** + * Removes a Pokemon's Substitute doll from the field. + * The Pokemon then moves back to its original position. + */ + SUBSTITUTE_REMOVE +} diff --git a/src/enums/trainer-type.ts b/src/enums/trainer-type.ts index 835a2c9d039..cb7509067b5 100644 --- a/src/enums/trainer-type.ts +++ b/src/enums/trainer-type.ts @@ -1,235 +1,255 @@ export enum TrainerType { - UNKNOWN, + UNKNOWN, - ACE_TRAINER, - ARTIST, - BACKERS, - BACKPACKER, - BAKER, - BEAUTY, - BIKER, - BLACK_BELT, - BREEDER, - CLERK, - CYCLIST, - DANCER, - DEPOT_AGENT, - DOCTOR, - FIREBREATHER, - FISHERMAN, - GUITARIST, - HARLEQUIN, - HIKER, - HOOLIGANS, - HOOPSTER, - INFIELDER, - JANITOR, - LINEBACKER, - MAID, - MUSICIAN, - HEX_MANIAC, - NURSERY_AIDE, - OFFICER, - PARASOL_LADY, - PILOT, - POKEFAN, - PRESCHOOLER, - PSYCHIC, - RANGER, - RICH, - RICH_KID, - ROUGHNECK, - SAILOR, - SCIENTIST, - SMASHER, - SNOW_WORKER, - STRIKER, - SCHOOL_KID, - SWIMMER, - TWINS, - VETERAN, - WAITER, - WORKER, - YOUNGSTER, - ROCKET_GRUNT, - ARCHER, - ARIANA, - PROTON, - PETREL, - MAGMA_GRUNT, - TABITHA, - COURTNEY, - AQUA_GRUNT, - MATT, - SHELLY, - GALACTIC_GRUNT, - JUPITER, - MARS, - SATURN, - PLASMA_GRUNT, - ZINZOLIN, - ROOD, - FLARE_GRUNT, - BRYONY, - XEROSIC, - AETHER_GRUNT, - FABA, - SKULL_GRUNT, - PLUMERIA, - MACRO_GRUNT, - OLEANA, - ROCKET_BOSS_GIOVANNI_1, - ROCKET_BOSS_GIOVANNI_2, - MAXIE, - MAXIE_2, - ARCHIE, - ARCHIE_2, - CYRUS, - CYRUS_2, - GHETSIS, - GHETSIS_2, - LYSANDRE, - LYSANDRE_2, - LUSAMINE, - LUSAMINE_2, - GUZMA, - GUZMA_2, - ROSE, - ROSE_2, + ACE_TRAINER, + ARTIST, + BACKERS, + BACKPACKER, + BAKER, + BEAUTY, + BIKER, + BLACK_BELT, + BREEDER, + CLERK, + CYCLIST, + DANCER, + DEPOT_AGENT, + DOCTOR, + FIREBREATHER, + FISHERMAN, + GUITARIST, + HARLEQUIN, + HIKER, + HOOLIGANS, + HOOPSTER, + INFIELDER, + JANITOR, + LINEBACKER, + MAID, + MUSICIAN, + HEX_MANIAC, + NURSERY_AIDE, + OFFICER, + PARASOL_LADY, + PILOT, + POKEFAN, + PRESCHOOLER, + PSYCHIC, + RANGER, + RICH, + RICH_KID, + ROUGHNECK, + SAILOR, + SCIENTIST, + SMASHER, + SNOW_WORKER, + STRIKER, + SCHOOL_KID, + SWIMMER, + TWINS, + VETERAN, + WAITER, + WORKER, + YOUNGSTER, + ROCKET_GRUNT, + ARCHER, + ARIANA, + PROTON, + PETREL, + MAGMA_GRUNT, + TABITHA, + COURTNEY, + AQUA_GRUNT, + MATT, + SHELLY, + GALACTIC_GRUNT, + JUPITER, + MARS, + SATURN, + PLASMA_GRUNT, + ZINZOLIN, + ROOD, + FLARE_GRUNT, + BRYONY, + XEROSIC, + AETHER_GRUNT, + FABA, + SKULL_GRUNT, + PLUMERIA, + MACRO_GRUNT, + OLEANA, + STAR_GRUNT, + GIACOMO, + MELA, + ATTICUS, + ORTEGA, + ERI, + ROCKET_BOSS_GIOVANNI_1, + ROCKET_BOSS_GIOVANNI_2, + MAXIE, + MAXIE_2, + ARCHIE, + ARCHIE_2, + CYRUS, + CYRUS_2, + GHETSIS, + GHETSIS_2, + LYSANDRE, + LYSANDRE_2, + LUSAMINE, + LUSAMINE_2, + GUZMA, + GUZMA_2, + ROSE, + ROSE_2, + PENNY, + PENNY_2, + BUCK, + CHERYL, + MARLEY, + MIRA, + RILEY, + VICTOR, + VICTORIA, + VIVI, + VICKY, + VITO, + BUG_TYPE_SUPERFAN, + EXPERT_POKEMON_BREEDER, - BROCK = 200, - MISTY, - LT_SURGE, - ERIKA, - JANINE, - SABRINA, - BLAINE, - GIOVANNI, - FALKNER, - BUGSY, - WHITNEY, - MORTY, - CHUCK, - JASMINE, - PRYCE, - CLAIR, - ROXANNE, - BRAWLY, - WATTSON, - FLANNERY, - NORMAN, - WINONA, - TATE, - LIZA, - JUAN, - ROARK, - GARDENIA, - MAYLENE, - CRASHER_WAKE, - FANTINA, - BYRON, - CANDICE, - VOLKNER, - CILAN, - CHILI, - CRESS, - CHEREN, - LENORA, - ROXIE, - BURGH, - ELESA, - CLAY, - SKYLA, - BRYCEN, - DRAYDEN, - MARLON, - VIOLA, - GRANT, - KORRINA, - RAMOS, - CLEMONT, - VALERIE, - OLYMPIA, - WULFRIC, - MILO, - NESSA, - KABU, - BEA, - ALLISTER, - OPAL, - BEDE, - GORDIE, - MELONY, - PIERS, - MARNIE, - RAIHAN, - KATY, - BRASSIUS, - IONO, - KOFU, - LARRY, - RYME, - TULIP, - GRUSHA, - LORELEI = 300, - BRUNO, - AGATHA, - LANCE, - WILL, - KOGA, - KAREN, - SIDNEY, - PHOEBE, - GLACIA, - DRAKE, - AARON, - BERTHA, - FLINT, - LUCIAN, - SHAUNTAL, - MARSHAL, - GRIMSLEY, - CAITLIN, - MALVA, - SIEBOLD, - WIKSTROM, - DRASNA, - HALA, - MOLAYNE, - OLIVIA, - ACEROLA, - KAHILI, - MARNIE_ELITE, - NESSA_ELITE, - BEA_ELITE, - ALLISTER_ELITE, - RAIHAN_ELITE, - RIKA, - POPPY, - LARRY_ELITE, - HASSEL, - CRISPIN, - AMARYS, - LACEY, - DRAYTON, - BLUE = 350, - RED, - LANCE_CHAMPION, - STEVEN, - WALLACE, - CYNTHIA, - ALDER, - IRIS, - DIANTHA, - HAU, - LEON, - GEETA, - NEMONA, - KIERAN, - RIVAL = 375, - RIVAL_2, - RIVAL_3, - RIVAL_4, - RIVAL_5, - RIVAL_6 + BROCK = 200, + MISTY, + LT_SURGE, + ERIKA, + JANINE, + SABRINA, + BLAINE, + GIOVANNI, + FALKNER, + BUGSY, + WHITNEY, + MORTY, + CHUCK, + JASMINE, + PRYCE, + CLAIR, + ROXANNE, + BRAWLY, + WATTSON, + FLANNERY, + NORMAN, + WINONA, + TATE, + LIZA, + JUAN, + ROARK, + GARDENIA, + MAYLENE, + CRASHER_WAKE, + FANTINA, + BYRON, + CANDICE, + VOLKNER, + CILAN, + CHILI, + CRESS, + CHEREN, + LENORA, + ROXIE, + BURGH, + ELESA, + CLAY, + SKYLA, + BRYCEN, + DRAYDEN, + MARLON, + VIOLA, + GRANT, + KORRINA, + RAMOS, + CLEMONT, + VALERIE, + OLYMPIA, + WULFRIC, + MILO, + NESSA, + KABU, + BEA, + ALLISTER, + OPAL, + BEDE, + GORDIE, + MELONY, + PIERS, + MARNIE, + RAIHAN, + KATY, + BRASSIUS, + IONO, + KOFU, + LARRY, + RYME, + TULIP, + GRUSHA, + LORELEI = 300, + BRUNO, + AGATHA, + LANCE, + WILL, + KOGA, + KAREN, + SIDNEY, + PHOEBE, + GLACIA, + DRAKE, + AARON, + BERTHA, + FLINT, + LUCIAN, + SHAUNTAL, + MARSHAL, + GRIMSLEY, + CAITLIN, + MALVA, + SIEBOLD, + WIKSTROM, + DRASNA, + HALA, + MOLAYNE, + OLIVIA, + ACEROLA, + KAHILI, + MARNIE_ELITE, + NESSA_ELITE, + BEA_ELITE, + ALLISTER_ELITE, + RAIHAN_ELITE, + RIKA, + POPPY, + LARRY_ELITE, + HASSEL, + CRISPIN, + AMARYS, + LACEY, + DRAYTON, + BLUE = 350, + RED, + LANCE_CHAMPION, + STEVEN, + WALLACE, + CYNTHIA, + ALDER, + IRIS, + DIANTHA, + HAU, + LEON, + GEETA, + NEMONA, + KIERAN, + RIVAL = 375, + RIVAL_2, + RIVAL_3, + RIVAL_4, + RIVAL_5, + RIVAL_6 } diff --git a/src/enums/variant-tiers.ts b/src/enums/variant-tiers.ts deleted file mode 100644 index 20a0e8ec4e4..00000000000 --- a/src/enums/variant-tiers.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum VariantTier { - COMMON, - RARE, - EPIC -} diff --git a/src/field/arena.ts b/src/field/arena.ts index 9f0a9691dee..9897da7cfd7 100644 --- a/src/field/arena.ts +++ b/src/field/arena.ts @@ -1,19 +1,19 @@ import BattleScene from "../battle-scene"; -import { BiomePoolTier, PokemonPools, BiomeTierTrainerPools, biomePokemonPools, biomeTrainerPools } from "../data/biomes"; +import { biomePokemonPools, BiomePoolTier, BiomeTierTrainerPools, biomeTrainerPools, PokemonPools } from "../data/biomes"; import { Constructor } from "#app/utils"; import * as Utils from "../utils"; import PokemonSpecies, { getPokemonSpecies } from "../data/pokemon-species"; -import { Weather, WeatherType, getTerrainClearMessage, getTerrainStartMessage, getWeatherClearMessage, getWeatherStartMessage } from "../data/weather"; +import { getTerrainClearMessage, getTerrainStartMessage, getWeatherClearMessage, getWeatherStartMessage, Weather, WeatherType } from "../data/weather"; import { CommonAnim } from "../data/battle-anims"; import { Type } from "../data/type"; import Move from "../data/move"; import { ArenaTag, ArenaTagSide, ArenaTrapTag, getArenaTag } from "../data/arena-tag"; import { BattlerIndex } from "../battle"; import { Terrain, TerrainType } from "../data/terrain"; -import { PostTerrainChangeAbAttr, PostWeatherChangeAbAttr, applyPostTerrainChangeAbAttrs, applyPostWeatherChangeAbAttrs } from "../data/ability"; +import { applyPostTerrainChangeAbAttrs, applyPostWeatherChangeAbAttrs, PostTerrainChangeAbAttr, PostWeatherChangeAbAttr } from "../data/ability"; import Pokemon from "./pokemon"; import Overrides from "#app/overrides"; -import { WeatherChangedEvent, TerrainChangedEvent, TagAddedEvent, TagRemovedEvent } from "../events/arena"; +import { TagAddedEvent, TagRemovedEvent, TerrainChangedEvent, WeatherChangedEvent } from "../events/arena"; import { ArenaTagType } from "#enums/arena-tag-type"; import { Biome } from "#enums/biome"; import { Moves } from "#enums/moves"; @@ -76,21 +76,21 @@ export class Arena { } } - randomSpecies(waveIndex: integer, level: integer, attempt?: integer, luckValue?: integer): PokemonSpecies { + randomSpecies(waveIndex: integer, level: integer, attempt?: integer, luckValue?: integer, isBoss?: boolean): PokemonSpecies { const overrideSpecies = this.scene.gameMode.getOverrideSpecies(waveIndex); if (overrideSpecies) { return overrideSpecies; } - const isBoss = !!this.scene.getEncounterBossSegments(waveIndex, level) && !!this.pokemonPool[BiomePoolTier.BOSS].length + const isBossSpecies = !!this.scene.getEncounterBossSegments(waveIndex, level) && !!this.pokemonPool[BiomePoolTier.BOSS].length && (this.biomeType !== Biome.END || this.scene.gameMode.isClassic || this.scene.gameMode.isWaveFinal(waveIndex)); - const randVal = isBoss ? 64 : 512; + const randVal = isBossSpecies ? 64 : 512; // luck influences encounter rarity let luckModifier = 0; if (typeof luckValue !== "undefined") { - luckModifier = luckValue * (isBoss ? 0.5 : 2); + luckModifier = luckValue * (isBossSpecies ? 0.5 : 2); } const tierValue = Utils.randSeedInt(randVal - luckModifier); - let tier = !isBoss + let tier = !isBossSpecies ? tierValue >= 156 ? BiomePoolTier.COMMON : tierValue >= 32 ? BiomePoolTier.UNCOMMON : tierValue >= 6 ? BiomePoolTier.RARE : tierValue >= 1 ? BiomePoolTier.SUPER_RARE : BiomePoolTier.ULTRA_RARE : tierValue >= 20 ? BiomePoolTier.BOSS : tierValue >= 6 ? BiomePoolTier.BOSS_RARE : tierValue >= 1 ? BiomePoolTier.BOSS_SUPER_RARE : BiomePoolTier.BOSS_ULTRA_RARE; console.log(BiomePoolTier[tier]); @@ -149,7 +149,7 @@ export class Arena { return this.randomSpecies(waveIndex, level, (attempt || 0) + 1); } - const newSpeciesId = ret.getWildSpeciesForLevel(level, true, isBoss, this.scene.gameMode); + const newSpeciesId = ret.getWildSpeciesForLevel(level, true, isBoss ?? isBossSpecies, this.scene.gameMode); if (newSpeciesId !== ret.speciesId) { console.log("Replaced", Species[ret.speciesId], "with", Species[newSpeciesId]); ret = getPokemonSpecies(newSpeciesId); @@ -157,12 +157,12 @@ export class Arena { return ret; } - randomTrainerType(waveIndex: integer): TrainerType { - const isBoss = !!this.trainerPool[BiomePoolTier.BOSS].length - && this.scene.gameMode.isTrainerBoss(waveIndex, this.biomeType, this.scene.offsetGym); + randomTrainerType(waveIndex: integer, isBoss: boolean = false): TrainerType { + const isTrainerBoss = !!this.trainerPool[BiomePoolTier.BOSS].length + && (this.scene.gameMode.isTrainerBoss(waveIndex, this.biomeType, this.scene.offsetGym) || isBoss); console.log(isBoss, this.trainerPool); - const tierValue = Utils.randSeedInt(!isBoss ? 512 : 64); - let tier = !isBoss + const tierValue = Utils.randSeedInt(!isTrainerBoss ? 512 : 64); + let tier = !isTrainerBoss ? tierValue >= 156 ? BiomePoolTier.COMMON : tierValue >= 32 ? BiomePoolTier.UNCOMMON : tierValue >= 6 ? BiomePoolTier.RARE : tierValue >= 1 ? BiomePoolTier.SUPER_RARE : BiomePoolTier.ULTRA_RARE : tierValue >= 20 ? BiomePoolTier.BOSS : tierValue >= 6 ? BiomePoolTier.BOSS_RARE : tierValue >= 1 ? BiomePoolTier.BOSS_SUPER_RARE : BiomePoolTier.BOSS_ULTRA_RARE; console.log(BiomePoolTier[tier]); @@ -320,7 +320,7 @@ export class Arena { this.eventTarget.dispatchEvent(new WeatherChangedEvent(oldWeatherType, this.weather?.weatherType!, this.weather?.turnsLeft!)); // TODO: is this bang correct? if (this.weather) { - this.scene.unshiftPhase(new CommonAnimPhase(this.scene, undefined, undefined, CommonAnim.SUNNY + (weather - 1))); + this.scene.unshiftPhase(new CommonAnimPhase(this.scene, undefined, undefined, CommonAnim.SUNNY + (weather - 1), true)); this.scene.queueMessage(getWeatherStartMessage(weather)!); // TODO: is this bang correct? } else { this.scene.queueMessage(getWeatherClearMessage(oldWeatherType)!); // TODO: is this bang correct? @@ -746,7 +746,7 @@ export class Arena { case Biome.TOWN: return 7.288; case Biome.PLAINS: - return 7.693; + return 17.485; case Biome.GRASS: return 1.995; case Biome.TALL_GRASS: @@ -762,7 +762,7 @@ export class Arena { case Biome.BEACH: return 3.462; case Biome.LAKE: - return 5.350; + return 7.215; case Biome.SEABED: return 2.600; case Biome.MOUNTAIN: @@ -774,13 +774,13 @@ export class Arena { case Biome.DESERT: return 1.143; case Biome.ICE_CAVE: - return 15.010; + return 0.000; case Biome.MEADOW: return 3.891; case Biome.POWER_PLANT: - return 2.810; + return 9.447; case Biome.VOLCANO: - return 5.116; + return 17.637; case Biome.GRAVEYARD: return 3.232; case Biome.DOJO: @@ -788,7 +788,7 @@ export class Arena { case Biome.FACTORY: return 4.985; case Biome.RUINS: - return 2.270; + return 0.000; case Biome.WASTELAND: return 6.336; case Biome.ABYSS: diff --git a/src/field/mystery-encounter-intro.ts b/src/field/mystery-encounter-intro.ts new file mode 100644 index 00000000000..7c58a494699 --- /dev/null +++ b/src/field/mystery-encounter-intro.ts @@ -0,0 +1,456 @@ +import { GameObjects } from "phaser"; +import BattleScene from "../battle-scene"; +import MysteryEncounter from "../data/mystery-encounters/mystery-encounter"; +import { Species } from "#enums/species"; +import { isNullOrUndefined } from "#app/utils"; +import { getSpriteKeysFromSpecies } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import PlayAnimationConfig = Phaser.Types.Animations.PlayAnimationConfig; + +type KnownFileRoot = + | "arenas" + | "battle_anims" + | "cg" + | "character" + | "effect" + | "egg" + | "events" + | "inputs" + | "items" + | "mystery-encounters" + | "pokeball" + | "pokemon" + | "pokemon/back" + | "pokemon/exp" + | "pokemon/female" + | "pokemon/icons" + | "pokemon/input" + | "pokemon/shiny" + | "pokemon/variant" + | "statuses" + | "trainer" + | "ui"; + +export class MysteryEncounterSpriteConfig { + /** The sprite key (which is the image file name). e.g. "ace_trainer_f" */ + spriteKey: string; + /** Refer to [/public/images](../../public/images) directorty for all folder names */ + fileRoot: KnownFileRoot & string | string; + /** Optional replacement for `spriteKey`/`fileRoot`. Just know this defaults to male/genderless, form 0, no shiny */ + species?: Species; + /** Enable shadow. Defaults to `false` */ + hasShadow?: boolean = false; + /** Disable animation. Defaults to `false` */ + disableAnimation?: boolean = false; + /** Repeat the animation. Defaults to `false` */ + repeat?: boolean = false; + /** What frame of the animation to start on. Defaults to 0 */ + startFrame?: number = 0; + /** Hidden at start of encounter. Defaults to `false` */ + hidden?: boolean = false; + /** Tint color. `0` - `1`. Higher means darker tint. */ + tint?: number; + /** X offset */ + x?: number; + /** Y offset */ + y?: number; + /** Y shadow offset */ + yShadow?: number; + /** Sprite scale. `0` - `n` */ + scale?: number; + /** If you are using a Pokemon sprite, set to `true`. This will ensure variant, form, gender, shiny sprites are loaded properly */ + isPokemon?: boolean; + /** If you are using an item sprite, set to `true` */ + isItem?: boolean; + /** The sprites alpha. `0` - `1` The lower the number, the more transparent */ + alpha?: number; +} + +/** + * When a mystery encounter spawns, there are visuals (mainly sprites) tied to the field for the new encounter to inform the player of the type of encounter + * These slide in with the field as part of standard field change cycle, and will typically be hidden after the player has selected an option for the encounter + * Note: intro visuals are not "Trainers" or any other specific game object, though they may contain trainer sprites + */ +export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Container { + public encounter: MysteryEncounter; + public spriteConfigs: MysteryEncounterSpriteConfig[]; + public enterFromRight: boolean; + + constructor(scene: BattleScene, encounter: MysteryEncounter) { + super(scene, -72, 76); + this.encounter = encounter; + this.enterFromRight = encounter.enterIntroVisualsFromRight ?? false; + // Shallow copy configs to allow visual config updates at runtime without dirtying master copy of Encounter + this.spriteConfigs = encounter.spriteConfigs.map(config => { + const result = { + ...config + }; + + if (!isNullOrUndefined(result.species)) { + const keys = getSpriteKeysFromSpecies(result.species!); + result.spriteKey = keys.spriteKey; + result.fileRoot = keys.fileRoot; + result.isPokemon = true; + } + + return result; + }); + if (!this.spriteConfigs) { + return; + } + + const getSprite = (spriteKey: string, hasShadow?: boolean, yShadow?: number) => { + const ret = this.scene.addFieldSprite(0, 0, spriteKey); + ret.setOrigin(0.5, 1); + ret.setPipeline(this.scene.spritePipeline, { tone: [0.0, 0.0, 0.0, 0.0], hasShadow: !!hasShadow, yShadowOffset: yShadow ?? 0 }); + return ret; + }; + + const getItemSprite = (spriteKey: string, hasShadow?: boolean, yShadow?: number) => { + const icon = this.scene.add.sprite(-19, 2, "items", spriteKey); + icon.setOrigin(0.5, 1); + icon.setPipeline(this.scene.spritePipeline, { tone: [0.0, 0.0, 0.0, 0.0], hasShadow: !!hasShadow, yShadowOffset: yShadow ?? 0 }); + return icon; + }; + + // Depending on number of sprites added, should space them to be on the circular field sprite + const minX = -40; + const maxX = 40; + const origin = 4; + let n = 0; + // Sprites with custom X or Y defined will not count for normal spacing requirements + const spacingValue = Math.round((maxX - minX) / Math.max(this.spriteConfigs.filter(s => !s.x && !s.y).length, 1)); + + this.spriteConfigs?.forEach((config) => { + const { spriteKey, isItem, hasShadow, scale, x, y, yShadow, alpha } = config; + + let sprite: GameObjects.Sprite; + let tintSprite: GameObjects.Sprite; + + if (!isItem) { + sprite = getSprite(spriteKey, hasShadow, yShadow); + tintSprite = getSprite(spriteKey); + } else { + sprite = getItemSprite(spriteKey, hasShadow, yShadow); + tintSprite = getItemSprite(spriteKey); + } + + sprite.setVisible(!config.hidden); + tintSprite.setVisible(false); + + if (scale) { + sprite.setScale(scale); + tintSprite.setScale(scale); + } + + // Sprite offset from origin + if (x || y) { + if (x) { + sprite.setPosition(origin + x, sprite.y); + tintSprite.setPosition(origin + x, tintSprite.y); + } + if (y) { + sprite.setPosition(sprite.x, sprite.y + y); + tintSprite.setPosition(tintSprite.x, tintSprite.y + y); + } + } else { + // Single sprite + if (this.spriteConfigs.length === 1) { + sprite.x = origin; + tintSprite.x = origin; + } else { + // Do standard sprite spacing (not including offset sprites) + sprite.x = minX + (n + 0.5) * spacingValue + origin; + tintSprite.x = minX + (n + 0.5) * spacingValue + origin; + n++; + } + } + + if (!isNullOrUndefined(alpha)) { + sprite.setAlpha(alpha); + tintSprite.setAlpha(alpha); + } + + this.add(sprite); + this.add(tintSprite); + }); + } + + /** + * Loads the assets that were defined on construction (async) + */ + loadAssets(): Promise { + return new Promise(resolve => { + if (!this.spriteConfigs) { + resolve(); + } + + this.spriteConfigs.forEach((config) => { + if (config.isPokemon) { + this.scene.loadPokemonAtlas(config.spriteKey, config.fileRoot); + } else if (config.isItem) { + this.scene.loadAtlas("items", ""); + } else { + this.scene.loadAtlas(config.spriteKey, config.fileRoot); + } + }); + + this.scene.load.once(Phaser.Loader.Events.COMPLETE, () => { + this.spriteConfigs.every((config) => { + if (config.isItem) { + return true; + } + + const originalWarn = console.warn; + + // Ignore warnings for missing frames, because there will be a lot + console.warn = () => { + }; + const frameNames = this.scene.anims.generateFrameNames(config.spriteKey, { zeroPad: 4, suffix: ".png", start: 1, end: 128 }); + + console.warn = originalWarn; + if (!(this.scene.anims.exists(config.spriteKey))) { + this.scene.anims.create({ + key: config.spriteKey, + frames: frameNames, + frameRate: 12, + repeat: -1 + }); + } + + return true; + }); + + resolve(); + }); + + if (!this.scene.load.isLoading()) { + this.scene.load.start(); + } + }); + } + + /** + * Sets the initial frames and tint of sprites after load + */ + initSprite(): void { + if (!this.spriteConfigs) { + return; + } + + this.getSprites().map((sprite, i) => { + if (!this.spriteConfigs[i].isItem) { + sprite.setTexture(this.spriteConfigs[i].spriteKey).setFrame(0); + } + }); + this.getTintSprites().map((tintSprite, i) => { + if (!this.spriteConfigs[i].isItem) { + tintSprite.setTexture(this.spriteConfigs[i].spriteKey).setFrame(0); + } + }); + + this.spriteConfigs.every((config, i) => { + if (!config.tint) { + return true; + } + + const tintSprite = this.getAt(i * 2 + 1); + this.tint(tintSprite, 0, config.tint); + + return true; + }); + } + + /** + * Attempts to animate a given set of {@linkcode Phaser.GameObjects.Sprite} + * @see {@linkcode Phaser.GameObjects.Sprite.play} + * @param sprite {@linkcode Phaser.GameObjects.Sprite} to animate + * @param tintSprite {@linkcode Phaser.GameObjects.Sprite} placed on top of the sprite to add a color tint + * @param animConfig {@linkcode Phaser.Types.Animations.PlayAnimationConfig} to pass to {@linkcode Phaser.GameObjects.Sprite.play} + * @returns true if the sprite was able to be animated + */ + tryPlaySprite(sprite: Phaser.GameObjects.Sprite, tintSprite: Phaser.GameObjects.Sprite, animConfig: Phaser.Types.Animations.PlayAnimationConfig): boolean { + // Show an error in the console if there isn't a texture loaded + if (sprite.texture.key === "__MISSING") { + console.error(`No texture found for '${animConfig.key}'!`); + + return false; + } + // Don't try to play an animation when there isn't one + if (sprite.texture.frameTotal <= 1) { + console.warn(`No animation found for '${animConfig.key}'. Is this intentional?`); + + return false; + } + + sprite.play(animConfig); + tintSprite.play(animConfig); + + return true; + } + + /** + * For sprites with animation and that do not have animation disabled, will begin frame animation + */ + playAnim(): void { + if (!this.spriteConfigs) { + return; + } + + const sprites = this.getSprites(); + const tintSprites = this.getTintSprites(); + this.spriteConfigs.forEach((config, i) => { + if (!config.disableAnimation) { + const trainerAnimConfig: PlayAnimationConfig = { + key: config.spriteKey, + repeat: config?.repeat ? -1 : 0, + startFrame: config?.startFrame ?? 0 + }; + + this.tryPlaySprite(sprites[i], tintSprites[i], trainerAnimConfig); + } + }); + } + + /** + * Returns a Sprite/TintSprite pair + * @param index + */ + getSpriteAtIndex(index: number): Phaser.GameObjects.Sprite[] { + if (!this.spriteConfigs) { + return []; + } + + const ret: Phaser.GameObjects.Sprite[] = []; + ret.push(this.getAt(index * 2)); // Sprite + ret.push(this.getAt(index * 2 + 1)); // Tint Sprite + + return ret; + } + + /** + * Gets all non-tint sprites (these are the "real" unmodified sprites) + */ + getSprites(): Phaser.GameObjects.Sprite[] { + if (!this.spriteConfigs) { + return []; + } + + const ret: Phaser.GameObjects.Sprite[] = []; + this.spriteConfigs.forEach((config, i) => { + ret.push(this.getAt(i * 2)); + }); + return ret; + } + + /** + * Gets all tint sprites (duplicate sprites that have different alpha and fill values) + */ + getTintSprites(): Phaser.GameObjects.Sprite[] { + if (!this.spriteConfigs) { + return []; + } + + const ret: Phaser.GameObjects.Sprite[] = []; + this.spriteConfigs.forEach((config, i) => { + ret.push(this.getAt(i * 2 + 1)); + }); + + return ret; + } + + /** + * Tints a single sprite + * @param sprite + * @param color + * @param alpha + * @param duration + * @param ease + */ + private tint(sprite, color: number, alpha?: number, duration?: integer, ease?: string): void { + // const tintSprites = this.getTintSprites(); + sprite.setTintFill(color); + sprite.setVisible(true); + + if (duration) { + sprite.setAlpha(0); + + this.scene.tweens.add({ + targets: sprite, + alpha: alpha || 1, + duration: duration, + ease: ease || "Linear" + }); + } else { + sprite.setAlpha(alpha); + } + } + + /** + * Tints all sprites + * @param color + * @param alpha + * @param duration + * @param ease + */ + tintAll(color: number, alpha?: number, duration?: integer, ease?: string): void { + const tintSprites = this.getTintSprites(); + tintSprites.map(tintSprite => { + this.tint(tintSprite, color, alpha, duration, ease); + }); + } + + /** + * Untints a single sprite over a duration + * @param sprite + * @param duration + * @param ease + */ + private untint(sprite, duration: integer, ease?: string): void { + if (duration) { + this.scene.tweens.add({ + targets: sprite, + alpha: 0, + duration: duration, + ease: ease || "Linear", + onComplete: () => { + sprite.setVisible(false); + sprite.setAlpha(1); + } + }); + } else { + sprite.setVisible(false); + sprite.setAlpha(1); + } + } + + /** + * Untints all sprites + * @param sprite + * @param duration + * @param ease + */ + untintAll(duration: integer, ease?: string): void { + const tintSprites = this.getTintSprites(); + tintSprites.map(tintSprite => { + this.untint(tintSprite, duration, ease); + }); + } + + /** + * Sets container and all child sprites to visible + * @param value - true for visible, false for hidden + */ + setVisible(value: boolean): this { + this.getSprites().forEach(sprite => { + sprite.setVisible(value); + }); + return super.setVisible(value); + } +} + +/** + * Interface is required so as not to override {@link Phaser.GameObjects.Container.scene} + */ +export default interface MysteryEncounterIntroVisuals { + scene: BattleScene +} diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 566eecbfeb6..e17272cd955 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -3,21 +3,21 @@ import BattleScene, { AnySound } from "../battle-scene"; import { Variant, VariantSet, variantColorCache } from "#app/data/variant"; import { variantData } from "#app/data/variant"; import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from "../ui/battle-info"; -import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, VariableMoveTypeAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatStagesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatStageChangeAttr, RechargeAttr, ChargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, OneHitKOAccuracyAttr, RespectAttackTypeImmunityAttr } from "../data/move"; +import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, VariableMoveTypeAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatStagesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatStageChangeAttr, RechargeAttr, ChargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, OneHitKOAccuracyAttr, RespectAttackTypeImmunityAttr, MoveTarget } from "../data/move"; import { default as PokemonSpecies, PokemonSpeciesForm, SpeciesFormKey, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm, getStarterValueFriendshipCap, speciesStarters, starterPassiveAbilities } from "../data/pokemon-species"; -import { Constructor } from "#app/utils"; +import { Constructor, isNullOrUndefined, randSeedInt } from "#app/utils"; import * as Utils from "../utils"; import { Type, TypeDamageMultiplier, getTypeDamageMultiplier, getTypeRgb } from "../data/type"; import { getLevelTotalExp } from "../data/exp"; import { Stat, type PermanentStat, type BattleStat, type EffectiveStat, PERMANENT_STATS, BATTLE_STATS, EFFECTIVE_STATS } from "#enums/stat"; -import { DamageMoneyRewardModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, HiddenAbilityRateBoosterModifier, BaseStatModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonNatureWeightModifier, ShinyRateBoosterModifier, SurviveDamageModifier, TempStatStageBoosterModifier, TempCritBoosterModifier, StatBoosterModifier, CritBoosterModifier, TerastallizeModifier } from "../modifier/modifier"; +import { DamageMoneyRewardModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, HiddenAbilityRateBoosterModifier, BaseStatModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonNatureWeightModifier, ShinyRateBoosterModifier, SurviveDamageModifier, TempStatStageBoosterModifier, TempCritBoosterModifier, StatBoosterModifier, CritBoosterModifier, TerastallizeModifier, PokemonBaseStatFlatModifier, PokemonBaseStatTotalModifier, PokemonIncrementingStatModifier, EvoTrackerModifier } from "../modifier/modifier"; import { PokeballType } from "../data/pokeball"; import { Gender } from "../data/gender"; import { initMoveAnim, loadMoveAnimAssets } from "../data/battle-anims"; import { Status, StatusEffect, getRandomStatus } from "../data/status-effect"; import { pokemonEvolutions, pokemonPrevolutions, SpeciesFormEvolution, SpeciesEvolutionCondition, FusionSpeciesFormEvolution } from "../data/pokemon-evolutions"; import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "../data/tms"; -import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, MoveRestrictionBattlerTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag, TarShotTag } from "../data/battler-tags"; +import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, SubstituteTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, MoveRestrictionBattlerTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag, TarShotTag } from "../data/battler-tags"; import { WeatherType } from "../data/weather"; import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "../data/arena-tag"; import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr } from "../data/ability"; @@ -58,7 +58,9 @@ import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; import { SwitchSummonPhase } from "#app/phases/switch-summon-phase"; import { ToggleDoublePositionPhase } from "#app/phases/toggle-double-position-phase"; import { Challenges } from "#enums/challenges"; +import { PokemonAnimType } from "#app/enums/pokemon-anim-type"; import { PLAYER_PARTY_MAX_SIZE } from "#app/constants"; +import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data"; export enum FieldPosition { CENTER, @@ -93,10 +95,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { public metLevel: integer; public metBiome: Biome | -1; public metSpecies: Species; + public metWave: number; public luck: integer; public pauseEvolutions: boolean; public pokerus: boolean; - public wildFlee: boolean; + public switchOutStatus: boolean; + public evoCounter: integer; public fusionSpecies: PokemonSpecies | null; public fusionFormIndex: integer; @@ -105,6 +109,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { public fusionVariant: Variant; public fusionGender: Gender; public fusionLuck: integer; + public fusionMysteryEncounterPokemonData: MysteryEncounterPokemonData | null; private summonDataPrimer: PokemonSummonData | null; @@ -112,6 +117,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { public battleData: PokemonBattleData; public battleSummonData: PokemonBattleSummonData; public turnData: PokemonTurnData; + public mysteryEncounterPokemonData: MysteryEncounterPokemonData; + + /** Used by Mystery Encounters to execute pokemon-specific logic (such as stat boosts) at start of battle */ + public mysteryEncounterBattleEffects?: (pokemon: Pokemon) => void; public fieldPosition: FieldPosition; @@ -137,7 +146,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.species = species; this.pokeball = dataSource?.pokeball || PokeballType.POKEBALL; this.level = level; - this.wildFlee = false; + this.switchOutStatus = false; // Determine the ability index if (abilityIndex !== undefined) { @@ -187,8 +196,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.luck = dataSource.luck; this.metBiome = dataSource.metBiome; this.metSpecies = dataSource.metSpecies ?? (this.metBiome !== -1 ? this.species.speciesId : this.species.getRootSpeciesId(true)); + this.metWave = dataSource.metWave ?? (this.metBiome === -1 ? -1 : 0); this.pauseEvolutions = dataSource.pauseEvolutions; this.pokerus = !!dataSource.pokerus; + this.evoCounter = dataSource.evoCounter ?? 0; this.fusionSpecies = dataSource.fusionSpecies instanceof PokemonSpecies ? dataSource.fusionSpecies : dataSource.fusionSpecies ? getPokemonSpecies(dataSource.fusionSpecies) : null; this.fusionFormIndex = dataSource.fusionFormIndex; this.fusionAbilityIndex = dataSource.fusionAbilityIndex; @@ -196,7 +207,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.fusionVariant = dataSource.fusionVariant || 0; this.fusionGender = dataSource.fusionGender; this.fusionLuck = dataSource.fusionLuck; + this.fusionMysteryEncounterPokemonData = dataSource.fusionMysteryEncounterPokemonData; this.usedTMs = dataSource.usedTMs ?? []; + this.mysteryEncounterPokemonData = new MysteryEncounterPokemonData(dataSource.mysteryEncounterPokemonData); } else { this.id = Utils.randSeedInt(4294967296); this.ivs = ivs || Utils.getIvsFromId(this.id); @@ -217,6 +230,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.variant = this.shiny ? this.generateVariant() : 0; } + this.mysteryEncounterPokemonData = new MysteryEncounterPokemonData(); + if (nature !== undefined) { this.setNature(nature); } else { @@ -229,6 +244,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.metLevel = level; this.metBiome = scene.currentBattle ? scene.arena.biomeType : -1; this.metSpecies = species.speciesId; + this.metWave = scene.currentBattle ? scene.currentBattle.waveIndex : -1; this.pokerus = false; if (level > 1) { @@ -318,9 +334,18 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @returns {boolean} True if pokemon is allowed in battle */ isAllowedInBattle(): boolean { + return !this.isFainted() && this.isAllowed(); + } + + /** + * Check if this pokemon is allowed (no challenge exclusion) + * This is frequently a better alternative to {@link isFainted} + * @returns {boolean} True if pokemon is allowed in battle + */ + isAllowed(): boolean { const challengeAllowed = new Utils.BooleanHolder(true); applyChallenges(this.scene.gameMode, ChallengeType.POKEMON_IN_BATTLE, this, challengeAllowed); - return !this.isFainted() && !this.wildFlee && challengeAllowed.value; + return challengeAllowed.value; } isActive(onField?: boolean): boolean { @@ -531,10 +556,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (!ignoreOverride && this.summonData?.speciesForm) { return this.summonData.speciesForm; } - if (!this.species.forms?.length) { - return this.species; + if (this.species.forms && this.species.forms.length > 0) { + return this.species.forms[this.formIndex]; } - return this.species.forms[this.formIndex]; + + return this.species; } getFusionSpeciesForm(ignoreOverride?: boolean): PokemonSpeciesForm { @@ -560,12 +586,30 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { getSpriteScale(): number { const formKey = this.getFormKey(); - if (formKey.indexOf(SpeciesFormKey.GIGANTAMAX) > -1 || formKey.indexOf(SpeciesFormKey.ETERNAMAX) > -1) { + if (this.isMax() === true || formKey === "segin-starmobile" || formKey === "schedar-starmobile" || formKey === "navi-starmobile" || formKey === "ruchbah-starmobile" || formKey === "caph-starmobile") { return 1.5; + } else if (this.mysteryEncounterPokemonData.spriteScale > 0) { + return this.mysteryEncounterPokemonData.spriteScale; } return 1; } + /** Resets the pokemon's field sprite properties, including position, alpha, and scale */ + resetSprite(): void { + // Resetting properties should not be shown on the field + this.setVisible(false); + + // Remove the offset from having a Substitute active + if (this.isOffsetBySubstitute()) { + this.x -= this.getSubstituteOffset()[0]; + this.y -= this.getSubstituteOffset()[1]; + } + + // Reset sprite display properties + this.setAlpha(1); + this.setScale(this.getSpriteScale()); + } + getHeldItems(): PokemonHeldItemModifier[] { if (!this.scene) { return []; @@ -640,6 +684,47 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } } + /** + * Returns the Pokemon's offset from its current field position in the event that + * it has a Substitute doll in effect. The offset is returned in `[ x, y ]` format. + * @see {@linkcode SubstituteTag} + * @see {@linkcode getFieldPositionOffset} + */ + getSubstituteOffset(): [ number, number ] { + return this.isPlayer() ? [-30, 10] : [30, -10]; + } + + /** + * Returns whether or not the Pokemon's position on the field is offset because + * the Pokemon has a Substitute active. + * @see {@linkcode SubstituteTag} + */ + isOffsetBySubstitute(): boolean { + const substitute = this.getTag(SubstituteTag); + if (substitute) { + if (substitute.sprite === undefined) { + return false; + } + + // During the Pokemon's MoveEffect phase, the offset is removed to put the Pokemon "in focus" + const currentPhase = this.scene.getCurrentPhase(); + if (currentPhase instanceof MoveEffectPhase && currentPhase.getPokemon() === this) { + return false; + } + return true; + } else { + return false; + } + } + + /** If this Pokemon has a Substitute on the field, removes its sprite from the field. */ + destroySubstitute(): void { + const substitute = this.getTag(SubstituteTag); + if (substitute && substitute.sprite) { + substitute.sprite.destroy(); + } + } + setFieldPosition(fieldPosition: FieldPosition, duration?: integer): Promise { return new Promise(resolve => { if (fieldPosition === this.fieldPosition) { @@ -791,22 +876,29 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @param stat the desired {@linkcode EffectiveStat} * @param opponent the target {@linkcode Pokemon} * @param move the {@linkcode Move} being used + * @param ignoreAbility determines whether this Pokemon's abilities should be ignored during the stat calculation + * @param ignoreOppAbility during an attack, determines whether the opposing Pokemon's abilities should be ignored during the stat calculation. * @param isCritical determines whether a critical hit has occurred or not (`false` by default) + * @param simulated if `true`, nullifies any effects that produce any changes to game state from triggering * @returns the final in-battle value of a stat */ - getEffectiveStat(stat: EffectiveStat, opponent?: Pokemon, move?: Move, isCritical: boolean = false): integer { + getEffectiveStat(stat: EffectiveStat, opponent?: Pokemon, move?: Move, ignoreAbility: boolean = false, ignoreOppAbility: boolean = false, isCritical: boolean = false, simulated: boolean = true): integer { const statValue = new Utils.NumberHolder(this.getStat(stat, false)); this.scene.applyModifiers(StatBoosterModifier, this.isPlayer(), this, stat, statValue); + // The Ruin abilities here are never ignored, but they reveal themselves on summon anyway const fieldApplied = new Utils.BooleanHolder(false); for (const pokemon of this.scene.getField(true)) { - applyFieldStatMultiplierAbAttrs(FieldMultiplyStatAbAttr, pokemon, stat, statValue, this, fieldApplied); + applyFieldStatMultiplierAbAttrs(FieldMultiplyStatAbAttr, pokemon, stat, statValue, this, fieldApplied, simulated); if (fieldApplied.value) { break; } } - applyStatMultiplierAbAttrs(StatMultiplierAbAttr, this, stat, statValue); - let ret = statValue.value * this.getStatStageMultiplier(stat, opponent, move, isCritical); + if (!ignoreAbility) { + applyStatMultiplierAbAttrs(StatMultiplierAbAttr, this, stat, statValue, simulated); + } + + let ret = statValue.value * this.getStatStageMultiplier(stat, opponent, move, ignoreOppAbility, isCritical, simulated); switch (stat) { case Stat.ATK: @@ -856,19 +948,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } // Get and manipulate base stats - const baseStats = this.getSpeciesForm(true).baseStats.slice(); - if (this.isFusion()) { - const fusionBaseStats = this.getFusionSpeciesForm(true).baseStats; - for (const s of PERMANENT_STATS) { - baseStats[s] = Math.ceil((baseStats[s] + fusionBaseStats[s]) / 2); - } - } else if (this.scene.gameMode.isSplicedOnly) { - for (const s of PERMANENT_STATS) { - baseStats[s] = Math.ceil(baseStats[s] / 2); - } - } - this.scene.applyModifiers(BaseStatModifier, this.isPlayer(), this, baseStats); - + const baseStats = this.calculateBaseStats(); // Using base stats, calculate and store stats one by one for (const s of PERMANENT_STATS) { let value = Math.floor(((2 * baseStats[s] + this.ivs[s]) * this.level) * 0.01); @@ -896,6 +976,29 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.setStat(s, value); } + this.scene.applyModifier(PokemonIncrementingStatModifier, this.isPlayer(), this, this.stats); + } + + calculateBaseStats(): number[] { + const baseStats = this.getSpeciesForm(true).baseStats.slice(0); + // Shuckle Juice + this.scene.applyModifiers(PokemonBaseStatTotalModifier, this.isPlayer(), this, baseStats); + // Old Gateau + this.scene.applyModifiers(PokemonBaseStatFlatModifier, this.isPlayer(), this, baseStats); + if (this.isFusion()) { + const fusionBaseStats = this.getFusionSpeciesForm(true).baseStats; + for (const s of PERMANENT_STATS) { + baseStats[s] = Math.ceil((baseStats[s] + fusionBaseStats[s]) / 2); + } + } else if (this.scene.gameMode.isSplicedOnly) { + for (const s of PERMANENT_STATS) { + baseStats[s] = Math.ceil(baseStats[s] / 2); + } + } + // Vitamins + this.scene.applyModifiers(BaseStatModifier, this.isPlayer(), this, baseStats); + + return baseStats; } getNature(): Nature { @@ -1063,8 +1166,31 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } if (!types.length || !includeTeraType) { - if (!ignoreOverride && this.summonData?.types && this.summonData.types.length !== 0) { + if (!ignoreOverride && this.summonData?.types && this.summonData.types.length > 0) { this.summonData.types.forEach(t => types.push(t)); + } else if (this.mysteryEncounterPokemonData.types && this.mysteryEncounterPokemonData.types.length > 0) { + // "Permanent" override for a Pokemon's normal types, currently only used by Mystery Encounters + types.push(this.mysteryEncounterPokemonData.types[0]); + + // Fusing a Pokemon onto something with "permanently changed" types will still apply the fusion's types as normal + const fusionSpeciesForm = this.getFusionSpeciesForm(ignoreOverride); + if (fusionSpeciesForm) { + // Check if the fusion Pokemon also had "permanently changed" types + const fusionMETypes = this.fusionMysteryEncounterPokemonData?.types; + if (fusionMETypes && fusionMETypes.length >= 2 && fusionMETypes[1] !== types[0]) { + types.push(fusionMETypes[1]); + } else if (fusionMETypes && fusionMETypes.length === 1 && fusionMETypes[0] !== types[0]) { + types.push(fusionMETypes[0]); + } else if (fusionSpeciesForm.type2 !== null && fusionSpeciesForm.type2 !== types[0]) { + types.push(fusionSpeciesForm.type2); + } else if (fusionSpeciesForm.type1 !== types[0]) { + types.push(fusionSpeciesForm.type1); + } + } + + if (types.length === 1 && this.mysteryEncounterPokemonData.types.length >= 2) { + types.push(this.mysteryEncounterPokemonData.types[1]); + } } else { const speciesForm = this.getSpeciesForm(ignoreOverride); @@ -1072,7 +1198,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const fusionSpeciesForm = this.getFusionSpeciesForm(ignoreOverride); if (fusionSpeciesForm) { - if (fusionSpeciesForm.type2 !== null && fusionSpeciesForm.type2 !== speciesForm.type1) { + // Check if the fusion Pokemon also had "permanently changed" types + // Otherwise, use standard fusion type logic + const fusionMETypes = this.fusionMysteryEncounterPokemonData?.types; + if (fusionMETypes && fusionMETypes.length >= 2 && fusionMETypes[1] !== types[0]) { + types.push(fusionMETypes[1]); + } else if (fusionMETypes && fusionMETypes.length === 1 && fusionMETypes[0] !== types[0]) { + types.push(fusionMETypes[0]); + } else if (fusionSpeciesForm.type2 !== null && fusionSpeciesForm.type2 !== speciesForm.type1) { types.push(fusionSpeciesForm.type2); } else if (fusionSpeciesForm.type1 !== speciesForm.type1) { types.push(fusionSpeciesForm.type1); @@ -1125,7 +1258,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return allAbilities[Overrides.OPP_ABILITY_OVERRIDE]; } if (this.isFusion()) { - return allAbilities[this.getFusionSpeciesForm(ignoreOverride).getAbility(this.fusionAbilityIndex)]; + if (!isNullOrUndefined(this.fusionMysteryEncounterPokemonData?.ability) && this.fusionMysteryEncounterPokemonData!.ability !== -1) { + return allAbilities[this.fusionMysteryEncounterPokemonData!.ability]; + } else { + return allAbilities[this.getFusionSpeciesForm(ignoreOverride).getAbility(this.fusionAbilityIndex)]; + } + } + if (!isNullOrUndefined(this.mysteryEncounterPokemonData.ability) && this.mysteryEncounterPokemonData.ability !== -1) { + return allAbilities[this.mysteryEncounterPokemonData.ability]; } let abilityId = this.getSpeciesForm(ignoreOverride).getAbility(this.abilityIndex); if (abilityId === Abilities.NONE) { @@ -1148,6 +1288,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (Overrides.OPP_PASSIVE_ABILITY_OVERRIDE && !this.isPlayer()) { return allAbilities[Overrides.OPP_PASSIVE_ABILITY_OVERRIDE]; } + if (!isNullOrUndefined(this.mysteryEncounterPokemonData.passive) && this.mysteryEncounterPokemonData.passive !== -1) { + return allAbilities[this.mysteryEncounterPokemonData.passive]; + } let starterSpeciesId = this.species.speciesId; while (pokemonPrevolutions.hasOwnProperty(starterSpeciesId)) { @@ -1163,13 +1306,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @param attrType {@linkcode AbAttr} The ability attribute to check for. * @param canApply {@linkcode Boolean} If false, it doesn't check whether the ability is currently active * @param ignoreOverride {@linkcode Boolean} If true, it ignores ability changing effects - * @returns {AbAttr[]} A list of all the ability attributes on this ability. + * @returns A list of all the ability attributes on this ability. */ - getAbilityAttrs(attrType: { new(...args: any[]): AbAttr }, canApply: boolean = true, ignoreOverride?: boolean): AbAttr[] { - const abilityAttrs: AbAttr[] = []; + getAbilityAttrs(attrType: { new(...args: any[]): T }, canApply: boolean = true, ignoreOverride?: boolean): T[] { + const abilityAttrs: T[] = []; if (!canApply || this.canApplyAbility()) { - abilityAttrs.push(...this.getAbility(ignoreOverride).getAttrs(attrType)); + abilityAttrs.push(...this.getAbility(ignoreOverride).getAttrs(attrType)); } if (!canApply || this.canApplyAbility(true)) { @@ -1414,6 +1557,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { applyPreDefendAbAttrs(FullHpResistTypeAbAttr, this, source, move, cancelledHolder, simulated, typeMultiplier); } + if (move.category === MoveCategory.STATUS && move.hitsSubstitute(source, this)) { + typeMultiplier.value = 0; + } + return (!cancelledHolder.value ? typeMultiplier.value : 0) as TypeDamageMultiplier; } @@ -1634,6 +1781,15 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } + /** + * Get a list of all egg moves + * + * @returns list of egg moves + */ + getEggMoves() : Moves[] { + return speciesEggMoves[this.species.speciesId]; + } + setMove(moveIndex: integer, moveId: Moves): void { const move = moveId ? new PokemonMove(moveId) : null; this.moveset[moveIndex] = move; @@ -1688,6 +1844,42 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return this.shiny; } + /** + * Function that tries to set a Pokemon shiny based on seed. + * For manual use only, usually to roll a Pokemon's shiny chance a second time. + * + * The base shiny odds are {@linkcode baseShinyChance} / 65536 + * @param thresholdOverride number that is divided by 2^16 (65536) to get the shiny chance, overrides {@linkcode shinyThreshold} if set (bypassing shiny rate modifiers such as Shiny Charm) + * @param applyModifiersToOverride If {@linkcode thresholdOverride} is set and this is true, will apply Shiny Charm and event modifiers to {@linkcode thresholdOverride} + * @returns true if the Pokemon has been set as a shiny, false otherwise + */ + trySetShinySeed(thresholdOverride?: integer, applyModifiersToOverride?: boolean): boolean { + /** `64/65536 -> 1/1024` */ + const baseShinyChance = 64; + const shinyThreshold = new Utils.IntegerHolder(baseShinyChance); + if (thresholdOverride === undefined || applyModifiersToOverride) { + if (thresholdOverride !== undefined && applyModifiersToOverride) { + shinyThreshold.value = thresholdOverride; + } + if (this.scene.eventManager.isEventActive()) { + shinyThreshold.value *= this.scene.eventManager.getShinyMultiplier(); + } + if (!this.hasTrainer()) { + this.scene.applyModifiers(ShinyRateBoosterModifier, true, shinyThreshold); + } + } else { + shinyThreshold.value = thresholdOverride; + } + + this.shiny = randSeedInt(65536) < shinyThreshold.value; + + if (this.shiny) { + this.initShinySparkle(); + } + + return this.shiny; + } + /** * Generates a variant * Has a 10% of returning 2 (epic variant) @@ -1768,6 +1960,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.fusionVariant = 0; this.fusionGender = 0; this.fusionLuck = 0; + this.fusionMysteryEncounterPokemonData = null; this.generateName(); this.calculateStats(); @@ -1971,7 +2164,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { hideInfo(): Promise { return new Promise(resolve => { - if (this.battleInfo.visible) { + if (this.battleInfo && this.battleInfo.visible) { this.scene.tweens.add({ targets: [ this.battleInfo, this.battleInfo.expMaskRect ], x: this.isPlayer() ? "+=150" : `-=${!this.isBoss() ? 150 : 246}`, @@ -1993,11 +2186,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * sets if the pokemon has fled (implies it's a wild pokemon) + * sets if the pokemon is switching out (if it's a enemy wild implies it's going to flee) * @param status - boolean */ - setWildFlee(status: boolean): void { - this.wildFlee = status; + setSwitchOutStatus(status: boolean): void { + this.switchOutStatus = status; } updateInfo(instant?: boolean): Promise { @@ -2075,10 +2268,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @param stat the desired {@linkcode EffectiveStat} * @param opponent the target {@linkcode Pokemon} * @param move the {@linkcode Move} being used + * @param ignoreOppAbility determines whether the effects of the opponent's abilities (i.e. Unaware) should be ignored (`false` by default) * @param isCritical determines whether a critical hit has occurred or not (`false` by default) + * @param simulated determines whether effects are applied without altering game state (`true` by default) * @return the stat stage multiplier to be used for effective stat calculation */ - getStatStageMultiplier(stat: EffectiveStat, opponent?: Pokemon, move?: Move, isCritical: boolean = false): number { + getStatStageMultiplier(stat: EffectiveStat, opponent?: Pokemon, move?: Move, ignoreOppAbility: boolean = false, isCritical: boolean = false, simulated: boolean = true): number { const statStage = new Utils.IntegerHolder(this.getStatStage(stat)); const ignoreStatStage = new Utils.BooleanHolder(false); @@ -2095,7 +2290,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { break; } } - applyAbAttrs(IgnoreOpponentStatStagesAbAttr, opponent, null, false, stat, ignoreStatStage); + if (!ignoreOppAbility) { + applyAbAttrs(IgnoreOpponentStatStagesAbAttr, opponent, null, simulated, stat, ignoreStatStage); + } if (move) { applyMoveAttrs(IgnoreOpponentStatStagesAttr, this, opponent, move, ignoreStatStage); } @@ -2160,13 +2357,68 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Apply the results of a move to this pokemon - * @param {Pokemon} source The pokemon using the move - * @param {PokemonMove} battlerMove The move being used - * @returns {HitResult} The result of the attack - */ - apply(source: Pokemon, move: Move): HitResult { - let result: HitResult; + * Calculates the base damage of the given move against this Pokemon when attacked by the given source. + * Used during damage calculation and for Shell Side Arm's forecasting effect. + * @param source the attacking {@linkcode Pokemon}. + * @param move the {@linkcode Move} used in the attack. + * @param moveCategory the move's {@linkcode MoveCategory} after variable-category effects are applied. + * @param ignoreAbility if `true`, ignores this Pokemon's defensive ability effects (defaults to `false`). + * @param ignoreSourceAbility if `true`, ignore's the attacking Pokemon's ability effects (defaults to `false`). + * @param isCritical if `true`, calculates effective stats as if the hit were critical (defaults to `false`). + * @param simulated if `true`, suppresses changes to game state during calculation (defaults to `true`). + * @returns The move's base damage against this Pokemon when used by the source Pokemon. + */ + getBaseDamage(source: Pokemon, move: Move, moveCategory: MoveCategory, ignoreAbility: boolean = false, ignoreSourceAbility: boolean = false, isCritical: boolean = false, simulated: boolean = true): number { + const isPhysical = moveCategory === MoveCategory.PHYSICAL; + + /** A base damage multiplier based on the source's level */ + const levelMultiplier = (2 * source.level / 5 + 2); + + /** The power of the move after power boosts from abilities, etc. have applied */ + const power = move.calculateBattlePower(source, this, simulated); + + /** + * The attacker's offensive stat for the given move's category. + * Critical hits cause negative stat stages to be ignored. + */ + const sourceAtk = new Utils.NumberHolder(source.getEffectiveStat(isPhysical ? Stat.ATK : Stat.SPATK, this, undefined, ignoreSourceAbility, ignoreAbility, isCritical, simulated)); + applyMoveAttrs(VariableAtkAttr, source, this, move, sourceAtk); + + /** + * This Pokemon's defensive stat for the given move's category. + * Critical hits cause positive stat stages to be ignored. + */ + const targetDef = new Utils.NumberHolder(this.getEffectiveStat(isPhysical ? Stat.DEF : Stat.SPDEF, source, move, ignoreAbility, ignoreSourceAbility, isCritical, simulated)); + applyMoveAttrs(VariableDefAttr, source, this, move, targetDef); + + /** + * The attack's base damage, as determined by the source's level, move power + * and Attack stat as well as this Pokemon's Defense stat + */ + const baseDamage = ((levelMultiplier * power * sourceAtk.value / targetDef.value) / 50) + 2; + + /** Debug message for non-simulated calls (i.e. when damage is actually dealt) */ + if (!simulated) { + console.log("base damage", baseDamage, move.name, power, sourceAtk.value, targetDef.value); + } + + return baseDamage; + } + + /** + * Calculates the damage of an attack made by another Pokemon against this Pokemon + * @param source {@linkcode Pokemon} the attacking Pokemon + * @param move {@linkcode Pokemon} the move used in the attack + * @param ignoreAbility If `true`, ignores this Pokemon's defensive ability effects + * @param ignoreSourceAbility If `true`, ignores the attacking Pokemon's ability effects + * @param isCritical If `true`, calculates damage for a critical hit. + * @param simulated If `true`, suppresses changes to game state during the calculation. + * @returns a {@linkcode DamageCalculationResult} object with three fields: + * - `cancelled`: `true` if the move was cancelled by another effect. + * - `result`: {@linkcode HitResult} indicates the attack's type effectiveness. + * - `damage`: `number` the attack's final damage output. + */ + getAttackDamage(source: Pokemon, move: Move, ignoreAbility: boolean = false, ignoreSourceAbility: boolean = false, isCritical: boolean = false, simulated: boolean = true): DamageCalculationResult { const damage = new Utils.NumberHolder(0); const defendingSide = this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; @@ -2184,279 +2436,330 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * The effectiveness of the move being used. Along with type matchups, this * accounts for changes in effectiveness from the move's attributes and the * abilities of both the source and this Pokemon. + * + * Note that the source's abilities are not ignored here */ - const typeMultiplier = this.getMoveEffectiveness(source, move, false, false, cancelled); + const typeMultiplier = this.getMoveEffectiveness(source, move, ignoreAbility, simulated, cancelled); - switch (moveCategory) { - case MoveCategory.PHYSICAL: - case MoveCategory.SPECIAL: - const isPhysical = moveCategory === MoveCategory.PHYSICAL; - const sourceTeraType = source.getTeraType(); + const isPhysical = moveCategory === MoveCategory.PHYSICAL; - const power = move.calculateBattlePower(source, this); + /** Combined damage multiplier from field effects such as weather, terrain, etc. */ + const arenaAttackTypeMultiplier = new Utils.NumberHolder(this.scene.arena.getAttackTypeMultiplier(moveType, source.isGrounded())); + applyMoveAttrs(IgnoreWeatherTypeDebuffAttr, source, this, move, arenaAttackTypeMultiplier); - if (cancelled.value) { - // Cancelled moves fail silently - source.stopMultiHit(this); - return HitResult.NO_EFFECT; - } else { - const typeBoost = source.findTag(t => t instanceof TypeBoostTag && t.boostedType === moveType) as TypeBoostTag; - if (typeBoost?.oneUse) { - source.removeTag(typeBoost.tagType); + const isTypeImmune = (typeMultiplier * arenaAttackTypeMultiplier.value) === 0; + + if (cancelled.value || isTypeImmune) { + return { + cancelled: cancelled.value, + result: move.id === Moves.SHEER_COLD ? HitResult.IMMUNE : HitResult.NO_EFFECT, + damage: 0 + }; + } + + // If the attack deals fixed damaged, return a result with that much damage + const fixedDamage = new Utils.IntegerHolder(0); + applyMoveAttrs(FixedDamageAttr, source, this, move, fixedDamage); + if (fixedDamage.value) { + return { + cancelled: false, + result: HitResult.EFFECTIVE, + damage: fixedDamage.value + }; + } + + // If the attack is a one-hit KO move, return a result with damage equal to this Pokemon's HP + const isOneHitKo = new Utils.BooleanHolder(false); + applyMoveAttrs(OneHitKOAttr, source, this, move, isOneHitKo); + if (isOneHitKo.value) { + return { + cancelled: false, + result: HitResult.ONE_HIT_KO, + damage: this.hp + }; + } + + /** + * The attack's base damage, as determined by the source's level, move power + * and Attack stat as well as this Pokemon's Defense stat + */ + const baseDamage = this.getBaseDamage(source, move, moveCategory, ignoreAbility, ignoreSourceAbility, isCritical, simulated); + + /** 25% damage debuff on moves hitting more than one non-fainted target (regardless of immunities) */ + const { targets, multiple } = getMoveTargets(source, move.id); + const numTargets = multiple ? targets.length : 1; + const targetMultiplier = (numTargets > 1) ? 0.75 : 1; + + /** 0.25x multiplier if this is an added strike from the attacker's Parental Bond */ + const parentalBondMultiplier = new Utils.NumberHolder(1); + if (!ignoreSourceAbility) { + applyPreAttackAbAttrs(AddSecondStrikeAbAttr, source, this, move, simulated, numTargets, new Utils.IntegerHolder(0), parentalBondMultiplier); + } + + /** Doubles damage if this Pokemon's last move was Glaive Rush */ + const glaiveRushMultiplier = new Utils.IntegerHolder(1); + if (this.getTag(BattlerTagType.RECEIVE_DOUBLE_DAMAGE)) { + glaiveRushMultiplier.value = 2; + } + + /** The damage multiplier when the given move critically hits */ + const criticalMultiplier = new Utils.NumberHolder(isCritical ? 1.5 : 1); + applyAbAttrs(MultCritAbAttr, source, null, simulated, criticalMultiplier); + + /** + * A multiplier for random damage spread in the range [0.85, 1] + * This is always 1 for simulated calls. + */ + const randomMultiplier = simulated ? 1 : ((this.randSeedIntRange(85, 100)) / 100); + + const sourceTypes = source.getTypes(); + const sourceTeraType = source.getTeraType(); + const matchesSourceType = sourceTypes.includes(moveType); + /** A damage multiplier for when the attack is of the attacker's type and/or Tera type. */ + const stabMultiplier = new Utils.NumberHolder(1); + if (matchesSourceType) { + stabMultiplier.value += 0.5; + } + if (sourceTeraType !== Type.UNKNOWN && sourceTeraType === moveType) { + stabMultiplier.value += 0.5; + } + + if (!ignoreSourceAbility) { + applyAbAttrs(StabBoostAbAttr, source, null, simulated, stabMultiplier); + } + + stabMultiplier.value = Math.min(stabMultiplier.value, 2.25); + + /** Halves damage if the attacker is using a physical attack while burned */ + const burnMultiplier = new Utils.NumberHolder(1); + if (isPhysical && source.status && source.status.effect === StatusEffect.BURN) { + if (!move.hasAttr(BypassBurnDamageReductionAttr)) { + const burnDamageReductionCancelled = new Utils.BooleanHolder(false); + if (!ignoreSourceAbility) { + applyAbAttrs(BypassBurnDamageReductionAbAttr, source, burnDamageReductionCancelled, simulated); } - - /** Combined damage multiplier from field effects such as weather, terrain, etc. */ - const arenaAttackTypeMultiplier = new Utils.NumberHolder(this.scene.arena.getAttackTypeMultiplier(moveType, source.isGrounded())); - applyMoveAttrs(IgnoreWeatherTypeDebuffAttr, source, this, move, arenaAttackTypeMultiplier); - - /** - * Whether or not this Pokemon is immune to the incoming move. - * Note that this isn't fully resolved in `getMoveEffectiveness` because - * of possible type-suppressing field effects (e.g. Desolate Land's effect on Water-type attacks). - */ - const isTypeImmune = (typeMultiplier * arenaAttackTypeMultiplier.value) === 0; - if (isTypeImmune) { - // Moves with no effect that were not cancelled queue a "no effect" message before failing - source.stopMultiHit(this); - result = (move.id === Moves.SHEER_COLD) - ? HitResult.IMMUNE - : HitResult.NO_EFFECT; - - if (result === HitResult.IMMUNE) { - this.scene.queueMessage(i18next.t("battle:hitResultImmune", { pokemonName: this.name })); - } else { - this.scene.queueMessage(i18next.t("battle:hitResultNoEffect", { pokemonName: getPokemonNameWithAffix(this) })); - } - - return result; - } - - const glaiveRushModifier = new Utils.IntegerHolder(1); - if (this.getTag(BattlerTagType.RECEIVE_DOUBLE_DAMAGE)) { - glaiveRushModifier.value = 2; - } - let isCritical: boolean; - const critOnly = new Utils.BooleanHolder(false); - const critAlways = source.getTag(BattlerTagType.ALWAYS_CRIT); - applyMoveAttrs(CritOnlyAttr, source, this, move, critOnly); - applyAbAttrs(ConditionalCritAbAttr, source, null, false, critOnly, this, move); - if (critOnly.value || critAlways) { - isCritical = true; - } else { - const critChance = [24, 8, 2, 1][Math.max(0, Math.min(this.getCritStage(source, move), 3))]; - isCritical = critChance === 1 || !this.scene.randBattleSeedInt(critChance); - if (Overrides.NEVER_CRIT_OVERRIDE) { - isCritical = false; - } - } - if (isCritical) { - const noCritTag = this.scene.arena.getTagOnSide(NoCritTag, defendingSide); - const blockCrit = new Utils.BooleanHolder(false); - applyAbAttrs(BlockCritAbAttr, this, null, false, blockCrit); - if (noCritTag || blockCrit.value) { - isCritical = false; - } - } - const sourceAtk = new Utils.IntegerHolder(source.getEffectiveStat(isPhysical ? Stat.ATK : Stat.SPATK, this, undefined, isCritical)); - const targetDef = new Utils.IntegerHolder(this.getEffectiveStat(isPhysical ? Stat.DEF : Stat.SPDEF, source, move, isCritical)); - const criticalMultiplier = new Utils.NumberHolder(isCritical ? 1.5 : 1); - applyAbAttrs(MultCritAbAttr, source, null, false, criticalMultiplier); - const screenMultiplier = new Utils.NumberHolder(1); - if (!isCritical) { - this.scene.arena.applyTagsForSide(WeakenMoveScreenTag, defendingSide, move.category, this.scene.currentBattle.double, screenMultiplier); - } - const sourceTypes = source.getTypes(); - const matchesSourceType = sourceTypes[0] === moveType || (sourceTypes.length > 1 && sourceTypes[1] === moveType); - const stabMultiplier = new Utils.NumberHolder(1); - if (sourceTeraType === Type.UNKNOWN && matchesSourceType) { - stabMultiplier.value += 0.5; - } else if (sourceTeraType !== Type.UNKNOWN && sourceTeraType === moveType) { - stabMultiplier.value += 0.5; - } - - applyAbAttrs(StabBoostAbAttr, source, null, false, stabMultiplier); - - if (sourceTeraType !== Type.UNKNOWN && matchesSourceType) { - stabMultiplier.value = Math.min(stabMultiplier.value + 0.5, 2.25); - } - - // 25% damage debuff on moves hitting more than one non-fainted target (regardless of immunities) - const { targets, multiple } = getMoveTargets(source, move.id); - const targetMultiplier = (multiple && targets.length > 1) ? 0.75 : 1; - - applyMoveAttrs(VariableAtkAttr, source, this, move, sourceAtk); - applyMoveAttrs(VariableDefAttr, source, this, move, targetDef); - - const effectPhase = this.scene.getCurrentPhase(); - let numTargets = 1; - if (effectPhase instanceof MoveEffectPhase) { - numTargets = effectPhase.getTargets().length; - } - const twoStrikeMultiplier = new Utils.NumberHolder(1); - applyPreAttackAbAttrs(AddSecondStrikeAbAttr, source, this, move, false, numTargets, new Utils.IntegerHolder(0), twoStrikeMultiplier); - - if (!isTypeImmune) { - const levelMultiplier = (2 * source.level / 5 + 2); - const randomMultiplier = (this.randSeedIntRange(85, 100) / 100); - damage.value = Utils.toDmgValue((((levelMultiplier * power * sourceAtk.value / targetDef.value) / 50) + 2) - * stabMultiplier.value - * typeMultiplier - * arenaAttackTypeMultiplier.value - * screenMultiplier.value - * twoStrikeMultiplier.value - * targetMultiplier - * criticalMultiplier.value - * glaiveRushModifier.value - * randomMultiplier); - - if (isPhysical && source.status && source.status.effect === StatusEffect.BURN) { - if (!move.hasAttr(BypassBurnDamageReductionAttr)) { - const burnDamageReductionCancelled = new Utils.BooleanHolder(false); - applyAbAttrs(BypassBurnDamageReductionAbAttr, source, burnDamageReductionCancelled, false); - if (!burnDamageReductionCancelled.value) { - damage.value = Utils.toDmgValue(damage.value / 2); - } - } - } - - applyPreAttackAbAttrs(DamageBoostAbAttr, source, this, move, false, damage); - - /** - * For each {@link HitsTagAttr} the move has, doubles the damage of the move if: - * The target has a {@link BattlerTagType} that this move interacts with - * AND - * The move doubles damage when used against that tag - */ - move.getAttrs(HitsTagAttr).filter(hta => hta.doubleDamage).forEach(hta => { - if (this.getTag(hta.tagType)) { - damage.value *= 2; - } - }); - } - - if (this.scene.arena.terrain?.terrainType === TerrainType.MISTY && this.isGrounded() && moveType === Type.DRAGON) { - damage.value = Utils.toDmgValue(damage.value / 2); - } - - const fixedDamage = new Utils.IntegerHolder(0); - applyMoveAttrs(FixedDamageAttr, source, this, move, fixedDamage); - if (!isTypeImmune && fixedDamage.value) { - damage.value = fixedDamage.value; - isCritical = false; - result = HitResult.EFFECTIVE; - } - result = result!; // telling TS compiler that result is defined! - - if (!result) { - const isOneHitKo = new Utils.BooleanHolder(false); - applyMoveAttrs(OneHitKOAttr, source, this, move, isOneHitKo); - if (isOneHitKo.value) { - result = HitResult.ONE_HIT_KO; - isCritical = false; - damage.value = this.hp; - } else if (typeMultiplier >= 2) { - result = HitResult.SUPER_EFFECTIVE; - } else if (typeMultiplier >= 1) { - result = HitResult.EFFECTIVE; - } else { - result = HitResult.NOT_VERY_EFFECTIVE; - } - } - - const isOneHitKo = result === HitResult.ONE_HIT_KO; - - if (!fixedDamage.value && !isOneHitKo) { - if (!source.isPlayer()) { - this.scene.applyModifiers(EnemyDamageBoosterModifier, false, damage); - } - if (!this.isPlayer()) { - this.scene.applyModifiers(EnemyDamageReducerModifier, false, damage); - } - - applyPreDefendAbAttrs(ReceivedMoveDamageMultiplierAbAttr, this, source, move, cancelled, false, damage); - } - - // This attribute may modify damage arbitrarily, so be careful about changing its order of application. - applyMoveAttrs(ModifiedDamageAttr, source, this, move, damage); - - console.log("damage", damage.value, move.name, power, sourceAtk, targetDef); - - // In case of fatal damage, this tag would have gotten cleared before we could lapse it. - const destinyTag = this.getTag(BattlerTagType.DESTINY_BOND); - - if (damage.value) { - if (this.isFullHp()) { - applyPreDefendAbAttrs(PreDefendFullHpEndureAbAttr, this, source, move, cancelled, false, damage); - } else if (!this.isPlayer() && damage.value >= this.hp) { - this.scene.applyModifiers(EnemyEndureChanceModifier, false, this); - } - - /** - * We explicitly require to ignore the faint phase here, as we want to show the messages - * about the critical hit and the super effective/not very effective messages before the faint phase. - */ - damage.value = this.damageAndUpdate(damage.value, result as DamageResult, isCritical, isOneHitKo, isOneHitKo, true); - this.turnData.damageTaken += damage.value; - - if (isCritical) { - this.scene.queueMessage(i18next.t("battle:hitResultCriticalHit")); - } - if (source.isPlayer()) { - this.scene.validateAchvs(DamageAchv, damage); - if (damage.value > this.scene.gameData.gameStats.highestDamage) { - this.scene.gameData.gameStats.highestDamage = damage.value; - } - } - source.turnData.damageDealt += damage.value; - source.turnData.currDamageDealt = damage.value; - this.battleData.hitCount++; - const attackResult = { move: move.id, result: result as DamageResult, damage: damage.value, critical: isCritical, sourceId: source.id, sourceBattlerIndex: source.getBattlerIndex() }; - this.turnData.attacksReceived.unshift(attackResult); - if (source.isPlayer() && !this.isPlayer()) { - this.scene.applyModifiers(DamageMoneyRewardModifier, true, source, damage); - } - } - - // want to include is.Fainted() in case multi hit move ends early, still want to render message - if (source.turnData.hitsLeft === 1 || this.isFainted()) { - switch (result) { - case HitResult.SUPER_EFFECTIVE: - this.scene.queueMessage(i18next.t("battle:hitResultSuperEffective")); - break; - case HitResult.NOT_VERY_EFFECTIVE: - this.scene.queueMessage(i18next.t("battle:hitResultNotVeryEffective")); - break; - case HitResult.ONE_HIT_KO: - this.scene.queueMessage(i18next.t("battle:hitResultOneHitKO")); - break; - case HitResult.IMMUNE: - case HitResult.NO_EFFECT: - console.error("Unhandled move immunity!"); - break; - } - } - - if (this.isFainted()) { - // set splice index here, so future scene queues happen before FaintedPhase - this.scene.setPhaseQueueSplice(); - this.scene.unshiftPhase(new FaintPhase(this.scene, this.getBattlerIndex(), isOneHitKo)); - this.resetSummonData(); - } - - if (damage) { - destinyTag?.lapse(source, BattlerTagLapseType.CUSTOM); + if (!burnDamageReductionCancelled.value) { + burnMultiplier.value = 0.5; } } - break; - case MoveCategory.STATUS: + } + + /** Reduces damage if this Pokemon has a relevant screen (e.g. Light Screen for special attacks) */ + const screenMultiplier = new Utils.NumberHolder(1); + this.scene.arena.applyTagsForSide(WeakenMoveScreenTag, defendingSide, move.category, this.scene.currentBattle.double, screenMultiplier); + + /** + * For each {@linkcode HitsTagAttr} the move has, doubles the damage of the move if: + * The target has a {@linkcode BattlerTagType} that this move interacts with + * AND + * The move doubles damage when used against that tag + */ + const hitsTagMultiplier = new Utils.NumberHolder(1); + move.getAttrs(HitsTagAttr).filter(hta => hta.doubleDamage).forEach(hta => { + if (this.getTag(hta.tagType)) { + hitsTagMultiplier.value *= 2; + } + }); + + /** Halves damage if this Pokemon is grounded in Misty Terrain against a Dragon-type attack */ + const mistyTerrainMultiplier = (this.scene.arena.terrain?.terrainType === TerrainType.MISTY && this.isGrounded() && moveType === Type.DRAGON) + ? 0.5 + : 1; + + damage.value = Utils.toDmgValue( + baseDamage + * targetMultiplier + * parentalBondMultiplier.value + * arenaAttackTypeMultiplier.value + * glaiveRushMultiplier.value + * criticalMultiplier.value + * randomMultiplier + * stabMultiplier.value + * typeMultiplier + * burnMultiplier.value + * screenMultiplier.value + * hitsTagMultiplier.value + * mistyTerrainMultiplier + ); + + /** Doubles damage if the attacker has Tinted Lens and is using a resisted move */ + if (!ignoreSourceAbility) { + applyPreAttackAbAttrs(DamageBoostAbAttr, source, this, move, simulated, damage); + } + + /** Apply the enemy's Damage and Resistance tokens */ + if (!source.isPlayer()) { + this.scene.applyModifiers(EnemyDamageBoosterModifier, false, damage); + } + if (!this.isPlayer()) { + this.scene.applyModifiers(EnemyDamageReducerModifier, false, damage); + } + + /** Apply this Pokemon's post-calc defensive modifiers (e.g. Fur Coat) */ + if (!ignoreAbility) { + applyPreDefendAbAttrs(ReceivedMoveDamageMultiplierAbAttr, this, 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); + + if (this.isFullHp() && !ignoreAbility) { + applyPreDefendAbAttrs(PreDefendFullHpEndureAbAttr, this, source, move, cancelled, false, damage); + } + + // debug message for when damage is applied (i.e. not simulated) + if (!simulated) { + console.log("damage", damage.value, move.name); + } + + let hitResult: HitResult; + if (typeMultiplier < 1) { + hitResult = HitResult.NOT_VERY_EFFECTIVE; + } else if (typeMultiplier > 1) { + hitResult = HitResult.SUPER_EFFECTIVE; + } else { + hitResult = HitResult.EFFECTIVE; + } + + return { + cancelled: cancelled.value, + result: hitResult, + damage: damage.value + }; + } + + /** + * Applies the results of a move to this pokemon + * @param source The {@linkcode Pokemon} using the move + * @param move The {@linkcode Move} being used + * @returns The {@linkcode HitResult} of the attack + */ + apply(source: Pokemon, move: Move): HitResult { + const defendingSide = this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; + if (move.category === MoveCategory.STATUS) { + const cancelled = new Utils.BooleanHolder(false); + const typeMultiplier = this.getMoveEffectiveness(source, move, false, false, cancelled); + if (!cancelled.value && typeMultiplier === 0) { this.scene.queueMessage(i18next.t("battle:hitResultNoEffect", { pokemonName: getPokemonNameWithAffix(this) })); } - result = (typeMultiplier === 0) ? HitResult.NO_EFFECT : HitResult.STATUS; - break; - } + return (typeMultiplier === 0) ? HitResult.NO_EFFECT : HitResult.STATUS; + } else { + /** Determines whether the attack critically hits */ + let isCritical: boolean; + const critOnly = new Utils.BooleanHolder(false); + const critAlways = source.getTag(BattlerTagType.ALWAYS_CRIT); + applyMoveAttrs(CritOnlyAttr, source, this, move, critOnly); + applyAbAttrs(ConditionalCritAbAttr, source, null, false, critOnly, this, move); + if (critOnly.value || critAlways) { + isCritical = true; + } else { + const critChance = [24, 8, 2, 1][Math.max(0, Math.min(this.getCritStage(source, move), 3))]; + isCritical = critChance === 1 || !this.scene.randBattleSeedInt(critChance); + } - return result; + const noCritTag = this.scene.arena.getTagOnSide(NoCritTag, defendingSide); + const blockCrit = new Utils.BooleanHolder(false); + applyAbAttrs(BlockCritAbAttr, this, null, false, blockCrit); + if (noCritTag || blockCrit.value || Overrides.NEVER_CRIT_OVERRIDE) { + isCritical = false; + } + + const { cancelled, result, damage: dmg } = this.getAttackDamage(source, move, false, false, isCritical, false); + + const typeBoost = source.findTag(t => t instanceof TypeBoostTag && t.boostedType === source.getMoveType(move)) as TypeBoostTag; + if (typeBoost?.oneUse) { + source.removeTag(typeBoost.tagType); + } + + if (cancelled || result === HitResult.IMMUNE || result === HitResult.NO_EFFECT) { + source.stopMultiHit(this); + + if (!cancelled) { + if (result === HitResult.IMMUNE) { + this.scene.queueMessage(i18next.t("battle:hitResultImmune", { pokemonName: getPokemonNameWithAffix(this) })); + } else { + this.scene.queueMessage(i18next.t("battle:hitResultNoEffect", { pokemonName: getPokemonNameWithAffix(this) })); + } + } + return result; + } + + // In case of fatal damage, this tag would have gotten cleared before we could lapse it. + const destinyTag = this.getTag(BattlerTagType.DESTINY_BOND); + + const isOneHitKo = result === HitResult.ONE_HIT_KO; + + if (dmg) { + this.lapseTags(BattlerTagLapseType.HIT); + + const substitute = this.getTag(SubstituteTag); + const isBlockedBySubstitute = !!substitute && move.hitsSubstitute(source, this); + if (isBlockedBySubstitute) { + substitute.hp -= dmg; + } + if (!this.isPlayer() && dmg >= this.hp) { + this.scene.applyModifiers(EnemyEndureChanceModifier, false, this); + } + + /** + * We explicitly require to ignore the faint phase here, as we want to show the messages + * about the critical hit and the super effective/not very effective messages before the faint phase. + */ + const damage = this.damageAndUpdate(isBlockedBySubstitute ? 0 : dmg, result as DamageResult, isCritical, isOneHitKo, isOneHitKo, true); + + if (damage > 0) { + if (source.isPlayer()) { + this.scene.validateAchvs(DamageAchv, damage); + if (damage > this.scene.gameData.gameStats.highestDamage) { + this.scene.gameData.gameStats.highestDamage = damage; + } + } + source.turnData.damageDealt += damage; + source.turnData.currDamageDealt = damage; + this.turnData.damageTaken += damage; + this.battleData.hitCount++; + const attackResult = { move: move.id, result: result as DamageResult, damage: damage, critical: isCritical, sourceId: source.id, sourceBattlerIndex: source.getBattlerIndex() }; + this.turnData.attacksReceived.unshift(attackResult); + if (source.isPlayer() && !this.isPlayer()) { + this.scene.applyModifiers(DamageMoneyRewardModifier, true, source, new Utils.NumberHolder(damage)); + } + } + } + + if (isCritical) { + this.scene.queueMessage(i18next.t("battle:hitResultCriticalHit")); + } + + // want to include is.Fainted() in case multi hit move ends early, still want to render message + if (source.turnData.hitsLeft === 1 || this.isFainted()) { + switch (result) { + case HitResult.SUPER_EFFECTIVE: + this.scene.queueMessage(i18next.t("battle:hitResultSuperEffective")); + break; + case HitResult.NOT_VERY_EFFECTIVE: + this.scene.queueMessage(i18next.t("battle:hitResultNotVeryEffective")); + break; + case HitResult.ONE_HIT_KO: + this.scene.queueMessage(i18next.t("battle:hitResultOneHitKO")); + break; + } + } + + if (this.isFainted()) { + // set splice index here, so future scene queues happen before FaintedPhase + this.scene.setPhaseQueueSplice(); + this.scene.unshiftPhase(new FaintPhase(this.scene, this.getBattlerIndex(), isOneHitKo)); + this.destroySubstitute(); + this.resetSummonData(); + } + + if (dmg) { + destinyTag?.lapse(source, BattlerTagLapseType.CUSTOM); + } + + return result; + } } /** @@ -2499,6 +2802,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { */ this.scene.setPhaseQueueSplice(); this.scene.unshiftPhase(new FaintPhase(this.scene, this.getBattlerIndex(), preventEndure)); + this.destroySubstitute(); this.resetSummonData(); } @@ -2701,16 +3005,40 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return this.getRestrictingTag(moveId) !== null; } + /** + * Gets whether the given move is currently disabled for the user based on the player's target selection + * + * @param {Moves} moveId {@linkcode Moves} ID of the move to check + * @param {Pokemon} user {@linkcode Pokemon} the move user + * @param {Pokemon} target {@linkcode Pokemon} the target of the move + * + * @returns {boolean} `true` if the move is disabled for this Pokemon due to the player's target selection + * + * @see {@linkcode MoveRestrictionBattlerTag} + */ + isMoveTargetRestricted(moveId: Moves, user: Pokemon, target: Pokemon): boolean { + for (const tag of this.findTags(t => t instanceof MoveRestrictionBattlerTag)) { + if ((tag as MoveRestrictionBattlerTag).isMoveTargetRestricted(moveId, user, target)) { + return (tag as MoveRestrictionBattlerTag !== null); + } + } + return false; + } + /** * Gets the {@link MoveRestrictionBattlerTag} that is restricting a move, if it exists. * * @param {Moves} moveId {@linkcode Moves} ID of the move to check + * @param {Pokemon} user {@linkcode Pokemon} the move user, optional and used when the target is a factor in the move's restricted status + * @param {Pokemon} target {@linkcode Pokemon} the target of the move, optional and used when the target is a factor in the move's restricted status * @returns {MoveRestrictionBattlerTag | null} the first tag on this Pokemon that restricts the move, or `null` if the move is not restricted. */ - getRestrictingTag(moveId: Moves): MoveRestrictionBattlerTag | null { + getRestrictingTag(moveId: Moves, user?: Pokemon, target?: Pokemon): MoveRestrictionBattlerTag | null { for (const tag of this.findTags(t => t instanceof MoveRestrictionBattlerTag)) { if ((tag as MoveRestrictionBattlerTag).isMoveRestricted(moveId)) { return tag as MoveRestrictionBattlerTag; + } else if (user && target && (tag as MoveRestrictionBattlerTag).isMoveTargetRestricted(moveId, user, target)) { + return tag as MoveRestrictionBattlerTag; } } return null; @@ -3114,6 +3442,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.updateFusionPalette(); } this.summonData = new PokemonSummonData(); + this.setSwitchOutStatus(false); if (!this.battleData) { this.resetBattleData(); } @@ -3124,6 +3453,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.summonData[k] = this.summonDataPrimer[k]; } } + // If this Pokemon has a Substitute when loading in, play an animation to add its sprite + if (this.getTag(SubstituteTag)) { + this.scene.triggerPokemonBattleAnim(this, PokemonAnimType.SUBSTITUTE_ADD); + this.getTag(SubstituteTag)!.sourceInFocus = false; + } this.summonDataPrimer = null; } this.updateInfo(); @@ -3503,21 +3837,24 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * info container. */ leaveField(clearEffects: boolean = true, hideInfo: boolean = true) { + this.resetSprite(); this.resetTurnData(); if (clearEffects) { + this.destroySubstitute(); this.resetSummonData(); this.resetBattleData(); } if (hideInfo) { this.hideInfo(); } - this.setVisible(false); this.scene.field.remove(this); + this.setSwitchOutStatus(true); this.scene.triggerPokemonFormChange(this, SpeciesFormChangeActiveTrigger, true); } destroy(): void { this.battleInfo?.destroy(); + this.destroySubstitute(); super.destroy(); } @@ -3535,6 +3872,25 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const rootForm = getPokemonSpecies(this.species.getRootSpeciesId()); return rootForm.getAbility(abilityIndex) === rootForm.getAbility(currentAbilityIndex); } + + /** + * Helper function to check if the player already owns the starter data of the Pokemon's + * current ability + * @param ownedAbilityAttrs the owned abilityAttr of this Pokemon's root form + * @returns true if the player already has it, false otherwise + */ + checkIfPlayerHasAbilityOfStarter(ownedAbilityAttrs: number): boolean { + if ((ownedAbilityAttrs & 1) > 0 && this.hasSameAbilityInRootForm(0)) { + return true; + } + if ((ownedAbilityAttrs & 2) > 0 && this.hasSameAbilityInRootForm(1)) { + return true; + } + if ((ownedAbilityAttrs & 4) > 0 && this.hasSameAbilityInRootForm(2)) { + return true; + } + return false; + } } export default interface Pokemon { @@ -3800,6 +4156,12 @@ export class PlayerPokemon extends Pokemon { this.updateInfo(true).then(() => resolve()); }); }; + if (preEvolution.speciesId === Species.GIMMIGHOUL) { + const evotracker = this.getHeldItems().filter(m => m instanceof EvoTrackerModifier)[0] ?? null; + if (evotracker) { + this.scene.removeModifier(evotracker); + } + } if (!this.scene.gameMode.isDaily || this.metBiome > -1) { this.scene.gameData.updateSpeciesDexIvs(this.species.speciesId, this.ivs); this.scene.gameData.setPokemonSeen(this, false); @@ -3827,6 +4189,7 @@ export class PlayerPokemon extends Pokemon { newPokemon.metLevel = this.metLevel; newPokemon.metBiome = this.metBiome; newPokemon.metSpecies = this.metSpecies; + newPokemon.metWave = this.metWave; newPokemon.fusionSpecies = this.fusionSpecies; newPokemon.fusionFormIndex = this.fusionFormIndex; newPokemon.fusionAbilityIndex = this.fusionAbilityIndex; @@ -3834,6 +4197,7 @@ export class PlayerPokemon extends Pokemon { newPokemon.fusionVariant = this.fusionVariant; newPokemon.fusionGender = this.fusionGender; newPokemon.fusionLuck = this.fusionLuck; + newPokemon.usedTMs = this.usedTMs; this.scene.getParty().push(newPokemon); newPokemon.evolve((!isFusion ? newEvolution : new FusionSpeciesFormEvolution(this.id, newEvolution)), evoSpecies); @@ -3901,6 +4265,10 @@ export class PlayerPokemon extends Pokemon { this.fusionVariant = pokemon.variant; this.fusionGender = pokemon.gender; this.fusionLuck = pokemon.luck; + this.fusionMysteryEncounterPokemonData = pokemon.mysteryEncounterPokemonData; + if ((pokemon.pauseEvolutions) || (this.pauseEvolutions)) { + this.pauseEvolutions = true; + } this.scene.validateAchv(achvs.SPLICE); this.scene.gameData.gameStats.pokemonFused++; @@ -4116,7 +4484,7 @@ export class EnemyPokemon extends Pokemon { } // Filter out any moves this Pokemon cannot use - const movePool = this.getMoveset().filter(m => m?.isUsable(this)); + let movePool = this.getMoveset().filter(m => m?.isUsable(this)); // If no moves are left, use Struggle. Otherwise, continue with move selection if (movePool.length) { // If there's only 1 move in the move pool, use it. @@ -4137,6 +4505,39 @@ export class EnemyPokemon extends Pokemon { return { move: moveId, targets: this.getNextTargets(moveId) }; case AiType.SMART_RANDOM: case AiType.SMART: + /** + * Search this Pokemon's move pool for moves that will KO an opposing target. + * If there are any moves that can KO an opponent (i.e. a player Pokemon), + * those moves are the only ones considered for selection on this turn. + */ + const koMoves = movePool.filter(pkmnMove => { + if (!pkmnMove) { + return false; + } + + const move = pkmnMove.getMove()!; + if (move.moveTarget === MoveTarget.ATTACKER) { + return false; + } + + const fieldPokemon = this.scene.getField(); + const moveTargets = getMoveTargets(this, move.id).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); + + return move.category !== MoveCategory.STATUS + && moveTargets.some(p => { + const doesNotFail = move.applyConditions(this, p, move) || [Moves.SUCKER_PUNCH, Moves.UPPER_HAND, Moves.THUNDERCLAP].includes(move.id); + return doesNotFail && p.getAttackDamage(this, move, !p.battleData.abilityRevealed, false, isCritical).damage >= p.hp; + }); + }, this); + + if (koMoves.length > 0) { + movePool = koMoves; + } + /** * Move selection is based on the move's calculated "benefit score" against the * best possible target(s) (as determined by {@linkcode getNextTargets}). @@ -4407,8 +4808,15 @@ export class EnemyPokemon extends Pokemon { return true; } + /** + * Go through a boss' health segments and give stats boosts for each newly cleared segment + * The base boost is 1 to a random stat that's not already maxed out per broken shield + * For Pokemon with 3 health segments or more, breaking the last shield gives +2 instead + * For Pokemon with 5 health segments or more, breaking the last two shields give +2 each + * @param segmentIndex index of the segment to get down to (0 = no shield left, 1 = 1 shield left, etc.) + */ handleBossSegmentCleared(segmentIndex: integer): void { - while (segmentIndex - 1 < this.bossSegmentIndex) { + while (this.bossSegmentIndex > 0 && segmentIndex - 1 < this.bossSegmentIndex) { // Filter out already maxed out stat stages and weigh the rest based on existing stats const leftoverStats = EFFECTIVE_STATS.filter((s: EffectiveStat) => this.getStatStage(s) < 6); const statWeights = leftoverStats.map((s: EffectiveStat) => this.getStat(s, false)); @@ -4489,6 +4897,7 @@ export class EnemyPokemon extends Pokemon { this.pokeball = pokeballType; this.metLevel = this.level; this.metBiome = this.scene.arena.biomeType; + this.metWave = this.scene.currentBattle.waveIndex; this.metSpecies = this.species.speciesId; const newPokemon = this.scene.addPlayerPokemon(this.species, this.level, this.abilityIndex, this.formIndex, this.gender, this.shiny, this.variant, this.ivs, this.nature, this); @@ -4540,6 +4949,7 @@ export class PokemonSummonData { public speciesForm: PokemonSpeciesForm | null; public fusionSpeciesForm: PokemonSpeciesForm; public ability: Abilities = Abilities.NONE; + public passiveAbility: Abilities = Abilities.NONE; public gender: Gender; public fusionGender: Gender; public stats: number[] = [ 0, 0, 0, 0, 0, 0 ]; @@ -4607,6 +5017,16 @@ export enum HitResult { export type DamageResult = HitResult.EFFECTIVE | HitResult.SUPER_EFFECTIVE | HitResult.NOT_VERY_EFFECTIVE | HitResult.ONE_HIT_KO | HitResult.OTHER; +/** Interface containing the results of a damage calculation for a given move */ +export interface DamageCalculationResult { + /** `true` if the move was cancelled (thus suppressing "No Effect" messages) */ + cancelled: boolean; + /** The effectiveness of the move */ + result: HitResult; + /** 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. diff --git a/src/field/trainer.ts b/src/field/trainer.ts index 326ef0edefb..b1d0263f604 100644 --- a/src/field/trainer.ts +++ b/src/field/trainer.ts @@ -35,11 +35,16 @@ export default class Trainer extends Phaser.GameObjects.Container { public name: string; public partnerName: string; - constructor(scene: BattleScene, trainerType: TrainerType, variant: TrainerVariant, partyTemplateIndex?: integer, name?: string, partnerName?: string) { + constructor(scene: BattleScene, trainerType: TrainerType, variant: TrainerVariant, partyTemplateIndex?: integer, name?: string, partnerName?: string, trainerConfigOverride?: TrainerConfig) { super(scene, -72, 80); this.config = trainerConfigs.hasOwnProperty(trainerType) ? trainerConfigs[trainerType] : trainerConfigs[TrainerType.ACE_TRAINER]; + + if (trainerConfigOverride) { + this.config = trainerConfigOverride; + } + this.variant = variant; this.partyTemplateIndex = Math.min(partyTemplateIndex !== undefined ? partyTemplateIndex : Utils.randSeedWeightedItem(this.config.partyTemplates.map((_, i) => i)), this.config.partyTemplates.length - 1); @@ -420,14 +425,32 @@ export default class Trainer extends Phaser.GameObjects.Container { } } - if (retry && (attempt || 0) < 10) { + // Prompts reroll of party member species if species already present in the enemy party + if (this.checkDuplicateSpecies(ret)) { + console.log("Duplicate species detected, prompting reroll..."); + retry = true; + } + + if (retry && (attempt ?? 0) < 10) { console.log("Rerolling party member..."); - ret = this.genNewPartyMemberSpecies(level, strength, (attempt || 0) + 1); + ret = this.genNewPartyMemberSpecies(level, strength, (attempt ?? 0) + 1); } return ret; } + /** + * Checks if the enemy trainer already has the Pokemon species in their party + * @param {PokemonSpecies} species {@linkcode PokemonSpecies} + * @returns `true` if the species is already present in the party + */ + checkDuplicateSpecies(species: PokemonSpecies): boolean { + const currentPartySpecies = this.scene.getEnemyParty().map(p => { + return p.species.speciesId; + }); + return currentPartySpecies.includes(species.speciesId); + } + getPartyMemberMatchupScores(trainerSlot: TrainerSlot = TrainerSlot.NONE, forSwitch: boolean = false): [integer, integer][] { if (trainerSlot && !this.isDouble()) { trainerSlot = TrainerSlot.NONE; diff --git a/src/game-mode.ts b/src/game-mode.ts index f5dadad6f1b..525c975a19b 100644 --- a/src/game-mode.ts +++ b/src/game-mode.ts @@ -29,8 +29,13 @@ interface GameModeConfig { hasRandomBosses?: boolean; isSplicedOnly?: boolean; isChallenge?: boolean; + hasMysteryEncounters?: boolean; } +// Describes min and max waves for MEs in specific game modes +export const CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES: [number, number] = [10, 180]; +export const CHALLENGE_MODE_MYSTERY_ENCOUNTER_WAVES: [number, number] = [10, 180]; + export class GameMode implements GameModeConfig { public modeId: GameModes; public isClassic: boolean; @@ -45,6 +50,9 @@ export class GameMode implements GameModeConfig { public isChallenge: boolean; public challenges: Challenge[]; public battleConfig: FixedBattleConfigs; + public hasMysteryEncounters: boolean; + public minMysteryEncounterWave: number; + public maxMysteryEncounterWave: number; constructor(modeId: GameModes, config: GameModeConfig, battleConfig?: FixedBattleConfigs) { this.modeId = modeId; @@ -260,7 +268,6 @@ export class GameMode implements GameModeConfig { isFixedBattle(waveIndex: integer): boolean { const dummyConfig = new FixedBattleConfig(); return this.battleConfig.hasOwnProperty(waveIndex) || applyChallenges(this, ChallengeType.FIXED_BATTLES, waveIndex, dummyConfig); - } /** @@ -317,6 +324,20 @@ export class GameMode implements GameModeConfig { } } + /** + * Returns the wave range where MEs can spawn for the game mode [min, max] + */ + getMysteryEncounterLegalWaves(): [number, number] { + switch (this.modeId) { + default: + return [0, 0]; + case GameModes.CLASSIC: + return CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES; + case GameModes.CHALLENGE: + return CHALLENGE_MODE_MYSTERY_ENCOUNTER_WAVES; + } + } + static getModeName(modeId: GameModes): string { switch (modeId) { case GameModes.CLASSIC: @@ -336,7 +357,7 @@ export class GameMode implements GameModeConfig { export function getGameMode(gameMode: GameModes): GameMode { switch (gameMode) { case GameModes.CLASSIC: - return new GameMode(GameModes.CLASSIC, { isClassic: true, hasTrainers: true }, classicFixedBattles); + return new GameMode(GameModes.CLASSIC, { isClassic: true, hasTrainers: true, hasMysteryEncounters: true }, classicFixedBattles); case GameModes.ENDLESS: return new GameMode(GameModes.ENDLESS, { isEndless: true, hasShortBiomes: true, hasRandomBosses: true }); case GameModes.SPLICED_ENDLESS: @@ -344,6 +365,6 @@ export function getGameMode(gameMode: GameModes): GameMode { case GameModes.DAILY: return new GameMode(GameModes.DAILY, { isDaily: true, hasTrainers: true, hasNoShop: true }); case GameModes.CHALLENGE: - return new GameMode(GameModes.CHALLENGE, { isClassic: true, hasTrainers: true, isChallenge: true }, classicFixedBattles); + return new GameMode(GameModes.CHALLENGE, { isClassic: true, hasTrainers: true, isChallenge: true, hasMysteryEncounters: true }, classicFixedBattles); } } diff --git a/src/interfaces/held-modifier-config.ts b/src/interfaces/held-modifier-config.ts new file mode 100644 index 00000000000..2285babdbfd --- /dev/null +++ b/src/interfaces/held-modifier-config.ts @@ -0,0 +1,8 @@ +import { PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; +import { PokemonHeldItemModifier } from "#app/modifier/modifier"; + +export default interface HeldModifierConfig { + modifier: PokemonHeldItemModifierType | PokemonHeldItemModifier; + stackCount?: number; + isTransferable?: boolean; +} diff --git a/src/loading-scene.ts b/src/loading-scene.ts index 6de441fb162..c3cb494d497 100644 --- a/src/loading-scene.ts +++ b/src/loading-scene.ts @@ -22,6 +22,7 @@ import { initStatsKeys } from "./ui/game-stats-ui-handler"; import { initVouchers } from "./system/voucher"; import { Biome } from "#enums/biome"; import { TrainerType } from "#enums/trainer-type"; +import {initMysteryEncounters} from "#app/data/mystery-encounters/mystery-encounters"; export class LoadingScene extends SceneBase { public static readonly KEY = "loading"; @@ -240,12 +241,15 @@ export class LoadingScene extends SceneBase { const lang = i18next.resolvedLanguage; if (lang !== "en") { if (Utils.verifyLang(lang)) { + this.loadAtlas(`statuses_${lang}`, ""); this.loadAtlas(`types_${lang}`, ""); } else { // Fallback to English + this.loadAtlas("statuses", ""); this.loadAtlas("types", ""); } } else { + this.loadAtlas("statuses", ""); this.loadAtlas("types", ""); } const availableLangs = ["en", "de", "it", "fr", "ja", "ko", "es", "pt-BR", "zh-CN"]; @@ -286,6 +290,9 @@ export class LoadingScene extends SceneBase { } } + // Load Mystery Encounter dex progress icon + this.loadImage("encounter_radar", "mystery-encounters"); + this.loadAtlas("dualshock", "inputs"); this.loadAtlas("xbox", "inputs"); this.loadAtlas("keyboard", "inputs"); @@ -362,6 +369,7 @@ export class LoadingScene extends SceneBase { initMoves(); initAbilities(); initChallenges(); + initMysteryEncounters(); } loadLoadingScreen() { diff --git a/src/locales/de/ability.json b/src/locales/de/ability.json index 84f30fac755..e6078371535 100644 --- a/src/locales/de/ability.json +++ b/src/locales/de/ability.json @@ -1237,6 +1237,6 @@ }, "poisonPuppeteer": { "name": "Giftpuppenspiel", - "description": "Wenn Infamomo ein Ziel mit einer Attacke vergiftet, so wird dieses auch verwirrt." + "description": "Wenn das Pokémon ein Ziel mit einer Attacke vergiftet, so wird dieses auch verwirrt." } } \ No newline at end of file diff --git a/src/locales/de/battle.json b/src/locales/de/battle.json index 38e36d4b2da..7a9c2570211 100644 --- a/src/locales/de/battle.json +++ b/src/locales/de/battle.json @@ -96,5 +96,7 @@ "unlockedSomething": "{{unlockedThing}} wurde freigeschaltet.", "congratulations": "Glückwunsch!", "beatModeFirstTime": "{{speciesName}} hat den {{gameMode}} Modus zum ersten Mal beendet! Du erhältst {{newModifier}}!", - "eggSkipPrompt": "Zur Ei-Zusammenfassung springen?" + "eggSkipPrompt": "Zur Ei-Zusammenfassung springen?", + "battlerTagsHealBlock": "{{pokemonNameWithAffix}} kann nicht geheilt werden, da die Heilung blockiert wird!", + "battlerTagsHealBlockOnRemove": "{{pokemonNameWithAffix}} kann wieder geheilt werden!" } diff --git a/src/locales/de/battler-tags.json b/src/locales/de/battler-tags.json index 1a04d3d4486..2f8a8d0c438 100644 --- a/src/locales/de/battler-tags.json +++ b/src/locales/de/battler-tags.json @@ -70,5 +70,8 @@ "stockpilingOnAdd": "{{pokemonNameWithAffix}} hortet {{stockpiledCount}}!", "disabledOnAdd": " {{moveName}} von {{pokemonNameWithAffix}} wurde blockiert!", "disabledLapse": "{{moveName}} von {{pokemonNameWithAffix}} ist nicht länger blockiert!", - "tarShotOnAdd": "{{pokemonNameWithAffix}} ist nun schwach gegenüber Feuer-Attacken!" + "tarShotOnAdd": "{{pokemonNameWithAffix}} ist nun schwach gegenüber Feuer-Attacken!", + "substituteOnAdd": "Ein Delegator von {{pokemonNameWithAffix}} ist erschienen!", + "substituteOnHit": "Der Delegator steckt den Schlag für {{pokemonNameWithAffix}} ein!", + "substituteOnRemove": "Der Delegator von {{pokemonNameWithAffix}} hört auf zu wirken!" } diff --git a/src/locales/de/bgm-name.json b/src/locales/de/bgm-name.json index 1eab276a70a..830fad50f7a 100644 --- a/src/locales/de/bgm-name.json +++ b/src/locales/de/bgm-name.json @@ -83,9 +83,11 @@ "battle_aether_grunt": "SM Vs. Æther Foundation", "battle_skull_grunt": "SM Vs. Team Skull Rüpel", "battle_macro_grunt": "SWSH Vs. Trainer", + "battle_star_grunt": "KAPU Vs. Team Star", "battle_galactic_admin": "BDSP Vs. Team Galactic Commander", "battle_skull_admin": "SM Vs. Team Skull Vorstand", - "battle_oleana": "SWSH Vs. Oleana", + "battle_oleana": "SWSH Vs. Olivia", + "battle_star_admin": "KAPU Vs. Team Star Boss", "battle_rocket_boss": "USUM Vs. Giovanni", "battle_aqua_magma_boss": "ORAS Vs. Team Aqua & Magma Boss", "battle_galactic_boss": "BDSP Vs. Zyrus", @@ -94,6 +96,7 @@ "battle_aether_boss": "SM Vs. Samantha", "battle_skull_boss": "SM Vs. Bromley", "battle_macro_boss": "SWSH Vs. Rose", + "battle_star_boss": "KAPU Vs. Cosima", "abyss": "PMD Erkundungsteam Himmel Dunkelkrater", "badlands": "PMD Erkundungsteam Himmel Kargtal", @@ -108,17 +111,17 @@ "forest": "PMD Erkundungsteam Himmel Düsterwald", "grass": "PMD Erkundungsteam Himmel Apfelwald", "graveyard": "PMD Erkundungsteam Himmel Verwirrwald", - "ice_cave": "PMD Erkundungsteam Himmel Rieseneisberg", + "ice_cave": "Firel - -50°C", "island": "PMD Erkundungsteam Himmel Schroffküste", "jungle": "Lmz - Jungle", "laboratory": "Firel - Laboratory", - "lake": "PMD Erkundungsteam Himmel Kristallhöhle", + "lake": "Lmz - Lake", "meadow": "PMD Erkundungsteam Himmel Himmelsgipfel-Wald", "metropolis": "Firel - Metropolis", "mountain": "PMD Erkundungsteam Himmel Hornberg", - "plains": "PMD Erkundungsteam Himmel Himmelsgipfel-Prärie", - "power_plant": "PMD Erkundungsteam Himmel Weite Ampere-Ebene", - "ruins": "PMD Erkundungsteam Himmel Tiefes Ruinenverlies", + "plains": "Firel - Route 888", + "power_plant": "Firel - The Klink", + "ruins": "Lmz - Ancient Ruins", "sea": "Andr06 - Marine Mystique", "seabed": "Firel - Seabed", "slum": "Andr06 - Sneaky Snom", @@ -128,7 +131,7 @@ "tall_grass": "PMD Erkundungsteam Himmel Nebelwald", "temple": "PMD Erkundungsteam Himmel Ägishöhle", "town": "PMD Erkundungsteam Himmel Zufälliges Dungeon-Theme 3", - "volcano": "PMD Erkundungsteam Himmel Dunsthöhle", + "volcano": "Firel - Twisturn Volcano", "wasteland": "PMD Erkundungsteam Himmel Verborgenes Hochland", "encounter_ace_trainer": "SW Trainerblicke treffen sich (Ass-Trainer)", "encounter_backpacker": "SW Trainerblicke treffen sich (Backpacker)", diff --git a/src/locales/de/dialogue.json b/src/locales/de/dialogue.json index 8a3dbb8880e..2e8804ab747 100644 --- a/src/locales/de/dialogue.json +++ b/src/locales/de/dialogue.json @@ -715,12 +715,16 @@ "encounter": { "1": "Achtung hier ist Endstation für dich!", "2": "Du bist ein Trainer, oder? Wir von MC Wertpapiere wissen so etwas.\n$Ich fürchte, das gibt dir trotzdem nicht das Recht, sich in unsere Arbeit einzumischen.", - "3": "Ich bin von MC Versicherungen! Hast du eine Lebensversicherung?" + "3": "Ich bin von MC Versicherungen! Hast du eine Lebensversicherung?", + "4": "Ich habe dich gefunden! Das bedeutet es ist Zeit für einen Pokémon-Kampf!", + "5": "Eine Standpauke von Frau Olivia ist schlimmer als alles, was Sie tun können!" }, "victory": { "1": "Ich habe keine andere Wahl, als respektvoll zurückzutreten.", "2": "Mein Erspartes aufzugeben bringt mich in die roten Zahlen...", - "3": "Okay zurück an die Arbeit. Versicherungen verkauft sich nicht von alleine." + "3": "Okay zurück an die Arbeit. Versicherungen verkauft sich nicht von alleine.", + "4": "Ich habe sogar meine Pokémon ausgetauscht...", + "5": "Kämpfen hat nicht funktioniert... Jetzt können wir nur noch rennen!" } }, "oleana": { @@ -735,6 +739,73 @@ "3": "Ich bin eine müde Olivia... Ob es Macro Cosmos Betten gibt?" } }, + "star_grunt": { + "encounter": { + "1": "Wir sind von Team Star, wo jeder nach den Sternen greifen kann!", + "2": "Wir werden mit voller Kraft auf dich losgehen - Hasta la vistar! ★", + "3": "Könntest du bitte wieder abzischen? Sonst muss ich dich davonjagen. Aus reinem Selbstschutz!", + "4": "Es tut mir furchtbar leid, aber wenn du nicht umkehrst, könnte es ungemütlich für dich werden.", + "4_female": "Es tut mir furchtbar leid, aber wenn du nicht umkehrst, könnte es ungemütlich für dich werden.", + "5": "Och nee, nicht noch so ein Clown..." + }, + "victory": { + "1": "Jetzt bin ich die Person, die Sterne sieht...", + "2": "Jemand wie du wäre bei Team Star wahrscheinlich im Nullkommanichts an der Spitze.$Alle hätten Angst vor dir. Trotzdem...", + "3": "Da war meine Selbstverteidigung wohl nicht gut genug...", + "4": "H-hasta la vistar... ★", + "5": "Als neues Mitglied bei Team Star bekommt man echt nur die Drecksarbeit ab..." + } + }, + "giacomo": { + "encounter": { + "1": "Du willst dich echt mit Team Star anlegen? Bist du lebensmüde, oder was?", + "2": "Weil ich so nett bin, leg ich zu deinem Abgang auch ein fettes Requiem auf!$Lass uns die Party in Schwung bringen" + }, + "victory": { + "1": " Besser hätte ich es auch nicht sagen können...", + "2": "Uff, da hab ich schon bessere Shows gegeben... Schade, aber verloren ist verloren." + } + }, + "mela": { + "encounter": { + "1": "Du bist also diese Pfeife, die sich unbedingt mit uns anlegen will...$Dir werd ich zeigen, was mit Leuten passiert, die sich mit uns anlegen!", + "2": "Yeah, lassen wir’s krachen!" + }, + "victory": { + "1": "Uff, ich hab echt versagt... Das war’s dann wohl...", + "2": "Ich... brannte so sehr auf diesen Kampf. Doch jetzt ist meine Flamme erloschen..." + } + }, + "atticus": { + "encounter": { + "1": "hr habt Team Star Leid angetan, unverschämter Schurke! Mein Gift soll Euer Niedergang sein!", + "2": "Eure Bereitschaft zum Duell erfreut mich! Möge der Kampf ein ehrwürdiger sein!" + }, + "victory": { + "1": "Meine Gefährten... Vergebt mir...", + "2": "Ich habe eine klare Niederlage erlitten, bei der Groll und Bitterkeit fehl am Platz wären." + } + }, + "ortega": { + "encounter": { + "1": "Wenn ich mit dir fertig bin, wirst du heulend nach Hause rennen!", + "2": "Ich werde gewinnen, also spar dir deinen überheblichen Auftritt!" + }, + "victory": { + "1": "Was?! Wie konnte ich nur verlieren? Warum? Warum nur?!", + "2": "Graaaah! Du bist viel zu stark, das ist so was von unfair!" + } + }, + "eri": { + "encounter": { + "1": "Wer auch immer es auf Team Star abgesehen hat, wird zerschmettert!", + "2": "Ich kann genauso gut austeilen wie einstecken! Wer am Ende noch steht, gewinnt." + }, + "victory": { + "1": "Leute, es tut mir so leid...", + "2": "Ich habe alles gegeben... Ich bereue nichts..." + } + }, "rocket_boss_giovanni_1": { "encounter": { "1": "Ich bin beeindruckt, du hast es bis hierher geschafft!\n$Ich bin Giovanni, der Anführer von Team Rocket!\n$Wir regieren den Untergrund von Kanto!\n$Und wir lassen sicherlich nicht zu, dass ein Kind uns aufhält!" @@ -933,6 +1004,28 @@ "1": "Ich nehme an, es muss den Anschein haben, dass ich etwas Schreckliches tue.\n$Ich erwarte nicht, dass du es verstehst. Aber ich muss der Galar-Region grenzenlose Energie\n$bereitstellen, um ewigen Wohlstand zu gewährleisten." } }, + "star_boss_penny_1": { + "encounter": { + "1": "Ich bin Team Stars Big Boss. Mein Name ist Cassiopeia...$Die Gründerin von Team Star ist kampfbereit! Verneigt euch vor meiner unermesslichen Kraft!" + }, + "victory": { + "1": "... ... .." + }, + "defeat": { + "1": "Heh..." + } + }, + "star_boss_penny_2": { + "encounter": { + "1": "Ich werde mich in diesem Kampf nicht zurückhalten! Ich werde dem Kodex von Team Star treu bleiben!$Unsere Evoli-Power verwandelt euch in Sternenstaub!" + }, + "victory": { + "1": "Es ist vorbei..." + }, + "defeat": { + "1": "Du bist unfassbar stark. Kein Wunder, dass die anderen Bosse gegen dich verloren haben..." + } + }, "brock": { "encounter": { "1": "Meine Expertise in Bezug auf Gesteins-Pokémon wird dich besiegen! Komm schon!", diff --git a/src/locales/de/move.json b/src/locales/de/move.json index 3c81ccfd7df..f3502978edd 100644 --- a/src/locales/de/move.json +++ b/src/locales/de/move.json @@ -3121,15 +3121,15 @@ }, "behemothBlade": { "name": "Gigantenhieb", - "effect": "Der Anwender wird zu einem riesigen Schwert und greift das Ziel an. Dynamaximierte Ziele erleiden doppelten Schaden." + "effect": "Der Anwender wird zu einem riesigen Schwert und greift das Ziel an." }, "behemothBash": { "name": "Gigantenstoß", - "effect": "Der Anwender wird zu einem riesigen Schild und greift das Ziel an. Dynamaximierte Ziele erleiden doppelten Schaden." + "effect": "Der Anwender wird zu einem riesigen Schild und greift das Ziel an." }, "auraWheel": { "name": "Aura-Rad", - "effect": "Mithilfe der in den Backentaschen gespeicherten Energie greift der Anwender an und erhöht seine Initiative. Der Typ der Attacke hängt von Morpekos Form ab." + "effect": "Mithilfe der in den Backentaschen gespeicherten Energie greift der Anwender an und erhöht seine Initiative. Wenn dies von Morpeko verwendet wird hängt der Typ der Attacke von dessen Form ab." }, "breakingSwipe": { "name": "Breitseite", diff --git a/src/locales/de/pokemon-form.json b/src/locales/de/pokemon-form.json index d621e3165fa..16efc3af653 100644 --- a/src/locales/de/pokemon-form.json +++ b/src/locales/de/pokemon-form.json @@ -1,4 +1,5 @@ { + "pikachu": "Normal", "pikachuCosplay": "Cosplay", "pikachuCoolCosplay": "Rocker-Pikachu", "pikachuBeautyCosplay": "Damen-Pikachu", @@ -6,7 +7,9 @@ "pikachuSmartCosplay": "Professoren-Pikachu", "pikachuToughCosplay": "Wrestler-Pikachu", "pikachuPartner": "Partner-Pikachu", + "eevee": "Normal", "eeveePartner": "Partner-Evoli", + "pichu": "Normal", "pichuSpiky": "Strubbelohr-Pichu", "unownA": "A", "unownB": "B", @@ -36,36 +39,65 @@ "unownZ": "Z", "unownExclamation": "!", "unownQuestion": "?", + "castform": "Normalform", "castformSunny": "Sonnenform", "castformRainy": "Regenform", "castformSnowy": "Schneeform", "deoxysNormal": "Normalform", + "deoxysAttack": "Angriffsform", + "deoxysDefense": "Verteidigungsform", + "deoxysSpeed": "Initiativeform", "burmyPlant": "Pflanzenumhang", "burmySandy": "Sandumhang", "burmyTrash": "Lumpenumhang", + "cherubiOvercast": "Wolkenform", + "cherubiSunshine": "Sonnenform", "shellosEast": "Östliches Meer", "shellosWest": "Westliches Meer", + "rotom": "Normalform", "rotomHeat": "Hitze-Rotom", "rotomWash": "Wasch-Rotom", "rotomFrost": "Frost-Rotom", "rotomFan": "Wirbel-Rotom", "rotomMow": "Schneid-Rotom", + "dialga": "Normalform", + "dialgaOrigin": "Urform", + "palkia": "Normalform", + "palkiaOrigin": "Urform", "giratinaAltered": "Wandelform", + "giratinaOrigin": "Urform", "shayminLand": "Landform", + "shayminSky": "Zenitform", "basculinRedStriped": "Rotlinige Form", "basculinBlueStriped": "Blaulinige Form", "basculinWhiteStriped": "Weißlinige Form", + "darumaka": "Normalmodus", + "darumakaZen": "Trance-Modus", "deerlingSpring": "Frühlingsform", "deerlingSummer": "Sommerform", "deerlingAutumn": "Herbstform", "deerlingWinter": "Winterform", "tornadusIncarnate": "Inkarnationsform", + "tornadusTherian": "Tiergeistform", "thundurusIncarnate": "Inkarnationsform", + "thundurusTherian": "Tiergeistform", "landorusIncarnate": "Inkarnationsform", + "landorusTherian": "Tiergeistform", + "kyurem": "Normal", + "kyuremBlack": "Schwarzes Kyurem", + "kyuremWhite": "Weißes Kyurem", "keldeoOrdinary": "Standardform", + "keldeoResolute": "Resolutform", "meloettaAria": "Gesangsform", "meloettaPirouette": "Tanzform", - "froakieBattleBond": "Ash-Form", + "genesect": "Normal", + "genesectShock": "Blitzmodul", + "genesectBurn": "Flammenmodul", + "genesectChill": "Gefriermodul", + "genesectDouse": "Aquamodul", + "froakie": "Normalform", + "froakieBattleBond": "Freundschaftsakt", + "froakieAsh": "Ash-Form", "scatterbugMeadow": "Blumenmeermuster", "scatterbugIcySnow": "Frostmuster", "scatterbugPolar": "Schneefeldmuster", @@ -91,6 +123,7 @@ "flabebeOrange": "Orangeblütler", "flabebeBlue": "Blaublütler", "flabebeWhite": "Weißblütler", + "furfrou": "Zottelform", "furfrouHeart": "Herzchenschnitt", "furfrouStar": "Sternchenschnitt", "furfrouDiamond": "Diamantenschitt", @@ -100,6 +133,11 @@ "furfrouLaReine": "Königinnenschnitt", "furfrouKabuki": "Kabuki-Schnitt", "furfrouPharaoh": "Herrscherschnitt", + "espurrMale": "männlich", + "espurrFemale": "weiblich", + "honedgeShiled": "Schildform", + "honedgeBlade": "Klingenform", + "pumpkaboo": "Größe M", "pumpkabooSmall": "Größe S", "pumpkabooLarge": "Größe L", "pumpkabooSuper": "Größe XL", @@ -110,11 +148,37 @@ "zygarde50Pc": "50% Form Scharwandel", "zygarde10Pc": "10% Form Scharwandel", "zygardeComplete": "Optimum-Form", + "hoopa": "Gebanntes Hoopa", + "hoopaUnbound": "Entfesseltes Hoopa", "oricorioBaile": "Flamenco-Stil", "oricorioPompom": "Cheerleading-Stil", "oricorioPau": "Hula-Stil", "oricorioSensu": "Buyo-Stil", + "rockruff": "Normalform", "rockruffOwnTempo": "Gleichmut", + "rockruffMidday": "Tagform", + "rockruffMidnight": "Nachtform", + "rockruffDusk": "Zwielichtform", + "wishiwashi": "Einzelform", + "wishiwashiSchool": "Schwarmform", + "typeNullNormal": "Typ:Normal", + "typeNullFighting": "Typ:Kampf", + "typeNullFlying": "Typ:Flug", + "typeNullPoison": "Typ:Gift", + "typeNullGround": "Typ:Boden", + "typeNullRock": "Typ:Gestein", + "typeNullBug": "Typ:Käfer", + "typeNullGhost": "Typ:Geist", + "typeNullSteel": "Typ:Stahl", + "typeNullFire": "Typ:Feuer", + "typeNullWater": "Typ:Wasser", + "typeNullGrass": "Typ:Pflanze", + "typeNullElectric": "Typ:Elektro", + "typeNullPsychic": "Typ:Psycho", + "typeNullIce": "Typ:Eis", + "typeNullDragon": "Typ:Drache", + "typeNullDark": "Typ:Unlicht", + "typeNullFairy": "Typ:Fee", "miniorRedMeteor": "Rote-Meteorform", "miniorOrangeMeteor": "Oranger-Meteorform", "miniorYellowMeteor": "Gelber-Meteorform", @@ -131,25 +195,66 @@ "miniorViolet": "Violetter Kern", "mimikyuDisguised": "Verkleidete Form", "mimikyuBusted": "Entlarvte Form", + "necrozma": "Normalform", + "necrozmaDuskMane": "Abendmähne", + "necrozmaDawnWings": "Morgenschwingen", + "necrozmaUltra": "Ultra-Necrozma", + "magearna": "Normalform", "magearnaOriginal": "Originalfarbe", + "marshadow": "Normalform", "marshadowZenith": "Zenitform", + "cramorant": "Normalform", + "cramorantGulping": "Schlingform", + "cramorantGorging": "Stopfform", + "toxelAmped": "Hoch-Form", + "toxelLowkey": "Tief-Form", "sinisteaPhony": "Fälschungsform", "sinisteaAntique": "Originalform", + "milceryVanillaCream": "Vanille-Creme", + "milceryRubyCream": "Ruby-Creme", + "milceryMatchaCream": "Matcha-Creme", + "milceryMintCream": "Minz-Creme", + "milceryLemonCream": "Zitronen-Creme", + "milcerySaltedCream": "Salz-Creme", + "milceryRubySwirl": "Ruby-Mix", + "milceryCaramelSwirl": "Karamell-Mix", + "milceryRainbowSwirl": "Trio-Mix", + "eiscue": "Tiefkühlkopf", "eiscueNoIce": "Wohlfühlkopf", "indeedeeMale": "männlich", "indeedeeFemale": "weiblich", "morpekoFullBelly": "Pappsattmuster", + "morpekoHangry": "Kohldampfmuster", "zacianHeroOfManyBattles": "Heldenhafter Krieger", + "zacianCrowned": "König des Schwertes", "zamazentaHeroOfManyBattles": "Heldenhafter Krieger", + "zamazentaCrowned": "König des Schildes", + "kubfuSingleStrike": "Fokussierter Stil", + "kubfuRapidStrike": "Fließender Stil", + "zarude": "Normalform", "zarudeDada": "Papa", + "calyrex": "Normalform", + "calyrexIce": "Schimmelreiter", + "calyrexShadow": "Rappenreiter", + "basculinMale": "männlich", + "basculinFemale": "weiblich", "enamorusIncarnate": "Inkarnationsform", + "enamorusTherian": "Tiergeistform", + "lechonkMale": "männlich", + "lechonkFemale": "weiblich", + "tandemausFour": "Dreierfamilie", + "tandemausThree": "Viererfamilie", "squawkabillyGreenPlumage": "Grüngefiedert", "squawkabillyBluePlumage": "Blaugefiedert", "squawkabillyYellowPlumage": "Gelbgefiedert", "squawkabillyWhitePlumage": "Weißgefiedert", + "finizenZero": "Alltagsform", + "finizenHero": "Heldenform", "tatsugiriCurly": "Gebogene Form", "tatsugiriDroopy": "Hängende Form", "tatsugiriStretchy": "Gestrekte Form", + "dunsparceTwo": "Zweisegmentform", + "dunsparceThree": "Dreisegmentform", "gimmighoulChest": "Truhenform", "gimmighoulRoaming": "Wanderform", "koraidonApexBuild": "Vollkommene Gestalt", @@ -164,7 +269,22 @@ "miraidonGlideMode": "Gleitmodus", "poltchageistCounterfeit": "Imitationsform", "poltchageistArtisan": "Kostbarkeitsform", + "poltchageistUnremarkable": "Simple Form", + "poltchageistMasterpiece": "Edle Form", + "ogerponTealMask": "Türkisgrüne Maske", + "ogerponTealMaskTera": "Türkisgrüne Maske (Terakristallisiert)", + "ogerponWellspringMask": "Brunnenmaske", + "ogerponWellspringMaskTera": "Brunnenmaske (Terakristallisiert)", + "ogerponHearthflameMask": "Ofenmaske", + "ogerponHearthflameMaskTera": "Ofenmaske (Terakristallisiert)", + "ogerponCornerstoneMask": "Fundamentmaske", + "ogerponCornerstoneMaskTera": "Fundamentmaske (Terakristallisiert)", + "terpagos": "Normalform", + "terpagosTerastal": "Terakristall-Form", + "terpagosStellar": "Stellarform", + "galarDarumaka": "Normalmodus", + "galarDarumakaZen": "Trance-Modus", "paldeaTaurosCombat": "Gefechtsvariante", "paldeaTaurosBlaze": "Flammenvariante", "paldeaTaurosAqua": "Flutenvariante" -} \ No newline at end of file +} diff --git a/src/locales/de/pokemon-summary.json b/src/locales/de/pokemon-summary.json index 1790c6878b9..3104fc10151 100644 --- a/src/locales/de/pokemon-summary.json +++ b/src/locales/de/pokemon-summary.json @@ -11,7 +11,7 @@ "cancel": "Abbrechen", "memoString": "Wesen: {{natureFragment}}\n{{metFragment}}", "metFragment": { - "normal": "Herkunft: {{biome}}\nMit Lv. {{level}} erhalten.", + "normal": "Herkunft: {{biome}} - Welle {{wave}}\nMit Lv. {{level}} erhalten.", "apparently": "Herkunft: {{biome}}\nOffenbar mit Lv. {{level}} erhalten." }, "natureFragment": { diff --git a/src/locales/de/splash-messages.json b/src/locales/de/splash-messages.json index ac3fd345f3f..ba126393ccb 100644 --- a/src/locales/de/splash-messages.json +++ b/src/locales/de/splash-messages.json @@ -1,5 +1,5 @@ { - "battlesWon": "Kämpfe gewonnen!", + "battlesWon": "{{count, number}} Kämpfe gewonnen!", "joinTheDiscord": "Tritt dem Discord bei!", "infiniteLevels": "Unendliche Level!", "everythingStacks": "Alles stapelt sich!", diff --git a/src/locales/de/trainer-classes.json b/src/locales/de/trainer-classes.json index 45826fcd310..08f9e0cebef 100644 --- a/src/locales/de/trainer-classes.json +++ b/src/locales/de/trainer-classes.json @@ -126,5 +126,8 @@ "skull_grunts": "Rüpel von Team Skull", "macro_grunt": "Angestellter von Macro Cosmos", "macro_grunt_female": "Angestellte von Macro Cosmos", - "macro_grunts": "Angestellte von Macro Cosmos" + "macro_grunts": "Angestellte von Macro Cosmos", + "star_grunt": "Rüpel von Team Star", + "star_grunt_female": "Rüpel von Team Star", + "star_grunts": "Rüpel von Team Star" } diff --git a/src/locales/de/trainer-names.json b/src/locales/de/trainer-names.json index ffbb772234c..e3430f2b283 100644 --- a/src/locales/de/trainer-names.json +++ b/src/locales/de/trainer-names.json @@ -142,6 +142,11 @@ "faba": "Fabian", "plumeria": "Fran", "oleana": "Olivia", + "giacomo": "Pinio", + "mela": "Irsa", + "atticus": "Shugi", + "ortega": "Otis", + "eri": "Rioba", "maxie": "Marc", "archie": "Adrian", @@ -151,6 +156,7 @@ "lusamine": "Samantha", "guzma": "Bromley", "rose": "Rose", + "cassiopeia": "Cosima", "blue_red_double": "Blau & Rot", "red_blue_double": "Rot & Blau", diff --git a/src/locales/de/trainer-titles.json b/src/locales/de/trainer-titles.json index 21a4b2fa7b6..2a270226463 100644 --- a/src/locales/de/trainer-titles.json +++ b/src/locales/de/trainer-titles.json @@ -19,6 +19,7 @@ "aether_boss": "Æther-Präsidentin", "skull_boss": "Skull-Boss", "macro_boss": "Geschäftsführer von Macro Cosmos", + "star_boss": "Team Star Big Boss", "rocket_admin": "Team Rocket Vorstand", "rocket_admin_female": "Team Rocket Vorstand", "magma_admin": "Team Magma Vorstand", @@ -33,6 +34,6 @@ "flare_admin_female": "Team Flare Vorstand", "aether_admin": "Æther-Regionalleiter", "skull_admin": "Team Skull Vorstand", - "macro_admin": "Vizepräsidentin von Macro Cosmos" - + "macro_admin": "Vizepräsidentin von Macro Cosmos", + "star_admin": "Team Star Boss" } diff --git a/src/locales/en/ability.json b/src/locales/en/ability.json index de2e063e966..f2ffa9b4927 100644 --- a/src/locales/en/ability.json +++ b/src/locales/en/ability.json @@ -1237,6 +1237,6 @@ }, "poisonPuppeteer": { "name": "Poison Puppeteer", - "description": "Pokémon poisoned by Pecharunt's moves will also become confused." + "description": "Pokémon poisoned by this Pokémon's moves will also become confused." } } \ No newline at end of file diff --git a/src/locales/en/achv.json b/src/locales/en/achv.json index 32d519fbf78..b04f23d4209 100644 --- a/src/locales/en/achv.json +++ b/src/locales/en/achv.json @@ -283,5 +283,9 @@ "INVERSE_BATTLE": { "name": "Mirror rorriM", "description": "Complete the Inverse Battle challenge.\n.egnellahc elttaB esrevnI eht etelpmoC" + }, + "BREEDERS_IN_SPACE": { + "name": "Breeders in Space!", + "description": "Beat the Expert Pokémon Breeder in the Space Biome." } } diff --git a/src/locales/en/battle.json b/src/locales/en/battle.json index 217c77422d1..2559dafecae 100644 --- a/src/locales/en/battle.json +++ b/src/locales/en/battle.json @@ -14,6 +14,10 @@ "moneyWon": "You got\n₽{{moneyAmount}} for winning!", "moneyPickedUp": "You picked up ₽{{moneyAmount}}!", "pokemonCaught": "{{pokemonName}} was caught!", + "pokemonObtained": "You got {{pokemonName}}!", + "pokemonBrokeFree": "Oh no!\nThe Pokémon broke free!", + "pokemonFled": "The wild {{pokemonName}} fled!", + "playerFled": "You fled from the {{pokemonName}}!", "addedAsAStarter": "{{pokemonName}} has been\nadded as a starter!", "partyFull": "Your party is full.\nRelease a Pokémon to make room for {{pokemonName}}?", "pokemon": "Pokémon", @@ -52,6 +56,7 @@ "noPokeballTrainer": "You can't catch\nanother trainer's Pokémon!", "noPokeballMulti": "You can only throw a Poké Ball\nwhen there is one Pokémon remaining!", "noPokeballStrong": "The target Pokémon is too strong to be caught!\nYou need to weaken it first!", + "noPokeballMysteryEncounter": "You aren't able to\ncatch this Pokémon!", "noEscapeForce": "An unseen force\nprevents escape.", "noEscapeTrainer": "You can't run\nfrom a trainer battle!", "noEscapePokemon": "{{pokemonName}}'s {{moveName}}\nprevents {{escapeVerb}}!", @@ -99,5 +104,8 @@ "unlockedSomething": "{{unlockedThing}}\nhas been unlocked.", "congratulations": "Congratulations!", "beatModeFirstTime": "{{speciesName}} beat {{gameMode}} Mode for the first time!\nYou received {{newModifier}}!", - "ppReduced": "It reduced the PP of {{targetName}}'s\n{{moveName}} by {{reduction}}!" + "ppReduced": "It reduced the PP of {{targetName}}'s\n{{moveName}} by {{reduction}}!", + "mysteryEncounterAppeared": "What's this?", + "battlerTagsHealBlock": "{{pokemonNameWithAffix}} can't restore its HP!", + "battlerTagsHealBlockOnRemove": "{{pokemonNameWithAffix}} can restore its HP again!" } \ No newline at end of file diff --git a/src/locales/en/battler-tags.json b/src/locales/en/battler-tags.json index 5c351fc6961..b31826b0244 100644 --- a/src/locales/en/battler-tags.json +++ b/src/locales/en/battler-tags.json @@ -70,5 +70,8 @@ "stockpilingOnAdd": "{{pokemonNameWithAffix}} stockpiled {{stockpiledCount}}!", "disabledOnAdd": "{{pokemonNameWithAffix}}'s {{moveName}}\nwas disabled!", "disabledLapse": "{{pokemonNameWithAffix}}'s {{moveName}}\nis no longer disabled.", - "tarShotOnAdd": "{{pokemonNameWithAffix}} became weaker to fire!" + "tarShotOnAdd": "{{pokemonNameWithAffix}} became weaker to fire!", + "substituteOnAdd": "{{pokemonNameWithAffix}} put in a substitute!", + "substituteOnHit": "The substitute took damage for {{pokemonNameWithAffix}}!", + "substituteOnRemove": "{{pokemonNameWithAffix}}'s substitute faded!" } diff --git a/src/locales/en/bgm-name.json b/src/locales/en/bgm-name.json index 8838942c8a6..0951de6b769 100644 --- a/src/locales/en/bgm-name.json +++ b/src/locales/en/bgm-name.json @@ -83,9 +83,11 @@ "battle_aether_grunt": "SM Aether Foundation Battle", "battle_skull_grunt": "SM Team Skull Battle", "battle_macro_grunt": "SWSH Trainer Battle", + "battle_star_grunt": "SV Team Star Battle", "battle_galactic_admin": "BDSP Team Galactic Admin Battle", "battle_skull_admin": "SM Team Skull Admin Battle", "battle_oleana": "SWSH Oleana Battle", + "battle_star_admin": "SV Team Star Boss", "battle_rocket_boss": "USUM Giovanni Battle", "battle_aqua_magma_boss": "ORAS Archie & Maxie Battle", "battle_galactic_boss": "BDSP Cyrus Battle", @@ -94,6 +96,7 @@ "battle_aether_boss": "SM Lusamine Battle", "battle_skull_boss": "SM Guzma Battle", "battle_macro_boss": "SWSH Rose Battle", + "battle_star_boss": "SV Cassiopeia Battle", "abyss": "PMD EoS Dark Crater", "badlands": "PMD EoS Barren Valley", @@ -108,17 +111,17 @@ "forest": "PMD EoS Dusk Forest", "grass": "PMD EoS Apple Woods", "graveyard": "PMD EoS Mystifying Forest", - "ice_cave": "PMD EoS Vast Ice Mountain", + "ice_cave": "Firel - -50°C", "island": "PMD EoS Craggy Coast", "jungle": "Lmz - Jungle", "laboratory": "Firel - Laboratory", - "lake": "PMD EoS Crystal Cave", + "lake": "Lmz - Lake", "meadow": "PMD EoS Sky Peak Forest", "metropolis": "Firel - Metropolis", "mountain": "PMD EoS Mt. Horn", - "plains": "PMD EoS Sky Peak Prairie", - "power_plant": "PMD EoS Far Amp Plains", - "ruins": "PMD EoS Deep Sealed Ruin", + "plains": "Firel - Route 888", + "power_plant": "Firel - The Klink", + "ruins": "Lmz - Ancient Ruins", "sea": "Andr06 - Marine Mystique", "seabed": "Firel - Seabed", "slum": "Andr06 - Sneaky Snom", @@ -128,7 +131,7 @@ "tall_grass": "PMD EoS Foggy Forest", "temple": "PMD EoS Aegis Cave", "town": "PMD EoS Random Dungeon Theme 3", - "volcano": "PMD EoS Steam Cave", + "volcano": "Firel - Twisturn Volcano", "wasteland": "PMD EoS Hidden Highland", "encounter_ace_trainer": "BW Trainers' Eyes Meet (Ace Trainer)", "encounter_backpacker": "BW Trainers' Eyes Meet (Backpacker)", @@ -146,5 +149,11 @@ "encounter_youngster": "BW Trainers' Eyes Meet (Youngster)", "heal": "BW Pokémon Heal", "menu": "PMD EoS Welcome to the World of Pokémon!", - "title": "PMD EoS Top Menu Theme" + "title": "PMD EoS Top Menu Theme", + + "mystery_encounter_weird_dream": "PMD EoS Temporal Spire", + "mystery_encounter_fun_and_games": "PMD EoS Guildmaster Wigglytuff", + "mystery_encounter_gen_5_gts": "BW GTS", + "mystery_encounter_gen_6_gts": "XY GTS", + "mystery_encounter_delibirdy": "Firel - DeliDelivery!" } diff --git a/src/locales/en/config.ts b/src/locales/en/config.ts index 024f7f10108..35eef91e2ad 100644 --- a/src/locales/en/config.ts +++ b/src/locales/en/config.ts @@ -53,7 +53,49 @@ import terrain from "./terrain.json"; import modifierSelectUiHandler from "./modifier-select-ui-handler.json"; import moveTriggers from "./move-trigger.json"; import runHistory from "./run-history.json"; +import mysteryEncounterMessages from "./mystery-encounter-messages.json"; +import lostAtSea from "./mystery-encounters/lost-at-sea-dialogue.json"; +import mysteriousChest from "#app/locales/en/mystery-encounters/mysterious-chest-dialogue.json"; +import mysteriousChallengers from "#app/locales/en/mystery-encounters/mysterious-challengers-dialogue.json"; +import darkDeal from "#app/locales/en/mystery-encounters/dark-deal-dialogue.json"; +import departmentStoreSale from "#app/locales/en/mystery-encounters/department-store-sale-dialogue.json"; +import fieldTrip from "#app/locales/en/mystery-encounters/field-trip-dialogue.json"; +import fieryFallout from "#app/locales/en/mystery-encounters/fiery-fallout-dialogue.json"; +import fightOrFlight from "#app/locales/en/mystery-encounters/fight-or-flight-dialogue.json"; +import safariZone from "#app/locales/en/mystery-encounters/safari-zone-dialogue.json"; +import shadyVitaminDealer from "#app/locales/en/mystery-encounters/shady-vitamin-dealer-dialogue.json"; +import slumberingSnorlax from "#app/locales/en/mystery-encounters/slumbering-snorlax-dialogue.json"; +import trainingSession from "#app/locales/en/mystery-encounters/training-session-dialogue.json"; +import theStrongStuff from "#app/locales/en/mystery-encounters/the-strong-stuff-dialogue.json"; +import pokemonSalesman from "#app/locales/en/mystery-encounters/the-pokemon-salesman-dialogue.json"; +import offerYouCantRefuse from "#app/locales/en/mystery-encounters/an-offer-you-cant-refuse-dialogue.json"; +import delibirdy from "#app/locales/en/mystery-encounters/delibirdy-dialogue.json"; +import absoluteAvarice from "#app/locales/en/mystery-encounters/absolute-avarice-dialogue.json"; +import aTrainersTest from "#app/locales/en/mystery-encounters/a-trainers-test-dialogue.json"; +import trashToTreasure from "#app/locales/en/mystery-encounters/trash-to-treasure-dialogue.json"; +import berriesAbound from "#app/locales/en/mystery-encounters/berries-abound-dialogue.json"; +import clowningAround from "#app/locales/en/mystery-encounters/clowning-around-dialogue.json"; +import partTimer from "#app/locales/en/mystery-encounters/part-timer-dialogue.json"; +import dancingLessons from "#app/locales/en/mystery-encounters/dancing-lessons-dialogue.json"; +import weirdDream from "#app/locales/en/mystery-encounters/weird-dream-dialogue.json"; +import theWinstrateChallenge from "#app/locales/en/mystery-encounters/the-winstrate-challenge-dialogue.json"; +import teleportingHijinks from "#app/locales/en/mystery-encounters/teleporting-hijinks-dialogue.json"; +import bugTypeSuperfan from "#app/locales/en/mystery-encounters/bug-type-superfan-dialogue.json"; +import funAndGames from "#app/locales/en/mystery-encounters/fun-and-games-dialogue.json"; +import uncommonBreed from "#app/locales/en/mystery-encounters/uncommon-breed-dialogue.json"; +import globalTradeSystem from "#app/locales/en/mystery-encounters/global-trade-system-dialogue.json"; +import expertPokemonBreeder from "#app/locales/en/mystery-encounters/the-expert-pokemon-breeder-dialogue.json"; +/** + * Dialogue/Text token injection patterns that can be used: + * - `$` will be treated as a new line for Message and Dialogue strings. + * - `@d{}` will add a time delay to text animation for Message and Dialogue strings. + * - `@s{}` will play a specified sound effect for Message and Dialogue strings. + * - `@f{}` will fade the screen to black for the given duration, then fade back in for Message and Dialogue strings. + * - `{{}}` (MYSTERY ENCOUNTERS ONLY) will auto-inject the matching dialogue token value that is stored in {@link IMysteryEncounter.dialogueTokens}. + * - (see [i18next interpolations](https://www.i18next.com/translation-function/interpolation)) for more details. + * - `@[]{}` (STATIC TEXT ONLY, NOT USEABLE WITH {@link UI.showText()} OR {@link UI.showDialogue()}) will auto-color the given text to a specified {@link TextStyle} (e.g. `TextStyle.SUMMARY_GREEN`). + */ export const enConfig = { ability, abilityTriggers, @@ -109,5 +151,41 @@ export const enConfig = { partyUiHandler, modifierSelectUiHandler, moveTriggers, - runHistory + runHistory, + mysteryEncounter: { + // DO NOT REMOVE + "unit_test_dialogue": "{{test}}{{test}} {{test{{test}}}} {{test1}} {{test\}} {{test\\}} {{test\\\}} {test}}", + mysteriousChallengers, + mysteriousChest, + darkDeal, + fightOrFlight, + slumberingSnorlax, + trainingSession, + departmentStoreSale, + shadyVitaminDealer, + fieldTrip, + safariZone, + lostAtSea, + fieryFallout, + theStrongStuff, + pokemonSalesman, + offerYouCantRefuse, + delibirdy, + absoluteAvarice, + aTrainersTest, + trashToTreasure, + berriesAbound, + clowningAround, + partTimer, + dancingLessons, + weirdDream, + theWinstrateChallenge, + teleportingHijinks, + bugTypeSuperfan, + funAndGames, + uncommonBreed, + globalTradeSystem, + expertPokemonBreeder + }, + mysteryEncounterMessages }; diff --git a/src/locales/en/dialogue.json b/src/locales/en/dialogue.json index 5565d2258c2..9d1f0ae1c80 100644 --- a/src/locales/en/dialogue.json +++ b/src/locales/en/dialogue.json @@ -764,12 +764,17 @@ "1": "It looks like this is the end of the line for you!", "2": "You are a trainer aren't you? I'm afraid that doesn't give you the right to interfere in our work.", "2_female": "You are a trainer aren't you? I'm afraid that doesn't give you the right to interfere in our work.", - "3": "I'm from Macro Cosmos Insurance! Do you have a life insurance policy?" + "3": "I'm from Macro Cosmos Insurance! Do you have a life insurance policy?", + "4": "I found you! In that case, time for a Pokémon battle!", + "4_female": "I found you! In that case, time for a Pokémon battle!", + "5": "An earful from Ms. Oleana is way worse than anything you can do!" }, "victory": { "1": "I have little choice but to respectfully retreat.", "2": "Having to give up my pocket money... Losing means I'm back in the red...", - "3": "Nobody can beat Macro Cosmos when it comes to our dedication to our work!" + "3": "Nobody can beat Macro Cosmos when it comes to our dedication to our work!", + "4": "I even switched up my Pokémon...", + "5": "Battles didn't work... Only thing to do now is run!" } }, "oleana": { @@ -785,6 +790,77 @@ "3": "*sigh* I am one tired Oleana..." } }, + "star_grunt": { + "encounter": { + "1": "We're Team Star, kid. We burn so bright, it hurts to look at us!", + "2": "We'll come at you full force - Hasta la vistaaar! ☆", + "3": "If you don't clear out real quick-like, I'll hafta come at you in self-defense. You get me?", + "4": "Sorry, but if you don't turn yourself around here, amigo, we'll have to send you packing!", + "4_female": "Sorry, but if you don't turn yourself around here, amiga, we'll have to send you packing!", + "5": "Oh great. Here comes another rando to ruin my day.", + "5_female": "Oh great. Here comes another rando to ruin my day." + }, + "victory": { + "1": "How come I'M the one seeing stars?!", + "2": "You're scary, kid. If you joined Team Star, you'd be looking down from the top in no time!", + "3": "I defended myself all right... But it wasn't enough!", + "4": "H-hasta la vistar... ☆", + "5": "I didn't think grunt work for Team Star newbies would be this much of a chore..." + } + }, + "giacomo": { + "encounter": { + "1": "You don't really think things through, do ya? Declarin' war on Team Star is a real bad move.", + "2": "I'll play you a sick requiem as you crash and burn. Let's get this party staaarteeed!" + }, + "victory": { + "1": "Guess that's that...", + "2": "You turned my melody into a threnody..." + } + }, + "mela": { + "encounter": { + "1": "So you're the dope who picked a fight with Team Star... Prepare to get messed up.", + "1_female": "So you're the dope who picked a fight with Team Star... Prepare to get messed up.", + "2": "All riiight, BRING IT! I'll blow everythin' sky high!" + }, + "victory": { + "1": "Ugh. Is this really how it's gonna end? What a hassle...", + "2": "I burned through everythin' I had...and now I've sputtered out." + } + }, + "atticus": { + "encounter": { + "1": "You have some nerve baring your fangs at Team Star. Come, then, villainous wretch!", + "1_female": "You have some nerve baring your fangs at Team Star. Come, then, villainous wretch!", + "2": "Be warned—I shall spare thee no mercy! En garde!" + }, + "victory": { + "1": "Forgive me, my friends...", + "2": "You have utterly bested me. But thy victory stir'd no bitterness within me—such was its brilliance." + } + }, + "ortega": { + "encounter": { + "1": "I promise I'll play nice, so don't blame me when this battle sends you blubbering back home!", + "2": "I'll wipe that smug look off your face for sure! You're going down!", + "2_female": "I'll wipe that smug look off your face for sure! You're going down!" + }, + "victory": { + "1": "Ugh! How could I LOSE! What the HECK!", + "2": "Arrrrgggh! That strength of yours is SO. NOT. FAIR." + } + }, + "eri": { + "encounter": { + "1": "Doesn't matter who you are. I'll bury anyone who tries to take down Team Star!", + "2": "I give as good as I get—that's a promise! We'll see who's left standing in the end!" + }, + "victory": { + "1": "I'm so sorry, everyone...", + "2": "I gave it my all, but it wasn't enough—I wasn't enough..." + } + }, "rocket_boss_giovanni_1": { "encounter": { "1": "So! I must say, I am impressed you got here!" @@ -985,6 +1061,138 @@ "1": "I suppose it must seem that I am doing something terrible. I don't expect you to understand.\n$But I must provide the Galar region with limitless energy to ensure everlasting prosperity." } }, + "star_boss_penny_1": { + "encounter": { + "1": "I'm the big boss of Team Star. The name's Cassiopeia. \n$Now, bow down before the overwhelming might of Team Star's founder!" + }, + "victory": { + "1": "... ... .." + }, + "defeat": { + "1": "Heh..." + } + }, + "star_boss_penny_2": { + "encounter": { + "1": "I won't hold back in this battle! I'll stay true to Team Star's code! \n$My Veevee power will crush you into stardust!" + }, + "victory": { + "1": "...It's all over now." + }, + "defeat": { + "1": "I can't fault you on your battle skills at all... Considering how the bosses fell at your hands." + } + }, + "stat_trainer_buck": { + "encounter": { + "1": "...I'm telling you right now. I'm seriously tough. Act surprised!", + "2": "I can feel my Pokémon shivering inside their Pokéballs!" + }, + "victory": { + "1": "Heeheehee!\nSo hot, you!", + "2": "Heeheehee!\nSo hot, you!" + }, + "defeat": { + "1": "Whoa! You're all out of gas, I guess.", + "2": "Whoa! You're all out of gas, I guess." + } + }, + "stat_trainer_cheryl": { + "encounter": { + "1": "My Pokémon have been itching for a battle.", + "2": "I should warn you, my Pokémon can be quite rambunctious." + }, + "victory": { + "1": "Striking the right balance of offense and defense... It's not easy to do.", + "2": "Striking the right balance of offense and defense... It's not easy to do." + }, + "defeat": { + "1": "Do your Pokémon need any healing?", + "2": "Do your Pokémon need any healing?" + } + }, + "stat_trainer_marley": { + "encounter": { + "1": "... OK.\nI'll do my best.", + "2": "... OK.\nI... won't lose...!" + }, + "victory": { + "1": "... Awww.", + "2": "... Awww." + }, + "defeat": { + "1": "... Goodbye.", + "2": "... Goodbye." + } + }, + "stat_trainer_mira": { + "encounter": { + "1": "You will be shocked by Mira!", + "2": "Mira will show you that Mira doesn't get lost anymore!" + }, + "victory": { + "1": "Mira wonders if she can get very far in this land.", + "2": "Mira wonders if she can get very far in this land." + }, + "defeat": { + "1": "Mira knew she would win!", + "2": "Mira knew she would win!" + } + }, + "stat_trainer_riley": { + "encounter": { + "1": "Battling is our way of greeting!", + "2": "We're pulling out all the stops to put your Pokémon down." + }, + "victory": { + "1": "At times we battle, and sometimes we team up...$It's great how Trainers can interact.", + "2": "At times we battle, and sometimes we team up...$It's great how Trainers can interact." + }, + "defeat": { + "1": "You put up quite the display.\nBetter luck next time.", + "2": "You put up quite the display.\nBetter luck next time." + } + }, + "winstrates_victor": { + "encounter": { + "1": "That's the spirit! I like you!" + }, + "victory": { + "1": "A-ha! You're stronger than I thought!" + } + }, + "winstrates_victoria": { + "encounter": { + "1": "My goodness! Aren't you young?$You must be quite the trainer to beat my husband, though.$Now I suppose it's my turn to battle!" + }, + "victory": { + "1": "Uwah! Just how strong are you?!" + } + }, + "winstrates_vivi": { + "encounter": { + "1": "You're stronger than Mom? Wow!$But I'm strong, too!\nReally! Honestly!" + }, + "victory": { + "1": "Huh? Did I really lose?\nSnivel... Grandmaaa!" + } + }, + "winstrates_vicky": { + "encounter": { + "1": "How dare you make my precious\ngranddaughter cry!$I see I need to teach you a lesson.\nPrepare to feel the sting of defeat!" + }, + "victory": { + "1": "Whoa! So strong!\nMy granddaughter wasn't lying." + } + }, + "winstrates_vito": { + "encounter": { + "1": "I trained together with my whole family,\nevery one of us!$I'm not losing to anyone!" + }, + "victory": { + "1": "I was better than everyone in my family.\nI've never lost before..." + } + }, "brock": { "encounter": { "1": "My expertise on Rock-type Pokémon will take you down! Come on!", diff --git a/src/locales/en/egg.json b/src/locales/en/egg.json index 8a5e061d883..d6b352fca1e 100644 --- a/src/locales/en/egg.json +++ b/src/locales/en/egg.json @@ -11,6 +11,7 @@ "gachaTypeLegendary": "Legendary Rate Up", "gachaTypeMove": "Rare Egg Move Rate Up", "gachaTypeShiny": "Shiny Rate Up", + "eventType": "Mystery Event", "selectMachine": "Select a machine.", "notEnoughVouchers": "You don't have enough vouchers!", "tooManyEggs": "You have too many eggs!", diff --git a/src/locales/en/modifier-select-ui-handler.json b/src/locales/en/modifier-select-ui-handler.json index bc49ce25931..15c930fb65e 100644 --- a/src/locales/en/modifier-select-ui-handler.json +++ b/src/locales/en/modifier-select-ui-handler.json @@ -8,5 +8,7 @@ "lockRaritiesDesc": "Lock item rarities on reroll (affects reroll cost).", "checkTeamDesc": "Check your team or use a form changing item.", "rerollCost": "₽{{formattedMoney}}", - "itemCost": "₽{{formattedMoney}}" + "itemCost": "₽{{formattedMoney}}", + "continueNextWaveButton": "Continue", + "continueNextWaveDescription": "Continue to the next wave." } \ No newline at end of file diff --git a/src/locales/en/modifier-type.json b/src/locales/en/modifier-type.json index babad57b81b..c362b3f30d4 100644 --- a/src/locales/en/modifier-type.json +++ b/src/locales/en/modifier-type.json @@ -68,6 +68,20 @@ "BaseStatBoosterModifierType": { "description": "Increases the holder's base {{stat}} by 10%. The higher your IVs, the higher the stack limit." }, + "PokemonBaseStatTotalModifierType": { + "name": "Shuckle Juice", + "description": "{{increaseDecrease}} all of the holder's base stats by {{statValue}}. You were {{blessCurse}} by the Shuckle.", + "extra": { + "increase": "Increases", + "decrease": "Decreases", + "blessed": "blessed", + "cursed": "cursed" + } + }, + "PokemonBaseStatFlatModifierType": { + "name": "Old Gateau", + "description": "Increases the holder's {{stats}} base stats by {{statValue}}. Found after a strange dream." + }, "AllPokemonFullHpRestoreModifierType": { "description": "Restores 100% HP for all Pokémon." }, @@ -226,6 +240,8 @@ "TOXIC_ORB": { "name": "Toxic Orb", "description": "It's a bizarre orb that exudes toxins when touched and will badly poison the holder during battle." }, "FLAME_ORB": { "name": "Flame Orb", "description": "It's a bizarre orb that gives off heat when touched and will affect the holder with a burn during battle." }, + "EVOLUTION_TRACKER_GIMMIGHOUL": { "name": "Treasures", "description": "This Pokémon loves treasure! Keep collecting treasure and something might happen!"}, + "BATON": { "name": "Baton", "description": "Allows passing along effects when switching Pokémon, which also bypasses traps." }, "SHINY_CHARM": { "name": "Shiny Charm", "description": "Dramatically increases the chance of a wild Pokémon being Shiny." }, @@ -247,7 +263,13 @@ "ENEMY_ATTACK_BURN_CHANCE": { "name": "Burn Token" }, "ENEMY_STATUS_EFFECT_HEAL_CHANCE": { "name": "Full Heal Token", "description": "Adds a 2.5% chance every turn to heal a status condition." }, "ENEMY_ENDURE_CHANCE": { "name": "Endure Token" }, - "ENEMY_FUSED_CHANCE": { "name": "Fusion Token", "description": "Adds a 1% chance that a wild Pokémon will be a fusion." } + "ENEMY_FUSED_CHANCE": { "name": "Fusion Token", "description": "Adds a 1% chance that a wild Pokémon will be a fusion." }, + + "MYSTERY_ENCOUNTER_SHUCKLE_JUICE": { "name": "Shuckle Juice" }, + "MYSTERY_ENCOUNTER_BLACK_SLUDGE": { "name": "Black Sludge", "description": "The stench is so powerful that shops will only sell you items at a steep cost increase." }, + "MYSTERY_ENCOUNTER_MACHO_BRACE": { "name": "Macho Brace", "description": "Defeating a Pokémon grants the holder a Macho Brace stack. Each stack slightly boosts stats, with an extra bonus at max stacks." }, + "MYSTERY_ENCOUNTER_OLD_GATEAU": { "name": "Old Gateau", "description": "Increases the holder's {{stats}} stats by {{statValue}}." }, + "MYSTERY_ENCOUNTER_GOLDEN_BUG_NET": { "name": "Golden Bug Net", "description": "Imbues the owner with luck to find Bug Type Pokémon more often. Has a strange heft to it." } }, "SpeciesBoosterItem": { "LIGHT_BALL": { "name": "Light Ball", "description": "It's a mysterious orb that boosts Pikachu's Attack and Sp. Atk stats." }, @@ -310,6 +332,21 @@ "TART_APPLE": "Tart Apple", "STRAWBERRY_SWEET": "Strawberry Sweet", "UNREMARKABLE_TEACUP": "Unremarkable Teacup", + "UPGRADE": "Upgrade", + "DUBIOUS_DISC": "Dubious Disc", + "DRAGON_SCALE": "Dragon Scale", + "PRISM_SCALE": "Prism Scale", + "RAZOR_CLAW": "Razor Claw", + "RAZOR_FANG": "Razor Fang", + "REAPER_CLOTH": "Reaper Cloth", + "ELECTIRIZER": "Electirizer", + "MAGMARIZER": "Magmarizer", + "PROTECTOR": "Protector", + "SACHET": "Sachet", + "WHIPPED_DREAM": "Whipped Dream", + "LEADERS_CREST": "Leader's Crest", + "SUN_FLUTE": "Sun Flute", + "MOON_FLUTE": "Moon Flute", "CHIPPED_POT": "Chipped Pot", "BLACK_AUGURITE": "Black Augurite", diff --git a/src/locales/en/move-trigger.json b/src/locales/en/move-trigger.json index 375ea354d33..bc58e2878dd 100644 --- a/src/locales/en/move-trigger.json +++ b/src/locales/en/move-trigger.json @@ -68,5 +68,7 @@ "swapArenaTags": "{{pokemonName}} swapped the battle effects affecting each side of the field!", "exposedMove": "{{pokemonName}} identified\n{{targetPokemonName}}!", "safeguard": "{{targetName}} is protected by Safeguard!", + "substituteOnOverlap": "{{pokemonName}} already\nhas a substitute!", + "substituteNotEnoughHp": "But it does not have enough HP\nleft to make a substitute!", "afterYou": "{{pokemonName}} took the kind offer!" -} +} \ No newline at end of file diff --git a/src/locales/en/move.json b/src/locales/en/move.json index 7a10335ed06..f54003a296c 100644 --- a/src/locales/en/move.json +++ b/src/locales/en/move.json @@ -3129,7 +3129,7 @@ }, "auraWheel": { "name": "Aura Wheel", - "effect": "Morpeko attacks and raises its Speed with the energy stored in its cheeks. This move's type changes depending on the user's form." + "effect": "The user attacks and raises its Speed with the energy stored in its cheeks. If used by Morpeko, this move's type changes depending on the user's form." }, "breakingSwipe": { "name": "Breaking Swipe", diff --git a/src/locales/en/mystery-encounter-messages.json b/src/locales/en/mystery-encounter-messages.json new file mode 100644 index 00000000000..3b81c8e46f0 --- /dev/null +++ b/src/locales/en/mystery-encounter-messages.json @@ -0,0 +1,7 @@ +{ + "paid_money": "You paid ₽{{amount, number}}.", + "receive_money": "You received ₽{{amount, number}}!", + "affects_pokedex": "Affects Pokédex Data", + "cancel_option": "Return to encounter option select.", + "view_party_button": "View Party" +} diff --git a/src/locales/en/mystery-encounters/a-trainers-test-dialogue.json b/src/locales/en/mystery-encounters/a-trainers-test-dialogue.json new file mode 100644 index 00000000000..c96c0d5f327 --- /dev/null +++ b/src/locales/en/mystery-encounters/a-trainers-test-dialogue.json @@ -0,0 +1,47 @@ +{ + "intro": "An extremely strong trainer approaches you...", + "buck": { + "intro_dialogue": "Yo, trainer! My name's Buck.$I have a super awesome proposal\nfor a strong trainer such as yourself!$I'm carrying two rare Pokémon Eggs with me,\nbut I'd like someone else to care for one.$If you can prove your strength as a trainer to me,\nI'll give you the rarer egg!", + "accept": "Whoooo, I'm getting fired up!", + "decline": "Darn, it looks like your\nteam isn't in peak condition.$Here, let me help with that." + }, + "cheryl": { + "intro_dialogue": "Hello, my name's Cheryl.$I have a particularly interesting request,\nfor a strong trainer such as yourself.$I'm carrying two rare Pokémon Eggs with me,\nbut I'd like someone else to care for one.$If you can prove your strength as a trainer to me,\nI'll give you the rarer Egg!", + "accept": "I hope you're ready!", + "decline": "I understand, it looks like your team\nisn't in the best condition at the moment.$Here, let me help with that." + }, + "marley": { + "intro_dialogue": "...@d{64} I'm Marley.$I have an offer for you...$I'm carrying two Pokémon Eggs with me,\nbut I'd like someone else to care for one.$If you're stronger than me,\nI'll give you the rarer Egg.", + "accept": "... I see.", + "decline": "... I see.$Your Pokémon look hurt...\nLet me help." + }, + "mira": { + "intro_dialogue": "Hi! I'm Mira!$Mira has a request\nfor a strong trainer like you!$Mira has two rare Pokémon Eggs,\nbut Mira wants someone else to take one!$If you show Mira that you're strong,\nMira will give you the rarer Egg!", + "accept": "You'll battle Mira?\nYay!", + "decline": "Aww, no battle?\nThat's okay!$Here, Mira will heal your team!" + }, + "riley": { + "intro_dialogue": "I'm Riley.$I have an odd proposal\nfor a strong trainer such as yourself.$I'm carrying two rare Pokémon Eggs with me,\nbut I'd like to give one to another trainer.$If you can prove your strength to me,\nI'll give you the rarer Egg!", + "accept": "That look you have...\nLet's do this.", + "decline": "I understand, your team looks beat up.$Here, let me help with that." + }, + "title": "A Trainer's Test", + "description": "It seems this trainer is willing to give you an Egg regardless of your decision. However, if you can manage to defeat this strong trainer, you'll receive a much rarer Egg.", + "query": "What will you do?", + "option": { + "1": { + "label": "Accept the Challenge", + "tooltip": "(-) Tough Battle\n(+) Gain a @[TOOLTIP_TITLE]{Very Rare Egg}" + }, + "2": { + "label": "Refuse the Challenge", + "tooltip": "(+) Full Heal Party\n(+) Gain an @[TOOLTIP_TITLE]{Egg}" + } + }, + "eggTypes": { + "rare": "a Rare Egg", + "epic": "an Epic Egg", + "legendary": "a Legendary Egg" + }, + "outro": "{{statTrainerName}} gave you {{eggType}}!" +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/absolute-avarice-dialogue.json b/src/locales/en/mystery-encounters/absolute-avarice-dialogue.json new file mode 100644 index 00000000000..1d675d93660 --- /dev/null +++ b/src/locales/en/mystery-encounters/absolute-avarice-dialogue.json @@ -0,0 +1,25 @@ +{ + "intro": "A {{greedentName}} ambushes you\nand steals your party's berries!", + "title": "Absolute Avarice", + "description": "The {{greedentName}} has caught you totally off guard now all your berries are gone!\n\nThe {{greedentName}} looks like it's about to eat them when it pauses to look at you, interested.", + "query": "What will you do?", + "option": { + "1": { + "label": "Battle It", + "tooltip": "(-) Tough Battle\n(+) Rewards from its Berry Hoard", + "selected": "The {{greedentName}} stuffs its cheeks\nand prepares for battle!", + "boss_enraged": "{{greedentName}}'s fierce love for food has it incensed!", + "food_stash": "It looks like the {{greedentName}} was guarding an enormous stash of food!$@s{item_fanfare}Each Pokémon in your party gains a {{foodReward}}!" + }, + "2": { + "label": "Reason with It", + "tooltip": "(+) Regain Some Lost Berries", + "selected": "Your pleading strikes a chord with the {{greedentName}}.$It doesn't give all your berries back, but still tosses a few in your direction." + }, + "3": { + "label": "Let It Have the Food", + "tooltip": "(-) Lose All Berries\n(?) The {{greedentName}} Will Like You", + "selected": "The {{greedentName}} devours the entire\nstash of berries in a flash!$Patting its stomach,\nit looks at you appreciatively.$Perhaps you could feed it\nmore berries on your adventure...$@s{level_up_fanfare}The {{greedentName}} wants to join your party!" + } + } +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/an-offer-you-cant-refuse-dialogue.json b/src/locales/en/mystery-encounters/an-offer-you-cant-refuse-dialogue.json new file mode 100644 index 00000000000..e286d89691a --- /dev/null +++ b/src/locales/en/mystery-encounters/an-offer-you-cant-refuse-dialogue.json @@ -0,0 +1,26 @@ +{ + "intro": "You're stopped by a rich looking boy.", + "speaker": "Rich Boy", + "intro_dialogue": "Good day to you.$I can't help but notice that your\n{{strongestPokemon}} looks positively divine!$I've always wanted to have a Pokémon like that!$I'd pay you handsomely,\nand also give you this old bauble!", + "title": "An Offer You Can't Refuse", + "description": "You're being offered a @[TOOLTIP_TITLE]{Shiny Charm} and {{price, money}} for your {{strongestPokemon}}!\n\nIt's an extremely good deal, but can you really bear to part with such a strong team member?", + "query": "What will you do?", + "option": { + "1": { + "label": "Accept the Deal", + "tooltip": "(-) Lose {{strongestPokemon}}\n(+) Gain a @[TOOLTIP_TITLE]{Shiny Charm}\n(+) Gain {{price, money}}", + "selected": "Wonderful!@d{32} Come along, {{strongestPokemon}}!$It's time to show you off to everyone at the yacht club!$They'll be so jealous!" + }, + "2": { + "label": "Extort the Kid", + "tooltip": "(+) {{option2PrimaryName}} uses {{moveOrAbility}}\n(+) Gain {{price, money}}", + "tooltip_disabled": "Your Pokémon need to have certain moves or abilities to choose this", + "selected": "My word, we're being robbed, {{liepardName}}!$You'll be hearing from my lawyers for this!" + }, + "3": { + "label": "Leave", + "tooltip": "(-) No Rewards", + "selected": "What a rotten day...$Ah, well. Let's return to the yacht club then, {{liepardName}}." + } + } +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/berries-abound-dialogue.json b/src/locales/en/mystery-encounters/berries-abound-dialogue.json new file mode 100644 index 00000000000..26eae2c6b88 --- /dev/null +++ b/src/locales/en/mystery-encounters/berries-abound-dialogue.json @@ -0,0 +1,26 @@ +{ + "intro": "There's a huge berry bush\nnear that Pokémon!", + "title": "Berries Abound", + "description": "It looks like there's a strong Pokémon guarding a berry bush. Battling is the straightforward approach, but it looks strong. Perhaps a fast Pokémon could grab some berries without getting caught?", + "query": "What will you do?", + "berries": "Berries!", + "option": { + "1": { + "label": "Battle the Pokémon", + "tooltip": "(-) Hard Battle\n(+) Gain Berries", + "selected": "You approach the\nPokémon without fear." + }, + "2": { + "label": "Race to the Bush", + "tooltip": "(-) {{fastestPokemon}} Uses its Speed\n(+) Gain Berries", + "selected": "Your {{fastestPokemon}} races for the berry bush!$It manages to nab {{numBerries}} before the {{enemyPokemon}} can react!$You quickly retreat with your newfound prize.", + "selected_bad": "Your {{fastestPokemon}} races for the berry bush!$Oh no! The {{enemyPokemon}} was faster and blocked off the approach!", + "boss_enraged": "The opposing {{enemyPokemon}} has become enraged!" + }, + "3": { + "label": "Leave", + "tooltip": "(-) No Rewards", + "selected": "You leave the strong Pokémon\nwith its prize and continue on." + } + } +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/bug-type-superfan-dialogue.json b/src/locales/en/mystery-encounters/bug-type-superfan-dialogue.json new file mode 100644 index 00000000000..09488addb98 --- /dev/null +++ b/src/locales/en/mystery-encounters/bug-type-superfan-dialogue.json @@ -0,0 +1,40 @@ +{ + "intro": "An unusual trainer with all kinds of Bug paraphernalia blocks your way!", + "intro_dialogue": "Hey, trainer! I'm on a mission to find the rarest Bug Pokémon in existence!$You must love Bug Pokémon too, right?\nEveryone loves Bug Pokémon!", + "title": "The Bug-Type Superfan", + "speaker": "Bug-Type Superfan", + "description": "The trainer prattles, not even waiting for a response...\n\nIt seems the only way to get out of this situation is by catching the trainer's attention!", + "query": "What will you do?", + "option": { + "1": { + "label": "Offer to Battle", + "tooltip": "(-) Challenging Battle\n(+) Teach a Pokémon a Bug Type Move", + "selected": "A challenge, eh?\nMy bugs are more than ready for you!" + }, + "2": { + "label": "Show Your Bug Types", + "tooltip": "(+) Receive a Gift Item", + "disabled_tooltip": "You need at least 1 Bug Type Pokémon on your team to select this.", + "selected": "You show the trainer all your Bug Type Pokémon...", + "selected_0_to_1": "Huh? You only have {{numBugTypes}}...$Guess I'm wasting my breath on someone like you...", + "selected_2_to_3": "Hey, you've got {{numBugTypes}}!\nNot bad.$Here, this might help you on your journey to catch more!", + "selected_4_to_5": "What? You have {{numBugTypes}}?\nNice!$You're not quite at my level, but I can see shades of myself in you!\n$Take this, my young apprentice!", + "selected_6": "Whoa! {{numBugTypes}}!\n$You must love Bug Types almost as much as I do!$Here, take this as a token of our camaraderie!" + }, + "3": { + "label": "Gift a Bug Item", + "tooltip": "(-) Give the trainer a {{requiredBugItems}}\n(+) Receive a Gift Item", + "disabled_tooltip": "You need to have a {{requiredBugItems}} to select this.", + "select_prompt": "Select an item to give.", + "invalid_selection": "Pokémon doesn't have that kind of item.", + "selected": "You hand the trainer a {{selectedItem}}.", + "selected_dialogue": "Whoa! A {{selectedItem}}, for me?\nYou're not so bad, kid!$As a token of my appreciation,\nI want you to have this special gift!$It's been passed all through my family, and now I want you to have it!" + } + }, + "battle_won": "Your knowledge and skill were perfect at exploiting our weaknesses!$In exchange for the valuable lesson,\nallow me to teach one of your Pokémon a Bug Type Move!", + "teach_move_prompt": "Select a move to teach a Pokémon.", + "confirm_no_teach": "You sure you don't want to learn one of these great moves?", + "outro": "I see great Bug Pokémon in your future!\nMay our paths cross again!$Bug out!", + "numBugTypes_one": "{{count}} Bug Type", + "numBugTypes_other": "{{count}} Bug Types" +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/clowning-around-dialogue.json b/src/locales/en/mystery-encounters/clowning-around-dialogue.json new file mode 100644 index 00000000000..17781240838 --- /dev/null +++ b/src/locales/en/mystery-encounters/clowning-around-dialogue.json @@ -0,0 +1,34 @@ +{ + "intro": "It's...@d{64} a clown?", + "speaker": "Clown", + "intro_dialogue": "Bumbling buffoon, brace for a brilliant battle!\nYou'll be beaten by this brawling busker!", + "title": "Clowning Around", + "description": "Something is off about this encounter. The clown seems eager to goad you into a battle, but to what end?\n\nThe {{blacephalonName}} is especially strange, like it has @[TOOLTIP_TITLE]{weird types and ability.}", + "query": "What will you do?", + "option": { + "1": { + "label": "Battle the Clown", + "tooltip": "(-) Strange Battle\n(?) Affects Pokémon Abilities", + "selected": "Your pitiful Pokémon are poised for a pathetic performance!", + "apply_ability_dialogue": "A sensational showcase!\nYour savvy suits a sensational skill as spoils!", + "apply_ability_message": "The clown is offering to permanently Skill Swap one of your Pokémon's ability to {{ability}}!", + "ability_prompt": "Would you like to permanently teach a Pokémon the {{ability}} ability?", + "ability_gained": "@s{level_up_fanfare}{{chosenPokemon}} gained the {{ability}} ability!" + }, + "2": { + "label": "Remain Unprovoked", + "tooltip": "(-) Upsets the Clown\n(?) Affects Pokémon Items", + "selected": "Dismal dodger, you deny a delightful duel?\nFeel my fury!", + "selected_2": "The clown's {{blacephalonName}} uses Trick!\nAll of your {{switchPokemon}}'s items were randomly swapped!", + "selected_3": "Flustered fool, fall for my flawless deception!" + }, + "3": { + "label": "Return the Insults", + "tooltip": "(-) Upsets the Clown\n(?) Affects Pokémon Types", + "selected": "Dismal dodger, you deny a delightful duel?\nFeel my fury!", + "selected_2": "The clown's {{blacephalonName}} uses a strange move!\nAll of your team's types were randomly swapped!", + "selected_3": "Flustered fool, fall for my flawless deception!" + } + }, + "outro": "The clown and his cohorts\ndisappear in a puff of smoke." +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/dancing-lessons-dialogue.json b/src/locales/en/mystery-encounters/dancing-lessons-dialogue.json new file mode 100644 index 00000000000..8e2883ecb16 --- /dev/null +++ b/src/locales/en/mystery-encounters/dancing-lessons-dialogue.json @@ -0,0 +1,27 @@ +{ + "intro": "An {{oricorioName}} dances sadly alone, without a partner.", + "title": "Dancing Lessons", + "description": "The {{oricorioName}} doesn't seem aggressive, if anything it seems sad.\n\nMaybe it just wants someone to dance with...", + "query": "What will you do?", + "option": { + "1": { + "label": "Battle It", + "tooltip": "(-) Tough Battle\n(+) Gain a Baton", + "selected": "The {{oricorioName}} is distraught and moves to defend itself!", + "boss_enraged": "The {{oricorioName}}'s fear boosted its stats!" + }, + "2": { + "label": "Learn Its Dance", + "tooltip": "(+) Teach a Pokémon Revelation Dance", + "selected": "You watch the {{oricorioName}} closely as it performs its dance...$@s{level_up_fanfare}Your {{selectedPokemon}} learned from the {{oricorioName}}!" + }, + "3": { + "label": "Show It a Dance", + "tooltip": "(-) Teach the {{oricorioName}} a Dance Move\n(+) The {{oricorioName}} Will Like You", + "disabled_tooltip": "Your Pokémon need to know a Dance move for this.", + "select_prompt": "Select a Dance type move to use.", + "selected": "The {{oricorioName}} watches in fascination as\n{{selectedPokemon}} shows off {{selectedMove}}!$It loves the display!$@s{level_up_fanfare}The {{oricorioName}} wants to join your party!" + } + }, + "invalid_selection": "This Pokémon doesn't know a Dance move" +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/dark-deal-dialogue.json b/src/locales/en/mystery-encounters/dark-deal-dialogue.json new file mode 100644 index 00000000000..3086ebb0f9b --- /dev/null +++ b/src/locales/en/mystery-encounters/dark-deal-dialogue.json @@ -0,0 +1,24 @@ + + +{ + "intro": "A strange man in a tattered coat\nstands in your way...", + "speaker": "Shady Guy", + "intro_dialogue": "Hey, you!$I've been working on a new device\nto bring out a Pokémon's latent power!$It completely rebinds the Pokémon's atoms\nat a molecular level into a far more powerful form.$Hehe...@d{64} I just need some sac-@d{32}\nErr, test subjects, to prove it works.", + "title": "Dark Deal", + "description": "The disturbing fellow holds up some Pokéballs.\n\"I'll make it worth your while! You can have these strong Pokéballs as payment, All I need is a Pokémon from your team! Hehe...\"", + "query": "What will you do?", + "option": { + "1": { + "label": "Accept", + "tooltip": "(+) 5 Rogue Balls\n(?) Enhance a Random Pokémon", + "selected_dialogue": "Let's see, that {{pokeName}} will do nicely!$Remember, I'm not responsible\nif anything bad happens!@d{32} Hehe...", + "selected_message": "The man hands you 5 Rogue Balls.${{pokeName}} hops into the strange machine...$Flashing lights and weird noises\nstart coming from the machine!$...@d{96} Something emerges\nfrom the device, raging wildly!" + }, + "2": { + "label": "Refuse", + "tooltip": "(-) No Rewards", + "selected": "Not gonna help a poor fellow out?\nPah!" + } + }, + "outro": "After the harrowing encounter,\nyou collect yourself and depart." +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/delibirdy-dialogue.json b/src/locales/en/mystery-encounters/delibirdy-dialogue.json new file mode 100644 index 00000000000..ca1fefa3a39 --- /dev/null +++ b/src/locales/en/mystery-encounters/delibirdy-dialogue.json @@ -0,0 +1,29 @@ + + +{ + "intro": "A pack of {{delibirdName}} have appeared!", + "title": "Delibir-dy", + "description": "The {{delibirdName}}s are looking at you expectantly, as if they want something. Perhaps giving them an item or some money would satisfy them?", + "query": "What will you give them?", + "invalid_selection": "Pokémon doesn't have that kind of item.", + "option": { + "1": { + "label": "Give Money", + "tooltip": "(-) Give the {{delibirdName}}s {{money, money}}\n(+) Receive a Gift Item", + "selected": "You toss the money to the {{delibirdName}}s,\nwho chatter amongst themselves excitedly.$They turn back to you and happily give you a present!" + }, + "2": { + "label": "Give Food", + "tooltip": "(-) Give the {{delibirdName}}s a Berry or Reviver Seed\n(+) Receive a Gift Item", + "select_prompt": "Select an item to give.", + "selected": "You toss the {{chosenItem}} to the {{delibirdName}}s,\nwho chatter amongst themselves excitedly.$They turn back to you and happily give you a present!" + }, + "3": { + "label": "Give an Item", + "tooltip": "(-) Give the {{delibirdName}}s a Held Item\n(+) Receive a Gift Item", + "select_prompt": "Select an item to give.", + "selected": "You toss the {{chosenItem}} to the {{delibirdName}}s,\nwho chatter amongst themselves excitedly.$They turn back to you and happily give you a present!" + } + }, + "outro": "The {{delibirdName}} pack happily waddles off into the distance.$What a curious little exchange!" +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/department-store-sale-dialogue.json b/src/locales/en/mystery-encounters/department-store-sale-dialogue.json new file mode 100644 index 00000000000..d651f32665a --- /dev/null +++ b/src/locales/en/mystery-encounters/department-store-sale-dialogue.json @@ -0,0 +1,27 @@ +{ + "intro": "It's a lady with a ton of shopping bags.", + "speaker": "Shopper", + "intro_dialogue": "Hello! Are you here for\nthe amazing sales too?$There's a special coupon that you can\nredeem for a free item during the sale!$I have an extra one. Here you go!", + "title": "Department Store Sale", + "description": "There is merchandise in every direction! It looks like there are 4 counters where you can redeem the coupon for various items. The possibilities are endless!", + "query": "Which counter will you go to?", + "option": { + "1": { + "label": "TM Counter", + "tooltip": "(+) TM Shop" + }, + "2": { + "label": "Vitamin Counter", + "tooltip": "(+) Vitamin Shop" + }, + "3": { + "label": "Battle Item Counter", + "tooltip": "(+) X Item Shop" + }, + "4": { + "label": "Pokéball Counter", + "tooltip": "(+) Pokéball Shop" + } + }, + "outro": "What a deal! You should shop there more often." +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/field-trip-dialogue.json b/src/locales/en/mystery-encounters/field-trip-dialogue.json new file mode 100644 index 00000000000..61900d56cd7 --- /dev/null +++ b/src/locales/en/mystery-encounters/field-trip-dialogue.json @@ -0,0 +1,31 @@ +{ + "intro": "It's a teacher and some school children!", + "speaker": "Teacher", + "intro_dialogue": "Hello, there! Would you be able to\nspare a minute for my students?$I'm teaching them about Pokémon moves\nand would love to show them a demonstration.$Would you mind showing us one of\nthe moves your Pokémon can use?", + "title": "Field Trip", + "description": "A teacher is requesting a move demonstration from a Pokémon. Depending on the move you choose, she might have something useful for you in exchange.", + "query": "Which move category will you show off?", + "option": { + "1": { + "label": "A Physical Move", + "tooltip": "(+) Physical Item Rewards" + }, + "2": { + "label": "A Special Move", + "tooltip": "(+) Special Item Rewards" + }, + "3": { + "label": "A Status Move", + "tooltip": "(+) Status Item Rewards" + }, + "selected": "{{pokeName}} shows off an awesome display of {{move}}!" + }, + "second_option_prompt": "Choose a move for your Pokémon to use.", + "incorrect": "...$That isn't a {{moveCategory}} move!\nI'm sorry, but I can't give you anything.$Come along children, we'll\nfind a better demonstration elsewhere.", + "incorrect_exp": "Looks like you learned a valuable lesson?$Your Pokémon also gained some experience.", + "correct": "Thank you so much for your kindness!\nI hope these items might be of use to you!", + "correct_exp": "{{pokeName}} also gained some valuable experience!", + "status": "Status", + "physical": "Physical", + "special": "Special" +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/fiery-fallout-dialogue.json b/src/locales/en/mystery-encounters/fiery-fallout-dialogue.json new file mode 100644 index 00000000000..a1644d89a3f --- /dev/null +++ b/src/locales/en/mystery-encounters/fiery-fallout-dialogue.json @@ -0,0 +1,26 @@ +{ + "intro": "You encounter a blistering storm of smoke and ash!", + "title": "Fiery Fallout", + "description": "The whirling ash and embers have cut visibility to nearly zero. It seems like there might be some... source that is causing these conditions. But what could be behind a phenomenon of this magnitude?", + "query": "What will you do?", + "option": { + "1": { + "label": "Find the Source", + "tooltip": "(?) Discover the source\n(-) Hard Battle", + "selected": "You push through the storm, and find two {{volcaronaName}}s in the middle of a mating dance!$They don't take kindly to the interruption and attack!" + }, + "2": { + "label": "Hunker Down", + "tooltip": "(-) Suffer the effects of the weather", + "selected": "The weather effects cause significant\nharm as you struggle to find shelter!$Your party takes 20% Max HP damage!", + "target_burned": "Your {{burnedPokemon}} also became burned!" + }, + "3": { + "label": "Your Fire Types Help", + "tooltip": "(+) End the conditions\n(+) Gain a Charcoal", + "disabled_tooltip": "You need at least 2 Fire Type Pokémon to choose this", + "selected": "Your {{option3PrimaryName}} and {{option3SecondaryName}} guide you to where two {{volcaronaName}}s are in the middle of a mating dance!$Thankfully, your Pokémon are able to calm them,\nand they depart without issue." + } + }, + "found_charcoal": "After the weather clears,\nyour {{leadPokemon}} spots something on the ground.$@s{item_fanfare}{{leadPokemon}} gained a Charcoal!" +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/fight-or-flight-dialogue.json b/src/locales/en/mystery-encounters/fight-or-flight-dialogue.json new file mode 100644 index 00000000000..3eb6cb87c16 --- /dev/null +++ b/src/locales/en/mystery-encounters/fight-or-flight-dialogue.json @@ -0,0 +1,25 @@ +{ + "intro": "Something shiny is sparkling\non the ground near that Pokémon!", + "title": "Fight or Flight", + "description": "It looks like there's a strong Pokémon guarding an item. Battling is the straightforward approach, but it looks strong. Perhaps you could steal the item, if you have the right Pokémon for the job.", + "query": "What will you do?", + "option": { + "1": { + "label": "Battle the Pokémon", + "tooltip": "(-) Hard Battle\n(+) New Item", + "selected": "You approach the\nPokémon without fear.", + "stat_boost": "The {{enemyPokemon}}'s latent strength boosted one of its stats!" + }, + "2": { + "label": "Steal the Item", + "disabled_tooltip": "Your Pokémon need to know certain moves to choose this", + "tooltip": "(+) {{option2PrimaryName}} uses {{option2PrimaryMove}}", + "selected": ".@d{32}.@d{32}.@d{32}$Your {{option2PrimaryName}} helps you out and uses {{option2PrimaryMove}}!$You nabbed the item!" + }, + "3": { + "label": "Leave", + "tooltip": "(-) No Rewards", + "selected": "You leave the strong Pokémon\nwith its prize and continue on." + } + } +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/fun-and-games-dialogue.json b/src/locales/en/mystery-encounters/fun-and-games-dialogue.json new file mode 100644 index 00000000000..f5d7d6e8ff8 --- /dev/null +++ b/src/locales/en/mystery-encounters/fun-and-games-dialogue.json @@ -0,0 +1,30 @@ +{ + "intro_dialogue": "Step right up, folks! Try your luck\non the brand new {{wobbuffetName}} Whack-o-matic!", + "speaker": "Showman", + "title": "Fun And Games!", + "description": "You've encountered a traveling show with a prize game! You will have @[TOOLTIP_TITLE]{3 turns} to bring the {{wobbuffetName}} as close to @[TOOLTIP_TITLE]{1 HP} as possible @[TOOLTIP_TITLE]{without KOing it} so it can wind up a huge Counter on the bell-ringing machine.\nBut be careful! If you KO the {{wobbuffetName}}, you'll have to pay for the cost of reviving it!", + "query": "Would you like to play?", + "option": { + "1": { + "label": "Play the Game", + "tooltip": "(-) Pay {{option1Money, money}}\n(+) Play {{wobbuffetName}} Whack-o-matic", + "selected": "Time to test your luck!" + }, + "2": { + "label": "Leave", + "tooltip": "(-) No Rewards", + "selected": "You hurry along your way,\nwith a slight feeling of regret." + } + }, + "ko": "Oh no! The {{wobbuffetName}} fainted!$You lose the game and\nhave to pay for the revive cost...", + "charging_continue": "The Wubboffet keeps charging its counter-swing!", + "turn_remaining_3": "Three turns remaining!", + "turn_remaining_2": "Two turns remaining!", + "turn_remaining_1": "One turn remaining!", + "end_game": "Time's up!$The {{wobbuffetName}} winds up to counter-swing and@d{16}.@d{16}.@d{16}.", + "best_result": "The {{wobbuffetName}} smacks the button so hard\nthe bell breaks off the top!$You win the grand prize!", + "great_result": "The {{wobbuffetName}} smacks the button, nearly hitting the bell!$So close!\nYou earn the second tier prize!", + "good_result": "The {{wobbuffetName}} hits the button hard enough to go midway up the scale!$You earn the third tier prize!", + "bad_result": "The {{wobbuffetName}} barely taps the button and nothing happens...$Oh no!\nYou don't win anything!", + "outro": "That was a fun little game!" +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/global-trade-system-dialogue.json b/src/locales/en/mystery-encounters/global-trade-system-dialogue.json new file mode 100644 index 00000000000..1cc420355b7 --- /dev/null +++ b/src/locales/en/mystery-encounters/global-trade-system-dialogue.json @@ -0,0 +1,32 @@ +{ + "intro": "It's an interface for the Global Trade System!", + "title": "The GTS", + "description": "Ah, the GTS! A technological wonder, you can connect with anyone else around the globe to trade Pokémon with them! Will fortune smile upon your trade today?", + "query": "What will you do?", + "option": { + "1": { + "label": "Check Trade Offers", + "tooltip": "(+) Select a trade offer for one of your Pokémon", + "trade_options_prompt": "Select a Pokémon to receive through trade." + }, + "2": { + "label": "Wonder Trade", + "tooltip": "(+) Send one of your Pokémon to the GTS and get a random Pokémon in return" + }, + "3": { + "label": "Trade an Item", + "trade_options_prompt": "Select an item to send.", + "invalid_selection": "This Pokémon doesn't have legal items to trade.", + "tooltip": "(+) Send one of your Items to the GTS and get a random new Item" + }, + "4": { + "label": "Leave", + "tooltip": "(-) No Rewards", + "selected": "No time to trade today!\nYou continue on." + } + }, + "pokemon_trade_selected": "{{tradedPokemon}} will be sent to {{tradeTrainerName}}.", + "pokemon_trade_goodbye": "Goodbye, {{tradedPokemon}}!", + "item_trade_selected": "{{chosenItem}} will be sent to {{tradeTrainerName}}.$.@d{64}.@d{64}.@d{64}\n@s{level_up_fanfare}Trade complete!$You received a {{itemName}} from {{tradeTrainerName}}!", + "trade_received": "@s{evolution_fanfare}{{tradeTrainerName}} sent over {{received}}!" +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/lost-at-sea-dialogue.json b/src/locales/en/mystery-encounters/lost-at-sea-dialogue.json new file mode 100644 index 00000000000..41709c66799 --- /dev/null +++ b/src/locales/en/mystery-encounters/lost-at-sea-dialogue.json @@ -0,0 +1,28 @@ +{ + "intro": "Wandering aimlessly through the sea, you've effectively gotten nowhere.", + "title": "Lost at Sea", + "description": "The sea is turbulent in this area, and you're running out of energy.\nThis is bad. Is there a way out of the situation?", + "query": "What will you do?", + "option": { + "1": { + "label": "{{option1PrimaryName}} Might Help", + "label_disabled": "Can't {{option1RequiredMove}}", + "tooltip": "(+) {{option1PrimaryName}} saves you\n(+) {{option1PrimaryName}} gains some EXP", + "tooltip_disabled": "You have no Pokémon to {{option1RequiredMove}} on", + "selected": "{{option1PrimaryName}} swims ahead, guiding you back on track.${{option1PrimaryName}} seems to also have gotten stronger in this time of need!" + }, + "2": { + "label": "{{option2PrimaryName}} Might Help", + "label_disabled": "Can't {{option2RequiredMove}}", + "tooltip": "(+) {{option2PrimaryName}} saves you\n(+) {{option2PrimaryName}} gains some EXP", + "tooltip_disabled": "You have no Pokémon to {{option2RequiredMove}} with", + "selected": "{{option2PrimaryName}} flies ahead of your boat, guiding you back on track.${{option2PrimaryName}} seems to also have gotten stronger in this time of need!" + }, + "3": { + "label": "Wander Aimlessly", + "tooltip": "(-) Each of your Pokémon lose {{damagePercentage}}% of their total HP", + "selected": "You float about in the boat, steering without direction until you finally spot a landmark you remember.$You and your Pokémon are fatigued from the whole ordeal." + } + }, + "outro": "You are back on track." +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/mysterious-challengers-dialogue.json b/src/locales/en/mystery-encounters/mysterious-challengers-dialogue.json new file mode 100644 index 00000000000..01f4e6092eb --- /dev/null +++ b/src/locales/en/mystery-encounters/mysterious-challengers-dialogue.json @@ -0,0 +1,22 @@ +{ + "intro": "Mysterious challengers have appeared!", + "title": "Mysterious Challengers", + "description": "If you defeat a challenger, you might impress them enough to receive a boon. But some look tough, are you up to the challenge?", + "query": "Who will you battle?", + "option": { + "1": { + "label": "A Clever, Mindful Foe", + "tooltip": "(-) Standard Battle\n(+) Move Item Rewards" + }, + "2": { + "label": "A Strong Foe", + "tooltip": "(-) Hard Battle\n(+) Good Rewards" + }, + "3": { + "label": "The Mightiest Foe", + "tooltip": "(-) Brutal Battle\n(+) Great Rewards" + }, + "selected": "The trainer steps forward..." + }, + "outro": "The mysterious challenger was defeated!" +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/mysterious-chest-dialogue.json b/src/locales/en/mystery-encounters/mysterious-chest-dialogue.json new file mode 100644 index 00000000000..1de7a5992ed --- /dev/null +++ b/src/locales/en/mystery-encounters/mysterious-chest-dialogue.json @@ -0,0 +1,23 @@ +{ + "intro": "You found...@d{32} a chest?", + "title": "The Mysterious Chest", + "description": "A beautifully ornamented chest stands on the ground. There must be something good inside... right?", + "query": "Will you open it?", + "option": { + "1": { + "label": "Open It", + "tooltip": "@[SUMMARY_BLUE]{(35%) Something terrible}\n@[SUMMARY_GREEN]{(40%) Okay Rewards}\n@[SUMMARY_GREEN]{(20%) Good Rewards}\n@[SUMMARY_GREEN]{(4%) Great Rewards}\n@[SUMMARY_GREEN]{(1%) Amazing Rewards}", + "selected": "You open the chest to find...", + "normal": "Just some normal tools and items.", + "good": "Some pretty nice tools and items.", + "great": "A couple great tools and items!", + "amazing": "Whoa! An amazing item!", + "bad": "Oh no!@d{32}\nThe chest was actually a {{gimmighoulName}} in disguise!$Your {{pokeName}} jumps in front of you\nbut is KOed in the process!" + }, + "2": { + "label": "Too Risky, Leave", + "tooltip": "(-) No Rewards", + "selected": "You hurry along your way,\nwith a slight feeling of regret." + } + } +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/part-timer-dialogue.json b/src/locales/en/mystery-encounters/part-timer-dialogue.json new file mode 100644 index 00000000000..801a409ee84 --- /dev/null +++ b/src/locales/en/mystery-encounters/part-timer-dialogue.json @@ -0,0 +1,31 @@ +{ + "intro": "A busy worker flags you down.", + "speaker": "Worker", + "intro_dialogue": "You look like someone with lots of capable Pokémon!$We can pay you if you're able to help us with some part-time work!", + "title": "Part-Timer", + "description": "Looks like there are plenty of tasks that need to be done. Depending how well-suited your Pokémon is to a task, they might earn more or less money.", + "query": "Which job will you choose?", + "invalid_selection": "Pokémon must be healthy enough.", + "option": { + "1": { + "label": "Make Deliveries", + "tooltip": "(-) Your Pokémon Uses its Speed\n(+) Earn @[MONEY]{Money}", + "selected": "Your {{selectedPokemon}} works a shift delivering orders to customers." + }, + "2": { + "label": "Warehouse Work", + "tooltip": "(-) Your Pokémon Uses its Strength and Endurance\n(+) Earn @[MONEY]{Money}", + "selected": "Your {{selectedPokemon}} works a shift moving items around the warehouse." + }, + "3": { + "label": "Sales Assistant", + "tooltip": "(-) Your {{option3PrimaryName}} uses {{option3PrimaryMove}}\n(+) Earn @[MONEY]{Money}", + "disabled_tooltip": "Your Pokémon need to know certain moves for this job", + "selected": "Your {{option3PrimaryName}} spends the day using {{option3PrimaryMove}} to draw customers to the business!" + } + }, + "job_complete_good": "Thanks for the assistance!\nYour {{selectedPokemon}} was incredibly helpful!$Here's your check for the day.", + "job_complete_bad": "Your {{selectedPokemon}} helped us out a bit!$Here's your check for the day.", + "pokemon_tired": "Your {{selectedPokemon}} is worn out!\nThe PP of all its moves was reduced to 2!", + "outro": "Come back and help out again sometime!" +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/safari-zone-dialogue.json b/src/locales/en/mystery-encounters/safari-zone-dialogue.json new file mode 100644 index 00000000000..8869f2055e5 --- /dev/null +++ b/src/locales/en/mystery-encounters/safari-zone-dialogue.json @@ -0,0 +1,46 @@ +{ + "intro": "It's a safari zone!", + "title": "The Safari Zone", + "description": "There are all kinds of rare and special Pokémon that can be found here!\nIf you choose to enter, you'll have a time limit of 3 wild encounters where you can try to catch these special Pokémon.\n\nBeware, though. These Pokémon may flee before you're able to catch them!", + "query": "Would you like to enter?", + "option": { + "1": { + "label": "Enter", + "tooltip": "(-) Pay {{option1Money, money}}\n@[SUMMARY_GREEN]{(?) Safari Zone}", + "selected": "Time to test your luck!" + }, + "2": { + "label": "Leave", + "tooltip": "(-) No Rewards", + "selected": "You hurry along your way,\nwith a slight feeling of regret." + } + }, + "safari": { + "1": { + "label": "Throw a Pokéball", + "tooltip": "(+) Throw a Pokéball", + "selected": "You throw a Pokéball!" + }, + "2": { + "label": "Throw Bait", + "tooltip": "(+) Increases Capture Rate\n(-) Chance to Increase Flee Rate", + "selected": "You throw some bait!" + }, + "3": { + "label": "Throw Mud", + "tooltip": "(+) Decreases Flee Rate\n(-) Chance to Decrease Capture Rate", + "selected": "You throw some mud!" + }, + "4": { + "label": "Flee", + "tooltip": "(?) Flee from this Pokémon" + }, + "watching": "{{pokemonName}} is watching carefully!", + "eating": "{{pokemonName}} is eating!", + "busy_eating": "{{pokemonName}} is busy eating!", + "angry": "{{pokemonName}} is angry!", + "beside_itself_angry": "{{pokemonName}} is beside itself with anger!", + "remaining_count": "{{remainingCount}} Pokémon remaining!" + }, + "outro": "That was a fun little excursion!" +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/shady-vitamin-dealer-dialogue.json b/src/locales/en/mystery-encounters/shady-vitamin-dealer-dialogue.json new file mode 100644 index 00000000000..d0003de07f1 --- /dev/null +++ b/src/locales/en/mystery-encounters/shady-vitamin-dealer-dialogue.json @@ -0,0 +1,27 @@ +{ + "intro": "A man in a dark coat approaches you.", + "speaker": "Shady Salesman", + "intro_dialogue": ".@d{16}.@d{16}.@d{16}$I've got the goods if you've got the money.$Make sure your Pokémon can handle it though.", + "title": "The Vitamin Dealer", + "description": "The man opens his jacket to reveal some Pokémon vitamins. The numbers he quotes seem like a really good deal. Almost too good...\nHe offers two package deals to choose from.", + "query": "Which deal will you choose?", + "invalid_selection": "Pokémon must be healthy enough.", + "option": { + "1": { + "label": "The Cheap Deal", + "tooltip": "(-) Pay {{option1Money, money}}\n(-) Side Effects?\n(+) Chosen Pokémon Gains 2 Random Vitamins" + }, + "2": { + "label": "The Pricey Deal", + "tooltip": "(-) Pay {{option2Money, money}}\n(+) Chosen Pokémon Gains 2 Random Vitamins" + }, + "3": { + "label": "Leave", + "tooltip": "(-) No Rewards", + "selected": "Heh, wouldn't have figured you for a coward." + }, + "selected": "The man hands you two bottles and quickly disappears.${{selectedPokemon}} gained {{boost1}} and {{boost2}} boosts!" + }, + "cheap_side_effects": "But the medicine had some side effects!$Your {{selectedPokemon}} takes some damage,\nand its Nature is changed to {{newNature}}!", + "no_bad_effects": "Looks like there were no side-effects from the medicine!" +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/slumbering-snorlax-dialogue.json b/src/locales/en/mystery-encounters/slumbering-snorlax-dialogue.json new file mode 100644 index 00000000000..cd3bb7465c4 --- /dev/null +++ b/src/locales/en/mystery-encounters/slumbering-snorlax-dialogue.json @@ -0,0 +1,25 @@ +{ + "intro": "As you walk down a narrow pathway, you see a towering silhouette blocking your path.$You get closer to see a {{snorlaxName}} sleeping peacefully.\nIt seems like there's no way around it.", + "title": "Slumbering {{snorlaxName}}", + "description": "You could attack it to try and get it to move, or simply wait for it to wake up. Who knows how long that could take, though...", + "query": "What will you do?", + "option": { + "1": { + "label": "Battle It", + "tooltip": "(-) Fight Sleeping {{snorlaxName}}\n(+) Special Reward", + "selected": "You approach the\nPokémon without fear." + }, + "2": { + "label": "Wait for It to Move", + "tooltip": "(-) Wait a Long Time\n(+) Recover Party", + "selected": ".@d{32}.@d{32}.@d{32}$You wait for a time, but the {{snorlaxName}}'s yawns make your party sleepy...", + "rest_result": "When you all awaken, the {{snorlaxName}} is no where to be found -\nbut your Pokémon are all healed!" + }, + "3": { + "label": "Steal Its Item", + "tooltip": "(+) {{option3PrimaryName}} uses {{option3PrimaryMove}}\n(+) Special Reward", + "disabled_tooltip": "Your Pokémon need to know certain moves to choose this", + "selected": "Your {{option3PrimaryName}} uses {{option3PrimaryMove}}!$@s{item_fanfare}It steals Leftovers off the sleeping\n{{snorlaxName}} and you make out like bandits!" + } + } +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/teleporting-hijinks-dialogue.json b/src/locales/en/mystery-encounters/teleporting-hijinks-dialogue.json new file mode 100644 index 00000000000..c295867f521 --- /dev/null +++ b/src/locales/en/mystery-encounters/teleporting-hijinks-dialogue.json @@ -0,0 +1,27 @@ +{ + "intro": "It's a strange machine, whirring noisily...", + "title": "Teleportating Hijinks", + "description": "The machine has a sign on it that reads:\n \"To use, insert money then step into the capsule.\"\n\nPerhaps it can transport you somewhere...", + "query": "What will you do?", + "option": { + "1": { + "label": "Put Money In", + "tooltip": "(-) Pay {{price, money}}\n(?) Teleport to New Biome", + "selected": "You insert some money, and the capsule opens.\nYou step inside..." + }, + "2": { + "label": "A Pokémon Helps", + "tooltip": "(-) {{option2PrimaryName}} Helps\n(+) {{option2PrimaryName}} gains EXP\n(?) Teleport to New Biome", + "disabled_tooltip": "You need a Steel or Electric Type Pokémon to choose this", + "selected": "{{option2PrimaryName}}'s Type allows it to bypass the machine's paywall!$The capsule opens, and you step inside..." + }, + "3": { + "label": "Inspect the Machine", + "tooltip": "(-) Pokémon Battle", + "selected": "You are drawn in by the blinking lights\nand strange noises coming from the machine...$You don't even notice as a wild\nPokémon sneaks up and ambushes you!" + } + }, + "transport": "The machine shakes violently,\nmaking all sorts of strange noises!$Just as soon as it had started, it quiets once more.", + "attacked": "You step out into a completely new area, startling a wild Pokémon!$The wild Pokémon attacks!", + "boss_enraged": "The opposing {{enemyPokemon}} has become enraged!" +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/the-expert-pokemon-breeder-dialogue.json b/src/locales/en/mystery-encounters/the-expert-pokemon-breeder-dialogue.json new file mode 100644 index 00000000000..3c74c7b2726 --- /dev/null +++ b/src/locales/en/mystery-encounters/the-expert-pokemon-breeder-dialogue.json @@ -0,0 +1,31 @@ +{ + "intro": "It's a trainer carrying tons of Pokémon Eggs!", + "intro_dialogue": "Hey there, trainer!$It looks like some of your\npartner Pokémon are feeling a little down.$Why not have a battle with me to cheer them up?", + "title": "The Expert Pokémon Breeder", + "description": "You've been challenged to a battle where @[TOOLTIP_TITLE]{you can only use a single Pokémon}. It might be tough, but it would surely deepen the bond you have with the Pokémon you choose!\nThe breeder will also give you some @[TOOLTIP_TITLE]{Pokémon Eggs} if you win!", + "query": "Who will you battle with?", + "cleffa_1_nickname": "Ace", + "cleffa_2_nickname": "Clefablest", + "cleffa_3_nickname": "{{speciesName}} the Great", + "option": { + "1": { + "label": "{{pokemon1Name}}", + "tooltip_base": "(-) Tough Battle\n(+) Gain Friendship with {{pokemon1Name}}" + }, + "2": { + "label": "{{pokemon2Name}}", + "tooltip_base": "(-) Tough Battle\n(+) Gain Friendship with {{pokemon2Name}}" + }, + "3": { + "label": "{{pokemon3Name}}", + "tooltip_base": "(-) Tough Battle\n(+) Gain Friendship with {{pokemon3Name}}" + }, + "selected": "Let's do this!" + }, + "outro": "Look how happy your {{chosenPokemon}} is now!$Here, you can have these as well.", + "outro_failed": "How disappointing...$It looks like you still have a long way\nto go to earn your Pokémon's trust!", + "gained_eggs": "@s{item_fanfare}You received {{numEggs}}!", + "eggs_tooltip": "\n(+) Earn {{eggs}}", + "numEggs_one": "{{count}} {{rarity}} Egg", + "numEggs_other": "{{count}} {{rarity}} Eggs" +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/the-pokemon-salesman-dialogue.json b/src/locales/en/mystery-encounters/the-pokemon-salesman-dialogue.json new file mode 100644 index 00000000000..7e8091bbfff --- /dev/null +++ b/src/locales/en/mystery-encounters/the-pokemon-salesman-dialogue.json @@ -0,0 +1,23 @@ +{ + "intro": "A chipper elderly man approaches you.", + "speaker": "Gentleman", + "intro_dialogue": "Hello there! Have I got a deal just for YOU!", + "title": "The Pokémon Salesman", + "description": "\"This {{purchasePokemon}} is extremely unique and carries an ability not normally found in its species! I'll let you have this swell {{purchasePokemon}} for just {{price, money}}!\"\n\n\"What do you say?\"", + "description_shiny": "\"This {{purchasePokemon}} is extremely unique and has a pigment not normally found in its species! I'll let you have this swell {{purchasePokemon}} for just {{price, money}}!\"\n\n\"What do you say?\"", + "query": "What will you do?", + "option": { + "1": { + "label": "Accept", + "tooltip": "(-) Pay {{price, money}}\n(+) Gain a {{purchasePokemon}} with its Hidden Ability", + "tooltip_shiny": "(-) Pay {{price, money}}\n(+) Gain a shiny {{purchasePokemon}}", + "selected_message": "You paid an outrageous sum and bought the {{purchasePokemon}}.", + "selected_dialogue": "Excellent choice!$I can see you've a keen eye for business.$Oh, yeah...@d{64} Returns not accepted, got that?" + }, + "2": { + "label": "Refuse", + "tooltip": "(-) No Rewards", + "selected": "No?@d{32} You say no?$I'm only doing this as a favor to you!" + } + } +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/the-strong-stuff-dialogue.json b/src/locales/en/mystery-encounters/the-strong-stuff-dialogue.json new file mode 100644 index 00000000000..b5403616c9b --- /dev/null +++ b/src/locales/en/mystery-encounters/the-strong-stuff-dialogue.json @@ -0,0 +1,21 @@ +{ + "intro": "It's a massive {{shuckleName}} and what appears\nto be a large stash of... juice?", + "title": "The Strong Stuff", + "description": "The {{shuckleName}} that blocks your path looks incredibly strong. Meanwhile, the juice next to it is emanating power of some kind.\n\nThe {{shuckleName}} extends its feelers in your direction. It seems like it wants to do something...", + "query": "What will you do?", + "option": { + "1": { + "label": "Approach the {{shuckleName}}", + "tooltip": "(?) Something awful or amazing might happen", + "selected": "You black out.", + "selected_2": "@f{150}When you awaken, the {{shuckleName}} is gone\nand juice stash completely drained.${{highBstPokemon1}} and {{highBstPokemon2}}\nfeel a terrible lethargy come over them!$Their base stats were reduced by {{reductionValue}}!$Your remaining Pokémon feel an incredible vigor, though!\nTheir base stats are increased by {{increaseValue}}!" + }, + "2": { + "label": "Battle the {{shuckleName}}", + "tooltip": "(-) Hard Battle\n(+) Special Rewards", + "selected": "Enraged, the {{shuckleName}} drinks some of its juice and attacks!", + "stat_boost": "The {{shuckleName}}'s juice boosts its stats!" + } + }, + "outro": "What a bizarre turn of events." +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/the-winstrate-challenge-dialogue.json b/src/locales/en/mystery-encounters/the-winstrate-challenge-dialogue.json new file mode 100644 index 00000000000..37807a91667 --- /dev/null +++ b/src/locales/en/mystery-encounters/the-winstrate-challenge-dialogue.json @@ -0,0 +1,22 @@ +{ + "intro": "It's a family standing outside their house!", + "speaker": "The Winstrates", + "intro_dialogue": "We're the Winstrates!$What do you say to taking on our family in a series of Pokémon battles?", + "title": "The Winstrate Challenge", + "description": "The Winstrates are a family of 5 trainers, and they want to battle! If you beat all of them back-to-back, they'll give you a grand prize. But can you handle the heat?", + "query": "What will you do?", + "option": { + "1": { + "label": "Accept the Challenge", + "tooltip": "(-) Brutal Battle\n(+) Special Item Reward", + "selected": "Let the challenge begin!" + }, + "2": { + "label": "Refuse the Challenge", + "tooltip": "(+) Full Heal Party\n(+) Gain a Rarer Candy", + "selected": "That's too bad. Say, your team looks worn out, why don't you stay awhile and rest?" + } + }, + "victory": "Congratulations on beating our challenge!$First off, we'd like you to have this Voucher.", + "victory_2": "Also, our family uses this Macho Brace to strengthen\nour Pokémon more effectively during training.$You may not need it considering that you beat the whole lot of us, but we hope you'll accept it anyway!" +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/training-session-dialogue.json b/src/locales/en/mystery-encounters/training-session-dialogue.json new file mode 100644 index 00000000000..f018018fe4e --- /dev/null +++ b/src/locales/en/mystery-encounters/training-session-dialogue.json @@ -0,0 +1,33 @@ +{ + "intro": "You've come across some\ntraining tools and supplies.", + "title": "Training Session", + "description": "These supplies look like they could be used to train a member of your party! There are a few ways you could train your Pokémon, by battling against it with the rest of your team.", + "query": "How should you train?", + "invalid_selection": "Pokémon must be healthy enough.", + "option": { + "1": { + "label": "Light Training", + "tooltip": "(-) Light Battle\n(+) Improve 2 Random IVs of Pokémon", + "finished": "{{selectedPokemon}} returns, feeling\nworn out but accomplished!$Its {{stat1}} and {{stat2}} IVs were improved!" + }, + "2": { + "label": "Moderate Training", + "tooltip": "(-) Moderate Battle\n(+) Change Pokémon's Nature", + "select_prompt": "Select a new nature\nto train your Pokémon in.", + "finished": "{{selectedPokemon}} returns, feeling\nworn out but accomplished!$Its nature was changed to {{nature}}!" + }, + "3": { + "label": "Heavy Training", + "tooltip": "(-) Harsh Battle\n(+) Change Pokémon's Ability", + "select_prompt": "Select a new ability\nto train your Pokémon in.", + "finished": "{{selectedPokemon}} returns, feeling\nworn out but accomplished!$Its ability was changed to {{ability}}!" + }, + "4": { + "label": "Leave", + "tooltip": "(-) No Rewards", + "selected": "You've no time for training.\nTime to move on." + }, + "selected": "{{selectedPokemon}} moves across\nthe clearing to face you..." + }, + "outro": "That was a successful training session!" +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/trash-to-treasure-dialogue.json b/src/locales/en/mystery-encounters/trash-to-treasure-dialogue.json new file mode 100644 index 00000000000..fe2cb54f5b1 --- /dev/null +++ b/src/locales/en/mystery-encounters/trash-to-treasure-dialogue.json @@ -0,0 +1,19 @@ +{ + "intro": "It's a massive pile of garbage!\nWhere did this come from?", + "title": "Trash to Treasure", + "description": "The garbage heap looms over you, and you can spot some items of value buried amidst the refuse. Are you sure you want to get covered in filth to get them, though?", + "query": "What will you do?", + "option": { + "1": { + "label": "Dig for Valuables", + "tooltip": "(-) Items in Shops Cost 3x\n(+) Gain Amazing Items", + "selected": "You wade through the garbage pile, becoming mired in filth.$There's no way any respectable shopkeeper would\nsell you items at the normal rate in your grimy state!$You'll have to pay extra for items now.$However, you found some incredible items in the garbage!" + }, + "2": { + "label": "Investigate Further", + "tooltip": "(?) Find the Source of the Garbage", + "selected": "You wander around the heap, searching for any indication as to how this might have appeared here...", + "selected_2": "Suddenly, the garbage shifts! It wasn't just garbage, it was a Pokémon!" + } + } +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/uncommon-breed-dialogue.json b/src/locales/en/mystery-encounters/uncommon-breed-dialogue.json new file mode 100644 index 00000000000..e6f5b3d3fcd --- /dev/null +++ b/src/locales/en/mystery-encounters/uncommon-breed-dialogue.json @@ -0,0 +1,26 @@ +{ + "intro": "That isn't just an ordinary Pokémon!", + "title": "Uncommon Breed", + "description": "That {{enemyPokemon}} looks special compared to others of its kind. @[TOOLTIP_TITLE]{Perhaps it knows a special move?} You could battle and catch it outright, but there might also be a way to befriend it.", + "query": "What will you do?", + "option": { + "1": { + "label": "Battle the Pokémon", + "tooltip": "(-) Tricky Battle\n(+) Strong Catchable Foe", + "selected": "You approach the\n{{enemyPokemon}} without fear.", + "stat_boost": "The {{enemyPokemon}}'s heightened abilities boost its stats!" + }, + "2": { + "label": "Give It Food", + "disabled_tooltip": "You need 4 berry items to choose this", + "tooltip": "(-) Give 4 Berries\n(+) The {{enemyPokemon}} Likes You", + "selected": "You toss the berries at the {{enemyPokemon}}!$It eats them happily!$The {{enemyPokemon}} wants to join your party!" + }, + "3": { + "label": "Befriend It", + "disabled_tooltip": "Your Pokémon need to know certain moves to choose this", + "tooltip": "(+) {{option3PrimaryName}} uses {{option3PrimaryMove}}\n(+) The {{enemyPokemon}} Likes You", + "selected": "Your {{option3PrimaryName}} uses {{option3PrimaryMove}} to charm the {{enemyPokemon}}!$The {{enemyPokemon}} wants to join your party!" + } + } +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/weird-dream-dialogue.json b/src/locales/en/mystery-encounters/weird-dream-dialogue.json new file mode 100644 index 00000000000..44acde84002 --- /dev/null +++ b/src/locales/en/mystery-encounters/weird-dream-dialogue.json @@ -0,0 +1,22 @@ +{ + "intro": "A shadowy woman blocks your path.\nSomething about her is unsettling...", + "speaker": "Woman", + "intro_dialogue": "I have seen your futures, your pasts...$Child, do you see them too?", + "title": "???", + "description": "The woman's words echo in your head. It wasn't just a singular voice, but a vast multitude, from all timelines and realities. You begin to feel dizzy, the question lingering on your mind...\n\n@[TOOLTIP_TITLE]{\"I have seen your futures, your pasts... Child, do you see them too?\"}", + "query": "What will you do?", + "option": { + "1": { + "label": "\"I See Them\"", + "tooltip": "@[SUMMARY_GREEN]{(?) Affects your Pokémon}", + "selected": "Her hand reaches out to touch you,\nand everything goes black.$Then...@d{64} You see everything.\nEvery timeline, all your different selves,\n past and future.$Everything that has made you,\neverything you will become...@d{64}", + "cutscene": "You see your Pokémon,@d{32} converging from\nevery reality to become something new...@d{64}", + "dream_complete": "When you awaken, the woman - was it a woman or a ghost? - is gone...$.@d{32}.@d{32}.@d{32}$Your Pokémon team has changed...\nOr is it the same team you've always had?" + }, + "2": { + "label": "Quickly Leave", + "tooltip": "(-) Affects your Pokémon", + "selected": "You tear your mind from a numbing grip, and hastily depart.$When you finally stop to collect yourself, you check the Pokémon in your team.$For some reason, all of their levels have decreased!" + } + } +} \ No newline at end of file diff --git a/src/locales/en/party-ui-handler.json b/src/locales/en/party-ui-handler.json index 9c2b3f30e5e..8e6e8046c7e 100644 --- a/src/locales/en/party-ui-handler.json +++ b/src/locales/en/party-ui-handler.json @@ -13,8 +13,10 @@ "ALL": "All", "PASS_BATON": "Pass Baton", "UNPAUSE_EVOLUTION": "Unpause Evolution", + "PAUSE_EVOLUTION": "Pause Evolution", "REVIVE": "Revive", "RENAME": "Rename", + "SELECT": "Select", "choosePokemon": "Choose a Pokémon.", "doWhatWithThisPokemon": "Do what with this Pokémon?", "noEnergy": "{{pokemonName}} has no energy\nleft to battle!", @@ -23,6 +25,7 @@ "tooManyItems": "{{pokemonName}} has too many\nof this item!", "anyEffect": "It won't have any effect.", "unpausedEvolutions": "Evolutions have been unpaused for {{pokemonName}}.", + "pausedEvolutions": "Evolutions have been paused for {{pokemonName}}.", "unspliceConfirmation": "Do you really want to unsplice {{fusionName}}\nfrom {{pokemonName}}? {{fusionName}} will be lost.", "wasReverted": "{{fusionName}} was reverted to {{pokemonName}}.", "releaseConfirmation": "Do you really want to release {{pokemonName}}?", diff --git a/src/locales/en/pokemon-form.json b/src/locales/en/pokemon-form.json index ea7e0f60c90..642d31a2a20 100644 --- a/src/locales/en/pokemon-form.json +++ b/src/locales/en/pokemon-form.json @@ -1,4 +1,5 @@ { + "pikachu": "Normal", "pikachuCosplay": "Cosplay", "pikachuCoolCosplay": "Cool Cosplay", "pikachuBeautyCosplay": "Beauty Cosplay", @@ -6,8 +7,10 @@ "pikachuSmartCosplay": "Smart Cosplay", "pikachuToughCosplay": "Tough Cosplay", "pikachuPartner": "Partner", + "eevee": "Normal", "eeveePartner": "Partner", - "pichuSpiky": "Spiky", + "pichu": "Normal", + "pichuSpiky": "Spiky-Eared", "unownA": "A", "unownB": "B", "unownC": "C", @@ -36,135 +39,252 @@ "unownZ": "Z", "unownExclamation": "!", "unownQuestion": "?", - "castformSunny": "Sunny", - "castformRainy": "Rainy", - "castformSnowy": "Snowy", - "deoxysNormal": "Normal", - "burmyPlant": "Plant", - "burmySandy": "Sandy", - "burmyTrash": "Trash", - "shellosEast": "East", - "shellosWest": "West", + "castform": "Normal Form", + "castformSunny": "Sunny Form", + "castformRainy": "Rainy Form", + "castformSnowy": "Snowy Form", + "deoxysNormal": "Normal Forme", + "deoxysAttack": "Attack Forme", + "deoxysDefense": "Defense Forme", + "deoxysSpeed": "Speed Forme", + "burmyPlant": "Plant Cloak", + "burmySandy": "Sandy Cloak", + "burmyTrash": "Trash Cloak", + "cherubiOvercast": "Overcast Form", + "cherubiSunshine": "Sunshine Form", + "shellosEast": "East Sea", + "shellosWest": "West Sea", + "rotom": "Normal", "rotomHeat": "Heat", "rotomWash": "Wash", "rotomFrost": "Frost", "rotomFan": "Fan", "rotomMow": "Mow", - "giratinaAltered": "Altered", - "shayminLand": "Land", - "basculinRedStriped": "Red Striped", - "basculinBlueStriped": "Blue Striped", - "basculinWhiteStriped": "White Striped", - "deerlingSpring": "Spring", - "deerlingSummer": "Summer", - "deerlingAutumn": "Autumn", - "deerlingWinter": "Winter", - "tornadusIncarnate": "Incarnate", - "thundurusIncarnate": "Incarnate", - "landorusIncarnate": "Incarnate", - "keldeoOrdinary": "Ordinary", - "meloettaAria": "Aria", - "meloettaPirouette": "Pirouette", + "dialga": "Normal", + "dialgaOrigin": "Origin Forme", + "palkia": "Normal", + "palkiaOrigin": "Origin Forme", + "giratinaAltered": "Altered Forme", + "giratinaOrigin": "Origin Forme", + "shayminLand": "Land Forme", + "shayminSky": "Sky Forme", + "basculinRedStriped": "Red-Striped Form", + "basculinBlueStriped": "Blue-Striped Form", + "basculinWhiteStriped": "White-Striped Form", + "darumaka": "Standard Mode", + "darumakaZen": "Zen Mode", + "deerlingSpring": "Spring Form", + "deerlingSummer": "Summer Form", + "deerlingAutumn": "Autumn Form", + "deerlingWinter": "Winter Form", + "tornadusIncarnate": "Incarnate Forme", + "tornadusTherian": "Therian Forme", + "thundurusIncarnate": "Incarnate Forme", + "thundurusTherian": "Therian Forme", + "landorusIncarnate": "Incarnate Forme", + "landorusTherian": "Therian Forme", + "kyurem": "Normal", + "kyuremBlack": "Black", + "kyuremWhite": "White", + "keldeoOrdinary": "Ordinary Form", + "keldeoResolute": "Resolute", + "meloettaAria": "Aria Forme", + "meloettaPirouette": "Pirouette Forme", + "genesect": "Normal", + "genesectShock": "Shock Drive", + "genesectBurn": "Burn Drive", + "genesectChill": "Chill Drive", + "genesectDouse": "Douse Drive", + "froakie": "Normal", "froakieBattleBond": "Battle Bond", - "scatterbugMeadow": "Meadow", - "scatterbugIcySnow": "Icy Snow", - "scatterbugPolar": "Polar", - "scatterbugTundra": "Tundra", - "scatterbugContinental": "Continental", - "scatterbugGarden": "Garden", - "scatterbugElegant": "Elegant", - "scatterbugModern": "Modern", - "scatterbugMarine": "Marine", - "scatterbugArchipelago": "Archipelago", - "scatterbugHighPlains": "High Plains", - "scatterbugSandstorm": "Sandstorm", - "scatterbugRiver": "River", - "scatterbugMonsoon": "Monsoon", - "scatterbugSavanna": "Savanna", - "scatterbugSun": "Sun", - "scatterbugOcean": "Ocean", - "scatterbugJungle": "Jungle", - "scatterbugFancy": "Fancy", - "scatterbugPokeBall": "Poké Ball", - "flabebeRed": "Red", - "flabebeYellow": "Yellow", - "flabebeOrange": "Orange", - "flabebeBlue": "Blue", - "flabebeWhite": "White", - "furfrouHeart": "Heart", - "furfrouStar": "Star", - "furfrouDiamond": "Diamond", - "furfrouDebutante": "Debutante", - "furfrouMatron": "Matron", - "furfrouDandy": "Dandy", - "furfrouLaReine": "La Reine", - "furfrouKabuki": "Kabuki", - "furfrouPharaoh": "Pharaoh", - "pumpkabooSmall": "Small", - "pumpkabooLarge": "Large", - "pumpkabooSuper": "Super", - "xerneasNeutral": "Neutral", - "xerneasActive": "Active", + "froakieAsh": "Ash", + "scatterbugMeadow": "Meadow Pattern", + "scatterbugIcySnow": "Icy Snow Pattern", + "scatterbugPolar": "Polar Pattern", + "scatterbugTundra": "Tundra Pattern", + "scatterbugContinental": "Continental Pattern", + "scatterbugGarden": "Garden Pattern", + "scatterbugElegant": "Elegant Pattern", + "scatterbugModern": "Modern Pattern", + "scatterbugMarine": "Marine Pattern", + "scatterbugArchipelago": "Archipelago Pattern", + "scatterbugHighPlains": "High Plains Pattern", + "scatterbugSandstorm": "Sandstorm Pattern", + "scatterbugRiver": "River Pattern", + "scatterbugMonsoon": "Monsoon Pattern", + "scatterbugSavanna": "Savanna Pattern", + "scatterbugSun": "Sun Pattern", + "scatterbugOcean": "Ocean Pattern", + "scatterbugJungle": "Jungle Pattern", + "scatterbugFancy": "Fancy Pattern", + "scatterbugPokeBall": "Poké Ball Pattern", + "flabebeRed": "Red Flower", + "flabebeYellow": "Yellow Flower", + "flabebeOrange": "Orange Flower", + "flabebeBlue": "Blue Flower", + "flabebeWhite": "White Flower", + "furfrou": "Natural Form", + "furfrouHeart": "Heart Trim", + "furfrouStar": "Star Trim", + "furfrouDiamond": "Diamond Trim", + "furfrouDebutante": "Debutante Trim", + "furfrouMatron": "Matron Trim", + "furfrouDandy": "Dandy Trim", + "furfrouLaReine": "La Reine Trim", + "furfrouKabuki": "Kabuki Trim", + "furfrouPharaoh": "Pharaoh Trim", + "espurrMale": "Male", + "espurrFemale": "Female", + "honedgeShiled": "Shield Forme", + "honedgeBlade": "Blade Forme", + "pumpkaboo": "Average Size", + "pumpkabooSmall": "Small Size", + "pumpkabooLarge": "Large Size", + "pumpkabooSuper": "Super Size", + "xerneasNeutral": "Neutral Mode", + "xerneasActive": "Active Mode", "zygarde50": "50% Forme", "zygarde10": "10% Forme", "zygarde50Pc": "50% Forme Power Construct", "zygarde10Pc": "10% Forme Power Construct", "zygardeComplete": "Complete Forme", - "oricorioBaile": "Baile", - "oricorioPompom": "Pom-Pom", - "oricorioPau": "Pau", - "oricorioSensu": "Sensu", + "hoopa": "Confined", + "hoopaUnbound": "Unbound", + "oricorioBaile": "Baile Style", + "oricorioPompom": "Pom-Pom Style", + "oricorioPau": "Pau Style", + "oricorioSensu": "Sensu Style", + "rockruff": "Normal", "rockruffOwnTempo": "Own Tempo", - "miniorRedMeteor": "Red Meteor", - "miniorOrangeMeteor": "Orange Meteor", - "miniorYellowMeteor": "Yellow Meteor", - "miniorGreenMeteor": "Green Meteor", - "miniorBlueMeteor": "Blue Meteor", - "miniorIndigoMeteor": "Indigo Meteor", - "miniorVioletMeteor": "Violet Meteor", - "miniorRed": "Red", - "miniorOrange": "Orange", - "miniorYellow": "Yellow", - "miniorGreen": "Green", - "miniorBlue": "Blue", - "miniorIndigo": "Indigo", - "miniorViolet": "Violet", - "mimikyuDisguised": "Disguised", - "mimikyuBusted": "Busted", + "rockruffMidday": "Midday Form", + "rockruffMidnight": "Midnight Form", + "rockruffDusk": "Dusk Form", + "wishiwashi": "Solo Form", + "wishiwashiSchool": "School", + "typeNullNormal": "Type: Normal", + "typeNullFighting": "Type: Fighting", + "typeNullFlying": "Type: Flying", + "typeNullPoison": "Type: Poison", + "typeNullGround": "Type: Ground", + "typeNullRock": "Type: Rock", + "typeNullBug": "Type: Bug", + "typeNullGhost": "Type: Ghost", + "typeNullSteel": "Type: Steel", + "typeNullFire": "Type: Fire", + "typeNullWater": "Type: Water", + "typeNullGrass": "Type: Grass", + "typeNullElectric": "Type: Electric", + "typeNullPsychic": "Type: Psychic", + "typeNullIce": "Type: Ice", + "typeNullDragon": "Type: Dragon", + "typeNullDark": "Type: Dark", + "typeNullFairy": "Type: Fairy", + "miniorRedMeteor": "Red Meteor Form", + "miniorOrangeMeteor": "Orange Meteor Form", + "miniorYellowMeteor": "Yellow Meteor Form", + "miniorGreenMeteor": "Green Meteor Form", + "miniorBlueMeteor": "Blue Meteor Form", + "miniorIndigoMeteor": "Indigo Meteor Form", + "miniorVioletMeteor": "Violet Meteor Form", + "miniorRed": "Red Core Form", + "miniorOrange": "Orange Core Form", + "miniorYellow": "Yellow Core Form", + "miniorGreen": "Green Core Form", + "miniorBlue": "Blue Core Form", + "miniorIndigo": "Indigo Core Form", + "miniorViolet": "Violet Core Form", + "mimikyuDisguised": "Disguised Form", + "mimikyuBusted": "Busted Form", + "necrozma": "Normal", + "necrozmaDuskMane": "Dusk Mane", + "necrozmaDawnWings": "Dawn Wings", + "necrozmaUltra": "Ultra", + "magearna": "Normal", "magearnaOriginal": "Original", + "marshadow": "Normal", "marshadowZenith": "Zenith", - "sinisteaPhony": "Phony", - "sinisteaAntique": "Antique", + "cramorant": "Normal", + "cramorantGulping": "Gulping Form", + "cramorantGorging": "Gorging Form", + "toxelAmped": "Amped Form", + "toxelLowkey": "Low-Key Form", + "sinisteaPhony": "Phony Form", + "sinisteaAntique": "Antique Form", + "milceryVanillaCream": "Vanilla Cream", + "milceryRubyCream": "Ruby Cream", + "milceryMatchaCream": "Matcha Cream", + "milceryMintCream": "Mint Cream", + "milceryLemonCream": "Lemon Cream", + "milcerySaltedCream": "Salted Cream", + "milceryRubySwirl": "Ruby Swirl", + "milceryCaramelSwirl": "Caramel Swirl", + "milceryRainbowSwirl": "Rainbow Swirl", + "eiscue": "Ice Face", "eiscueNoIce": "No Ice", "indeedeeMale": "Male", "indeedeeFemale": "Female", - "morpekoFullBelly": "Full Belly", + "morpekoFullBelly": "Full Belly Mode", + "morpekoHangry": "Hangry Mode", "zacianHeroOfManyBattles": "Hero Of Many Battles", + "zacianCrowned": "Crowned", "zamazentaHeroOfManyBattles": "Hero Of Many Battles", + "zamazentaCrowned": "Crowned", + "kubfuSingleStrike": "Single Strike Style", + "kubfuRapidStrike": "Rapid Strike Style", + "zarude": "Normal", "zarudeDada": "Dada", - "enamorusIncarnate": "Incarnate", + "calyrex": "Normal", + "calyrexIce": "Ice Rider", + "calyrexShadow": "Shadow Rider", + "basculinMale": "Male", + "basculinFemale": "Female", + "enamorusIncarnate": "Incarnate Forme", + "enamorusTherian": "Therian Forme", + "lechonkMale": "Male", + "lechonkFemale": "Female", + "tandemausFour": "Family of Four", + "tandemausThree": "Family of Three", "squawkabillyGreenPlumage": "Green Plumage", "squawkabillyBluePlumage": "Blue Plumage", "squawkabillyYellowPlumage": "Yellow Plumage", "squawkabillyWhitePlumage": "White Plumage", - "tatsugiriCurly": "Curly", - "tatsugiriDroopy": "Droopy", - "tatsugiriStretchy": "Stretchy", - "gimmighoulChest": "Chest", - "gimmighoulRoaming": "Roaming", + "finizenZero": "Zero Form", + "finizenHero": "Hero Form", + "tatsugiriCurly": "Curly Form", + "tatsugiriDroopy": "Droopy Form", + "tatsugiriStretchy": "Stretchy Form", + "dunsparceTwo": "Two-Segment Form", + "dunsparceThree": "Three-Segment Form", + "gimmighoulChest": "Chest Form", + "gimmighoulRoaming": "Roaming Form", "koraidonApexBuild": "Apex Build", "koraidonLimitedBuild": "Limited Build", "koraidonSprintingBuild": "Sprinting Build", "koraidonSwimmingBuild": "Swimming Build", "koraidonGlidingBuild": "Gliding Build", "miraidonUltimateMode": "Ultimate Mode", - "miraidonLowPowerMode": "Low Power Mode", + "miraidonLowPowerMode": "Low-Power Mode", "miraidonDriveMode": "Drive Mode", "miraidonAquaticMode": "Aquatic Mode", "miraidonGlideMode": "Glide Mode", - "poltchageistCounterfeit": "Counterfeit", - "poltchageistArtisan": "Artisan", - "paldeaTaurosCombat": "Combat", - "paldeaTaurosBlaze": "Blaze", - "paldeaTaurosAqua": "Aqua" -} \ No newline at end of file + "poltchageistCounterfeit": "Counterfeit Form", + "poltchageistArtisan": "Artisan Form", + "poltchageistUnremarkable": "Unremarkable Form", + "poltchageistMasterpiece": "Masterpiece Form", + "ogerponTealMask": "Teal Mask", + "ogerponTealMaskTera": "Teal Mask Terastallized", + "ogerponWellspringMask": "Wellspring Mask", + "ogerponWellspringMaskTera": "Wellspring Mask Terastallized", + "ogerponHearthflameMask": "Hearthflame Mask", + "ogerponHearthflameMaskTera": "Hearthflame Mask Terastallized", + "ogerponCornerstoneMask": "Cornerstone Mask", + "ogerponCornerstoneMaskTera": "Cornerstone Mask Terastallized", + "terpagos": "Normal Form", + "terpagosTerastal": "Terastal Form", + "terpagosStellar": "Stellar Form", + "galarDarumaka": "Standard Mode", + "galarDarumakaZen": "Zen Mode", + "paldeaTaurosCombat": "Combat Breed", + "paldeaTaurosBlaze": "Blaze Breed", + "paldeaTaurosAqua": "Aqua Breed" +} diff --git a/src/locales/en/pokemon-summary.json b/src/locales/en/pokemon-summary.json index 80e0cdab010..458fad0efe0 100644 --- a/src/locales/en/pokemon-summary.json +++ b/src/locales/en/pokemon-summary.json @@ -11,7 +11,7 @@ "cancel": "Cancel", "memoString": "{{natureFragment}} nature,\n{{metFragment}}", "metFragment": { - "normal": "met at Lv{{level}},\n{{biome}}.", + "normal": "met at Lv{{level}},\n{{biome}}, Wave {{wave}}.", "apparently": "apparently met at Lv{{level}},\n{{biome}}." }, "natureFragment": { diff --git a/src/locales/en/splash-messages.json b/src/locales/en/splash-messages.json index c0686e6ad75..168974525f8 100644 --- a/src/locales/en/splash-messages.json +++ b/src/locales/en/splash-messages.json @@ -1,5 +1,5 @@ { - "battlesWon": "Battles Won!", + "battlesWon": "{{count, number}} Battles Won!", "joinTheDiscord": "Join the Discord!", "infiniteLevels": "Infinite Levels!", "everythingStacks": "Everything Stacks!", @@ -32,5 +32,17 @@ "alsoTryRadicalRed": "Also Try Radical Red!", "eeveeExpo": "Eevee Expo!", "ynoproject": "YNOproject!", - "breedersInSpace": "Breeders in space!" + "breedersInSpace": "Breeders in space!", + "halloween": { + "pumpkaboosAbout": "Pumpkaboos about!", + "mayContainSpiders": "May contain spiders!", + "spookyScaryDuskulls": "Spooky, Scary Duskulls!" + }, + "xmas": { + "happyHolidays": "Happy Holidays!", + "delibirdSeason": "Delibird Season!" + }, + "newYears": { + "happyNewYear": "Happy New Year!" + } } \ No newline at end of file diff --git a/src/locales/en/trainer-classes.json b/src/locales/en/trainer-classes.json index 9e30915dee6..092bc8d895d 100644 --- a/src/locales/en/trainer-classes.json +++ b/src/locales/en/trainer-classes.json @@ -126,5 +126,8 @@ "skull_grunts": "Team Skull Grunts", "macro_grunt": "Macro Cosmos Trainer", "macro_grunt_female": "Macro Cosmos Trainer", - "macro_grunts": "Macro Cosmos Trainers" + "macro_grunts": "Macro Cosmos Trainers", + "star_grunt": "Star Grunt", + "star_grunt_female": "Star Grunt", + "star_grunts": "Star Grunts" } diff --git a/src/locales/en/trainer-names.json b/src/locales/en/trainer-names.json index 50a2ce18f34..5a9db128e2f 100644 --- a/src/locales/en/trainer-names.json +++ b/src/locales/en/trainer-names.json @@ -141,6 +141,11 @@ "faba": "Faba", "plumeria": "Plumeria", "oleana": "Oleana", + "giacomo": "Giacomo", + "mela": "Mela", + "atticus": "Atticus", + "ortega": "Ortega", + "eri": "Eri", "maxie": "Maxie", "archie": "Archie", @@ -150,6 +155,7 @@ "lusamine": "Lusamine", "guzma": "Guzma", "rose": "Rose", + "cassiopeia": "Penny", "blue_red_double": "Blue & Red", "red_blue_double": "Red & Blue", @@ -160,5 +166,18 @@ "alder_iris_double": "Alder & Iris", "iris_alder_double": "Iris & Alder", "marnie_piers_double": "Marnie & Piers", - "piers_marnie_double": "Piers & Marnie" + "piers_marnie_double": "Piers & Marnie", + + "buck": "Buck", + "cheryl": "Cheryl", + "marley": "Marley", + "mira": "Mira", + "riley": "Riley", + "victor": "Victor", + "victoria": "Victoria", + "vivi": "Vivi", + "vicky": "Vicky", + "vito": "Vito", + "bug_type_superfan": "Bug-Type Superfan", + "expert_pokemon_breeder": "Expert Pokémon Breeder" } diff --git a/src/locales/en/trainer-titles.json b/src/locales/en/trainer-titles.json index b9c919022be..ae19fc30790 100644 --- a/src/locales/en/trainer-titles.json +++ b/src/locales/en/trainer-titles.json @@ -19,6 +19,7 @@ "aether_boss": "Aether President", "skull_boss": "Team Skull Boss", "macro_boss": "Macro Cosmos President", + "star_boss": "Team Star Leader", "rocket_admin": "Team Rocket Admin", "rocket_admin_female": "Team Rocket Admin", @@ -34,5 +35,8 @@ "flare_admin_female": "Team Flare Admin", "aether_admin": "Aether Foundation Admin", "skull_admin": "Team Skull Admin", - "macro_admin": "Macro Cosmos" + "macro_admin": "Macro Cosmos", + "star_admin": "Team Star Squad Boss", + + "the_winstrates": "The Winstrates'" } diff --git a/src/locales/es/ability-trigger.json b/src/locales/es/ability-trigger.json index 4380c84b8e9..07ce3459dc8 100644 --- a/src/locales/es/ability-trigger.json +++ b/src/locales/es/ability-trigger.json @@ -4,7 +4,7 @@ "costar": "¡{{pokemonName}} copió los cambios de características de {{allyName}}!", "iceFaceAvoidedDamage": "¡{{pokemonNameWithAffix}} evitó\ndaño con {{abilityName}}!", "perishBody": "¡{{abilityName}} de {{pokemonName}} debilitará a ambos Pokémon en 3 turnos!", - "poisonHeal": "¡{{pokemonNameWithAffix}} restauró algunos de sus PS gracias a {{abilityName}}!", + "poisonHeal": "¡{{pokemonName}} restauró algunos de sus PS gracias a {{abilityName}}!", "trace": "¡{{pokemonName}} ha copiado la habilidad {{abilityName}} \nde {{targetName}}!", "windPowerCharged": "¡{{pokemonName}} se ha cargado de electricidad gracias a {{moveName}}!", "quickDraw": "¡{{pokemonName}} ataca primero gracias a la habilidad Mano Rápida!", diff --git a/src/locales/es/ability.json b/src/locales/es/ability.json index 598694f441e..01b5348d742 100644 --- a/src/locales/es/ability.json +++ b/src/locales/es/ability.json @@ -1237,6 +1237,6 @@ }, "poisonPuppeteer": { "name": "Títere Tóxico", - "description": "Los rivales que Pecharunt envenene con sus movimientos también sufrirán confusión." + "description": "Los rivales que el usuario envenene con sus movimientos también sufrirán confusión." } } diff --git a/src/locales/es/battle.json b/src/locales/es/battle.json index c79315f297b..c5fbf25f3ae 100644 --- a/src/locales/es/battle.json +++ b/src/locales/es/battle.json @@ -85,5 +85,7 @@ "statSeverelyFell_one": "¡El {{stats}} de {{pokemonNameWithAffix}} ha bajado muchísimo!", "statSeverelyFell_other": "¡{{stats}} de\n{{pokemonNameWithAffix}} han bajado muchísimo!", "statWontGoAnyLower_one": "¡El {{stats}} de {{pokemonNameWithAffix}} no puede bajar más!", - "statWontGoAnyLower_other": "¡{{stats}} de\n{{pokemonNameWithAffix}} no pueden bajar más!" + "statWontGoAnyLower_other": "¡{{stats}} de\n{{pokemonNameWithAffix}} no pueden bajar más!", + "battlerTagsHealBlock": "¡{{pokemonNameWithAffix}} no puede restaurar sus PS!", + "battlerTagsHealBlockOnRemove": "¡{{pokemonNameWithAffix}} ya puede recuperar PS!" } diff --git a/src/locales/es/battler-tags.json b/src/locales/es/battler-tags.json index 49efed6e8b4..bb4f0fe6c8a 100644 --- a/src/locales/es/battler-tags.json +++ b/src/locales/es/battler-tags.json @@ -70,5 +70,8 @@ "stockpilingOnAdd": "¡{{pokemonNameWithAffix}} ha reservado energía por {{stockpiledCount}}ª vez!", "disabledOnAdd": "¡Se ha anulado el movimiento {{moveName}}\nde {{pokemonNameWithAffix}}!", "disabledLapse": "¡El movimiento {{moveName}} de {{pokemonNameWithAffix}} ya no está anulado!", - "tarShotOnAdd": "¡{{pokemonNameWithAffix}} se ha vuelto débil ante el fuego!" + "tarShotOnAdd": "¡{{pokemonNameWithAffix}} se ha vuelto débil ante el fuego!", + "substituteOnAdd": "¡{{pokemonNameWithAffix}} creó un sustituto!", + "substituteOnHit": "¡El sustituto recibe daño en lugar del {{pokemonNameWithAffix}}!", + "substituteOnRemove": "¡El sustituto del {{pokemonNameWithAffix}} se debilitó!" } diff --git a/src/locales/es/bgm-name.json b/src/locales/es/bgm-name.json index f0e0ab7e852..0f261bbd563 100644 --- a/src/locales/es/bgm-name.json +++ b/src/locales/es/bgm-name.json @@ -107,17 +107,17 @@ "forest": "PMD EoS - Bosque Sombrío", "grass": "PMD EoS - Manzanar", "graveyard": "PMD EoS - Bosque Misterio", - "ice_cave": "PMD EoS - Gran Iceberg", + "ice_cave": "Firel - -50°C", "island": "PMD EoS - Costa Escarpada", "jungle": "Lmz - Jungla", "laboratory": "Firel - Laboratorio", - "lake": "PMD EoS - Cueva Cristal", + "lake": "Lmz - Lake", "meadow": "PMD EoS - Bosque de la Cumbre del Cielo", "metropolis": "Firel - Metrópolis", "mountain": "PMD EoS - Monte Cuerno", - "plains": "PMD EoS - Pradera de la Cumbre del Cielo", - "power_plant": "PMD EoS - Pradera Destello", - "ruins": "PMD EoS - Sima Hermética", + "plains": "Firel - Route 888", + "power_plant": "Firel - The Klink", + "ruins": "Lmz - Ancient Ruins", "sea": "Andr06 - Misticismo marino", "seabed": "Firel - Lecho del mar", "slum": "Andr06 - Snom sigiloso", @@ -127,7 +127,7 @@ "tall_grass": "PMD EoS - Bosque Niebla", "temple": "PMD EoS - Cueva Regia", "town": "PMD EoS - Tema del territorio aleatorio 3", - "volcano": "PMD EoS - Cueva Vapor", + "volcano": "Firel - Twisturn Volcano", "wasteland": "PMD EoS - Corazón Tierra Oculta", "encounter_ace_trainer": "BW - ¡Vs. entrenador guay!", "encounter_backpacker": "BW - ¡Vs. mochilero!", diff --git a/src/locales/es/menu.json b/src/locales/es/menu.json index ef1ae93dd82..a35025819fa 100644 --- a/src/locales/es/menu.json +++ b/src/locales/es/menu.json @@ -51,7 +51,7 @@ "renamePokemon": "Renombrar Pokémon.", "rename": "Renombrar", "nickname": "Apodo", - "errorServerDown": "¡Ups! Ha habido un problema al contactar con el servidor.\n\nPuedes mantener esta ventana abierta, el juego se reconectará automáticamente.", + "errorServerDown": "¡Ups! Ha habido un problema al contactar con el servidor.\n\nPuedes mantener esta ventana abierta,\nel juego se reconectará automáticamente.", "noSaves": "No tienes ninguna partida guardada registrada!", "tooManySaves": "¡Tienes demasiadas partidas guardadas registradas!" } diff --git a/src/locales/es/move.json b/src/locales/es/move.json index f4c28dd02e7..21f73bbf1d3 100644 --- a/src/locales/es/move.json +++ b/src/locales/es/move.json @@ -3129,7 +3129,7 @@ }, "auraWheel": { "name": "Rueda Aural", - "effect": "La energía que acumula en las mejillas le sirve para atacar y aumentar su Velocidad. Este movimiento cambia de tipo según la forma que adopte Morpeko." + "effect": "La energía que acumula en las mejillas le sirve para atacar y aumentar su Velocidad. Si es utilizado por Morpeko, este movimiento cambia de tipo según la forma que adopte." }, "breakingSwipe": { "name": "Vasto Impacto", diff --git a/src/locales/es/pokemon-form.json b/src/locales/es/pokemon-form.json index 2f70038ad2d..b6008357d89 100644 --- a/src/locales/es/pokemon-form.json +++ b/src/locales/es/pokemon-form.json @@ -1,4 +1,5 @@ { + "pikachu": "Normal", "pikachuCosplay": "Coqueta", "pikachuCoolCosplay": "Roquera", "pikachuBeautyCosplay": "Aristócrata", @@ -6,7 +7,9 @@ "pikachuSmartCosplay": "Erudita", "pikachuToughCosplay": "Enmascarada", "pikachuPartner": "Compañero", + "eevee": "Normal", "eeveePartner": "Compañero", + "pichu": "Normal", "pichuSpiky": "Picoreja", "unownA": "A", "unownB": "B", @@ -36,36 +39,65 @@ "unownZ": "Z", "unownExclamation": "!", "unownQuestion": "?", + "castform": "Normal Form", "castformSunny": "Sol", "castformRainy": "Lluvia", "castformSnowy": "Nieve", "deoxysNormal": "Normal", + "deoxysAttack": "Ataque", + "deoxysDefense": "Defensa", + "deoxysSpeed": "Velocidad", "burmyPlant": "Planta", "burmySandy": "Arena", "burmyTrash": "Basura", + "cherubiOvercast": "Encapotado", + "cherubiSunshine": "Soleado", "shellosEast": "Este", "shellosWest": "Oeste", + "rotom": "Normal", "rotomHeat": "Calor", "rotomWash": "Lavado", "rotomFrost": "Frío", "rotomFan": "Ventilador", "rotomMow": "Corte", + "dialga": "Normal", + "dialgaOrigin": "Origen", + "palkia": "Normal", + "palkiaOrigin": "Origen", "giratinaAltered": "Modificada", + "giratinaOrigin": "Origen", "shayminLand": "Tierra", + "shayminSky": "Cielo", "basculinRedStriped": "Raya Roja", "basculinBlueStriped": "Raya Azul", "basculinWhiteStriped": "Raya Blanca", + "darumaka": "Modo Normal", + "darumakaZen": "Modo Daruma", "deerlingSpring": "Primavera", "deerlingSummer": "Verano", "deerlingAutumn": "Otoño", "deerlingWinter": "Invierno", "tornadusIncarnate": "Avatar", + "tornadusTherian": "Tótem", "thundurusIncarnate": "Avatar", + "thundurusTherian": "Tótem", "landorusIncarnate": "Avatar", + "landorusTherian": "Tótem", + "kyurem": "Normal", + "kyuremBlack": "Negro", + "kyuremWhite": "Blanco", "keldeoOrdinary": "Habitual", + "keldeoResolute": "Brío", "meloettaAria": "Lírica", "meloettaPirouette": "Danza", + "genesect": "Normal", + "genesectShock": "FulgoROM", + "genesectBurn": "PiroROM", + "genesectChill": "CrioROM", + "genesectDouse": "HidroROM", + "froakie": "Normal", "froakieBattleBond": "Fuerte Afecto", + "froakieAsh": "Ash", "scatterbugMeadow": "Floral", "scatterbugIcySnow": "Polar", "scatterbugPolar": "Taiga", @@ -91,6 +123,7 @@ "flabebeOrange": "Naranja", "flabebeBlue": "Azul", "flabebeWhite": "Blanco", + "furfrou": "Salvaje", "furfrouHeart": "Corazón", "furfrouStar": "Estrella", "furfrouDiamond": "Diamante", @@ -100,9 +133,14 @@ "furfrouLaReine": "Aristócrata", "furfrouKabuki": "Kabuki", "furfrouPharaoh": "Faraónico", - "pumpkabooSmall": "Pequeño", - "pumpkabooLarge": "Grande", - "pumpkabooSuper": "Enorme", + "espurrMale": "Macho", + "espurrFemale": "Hembra", + "honedgeShiled": "Escudo", + "honedgeBlade": "Filo", + "pumpkaboo": "Tamaño Normal", + "pumpkabooSmall": "Tamaño Pequeño", + "pumpkabooLarge": "Tamaño Grande", + "pumpkabooSuper": "Tamaño Extragrande", "xerneasNeutral": "Relajado", "xerneasActive": "Activo", "zygarde50": "Al 50%", @@ -110,11 +148,37 @@ "zygarde50Pc": "Zygarde al 50%", "zygarde10Pc": "Zygarde al 10%", "zygardeComplete": "Zygarde Completo", + "hoopa": "Contenido", + "hoopaUnbound": "Desatado", "oricorioBaile": "Apasionado", "oricorioPompom": "Animado", "oricorioPau": "Plácido", "oricorioSensu": "Refinado", + "rockruff": "Normal", "rockruffOwnTempo": "Ritmo Propio", + "rockruffMidday": "Diurna", + "rockruffMidnight": "Nocturna", + "rockruffDusk": "Crepuscular", + "wishiwashi": "Solo Form", + "wishiwashiSchool": "Banco", + "typeNullNormal": "Tipo Normal", + "typeNullFighting": "Tipo Lucha", + "typeNullFlying": "Tipo Volador", + "typeNullPoison": "Tipo Veneno", + "typeNullGround": "Tipo Tierra", + "typeNullRock": "Tipo Roca", + "typeNullBug": "Tipo Bicho", + "typeNullGhost": "Tipo Fantasma", + "typeNullSteel": "Tipo Acero", + "typeNullFire": "Tipo Fuego", + "typeNullWater": "Tipo Agua", + "typeNullGrass": "Tipo Planta", + "typeNullElectric": "Tipo Eléctrico", + "typeNullPsychic": "Tipo Psíquico", + "typeNullIce": "Tipo Hielo", + "typeNullDragon": "Tipo Dragón", + "typeNullDark": "Tipo Siniestro", + "typeNullFairy": "Tipo Hada", "miniorRedMeteor": "Núcleo Rojo", "miniorOrangeMeteor": "Núcleo Naranja", "miniorYellowMeteor": "Núcleo Amarillo", @@ -131,25 +195,66 @@ "miniorViolet": "Violeta", "mimikyuDisguised": "Encubierta", "mimikyuBusted": "Descubierta", + "necrozma": "Normal", + "necrozmaDuskMane": "Melena Crepuscular", + "necrozmaDawnWings": "Asas Alvorada", + "necrozmaUltra": "Ultra", + "magearna": "Normal", "magearnaOriginal": "Vetusto", + "marshadow": "Normal", "marshadowZenith": "Cénit", + "cramorant": "Normal", + "cramorantGulping": "Tragatodo", + "cramorantGorging": "Engulletodo", + "toxelAmped": "Agudo", + "toxelLowkey": "Grave", "sinisteaPhony": "Falsificada", "sinisteaAntique": "Genuina", + "milceryVanillaCream": "Crema de Vainilla", + "milceryRubyCream": "Crema Rosa", + "milceryMatchaCream": "Crema de Té", + "milceryMintCream": "Crema de Menta", + "milceryLemonCream": "Crema de Limón", + "milcerySaltedCream": "Crema Salada", + "milceryRubySwirl": "Mezcla Rosa", + "milceryCaramelSwirl": "Mezcla Caramelo", + "milceryRainbowSwirl": "Tres Sabores", + "eiscue": "Cara de Hielo", "eiscueNoIce": "Cara Deshielo", "indeedeeMale": "Macho", "indeedeeFemale": "Hembra", "morpekoFullBelly": "Saciada", + "morpekoHangry": "Voraz", "zacianHeroOfManyBattles": "Guerrero avezado", + "zacianCrowned": "Espada Suprema", "zamazentaHeroOfManyBattles": "Guerrero avezado", + "zamazentaCrowned": "Escudo Supremo", + "kubfuSingleStrike": "Estilo Brusco", + "kubfuRapidStrike": "Estilo Fluido", + "zarude": "Normal", "zarudeDada": "Papá", + "calyrex": "Normal", + "calyrexIce": "Jinete Glacial", + "calyrexShadow": "Jinete Espectral", + "basculinMale": "Macho", + "basculinFemale": "Hembra", "enamorusIncarnate": "Avatar", + "enamorusTherian": "Tótem", + "lechonkMale": "Macho", + "lechonkFemale": "Hembra", + "tandemausFour": "Familia de Cuatro", + "tandemausThree": "Familia de Tres", "squawkabillyGreenPlumage": "Plumaje Verde", "squawkabillyBluePlumage": "Plumaje Azul", "squawkabillyYellowPlumage": "Plumaje Amarillo", "squawkabillyWhitePlumage": "Plumaje Blanco", + "finizenZero": "Ingenua", + "finizenHero": "Heroica", "tatsugiriCurly": "Curvada", "tatsugiriDroopy": "Lánguida", "tatsugiriStretchy": "Estirada", + "dunsparceTwo": "Binodular", + "dunsparceThree": "Trinodular", "gimmighoulChest": "Cofre", "gimmighoulRoaming": "Andante", "koraidonApexBuild": "Forma Plena", @@ -164,6 +269,21 @@ "miraidonGlideMode": "Modo Planeo", "poltchageistCounterfeit": "Fraudulenta", "poltchageistArtisan": "Opulenta", + "poltchageistUnremarkable": "Mediocre", + "poltchageistMasterpiece": "Exquisita", + "ogerponTealMask": "Máscara Turquesa", + "ogerponTealMaskTera": "Máscara Turquesa Teracristal", + "ogerponWellspringMask": "Máscara Fuente", + "ogerponWellspringMaskTera": "Máscara Fuente Teracristal", + "ogerponHearthflameMask": "Máscara Horno", + "ogerponHearthflameMaskTera": "Máscara Horno Teracristal", + "ogerponCornerstoneMask": "Máscara Cimiento", + "ogerponCornerstoneMaskTera": "Máscara Cimiento Teracristal", + "terpagos": "Normal", + "terpagosTerastal": "Teracristal", + "terpagosStellar": "Astral", + "galarDarumaka": "Modo Normal", + "galarDarumakaZen": "Modo Daruma", "paldeaTaurosCombat": "Combatiente", "paldeaTaurosBlaze": "Ardiente", "paldeaTaurosAqua": "Acuático" diff --git a/src/locales/es/pokemon-summary.json b/src/locales/es/pokemon-summary.json index e47335c8394..fe33c9418cc 100644 --- a/src/locales/es/pokemon-summary.json +++ b/src/locales/es/pokemon-summary.json @@ -11,7 +11,7 @@ "cancel": "Salir", "memoString": "Naturaleza {{natureFragment}},\n{{metFragment}}", "metFragment": { - "normal": "encontrado al Nv. {{level}},\n{{biome}}.", + "normal": "encontrado al Nv. {{level}},\n{{biome}}, Oleada {{wave}}.", "apparently": "aparentemente encontrado al Nv. {{level}},\n{{biome}}." } -} \ No newline at end of file +} diff --git a/src/locales/es/splash-messages.json b/src/locales/es/splash-messages.json index b1d4820b06e..da31d394c0f 100644 --- a/src/locales/es/splash-messages.json +++ b/src/locales/es/splash-messages.json @@ -1,5 +1,5 @@ { - "battlesWon": "¡Batallas ganadas!", + "battlesWon": "¡{{count, number}} Batallas ganadas!", "joinTheDiscord": "¡Únete al Discord!", "infiniteLevels": "¡Niveles infinitos!", "everythingStacks": "¡Todo se acumula!", diff --git a/src/locales/es/trainer-classes.json b/src/locales/es/trainer-classes.json index 0677193e4f8..c8517ae73d3 100644 --- a/src/locales/es/trainer-classes.json +++ b/src/locales/es/trainer-classes.json @@ -98,5 +98,8 @@ "youngster": "Joven", "aether_grunt": "Empleado de la Fundación Æther", "aether_grunt_female": "Empleada de la Fundación Æther", - "aether_grunts": "Empleados de la Fundación Æther" + "aether_grunts": "Empleados de la Fundación Æther", + "star_grunt": "Star Grunt", + "star_grunt_female": "Star Grunt", + "star_grunts": "Star Grunts" } diff --git a/src/locales/es/trainer-names.json b/src/locales/es/trainer-names.json index ce09a0c9037..da18b6bbcd9 100644 --- a/src/locales/es/trainer-names.json +++ b/src/locales/es/trainer-names.json @@ -138,6 +138,15 @@ "rood": "Ruga", "xerosic": "Xero", "bryony": "Begonia", + "faba": "Fabio", + "plumeria": "Francine", + "oleana": "Olivia", + "giacomo": "Anán", + "mela": "Melo", + "atticus": "Henzo", + "ortega": "Gus", + "eri": "Erin", + "maxie": "Magno", "archie": "Aquiles", "cyrus": "Helio", @@ -145,6 +154,10 @@ "lysandre": "Lysson", "faba": "Fabio", "lusamine": "Samina", + "guzma": "Guzmán", + "rose": "Rose", + "cassiopeia": "Noa", + "blue_red_double": "Azul y Rojo", "red_blue_double": "Rojo y Azul", "tate_liza_double": "Vito y Leti", diff --git a/src/locales/es/trainer-titles.json b/src/locales/es/trainer-titles.json index 5bee9fc8c51..eda3e4b36a4 100644 --- a/src/locales/es/trainer-titles.json +++ b/src/locales/es/trainer-titles.json @@ -17,6 +17,9 @@ "plasma_boss": "Team Plasma Boss", "flare_boss": "Team Flare Boss", "aether_boss": "Presidente Æther", + "skull_boss": "Team Skull Boss", + "macro_boss": "Macro Cosmos President", + "star_boss": "Team Star Leader", "rocket_admin": "Team Rocket Admin", "rocket_admin_female": "Team Rocket Admin", @@ -30,5 +33,8 @@ "plasma_admin": "Team Plasma Admin", "flare_admin": "Team Flare Admin", "flare_admin_female": "Team Flare Admin", - "aether_admin": "Director de la Fundación Æther" + "aether_admin": "Director de la Fundación Æther", + "skull_admin": "Team Skull Admin", + "macro_admin": "Macro Cosmos", + "star_admin": "Team Star Squad Boss" } diff --git a/src/locales/fr/ability.json b/src/locales/fr/ability.json index 7db44c45fa7..e5ac2fd3361 100644 --- a/src/locales/fr/ability.json +++ b/src/locales/fr/ability.json @@ -1077,7 +1077,7 @@ }, "thermalExchange": { "name": "Thermodynamique", - "description": "Lorsque le Pokémon est touché par une capacité de type Feu, il ne subit aucun dégât et son Attaque augmente." + "description": "Lorsque le Pokémon est touché par une capacité de type Feu, son Attaque augmente. Il ne peut pas être brulé." }, "angerShell": { "name": "Courroupace", @@ -1237,6 +1237,6 @@ }, "poisonPuppeteer": { "name": "Emprise Toxique", - "description": "Lorsque Pêchaminus empoisonne un Pokémon grâce à l’une de ses capacités, ce dernier devient également confus." + "description": "Lorsque le Pokémon en empoisonne un autre grâce à l’une de ses capacités, ce dernier devient également confus." } } diff --git a/src/locales/fr/battle.json b/src/locales/fr/battle.json index 7b78c963187..6c555fa77be 100644 --- a/src/locales/fr/battle.json +++ b/src/locales/fr/battle.json @@ -14,6 +14,10 @@ "moneyWon": "Vous remportez\n{{moneyAmount}} ₽ !", "moneyPickedUp": "Vous obtenez {{moneyAmount}} ₽ !", "pokemonCaught": "Vous avez attrapé\n{{pokemonName}} !", + "pokemonObtained": "Vous obtenez\nun {{pokemonName}} !", + "pokemonBrokeFree": "Oh non !\nLe Pokémon s’est libéré !", + "pokemonFled": "Le {{pokemonName}} sauvage\nprend la fuite !", + "playerFled": "Vous fuyez le {{pokemonName}} !", "addedAsAStarter": "{{pokemonName}} est ajouté\ncomme starter !", "partyFull": "Votre équipe est pleine.\nRelâcher un Pokémon pour {{pokemonName}} ?", "pokemon": "de Pokémon", @@ -44,7 +48,10 @@ "moveNotImplemented": "{{moveName}} n’est pas encore implémenté et ne peut pas être sélectionné.", "moveNoPP": "Il n’y a plus de PP pour\ncette capacité !", "moveDisabled": "{{moveName}} est sous entrave !", + "canOnlyUseMove": "{{pokemonName}} ne peut utiliser\nque la capacité {{moveName}} !", + "moveCannotBeSelected": "La capacité {{moveName}}\nne peut pas être choisie !", "disableInterruptedMove": "Il y a une entrave sur la capacité {{moveName}}\nde{{pokemonNameWithAffix}} !", + "throatChopInterruptedMove": "Exécu-Son empêche {{pokemonName}}\nd’utiliser la capacité !", "noPokeballForce": "Une force mystérieuse\nempêche l’utilisation des Poké Balls.", "noPokeballTrainer": "Le Dresseur détourne la Ball\nVoler, c’est mal !", "noPokeballMulti": "Impossible ! On ne peut pas viser\nquand il y a deux Pokémon !", @@ -62,6 +69,7 @@ "skipItemQuestion": "Êtes-vous sûr·e de ne pas vouloir prendre d’objet ?", "itemStackFull": "Quantité maximale de {{fullItemName}} atteinte.\nVous recevez {{itemName}} à la place.", "eggHatching": "Hein ?", + "eggSkipPrompt": "Aller directement au résumé des Œufs éclos ?", "ivScannerUseQuestion": "Utiliser le Scanner d’IV\nsur {{pokemonName}} ?", "wildPokemonWithAffix": "{{pokemonName}} sauvage", "foePokemonWithAffix": "{{pokemonName}} ennemi", @@ -96,5 +104,7 @@ "unlockedSomething": "{{unlockedThing}}\na été débloqué.", "congratulations": "Félicitations !", "beatModeFirstTime": "{{speciesName}} a battu le mode {{gameMode}} pour la première fois !\nVous avez reçu {{newModifier}} !", - "eggSkipPrompt": "Aller directement au résumé des Œufs éclos ?" + "ppReduced": "Les PP de la capacité {{moveName}}\nde{{targetName}} baissent de {{reduction}} !", + "battlerTagsHealBlock": "{{pokemonNameWithAffix}} ne peut pas guérir !", + "battlerTagsHealBlockOnRemove": "Le blocage de soins qui affectait\n{{pokemonNameWithAffix}} s’est dissipé !" } diff --git a/src/locales/fr/battler-tags.json b/src/locales/fr/battler-tags.json index c4a88bb91aa..4c5c7ea0df6 100644 --- a/src/locales/fr/battler-tags.json +++ b/src/locales/fr/battler-tags.json @@ -70,5 +70,8 @@ "stockpilingOnAdd": "{{pokemonNameWithAffix}} utilise\nla capacité Stockage {{stockpiledCount}} fois !", "disabledOnAdd": "La capacité {{moveName}}\nde {{pokemonNameWithAffix}} est mise sous entrave !", "disabledLapse": "La capacité {{moveName}}\nde {{pokemonNameWithAffix}} n’est plus sous entrave !", - "tarShotOnAdd": "{{pokemonNameWithAffix}} est maintenant\nvulnérable au feu !" + "tarShotOnAdd": "{{pokemonNameWithAffix}} est maintenant\nvulnérable au feu !", + "substituteOnAdd": "{{pokemonNameWithAffix}}\ncrée un clone !", + "substituteOnHit": "Le clone subit les dégâts à la place\nde {{pokemonNameWithAffix}} !", + "substituteOnRemove": "Le clone de {{pokemonNameWithAffix}}\ndisparait…" } diff --git a/src/locales/fr/bgm-name.json b/src/locales/fr/bgm-name.json index ecf0075e79d..06eca763914 100644 --- a/src/locales/fr/bgm-name.json +++ b/src/locales/fr/bgm-name.json @@ -83,9 +83,11 @@ "battle_aether_grunt": "SL - Vs. Fondation Æther", "battle_skull_grunt": "SL - Vs. Team Skull", "battle_macro_grunt": "ÉB - Vs. Macro Cosmos", + "battle_star_grunt": "ÉV - Vs. Team Star", "battle_galactic_admin": "DÉPS - Vs. Admin Team Galaxie", "battle_skull_admin": "SL - Vs. Admin Team Skull", "battle_oleana": "ÉB - Vs. Liv", + "battle_star_admin": "ÉV - Vs. Boss de la Team Star", "battle_rocket_boss": "USUL - Vs. Giovanni", "battle_aqua_magma_boss": "ROSA - Vs. Arthur/Max", "battle_galactic_boss": "DÉPS - Vs. Hélio", @@ -94,6 +96,7 @@ "battle_aether_boss": "SL - Vs. Elsa-Mina", "battle_skull_boss": "SL - Vs. Guzma", "battle_macro_boss": "ÉB - Vs. Shehroz", + "battle_star_boss": "ÉV - Vs. Cassiopée", "abyss": "PDM EdC - Cratère Obscur", "badlands": "PDM EdC - Vallée Stérile", @@ -108,17 +111,17 @@ "forest": "PDM EdC - Forêt Crépuscule", "grass": "PDM EdC - Bois aux Pommes", "graveyard": "PDM EdC - Forêt Trompeuse", - "ice_cave": "PDM EdC - Montagne Glacier", + "ice_cave": "Firel - -50°C", "island": "PDM EdC - Côte Escarpée", "jungle": "Lmz - Jungle", "laboratory": "Firel - Laboratory", - "lake": "PDM EdC - Caverne Cristal", + "lake": "Lmz - Lake", "meadow": "PDM EdC - Pic Céleste (forêt)", "metropolis": "Firel - Metropolis", "mountain": "PDM EdC - Mont Corne", - "plains": "PDM EdC - Pic Céleste (prairie)", - "power_plant": "PDM EdC - Plaines Élek", - "ruins": "PDM EdC - Ruine Scellée", + "plains": "Firel - Route 888", + "power_plant": "Firel - The Klink", + "ruins": "Lmz - Ancient Ruins", "sea": "Andr06 - Marine Mystique", "seabed": "Firel - Seabed", "slum": "Andr06 - Sneaky Snom", @@ -128,7 +131,7 @@ "tall_grass": "PDM EdC - Forêt Brumeuse", "temple": "PDM EdC - Grotte Égide", "town": "PDM EdC - Donjon aléatoire - Thème 3", - "volcano": "PDM EdC - Grotte Étuve", + "volcano": "Firel - Twisturn Volcano", "wasteland": "PDM EdC - Terres Illusoires", "encounter_ace_trainer": "NB - Regards croisés (Topdresseur·euse)", "encounter_backpacker": "NB - Regards croisés (Randonneur·euse)", diff --git a/src/locales/fr/challenges.json b/src/locales/fr/challenges.json index a83ec2e0be4..86a21881a50 100644 --- a/src/locales/fr/challenges.json +++ b/src/locales/fr/challenges.json @@ -1,6 +1,7 @@ { "title": "Paramètres du Challenge", "illegalEvolution": "{{pokemon}} est devenu\ninéligible pour ce challenge !", + "noneSelected": "Aucun sélectionné", "singleGeneration": { "name": "Monogénération", "desc": "Vous ne pouvez choisir que des Pokémon de {{gen}} génération.", @@ -33,4 +34,4 @@ "value.0": "Non", "value.1": "Oui" } -} \ No newline at end of file +} diff --git a/src/locales/fr/dialogue.json b/src/locales/fr/dialogue.json index adc58de0563..e8df718ab08 100644 --- a/src/locales/fr/dialogue.json +++ b/src/locales/fr/dialogue.json @@ -344,12 +344,17 @@ "1": "Hop hop hop ! Terminus !", "2": "T’es un Dresseur n’est-ce pas ?\n$J’ai bien peur ce que ne soit pas une excuse suffisante pour nous interrompre dans notre travail.", "2_female": "T’es une Dresseuse n’est-ce pas ?\n$J’ai bien peur ce que ne soit pas une excuse suffisante pour nous interrompre dans notre travail.", - "3": "Je travaille à Macro Cosmos Assurances !\nBesoin d’une assurance-vie ?" + "3": "Je travaille à Macro Cosmos Assurances !\nBesoin d’une assurance-vie ?", + "4": "Trouvé !\nPetit interlude combat !", + "4_female": "Trouvée !\nPetit interlude combat !", + "5": "Une soufflante de la part de madame Liv est la pire chose qui puisse arriver !" }, "victory": { "1": "Je n’ai d’autre choix que respectueusement me retirer.", "2": "Mon argent de poche…\nPlus qu’à manger des pâtes pour la fin du mois…", - "3": "Chez Macro Cosmos, rien n’est comparable à notre dévotion au travail !" + "3": "Chez Macro Cosmos, rien n’est comparable à notre dévotion au travail !", + "4": "J’ai même pensé à chager de Pokémon en combat…", + "5": "Combattre ne sert à rien…\nPlus qu’à me barrer !" } }, "oleana": { @@ -365,6 +370,77 @@ "3": "*soupir* Je suis fatiguée parton…" } }, + "star_grunt": { + "encounter": { + "1": "Ouais, un peu de respect, tu veux ?\n Tu sais à qui t’as affaire ?", + "2": "Notre comité d’accueil va pas te faire de cadeau ! Hasta la vistaaar ! ★", + "3": "Tu peux aller voir ailleurs si j’y suis ?\n$Si tu refuses, je m’autorise à faire valoir mes droits de légitime défense, je te préviens !", + "4": "Désolé, mais si tu refuses de partir, on sera obligés de t’y forcer, et ça sera pas joli joli.", + "4_female": "Désolé, mais si tu refuses de partir, on sera obligés de t’y forcer, et ça sera pas joli joli.", + "5": "Quoi ? Toi aussi, t’es venu me casser les pieds ? C’est vraiment pas ma journée…", + "5_female": "Quoi ? Toi aussi, t’es venue me casser les pieds ? C’est vraiment pas ma journée…" + }, + "victory": { + "1": "Comment c'est ça peut être moi qui voit les étoiles ?!", + "2": "T’en as dans le ventre.\n$T’as peut-être ce qu’il faut pour te faire une place dans la constellation de la Team Star.", + "3": "Ma défense était pourtant bonne…\nMais c’était pas assez visiblement !", + "4": "Ha… hasta la vistar… ☆", + "5": "J’aurais jamais cru qu’être Sbire de la Team Star serait un tel fadeau…" + } + }, + "giacomo": { + "encounter": { + "1": "Tu dois vraiment pas tenir à ta life pour défier la Team Star comme ça !", + "2": "Tends un peu l’oreille… Tu l’entends ?\nC’est ton requiem !" + }, + "victory": { + "1": "Pfff, quelle ironie, franchement…", + "2": "J’pensais pas jouer le refrain de ma défaite…" + } + }, + "mela": { + "encounter": { + "1": "Alors, c’est toi le guedin qu’ose nous défier ?\nSois prêt pour ta raclée.", + "1_female": "Alors, c’est toi la guedin qu’ose nous défier ?\nSois prête pour ta raclée.", + "2": "Y a pas à dire, t’es bien une tête brûlée !\nMais fais gaffe, j’en ai aussi sous l’capot !" + }, + "victory": { + "1": "Quoi, c’est comme ça qu’ça finit ?\nPfff… J’ai la haine…", + "2": "J’voulais tout bruler… Mais j’me suis juste consumée.\nY reste plus rien." + } + }, + "atticus": { + "encounter": { + "1": "T’as vraiment du cran de venir provoquer la Team Star.\nDestinée fatale - Au poison succombera - L’ennemi juré.", + "1_female": "T’as vraiment du cran de venir provoquer la Team Star.\nDestinée fatale - Au poison succombera - L’ennemie jurée.", + "2": "Vive expectative - Respect et adversité - Combat au sommet." + }, + "victory": { + "1": "Pardon, mes amis…", + "2": "Ta victoire n’évoque en moi ni rancune ni douleur.\nMon âme est en paix et j’accepte ma défaite." + } + }, + "ortega": { + "encounter": { + "1": "J’vais bien m’occuper de toi, alors viens pas pleurer si tu perds.", + "2": "T’as beau avoir l’air sûr de toi, aujourd’hui, c’est moi qui vais gagner. Je te le garantis !", + "2_female": "T’as beau avoir l’air sûr de toi, aujourd’hui, c’est moi qui vais gagner. Je te le garantis !" + }, + "victory": { + "1": "Hein ? J’ai perdu ? Mais pourquoi ?!\nPourquoi, pourquoi, pourquoi, POURQUOI ?!", + "2": "Mais pourquoi ça fonctionne pas ?!" + } + }, + "eri": { + "encounter": { + "1": "J’ignore tes intentions, mais mon rôle est d’anéantir quiconque s’attaque à la Team Star !", + "2": "Si on m’attaque, je riposte ! Nous verrons bien qui de nous deux restera debout à la fin !" + }, + "victory": { + "1": "Les amis…\nJe suis désolée…", + "2": "J’ai donné mon maximum… et pourtant…\nÇa n’a pas suffi… J’ai échoué…" + } + }, "rocket_boss_giovanni_1": { "encounter": { "1": "Bien. Je dois admettre que je suis impressionné de te voir ici !" @@ -499,6 +575,28 @@ "1": "Les ignorants sans aucune vision n’auront donc de cesse de souiller ce monde." } }, + "star_boss_penny_1": { + "encounter": { + "1": "Je suis la Big Boss de la Team Star, Cassiopée…\n$Incline-toi devant la toute-puissance de la Big Boss !!!" + }, + "victory": { + "1": "… … …" + }, + "defeat": { + "1": "Hé…" + } + }, + "star_boss_penny_2": { + "encounter": { + "1": "Le code d’honneur de la Team Star exige que nous donnions le maximum en combat !\n$Par le pouvoir de l’Évolition, on va te réduire en poussière d’étoiles !" + }, + "victory": { + "1": "… Tout est fini" + }, + "defeat": { + "1": "Tu es redoutable.\nPas étonnant que les boss de la Team aient perdu contre toi." + } + }, "brock": { "encounter": { "1": "Mon expertise des types Roche va te mettre au sol ! En garde !", diff --git a/src/locales/fr/menu.json b/src/locales/fr/menu.json index 277b0f5fd04..35cd06606a7 100644 --- a/src/locales/fr/menu.json +++ b/src/locales/fr/menu.json @@ -46,7 +46,7 @@ "yes": "Oui", "no": "Non", "disclaimer": "AVERTISSEMENT", - "disclaimerDescription": "Ce jeu n’est pas un produit fini et peut contenir des problèmes de jouabilité, dont de possibles pertes de sauvegardes,\ndes modifications sans avertissement et pourrait ou non encore être mis à jour ou terminé.", + "disclaimerDescription": "Ce jeu n’est pas un produit fini.\nIl peut contenir des problèmes de jouabilité, dont de possibles pertes de sauvegardes,\ndes modifications sans avertissement et pourrait à tout moment cesser d’être mis à jour.", "choosePokemon": "Sélectionnez un Pokémon.", "renamePokemon": "Renommer le Pokémon", "rename": "Renommer", diff --git a/src/locales/fr/move-trigger.json b/src/locales/fr/move-trigger.json index 3704bc90718..6f9d9d4dd63 100644 --- a/src/locales/fr/move-trigger.json +++ b/src/locales/fr/move-trigger.json @@ -7,6 +7,7 @@ "switchedStat": "{{pokemonName}} et sa cible échangent leur {{stat}} !", "sharedGuard": "{{pokemonName}} additionne sa garde à celle de sa cible et redistribue le tout équitablement !", "sharedPower": "{{pokemonName}} additionne sa force à celle de sa cible et redistribue le tout équitablement !", + "shiftedStats": "{{pokemonName}} échange {{statToSwitch}} et {{statToSwitchWith}} !", "goingAllOutForAttack": "{{pokemonName}} a pris\ncette capacité au sérieux !", "regainedHealth": "{{pokemonName}}\nrécupère des PV !", "keptGoingAndCrashed": "{{pokemonName}}\ns’écrase au sol !", @@ -67,5 +68,7 @@ "swapArenaTags": "Les effets affectant chaque côté du terrain\nont été échangés par {{pokemonName}} !", "exposedMove": "{{targetPokemonName}} est identifié\npar {{pokemonName}} !", "safeguard": "{{targetName}} est protégé\npar la capacité Rune Protect !", + "substituteOnOverlap": "{{pokemonName}} a déjà\nun clone !", + "substituteNotEnoughHp": "Mais il est trop faible\npour créer un clone !", "afterYou": "{{pokemonName}} accepte\navec joie !" } diff --git a/src/locales/fr/move.json b/src/locales/fr/move.json index a48e17b3fd9..6fb3cb73385 100644 --- a/src/locales/fr/move.json +++ b/src/locales/fr/move.json @@ -364,8 +364,8 @@ "effect": "Le lanceur creuse au premier tour et frappe au second." }, "toxic": { - "name": "Fil Toxique", - "effect": "Tisse un fil imprégné de venin. Empoisonne la cible et baisse sa Vitesse." + "name": "Toxik", + "effect": "Le lanceur empoisonne gravement la cible. Les dégâts dus au poison augmentent à chaque tour." }, "confusion": { "name": "Choc Mental", @@ -3121,15 +3121,15 @@ }, "behemothBlade": { "name": "Gladius Maximus", - "effect": "Le lanceur se transforme en une immense épée et pourfend sa cible. Cette capacité inflige le double de dégâts aux Pokémon Dynamax." + "effect": "Le lanceur se transforme en une immense épée et pourfend sa cible." }, "behemothBash": { "name": "Aegis Maxima", - "effect": "Le lanceur se transforme en un immense bouclier et charge sa cible. Cette capacité inflige le double de dégâts aux Pokémon Dynamax." + "effect": "Le lanceur se transforme en un immense bouclier et charge sa cible." }, "auraWheel": { "name": "Roue Libre", - "effect": "Inflige et change en type Ténèbres" + "effect": "Le Pokémon libère l’énergie stockée dans ses joues pour attaquer et augmenter sa Vitesse. Le type de cette capacité change en fonction de la forme du lanceur." }, "breakingSwipe": { "name": "Abattage", diff --git a/src/locales/fr/party-ui-handler.json b/src/locales/fr/party-ui-handler.json index 6adba2c8309..4eef55da790 100644 --- a/src/locales/fr/party-ui-handler.json +++ b/src/locales/fr/party-ui-handler.json @@ -13,6 +13,7 @@ "ALL": "Tout", "PASS_BATON": "Relais", "UNPAUSE_EVOLUTION": "Réactiver Évolution", + "PAUSE_EVOLUTION": "Empêcher Évolution", "REVIVE": "Ranimer", "RENAME": "Renommer", "choosePokemon": "Sélectionnez un Pokémon.", @@ -23,6 +24,7 @@ "tooManyItems": "{{pokemonName}} porte trop\nd’exemplaires de cet objet !", "anyEffect": "Cela n’aura aucun effet.", "unpausedEvolutions": "{{pokemonName}} peut de nouveau évoluer.", + "pausedEvolutions": "{{pokemonName}} ne peut plus évoluer.", "unspliceConfirmation": "Voulez-vous vraiment séparer {{fusionName}}\nde {{pokemonName}} ? {{fusionName}} sera perdu.", "wasReverted": "{{fusionName}} est redevenu {{pokemonName}}.", "releaseConfirmation": "Voulez-vous relâcher {{pokemonName}} ?", @@ -44,4 +46,4 @@ "untilWeMeetAgain": "À la prochaine, {{pokemonName}} !", "sayonara": "Sayonara, {{pokemonName}} !", "smellYaLater": "À la revoyure, {{pokemonName}} !" -} \ No newline at end of file +} diff --git a/src/locales/fr/pokemon-form.json b/src/locales/fr/pokemon-form.json index 44f2bcecf2c..b1201ae0dc9 100644 --- a/src/locales/fr/pokemon-form.json +++ b/src/locales/fr/pokemon-form.json @@ -1,4 +1,5 @@ { + "pikachu": "Normal", "pikachuCosplay": "Cosplayeur", "pikachuCoolCosplay": "Cosplay Rockeur", "pikachuBeautyCosplay": "Cosplay Lady", @@ -6,7 +7,9 @@ "pikachuSmartCosplay": "Cosplay Docteur", "pikachuToughCosplay": "Cosplay Catcheur", "pikachuPartner": "Partenaire", + "eevee": "Normal", "eeveePartner": "Partenaire", + "pichu": "Normal", "pichuSpiky": "Troizépi", "unownA": "A", "unownB": "B", @@ -36,36 +39,65 @@ "unownZ": "Z", "unownExclamation": "!", "unownQuestion": "?", + "castform": "Normal Form", "castformSunny": "Solaire", "castformRainy": "Eau de Pluie", "castformSnowy": "Blizzard", "deoxysNormal": "Normal", + "deoxysAttack": "Attaque", + "deoxysDefense": "Défense", + "deoxysSpeed": "Vitesse", "burmyPlant": "Plante", "burmySandy": "Sable", "burmyTrash": "Déchet", + "cherubiOvercast": "Couvert", + "cherubiSunshine": "Ensoleillé", "shellosEast": "Orient", "shellosWest": "Occident", + "rotom": "Normal", "rotomHeat": "Chaleur", "rotomWash": "Lavage", "rotomFrost": "Froid", "rotomFan": "Hélice", "rotomMow": "Tonte", + "dialga": "Normal", + "dialgaOrigin": "Originel", + "palkia": "Normal", + "palkiaOrigin": "Originel", "giratinaAltered": "Alternatif", + "giratinaOrigin": "Originel", "shayminLand": "Terrestre", + "shayminSky": "Céleste", "basculinRedStriped": "Motif Rouge", "basculinBlueStriped": "Motif Bleu", "basculinWhiteStriped": "Motif Blanc", + "darumaka": "Mode Normal", + "darumakaZen": "Mode Transe", "deerlingSpring": "Printemps", "deerlingSummer": "Été", "deerlingAutumn": "Automne", "deerlingWinter": "Hiver", "tornadusIncarnate": "Avatar", + "tornadusTherian": "Totémique", "thundurusIncarnate": "Avatar", + "thundurusTherian": "Totémique", "landorusIncarnate": "Avatar", + "landorusTherian": "Totémique", + "kyurem": "Normal", + "kyuremBlack": "Noir", + "kyuremWhite": "Blanc", "keldeoOrdinary": "Normal", + "keldeoResolute": "Décidé", "meloettaAria": "Chant", "meloettaPirouette": "Danse", + "genesect": "Normal", + "genesectShock": "Module Choc", + "genesectBurn": "Module Pyro", + "genesectChill": "Module Cryo", + "genesectDouse": "Module Aqua", + "froakie": "Normal", "froakieBattleBond": "Synergie", + "froakieAsh": "Sachanobi", "scatterbugMeadow": "Floraison", "scatterbugIcySnow": "Blizzard", "scatterbugPolar": "Banquise", @@ -91,6 +123,7 @@ "flabebeOrange": "Orange", "flabebeBlue": "Bleu", "flabebeWhite": "Blanc", + "furfrou": "Sauvage", "furfrouHeart": "Cœur", "furfrouStar": "Étoile", "furfrouDiamond": "Diamant", @@ -100,21 +133,52 @@ "furfrouLaReine": "Reine", "furfrouKabuki": "Kabuki", "furfrouPharaoh": "Pharaon", - "pumpkabooSmall": "Mini", - "pumpkabooLarge": "Maxi", - "pumpkabooSuper": "Ultra", + "espurrMale": "Mâle", + "espurrFemale": "Femelle", + "honedgeShiled": "Parade", + "honedgeBlade": "Assaut", + "pumpkaboo": "Taille Normale", + "pumpkabooSmall": "Taille Mini", + "pumpkabooLarge": "Taille Maxi", + "pumpkabooSuper": "Taille Ultra", "xerneasNeutral": "Paisible", - "xerneasActive": "Déchaîné", + "xerneasActive": "Déchainé", "zygarde50": "Forme 50%", "zygarde10": "Forme 10%", "zygarde50Pc": "Rassemblement Forme 50%", "zygarde10Pc": "Rassemblement Forme 10%", + "hoopa": "Enchainé", + "hoopaUnbound": "Déchainé", "zygardeComplete": "Parfait", "oricorioBaile": "Flamenco", "oricorioPompom": "Pom-Pom", "oricorioPau": "Hula", "oricorioSensu": "Buyō", + "rockruff": "Normal", "rockruffOwnTempo": "Tempo Perso", + "rockruffMidday": "Diurne", + "rockruffMidnight": "Nocturne", + "rockruffDusk": "Crépusculaire", + "wishiwashi": "Solitaire", + "wishiwashiSchool": "Banc", + "typeNullNormal": "Type: Normal", + "typeNullFighting": "Type: Combat", + "typeNullFlying": "Type: Vol", + "typeNullPoison": "Type: Poison", + "typeNullGround": "Type: Sol", + "typeNullRock": "Type: Roche", + "typeNullBug": "Type: Insecte", + "typeNullGhost": "Type: Spectre", + "typeNullSteel": "Type: Acier", + "typeNullFire": "Type: Feu", + "typeNullWater": "Type: Eau", + "typeNullGrass": "Type: Plante", + "typeNullElectric": "Type: Électrik", + "typeNullPsychic": "Type: Psy", + "typeNullIce": "Type: Glace", + "typeNullDragon": "Type: Dragon", + "typeNullDark": "Type: Ténèbres", + "typeNullFairy": "Type: Fée", "miniorRedMeteor": "Météore Rouge", "miniorOrangeMeteor": "Météore Orange", "miniorYellowMeteor": "Météore Jaune", @@ -131,25 +195,66 @@ "miniorViolet": "Violet", "mimikyuDisguised": "Déguisé", "mimikyuBusted": "Démasqué", + "necrozma": "Normal", + "necrozmaDuskMane": "Crinière du Couchant", + "necrozmaDawnWings": "Ailes de l’Aurore", + "necrozmaUltra": "Ultra", + "magearna": "Normal", "magearnaOriginal": "Couleur du Passé", + "marshadow": "Normal", "marshadowZenith": "Zénith", + "cramorant": "Normal", + "cramorantGulping": "Gobe-Tout", + "cramorantGorging": "Gobe-Chu", + "toxelAmped": "Aigu", + "toxelLowkey": "Grave", "sinisteaPhony": "Contrefaçon", "sinisteaAntique": "Authentique", + "milceryVanillaCream": "Lait Vanille", + "milceryRubyCream": "Lait Ruby", + "milceryMatchaCream": "Lait Matcha", + "milceryMintCream": "Lait Menthe", + "milceryLemonCream": "Lait Citron", + "milcerySaltedCream": "Lait Salé", + "milceryRubySwirl": "Mélange Ruby", + "milceryCaramelSwirl": "Mélange Caramel", + "milceryRainbowSwirl": "Mélange Tricolore", + "eiscue": "Tête de Gel", "eiscueNoIce": "Tête Dégel", "indeedeeMale": "Mâle", "indeedeeFemale": "Femelle", "morpekoFullBelly": "Rassasié", + "morpekoHangry": "Affamé", "zacianHeroOfManyBattles": "Héros Aguerri", + "zacianCrowned": "Épée Suprême", "zamazentaHeroOfManyBattles": "Héros Aguerri", + "zamazentaCrowned": "Bouclier Suprême", + "kubfuSingleStrike": "Poing Final", + "kubfuRapidStrike": "Mille Poings", + "zarude": "Normal", "zarudeDada": "Papa", + "calyrex": "Normal", + "calyrexIce": "Cavalier du Froid", + "calyrexShadow": "Cavalier d’Effroi", + "basculinMale": "Mâle", + "basculinFemale": "Femelle", "enamorusIncarnate": "Avatar", + "enamorusTherian": "Totémique", + "lechonkMale": "Mâle", + "lechonkFemale": "Femelle", + "tandemausFour": "Famille de Quatre", + "tandemausThree": "Famille de Trois", "squawkabillyGreenPlumage": "Plumage Vert", "squawkabillyBluePlumage": "Plumage Bleu", "squawkabillyYellowPlumage": "Plumage Jaune", "squawkabillyWhitePlumage": "Plumage Blanc", + "finizenZero": "Ordinaire", + "finizenHero": "Super", "tatsugiriCurly": "Courbé", "tatsugiriDroopy": "Affalé", "tatsugiriStretchy": "Raide", + "dunsparceTwo": "Double", + "dunsparceThree": "Triple", "gimmighoulChest": "Coffre", "gimmighoulRoaming": "Marche", "koraidonApexBuild": "Final", @@ -164,7 +269,22 @@ "miraidonGlideMode": "Aérien", "poltchageistCounterfeit": "Imitation", "poltchageistArtisan": "Onéreux", + "poltchageistUnremarkable": "Médiocre", + "poltchageistMasterpiece": "Exceptionnelle", + "ogerponTealMaskTera": "Masque Turquoise", + "ogerponTealMask": "Masque Turquoise Téracristal", + "ogerponWellspringMask": "Masque du Puits", + "ogerponWellspringMaskTera": "Masque du Puits Téracristal", + "ogerponHearthflameMask": "Masque du Fourneau", + "ogerponHearthflameMaskTera": "Masque du Fourneau Téracristal", + "ogerponCornerstoneMask": "Masque de la Pierre", + "ogerponCornerstoneMaskTera": "Masque de la Pierre Téracristal", + "terpagos": "Normal", + "terpagosTerastal": "Téracristal", + "terpagosStellar": "Stellaire", + "galarDarumaka": "Mode Normal", + "galarDarumakaZen": "Mode Transe", "paldeaTaurosCombat": "Combatif", "paldeaTaurosBlaze": "Flamboyant", "paldeaTaurosAqua": "Aquatique" -} \ No newline at end of file +} diff --git a/src/locales/fr/pokemon-summary.json b/src/locales/fr/pokemon-summary.json index 01e712c8468..a038b3a51f9 100644 --- a/src/locales/fr/pokemon-summary.json +++ b/src/locales/fr/pokemon-summary.json @@ -11,7 +11,7 @@ "cancel": "Annuler", "memoString": "{{natureFragment}} de nature,\n{{metFragment}}", "metFragment": { - "normal": "rencontré au N.{{level}},\n{{biome}}.", + "normal": "rencontré au N.{{level}},\n{{biome}}, Vague {{wave}}.", "apparently": "apparemment rencontré au N.{{level}},\n{{biome}}." }, "natureFragment": { diff --git a/src/locales/fr/settings.json b/src/locales/fr/settings.json index c752b336b6e..e310f5d5733 100644 --- a/src/locales/fr/settings.json +++ b/src/locales/fr/settings.json @@ -11,6 +11,10 @@ "expGainsSpeed": "Vit. barre d’Exp", "expPartyDisplay": "Afficher Exp équipe", "skipSeenDialogues": "Passer dialogues connus", + "eggSkip": "Animation d’éclosion", + "never": "Jamais", + "always": "Toujours", + "ask": "Demander", "battleStyle": "Style de combat", "enableRetries": "Activer les réessais", "hideIvs": "Masquer Scanner d’IV", diff --git a/src/locales/fr/splash-messages.json b/src/locales/fr/splash-messages.json index 9dd3e86fb32..2ac85680e58 100644 --- a/src/locales/fr/splash-messages.json +++ b/src/locales/fr/splash-messages.json @@ -1,5 +1,5 @@ { - "battlesWon": "combats gagnés !", + "battlesWon": "{{count, number}} combats gagnés !", "joinTheDiscord": "Rejoins le Discord !", "infiniteLevels": "Niveaux infinis !", "everythingStacks": "Tout se cumule !", diff --git a/src/locales/fr/trainer-classes.json b/src/locales/fr/trainer-classes.json index b7027cf544f..a68f766dc8b 100644 --- a/src/locales/fr/trainer-classes.json +++ b/src/locales/fr/trainer-classes.json @@ -126,5 +126,8 @@ "skull_grunts": "Sbires de la Team Skull", "macro_grunt": "Employé de Macro Cosmos", "macro_grunt_female": "Employée de Macro Cosmos", - "macro_grunts": "Employés de Macro Cosmos" + "macro_grunts": "Employés de Macro Cosmos", + "star_grunt": "Sbire de la Team Star", + "star_grunt_female": "Sbire de la Team Star", + "star_grunts": "Sbires de la Team Star" } diff --git a/src/locales/fr/trainer-names.json b/src/locales/fr/trainer-names.json index 5864976cc34..85765dbe22e 100644 --- a/src/locales/fr/trainer-names.json +++ b/src/locales/fr/trainer-names.json @@ -94,7 +94,7 @@ "caitlin": "Percila", "malva": "Malva", "siebold": "Narcisse", - "wikstrom": "Tileo", + "wikstrom": "Thyméo", "drasna": "Dracéna", "hala": "Pectorius", "molayne": "Molène", @@ -141,6 +141,11 @@ "faba": "Saubohne", "plumeria": "Apocyne", "oleana": "Liv", + "giacomo": "Brome", + "mela": "Meloco", + "atticus": "Erio", + "ortega": "Ortiga", + "eri": "Nèflie", "maxie": "Max", "archie": "Arthur", @@ -150,6 +155,7 @@ "lusamine": "Elsa-Mina", "guzma": "Guzma", "rose": "Shehroz", + "cassiopeia": "Pania", "blue_red_double": "Blue & Red", "red_blue_double": "Red & Blue", diff --git a/src/locales/fr/trainer-titles.json b/src/locales/fr/trainer-titles.json index 6d966bbd9ec..ceefa452881 100644 --- a/src/locales/fr/trainer-titles.json +++ b/src/locales/fr/trainer-titles.json @@ -19,6 +19,7 @@ "aether_boss": "Présidente d’Æther", "skull_boss": "Boss de la Team Skull", "macro_boss": "Président de Macro Cosmos", + "star_boss": "Leader de la Team Star", "rocket_admin": "Admin Team Rocket", "rocket_admin_female": "Admin Team Rocket", @@ -34,5 +35,6 @@ "flare_admin_female": "Manageuse de la Team Flare", "aether_admin": "Directeur d’Æther", "skull_admin": "Admin Team Skull", - "macro_admin": "Macro Cosmos" + "macro_admin": "Macro Cosmos", + "star_admin": "Boss d’équipe de la Team Star" } diff --git a/src/locales/fr/tutorial.json b/src/locales/fr/tutorial.json index f15a7c7c6d4..3236bdafea2 100644 --- a/src/locales/fr/tutorial.json +++ b/src/locales/fr/tutorial.json @@ -2,9 +2,9 @@ "intro": "Bienvenue dans PokéRogue, un fangame axé sur les combats Pokémon avec des éléments roguelite !\n$Ce jeu n’est pas monétisé et nous ne prétendons à la propriété d’aucun élément sous copyright utilisé.\n$Bien qu’en développement permanent, PokéRogue reste entièrement jouable.\n$Tout signalement de bugs et d’erreurs quelconques passe par le serveur Discord.\n$Si le jeu est lent, vérifiez que l’Accélération Matérielle est activée dans les paramètres du navigateur.", "accessMenu": "Accédez au menu avec M ou Échap lors de l’attente d’une\naction.\n$Il contient les paramètres et diverses fonctionnalités.", "menu": "Vous pouvez accéder aux paramètres depuis ce menu.\n$Vous pouvez entre autres y changer la vitesse du jeu ou le style de fenêtre…\n$Mais également des tonnes d’autres fonctionnalités, jetez-y un œil !", - "starterSelect": "Choisissez vos starters depuis cet écran avec Z ou Espace.\nIls formeront votre équipe de départ.\n$Chacun possède une valeur. Votre équipe peut avoir jusqu’à 6 membres, sans dépasser un cout de 10.\n$Vous pouvez aussi choisir le sexe, le talent et la forme en\nfonction des variants déjà capturés ou éclos.\n$Les IV d’un starter sont les meilleurs de tous ceux de son espèce déjà possédés. Obtenez-en plusieurs !", + "starterSelect": "Choisissez vos starters depuis cet écran avec Z ou Espace.\nIls formeront votre équipe de départ.\n$Chacun possède une valeur. Votre équipe peut avoir\njusqu’à 6 membres, sans dépasser un cout de 10.\n$Vous pouvez aussi choisir le sexe, le talent et la forme en\nfonction des variants déjà capturés ou éclos.\n$Les IV d’un starter sont les meilleurs de tous ceux de\nson espèce déjà possédés. Obtenez-en plusieurs !", "pokerus": "Chaque jour, 3 starters tirés aléatoirement ont un contour violet.\n$Si un starter que vous possédez l’a, essayez de l’ajouter à votre équipe. Vérifiez bien son résumé !", - "statChange": "Les changements de stats persistent à travers les combats tant que le Pokémon n’est pas rappelé.\n$Vos Pokémon sont rappelés avant un combat de Dresseur et avant d’entrer dans un nouveau biome.\n$Vous pouvez voir en combat les changements de stats d’un Pokémon en maintenant C ou Maj.\n$Vous pouvez également voir les capacités de l’adversaire en maintenant V.\n$Seules les capacités que le Pokémon a utilisées dans ce combat sont consultables.", - "selectItem": "Après chaque combat, vous avez le choix entre 3 objets\ntirés au sort. Vous ne pouvez en prendre qu’un.\n$Cela peut être des objets consommables, des objets à\nfaire tenir, ou des objets passifs aux effets permanents.\n$La plupart des effets des objets non-consommables se cumuleront de diverses manières.\n$Certains objets n’apparaitront que s’ils ont une utilité immédiate, comme les objets d’évolution.\n$Vous pouvez aussi transférer des objets tenus entre Pokémon en utilisant l’option de transfert.\n$L’option de transfert apparait en bas à droite dès qu’un Pokémon de l’équipe porte un objet.\n$Vous pouvez acheter des consommables avec de l’argent.\nPlus vous progressez, plus le choix sera large.\n$Choisir un des objets gratuits déclenchera le prochain combat, donc faites bien tous vos achats avant.", - "eggGacha": "Depuis cet écran, vous pouvez utiliser vos coupons\npour recevoir Œufs de Pokémon au hasard.\n$Les Œufs éclosent après avoir remporté un certain nombre de combats. Plus ils sont rares, plus ils mettent de temps.\n$Les Pokémon éclos ne rejoindront pas votre équipe, mais seront ajoutés à vos starters.\n$Les Pokémon issus d’Œufs ont généralement de meilleurs IV que les Pokémon sauvages.\n$Certains Pokémon ne peuvent être obtenus que dans des Œufs.\n$Il y a 3 différentes machines à actionner avec différents\nbonus, prenez celle qui vous convient le mieux !" + "statChange": "Les changements de stats persistent à travers\nles combats tant que le Pokémon n’est pas rappelé.\n$Vos Pokémon sont rappelés avant un combat de\nDresseur et avant d’entrer dans un nouveau biome.\n$Vous pouvez voir en combat les changements de stats\nd’un Pokémon en maintenant C ou Maj.\n$Vous pouvez également voir les capacités de l’adversaire\nen maintenant V.\n$Seules les capacités que le Pokémon a utilisées dans\nce combat sont consultables.", + "selectItem": "Après chaque combat, vous avez le choix entre 3 objets\ntirés au sort. Vous ne pouvez en prendre qu’un.\n$Cela peut être des objets consommables, des objets à\nfaire tenir, ou des objets passifs aux effets permanents.\n$La plupart des effets des objets non-consommables se cumuleront de diverses manières.\n$Certains objets n’apparaitront que s’ils ont une utilité immédiate, comme les objets d’évolution.\n$Vous pouvez aussi transférer des objets tenus entre\nPokémon en utilisant l’option de transfert.\n$L’option de transfert apparait en bas à droite dès\nqu’un Pokémon de l’équipe porte un objet.\n$Vous pouvez acheter des consommables avec de\nl’argent. Plus vous progressez, plus le choix sera large.\n$Choisir un des objets gratuits déclenchera le prochain\ncombat, donc faites bien tous vos achats avant.", + "eggGacha": "Depuis cet écran, vous pouvez utiliser vos coupons\npour recevoir Œufs de Pokémon au hasard.\n$Les Œufs éclosent après avoir remporté un certain nombre de combats.\n$Plus ils sont rares, plus ils mettent de temps.\n$Les Pokémon éclos ne rejoindront pas votre équipe, mais seront ajoutés à vos starters.\n$Les Pokémon issus d’Œufs ont généralement de meilleurs IV que les Pokémon sauvages.\n$Certains Pokémon ne peuvent être obtenus que dans des Œufs.\n$Il y a 3 différentes machines à actionner avec différents\nbonus, prenez celle qui vous convient le mieux !" } diff --git a/src/locales/it/ability.json b/src/locales/it/ability.json index 18eb133d824..deec995cde7 100644 --- a/src/locales/it/ability.json +++ b/src/locales/it/ability.json @@ -1237,6 +1237,6 @@ }, "poisonPuppeteer": { "name": "\tMalia Tossica", - "description": "I Pokémon avvelenati dalle mosse di Pecharunt entreranno anche in stato di confusione." + "description": "I Pokémon avvelenati dalle mosse di questo Pokémon entreranno anche in stato di confusione." } } \ No newline at end of file diff --git a/src/locales/it/battler-tags.json b/src/locales/it/battler-tags.json index bd24f380f9e..6ab69f4efa2 100644 --- a/src/locales/it/battler-tags.json +++ b/src/locales/it/battler-tags.json @@ -70,5 +70,8 @@ "stockpilingOnAdd": "{{pokemonNameWithAffix}} ha usato Accumulo per la\n{{stockpiledCount}}ª volta!", "disabledOnAdd": "La mossa {{moveName}} di\n{{pokemonNameWithAffix}} è stata bloccata!", "disabledLapse": "La mossa {{moveName}} di\n{{pokemonNameWithAffix}} non è più bloccata!", - "tarShotOnAdd": "{{pokemonNameWithAffix}} è diventato vulnerabile\nal tipo Fuoco!" + "tarShotOnAdd": "{{pokemonNameWithAffix}} è diventato vulnerabile\nal tipo Fuoco!", + "substituteOnAdd": "Appare un sostituto di {{pokemonNameWithAffix}}!", + "substituteOnHit": "Il sostituto viene colpito al posto di {{pokemonNameWithAffix}}!", + "substituteOnRemove": "Il sostituto di {{pokemonNameWithAffix}} svanisce!" } diff --git a/src/locales/it/move.json b/src/locales/it/move.json index f5bb1954278..ba72e86f812 100644 --- a/src/locales/it/move.json +++ b/src/locales/it/move.json @@ -3129,7 +3129,7 @@ }, "auraWheel": { "name": "Ruota d'Aura", - "effect": "Il Pokémon emette l'energia accumulata nelle guance per attaccare e aumentare la Velocità. Il tipo della mossa cambia in base alla forma assunta da Morpeko." + "effect": "Il Pokémon emette l'energia accumulata nelle guance per attaccare e aumentare la Velocità. Se usata da Morpeko, il tipo della mossa cambia in base alla forma assunta." }, "breakingSwipe": { "name": "Vastoimpatto", diff --git a/src/locales/it/pokemon-form.json b/src/locales/it/pokemon-form.json index 505173e4c8a..e1ecb708b89 100644 --- a/src/locales/it/pokemon-form.json +++ b/src/locales/it/pokemon-form.json @@ -1,4 +1,5 @@ { + "pikachu": "Normale", "pikachuCosplay": "Cosplay", "pikachuCoolCosplay": "Cosplay classe", "pikachuBeautyCosplay": "Cosplay bellezza", @@ -6,7 +7,9 @@ "pikachuSmartCosplay": "Cosplay acume", "pikachuToughCosplay": "Cosplay grinta", "pikachuPartner": "Compagno", + "eevee": "Normale", "eeveePartner": "Compagno", + "pichu": "Normale", "pichuSpiky": "Spunzorek", "unownA": "A", "unownB": "B", @@ -36,36 +39,65 @@ "unownZ": "Z", "unownExclamation": "!", "unownQuestion": "?", + "castform": "Normale", "castformSunny": "Sole", "castformRainy": "Pioggia", "castformSnowy": "Nuvola di neve", "deoxysNormal": "Normale", + "deoxysAttack": "Attacco", + "deoxysDefense": "Difesa", + "deoxysSpeed": "Velocità", "burmyPlant": "Pianta", "burmySandy": "Sabbia", "burmyTrash": "Scarti", + "cherubiOvercast": "Nuvola", + "cherubiSunshine": "Splendore", "shellosEast": "Est", "shellosWest": "Ovest", + "rotom": "Normale", "rotomHeat": "Calore", "rotomWash": "Lavaggio", "rotomFrost": "Gelo", "rotomFan": "Vortice", "rotomMow": "Taglio", + "dialga": "Normale", + "dialgaOrigin": "Originale", + "palkia": "Normale", + "palkiaOrigin": "Originale", "giratinaAltered": "Alterata", + "giratinaOrigin": "Originale", "shayminLand": "Terra", + "shayminSky": "Cielo", "basculinRedStriped": "Linearossa", "basculinBlueStriped": "Lineablu", "basculinWhiteStriped": "Lineabianca", + "darumaka": "Stato Normale", + "darumakaZen": "Stato Zen", "deerlingSpring": "Primavera", "deerlingSummer": "Estate", "deerlingAutumn": "Autunno", "deerlingWinter": "Inverno", "tornadusIncarnate": "Incarnazione", + "tornadusTherian": "Totem", "thundurusIncarnate": "Incarnazione", + "thundurusTherian": "Totem", "landorusIncarnate": "Incarnazione", + "landorusTherian": "Totem", + "kyurem": "Normale", + "kyuremBlack": "Nero", + "kyuremWhite": "Bianco", "keldeoOrdinary": "Normale", + "keldeoResolute": "Risoluta", "meloettaAria": "Canto", "meloettaPirouette": "Danza", + "genesect": "Normale", + "genesectShock": "Voltmodulo", + "genesectBurn": "Piromodulo", + "genesectChill": "Gelomodulo", + "genesectDouse": "Idromodulo", + "froakie": "Normale", "froakieBattleBond": "Morfosintonia", + "froakieAsh": "Ash", "scatterbugMeadow": "Giardinfiore", "scatterbugIcySnow": "Nevi perenni", "scatterbugPolar": "Nordico", @@ -91,6 +123,7 @@ "flabebeOrange": "Arancione", "flabebeBlue": "Blu", "flabebeWhite": "Bianco", + "furfrou": "Selvatica", "furfrouHeart": "Cuore", "furfrouStar": "Stella", "furfrouDiamond": "Diamante", @@ -100,21 +133,52 @@ "furfrouLaReine": "Regina", "furfrouKabuki": "Kabuki", "furfrouPharaoh": "Faraone", + "espurrMale": "Maschio", + "espurrFemale": "Femmina", + "honedgeShiled": "Scudo", + "honedgeBlade": "Spada", + "pumpkaboo": "Normale", "pumpkabooSmall": "Mini", "pumpkabooLarge": "Grande", "pumpkabooSuper": "Maxi", "xerneasNeutral": "Relax", "xerneasActive": "Attivo", - "zygarde50": "Forma 50%", - "zygarde10": "Forma 10%", - "zygarde50Pc": "Forma 50% Sciamefusione", - "zygarde10Pc": "Forma 10% Sciamefusione", - "zygardeComplete": "Forma perfetta", + "zygarde50": "50%", + "zygarde10": "10%", + "zygarde50Pc": "50% Sciamefusione", + "zygarde10Pc": "10% Sciamefusione", + "zygardeComplete": "perfetta", + "hoopa": "Vincolato", + "hoopaUnbound": "Libero", "oricorioBaile": "Flamenco", "oricorioPompom": "Cheerdance", "oricorioPau": "Hula", "oricorioSensu": "Buyō", + "rockruff": "Normale", "rockruffOwnTempo": "Mentelocale", + "rockruffMidday": "Giorno", + "rockruffMidnight": "Notte", + "rockruffDusk": "Crepuscolo", + "wishiwashi": "Individuale", + "wishiwashiSchool": "Banco", + "typeNullNormal": "Tipo Normale", + "typeNullFighting": "Tipo Lotta", + "typeNullFlying": "Tipo Volante", + "typeNullPoison": "Tipo Veleno", + "typeNullGround": "Tipo Terra", + "typeNullRock": "Tipo Roccia", + "typeNullBug": "Tipo Coleottero", + "typeNullGhost": "Tipo Spettro", + "typeNullSteel": "Tipo Acciaio", + "typeNullFire": "Tipo Fuoco", + "typeNullWater": "Tipo Acqua", + "typeNullGrass": "Tipo Erba", + "typeNullElectric": "Tipo Elettro", + "typeNullPsychic": "Tipo Psico", + "typeNullIce": "Tipo Ghiaccio", + "typeNullDragon": "Tipo Drago", + "typeNullDark": "Tipo Buio", + "typeNullFairy": "Tipo Folletto", "miniorRedMeteor": "Nucleo Rosso", "miniorOrangeMeteor": "Nucleo Arancione", "miniorYellowMeteor": "Nucleo Giallo", @@ -131,25 +195,66 @@ "miniorViolet": "Violetto", "mimikyuDisguised": "Mascherata", "mimikyuBusted": "Smascherata", + "necrozma": "Normale", + "necrozmaDuskMane": "Criniera del Vespro", + "necrozmaDawnWings": "Ali dell'Aurora", + "necrozmaUltra": "Ultra", + "magearna": "Normale", "magearnaOriginal": "Colore Antico", + "marshadow": "Normale", "marshadowZenith": "Zenith", + "cramorant": "Normale", + "cramorantGulping": "Inghiottitutto", + "cramorantGorging": "Inghiottintero", + "toxelAmped": "Melodia", + "toxelLowkey": "Basso", "sinisteaPhony": "Contraffatta", "sinisteaAntique": "Autentica", + "milceryVanillaCream": "Lattevaniglia", + "milceryRubyCream": "Latterosa", + "milceryMatchaCream": "Lattematcha", + "milceryMintCream": "Lattementa", + "milceryLemonCream": "Lattelimone", + "milcerySaltedCream": "Lattesale", + "milceryRubySwirl": "Rosamix", + "milceryCaramelSwirl": "Caramelmix", + "milceryRainbowSwirl": "Triplomix", + "eiscue": "Gelofaccia", "eiscueNoIce": "Liquefaccia", "indeedeeMale": "Maschio", "indeedeeFemale": "Femmina", "morpekoFullBelly": "Panciapiena", + "morpekoHangry": "Panciavuota", "zacianHeroOfManyBattles": "Eroe di Mille Lotte", + "zacianCrowned": "Re delle Spade", "zamazentaHeroOfManyBattles": "Eroe di Mille Lotte", + "zamazentaCrowned": "Re degli Scudi", + "kubfuSingleStrike": "Singolcolpo", + "kubfuRapidStrike": "Pluricolpo", + "zarude": "Normale", "zarudeDada": "Papà", + "calyrex": "Normale", + "calyrexIce": "Cavaliere Glaciale", + "calyrexShadow": "Cavaliere Spettrale", + "basculinMale": "Maschio", + "basculinFemale": "Femmina", "enamorusIncarnate": "Incarnazione", + "enamorusTherian": "Totem", + "lechonkMale": "Maschio", + "lechonkFemale": "Femmina", + "tandemausFour": "Quadrifamiglia", + "tandemausThree": "Trifamiglia", "squawkabillyGreenPlumage": "Piume Verdi", "squawkabillyBluePlumage": "Piume Azzurre", "squawkabillyYellowPlumage": "Piume Gialle", "squawkabillyWhitePlumage": "Piume Bianche", + "finizenZero": "Ingenua", + "finizenHero": "Possente", "tatsugiriCurly": "Arcuata", "tatsugiriDroopy": "Adagiata", "tatsugiriStretchy": "Tesa", + "dunsparceTwo": "Bimetamero", + "dunsparceThree": "Trimetamero", "gimmighoulChest": "Scrigno", "gimmighoulRoaming": "Ambulante", "koraidonApexBuild": "Foggia Integrale", @@ -164,7 +269,22 @@ "miraidonGlideMode": "Assetto Planata", "poltchageistCounterfeit": "Taroccata", "poltchageistArtisan": "Pregiata", + "poltchageistUnremarkable": "Dozzinale", + "poltchageistMasterpiece": "Eccezionale", + "ogerponTealMask": "Maschera Turchese", + "ogerponTealMaskTera": "Maschera Turchese Teracristal", + "ogerponWellspringMask": "Maschera Pozzo", + "ogerponWellspringMaskTera": "Maschera Pozzo Teracristal", + "ogerponHearthflameMask": "Maschera Focolare", + "ogerponHearthflameMaskTera": "Maschera Focolare Teracristal", + "ogerponCornerstoneMask": "Maschera Fondamenta", + "ogerponCornerstoneMaskTera": "Maschera Fondamenta Teracristal", + "terpagos": "Normale", + "terpagosTerastal": "Teracristal", + "terpagosStellar": "Astrale", + "galarDarumaka": "Normale", + "galarDarumakaZen": "Stato Zen", "paldeaTaurosCombat": "Combattiva", "paldeaTaurosBlaze": "Infuocata", "paldeaTaurosAqua": "Acquatica" -} \ No newline at end of file +} diff --git a/src/locales/it/pokemon-summary.json b/src/locales/it/pokemon-summary.json index f6c9290f783..81cd9a278b8 100644 --- a/src/locales/it/pokemon-summary.json +++ b/src/locales/it/pokemon-summary.json @@ -11,7 +11,7 @@ "cancel": "Annulla", "memoString": "Natura {{natureFragment}},\n{{metFragment}}", "metFragment": { - "normal": "incontrato al Lv.{{level}},\n{{biome}}.", + "normal": "incontrato al Lv.{{level}},\n{{biome}}, Onda {{wave}}.", "apparently": "apparentemente incontrato al Lv.{{level}},\n{{biome}}." } -} \ No newline at end of file +} diff --git a/src/locales/it/splash-messages.json b/src/locales/it/splash-messages.json index 55018d0ada0..d4b411241b6 100644 --- a/src/locales/it/splash-messages.json +++ b/src/locales/it/splash-messages.json @@ -1,5 +1,5 @@ { - "battlesWon": "Battaglie Vinte!", + "battlesWon": "{{count, number}} Battaglie Vinte!", "joinTheDiscord": "Entra nel Discord!", "infiniteLevels": "Livelli Infiniti!", "everythingStacks": "Tutto si impila!", diff --git a/src/locales/it/trainer-classes.json b/src/locales/it/trainer-classes.json index 205a7c59d42..a613e34b39b 100644 --- a/src/locales/it/trainer-classes.json +++ b/src/locales/it/trainer-classes.json @@ -126,5 +126,8 @@ "skull_grunts": "Reclute Team Skull", "macro_grunt": "Impiegato Macro Cosmos", "macro_grunt_female": "Impiegata Macro Cosmos", - "macro_grunts": "Impiegati Macro Cosmos" + "macro_grunts": "Impiegati Macro Cosmos", + "star_grunt": "Recluta Team Star", + "star_grunt_female": "Recluta Team Star", + "star_grunts": "Reclute Team Star" } diff --git a/src/locales/it/trainer-names.json b/src/locales/it/trainer-names.json index 6d1373c0bb3..78e76b8434c 100644 --- a/src/locales/it/trainer-names.json +++ b/src/locales/it/trainer-names.json @@ -139,6 +139,13 @@ "xerosic": "Xante", "bryony": "Bromelia", "faba": "Vicio", + "plumeria": "Plumeria", + "oleana": "Olive", + "giacomo": "Romelio", + "mela": "Pruna", + "atticus": "Henzo", + "ortega": "Ortiz", + "eri": "Nespera", "maxie": "Max", "archie": "Ivan", @@ -147,6 +154,9 @@ "lysandre": "Elisio", "lusamine": "Samina", "guzma": "Guzman", + "rose": "Rose", + "cassiopeia": "Penny", + "blue_red_double": "Blu & Rosso", "red_blue_double": "Rosso & Blu", "tate_liza_double": "Tell & Pat", diff --git a/src/locales/it/trainer-titles.json b/src/locales/it/trainer-titles.json index eff152795cd..8f28efab45d 100644 --- a/src/locales/it/trainer-titles.json +++ b/src/locales/it/trainer-titles.json @@ -19,6 +19,7 @@ "aether_boss": "Direttrice Æther", "skull_boss": "Capo Team Skull", "macro_boss": "Presidente Macro Cosmos", + "star_boss": "Arcicapo Team Star", "rocket_admin": "Tenente Team Rocket", "rocket_admin_female": "Tenente Team Rocket", @@ -34,6 +35,7 @@ "flare_admin_female": "Ufficiale Team Flare", "aether_admin": "Capo Filiale Æther", "skull_admin": "Ufficiale Team Skull", - "macro_admin": "Vicepresidente Macro Cosmos" + "macro_admin": "Vicepresidente Macro Cosmos", + "star_admin": "Capobanda Team Star" } diff --git a/src/locales/ja/ability.json b/src/locales/ja/ability.json index c44eeb06234..c7828d89c75 100644 --- a/src/locales/ja/ability.json +++ b/src/locales/ja/ability.json @@ -1237,6 +1237,6 @@ }, "poisonPuppeteer": { "name": "どくくぐつ", - "description": "モモワロウの 技によって どく状態に なった 相手は こんらん状態にも なってしまう。" + "description": "このポケモンの 技によって どく状態に なった 相手は こんらん状態にも なってしまう。" } } diff --git a/src/locales/ja/bgm-name.json b/src/locales/ja/bgm-name.json index fc3d4c0fdd2..e177d1bddfb 100644 --- a/src/locales/ja/bgm-name.json +++ b/src/locales/ja/bgm-name.json @@ -83,9 +83,11 @@ "battle_aether_grunt": "SM 戦闘!エーテル財団トレーナー", "battle_skull_grunt": "SM 戦闘!スカル団", "battle_macro_grunt": "SWSH 戦闘!トレーナー", + "battle_star_grunt": "SV 戦闘!スター団", "battle_galactic_admin": "BDSP 戦闘!ギンガ団幹部", "battle_skull_admin": "SM 戦闘!スカル団幹部", "battle_oleana": "SWSH 戦闘!オリーヴ", + "battle_star_admin": "SV 戦闘!スター団ボス", "battle_rocket_boss": "USUM 戦闘!レインボーロケット団ボス", "battle_aqua_magma_boss": "ORAS 戦闘!アクア団・マグマ団のリーダー", "battle_galactic_boss": "BDSP 戦闘!ギンガ団ボス", @@ -94,6 +96,7 @@ "battle_aether_boss": "SM 戦闘!ルザミーネ", "battle_skull_boss": "SM 戦闘!スカル団ボス", "battle_macro_boss": "SWSH 戦闘!ローズ", + "battle_star_boss": "SV 戦闘!カシオペア", "abyss": "ポケダン空 やみのかこう", "badlands": "ポケダン空 こかつのたに", @@ -108,17 +111,17 @@ "forest": "ポケダン空 くろのもり", "grass": "ポケダン空 リンゴのもり", "graveyard": "ポケダン空 しんぴのもり", - "ice_cave": "ポケダン空 だいひょうざん", + "ice_cave": "Firel - -50°C", "island": "ポケダン空 えんがんのいわば", "jungle": "Lmz - Jungle(ジャングル)", "laboratory": "Firel - Laboratory(ラボラトリー)", - "lake": "ポケダン空 すいしょうのどうくつ", + "lake": "Lmz - Lake(湖)", "meadow": "ポケダン空 そらのいただき(もり)", "metropolis": "Firel - Metropolis(大都市)", "mountain": "ポケダン空 ツノやま", - "plains": "ポケダン空 そらのいただき(そうげん)", - "power_plant": "ポケダン空 エレキへいげん", - "ruins": "ポケダン空 ふういんのいわば", + "plains": "Firel - Route 888(888ばんどうろ)", + "power_plant": "Firel - The Klink(ザ・ギアル)", + "ruins": "Lmz - Ancient Ruins", "sea": "Andr06 - Marine Mystique(海の神秘性)", "seabed": "Firel - Seabed(海底)", "slum": "Andr06 - Sneaky Snom(ずるいユキハミ)", @@ -128,7 +131,7 @@ "tall_grass": "ポケダン空 のうむのもり", "temple": "ポケダン空 ばんにんのどうくつ", "town": "ポケダン空 ランダムダンジョン3", - "volcano": "ポケダン空 ねっすいのどうくつ", + "volcano": "Firel - Twisturn Volcano(曲がる折れる火山)", "wasteland": "ポケダン空 まぼろしのだいち", "encounter_ace_trainer": "BW 視線!エリートトレーナー", "encounter_backpacker": "BW 視線!バックパッカー", diff --git a/src/locales/ja/dialogue-final-boss.json b/src/locales/ja/dialogue-final-boss.json index f20d0f013d1..2378a09f6d6 100644 --- a/src/locales/ja/dialogue-final-boss.json +++ b/src/locales/ja/dialogue-final-boss.json @@ -1,10 +1,10 @@ { - "encounter": "It appears the time has finally come once again.\nYou know why you have come here, do you not?\n$You were drawn here, because you have been here before.\nCountless times.\n$Though, perhaps it can be counted.\nTo be precise, this is in fact your {{cycleCount}} cycle.\n$Each cycle your mind reverts to its former state.\nEven so, somehow, remnants of your former selves remain.\n$Until now you have yet to succeed, but I sense a different presence in you this time.\n\n$You are the only one here, though it is as if there is… another.\n$Will you finally prove a formidable challenge to me?\nThe challenge I have longed after for millennia?\n$We begin.", - "encounter_female": "It appears the time has finally come once again.\nYou know why you have come here, do you not?\n$You were drawn here, because you have been here before.\nCountless times.\n$Though, perhaps it can be counted.\nTo be precise, this is in fact your {{cycleCount}} cycle.\n$Each cycle your mind reverts to its former state.\nEven so, somehow, remnants of your former selves remain.\n$Until now you have yet to succeed, but I sense a different presence in you this time.\n\n$You are the only one here, though it is as if there is… another.\n$Will you finally prove a formidable challenge to me?\nThe challenge I have longed after for millennia?\n$We begin.", - "firstStageWin": "I see. The presence I felt was indeed real.\nIt appears I no longer need to hold back.\n$Do not disappoint me.", - "secondStageWin": "…Magnificent.", - "key_ordinal_one": "st", - "key_ordinal_two": "nd", - "key_ordinal_few": "rd", - "key_ordinal_other": "th" + "encounter": "又しても 時が満ちた 様 である。\nこちらへ 至る 理由は 存知するな。\n$汝は この場所へ 引かれた……\nこの何度となく 至った場所。\n$けれども 数えられぬとも 限らぬ……\n正確に宣ふ(のたまう)と 現在の 循環は {{cycleCount}}回目 である。\n$各循環に 心… 意識… 両方も 元の有様に 戻る。\nなれども 故吾の 残影は 汝の 中に 存する。\n$未だに 成功せぬ ままでも\n異なる 存在を 感ずる。\n$御座在る者は 一人。\nなれども 感ずるは… もう 他人。\n$到頭 汝から いかめしい 挑戦は 我が目にかかるか?\n千歳 万歳 相まってた挑戦……\n$始もう。", + "encounter_female": "又しても 時が満ちた 様 である。\nこちらへ 至る 理由は 存知するな。\n$汝は この場所へ 引かれた……\nこの何度となく 至った場所。\n$けれども 数えられぬとも 限らぬ……\n正確に宣ふ(のたまう)と 現在の 循環は {{cycleCount}}回目 である。\n$各循環に 心… 意識… 両方も 元の有様に 戻る。\nなれども 故吾の 残影は 汝の 中に 存する。\n$未だに 成功せぬ ままでも\n異なる 存在を 感ずる。\n$御座在る者は 一人。\nなれども 感ずるは… もう 他人。\n$到頭 汝から いかめしい 挑戦は 我が目にかかるか?\n千歳 万歳 相まってた挑戦……\n$始もう。", + "firstStageWin": "成る程。 感じた 存在は 正身(むざね) であった。\n自分を括る 必要 有らぬ 様である。\n$失望させぬが良い。", + "secondStageWin": "……お見事でございます。", + "key_ordinal_one": "", + "key_ordinal_two": "", + "key_ordinal_few": "", + "key_ordinal_other": "" } diff --git a/src/locales/ja/dialogue-misc.json b/src/locales/ja/dialogue-misc.json index 2f333b5f383..d0527564613 100644 --- a/src/locales/ja/dialogue-misc.json +++ b/src/locales/ja/dialogue-misc.json @@ -1,6 +1,6 @@ { - "ending": "@c{shock}You're back?@d{32} Does that mean…@d{96} you won?!\n@c{smile_ehalf}I should have known you had it in you.\n$@c{smile_eclosed}Of course… I always had that feeling.\n@c{smile}It's over now, right? You ended the loop.\n$@c{smile_ehalf}You fulfilled your dream too, didn't you?\nYou didn't lose even once.\n$I'll be the only one to remember what you did.\n@c{angry_mopen}I'll try not to forget!\n$@c{smile_wave_wink}Just kidding!@d{64} @c{smile}I'd never forget.@d{32}\nYour legend will live on in our hearts.\n$@c{smile_wave}Anyway,@d{64} it's getting late…@d{96} I think?\nIt's hard to tell in this place.\n$Let's go home. @c{smile_wave_wink}Maybe tomorrow, we can have another battle, for old time's sake?", - "ending_female": "@c{smile}Oh? You won?@d{96} @c{smile_eclosed}I guess I should've known.\nBut, you're back now.\n$@c{smile}It's over.@d{64} You ended the loop.\n$@c{serious_smile_fists}You fulfilled your dream too, didn't you?\nYou didn't lose even once.\n$@c{neutral}I'm the only one who'll remember what you did.@d{96}\nI guess that's okay, isn't it?\n$@c{serious_smile_fists}Your legend will always live on in our hearts.\n$@c{smile_eclosed}Anyway, I've had about enough of this place, haven't you? Let's head home.\n$@c{serious_smile_fists}Maybe when we get back, we can have another battle?\nIf you're up to it.", - "ending_endless": "Congratulations on reaching the current end!\nMore content is coming soon.", - "ending_name": "Devs" + "ending": "@c{shock}帰ってきた?@d{32} それなら…@d{96} 勝った っていうこと だろう?!\n@c{smile_ehalf}絶対 やれると思う べきだったな。\n$@c{smile_eclosed}もちろん、ずっと そんな気 がしたんだな。\n@c{smile}ついに 終わった だろう? ループを 断ち切った。\n$@c{smile_ehalf}キミの夢も 叶ったよな?\n一回も 負けなかった。\n$キミの しつくした事を 覚えるのは おれだけだ。\n@c{angry_mopen}忘れない ように するが…\n$@c{smile_wave_wink}なんつって!@d{64} @c{smile}一生 忘れない。@d{32}\nキミの伝説は いつまでも みんなの 心の中に 残っているから。\n$@c{smile_wave}とにかく、@d{64} そろそろ 遅くなる……@d{96} かな?\nこの場所で よく 分からない。\n$さあ、帰ろう。\n@c{smile_wave_wink}明日、昔のよしみで バトルでも しないか?", + "ending_female": "@c{smile}えぇ? 勝ちゃった?@d{96} @c{smile_eclosed}勝てるのが 分かる べきだったね。\nやっぱり 帰ってきた…\n$@c{smile}ついに終わった。@d{64} ループを 断ち切った。\n$@c{serious_smile_fists} アナタの夢も 叶ったよね?\n一回も 負けなかった!\n$@c{neutral}アタシだけが アナタが できた事 を覚えていく、ね。@d{96}\nでもね、たぶん 大丈夫なの かなぁ?\n$@c{serious_smile_fists}アナタの伝説は みんなの 心の中に\nずっと 残っているからね……\n$@c{smile_eclosed}じゃあ、こんなトコは もう飽きた だろう?\n帰ろうよ。\n$@c{serious_smile_fists}ふるさとに 着いたら、 また バトルしよう?\nやる気 あればね!", + "ending_endless": "現在のエンドまで やって来て おめでとう!\n更に多くの コンテンツは  近日公開予定です。", + "ending_name": "開発者" } diff --git a/src/locales/ja/dialogue.json b/src/locales/ja/dialogue.json index 6130ade1cb4..9ba82ac54de 100644 --- a/src/locales/ja/dialogue.json +++ b/src/locales/ja/dialogue.json @@ -40,839 +40,910 @@ }, "lass": { "encounter": { - "1": "Let's have a battle, shall we?", - "2": "You look like a new trainer. Let's have a battle!", - "2_female": "You look like a new trainer. Let's have a battle!", - "3": "I don't recognize you. How about a battle?", - "4": "Let's have a fun Pokémon battle!", - "5": "I'll show you the ropes of how to really use Pokémon!", - "6": "A serious battle starts from a serious beginning! Are you sure you're ready?", - "6_female": "A serious battle starts from a serious beginning! Are you sure you're ready?", - "7": "You're only young once. And you only get one shot at a given battle. Soon, you'll be nothing but a memory.", - "8": "You'd better go easy on me, OK? Though I'll be seriously fighting!", - "9": "School is boring. I've got nothing to do. Yawn. I'm only battling to kill the time." + "1": "勝負しない?", + "2": "新人 トレーナーだろう? 勝負しよう!", + "2_female": "新しい トレーナーだろう? 勝負しよう!", + "3": "初めて 見る 顔ね! 勝負しない?", + "4": "ドッキドキで ワックワクな\nポケモン勝負に しよう!", + "5": "手取り 足取り 教えて あげる!\nホントの ポケモンの 使いかた!", + "6": "マジの 勝負 マジ始めっからー\n準備とか 覚悟とか オーケー?", + "6_female": "マジの 勝負 マジ始めっからー\n準備とか 覚悟とか オーケー?", + "7": "青春も バトルも 一度きり!!\nあなたを 思い出に させて!!", + "8": "あんた ちょっとは 手加減してよ…\nあたしは 本気で いくけど!", + "9": "学校 タルいし やることないし\nヒマだけあるから バトルしてんの。" }, "victory": { - "1": "That was impressive! I've got a lot to learn.", - "2": "I didn't think you'd beat me that bad…", - "2_female": "I didn't think you'd beat me that bad…", - "3": "I hope we get to have a rematch some day.", - "4": "That was pretty amazingly fun! You've totally exhausted me…", - "5": "You actually taught me a lesson! You're pretty amazing!", - "6": "Seriously, I lost. That is, like, seriously depressing, but you were seriously cool.", - "6_female": "Seriously, I lost. That is, like, seriously depressing, but you were seriously cool.", - "7": "I don't need memories like this. Deleting memory…", - "8": "Hey! I told you to go easy on me! Still, you're pretty cool when you're serious.", - "8_female": "Hey! I told you to go easy on me! Still, you're pretty cool when you're serious.", - "9": "I'm actually getting tired of battling… There's gotta be something new to do…" + "1": "すごい! 覚えること いっぱいね。", + "2": "新人に こんなに ぶっ壊されて なんて……", + "2_female": "新人に こんなに ぶっ壊されて なんて……", + "3": "またいつか 勝負を できると いいね!", + "4": "キミに ドッキ ドキドキ\n私は ボッロ ボロボロ……", + "5": "逆に 教えられちゃった!\nキミって 結構 スゴいんだ!", + "6": "マジ負けてる マジ凹む マジ辛い\nでも アンタは マジで イケてるかも。", + "6_female": "マジ負けてる マジ凹む マジ辛い\nでも アンタは マジで イケてるかも。", + "7": "こんな 思い出 いらないし\n記憶から 消しちゃおっと…", + "8": "ぷんぷん! 手加減してよね!!\n……本気の あんたも いいけどさ。", + "8_female": "ぷんぷん! 手加減してよね!!\n……本気の あんたも いいけどさ。", + "9": "バトルも そろそろ あきたわ 実際…\n何か 新しいこと とか ない?" } }, "breeder": { "encounter": { - "1": "Obedient Pokémon, selfish Pokémon… Pokémon have unique characteristics.", - "2": "Even though my upbringing and behavior are poor, I've raised my Pokémon well.", - "3": "Hmm, do you discipline your Pokémon? Pampering them too much is no good." + "1": "素直なヤツに ワガママなヤツ……\nポケモンにも 個性が あるんだよな。", + "2": "育ちも 素行も 悪い オレが\n育てた割に いい ポケモン だぜ!", + "3": "きみは ポケモンを しつけてるか?\n甘やかすだけじゃ ダメなんだぜ!" }, "victory": { - "1": "It is important to nurture and train each Pokémon's characteristics.", - "2": "Unlike my diabolical self, these are some good Pokémon.", - "3": "Too much praise can spoil both Pokémon and people." + "1": "ポケモンの 持っている 個性を\n活かして 育てることが 大切だ。", + "2": "極悪非道の オレと 違って\nこいつらは いいポケモン だよ!", + "3": "褒めてばっかりだと ポケモンも\n人も ダメに なっちゃうよな…" }, "defeat": { - "1": "You should not get angry at your Pokémon, even if you lose a battle.", - "2": "Right? Pretty good Pokémon, huh? I'm suited to raising things.", - "3": "No matter how much you love your Pokémon, you still have to discipline them when they misbehave." + "1": "負けたからって むやみに ポケモンを\n怒ったりしては ダメなんだぞ。", + "2": "な? なかなか いいポケモン だろ?\n育てるってのが 向いてるんだな オレ。", + "3": "可愛くて 大好きなポケモンでも\n悪いこと したら 叱らないとな!" } }, "breeder_female": { "encounter": { - "1": "Pokémon never betray you. They return all the love you give them.", - "2": "Shall I give you a tip for training good Pokémon?", - "3": "I have raised these very special Pokémon using a special method." + "1": "ポケモンは 裏切らないからね、\nささげた 愛情は 返ってくる。", + "2": "いいポケモンを 育てるコツを\nキミに 教えてあげようかな?", + "3": "秘密の 方法で 育てあげた\nとっても スペシャルな ポケモンだ。" }, "victory": { - "1": "Ugh… It wasn't supposed to be like this. Did I administer the wrong blend?", - "2": "How could that happen to my Pokémon… What are you feeding your Pokémon?", - "3": "If I lose, that tells you I was just killing time. It doesn't damage my ego at all." + "1": "うーん…… こんな はずでは……\nエキスの 配合を 間違えたか?", + "2": "ワイの 育てた ポケモンが そんな……\nキミは ポケモンに なに あげとるんや?", + "3": "負けたところで ただのヒマつぶし\n心に ダメージ ございません…" }, "defeat": { - "1": "This proves my Pokémon have accepted my love.", - "2": "The real trick behind training good Pokémon is catching good Pokémon.", - "3": "Pokémon will be strong or weak depending on how you raise them." + "1": "私の 愛情が ポケモンに\n届いてる 証拠 だわね。", + "2": "いいポケモンを 育てるコツは\nいいポケモンを 捕まえることだ。", + "3": "育てかた 次第で ポケモンは\n強くも 弱くも なるのだ。" } }, "fisherman": { "encounter": { - "1": "Aack! You made me lose a bite!\nWhat are you going to do about it?", - "2": "Go away! You're scaring the Pokémon!", - "3": "Let's see if you can reel in a victory!" + "1": "釣り糸が 絡まって イライラするーっ!\nええいっ 勝負だっ!", + "2": "海釣りと 川釣り キミは どっちが 好き?", + "3": "よーし! ポケモン好きと 釣り好きの 対決だ!" }, "victory": { - "1": "Just forget about it.", - "2": "Next time, I'll be reelin' in the triumph!", - "3": "Guess I underestimated the currents this time." + "1": "負けたっ! 余計に イライラするぞーっ!", + "2": "まるで 海釣りの ように 豪快に 負けたぞーっ!", + "3": "わあ! 糸が 絡まった! まさに 後の祭り…" } }, "fisherman_female": { "encounter": { - "1": "Woah! I've hooked a big one!", - "2": "Line's in, ready to reel in success!", - "3": "Ready to make waves!" + "1": "でっかいの 釣れたっ でっかいの!", + "2": "うちは 常に 大物ねらい!\n雑魚だったら 引っ込んで おくれよ!", + "3": "釣りたて ピチピチの\nポケモンで 勝負してあげよう!!" }, "victory": { - "1": "I'll be back with a stronger hook.", - "2": "I'll reel in victory next time.", - "3": "I'm just sharpening my hooks for the comeback!" + "1": "あれー 大きさで 負けたかな…", + "2": "なかなかの 引きな キミ!\nナイス ファイトだった!", + "3": "釣りあげられたのは……\nこの 私だったか!" } }, "swimmer": { "encounter": { - "1": "Time to dive in!", - "2": "Let's ride the waves of victory!", - "3": "Ready to make a splash!" + "1": "全力で 飛び込むよ!", + "2": "勝利の 大波に 乗っていくよ!", + "3": "完全に 浸しちゃうよ!" }, "victory": { - "1": "Drenched in defeat!", - "2": "A wave of defeat!", - "3": "Back to shore, I guess." + "1": "失敗で ぐしゃぐしゃ!", + "2": "敗北の 小波だった……", + "3": "そろそろ 漂って帰ろう……" } }, "backpacker": { "encounter": { - "1": "Pack up, game on!", - "2": "Let's see if you can keep pace!", - "3": "Gear up, challenger!", - "4": "I've spent 20 years trying to find myself… But where am I?" + "1": "旅する バックパッカー\n同じく 旅のトレーナーに 会う…", + "2": "私と 旅しているのは 人気の\nガイドブックで お薦めの ポケモン です!", + "3": "旅をしても していなくても\n私は 発見したいのです!", + "4": "旅を 続け 幾年月、\n知らない 国は ないくらい。" }, "victory": { - "1": "Tripped up this time!", - "2": "Oh, I think I'm lost.", - "3": "Dead end!", - "4": "Wait up a second! Hey! Don't you know who I am?" + "1": "きみと 出会うため\n旅していたのかも? なんて", + "2": "ガイドブックに 勝ち方は\n載っていないのよね…", + "3": "あっ! あなたの いいところ\n発見しちゃったかも!", + "4": "本当は ただ 「お帰り」 って\n言ってくれる ダーリンが 欲しい…" } }, "ace_trainer": { "encounter": { - "1": "You seem quite confident.", - "1_female": "You seem quite confident.", - "2": "Your Pokémon… Show them to me…", - "3": "Because I'm an Ace Trainer, people think I'm strong.", - "4": "Are you aware of what it takes to be an Ace Trainer?" + "1": "自信満々って 感じ ですな…", + "1_female": "自信満々って 感じ だね…", + "2": "君の ポケモン…… 私に 見せてごらんよ……", + "3": "エリートトレーナーなんて やってるから\n強い 人って 思われるんだ。", + "4": "エリートトレーナーたる 資格 って\nあなたは ご存じ かしら?" }, "victory": { - "1": "Yes… You have good Pokémon…", - "2": "What?! But I'm a battling genius!", - "3": "Of course, you are the main character!", - "3_female": "Of course, you are the main character!", - "4": "OK! OK! You could be an Ace Trainer!", - "4_female": "OK! OK! You could be an Ace Trainer!" + "1": "うん…… いいポケモンだね……", + "2": "勝負の 天才 いわれる\n私も 負けるこつの あっけんか!", + "3": "やはり あなたこそ 主人公だ!", + "3_female": "やはり あなたこそ 主人公だ!", + "4": "戦う 姿が なじんでるのね\nエリートトレーナーに なれるよ キミ!", + "4_female": "戦う 姿が なじんでるのね\nエリートトレーナーに なれるよ キミ!" }, "defeat": { - "1": "I am devoting my body and soul to Pokémon battles!", - "2": "All within my expectations… Nothing to be surprised about…", - "3": "I thought I'd grow up to be a frail person who looked like they would break if you squeezed them too hard.", - "4": "Of course I'm strong and don't lose. It's important that I win gracefully." + "1": "私はな ポケモン勝負に\n魂 かけてんだよォ!", + "2": "すべて 予想の はんちゅう……\n何の 驚きもないね……", + "3": "弱くて 抱きしめたら 折れそうな\n人に なるはずだったのにな。", + "4": "強いこと 負けないことは 当たり前……\nいかに 優雅に 勝つかが 重要 なの。" } }, "parasol_lady": { "encounter": { - "1": "Time to grace the battlefield with elegance and poise!" + "1": "傘が ないと パラソルおねえさんって\n言えないからさ 仕方なく 差してんの。" }, "victory": { - "1": "My elegance remains unbroken!" + "1": "傘がっ 傘が 折れちゃったっ!\nただの おねえさんに なっちゃう!" } }, "twins": { "encounter": { - "1": "Get ready, because when we team up, it's double the trouble!", - "2": "Two hearts, one strategy – let's see if you can keep up with our twin power!", - "3": "Hope you're ready for double trouble, because we're about to bring the heat!", - "3_female": "Hope you're ready for double trouble, because we're about to bring the heat!" + "1": "準備してね! あたしたちが 力を 組み合うと Wトラブルでちゅ!", + "2": "心は二つ、 作戦は一つ!\nツインパワーを 見せるなだ!", + "3": "二倍の 力で 熱いバトルを しましょうよ!", + "3_female": "二倍の 力で 熱いバトルを しましょうよ!" }, "victory": { - "1": "We may have lost this round, but our bond remains unbreakable!", - "2": "Our twin spirit won't be dimmed for long.", - "3": "We'll come back stronger as a dynamic duo!" + "1": "バトル 負けたけど、 絆 壊れないんでちゅ!", + "2": "何が起こっても ツイン魂が あせてはしないよ!", + "3": "二倍、 いや、 四倍の 力で 戻って来るよ!" }, "defeat": { - "1": "Twin power reigns supreme!", - "2": "Two hearts, one triumph!", - "3": "Double the smiles, double the victory dance!" + "1": "ツインパワーに 誰も 勝てない!", + "2": "心は二つ、 勝利は一つ!", + "3": "笑顔も 勝利の舞も 二倍!" } }, "cyclist": { "encounter": { - "1": "Get ready to eat my dust!", - "2": "Gear up, challenger! I'm about to leave you in the dust!", - "3": "Pedal to the metal, let's see if you can keep pace!" + "1": "両足 砕けようとも 漕ぐ、\nこれ サイクリング 極意なり…", + "2": "やー 悪いねー ついつい パートナー\n連れてきちゃったわ ラブラブだからさ!", + "3": "もうダメェ! 物足りないのォ!\n自転車じゃ 物足りないよォ!" }, "victory": { - "1": "Spokes may be still, but determination pedals on.", - "2": "Outpaced!", - "3": "The road to victory has many twists and turns yet to explore." + "1": "我が サイクリングは 無限\nいずれまた 貴様に 挑む…", + "2": "冷たいボディに ふんわりサドル\nコイツは 最高の パートナーだぜ…", + "3": "自転車じゃ 満足できないのォ!\n暴走族に 入れてもらうのォ!" } }, "black_belt": { "encounter": { - "1": "I praise your courage in challenging me! For I am the one with the strongest kick!", - "2": "Oh, I see. Would you like to be cut to pieces? Or do you prefer the role of punching bag?", - "2_female": "Oh, I see. Would you like to be cut to pieces? Or do you prefer the role of punching bag?" + "1": "宇宙一の キックを 持つ\nわしに 挑むとは 褒めてやろう!", + "2": "んー そうだね、 ズタズタ されたい?\nそれとも ボロボロが 好み かな?", + "2_female": "んー そうだね、 ズタズタ されたい?\nそれとも ボロボロが 好み かな?" }, "victory": { - "1": "Oh. The Pokémon did the fighting. My strong kick didn't help a bit.", - "2": "Hmmm… If I was going to lose anyway, I was hoping to get totally messed up in the process." + "1": "あ 戦うのは ポケモンだっけ\nわしの キックは 関係ないわ。", + "2": "んー…… どうせ 負けるなら\nボクは メチャクチャに されたかったんだけどね。" } }, "battle_girl": { "encounter": { - "1": "You don't have to try to impress me. You can lose against me." + "1": "いいのよ、 見栄 張らなくても\n私には 負けてもいいのよ。" }, "victory": { - "1": "It's hard to say good-bye, but we are running out of time…" + "1": "さようならするのは つらいけれど\nもう 時間が ないわね……" } }, "hiker": { "encounter": { - "1": "My middle-age spread has given me as much gravitas as the mountains I hike!", - "2": "I inherited this big-boned body from my parents… I'm like a living mountain range…" + "1": "中年太りの このボディ\n山の ごとき 貫禄で ゴワス!", + "2": "親 から もらった メタボな ボディ……\n生ける 山脈とは オレの こと……" }, "victory": { - "1": "At least I cannot lose when it comes to BMI!", - "2": "It's not enough… It's never enough. My bad cholesterol isn't high enough…" + "1": "皮下脂肪 だったら\n負けないのにで ゴワス!", + "2": "足りぬ…… 足りぬぞ……\n悪玉コレステロールが 足りぬぞ……" } }, "ranger": { "encounter": { - "1": "When I am surrounded by nature, most other things cease to matter.", - "2": "When I'm living without nature in my life, sometimes I'll suddenly feel an anxiety attack coming on." + "1": "自然に 囲まれ 暮らしているとな\n大抵の ことが どうでも良くなる……", + "2": "自然から 離れ 暮らしているとな\n時々 急に 苦しくなるのさ。" }, "victory": { - "1": "It doesn't matter to the vastness of nature whether I win or lose…", - "2": "Something like this is pretty trivial compared to the stifling feelings of city life." + "1": "ぼくが 負けたって 大自然に\nとっては どうだって いいことさ……", + "2": "都会の 息苦しさに 比べれば\nこんなこと へ でも ないさ……" }, "defeat": { - "1": "I won the battle. But victory is nothing compared to the vastness of nature…", - "2": "I'm sure how you feel is not so bad if you compare it to my anxiety attacks…" + "1": "ぼくが 勝ったことも 大自然に\n比べたら どうだっていいのさ……", + "2": "僕の 不安に 比べれば\n大したこと ないさ きっと……" } }, "scientist": { "encounter": { - "1": "My research will lead this world to peace and joy." + "1": "ボクノ 研究ガ 世界ヲ\n平和ト 幸セニ 導クノデス。" }, "victory": { - "1": "I am a genius… I am not supposed to lose against someone like you…" + "1": "ボクハ 天才 ナンダ……\nコンナ 相手ニ 負ケルハズ……" } }, "school_kid": { "encounter": { - "1": "…Heehee. I'm confident in my calculations and analysis.", - "2": "I'm gaining as much experience as I can because I want to be a Gym Leader someday." + "1": "……グフフ 計算と 分析には\n自信が ありますからね 僕!", + "2": "ここで いっぱい 経験 積んで\nいつかは ジムリーダーを 目指すんだ!" }, "victory": { - "1": "Ohhhh… Calculation and analysis are perhaps no match for chance…", - "2": "Even difficult, trying experiences have their purpose, I suppose." + "1": "ムググ…… 計算も 分析も\n偶然には 敵わないか……", + "2": "辛く 悲しい 経験も\nいつかは 役に 立つはずです……" } }, "artist": { "encounter": { - "1": "I used to be popular, but now I am all washed up." + "1": "かつては おれも 売れっ子だったが\n今では だれにも 相手にされん……" }, "victory": { - "1": "As times change, values also change. I realized that too late." + "1": "時代が 変われば 価値も 移ろう\nそれに 気がつくのが 遅かったのだ……" } }, "guitarist": { "encounter": { - "1": "Get ready to feel the rhythm of defeat as I strum my way to victory!" + "1": "ロックン ロールッ!\n戦いは リズムッ!" }, "victory": { - "1": "Silenced for now, but my melody of resilience will play on." + "1": "ぎゅぎゅーん……\n悲しい メロディ だぜ……" } }, "worker": { "encounter": { - "1": "It bothers me that people always misunderstand me. I'm a lot more pure than everyone thinks." + "1": "作業員 ってのは 命がけ\nだけど やりがいは 十分よ!" }, "victory": { - "1": "I really don't want my skin to burn, so I want to stay in the shade while I work." + "1": "100年 残る ビルや 橋を\n建てるなんて ロマン だろう?" } }, "worker_female": { "encounter": { - "1": "It bothers me that people always misunderstand me.\n$I'm a lot more pure than everyone thinks." + "1": "よく 勘違い されて 困るんだけど\nみんなが 思うよりも ナイーブ なのね。" }, "victory": { - "1": "I really don't want my skin to burn, so I want to stay in the shade while I work." + "1": "本当は お肌 焼きたくない から\n日陰 でしか 作業 したくないのね……" }, "defeat": { - "1": "My body and mind aren't necessarily always in sync." + "1": "心と体は 必ずしも\n一致するわけでは ないのね……" } }, "worker_double": { "encounter": { - "1": "I'll show you we can break you. We've been training in the field!" + "1": "現場で 鍛えた ワシらの 力\n今 見せちゃるけえのぉ!" }, "victory": { - "1": "How strange… How could this be… I shouldn't have been outmuscled." + "1": "おかしいのぉ なんでかのぉ\n力で 負けるはず ないんじゃが……" } }, "hex_maniac": { "encounter": { - "1": "I normally only ever listen to classical music, but if I lose, I think I shall try a bit of new age!", - "2": "I grow stronger with each tear I cry." + "1": "普段 クラシックしか 聴きませんが 負けたら ラップを してみます!", + "2": "涙の 数だけ 強くなれるの だって\nわたし 女の子 だもん。" }, "victory": { - "1": "Is this the dawning of the age of Aquarius?", - "2": "Now I can get even stronger. I grow with every grudge." + "1": "アタイの 想いは アイツに 重い!\n呪いと 願いは 大体 同じッ!", + "2": "これで わたしは また 強くなれる\n恨んだ数だけ 成長 するの。" }, "defeat": { - "1": "New age simply refers to twentieth century classical composers, right?", - "2": "Don't get hung up on sadness or frustration. You can use your grudges to motivate yourself.", - "2_female": "Don't get hung up on sadness or frustration. You can use your grudges to motivate yourself." + "1": "ラッパーって ラップ現象を\n起こすのが 仕事なんですよね?", + "2": "辛いことや 悲しいことを バネにせず\nそのまま 恨みの 力に 変えるんだ。", + "2_female": "辛いことや 悲しいことを バネにせず\nそのまま 恨みの 力に 変えるんだ。" } }, "psychic": { "encounter": { - "1": "Hi! Focus!" + "1": "ハイッ!! 集中ッ!!" }, "victory": { - "1": "Eeeeek!" + "1": "フギャンッ!!" } }, "officer": { "encounter": { - "1": "Brace yourself, because justice is about to be served!", - "2": "Ready to uphold the law and serve justice on the battlefield!" + "1": "ボクは お巡り なんだよ!!\nつまり ボクは ジャスティスだよ!", + "2": "さぼってないのでー ありますっ!\nパトロールなのでー ありますっ!" }, "victory": { - "1": "The weight of justice feels heavier than ever…", - "2": "The shadows of defeat linger in the precinct." + "1": "ボクはっ ボクは お巡りだぞぉ!!\nジャスティス なのにーっ!!", + "2": "キミ 要注意でー ありますっ!\nマークしちゃうでー ありますっ!" } }, "beauty": { "encounter": { - "1": "My last ever battle… That's the way I'd like us to view this match…" + "1": "これが 最後の 勝負……\nそう 思って 相手するわね。" }, "victory": { - "1": "It's been fun… Let's have another last battle again someday…" + "1": "楽しかった……\nまた 最後の 勝負を したいものね。" } }, "baker": { "encounter": { - "1": "Hope you're ready to taste defeat!", - "1_female": "Hope you're ready to taste defeat!" + "1": "きみは もうすぐ\n敗北を 味わっちゃうわ!", + "1_female": "君は もうすぐ\n敗北を 味わっちゃうわ!" }, "victory": { - "1": "I'll bake a comeback." + "1": "諦める べーき かもね……" } }, "biker": { "encounter": { - "1": "Time to rev up and leave you in the dust!" + "1": "おんしゃー 覚悟は できとるんか?\n暴走族 なめたら いかんぜよ!" }, "victory": { - "1": "I'll tune up for the next race." + "1": "ボクらぁー ほんまは 真面目です!\nちっくとも 悪いことは せんです!!" } }, "firebreather": { "encounter": { - "1": "My flames shall devour you!", - "2": "My soul is on fire. I'll show you how hot it burns!", - "3": "Step right up and take a look!" + "1": "やけたとうで 火を 吹く 練習をした!\n良い子は 真似 すんなよ!", + "2": "俺たち 火吹きやろうは 誰よりも 火の こわさを 知ってるのさ!", + "3": "よってらっしゃい みてらっしゃい!" }, "victory": { - "1": "I burned down to ashes...", - "2": "Yow! That's hot!", - "3": "Ow! I scorched the tip of my nose!" + "1": "おっと 誤解 しないでくれ!\nあのとうが 燃えたのは 俺のせいじゃ ないんだ!", + "2": "アツい 勝負を ありがとう!", + "3": "あちちち 鼻の 先っぽ 焦げちゃった!" } }, "sailor": { "encounter": { - "1": "Matey, you're walking the plank if you lose!", - "2": "Come on then! My sailor's pride is at stake!", - "3": "Ahoy there! Are you seasick?", - "3_female": "Ahoy there! Are you seasick?" + "1": "うっしゃー! 負けたら 海に 落とすぞー!", + "2": "そら 来い! 船乗り 魂に かけて 勝つ!", + "3": "おい あんた! 乗ってて 船酔い しないか?", + "3_female": "おい あんた! 乗ってて 船酔い しないか?" }, "victory": { - "1": "Argh! Beaten by a kid!", - "2": "Your spirit sank me!", - "3": "I think it's me that's seasick..." + "1": "くー やられた!", + "2": "船乗り 魂も お前には 負けた!", + "3": "船酔いしてるのが 俺か……" } }, "archer": { "encounter": { - "1": "Before you go any further, let's see how you fare against us, Team Rocket!", - "2": "I have received reports that your skills are not insignificant. Let's see if they are true.", - "3": "I am Archer, an Admin of Team Rocket. And I do not go easy on enemies of our organization." + "1": "先に進む 前に 我々 ロケット団と 戦って もらおうか?", + "2": "あなたも 相当の腕だと 報告が 来ています。 確認しましょう。", + "3": "私は ロケット団幹部の アポロ。\n私達の 敵に 手加減 しませんよ!" }, "victory": { - "1": "What a blunder!", - "2": "With my current skills, I was not up to the task after all.", - "3": "F-forgive me, Giovanni... For me to be defeated by a mere trainer..." + "1": "……なんという 失態!", + "2": "今の腕で やはり 私では 無理 でしたか……", + "3": "さ サカキ様 お許しください\n私とも あろう ものが……" } }, "ariana": { "encounter": { - "1": "Hold it right there! We can't someone on the loose.\n$It's harmful to Team Rocket's pride, you see.", - "2": "I don't know or care if what I'm doing is right or wrong...\n$I just put my faith in Giovanni and do as I am told", - "3": "Your trip ends here. I'm going to take you down!" + "1": "そこまでよーーーっ!!\n$あなたみたいな ヤツを いつまでも のさばらせて おいたら\nロケット団の プライドは キズついて キズついて キズだらけに なっちゃうのよー!", + "2": "私たちの している ことが\n正しいかどうか なんて どうでもいい…\n$私は サカキ様を 信じて ただ 付いてきた のよ!", + "3": "ここは 通さないよ。\nだって 私が 勝つんだから!" }, "victory": { - "1": "Tch, you really are strong. It's too bad.\n$If you were to join Team Rocket, you could become an Executive.", - "1_female": "Tch, you really are strong. It's too bad.\n$If you were to join Team Rocket, you could become an Executive.", - "2": "I... I'm shattered...", - "3": "Aaaieeeee! This can't be happening! I fought hard, but I still lost…" + "1": "あら 強いのね 残念ね\nあなたなら ロケット団に くれば 幹部に だって なれるかもよ。", + "1_female": "あら 強いのね 残念ね\nあなたなら ロケット団に くれば 幹部に だって なれるかもよ。", + "2": "ま まけたわ……", + "3": "くききききーっ! 全力で 戦ったのに……\nこれでも 勝てない なんて!" } }, "proton": { "encounter": { - "1": "What do you want? If you interrupt our work, don't expect any mercy!", - "2": "What do we have here? I am often labeled as the scariest and cruelest guy in Team Rocket…\n$I strongly urge you not to interfere with our business!", - "3": "I am Proton, an Admin of Team Rocket. I am here to put an end to your meddling!" + "1": "まちな! ロケット団の 砦と言われた この私!\nこれ以上 先には 行かせません!", + "2": "なんですか? 私は ロケット団で もっとも 冷酷と 呼ばれた 男……\n$私達の 仕事の ジャマなど させはしませんよ!", + "3": "私は ロケット団幹部の ランス。\nこれ以上 仕事の ジャマは させませんよ!" }, "victory": { - "1": "The fortress came down!", - "2": "You may have won this time… But all you did was make Team Rocket's wrath grow…", - "3": "I am defeated… But I will not forget this!" + "1": "砦が 崩れました……", + "2": "私に 勝った ところで\n所詮は ロケット団の 怒りを 強めた だけですよ……", + "3": "くっ! なかなか やりますね。\nしかし、 次からは そうは 行きません!" } }, "petrel": { "encounter": { - "1": "Muhahaha, we've been waiting for you. Me? You don't know who I am? It is me, Giovanni.\n$The majestic Giovanni himself! Wahahaha! …Huh? I don't sound anything like Giovanni?\n$I don't even look like Giovanni? How come? I've worked so hard to mimic him!", - "2": "I am Petrel, an Admin of Team Rocket. I will not allow you to interfere with our plans!", - "3": "Rocket Executive Petrel will deal with this intruder!" + "1": "ぐっふっふっ よくきたな…\nおや? 私が 誰か 分からんかね?\n$サカキだよ サカキ様 だよ!\nぐわぁーっはっはーっ!\n$……あれ? 全然 似てない? サカキ様に 見えない?\nくっそー 一生懸命 練習したのに!", + "2": "私は ロケット団幹部の ラムダ。\n計画を 邪魔するのは 許さない!", + "3": "侵入者には  ロケット団幹部の ラムダが 対処するぞ!" }, "victory": { - "1": "OK, OK. I'll tell you where he is.", - "2": "I… I couldn't do a thing… Giovanni, please forgive me…", - "3": "No, I can't let this affect me. I have to inform the others…" + "1": "わ 分かった…… 局長の 居場所 教える……", + "2": "ぐうう…… まったく 歯が立たない……\nサカキ様 お許し ください……", + "3": "いかん 負けて 落ち込んでる 場合じゃない\n仲間に 知らせなくては……" } }, "tabitha": { "encounter": { - "1": "Hehehe! So you've come all the way here! But you're too late!", - "2": "Hehehe... Got here already, did you? We underestimated you! But this is it! \n$I'm a cut above the Grunts you've seen so far. I'm not stalling for time.\n$I'm going to pulverize you!", - "3": "I'm going to give you a little taste of pain! Resign yourself to it!" + "1": "ウヒョヒョ! お前 ここまで 来たのか! だけど 遅かったぜ!", + "2": "ウヒョヒョ…… もう ここまで 来たのか!\n思っていたより やるな! だが ここまでだ!\n$俺は これまでの 下っ端ども とは 一味違う!\n時間稼ぎなんか しねえで コテンパンしてやるぜ!", + "3": "ウヒョヒョ! 痛みを 味わわせて あげますよ! 身を 委ねろ!" }, "victory": { - "1": "Hehehe! You might have beaten me, but you don't stand a chance against the boss!\n$If you get lost now, you won't have to face a sound whipping!", - "2": "Hehehe... So, I lost, too...", - "3": "Ahya! How could this be? For an Admin like me to lose to some random trainer...", - "3_female": "Ahya! How could this be? For an Admin like me to lose to some random trainer..." + "1": "「ウヒョヒョ‥‥! 俺に 勝てても リーダーには 勝てないぜ!\n$さっさと 帰ったほうが 痛い 思い しなくて 済むぜ!", + "2": "ウヒョヒョ…… 負けちまったか……", + "3": "うっひょーん……! なんたる事だろうか……!\nサブリーダーの ホムラさんが こんな デタラメなヤツ なぞに……", + "3_female": "うっひょーん……! なんたる事だろうか……!\nサブリーダーの ホムラさんが こんな デタラメなヤツ なぞに……" } }, "courtney": { "encounter": { - "1": "Don't. Get. In. My. Way.", - "2": "You... ...I want to...analyze. Ahahaha", - "3": "... Well then...Deleting..." + "1": "………ジャっ…マ……ッ\nするなあああああッッッッ!!!!!!", + "2": "キミを …………アナライズ\n…………したい …………ァハハハッ♪", + "3": "…………………じゃあ\n…………………デリートします" }, "victory": { - "1": "Hah hah... Uhn...hah hah...", - "2": "As anticipated. Unanticipated. You. Target lock...completed.\n$Commencing...experiment. You. Forever. Aha... ♪", - "3": "That's unanticipated. ...I knew it. You...are interesting! ...Haha. ♪" + "1": "…………はぁはぁ……\n………んぅ…はぁはぁ………", + "2": "…………予想内 …………予想外\n…………キミ …………ターゲットロック …………したから\n$…………エクスペリメント …………するから\n…………キミを …………ずっと …………ァハッ……♪", + "3": "…………また …………予想外\n…………やっぱり …………キミ……オモチロイ……! ……ァハハッ……♪" } }, "shelly": { "encounter": { - "1": "Ahahahaha! You're going to meddle in Team Aqua's affairs?\n$You're either absolutely fearless, simply ignorant, or both!\n$You're so cute, you're disgusting! I'll put you down", - "2": "What's this? Who's this spoiled brat?", - "3": "Cool your jets. Be patient. I'll crush you shortly.", - "3_female": "Cool your jets. Be patient. I'll crush you shortly." + "1": "オーッホッホ! 我々 アクア団の\n邪魔を しようと 言うの!?\n$もう 怖いもの知らず と言おうか\nただの 愚か者 と言おうか……\n$かわいすぎて 憎らしく なっちゃう!\nやっつけて あげるわね!", + "2": "ああん? なんだい?\nこの クソ生意気な オコチャマは……?", + "3": "熱くなってん じゃないよ、 少しは 頭を 冷やしな。\nすぐ ぶっ壊すから。", + "3_female": "熱くなってん じゃないよ、 少しは 頭を 冷やしな。\nすぐ ぶっ壊すから。" }, "victory": { - "1": "Ahahahaha! We got meddled with unexpectedly! We're out of options.\n$We'll have to pull out. But this isn't the last you'll see of Team Aqua!\n$We have other plans! Don't you forget it!", - "2": "Ahhh?! Did I go too easy on you?!", - "3": "Uh. Are you telling me you've upped your game even more during the fight?\n$You're a brat with a bright future… My Pokémon and I don't have any strength left to fight…\n$Go on… Go and be destroyed by Archie.", - "3_female": "Uh. Are you telling me you've upped your game even more during the fight?\n$You're a brat with a bright future… My Pokémon and I don't have any strength left to fight…\n$Go on… Go and be destroyed by Archie." + "1": "オーッホッホ! 思わぬ 邪魔が 入っちゃったわ!\n仕方ないわね!\n$ここは 一度 引き上げちゃう! でもね アクア団の\n活動は まだまだ 続くんだから 覚えておきなさーい!", + "2": "くううっ……!?\n手を 抜きすぎちゃった かしら……!", + "3": "……うぅ ……この間 よりも\n更に ウデを上げてる ですって……!?\n$末恐ろしい オコチャマだわ……\n……アタシと ポケモンたちに もう 戦うチカラは 残っちゃいない\n$……行きなさいよ 行って\nアオギリ様に 粛清されるが いいわ。", + "3_female": "……うぅ ……この間 よりも\n更に ウデを上げてる ですって……!?\n$末恐ろしい オコチャマだわ……\n……アタシと ポケモンたちに もう 戦うチカラは 残っちゃいない\n$……行きなさいよ 行って\nアオギリ様に 粛清されるが いいわ。" } }, "matt": { "encounter": { - "1": "All right then, until the boss has time for you, I'll be your opponent!", - "2": "Hooah! Full on! I'm burning up! Well! Welll! Wellllll! Let's battle it out until we've got nothing left!", - "3": "Hoo hah! I'm gonna smash you up!" + "1": "……ってな ワケでヨォ あにィが ジカンが開くマデ\nオレッちの 相手を してもらうゼぃ!", + "2": "フウハアッ!! マックスッ!!\nタギってッ!! きたゼェェェェッ!!!!!\n$サアッ! サアッ! サアアアッ!!!\nチカラ 尽きハテるまで ヤリあおうゼッッ!!!", + "3": "UPAAAAA!!!\nモミツブシテ ヤルゼェェェ!!" }, "victory": { - "1": "Muwuhahaha! That battle was fun even though I lost!", - "2": "I can feel it! I can feel it, all right! The strength coming offa you!\n$More! I still want more! But looks like we're outta time...", - "3": "Oho! That's a loss I can be proud of!", - "3_female": "Oho! That's a loss I can be proud of!" + "1": "フゥーハッハッハァァァ!!!\nマケても タノしい ショウブ だったゼ!", + "2": "ビンビン かんじるゼェ!! オメェの ツヨサ!\n$モミつぶせる ときを タノシミに してるゼィ!", + "3": "オウホウッ! タカぶる ハイボク だぜっ!", + "3_female": "オウホウッ! タカぶる ハイボク だぜっ!" } }, "mars": { "encounter": { - "1": "I'm Mars, one of Team Galactic's top Commanders.", - "2": "Team Galactic's vision for the future is unwavering. Opposition will be crushed without mercy!", - "3": "Feeling nervous? You should be!", - "3_female": "Feeling nervous? You should be!" + "1": "あたしは ギンガ団幹部の マーズ!\n強くて 美しいの!", + "2": "ギンガ団は 生み出そうと しているのは 新しい 世界。\nジャマは 許さないわ!", + "3": "ボスの ジャマは させないわよ!\nこの先に 進みたいなら あたしが 相手するわ!", + "3_female": "ボスの ジャマは させないわよ!\nこの先に 進みたいなら あたしが 相手するわ!" }, "victory": { - "1": "This can't be happening! How did I lose?!", - "2": "You have some skill, I'll give you that.", - "3": "Defeated... This was a costly mistake." + "1": "負けた……! ギンガ団の 幹部 として……\nこんなことって ありえない!!", + "2": "何なのよッ! あたしのこと 嫌いなの!?", + "3": "まさか! 負けるだなんて!?\n生意気な ヤツね!!" } }, "jupiter": { "encounter": { - "1": "Jupiter, Commander of Team Galactic, at your service.", - "2": "Resistance is futile. Team Galactic will prevail!", - "3": "You're trembling... scared already?" + "1": "いいわ!\nこの ジュピターが 相手 してあげましょう!", + "2": "反対なんて 虚しい!\nギンガ団は 勝利するわ!", + "3": "どうしたの? もしかして 震えているのかしら?" }, "victory": { - "1": "No way... I lost?!", - "2": "Impressive, you've got guts!", - "3": "Losing like this... How embarrassing." + "1": "フン! 相変わらずの 強さ ちっとも かわいくないわね!", + "2": "フン! なかなか やるじゃない!", + "3": "フン! 次は 泣かして あげるんだから!" } }, "saturn": { "encounter": { - "1": "I am Saturn, Commander of Team Galactic.", - "2": "Our mission is absolute. Any hindrance will be obliterated!", - "3": "Is that fear I see in your eyes?" + "1": "ミッションは 順調!\nボスも 満足 なさるだろう。\n$全ては みんなの ために\nそして ギンガ団の ために!", + "2": "ギンガ団の 使命を ジャマするなら\nどんな 可能性でも 潰す!", + "3": "わたしたち ギンガ団は 必要な ものを 独占し\n要らない ものは 捨てるだけ!" }, "victory": { - "1": "Impossible... Defeated by you?!", - "2": "You have proven yourself a worthy adversary.", - "3": "Bestowed in defeat... This is unacceptable." + "1": "強い! だが 哀れだな。", + "2": "……なるほど 強い!\nギンガ団に 歯向かう わけだ。", + "3": "くっ! この わたしが!\n時間稼ぎにしか ならないだと…\n$まあ いい! おまえが 何をしても\n流れる 時間は 止められない!" } }, "zinzolin": { "encounter": { - "1": "You could become a threat to Team Plasma, so we will eliminate you here and now!", - "1_female": "You could become a threat to Team Plasma, so we will eliminate you here and now!", - "2": "You don't have the sense to know when to quit, it seems. It's an act of mercy on my part to bring an end to this now!", - "3": "You're an impressive Trainer to have made it this far. But it ends here.", - "3_female": "You're an impressive Trainer to have made it this far. But it ends here." + "1": "さて…… おまえは プラズマ団に とって\n不安要素に なりかねない。\n$ここで 排除するのだ!", + "1_female": "さて…… おまえは プラズマ団に とって\n不安要素に なりかねない。\n$ここで 排除するのだ!", + "2": "諦めきれぬか? なら 引導を 渡すのが\nワタシなりの 優しさ なのだ!", + "3": "ここまで 来るとは 大した トレーナー だが、\n今は 終わりだ。", + "3_female": "ここまで 来るとは 大した トレーナー が、\n今は 終わりだ。" }, "victory": { - "1": "Ghetsis... I have failed you...", - "2": "It's bitter cold. I'm shivering. I'm suffering. Yet, we will stand victorious.", - "3": "Hmph. You're a smarter Trainer than I expected, but not smart enough.", - "3_female": "Hmph. You're a smarter Trainer than I expected, but not smart enough." + "1": "ゲーチス様…… 失望させました……", + "2": "寒い。 ワタシは 震えている。\n苦しいが いつか ワタシたちは 成功する。", + "3": "ふむう。 存外 さとい トレーナーだ。", + "3_female": "ふむう。 存外 さとい トレーナーだ。" } }, "rood": { "encounter": { - "1": "You are a threat to Team Plasma. We cannot let you walk away from here and now!", - "1_female": "You are a threat to Team Plasma. We cannot let you walk away from here and now!", - "2": "It seems you don't know when to give up. I'll make sure no one interferes with our plans!", - "3": "You are a remarkable Trainer to have made it this far. But this is where it ends.", - "3_female": "You are a remarkable Trainer to have made it this far. But this is where it ends." + "1": "おまえ という トレーナーが\nどんな 人物か 見せてもらいたい…\n$そう、 ポケモン勝負 でな。", + "1_female": "おまえ という トレーナーが\nどんな 人物か 見せてもらいたい…\n$そう、 ポケモン勝負 でな。", + "2": "見限らぬようだ。 だれにも ジャマは させぬよ。", + "3": "……これが おまえの\n望みと あらば", + "3_female": "……これが おまえの\n望みと あらば" }, "victory": { - "1": "Ghetsis... I have failed my mission...", - "2": "The cold is piercing. I'm shivering. I'm suffering. Yet, we will stand triumphant.", - "3": "Hm. You are a talented Trainer, but unfortunately not talented enough." + "1": "ほう! ポケモンと 心が\n通じているような 戦い方……", + "2": "失敗しました……\nそぞろに 潮の 匂いが 恋しくなる……", + "3": "正直言って…… ゲーチス様の なにが 真実で\nなにが 虚構か わからないがね……" } }, - "xerosic": { +"xerosic": { "encounter": { - "1": "Ah ha ha! It would be my pleasure. Come on, little Trainer! Let's see what you've got!", - "1_female": "Ah ha ha! It would be my pleasure. Come on, little Trainer! Let's see what you've got!", - "2": "Hmm... You're more powerful than you look. I wonder how much energy there is inside you.", - "2_female": "Hmm... You're more powerful than you look. I wonder how much energy there is inside you.", - "3": "I've been waiting for you! I need to do a little research on you! Come, let us begin!" + "1": "オマエを 倒せば ワタシの 科学力の\nすごさを 証明できるゾ! よし いくのだ!", + "1_female": "オマエを 倒せば ワタシの 科学力の\nすごさを 証明できるゾ! よし いくのだ!", + "2": "なるほど ジャマを したのは オマエだったのか!\nわかったゾ! よーし! オマエで 実験だゾ!", + "2_female": "なるほど ジャマを したのは オマエだったのか!\nわかったゾ! よーし! オマエで 実験だゾ!", + "3": "おおー ウワサの オマエか 待っていたゾ!\nオマエを 調べる ほら 始めるゾ!" }, "victory": { - "1": "Ah, you're quite strong. Oh yes—very strong, indeed.", - "2": "Ding-ding-ding! You did it! To the victor go the spoils!", - "2_female": "Ding-ding-ding! You did it! To the victor go the spoils!", - "3": "Wonderful! Amazing! You have tremendous skill and bravery!" + "1": "グヌウウ…… なぜだ……\nなぜ こんなことが 起こるのだ……?", + "2": "なんだと! オマエ すごいゾ!\nオマエの ポケモン すごいゾ!", + "2_female": "なんだと! オマエ すごいゾ!\nオマエの ポケモン すごいゾ!", + "3": "すごいな オマエ! すごいぞ オマエ!\nワタシは オマエを 認める イコール 金を あげる!" } }, "bryony": { "encounter": { - "1": "I am Bryony, and it would be my pleasure to battle you. Show me what you've got.", - "2": "Impressive... You're more powerful than you appear. Let's see the true extent of your energy.", - "2_female": "Impressive... You're more powerful than you appear. Let's see the true extent of your energy.", - "3": "I've anticipated your arrival. It's time for a little test. Shall we begin?" + "1": "一人で やっつける。\n勝てる 確率を あげる 必要ない。", + "2": "どこかで 見た 顔?\nわかんないけど フレア団 じゃないし やっつけようよ…", + "2_female": "どこかで 見た 顔?\nわかんないけど フレア団 じゃないし やっつけようよ…", + "3": "あら? あら?" }, "victory": { - "1": "You're quite strong. Oh yes—very strong, indeed.", - "2": "Ding-ding-ding! You've done well. Victory is yours.", - "3": "Wonderful! Remarkable! Your skill and bravery are commendable." + "1": "確率は あくまでも 確率。\n絶対では ないのよね……", + "2": "確率を 無視する トレーナー\nあなたの パワーの 源は?", + "3": "あらま! こいつめ!\nわたしが かわいそうでしょ!" } }, "rocket_grunt": { "encounter": { - "1": "Prepare for trouble!", - "2": "We're pulling a big job here! Get lost, kid!", - "2_female": "We're pulling a big job here! Get lost, kid!", - "3": "Hand over your Pokémon, or face the wrath of Team Rocket!", - "4": "You're about to experience the true terror of Team Rocket!", - "5": "Hey, kid! Me am a Team Rocket member kind of guy!", - "5_female": "Hey, kid! Me am a Team Rocket member kind of guy!" + "1": "なんだかんだと 聞かれたら\n答えてあげるのが 世の情け!", + "2": "おれ達は 大事な 仕事を してるんだ! お家へ 帰りな!", + "2_female": "おれ達は 大事な 仕事を してるんだ! お家へ 帰りな!", + "3": "ロケット団の 恐ろしさを 知りたくないなら\nおまえの ポケモンを よこせ!", + "4": "ロケット団の 本当の 恐ろしさ\nおまえに 教えて 差しあげよう!", + "5": "ロケット団に ガイコクジン\nワタシ オンリーワン だけど……\n$しかーし! そんなコト ノー カンケー!", + "5_female": "ロケット団に ガイコクジン\nワタシ オンリーワン だけど……\n$しかーし! そんなコト ノー カンケー!" }, "victory": { - "1": "Team Rocket blasting off again!", - "2": "Oh no! I dropped the Lift Key!", - "3": "I blew it!", - "4": "My associates won't stand for this!", - "5": "You say what? Team Rocket bye-bye a go-go? Broken it is says you?" + "1": "やな感じ~!", + "2": "しまった…! せっかく 隠して置いた エレベータの カギが…!", + "3": "しくじったか!", + "4": "くそ! 仲間が 黙っちゃ いねえぞ!", + "5": "オー ノー! キャント ビリーブ!\nユーは ブルーベリー ストロベリー!\n$……ソーリー ミステイク!\nユーは ベリー ストロング!\n$ティース キャント スタンダップ!\n歯が 立ちませーん!" } }, "magma_grunt": { "encounter": { - "1": "If you get in the way of Team Magma, don’t expect any mercy!", - "2": "You'd better not interfere with our plans! We're making the world a better place!", - "3": "You're in the way! Team Magma has no time for kids like you!", - "4": "I hope you brought marshmallows because things are about to heat up!", - "5": "We're going to use the power of a volcano! It's gonna be... explosive! Get it? Heh heh!" + "1": "おれたち マグマ団の 邪魔を するなら\n容赦は しないぜ!", + "2": "すべての 人々の ために\n我々 マグマ団は あるのよ!", + "3": "邪魔だ! マグマ団は お前の ような ヤツに\n用が ねーんだ! ほら さっさと 帰れよ!", + "4": "マシュマロ 持ってきた?\nすぐ 炎は 燃え上がっちゃう からー!!", + "5": "燃料の 力を 利用して 火山を 噴火させて やるのさ! ズドドーン とな!" }, "victory": { - "1": "Huh? I lost?!", - "2": "I can't believe I lost! I even skipped lunch for this", - "3": "No way! You're just a kid!", - "3_female": "No way! You're just a kid!", - "4": "Urrrgh... I should've ducked into our hideout right away...", - "5": "You beat me... Do you think the boss will dock my pay for this?" + "1": "ぬぬぬー 敗北 するなどー!?", + "2": "負けちゃった! せっかくの 昼ご飯を 抜いたのにー", + "3": "ガキの くせに この 強さ だと!?", + "3_female": "ガキの くせに この 強さ だと!?", + "4": "うぐぐ‥‥ 早く アジトに 逃げ込めば 良かった……", + "5": "負けた… そんなことじゃ ボーナスが 減るぜ?" } }, "aqua_grunt": { "encounter": { - "1": "No one who crosses Team Aqua gets any mercy, not even kids!", - "2": "Grrr... You've got some nerve meddling with Team Aqua!", - "3": "You're about to get soaked! And not just from my water Pokémon!", - "4": "We, Team Aqua, exist for the good of all!", - "5": "Prepare to be washed away by the tides of my... uh, Pokémon! Yeah, my Pokémon!" + "1": "おれら アクア団の ジャマするつもりかい?\nガキでも 容赦せーへんで!", + "2": "うむむ…… アクア団に 逆らうと いい度胸 だろ!", + "3": "おれの みずポケモンが おまれを ずぶ濡れに しちゃうぞ!", + "4": "活動を 邪魔する つもりなら ぶっ壊す!\nこの世界を より良い 場所に してるぞ!", + "5": "俺の ポケモンの 水の に 押し流されちゃうぞ!" }, "victory": { - "1": "You're kidding me!", - "2": "Arrgh, I didn't count on being meddled with by some meddling kid!", - "3": "I lost?! Guess I'll have to swim back to the hideout now...", - "4": "Oh, man, what a disaster... The boss is going to be furious...", - "5": "You beat me... Do you think the boss will make me walk the plank for this?" + "1": "冗談じゃねえ?!", + "2": "うむ‥‥ まさか ヤツに 邪魔 されるなんて\nこれっぽっちも おもってなかったぜ!", + "3": "チクショウ… アジトへ 泳ぎ帰らなくちゃ……", + "4": "……やばい! このまま じゃあ\nリーダーに 怒られちまうぞ……", + "5": "倒された… 板歩きの刑に なっちゃうかも……" } }, "galactic_grunt": { "encounter": { - "1": "Don't mess with Team Galactic!", - "2": "Witness the power of our technology and the future we envision!", - "3": "In the name of Team Galactic, I'll eliminate anyone who stands in our way!", - "4": "Get ready to lose!", - "5": "Hope you're ready for a cosmic beatdown!", - "5_female": "Hope you're ready for a cosmic beatdown!" + "1": "ギンガ団に 逆らうな!", + "2": "我らの 技術の 力と\n目論む 未来 を目に当たりに しちゃえ!", + "3": "ギンガ団にかわって 邪魔する 誰もを 排除するぞ!", + "4": "負ける 準備 いいかい!?", + "5": "コスミックに 破壊しちゃうぞ!\n覚悟せよ!", + "5_female": "コスミックに 破壊しちゃうぞ!\n覚悟せよ!" }, "victory": { - "1": "Shut down...", - "2": "This setback means nothing in the grand scheme.", - "3": "Our plans are bigger than this defeat.", - "4": "How?!", - "5": "Note to self: practice Pokémon battling, ASAP." + "1": "墜落……", + "2": "この負けは 次のカケで\n取り返せば いいさ!", + "3": "かまわない!\n我らの 目的の方は この負けより 大きい!", + "4": "ホワイ?! なぜ?!", + "5": "自分への覚え書き:\nポケモン勝負練習 なる早" } }, "plasma_grunt": { "encounter": { - "1": "We won't tolerate people who have different ideas!", - "2": "If I win against you, release your Pokémon!", - "3": "If you get in the way of Team Plasma, I'll take care of you!", - "4": "Team Plasma will liberate Pokémon from selfish humans like you!", - "5": "Our hairstyles are out of this world... but our battling skills? You'll find out soon enough." + "1": "あたしたちと 違う\n考えの 持ち主は 許さない!", + "2": "オレが 勝てば\nおまえの ポケモンを 解き放て!", + "3": "プラズマ団の ジャマをするなら\nアタシが やっつけてやる!!", + "4": "プラズマ団は お前のような 身勝手な\n人間から 開放する!", + "5": "おまえの ような トレーナーが\nポケモンを 苦しめているのだ!" }, "victory": { - "1": "Plasmaaaaaaaaa!", - "2": "How could I lose...", - "3": "...What a weak Pokémon, I'll just have to go steal some better ones!", - "4": "Great plans are always interrupted.", - "5": "This is bad... Badbadbadbadbadbadbad! Bad for Team Plasma! Or Plasbad, for short!" + "1": "プラズマーーーーー!!!", + "2": "なんてこと……! おれが 負けるなど!", + "3": "……弱い ポケモンね\n別の ポケモンを 奪わなきゃ!!", + "4": "すばらしい 計画に\nジャマは 付き物 ね!", + "5": "マズイ……\nマズイマズイマズイマズイマズイマズイ\n$プラズマ団と して マズイ\n縮めて プラズマズイ!" } }, "flare_grunt": { "encounter": { - "1": "Your Pokémon are no match for the elegance of Team Flare.", - "2": "Hope you brought your sunglasses, because things are about to get bright!", - "2_female": "Hope you brought your sunglasses, because things are about to get bright!", - "3": "Team Flare will cleanse the world of imperfection!", - "4": "Prepare to face the brilliance of Team Flare!", - "5": "Fashion is most important to us!" + "1": "お前の ポケモンが フレア団の 優美さには 敵わない!", + "2": "この サングラス いいだろう?\n羨ましいだろ? でも あげない!", + "2_female": "この サングラス いいだろう?\n羨ましいだろ? でも あげない!", + "3": "無風流は 許さない フレア団が お前を 打ち倒す!", + "4": "フレア団の 眩しさは お前を 圧倒するぞ!", + "5": "おれら 泣く子も 黙る\nオシャレチーム フレア団!" }, "victory": { - "1": "The future doesn't look bright for me.", - "2": "Perhaps there's more to battling than I thought. Back to the drawing board.", - "3": "Gahh?! I lost?!", - "4": "Even in defeat, Team Flare's elegance shines through.", - "5": "You may have beaten me, but when I lose, I go out in style!" + "1": "敗北でも フレア団が 優美さが 華やかに 輝く!", + "2": "目の前が 真っ暗……\nあっ サングラス だからか?", + "3": "ぐあああ! 負けた! シャレにならない!", + "4": "やっぱり バトルには\nオシャレより 大事なことが あるかも…", + "5": "ちっ! フレア団 御用達の\nオシャレスーツが 汚れたぜ!" } }, "aether_grunt": { "encounter": { - "1": "I'll fight you with all I have to wipe you out!", - "2": "I don't care if you're a kid or what. I'll send you flying if you threaten us!", - "2_female": "I don't care if you're a kid or what. I'll send you flying if you threaten us!", - "3": "I was told to turn away Trainers, whomever they might be!", - "4": "I'll show you the power of Aether Paradise!", - "5": "Now that you've learned of the darkness at the heart of Aether Paradise, we'll need you to conveniently disappear!" + "1": "侵入者 発見!\nシークレットラボを 守ります!", + "2": "大事な 研究なのよ!\n子供とはいえ ぶっとばすわよ!", + "2_female": "大事な 研究なのよ!\n子供とはいえ ぶっとばすわよ!", + "3": "どんな トレーナーだろうと\n追い返すよう いわれてるのよ!", + "4": "エーテルパラダイスの\n開発力を みせてやろう!", + "5": "エーテルパラダイスの 闇を 知った\nおまえには 消えてもらうぜ!" }, "victory": { - "1": "Hmph! You seem to have a lot of skill.", - "2": "What does this mean? What does this mean!", - "3": "Hey! You're so strong that there's no way I can turn you away!", - "4": "Hmm... It seems as though I may have lost.", - "5": "Here's an impression for you: Aiyee!" + "1": "ふむう…… どうやら\nわたしは 負けたようですな。", + "2": "どういう ことだ……!\nどういう ことだ……?", + "3": "ちょっと! 強すぎて\n追い返すなんて ムリムリ!", + "4": "ポケモン勝負に 関する\n発見は してなかった!", + "5": "真似して ぎゃひーん!" } }, "faba": { "encounter": { - "1": "I, Branch Chief Faba, shall show you the harshness of the real world!", - "2": "The man who is called Aether Paradise's last line of defense is to battle a mere child?", - "2_female": "The man who is called Aether Paradise's last line of defense is to battle a mere child?", - "3": "I, Faba, am the Aether Branch Chief. The only one in the world, I'm irreplaceable." + "1": "支部長 ザオボーは あなたに 現実を みせて さしあげますよ!", + "2": "ザオボー 人呼んで エーテルパラダイス\n最後の 最後の 最後の 砦は あなたを 壊します!", + "2_female": "ザオボー 人呼んで エーテルパラダイス\n最後の 最後の 最後の 砦は あなたを 壊します!", + "3": "エーテル財団の 支部長 といえば\n世界に ただ一人…… この ザオボーだけで ございます。" }, "victory": { - "1": "Aiyee!", - "2": "H-h-how can this be?! How could this child...", - "2_female": "H-h-how can this be?! How could this child...", - "3": "This is why... This is why I can't bring myself to like children." + "1": "ぎゃひーん!!!", + "2": "な ななな なんということでしょう?\nこの わたしが お子さま相手に……", + "2_female": "な ななな なんということでしょう?\nこの わたしが お子さま相手に……", + "3": "なんということでしょう!?" } }, "skull_grunt": { "encounter": { - "1": "We're not bad-we're just hard!", - "2": "You want some? That's how we say hello! Nice knowing you, punks!", - "2_female": "You want some? That's how we say hello! Nice knowing you, punks!", - "3": "We're just a bunch of guys and gals with a great interest in other people's Pokémon!", - "4": "Why you trying to act hard when we're already hard as bones out here, homie?", - "4_female": "Why you trying to act hard when we're already hard as bones out here, homie?", - "5": "Team Skull represent! We can't pay the rent! Had a lot of fun, but our youth was misspent!", - "5_female": "Team Skull represent! We can't pay the rent! Had a lot of fun, but our youth was misspent!" + "1": "まじめが キライでよ!\n$スカル団 やっているのに\nまじめに 下っ端 してるぜ?", + "2": "なんだ とは ご挨拶 ジャン!\nこいつら まとめて しめちゃおうよ!", + "2_female": "なんだ とは ご挨拶 ジャン!\nこいつら まとめて しめちゃおうよ!", + "3": "オレが ポケモンを 使いこなす\nすごいとこ みせてやりまスカ!", + "4": "あんたが ホネ身を 惜しまないかを 確かめたいから バトルッスカ!", + "4_female": "あんたが ホネ身を 惜しまないかを 確かめたいから バトルッスカ!", + "5": "だからYo♪ きけYo♪\n侵入者さんYo♪\n$オレら レペゼン サボる♪ スカル♪\nすこぶる♪ そそる♪ 話するYo♪", + "5_female": "だからYo♪ きけYo♪\n侵入者さんYo♪\n$オレら レペゼン サボる♪ スカル♪\nすこぶる♪ そそる♪ 話するYo♪" }, "victory": { - "1": "Huh? Is it over already?", - "2": "Time for us to break out, yo! Gotta tell y'all peace out, yo!", - "3": "We don't need your wack Pokémon anyway!", - "4": "Wha-?! This kid's way too strong-no bones about it!", - "5": "So, what? I'm lower than a Pokémon?! I already got self-esteem issues, man." + "1": "ちっ! まじめ だから\n……応援してやるよ\n$ヨーヨー 止まるなよ\n負けてもいいから 止まるなよ!", + "2": "しめるなよ! 袋叩き するなよ!", + "3": "オマエが ポケモンを 使いこなす\nすごいとこ みせられたのでスカ!?", + "4": "あんたらの ハート ホネ身に しみちゃう……!", + "5": "そーかYo♪\n後悔すんじゃねーYo♪" } }, "plumeria": { "encounter": { - "1": " ...Hmph. You don't look like anything special to me.", - "1_female": " ...Hmph. You don't look like anything special to me.", - "2": "It takes these dumb Grunts way too long to deal with you kids...", - "3": "Mess with anyone in Team Skull, and I'll show you how serious I can get." + "1": "あんたね…… さっき 聞いたの\n$……なんにも 感じない\nふつーのコに みえるけどねえ", + "1_female": " .あんたね…… さっき 聞いたの\n$……なんにも 感じない\nふつーのコに みえるけどねえ", + "2": "下っ端 あんたのような\n雑魚相手に モタモタ してるからさ", + "3": "スカル団を 束ねている……\n言うなれば あねごって ところ。\n$かわいい あいつらを\nいじめる あんたが ジャマなのよ!" }, "victory": { - "1": "Hmmph! You're pretty strong. I'll give you that.", - "1_female": "Hmmph! You're pretty strong. I'll give you that.", - "2": "Hmmph. Guess you are pretty tough. Now I understand why my Grunts waste so much time battling kids.", - "3": "Hmmph! I guess I just have to hold that loss." + "1": "ハンッ! たいした もんだよ\nただし 次 ジャマしたら 本気で やっちまうから", + "1_female": "ハンッ! たいした もんだよ\nただし 次 ジャマしたら 本気で やっちまうから", + "2": "あんた たいした もんだよ\nま 雑魚相手に 手間取るのも わかる 強さか。", + "3": "……チッ" } }, "macro_grunt": { "encounter": { - "1": "It looks like this is the end of the line for you!", - "2": "You are a trainer aren't you? I'm afraid that doesn't give you the right to interfere in our work.", - "2_female": "You are a trainer aren't you? I'm afraid that doesn't give you the right to interfere in our work.", - "3": "I'm from Macro Cosmos Insurance! Do you have a life insurance policy?" + "1": "不審者を 追い払って\nたんまり ボーナス いただくぜ!", + "2": "ジャマは 許しません!\n$マクロコスモスの さまざまな\n関連会社を 守るためにも 追い返します!", + "2_female": "ジャマは 許しません!\n$マクロコスモスの さまざまな\n関連会社を 守るためにも 追い返します!", + "3": "マクロコスモス生命です!\n保険に 入っていますか?", + "4": "見つけた! では 勝負だ!", + "5": "オリーヴさまに 怒られたくないから あきらめない!" }, "victory": { - "1": "I have little choice but to respectfully retreat.", - "2": "Having to give up my pocket money... Losing means I'm back in the red...", - "3": "Nobody can beat Macro Cosmos when it comes to our dedication to our work!" + "1": "ボーナスが……\n夢の マイホームが……", + "2": "負けたからには\n素直に 引き下がりましょう。\n$だが ローズ委員長の\nジャマは しないでくださいよ。", + "3": "マクロコスモス生命の 仕事なら\n誰にも 負けないのに…", + "4": "ポケモンを 入れ替えたのに……", + "5": "かくれんぼも ダメ!\n勝負も ダメ! 逃げるしかない!" } }, "oleana": { "encounter": { - "1": "I won't let anyone interfere with Mr. Rose's plan!", - "2": "So, you got through all of the special staff that I had ordered to stop you. I would expect nothing less.", - "3": "For the chairman! I won't lose!" + "1": "ローズ様の ジャマ だなんて\nわたくし 絶対に 許せません!", + "2": "わたくしの オーダーを こなす 特別な\nスタッフ達を ものともせずに やってくるなんて……", + "3": "あなたを ボコボコに すれば\n委員長の 計画が すらっと 進めます!" }, "victory": { - "1": "*sigh* I wasn't able to win... Oleana...you really are a hopeless woman.", - "2": "Arghhh! This is inexcusable... What was I thinking... Any trainer who's made it this far would be no pushover..", - "2_female": "Arghhh! This is inexcusable... What was I thinking... Any trainer who's made it this far would be no pushover..", - "3": "*sigh* I am one tired Oleana..." + "1": "はあああぁ 勝てないなんて……\nオリーヴ…… ほんとに ダメな子", + "2": "はああ……! なんてこと……\n$勝ちあがった トレーナーの\n実力を みくびっていました……", + "2_female": "はああ……! なんてこと……\n$勝ちあがった トレーナーの\n実力を みくびっていました……", + "3": "まあ 生意気!\nオリーヴの パートナーを キズつけるなんて!" + } + }, + "star_grunt": { + "encounter": { + "1": "We're Team Star, kid. We burn so bright, it hurts to look at us!", + "2": "We'll come at you full force - Hasta la vistaaar! ★", + "3": "If you don't clear out real quick-like, I'll hafta come at you in self-defense. You get me?", + "4": "Sorry, but if you don't turn yourself around here, amigo, we'll have to send you packing!", + "4_female": "Sorry, but if you don't turn yourself around here, amiga, we'll have to send you packing!", + "5": "Oh great. Here comes another rando to ruin my day." + }, + "victory": { + "1": "How come I'M the one seeing stars?!", + "2": "You're scary, kid. If you joined Team Star, you'd be looking down from the top in no time!", + "3": "I defended myself all right... But it wasn't enough!", + "4": "H-hasta la vistar... ★", + "5": "I didn't think grunt work for Team Star newbies would be this much of a chore..." + } + }, + "giacomo": { + "encounter": { + "1": "You don't really think things through, do ya? Declarin' war on Team Star is a real bad move.", + "2": "I'll play you a sick requiem as you crash and burn. Let's get this party staaarteeed!" + }, + "victory": { + "1": "Guess that's that...", + "2": "You turned my melody into a threnody..." + } + }, + "mela": { + "encounter": { + "1": "So you're the dope who picked a fight with Team Star... Prepare to get messed up.", + "2": "All riiight, BRING IT! I'll blow everythin' sky high!" + }, + "victory": { + "1": "Ugh. Is this really how it's gonna end? What a hassle...", + "2": "I burned through everythin' I had...and now I've sputtered out." + } + }, + "atticus": { + "encounter": { + "1": "You have some nerve baring your fangs at Team Star. Come, then, villainous wretch!", + "2": "Be warned—I shall spare thee no mercy! En garde!" + }, + "victory": { + "1": "Forgive me, my friends...", + "2": "You have utterly bested me. But thy victory stir'd no bitterness within me—such was its brilliance." + } + }, + "ortega": { + "encounter": { + "1": "I promise I'll play nice, so don't blame me when this battle sends you blubbering back home!", + "2": "I'll wipe that smug look off your face for sure! You're going down!" + }, + "victory": { + "1": "Ugh! How could I LOSE! What the HECK!", + "2": "Arrrrgggh! That strength of yours is SO. NOT. FAIR." + } + }, + "eri": { + "encounter": { + "1": "Doesn't matter who you are. I'll bury anyone who tries to take down Team Star!", + "2": "I give as good as I get—that's a promise! We'll see who's left standing in the end!" + }, + "victory": { + "1": "I'm so sorry, everyone...", + "2": "I gave it my all, but it wasn't enough—I wasn't enough..." } }, "rocket_boss_giovanni_1": { "encounter": { - "1": "So! I must say, I am impressed you got here!" + "1": "こんな所 まで よく来た…" }, "victory": { - "1": "WHAT! This cannot be!" + "1": "ぐ ぐーッ! そんな ばかなーッ!" }, "defeat": { - "1": "Mark my words. Not being able to measure your own strength shows that you are still a child.", - "1_female": "Mark my words. Not being able to measure your own strength shows that you are still a child." + "1": "自分の 力を 把握できない 内は\nまだ 子供 ということだ…… 覚えておくがいい……", + "1_female": "自分の 力を 把握できない 内は\nまだ 子供 ということだ…… 覚えておくがいい……" } }, "rocket_boss_giovanni_2": { "encounter": { - "1": "My old associates need me... Are you going to get in my way?" + "1": "かつての 仲間たちが 私を 必要としてる…… 先の 失敗は もう 二度と 繰り返さない! " }, "victory": { - "1": "How is this possible...? The precious dream of Team Rocket has become little more than an illusion..." + "1": "なっ なぜだ……!\nロケット団 最高の 夢が 幻となって 消えていく……" }, "defeat": { - "1": "Team Rocket will be reborn again, and I will rule the world!" + "1": "ロケット団は 生まれ変わり\n世界を 我が物に するのだ!" } }, "magma_boss_maxie_1": { "encounter": { - "1": "I will bury you by my own hand. I hope you appreciate this honor!" + "1": "私 自らの 手で 葬ってやる……\n光栄に 思うが よい!" }, "victory": { - "1": "Ugh! You are... quite capable...\nI fell behind, but only by an inch..." + "1": "グッ…… やりおる……!\nわずか 1ミリ 及ばぬか……!" }, "defeat": { - "1": "Team Magma will prevail!" + "1": "マグマ団の 活動を 止めることなど 誰にも できぬ!" } }, "magma_boss_maxie_2": { "encounter": { - "1": "You are the final obstacle remaining between me and my goals.\n$Brace yourself for my ultimate attack! Fuhahaha!" + "1": "……キサマは 私が 図った日を\n迎えるための 最後の カベ―――\n$この マツブサが 治めし すべての チカラを もって 排除してやろう……!" }, "victory": { - "1": "This... This is not.. Ngh..." + "1": "こん…な……" }, "defeat": { - "1": "And now... I will transform this planet to a land ideal for humanity." + "1": "そして……\n$この世界は 人類にとって 理想の……" } }, "aqua_boss_archie_1": { "encounter": { - "1": "I'm the leader of Team Aqua, so I'm afraid it's the rope's end for you." + "1": "アクア団 リーダーとして テメエの ポケモン\nもろとも バッキバキに 揉み潰して やるよ!" }, "victory": { - "1": "Let's meet again somewhere. I'll be sure to remember that face." + "1": "んじゃあ またな\n……そのツラ 忘れねえぜ" }, "defeat": { - "1": "Brilliant! My team won't hold back now!" + "1": "おもしれえッ!\n今は アクア団が 全開を!" } }, "aqua_boss_archie_2": { "encounter": { - "1": "I've been waiting so long for this day to come.\nThis is the true power of my team!" + "1": "前回の勝負 じゃあ 見せられなかった\nポケモン達と オレの 全開パワー……\n$たーんと 食らわせてやるぜえああああッ!" }, "victory": { - "1": "Like I figured..." + "1": "……流石…だな…ッ!" }, "defeat": { - "1": "I'll return everything in this world to its original, pure state!!" + "1": "オレは この日が 来るのを 長い間 待っていた‥‥\n今は この世界を あるがままの 姿にッ!" } }, "galactic_boss_cyrus_1": { "encounter": { - "1": "You were compelled to come here by such vacuous sentimentality.\n$I will make you regret paying heed to your heart!", - "1_female": "You were compelled to come here by such vacuous sentimentality.\n$I will make you regret paying heed to your heart!" + "1": "心いう 不完全なものが 感じる\n哀れみや 優しさ……\n$そんな 曖昧なものに 突き動かされ \nここに来たことを わたしが 公開させてあげよう。", + "1_female": "心いう 不完全なものが 感じる\n哀れみや 優しさ……\n$そんな 曖昧なものに 突き動かされ \nここに来たことを わたしが 公開させてあげよう" }, "victory": { - "1": "Interesting. And quite curious." + "1": "面白い\nそして 興味深い" }, "defeat": { - "1": "I will create my new world..." + "1": "まさに 新しい ギンガの! 宇宙の 誕生だ!" } }, "galactic_boss_cyrus_2": { "encounter": { - "1": "So we meet again. It seems our fates have become intertwined.\n$But here and now, I will finally break that bond!" + "1": "またキミか。\n$キミとは ほとほと 縁が あるね…\n$腐れ縁 といっても いいが\n今 ここで 断ち切ろう!" }, "victory": { - "1": "How? How? HOW?!" + "1": "まさか まさか まさかッ!" }, "defeat": { - "1": "Farewell." + "1": "さらばだ。" } }, "plasma_boss_ghetsis_1": { @@ -985,6 +1056,28 @@ "1": "I suppose it must seem that I am doing something terrible. I don't expect you to understand.\n$But I must provide the Galar region with limitless energy to ensure everlasting prosperity." } }, + "star_boss_penny_1": { + "encounter": { + "1": "I'm the big boss of Team Star. The name's Cassiopeia. \n$Now, bow down before the overwhelming might of Team Star's founder!" + }, + "victory": { + "1": "... ... .." + }, + "defeat": { + "1": "Heh..." + } + }, + "star_boss_penny_2": { + "encounter": { + "1": "I won't hold back in this battle! I'll stay true to Team Star's code! \n$My Veevee power will crush you into stardust!" + }, + "victory": { + "1": "...It's all over now." + }, + "defeat": { + "1": "I can't fault you on your battle skills at all... Considering how the bosses fell at your hands." + } + }, "brock": { "encounter": { "1": "My expertise on Rock-type Pokémon will take you down! Come on!", @@ -2718,7 +2811,7 @@ }, "rival": { "encounter": { - "1": "@c{smile}あっ、ここに いたんだ! 旅に 出る前に 「じゃ またね!」って くらい 聞きたかったよ……$@c{smile_eclosed}やっぱり 夢を 追ってこうと しているんだ? 信じられない ほどね……$@c{serious_smile_fists}じゃあ、 ここまで 来たから バトルしよっか? 覚悟してるかを 確かめたい から!$@c{serious_mopen_fists}遠慮せずに 全力で かかってこいぜ!" + "1": "@c{smile}あっ、ここに いたんだ! 旅に 出る前に\n「じゃ またね!」って くらい 聞きたかったよ……$@c{smile_eclosed}やっぱり 夢を 追ってこうと しているんだ?\n信じられない ほどね……$@c{serious_smile_fists}じゃあ、 ここまで 来たから バトルしよっか?\n覚悟してるかを 確かめたい から!$@c{serious_mopen_fists}遠慮せずに 全力で かかってこいぜ!" }, "victory": { "1": "@c{shock}ウワッ、カンゼンに ぶっ壊したぜ。\n初心者だとは 思えないほど……$@c{smile}たぶん 運が良っかった だけが……\n最後まで 行ける素質が あるかもな!$こっちの アイテムを あげよう、 博士に そう言いつけたから。 結構 スゴそうな もんだ!$@c{serious_smile_fists}ここからも ガンバレ!" @@ -2734,7 +2827,7 @@ }, "rival_2": { "encounter": { - "1": "@c{smile}おや、なんと グウゼン。\n@c{smile_eclosed}今までも パーフェクトに 勝った ようだな……\n$@c{serious_mopen_fists}なんか 忍び寄った みたいだとは 分かるけど、 そんなことない… ほとんどはな。\n$@c{serious_smile_fists}ぶっちゃけ言うと、 オレが 負けた時から 再戦したくて ウズウズしてたぜ。\n$張り切って 特訓したから 今は ちゃんと 勢い 見せるんだ。\n$@c{serious_mopen_fists}今回も 遠慮しな!\n行こうぜ!" + "1": "@c{smile}おや、なんと グウゼン。\n@c{smile_eclosed}今までも パーフェクトに 勝った ようだな……\n$@c{serious_mopen_fists}なんか 忍び寄った みたいだとは 分かるけど、 そんなことない… ほとんどはな。\n$@c{serious_smile_fists}ぶっちゃけ言うと、 おれが 負けた時から 再戦したくて ウズウズしてたぜ。\n$張り切って 特訓したから 今は ちゃんと 勢い 見せるんだ。\n$@c{serious_mopen_fists}今回も 遠慮しな!\n行こうぜ!" }, "victory": { "1": "@c{neutral_eclosed}あ。 自信過剰かも。\n$@c{smile}いいけどさ、 こうなるのを 見込んだから。\n@c{serious_mopen_fists}次回まで もっと頑張らなくちゃ ってことだよな!\n\n$@c{smile}きっと 助け 要らないんだが、 もう一つの アイテムが 欲しいかと 思ったから あげるぜ。\n\n$@c{serious_smile_fists}でも これで ラストだ!\n相手に 利点を あげ続けると 行けないんだろう!" @@ -2753,10 +2846,10 @@ }, "rival_3": { "encounter": { - "1": "@c{smile}Hey, look who it is! It's been a while.\n@c{neutral}You're… still undefeated? Huh.\n$@c{neutral_eclosed}Things have been kind of… strange.\nIt's not the same back home without you.\n$@c{serious}I know it's selfish, but I need to get this off my chest.\n@c{neutral_eclosed}I think you're in over your head here.\n$@c{serious}Never losing once is just unrealistic.\nWe need to lose sometimes in order to grow.\n$@c{neutral_eclosed}You've had a great run but there's still so much ahead, and it only gets harder. @c{neutral}Are you prepared for that?\n$@c{serious_mopen_fists}If so, prove it to me." + "1": "@c{smile}おお 誰かと思ったら な! 久しぶり!\n@c{neutral}もう… 倒されなかった か? フン\n$@c{neutral_eclosed}最近 なんか… 変な 気分だな。\nキミが いないと ふるさとは 同じ場所 じゃない。\n$@c{serious}わがまま かもしれないが、本音 明かさなくちゃ。\n@c{neutral_eclosed}このままで すぐ キミの 手に 負えなくなる。\n$@c{serious}一回も 負けないこと って むちゃくちゃだろう。\nみんなは 時々 失敗しなくちゃ。 そうでなけりゃ 成長できない。\n$@c{neutral_eclosed}ここまで よく やって来たが、\nまだまだ 先が 辛いこと ばかり。@c{neutral}覚悟してるか?\n$@c{serious_mopen_fists}それなら、見せてくれ。" }, "victory": { - "1": "@c{angry_mhalf}This is ridiculous… I've hardly stopped training…\nHow are we still so far apart?" + "1": "@c{angry_mhalf}こりゃ むっちゃ だろ… 訓練しか してないよ…\nなぜ 力が もう こんなに 違うなだ?" } }, "rival_3_female": { @@ -2772,10 +2865,10 @@ }, "rival_4": { "encounter": { - "1": "@c{neutral}Hey.\n$I won't mince words or pleasantries with you.\n@c{neutral_eclosed}I'm here to win, plain and simple.\n$@c{serious_mhalf_fists}I've learned to maximize my potential by putting all my time into training.\n$@c{smile}You get a lot of extra time when you cut out the unnecessary sleep and social interaction.\n$@c{serious_mopen_fists}None of that matters anymore, not until I win.\n$@c{neutral_eclosed}I've even reached the point where I don't lose anymore.\n@c{smile_eclosed}I suppose your philosophy wasn't so wrong after all.\n$@c{angry_mhalf}Losing is for the weak, and I'm not weak anymore.\n$@c{serious_mopen_fists}Prepare yourself." + "1": "@c{neutral}よっ。\n$歯に 衣着せない。\n@c{neutral_eclosed}キミに 勝つために ここに 来た、それだけ。\n$@c{serious_mhalf_fists}地力を 最大限に 引き出す ために\n全労力を 費やして 訓練していた。\n$@c{smile}不要な 睡眠や 人間関係なんか 抜くと\n訓練の 時間は 割と 増えるね。\n$@c{serious_mopen_fists}そんなことは 勝てるときまで 全然 どうでもない。\n$@c{neutral_eclosed}今 負けられないとこまで やって来た。\n@c{smile_eclosed}キミの 考え方は 違いない ようだね。\n$@c{angry_mhalf}負けるのは 弱き者。 おれは もう 弱くない。\n$@c{serious_mopen_fists}覚悟せよ。" }, "victory": { - "1": "@c{neutral}What…@d{64} What are you?" + "1": "@c{neutral}一体…@d{64} 何モノか……?" } }, "rival_4_female": { @@ -2783,7 +2876,7 @@ "1": "@c{neutral}アタシよ! また 忘れちゃった… のね?\n$@c{smile}こんな 遠くまで 来たのは 鼻が高いことだよ! おめでと~\nしかし、 ここは 終着点だね。\n$@c{smile_eclosed}アタシの 中にある 全然 知らなかった 部分を 目覚めたよ。\n今は、 トレーニングしか してないみたい。\n$@c{smile_ehalf}食べたり 寝たりも しなくて 朝から晩まで ポケモンを 育って、 毎日 昨日より 強くなってる。\n$@c{neutral}実は… もう 自分 認識できない。\n$結局、 峠を越して まるで カミに なった。\n今は 誰にも アタシを 倒せないと 思う。\n$ねえ、分かる? 全ては アンタの お陰で。\n@c{smile_ehalf}お礼を言うか アンタのこと嫌いか どうしたらいいの 分からない。\n$@c{angry_mopen}覚悟しなさい。" }, "victory": { - "1": "@c{neutral}一体…@d{64} 何モノか…?" + "1": "@c{neutral}一体…@d{64} 何モノか……?" }, "defeat": { "1": "$@c{smile}ここまで 頑張ってたのを 誇りに思ってね。" @@ -2810,10 +2903,10 @@ }, "rival_6": { "encounter": { - "1": "@c{smile_eclosed}We meet again.\n$@c{neutral}I've had some time to reflect on all this.\nThere's a reason this all seems so strange.\n$@c{neutral_eclosed}Your dream, my drive to beat you…\nIt's all a part of something greater.\n$@c{serious}This isn't about me, or about you… This is about the world, @c{serious_mhalf_fists}and it's my purpose to push you to your limits.\n$@c{neutral_eclosed}Whether I've fulfilled that purpose I can't say, but I've done everything in my power.\n$@c{neutral}This place we ended up in is terrifying… Yet somehow I feel unphased, like I've been here before.\n$@c{serious_mhalf_fists}You feel the same, don't you?\n$@c{serious}…and it's like something here is speaking to me.\nThis is all the world's known for a long time now.\n$Those times we cherished together that seem so recent are nothing but a distant memory.\n$@c{neutral_eclosed}Who can say whether they were ever even real in the first place.\n$@c{serious_mopen_fists}You need to keep pushing, because if you don't, it will never end. You're the only one who can do this.\n$@c{serious_smile_fists}I hardly know what any of this means, I just know that it's true.\n$@c{serious_mopen_fists}If you can't defeat me here and now, you won't stand a chance." + "1": "@c{smile_eclosed}また 会ったね。\n$@c{neutral}今までの ことを 振り返る 時間があった。\n全てが 変に感じる 訳が あるよ。\n$@c{neutral_eclosed}キミの夢、おれが キミに 倒したい熱心……\nより大きい 何かの 部分だけだ。\n$@c{serious}おれや キミの 物語じゃない。これは 全世界の物語だ。\n@c{serious_mhalf_fists}この物語で、 おれの「役割」 っていうのは キミを 限界まで 押すこと。\n$@c{neutral_eclosed}その役割を 果たしたのかは 言えないが、 全力を尽くした。\n$@c{neutral}最後に行き着いた この場所って 恐ろしいが……\n以前 ここに来た ことがある ような 気がして、 怯えもしない。\n$@c{serious_mhalf_fists}キミも 同じ 気がするん だろう?\n$@c{serious}……何かが おれに 話してるようだ。\n昔から これだけしかは この世界こそ そのもの。\n$大事にしてた 最近だと思ってた 一緒にいた日々、\n今は もう 遠い記憶 だけだ。\n$@c{neutral_eclosed}そもそも 現実だったかは もう 言えなくなったな。\n$@c{serious_mopen_fists}キミは 頑張り続かないと、\n決して 終わらない。 キミしか できやしない。\n$@c{serious_smile_fists}全ての意味、 全然 分からない。\nしかし、 真実だと 知ってるな。\n$@c{serious_mopen_fists}今ここで おれを 倒せなきゃ、 最後に 勝ち目が ナイ。" }, "victory": { - "1": "@c{smile_eclosed}It looks like my work is done here.\n$I want you to promise me one thing.\n@c{smile}After you heal the world, please come home." + "1": "@c{smile_eclosed}おれの 仕事が 終わったようだな。\n$一つだけの ことを 約束してほしい。\n@c{smile}この世界を 癒やした後、 お願い…… 帰ってくれ。" } }, "rival_6_female": { diff --git a/src/locales/ja/menu.json b/src/locales/ja/menu.json index f0914a7941c..b7db414493c 100644 --- a/src/locales/ja/menu.json +++ b/src/locales/ja/menu.json @@ -6,7 +6,7 @@ "newGame": "はじめから", "settings": "設定", "selectGameMode": "ゲームモードを 選んでください。", - "logInOrCreateAccount": "始めるには、ログイン、または 登録して ください。\nメールアドレスは 必要が ありません!", + "logInOrCreateAccount": "始めるには、ログイン、または 登録して ください。\nメールアドレスは 必要 ありません!", "username": "ユーザー名", "password": "パスワード", "login": "ログイン", @@ -14,7 +14,7 @@ "register": "登録", "emptyUsername": "ユーザー名を 空にする ことは できません", "invalidLoginUsername": "入力されたユーザー名は無効です", - "invalidRegisterUsername": "ユーザー名には 英文字、 数字、 アンダースコアのみを 含くむ必要が あります", + "invalidRegisterUsername": "ユーザー名には 英文字、 数字、 アンダースコアのみを 含くむことが 必要です", "invalidLoginPassword": "入力したパスワードは無効です", "invalidRegisterPassword": "パスワードは 6文字以上 でなければなりません", "usernameAlreadyUsed": "入力したユーザー名は すでに 使用されています", diff --git a/src/locales/ja/move.json b/src/locales/ja/move.json index 2e602407902..15c63a81e36 100644 --- a/src/locales/ja/move.json +++ b/src/locales/ja/move.json @@ -3129,7 +3129,7 @@ }, "auraWheel": { "name": "オーラぐるま", - "effect": "ほほぶくろに 溜めた エネルギーで 攻撃し 自分の 素早さを あげる。 モルペコの 姿で タイプが 変わる。" + "effect": "ほほぶくろに 溜めた エネルギーで 攻撃し 自分の 素早さを あげる。 モルペコが この技を 使う場合 姿で 技の タイプが 変わる。" }, "breakingSwipe": { "name": "ワイドブレイカー", diff --git a/src/locales/ja/pokemon-form.json b/src/locales/ja/pokemon-form.json index 76124904456..8ef61ec2c14 100644 --- a/src/locales/ja/pokemon-form.json +++ b/src/locales/ja/pokemon-form.json @@ -1,4 +1,5 @@ { + "pikachu": "通常", "pikachuCosplay": "コスプレ", "pikachuCoolCosplay": "クールなコスプレ", "pikachuBeautyCosplay": "きれいなコスプレ", @@ -6,7 +7,9 @@ "pikachuSmartCosplay": "かしこいコスプレ", "pikachuToughCosplay": "パワフルなコスプレ", "pikachuPartner": "パートナー", + "eevee": "通常", "eeveePartner": "パートナー", + "pichu": "通常", "pichuSpiky": "ギザみみ", "unownA": "A", "unownB": "B", @@ -36,36 +39,65 @@ "unownZ": "Z", "unownExclamation": "!", "unownQuestion": "?", + "castform": "ポワルンのすがた", "castformSunny": "たいよう", "castformRainy": "あまみず", "castformSnowy": "ゆきぐも", - "deoxysNormal": "ノーマル", - "burmyPlant": "くさき", - "burmySandy": "すなち", - "burmyTrash": "ゴミ", + "deoxysNormal": "ノーマルフォルム", + "deoxysAttack": "アタック", + "deoxysDefense": "ディフェンス", + "deoxysSpeed": "スピード", + "burmyPlant": "くさきのミノ", + "burmySandy": "すなちのミノ", + "burmyTrash": "ゴミのミノ", + "cherubiOvercast": "ネガフォルム", + "cherubiSunshine": "ポジフォルム", "shellosEast": "ひがし", "shellosWest": "にし", + "rotom": "通常", "rotomHeat": "ヒート", "rotomWash": "ウォッシュ", "rotomFrost": "フロスト", "rotomFan": "スピン", "rotomMow": "カット", - "giratinaAltered": "アナザー", - "shayminLand": "ランド", + "dialga": "通常", + "dialgaOrigin": "オリジンフォルム", + "palkia": "通常", + "palkiaOrigin": "オリジンフォルム", + "giratinaAltered": "アナザーフォルム", + "giratinaOrigin": "オリジンフォルム", + "shayminLand": "ランドフォルム", + "shayminSky": "スカイフォルム", "basculinRedStriped": "赤筋", "basculinBlueStriped": "青筋", "basculinWhiteStriped": "白筋", + "darumaka": "ノーマルモード", + "darumakaZen": "ダルマモード", "deerlingSpring": "春", "deerlingSummer": "夏", "deerlingAutumn": "秋", "deerlingWinter": "冬", - "tornadusIncarnate": "けしん", - "thundurusIncarnate": "けしん", - "landorusIncarnate": "けしん", - "keldeoOrdinary": "いつも", + "tornadusIncarnate": "けしんフォルム", + "tornadusTherian": "れいじゅうフォルム", + "thundurusIncarnate": "けしんフォルム", + "thundurusTherian": "れいじゅうフォルム", + "landorusIncarnate": "けしんフォルム", + "landorusTherian": "れいじゅうフォルム", + "kyurem": "通常", + "kyuremBlack": "ブラックキュレム", + "kyuremWhite": "ホワイトキュレム", + "keldeoOrdinary": "いつものすがた", + "keldeoResolute": "かくごのすがた", "meloettaAria": "ボイス", "meloettaPirouette": "ステップ", + "genesect": "通常", + "genesectShock": "イナズマカセット", + "genesectBurn": "ブレイズカセット", + "genesectChill": "フリーズカセット", + "genesectDouse": "アクアカセット", + "froakie": "通常", "froakieBattleBond": "きずなへんげ", + "froakieAsh": "サトシゲッコウガ", "scatterbugMeadow": "はなぞの", "scatterbugIcySnow": "ひょうせつ", "scatterbugPolar": "ゆきぐに", @@ -91,6 +123,7 @@ "flabebeOrange": "オレンジ", "flabebeBlue": "青", "flabebeWhite": "白", + "furfrou": "やせいのすがた", "furfrouHeart": "ハート", "furfrouStar": "スター", "furfrouDiamond": "ダイア", @@ -100,9 +133,14 @@ "furfrouLaReine": "クイーン", "furfrouKabuki": "カブキ", "furfrouPharaoh": "キングダム", - "pumpkabooSmall": "ちいさい", - "pumpkabooLarge": "おおきい", - "pumpkabooSuper": "とくだい", + "espurrMale": "オス", + "espurrFemale": "メス", + "honedgeShiled": "シールドフォルム", + "honedgeBlade": "ブレードフォルム", + "pumpkaboo": "ふつうのサイズ", + "pumpkabooSmall": "ちいさいサイズ", + "pumpkabooLarge": "おおきいサイズ", + "pumpkabooSuper": "とくだいサイズ", "xerneasNeutral": "リラックス", "xerneasActive": "アクティブ", "zygarde50": "50%フォルム", @@ -110,11 +148,37 @@ "zygarde50Pc": "50%フォルム スワームチェンジ", "zygarde10Pc": "10%フォルム スワームチェンジ", "zygardeComplete": "パーフェクトフォルム", + "hoopa": "いましめられしフーパ", + "hoopaUnbound": "ときはなたれしフーパ", "oricorioBaile": "めらめら", "oricorioPompom": "ぱちぱち", "oricorioPau": "ふらふら", "oricorioSensu": "まいまい", + "rockruff": "通常", "rockruffOwnTempo": "マイペース", + "rockruffMidday": "まひるのすがた", + "rockruffMidnight": "まよなかのすがた", + "rockruffDusk": "たそがれのすがた", + "wishiwashi": "たんどくのすがた", + "wishiwashiSchool": "むれたすがた", + "typeNullNormal": "タイプ:ノーマル", + "typeNullFighting": "タイプ:かくとう", + "typeNullFlying": "タイプ:ひこう", + "typeNullPoison": "タイプ:どく", + "typeNullGround": "タイプ:じめん", + "typeNullRock": "タイプ:いわ", + "typeNullBug": "タイプ:むし", + "typeNullGhost": "タイプ:ゴースト", + "typeNullSteel": "タイプ:はがね", + "typeNullFire": "タイプ:ほのお", + "typeNullWater": "タイプ:みず", + "typeNullGrass": "タイプ:くさ", + "typeNullElectric": "タイプ:でんき", + "typeNullPsychic": "タイプ:エスパー", + "typeNullIce": "タイプ:こおり", + "typeNullDragon": "タイプ:ドラゴン", + "typeNullDark": "タイプ:あく", + "typeNullFairy": "タイプ:フェアリー", "miniorRedMeteor": "赤 りゅうせい", "miniorOrangeMeteor": "オレンジ りゅうせい", "miniorYellowMeteor": "黄 りゅうせい", @@ -131,22 +195,63 @@ "miniorViolet": "紫", "mimikyuDisguised": "ばけたすがた", "mimikyuBusted": "ばれたすがた", - "magearnaOriginal": "500ねんまえ", + "necrozma": "ネクロズマ", + "necrozmaDuskMane": "たそがれのたてがみ", + "necrozmaDawnWings": "あかつきのつばさ", + "necrozmaUltra": "ウルトラネクロズマ", + "magearna": "通常", + "magearnaOriginal": "500ねんまえのいろ", + "marshadow": "通常", "marshadowZenith": "Zパワー", + "cramorant": "通常", + "cramorantGulping": "うのみのすがた", + "cramorantGorging": "まるのみのすがた", + "toxelAmped": "ハイなすがた", + "toxelLowkey": "ローなすがた", "sinisteaPhony": "がんさく", "sinisteaAntique": "しんさく", - "eiscueNoIce": "ナイスなし", + "milceryVanillaCream": "ミルキィバニラ", + "milceryRubyCream": "ミルキィルビー", + "milceryMatchaCream": "ミルキィまっちゃ", + "milceryMintCream": "ミルキィミント", + "milceryLemonCream": "ミルキィレモン", + "milcerySaltedCream": "ミルキィソルト", + "milceryRubySwirl": "ルビーミックス", + "milceryCaramelSwirl": "キャラメルミックス", + "milceryRainbowSwirl": "トリプルミックス", + "eiscue": "アイスフェイス", + "eiscueNoIce": "ナイスフェイス", "indeedeeMale": "オス", "indeedeeFemale": "メス", - "morpekoFullBelly": "まんぷく", + "morpekoFullBelly": "まんぷくもよう", + "morpekoHangry": "はらぺこもよう", "zacianHeroOfManyBattles": "れきせんのゆうしゃ", + "zacianCrowned": "けんのおう", "zamazentaHeroOfManyBattles": "れきせんのゆうしゃ", + "zamazentaCrowned": "たてのおう", + "kubfuSingleStrike": "いちげきのかた", + "kubfuRapidStrike": "れんげきのかた", + "zarude": "通常", "zarudeDada": "とうちゃん", - "enamorusIncarnate": "けしん", + "calyrex": "通常", + "calyrexIce": "はくばじょうのすがた", + "calyrexShadow": "こくばじょうのすがた", + "basculinMale": "オス", + "basculinFemale": "メス", + "enamorusIncarnate": "けしんフォルム", + "enamorusTherian": "れいじゅうフォルム", + "lechonkMale": "オス", + "lechonkFemale": "メス", + "tandemausFour": "4ひきかぞく", + "tandemausThree": "3びきかぞく", "squawkabillyGreenPlumage": "グリーンフェザー", "squawkabillyBluePlumage": "ブルーフェザー", "squawkabillyYellowPlumage": "イエローフェザー", "squawkabillyWhitePlumage": "ホワイトフェザー", + "dunsparceTwo": "ふたふしフォルム", + "dunsparceThree": "みつふしフォルム", + "finizenZero": "ナイーブフォルム", + "finizenHero": "マイティフォルム", "tatsugiriCurly": "そったすがた", "tatsugiriDroopy": "たれたすがた", "tatsugiriStretchy": "のびたすがた", @@ -164,7 +269,22 @@ "miraidonGlideMode":"グライドモード", "poltchageistCounterfeit": "マガイモノ", "poltchageistArtisan": "タカイモノ", + "poltchageistUnremarkable": "ボンサクのすがた", + "poltchageistMasterpiece": "ケッサクのすがた", + "ogerponTealMask": "みどりのめん", + "ogerponTealMaskTera": "みどりのめん テラスタル", + "ogerponWellspringMask": "いどのめん", + "ogerponWellspringMaskTera": "いどのめん テラスタル", + "ogerponHearthflameMask": "かまどのめん", + "ogerponHearthflameMaskTera": "かまどのめん テラスタル", + "ogerponCornerstoneMask": "いしずえのめん", + "ogerponCornerstoneMaskTera": "いしずえのめん テラスタル", + "terpagos": "ノーマルフォルム", + "terpagosTerastal": "テラスタルフォルム", + "terpagosStellar": "ステラフォルム", + "galarDarumaka": "ノーマルモード", + "galarDarumakaZen": "ダルマモード", "paldeaTaurosCombat": "コンバット", "paldeaTaurosBlaze": "ブレイズ", "paldeaTaurosAqua": "ウォーター" -} \ No newline at end of file +} diff --git a/src/locales/ja/pokemon-summary.json b/src/locales/ja/pokemon-summary.json index cf35befe6fd..9465bcd346d 100644 --- a/src/locales/ja/pokemon-summary.json +++ b/src/locales/ja/pokemon-summary.json @@ -11,7 +11,7 @@ "cancel": "キャンセル", "memoString": "{{natureFragment}}な性格。\n{{metFragment}}", "metFragment": { - "normal": "{{biome}}で\nLv.{{level}}の時に出会った。", + "normal": "ラウンド{{wave}}に{{biome}}で\nLv.{{level}}の時に出会った。", "apparently": "{{biome}}で\nLv.{{level}}の時に出会ったようだ。" }, "natureFragment": { diff --git a/src/locales/ja/splash-messages.json b/src/locales/ja/splash-messages.json index b7378e7a916..db3948fa2f1 100644 --- a/src/locales/ja/splash-messages.json +++ b/src/locales/ja/splash-messages.json @@ -1,5 +1,5 @@ { - "battlesWon": "Battles Won!", + "battlesWon": "勝ったバトル:{{count, number}}回!", "joinTheDiscord": "Join the Discord!", "infiniteLevels": "Infinite Levels!", "everythingStacks": "Everything Stacks!", diff --git a/src/locales/ja/trainer-classes.json b/src/locales/ja/trainer-classes.json index aba294fbbbd..a104e4e827e 100644 --- a/src/locales/ja/trainer-classes.json +++ b/src/locales/ja/trainer-classes.json @@ -126,5 +126,8 @@ "skull_grunts": "スカル団の下っ端", "macro_grunt": "マクロコスモスのトレーナ", "macro_grunt_female": "マクロコスモスのトレーナ", - "macro_grunts": "マクロコスモスのトレーナ" + "macro_grunts": "マクロコスモスのトレーナ", + "star_grunt": "スター団の下っ端", + "star_grunt_female": "スター団の下っ端", + "star_grunts": "スター団の下っ端" } diff --git a/src/locales/ja/trainer-names.json b/src/locales/ja/trainer-names.json index 70841734b5b..c152e7864cc 100644 --- a/src/locales/ja/trainer-names.json +++ b/src/locales/ja/trainer-names.json @@ -141,6 +141,11 @@ "faba": "ザオボー", "plumeria": "プルメリ", "oleana": "オリーヴ", + "giacomo": "ピーニャ", + "mela": "メロコ", + "atticus": "シュウメイ", + "ortega": "オルティガ", + "eri": "ビワ", "maxie": "マツブサ", "archie": "アオギリ", @@ -150,6 +155,7 @@ "lusamine": "ルザミーネ", "guzma": "グズマ", "rose": "ローズ", + "cassiopeia": "ボタン", "blue_red_double": "グリーンとレッド", "red_blue_double": "レッドとグリーン", diff --git a/src/locales/ja/trainer-titles.json b/src/locales/ja/trainer-titles.json index b3829c701e5..b558f7ad80c 100644 --- a/src/locales/ja/trainer-titles.json +++ b/src/locales/ja/trainer-titles.json @@ -19,6 +19,7 @@ "aether_boss": "エーテル代表", "skull_boss": "スカル団ボス", "macro_boss": "マクロコスモス社長", + "star_boss": "スター団ボス", "rocket_admin": "ロケット団幹部", "rocket_admin_female": "ロケット団幹部", @@ -34,5 +35,6 @@ "flare_admin_female": "フレア団幹部", "aether_admin": "エーテル支部長", "skull_admin": "スカル団幹部", - "macro_admin": "マクロコスモス" + "macro_admin": "マクロコスモス", + "star_admin": "スター団 組ボス" } diff --git a/src/locales/ko/ability.json b/src/locales/ko/ability.json index 420d27c6011..631a6864e85 100644 --- a/src/locales/ko/ability.json +++ b/src/locales/ko/ability.json @@ -1237,6 +1237,6 @@ }, "poisonPuppeteer": { "name": "독조종", - "description": "복숭악동의 기술에 의해 독 상태가 된 상대는 혼란 상태도 되어 버린다." + "description": "이 기술에 의해 독 상태가 된 상대는 혼란 상태도 되어 버린다." } } \ No newline at end of file diff --git a/src/locales/ko/battler-tags.json b/src/locales/ko/battler-tags.json index 21e548a01a6..1cd6c86377e 100644 --- a/src/locales/ko/battler-tags.json +++ b/src/locales/ko/battler-tags.json @@ -70,5 +70,8 @@ "stockpilingOnAdd": "{{pokemonNameWithAffix}}[[는]]\n{{stockpiledCount}}개 비축했다!", "disabledOnAdd": "{{pokemonNameWithAffix}}의 {{moveName}}[[는]]\n사용할 수 없다!", "disabledLapse": "{{pokemonNameWithAffix}}의 {{moveName}}[[는]]\n이제 사용할 수 있다.", - "tarShotOnAdd": "{{pokemonNameWithAffix}}[[는]] 불꽃에 약해졌다!" + "tarShotOnAdd": "{{pokemonNameWithAffix}}[[는]] 불꽃에 약해졌다!", + "substituteOnAdd": "{{pokemonNameWithAffix}}의\n대타가 나타났다!", + "substituteOnHit": "{{pokemonNameWithAffix}}[[를]] 대신하여\n대타가 공격을 받았다!", + "substituteOnRemove": "{{pokemonNameWithAffix}}의\n대타는 사라져 버렸다..." } diff --git a/src/locales/ko/bgm-name.json b/src/locales/ko/bgm-name.json index 5295e2d8708..f010a7ac18e 100644 --- a/src/locales/ko/bgm-name.json +++ b/src/locales/ko/bgm-name.json @@ -83,9 +83,11 @@ "battle_aether_grunt": "SM 에테르재단 배틀", "battle_skull_grunt": "SM 스컬단 배틀", "battle_macro_grunt": "SWSH 트레이너 배틀", + "battle_star_grunt": "SV 스타단 배틀", "battle_galactic_admin": "BDSP 갤럭시단 간부 배틀", "battle_skull_admin": "SM 스컬단 간부 배틀", "battle_oleana": "SWSH 올리브 배틀", + "battle_star_admin": "SV 스타단 보스 배틀", "battle_rocket_boss": "USUM 비주기 배틀", "battle_aqua_magma_boss": "ORAS 아강 & 마적 배틀", "battle_galactic_boss": "BDSP 태홍 배틀", @@ -94,6 +96,7 @@ "battle_aether_boss": "SM 루자미네 배틀", "battle_skull_boss": "SM 구즈마 배틀", "battle_macro_boss": "SWSH 로즈 배틀", + "battle_star_boss": "SV 카시오페아 배틀", "abyss": "불가사의 던전 하늘의 탐험대 어둠의 화구", "badlands": "불가사의 던전 하늘의 탐험대 불모의 계곡", "beach": "불가사의 던전 하늘의 탐험대 축축한 암반", @@ -107,17 +110,17 @@ "forest": "불가사의 던전 하늘의 탐험대 검은 숲", "grass": "불가사의 던전 하늘의 탐험대 사과의 숲", "graveyard": "불가사의 던전 하늘의 탐험대 신비의 숲", - "ice_cave": "불가사의 던전 하늘의 탐험대 광대한 얼음산", + "ice_cave": "Firel - -50°C", "island": "불가사의 던전 하늘의 탐험대 연안의 암반", - "jungle": "Lmz - Jungle", - "laboratory": "Firel - Laboratory", - "lake": "불가사의 던전 하늘의 탐험대 수정 동굴", + "jungle": "Lmz - 정글", + "laboratory": "Firel - 연구소", + "lake": "Lmz - 호수", "meadow": "불가사의 던전 하늘의 탐험대 하늘 꼭대기 숲", "metropolis": "Firel - Metropolis", "mountain": "불가사의 던전 하늘의 탐험대 뿔산", - "plains": "불가사의 던전 하늘의 탐험대 하늘 꼭대기 초원", - "power_plant": "불가사의 던전 하늘의 탐험대 일렉트릭 평원", - "ruins": "불가사의 던전 하늘의 탐험대 봉인의 암반", + "plains": "Firel - Route 888", + "power_plant": "Firel - 기어르", + "ruins": "Lmz - 고대 유적", "sea": "Andr06 - Marine Mystique", "seabed": "Firel - Seabed", "slum": "Andr06 - Sneaky Snom", @@ -127,7 +130,7 @@ "tall_grass": "불가사의 던전 하늘의 탐험대 짙은 안개의 숲", "temple": "불가사의 던전 하늘의 탐험대 파수꾼의 동굴", "town": "불가사의 던전 하늘의 탐험대 랜덤 던전 테마 3", - "volcano": "불가사의 던전 하늘의 탐험대 열수의 동굴", + "volcano": "Firel - Twisturn Volcano", "wasteland": "불가사의 던전 하늘의 탐험대 환상의 대지", "encounter_ace_trainer": "BW 눈이 마주치면 승부! (엘리트 트레이너)", "encounter_backpacker": "BW 눈이 마주치면 승부! (등산가)", diff --git a/src/locales/ko/dialogue.json b/src/locales/ko/dialogue.json index 13fcd64a8d3..407b0a1ce05 100644 --- a/src/locales/ko/dialogue.json +++ b/src/locales/ko/dialogue.json @@ -715,12 +715,16 @@ "encounter": { "1": "당신은 여기서 끝날 것 같네요!", "2": "당신은 트레이너 맞죠? 하지만 우리를 방해하는 건 용납 못 합니다!", - "3": "매크로코스모스 생명입니다! 가입하신 실비보험은 있으신가요?" + "3": "매크로코스모스 생명입니다! 가입하신 실비보험은 있으신가요?", + "4": "찾았다! 그렇다면 포켓몬 승부입니다!", + "5": "올리브님에게 혼나기 싫으니까 포기하지 않겠습니다!" }, "victory": { "1": "순순히 물러나는 것 말고는 선택지가 없군요.", "2": "용돈을 뺏기다니… 패배는 적자로 이어지는구나…", - "3": "매크로코스모스 생명에 관한 일이라면 누구에게도 지지 않을 텐데…" + "3": "매크로코스모스 생명에 관한 일이라면 누구에게도 지지 않을 텐데…", + "4": "심지어 포켓몬 교체도 했는데…", + "5": "승부도 안 되면! 도망치는 수밖에 없다!" } }, "oleana": { @@ -735,6 +739,73 @@ "3": "아아… 이 올리브님 조금 지쳤어…" } }, + "star_grunt": { + "encounter": { + "1": "우리는 우는 아이도 웃게 하는 스타단!", + "2": "멤버들을 총동원해서 공격할 테니, 수고하셨스타~! ★", + "3": "빨리 돌아가지 그래? 아니면 방어권을 행사하는 수밖에 없다고?", + "4": "미안하지만 돌아가지 않겠다면 힘으로라도 쫓아내 주겠어!", + "4_female": "미안하지만 돌아가지 않겠다면 힘으로라도 쫓아내 주겠어!", + "5": "아~ 또 사람이 와 버렸잖아." + }, + "victory": { + "1": "저 하늘의 별이 되는 건 나였네!?", + "2": "스타단에 들어가면 다들 쫄아서 꼭대기에서 군림할 수 있는 거 아니었어?", + "3": "나의 방어권이…!", + "4": "수, 수고하셨스타… ★", + "5": "스타단 신입이 이렇게 귀찮은 일일 줄이야…" + } + }, + "giacomo": { + "encounter": { + "1": "스타단에게 싸움을 걸다니, 넌 정말 겁이 없구나?", + "2": "레퀴엠을 들려줄 테니! 자! 파티를 시작하자고!" + }, + "victory": { + "1": "결국 이렇게 되는 건가…", + "2": "레퀴엠을 들은 건 내 쪽이었네." + } + }, + "mela": { + "encounter": { + "1": "…우리에게 싸움을 걸었다는 녀석이 너냐? …터뜨려 주지.", + "2": "좋아! …그럼, 한번 터뜨려 볼까!" + }, + "victory": { + "1": "이걸로 끝인 건가? …이런 이런.", + "2": "그렇게 타오르고 타오르다… 완전히 연소해 버린 건가…" + } + }, + "atticus": { + "encounter": { + "1": "스타단을 해하려 하는 괘씸한 자는 독으로 해치울 뿐!", + "2": "그럼, 진검승부를 펼쳐 봅시다!" + }, + "victory": { + "1": "동지들이여, 미안하오…", + "2": "미련 하나 남지 않을 정도로 명백한 소인의 완패였소…" + } + }, + "ortega": { + "encounter": { + "1": "잔뜩 귀여워해 줄 테니까 울며불며 돌아갈 준비나 하라고!", + "2": "어디 한번 여유롭게 굴어 보시지. 내가 이길 테니까!" + }, + "victory": { + "1": "어째서 내가 지는 건데!? 대체 왜! 어째서 진 거냐고~!!", + "2": "젠장~! 너무 강하잖아! 비겁해!" + } + }, + "eri": { + "encounter": { + "1": "어디의 누가 됐든 스타단을 노리는 자는 박살 낼 뿐!", + "2": "맞았으면 그저 맞받아칠 뿐!! 승리는 끝까지 서 있는 사람의 것이니까!!" + }, + "victory": { + "1": "얘들아… 미안해…", + "2": "열심히… 했는데… 나는 역시… 부족했어…" + } + }, "rocket_boss_giovanni_1": { "encounter": { "1": "그래서! 여기까지 오다니, 감탄이 절로 나오는군!" @@ -933,6 +1004,28 @@ "1": "너희가 보기에는 내가 끔찍한 짓을 벌이고 있는 것처럼 보이겠지? 조금도 이해가 가지 않을 거야.\n$하지만 난 가라르지방의 영원한 번영을 위해서 무한한 에너지를 가져다줘야 해." } }, + "star_boss_penny_1": { + "encounter": { + "1": "내가 바로 스타단의 진 보스, 카시오페아… \n$…진 보스의 힘 앞에 무릎 꿇게 만들어 주겠어!!" + }, + "victory": { + "1": "… … …" + }, + "defeat": { + "1": "후후…" + } + }, + "star_boss_penny_2": { + "encounter": { + "1": "승부인 이상, 봐주지 않아! 그것이 스타단의 규칙이니까! \n$브이브이 파워로 우주의 먼지로 만들어 주겠어!!" + }, + "victory": { + "1": "이걸로 진짜 끝이구나…" + }, + "defeat": { + "1": "군더더기가 없는 실력이네. 보스들이 당한 걸 생각해 보면 말이야." + } + }, "brock": { "encounter": { "1": "내 전문인 바위 타입 포켓몬으로 널 쓰러뜨려줄게! 덤벼!", diff --git a/src/locales/ko/move.json b/src/locales/ko/move.json index a06bb2b3e27..5b0d6eaeaad 100644 --- a/src/locales/ko/move.json +++ b/src/locales/ko/move.json @@ -3129,7 +3129,7 @@ }, "auraWheel": { "name": "오라휠", - "effect": "볼주머니에 저장해둔 에너지로 공격하고 자신의 스피드를 올린다. 모르페코의 모습에 따라 타입이 바뀐다." + "effect": "볼주머니에 저장해둔 에너지로 공격하고 자신의 스피드를 올린다. 모르페코가 사용할 경우 모습에 따라 타입이 바뀐다." }, "breakingSwipe": { "name": "와이드브레이커", diff --git a/src/locales/ko/party-ui-handler.json b/src/locales/ko/party-ui-handler.json index 468f33bf960..6e7ba120b71 100644 --- a/src/locales/ko/party-ui-handler.json +++ b/src/locales/ko/party-ui-handler.json @@ -13,8 +13,10 @@ "ALL": "전부", "PASS_BATON": "배턴터치한다", "UNPAUSE_EVOLUTION": "진화 재개", + "PAUSE_EVOLUTION": "진화 중지", "REVIVE": "되살린다", "RENAME": "닉네임 바꾸기", + "SELECT": "선택한다", "choosePokemon": "포켓몬을 선택하세요.", "doWhatWithThisPokemon": "포켓몬을 어떻게 하겠습니까?", "noEnergy": "{{pokemonName}}[[는]] 싸울 수 있는\n기력이 남아 있지 않습니다!", @@ -23,6 +25,7 @@ "tooManyItems": "{{pokemonName}}[[는]] 지닌 도구의 수가\n너무 많습니다", "anyEffect": "써도 효과가 없다.", "unpausedEvolutions": "{{pokemonName}}의 진화가 재개되었다.", + "pausedEvolutions": "{{pokemonName}}[[가]] 진화하지 않도록 했다.", "unspliceConfirmation": "{{pokemonName}}로부터 {{fusionName}}의 융합을 해제하시겠습니까?\n{{fusionName}}는 사라지게 됩니다.", "wasReverted": "{{fusionName}}은 {{pokemonName}}의 모습으로 돌아갔습니다!", "releaseConfirmation": "{{pokemonName}}[[를]]\n정말 놓아주겠습니까?", diff --git a/src/locales/ko/pokemon-form.json b/src/locales/ko/pokemon-form.json index 885f9a9b891..dce2fcd35cf 100644 --- a/src/locales/ko/pokemon-form.json +++ b/src/locales/ko/pokemon-form.json @@ -1,4 +1,5 @@ { + "pikachu": "일반", "pikachuCosplay": "옷갈아입기", "pikachuCoolCosplay": "하드록", "pikachuBeautyCosplay": "마담", @@ -6,7 +7,9 @@ "pikachuSmartCosplay": "닥터", "pikachuToughCosplay": "마스크드", "pikachuPartner": "파트너", + "eevee": "일반", "eeveePartner": "파트너", + "pichu": "일반", "pichuSpiky": "삐쭉귀", "unownA": "A", "unownB": "B", @@ -36,36 +39,65 @@ "unownZ": "Z", "unownExclamation": "!", "unownQuestion": "?", + "castform": "평상시", "castformSunny": "태양의 모습", "castformRainy": "빗방울의 모습", "castformSnowy": "설운의 모습", "deoxysNormal": "노말폼", + "deoxysAttack": "어택폼", + "deoxysDefense": "디펜스폼", + "deoxysSpeed": "스피드폼", "burmyPlant": "초목도롱", "burmySandy": "모래땅도롱", "burmyTrash": "슈레도롱", + "cherubiOvercast": "네거폼", + "cherubiSunshine": "포지폼", "shellosEast": "동쪽바다의 모습", "shellosWest": "서쪽바다의 모습", - "rotomHeat": "히트", - "rotomWash": "워시", - "rotomFrost": "프로스트", - "rotomFan": "스핀", - "rotomMow": "커트", + "rotom": "로토무", + "rotomHeat": "히트로토무", + "rotomWash": "워시로토무", + "rotomFrost": "프로스트로토무", + "rotomFan": "스핀로토무", + "rotomMow": "커트로토무", + "dialga": "어나더폼", + "dialgaOrigin": "오리진폼", + "palkia": "어나더폼", + "palkiaOrigin": "오리진폼", "giratinaAltered": "어나더폼", + "giratinaOrigin": "오리진폼", "shayminLand": "랜드폼", + "shayminSky": "스카이폼", "basculinRedStriped": "적색근의 모습", "basculinBlueStriped": "청색근의 모습", "basculinWhiteStriped": "백색근의 모습", + "darumaka": "노말모드", + "darumakaZen": "달마모드", "deerlingSpring": "봄의 모습", "deerlingSummer": "여름의 모습", "deerlingAutumn": "가을의 모습", "deerlingWinter": "겨울의 모습", "tornadusIncarnate": "화신폼", + "tornadusTherian": "영물폼", "thundurusIncarnate": "화신폼", + "thundurusTherian": "영물폼", "landorusIncarnate": "화신폼", + "landorusTherian": "영물폼", + "kyurem": "큐레무", + "kyuremBlack": "블랙큐레무", + "kyuremWhite": "화이트큐레무", "keldeoOrdinary": "평상시 모습", + "keldeoResolute": "각오의 모습", "meloettaAria": "보이스폼", "meloettaPirouette": "스텝폼", + "genesect": "노말폼", + "genesectShock": "라이트닝폼", + "genesectBurn": "블레이즈폼", + "genesectChill": "프리즈폼", + "genesectDouse": "아쿠아폼", + "froakie": "개굴닌자", "froakieBattleBond": "유대변화", + "froakieAsh": "지우개굴닌자", "scatterbugMeadow": "화원의 모양", "scatterbugIcySnow": "빙설의 모양", "scatterbugPolar": "설국의 모양", @@ -91,6 +123,7 @@ "flabebeOrange": "오렌지색 꽃", "flabebeBlue": "파란 꽃", "flabebeWhite": "하얀 꽃", + "furfrou": "일반", "furfrouHeart": "하트컷", "furfrouStar": "스타컷", "furfrouDiamond": "다이아컷", @@ -100,6 +133,11 @@ "furfrouLaReine": "퀸컷", "furfrouKabuki": "가부키컷", "furfrouPharaoh": "킹덤컷", + "espurrMale": "수컷의 모습", + "espurrFemale": "암컷의 모습", + "honedgeShiled": "실드폼", + "honedgeBlade": "블레이드폼", + "pumpkaboo": "보통 사이즈", "pumpkabooSmall": "작은 사이즈", "pumpkabooLarge": "큰 사이즈", "pumpkabooSuper": "특대 사이즈", @@ -110,11 +148,37 @@ "zygarde50Pc": "스웜체인지 50%폼", "zygarde10Pc": "스웜체인지 10%폼", "zygardeComplete": "퍼펙트폼", + "hoopa": "굴레에 빠진 모습", + "hoopaUnbound": "굴레를 벗어난 모습", "oricorioBaile": "이글이글스타일", "oricorioPompom": "파칙파칙스타일", "oricorioPau": "훌라훌라스타일", "oricorioSensu": "하늘하늘스타일", + "rockruff": "일반", "rockruffOwnTempo": "마이페이스", + "rockruffMidday": "한낮의 모습", + "rockruffMidnight": "한밤중의 모습", + "rockruffDusk": "황혼의 모습", + "wishiwashi": "단독의 모습", + "wishiwashiSchool": "군집의 모습", + "typeNullNormal": "노말", + "typeNullFighting": "격투", + "typeNullFlying": "비행", + "typeNullPoison": "독", + "typeNullGround": "땅", + "typeNullRock": "바위", + "typeNullBug": "벌레", + "typeNullGhost": "고스트", + "typeNullSteel": "강철", + "typeNullFire": "불꽃", + "typeNullWater": "물", + "typeNullGrass": "풀", + "typeNullElectric": "전기", + "typeNullPsychic": "에스퍼", + "typeNullIce": "얼음", + "typeNullDragon": "드래곤", + "typeNullDark": "악", + "typeNullFairy": "페어리", "miniorRedMeteor": "유성의 모습(빨강)", "miniorOrangeMeteor": "유성의 모습(주황)", "miniorYellowMeteor": "유성의 모습(노랑)", @@ -131,25 +195,66 @@ "miniorViolet": "보라색 코어", "mimikyuDisguised": "둔갑한 모습", "mimikyuBusted": "들킨 모습", + "necrozma": "네크로즈마", + "necrozmaDuskMane": "황혼의 갈기", + "necrozmaDawnWings": "새벽의 날개", + "necrozmaUltra": "울트라네크로즈마", + "magearna": "일반적인 모습", "magearnaOriginal": "500년 전의 색", - "marshadowZenith": "투지를 불태운 마샤도", + "marshadow": "일반적인 모습", + "marshadowZenith": "타오르는 투지의 모습", + "cramorant": "일반", + "cramorantGulping": "그대로 삼킨 모습", + "cramorantGorging": "통째로 삼킨 모습", + "toxelAmped": "하이한 모습", + "toxelLowkey": "로우한 모습", "sinisteaPhony": "위작품", "sinisteaAntique": "진작품", + "milceryVanillaCream": "밀키바닐라", + "milceryRubyCream": "밀키루비", + "milceryMatchaCream": "밀키말차", + "milceryMintCream": "밀키민트", + "milceryLemonCream": "밀키레몬", + "milcerySaltedCream": "밀키솔트", + "milceryRubySwirl": "루비믹스", + "milceryCaramelSwirl": "캐러멜믹스", + "milceryRainbowSwirl": "트리플믹스", + "eiscue": "아이스페이스", "eiscueNoIce": "나이스페이스", "indeedeeMale": "수컷의 모습", "indeedeeFemale": "암컷의 모습", "morpekoFullBelly": "배부른 모양", + "morpekoHangry": "배고픈 모양", "zacianHeroOfManyBattles": "역전의 용사", + "zacianCrowned": "검왕", "zamazentaHeroOfManyBattles": "역전의 용사", + "zamazentaCrowned": "방패왕", + "kubfuSingleStrike": "일격의 태세", + "kubfuRapidStrike": "연격의 태세", + "zarude": "일반", "zarudeDada": "아빠", + "calyrex": "일반", + "calyrexIce": "백마 탄 모습", + "calyrexShadow": "흑마 탄 모습", + "basculinMale": "수컷의 모습", + "basculinFemale": "암컷의 모습", "enamorusIncarnate": "화신폼", + "enamorusTherian": "영물폼", + "lechonkMale": "수컷의 모습", + "lechonkFemale": "암컷의 모습", + "tandemausFour": "네 식구", + "tandemausThree": "세 식구", "squawkabillyGreenPlumage": "그린 페더", "squawkabillyBluePlumage": "블루 페더", "squawkabillyYellowPlumage": "옐로 페더", "squawkabillyWhitePlumage": "화이트 페더", + "finizenZero": "나이브폼", + "finizenHero": "마이티폼", "tatsugiriCurly": "젖힌 모습", "tatsugiriDroopy": "늘어진 모습", "tatsugiriStretchy": "뻗은 모습", + "dunsparceTwo": "두 마디 폼", + "dunsparceThree": "세 마디 폼", "gimmighoulChest": "상자폼", "gimmighoulRoaming": "도보폼", "koraidonApexBuild": "완전형태", @@ -164,7 +269,22 @@ "miraidonGlideMode": "글라이드모드", "poltchageistCounterfeit": "가짜배기의 모습", "poltchageistArtisan": "알짜배기의 모습", + "poltchageistUnremarkable": "범작의 모습", + "poltchageistMasterpiece": "걸작의 모습", + "ogerponTealMask": "벽록의가면", + "ogerponTealMaskTera": "벽록의가면 테라스탈", + "ogerponWellspringMask": "우물의가면", + "ogerponWellspringMaskTera": "우물의가면 테라스탈", + "ogerponHearthflameMask": "화덕의가면", + "ogerponHearthflameMaskTera": "화덕의가면 테라스탈", + "ogerponCornerstoneMask": "주춧돌의가면", + "ogerponCornerstoneMaskTera": "주춧돌의가면 테라스탈", + "terpagos": "노말폼", + "terpagosTerastal": "테라스탈폼", + "terpagosStellar": "스텔라폼", + "galarDarumaka": "노말모드", + "galarDarumakaZen": "달마모드", "paldeaTaurosCombat": "컴뱃종", "paldeaTaurosBlaze": "블레이즈종", "paldeaTaurosAqua": "워터종" -} \ No newline at end of file +} diff --git a/src/locales/ko/pokemon-summary.json b/src/locales/ko/pokemon-summary.json index d9119623662..ca4b7a22b65 100644 --- a/src/locales/ko/pokemon-summary.json +++ b/src/locales/ko/pokemon-summary.json @@ -11,7 +11,7 @@ "cancel": "그만둔다", "memoString": "{{natureFragment}}.\n{{metFragment}}", "metFragment": { - "normal": "{{biome}}에서\n레벨 {{level}}일 때 만났다.", + "normal": "{{biome}}에서 웨이브{{wave}},\n레벨 {{level}}일 때 만났다.", "apparently": "{{biome}}에서\n레벨 {{level}}일 때 만난 것 같다." }, "natureFragment": { diff --git a/src/locales/ko/settings.json b/src/locales/ko/settings.json index c10046385e1..d0655009a3c 100644 --- a/src/locales/ko/settings.json +++ b/src/locales/ko/settings.json @@ -11,6 +11,10 @@ "expGainsSpeed": "경험치 획득 속도", "expPartyDisplay": "파티 경험치 표시", "skipSeenDialogues": "본 대화 생략", + "eggSkip": "알 스킵", + "never": "안 함", + "always": "항상", + "ask": "확인하기", "battleStyle": "시합 룰", "enableRetries": "재도전 허용", "hideIvs": "개체값탐지기 효과 끄기", diff --git a/src/locales/ko/splash-messages.json b/src/locales/ko/splash-messages.json index 6cf7ce050b7..1e89713ccde 100644 --- a/src/locales/ko/splash-messages.json +++ b/src/locales/ko/splash-messages.json @@ -1,5 +1,5 @@ { - "battlesWon": "전투에서 승리하세요!", + "battlesWon": "{{count, number}} 전투에서 승리하세요!", "joinTheDiscord": "디스코드에 가입하세요!", "infiniteLevels": "무한한 레벨!", "everythingStacks": "모든 것이 누적됩니다!", diff --git a/src/locales/ko/trainer-classes.json b/src/locales/ko/trainer-classes.json index da6c6f4aceb..89cbe250c83 100644 --- a/src/locales/ko/trainer-classes.json +++ b/src/locales/ko/trainer-classes.json @@ -126,5 +126,8 @@ "skull_grunts": "스컬단 조무래기들", "macro_grunt": "매크로코스모스 직원", "macro_grunt_female": "매크로코스모스 직원", - "macro_grunts": "매크로코스모스 직원들" + "macro_grunts": "매크로코스모스 직원들", + "star_grunt": "스타단 조무래기", + "star_grunt_female": "스타단 조무래기", + "star_grunts": "스타단 조무래기들" } diff --git a/src/locales/ko/trainer-names.json b/src/locales/ko/trainer-names.json index f1357a428ba..7d18cd51c4e 100644 --- a/src/locales/ko/trainer-names.json +++ b/src/locales/ko/trainer-names.json @@ -141,6 +141,11 @@ "faba": "자우보", "plumeria": "플루메리", "oleana": "올리브", + "giacomo": "피나", + "mela": "멜로코", + "atticus": "추명", + "ortega": "오르티가", + "eri": "비파", "maxie": "마적", "archie": "아강", @@ -150,6 +155,7 @@ "lusamine": "루자미네", "guzma": "구즈마", "rose": "로즈", + "cassiopeia": "모란", "blue_red_double": "그린 & 레드", "red_blue_double": "레드 & 그린", diff --git a/src/locales/ko/trainer-titles.json b/src/locales/ko/trainer-titles.json index 7cff2207817..7b5a72f6fc1 100644 --- a/src/locales/ko/trainer-titles.json +++ b/src/locales/ko/trainer-titles.json @@ -19,6 +19,7 @@ "aether_boss": "에테르재단 대표", "skull_boss": "스컬단 보스", "macro_boss": "매크로코스모스 사장", + "star_boss": "스타단 보스", "rocket_admin": "로켓단 간부", "rocket_admin_female": "로켓단 간부", @@ -34,5 +35,6 @@ "flare_admin_female": "플레어단 간부", "aether_admin": "에테르재단 지부장", "skull_admin": "스컬단 간부", - "macro_admin": "매크로코스모스 간부" + "macro_admin": "매크로코스모스 간부", + "star_admin": "스타단 군단 보스" } diff --git a/src/locales/pt_BR/ability.json b/src/locales/pt_BR/ability.json index c4180ff01dd..e30a7dd8c4f 100644 --- a/src/locales/pt_BR/ability.json +++ b/src/locales/pt_BR/ability.json @@ -1237,6 +1237,6 @@ }, "poisonPuppeteer": { "name": "Poison Puppeteer", - "description": "Pokémon envenenados pelos movimentos de Pecharunt também ficarão confusos." + "description": "Pokémon envenenados pelos movimentos deste Pokémon também ficarão confusos." } } diff --git a/src/locales/pt_BR/battle.json b/src/locales/pt_BR/battle.json index 08eeb99e0cd..392f7b2ec38 100644 --- a/src/locales/pt_BR/battle.json +++ b/src/locales/pt_BR/battle.json @@ -96,5 +96,7 @@ "retryBattle": "Você gostaria de tentar novamente desde o início da batalha?", "unlockedSomething": "{{unlockedThing}}\nfoi desbloqueado.", "congratulations": "Parabéns!", - "beatModeFirstTime": "{{speciesName}} venceu o Modo {{gameMode}} pela primeira vez!\nVocê recebeu {{newModifier}}!" + "beatModeFirstTime": "{{speciesName}} venceu o Modo {{gameMode}} pela primeira vez!\nVocê recebeu {{newModifier}}!", + "battlerTagsHealBlock": "{{pokemonNameWithAffix}} não pode restaurar seus PS!", + "battlerTagsHealBlockOnRemove": "{{pokemonNameWithAffix}} pode restaurar seus PS novamente!" } diff --git a/src/locales/pt_BR/battler-tags.json b/src/locales/pt_BR/battler-tags.json index ec6559e12e5..ce645a3d24f 100644 --- a/src/locales/pt_BR/battler-tags.json +++ b/src/locales/pt_BR/battler-tags.json @@ -70,5 +70,8 @@ "stockpilingOnAdd": "{{pokemonNameWithAffix}} estocou {{stockpiledCount}}!", "disabledOnAdd": "{{moveName}} de {{pokemonNameWithAffix}}\nfoi desabilitado!", "disabledLapse": "{{moveName}} de {{pokemonNameWithAffix}}\nnão está mais desabilitado.", - "tarShotOnAdd": "{{pokemonNameWithAffix}} tornou-se mais fraco ao fogo!" + "tarShotOnAdd": "{{pokemonNameWithAffix}} tornou-se mais fraco ao fogo!", + "substituteOnAdd": "{{pokemonNameWithAffix}} colocou um substituto!", + "substituteOnHit": "O substituto tomou o dano pelo {{pokemonNameWithAffix}}!", + "substituteOnRemove": "O substituto de {{pokemonNameWithAffix}} desbotou!" } diff --git a/src/locales/pt_BR/bgm-name.json b/src/locales/pt_BR/bgm-name.json index 86d8ce3e790..d73347ad988 100644 --- a/src/locales/pt_BR/bgm-name.json +++ b/src/locales/pt_BR/bgm-name.json @@ -83,9 +83,11 @@ "battle_aether_grunt": "SM Batalha da Fundação Aether", "battle_skull_grunt": "SM Batalha da Equipe Skull", "battle_macro_grunt": "SWSH Batalha de Treinador", + "battle_star_grunt": "SV Batalha da Equipe Estrela", "battle_galactic_admin": "BDSP Batalha com Admin da Equipe Galáctica", "battle_skull_admin": "SM Batalha com Admin da Euipe Skull", "battle_oleana": "SWSH Batalha da Oleana", + "battle_star_admin": "SV Chefe da Equipe Estrela", "battle_rocket_boss": "USUM Batalha do Giovanni", "battle_aqua_magma_boss": "ORAS Batalha do Maxie & Archie", "battle_galactic_boss": "BDSP Batalha do Cyrus", @@ -94,6 +96,7 @@ "battle_aether_boss": "SM Batalha da Lusamine", "battle_skull_boss": "SM Batalha do Guzma", "battle_macro_boss": "SWSH Batalha do Rose", + "battle_star_boss": "SV Batalha da Cassiopeia", "abyss": "PMD EoS Dark Crater", "badlands": "PMD EoS Barren Valley", @@ -108,17 +111,17 @@ "forest": "PMD EoS Dusk Forest", "grass": "PMD EoS Apple Woods", "graveyard": "PMD EoS Mystifying Forest", - "ice_cave": "PMD EoS Vast Ice Mountain", + "ice_cave": "Firel - -50°C", "island": "PMD EoS Craggy Coast", "jungle": "Lmz - Jungle", "laboratory": "Firel - Laboratory", - "lake": "PMD EoS Crystal Cave", + "lake": "Lmz - Lake", "meadow": "PMD EoS Sky Peak Forest", "metropolis": "Firel - Metropolis", "mountain": "PMD EoS Mt. Horn", - "plains": "PMD EoS Sky Peak Prairie", - "power_plant": "PMD EoS Far Amp Plains", - "ruins": "PMD EoS Deep Sealed Ruin", + "plains": "Firel - Route 888", + "power_plant": "Firel - The Klink", + "ruins": "Lmz - Ancient Ruins", "sea": "Andr06 - Marine Mystique", "seabed": "Firel - Seabed", "slum": "Andr06 - Sneaky Snom", @@ -128,7 +131,7 @@ "tall_grass": "PMD EoS Foggy Forest", "temple": "PMD EoS Aegis Cave", "town": "PMD EoS Random Dungeon Theme 3", - "volcano": "PMD EoS Steam Cave", + "volcano": "Firel - Twisturn Volcano", "wasteland": "PMD EoS Hidden Highland", "encounter_ace_trainer": "BW Encontro com Treinador (Treinador Ás)", "encounter_backpacker": "BW Encontro com Treinador (Mochileiro)", diff --git a/src/locales/pt_BR/dialogue.json b/src/locales/pt_BR/dialogue.json index 2f39442ee5a..fdf472526c1 100644 --- a/src/locales/pt_BR/dialogue.json +++ b/src/locales/pt_BR/dialogue.json @@ -749,12 +749,16 @@ "encounter": { "1": "Parece que aqui é o fim da linha para você!", "2": "Você é um treinador, não é? Temo que isso não lhe dê o direito de interferir em nosso trabalho.", - "3": "Sou da Macro Cosmos Seguros! Já tem um seguro de vida?" + "3": "Sou da Macro Cosmos Seguros! Já tem um seguro de vida?", + "4": "Te encontrei! Nesse caso, é hora de uma batalha Pokémon!", + "5": "Ouvir uma bronca da Srta. Oleana é bem pior do que qualquer coisa que você possa fazer!" }, "victory": { "1": "Eu não tenho muita escolha a não ser recuar respeitosamente.", "2": "Ter que desistir do meu dinheiro... Perder significa que estou de volta no vermelho...", - "3": "Ninguém pode vencer a Macro Cosmos quando se trata de nossa dedicação ao trabalho!" + "3": "Ninguém pode vencer a Macro Cosmos quando se trata de nossa dedicação ao trabalho!", + "4": "Eu até troquei meu Pokémon...", + "5": "As batalhas não funcionaram... A única coisa a fazer agora é fugir!" } }, "oleana": { @@ -769,6 +773,73 @@ "3": "*suspiro* Eu sou uma Oleana cansada..." } }, + "star_grunt": { + "encounter": { + "1": "Nós somos a Equipe Estrela, garoto. Brilhamos tão forte que dói olhar pra nós!", + "2": "Vamos com tudo pra cima de você - Hasta la vistaaar! ★", + "3": "Se você não sair rapidinho, vou ter que me defender. Entendeu?", + "4": "Desculpe, mas se você não der meia-volta, amigo, vamos ter que te despachar!", + "4_female": "Desculpe, mas se você não der meia-volta, amiga, vamos ter que te despachar!", + "5": "Ótimo. Lá vem mais um qualquer para estragar o meu dia." + }, + "victory": { + "1": "Como é que EU estou vendo estrelas?!", + "2": "Você é assustador, garoto. Se você se juntasse à Equipe Estrela, estaria no topo rapidinho!", + "3": "Me defendi bem... Mas não foi o suficiente!", + "4": "H-hasta la vistar... ★", + "5": "Eu não achei que o trabalho de recruta da Equipe Estrela seria uma tarefa tão pesada..." + } + }, + "giacomo": { + "encounter": { + "1": "Você realmente não pensa muito, né? Declarar guerra contra a Equipe Estrela é uma jogada muito ruim.", + "2": "Vou tocar uma sinfonia do seu fracasso enquanto você cai e queima. Vamos começar a festa!" + }, + "victory": { + "1": "Acho que é isso...", + "2": "Você transformou minha melodia em um lamento..." + } + }, + "mela": { + "encounter": { + "1": "Então você é o idiota que arrumou briga com a Equipe Estrela... Prepare-se para ser destruído.", + "2": "Tudo bem, VAMOS LÁ! Vou explodir tudo!" + }, + "victory": { + "1": "Ugh. É assim que isso vai acabar? Que problema...", + "2": "Usei tudo o que eu tinha... e agora apaguei." + } + }, + "atticus": { + "encounter": { + "1": "Você tem coragem de mostrar suas garras para a Equipe Estrela. Venha, então, vil criatura!", + "2": "Esteja avisado—não te darei misericórdia! Em guarda!" + }, + "victory": { + "1": "Perdoem-me, meus amigos...", + "2": "Você me venceu completamente. Mas sua vitória não me trouxe amargura—tamanha foi sua brilhante execução." + } + }, + "ortega": { + "encounter": { + "1": "Prometo que serei gentil, então não me culpe quando essa batalha te mandar chorando de volta para casa!", + "2": "Vou apagar esse sorriso convencido do seu rosto com certeza! Você vai perder!" + }, + "victory": { + "1": "Ugh! Como eu pude PERDER! Que RAIVA!", + "2": "Arrrrgggh! Essa sua força é tão INJUSTA." + } + }, + "eri": { + "encounter": { + "1": "Não importa quem você é. Vou derrubar qualquer um que tente destruir a Equipe Estrela!", + "2": "Eu dou tudo de mim, e isso é uma promessa! Vamos ver quem fica de pé no final!" + }, + "victory": { + "1": "Me desculpem, pessoal...", + "2": "Dei tudo de mim, mas não foi o suficiente—eu não fui o suficiente..." + } + }, "rocket_boss_giovanni_1": { "encounter": { "1": "Tenho que admitir, estou impressionado que tenha chegado até aqui!" @@ -968,6 +1039,28 @@ "1": "Eu suponho que deve parecer que estou fazendo algo terrível. Eu não espero que você entenda.\n$Mas eu devo fornecer à região de Galar energia ilimitada para garantir prosperidade eterna." } }, + "star_boss_penny_1": { + "encounter": { + "1": "Eu sou a grande chefe da Equipe Estrela. Meu nome é Cassiopeia. \n$Agora, ajoelhe-se diante do poder esmagador da fundadora da Equipe Estrela!" + }, + "victory": { + "1": "... ... .." + }, + "defeat": { + "1": "Heh..." + } + }, + "star_boss_penny_2": { + "encounter": { + "1": "Não vou me segurar nesta batalha! Vou permanecer fiel ao código da Equipe Estrela! \n$Meu poder Veevee vai te reduzir a pó estelar!" + }, + "victory": { + "1": "...Agora acabou tudo." + }, + "defeat": { + "1": "Não posso te culpar por suas habilidades de batalha... Considerando como os chefes caíram diante de você." + } + }, "brock": { "encounter": { "1": "Minha especialidade em Pokémon do tipo Pedra vai te derrubar! Vamos lá!", diff --git a/src/locales/pt_BR/move.json b/src/locales/pt_BR/move.json index c463665f1ad..3c365a207ae 100644 --- a/src/locales/pt_BR/move.json +++ b/src/locales/pt_BR/move.json @@ -3129,7 +3129,7 @@ }, "auraWheel": { "name": "Aura Wheel", - "effect": "Morpeko ataca e aumenta sua Velocidade com a energia armazenada em suas bochechas. O tipo deste movimento muda dependendo da forma do usuário." + "effect": "O usuário ataca e aumenta sua Velocidade com a energia armazenada em suas bochechas. Se usado por Morpeko, o tipo deste movimento muda dependendo da forma do usuário." }, "breakingSwipe": { "name": "Breaking Swipe", diff --git a/src/locales/pt_BR/pokemon-form.json b/src/locales/pt_BR/pokemon-form.json index aa8f32a5d36..416f09c97bf 100644 --- a/src/locales/pt_BR/pokemon-form.json +++ b/src/locales/pt_BR/pokemon-form.json @@ -1,4 +1,5 @@ { + "pikachu": "Normal", "pikachuCosplay": "Cosplay", "pikachuCoolCosplay": "Cosplay Legal", "pikachuBeautyCosplay": "Cosplay Bonito", @@ -6,7 +7,9 @@ "pikachuSmartCosplay": "Cosplay Inteligente", "pikachuToughCosplay": "Cosplay Forte", "pikachuPartner": "Parceiro", + "eevee": "Normal", "eeveePartner": "Parceiro", + "pichu": "Normal", "pichuSpiky": "Orelha Espetada", "unownA": "A", "unownB": "B", @@ -36,36 +39,65 @@ "unownZ": "Z", "unownExclamation": "!", "unownQuestion": "?", + "castform": "Normal", "castformSunny": "Ensolarado", "castformRainy": "Chuvoso", "castformSnowy": "Nevado", "deoxysNormal": "Normal", + "deoxysAttack": "Ataque", + "deoxysDefense": "Defesa", + "deoxysSpeed": "Velocidade", "burmyPlant": "Vegetal", "burmySandy": "Arenoso", "burmyTrash": "Lixo", + "cherubiOvercast": "Nublado", + "cherubiSunshine": "Solar", "shellosEast": "Leste", "shellosWest": "Oeste", + "rotom": "Normal", "rotomHeat": "Calor", "rotomWash": "Lavagem", "rotomFrost": "Congelante", "rotomFan": "Ventilador", "rotomMow": "Corte", + "dialga": "Normal", + "dialgaOrigin": "Origem", + "palkia": "Normal", + "palkiaOrigin": "Origem", "giratinaAltered": "Alterado", + "giratinaOrigin": "Origem", "shayminLand": "Terrestre", + "shayminSky": "Céu", "basculinRedStriped": "Listras Vermelhas", "basculinBlueStriped": "Listras Azuis", "basculinWhiteStriped": "Listras Brancas", + "darumaka": "Padrão", + "darumakaZen": "Zen", "deerlingSpring": "Primavera", "deerlingSummer": "Verão", "deerlingAutumn": "Outono", "deerlingWinter": "Inverno", "tornadusIncarnate": "Materializado", + "tornadusTherian": "Therian", "thundurusIncarnate": "Materializado", + "thundurusTherian": "Therian", "landorusIncarnate": "Materializado", + "landorusTherian": "Therian", + "kyurem": "Normal", + "kyuremBlack": "Preto", + "kyuremWhite": "Branco", "keldeoOrdinary": "Comum", + "keldeoResolute": "Resoluto", "meloettaAria": "Ária", "meloettaPirouette": "Pirueta", + "genesect": "Normal", + "genesectShock": "Disco Elétrico", + "genesectBurn": "Disco Incendiante", + "genesectChill": "Disco Congelante", + "genesectDouse": "Disco Hídrico", + "froakie": "Normal", "froakieBattleBond": "Vínculo de Batalha", + "froakieAsh": "Ash", "scatterbugMeadow": "Prado", "scatterbugIcySnow": "Neve Congelada", "scatterbugPolar": "Polar", @@ -91,6 +123,7 @@ "flabebeOrange": "Laranja", "flabebeBlue": "Azul", "flabebeWhite": "Branca", + "furfrou": "Selvagem", "furfrouHeart": "Coração", "furfrouStar": "Estrela", "furfrouDiamond": "Diamante", @@ -100,6 +133,11 @@ "furfrouLaReine": "Aristocrático", "furfrouKabuki": "Kabuki", "furfrouPharaoh": "Faraó", + "espurrMale": "Macho", + "espurrFemale": "Fêmea", + "honedgeShiled": "Escudo", + "honedgeBlade": "Lâmina", + "pumpkaboo": "Normal", "pumpkabooSmall": "Pequeno", "pumpkabooLarge": "Grande", "pumpkabooSuper": "Extragrande", @@ -110,11 +148,37 @@ "zygarde50Pc": "Forma 50% Agrupada", "zygarde10Pc": "Forma 10% Agrupada", "zygardeComplete": "Forma Completa", + "hoopa": "Contido", + "hoopaUnbound": "Libertado", "oricorioBaile": "Flamenco", "oricorioPompom": "Pompom", "oricorioPau": "Hula", "oricorioSensu": "Leque", + "rockruff": "Normal", "rockruffOwnTempo": "Próprio Tempo", + "rockruffMidday": "Diurno", + "rockruffMidnight": "Noturno", + "rockruffDusk": "Crepúsculo", + "wishiwashi": "Individual", + "wishiwashiSchool": "Cardume", + "typeNullNormal": "Tipo: Normal", + "typeNullFighting": "Tipo: Lutador", + "typeNullFlying": "Tipo: Voador", + "typeNullPoison": "Tipo: Veneno", + "typeNullGround": "Tipo: Terra", + "typeNullRock": "Tipo: Pedra", + "typeNullBug": "Tipo: Inseto", + "typeNullGhost": "Tipo: Fantasma", + "typeNullSteel": "Tipo: Aço", + "typeNullFire": "Tipo: Fogo", + "typeNullWater": "Tipo: Água", + "typeNullGrass": "Tipo: Grama", + "typeNullElectric": "Tipo: Elétrico", + "typeNullPsychic": "Tipo: Psíquico", + "typeNullIce": "Tipo: Gelo", + "typeNullDragon": "Tipo: Dragão", + "typeNullDark": "Tipo: Sombrio", + "typeNullFairy": "Tipo: Fada", "miniorRedMeteor": "Meteoro Vermelho", "miniorOrangeMeteor": "Meteoro Laranja", "miniorYellowMeteor": "Meteoro Amarelo", @@ -131,25 +195,66 @@ "miniorViolet": "Violeta", "mimikyuDisguised": "Disfarçado", "mimikyuBusted": "Descoberto", + "necrozma": "Normal", + "necrozmaDuskMane": "Juba Crepúsculo", + "necrozmaDawnWings": "Asas Alvorada", + "necrozmaUltra": "Ultra", + "magearna": "Normal", "magearnaOriginal": "Original", + "marshadow": "Normal", "marshadowZenith": "Zênite", + "cramorant": "Normal", + "cramorantGulping": "Engolidor", + "cramorantGorging": "Devorador", + "toxelAmped": "Agudo", + "toxelLowkey": "Grave", "sinisteaPhony": "Falsificado", "sinisteaAntique": "Autêntico", + "milceryVanillaCream": "Creme de Baunilha", + "milceryRubyCream": "Creme Rubi", + "milceryMatchaCream": "Creme de Chá Verde", + "milceryMintCream": "Creme de Menta", + "milceryLemonCream": "Creme de Lima", + "milcerySaltedCream": "Creme Salgado", + "milceryRubySwirl": "Mistura Rubi", + "milceryCaramelSwirl": "Mistura de Caramelo", + "milceryRainbowSwirl": "Mistura Tricolor", + "eiscue": "Cara de Gelo", "eiscueNoIce": "Descongelado", "indeedeeMale": "Macho", "indeedeeFemale": "Fêmea", "morpekoFullBelly": "Saciado", + "morpekoHangry": "Voraz", "zacianHeroOfManyBattles": "Herói Veterano", + "zacianCrowned": "Coroado", "zamazentaHeroOfManyBattles": "Herói Veterano", + "zamazentaCrowned": "Coroado", + "kubfuSingleStrike": "Golpe Decisivo", + "kubfuRapidStrike": "Golpe Fluido", + "zarude": "Normal", "zarudeDada": "Papa", + "calyrex": "Normal", + "calyrexIce": "Cavaleiro Glacial", + "calyrexShadow": "Cavaleiro Espectral", + "basculinMale": "Macho", + "basculinFemale": "Fêmea", "enamorusIncarnate": "Materializado", + "enamorusTherian": "Therian", + "lechonkMale": "Macho", + "lechonkFemale": "Fêmea", + "tandemausFour": "Família de Quatro", + "tandemausThree": "Família de Três", "squawkabillyGreenPlumage": "Plumas Verdes", "squawkabillyBluePlumage": "Plumas Azuis", "squawkabillyYellowPlumage": "Plumas Amarelas", "squawkabillyWhitePlumage": "Plumas Brancas", + "finizenZero": "Ingênuo", + "finizenHero": "Heroico", "tatsugiriCurly": "Curvado", "tatsugiriDroopy": "Caído", "tatsugiriStretchy": "Reto", + "dunsparceTwo": "Duplo", + "dunsparceThree": "Triplo", "gimmighoulChest": "Baú", "gimmighoulRoaming": "Perambulante", "koraidonApexBuild": "Forma Plena", @@ -164,6 +269,21 @@ "miraidonGlideMode": "Modo Aéreo", "poltchageistCounterfeit": "Imitação", "poltchageistArtisan": "Artesão", + "poltchageistUnremarkable": "Medíocre", + "poltchageistMasterpiece": "Excepcional", + "ogerponTealMask": "Máscara Turquesa", + "ogerponTealMaskTera": "Máscara Turquesa Terastalizada", + "ogerponWellspringMask": "Máscara Nascente", + "ogerponWellspringMaskTera": "Máscara Nascente Terastalizada", + "ogerponHearthflameMask": "Máscara Fornalha", + "ogerponHearthflameMaskTera": "Máscara Fornalha Terastalizada", + "ogerponCornerstoneMask": "Máscara Alicerce", + "ogerponCornerstoneMaskTera": "Máscara Alicerce Terastalizada", + "terpagos": "Normal", + "terpagosTerastal": "Teracristal", + "terpagosStellar": "Astral", + "galarDarumaka": "Padrão", + "galarDarumakaZen": "Zen", "paldeaTaurosCombat": "Combate", "paldeaTaurosBlaze": "Chamas", "paldeaTaurosAqua": "Aquático" diff --git a/src/locales/pt_BR/pokemon-summary.json b/src/locales/pt_BR/pokemon-summary.json index 4c427dbac4f..14b736a0cf2 100644 --- a/src/locales/pt_BR/pokemon-summary.json +++ b/src/locales/pt_BR/pokemon-summary.json @@ -11,7 +11,7 @@ "cancel": "Cancelar", "memoString": "Natureza {{natureFragment}},\n{{metFragment}}", "metFragment": { - "normal": "encontrado no Nv.{{level}},\n{{biome}}.", + "normal": "encontrado no Nv.{{level}},\n{{biome}}, Onda {{wave}}.", "apparently": "aparentemente encontrado no Nv.{{level}},\n{{biome}}." }, "natureFragment": { diff --git a/src/locales/pt_BR/splash-messages.json b/src/locales/pt_BR/splash-messages.json index 55c0b1b9e74..237b0f21202 100644 --- a/src/locales/pt_BR/splash-messages.json +++ b/src/locales/pt_BR/splash-messages.json @@ -1,5 +1,5 @@ { - "battlesWon": "Batalhas Ganhas!", + "battlesWon": "{{count, number}} Batalhas Ganhas!", "joinTheDiscord": "Junte-se ao Discord!", "infiniteLevels": "Níveis Infinitos!", "everythingStacks": "Tudo Acumula!", diff --git a/src/locales/pt_BR/trainer-classes.json b/src/locales/pt_BR/trainer-classes.json index 482265d00c6..e5d1e1bb4b1 100644 --- a/src/locales/pt_BR/trainer-classes.json +++ b/src/locales/pt_BR/trainer-classes.json @@ -126,6 +126,8 @@ "skull_grunts": "Capangas da Equipe Skull", "macro_grunt": "Treinador da Macro Cosmos", "macro_grunt_female": "Treinadora da Macro Cosmos", - "macro_grunts": "Treinadores da Macro Cosmos" - + "macro_grunts": "Treinadores da Macro Cosmos", + "star_grunt": "Capanga da Equipe Estrela", + "star_grunt_female": "Capanga da Equipe Estrela", + "star_grunts": "Capangas da Equipe Estrela" } diff --git a/src/locales/pt_BR/trainer-names.json b/src/locales/pt_BR/trainer-names.json index 5500e2ddb46..83557e70ee2 100644 --- a/src/locales/pt_BR/trainer-names.json +++ b/src/locales/pt_BR/trainer-names.json @@ -141,6 +141,12 @@ "faba": "Faba", "plumeria": "Plumeria", "oleana": "Oleana", + "giacomo": "Giacomo", + "mela": "Mela", + "atticus": "Atticus", + "ortega": "Ortega", + "eri": "Eri", + "maxie": "Maxie", "archie": "Archie", "cyrus": "Cyrus", @@ -149,6 +155,8 @@ "lusamine": "Lusamine", "guzma": "Guzma", "rose": "Rose", + "cassiopeia": "Penny", + "blue_red_double": "Blue & Red", "red_blue_double": "Red & Blue", "tate_liza_double": "Tate & Liza", diff --git a/src/locales/pt_BR/trainer-titles.json b/src/locales/pt_BR/trainer-titles.json index 701c2e143fa..d416e0225bf 100644 --- a/src/locales/pt_BR/trainer-titles.json +++ b/src/locales/pt_BR/trainer-titles.json @@ -19,6 +19,7 @@ "aether_boss": "Presidente Aether", "skull_boss": "Chefe da Equipe Skull", "macro_boss": "Presidente da Macro Cosmos", + "star_boss": "Líder da Equipe Estrela", "rocket_admin": "Admin da Equipe Rocket", "rocket_admin_female": "Admin da Equipe Rocket", @@ -34,5 +35,6 @@ "flare_admin_female": "Admin da Equipe Flare", "aether_admin": "Admin da Fundação Aether", "skull_admin": "Admin da Equipe Skull", - "macro_admin": "Macro Cosmos" + "macro_admin": "Macro Cosmos", + "star_admin": "Chefe de Esquadrão da Equipe Estrela" } diff --git a/src/locales/zh_CN/ability.json b/src/locales/zh_CN/ability.json index 31e3c08161d..0a81a9c6ad2 100644 --- a/src/locales/zh_CN/ability.json +++ b/src/locales/zh_CN/ability.json @@ -1237,6 +1237,6 @@ }, "poisonPuppeteer": { "name": "毒傀儡", - "description": "因桃歹郎的招式而陷入中毒状态的\n对手同时也会陷入混乱状态。" + "description": "因此宝可梦的招式而陷入中毒状态的对手\n同时也会陷入混乱状态。" } } diff --git a/src/locales/zh_CN/battler-tags.json b/src/locales/zh_CN/battler-tags.json index 7a01f5dff23..a7859380b7a 100644 --- a/src/locales/zh_CN/battler-tags.json +++ b/src/locales/zh_CN/battler-tags.json @@ -70,5 +70,8 @@ "stockpilingOnAdd": "{{pokemonNameWithAffix}}蓄力了{{stockpiledCount}}次!", "disabledOnAdd": "封住了{{pokemonNameWithAffix}}的\n{{moveName}}!", "disabledLapse": "{{pokemonNameWithAffix}}的\n定身法解除了!", - "tarShotOnAdd": "{{pokemonNameWithAffix}}\n变得怕火了!" + "tarShotOnAdd": "{{pokemonNameWithAffix}}\n变得怕火了!", + "substituteOnAdd": "{{pokemonNameWithAffix}}的\n替身出现了!", + "substituteOnHit": "替身代替{{pokemonNameWithAffix}}\n承受了攻击!", + "substituteOnRemove": "{{pokemonNameWithAffix}}的\n替身消失了……" } diff --git a/src/locales/zh_CN/bgm-name.json b/src/locales/zh_CN/bgm-name.json index 065347e3bb6..7c3f5e617ae 100644 --- a/src/locales/zh_CN/bgm-name.json +++ b/src/locales/zh_CN/bgm-name.json @@ -81,9 +81,11 @@ "battle_aether_grunt": "日月「战斗!以太基金会」", "battle_skull_grunt": "日月「战斗!骷髅队」", "battle_macro_grunt": "剑盾「战斗!马洛科蒙集团」", + "battle_star_grunt": "SV Team Star Battle", "battle_galactic_admin": "晶灿钻石·明亮珍珠「战斗!银河队干部」", "battle_skull_admin": "日月「战斗!骷髅队干部」", "battle_oleana": "剑盾「战斗!奥利薇」", + "battle_star_admin": "SV Team Star Boss", "battle_rocket_boss": "究极日月「战斗!坂木」", "battle_aqua_magma_boss": "Ω红宝石α蓝宝石「战斗!水梧桐・赤焰松」", "battle_galactic_boss": "晶灿钻石·明亮珍珠「战斗!赤日」", @@ -92,6 +94,7 @@ "battle_aether_boss": "日月「战斗!露莎米奈」", "battle_skull_boss": "日月「战斗!古兹马」", "battle_macro_boss": "剑盾「战斗!洛兹」", + "battle_star_boss": "SV Cassiopeia Battle", "abyss": "空之探险队「黑暗小丘」", "badlands": "空之探险队「枯竭之谷」", @@ -106,17 +109,17 @@ "forest": "空之探险队「黑暗森林」", "grass": "空之探险队「苹果森林」", "graveyard": "空之探险队「神秘森林」", - "ice_cave": "空之探险队「大冰山」", + "ice_cave": "Firel - -50°C", "island": "空之探险队「沿岸岩地」", "jungle": "Lmz - 丛林", "laboratory": "Firel - 研究所", - "lake": "空之探险队「水晶洞窟」", + "lake": "Lmz - Lake", "meadow": "空之探险队「天空顶端(森林)」", "metropolis": "Firel - 城市", "mountain": "空之探险队「角山」", - "plains": "空之探险队「天空顶端(草原)」", - "power_plant": "空之探险队「电气平原 深处」", - "ruins": "空之探险队「封印岩地 深处」", + "plains": "Firel - Route 888", + "power_plant": "Firel - The Klink", + "ruins": "Lmz - Ancient Ruins", "sea": "Andr06 - 海洋之秘", "seabed": "Firel - 海底", "slum": "Andr06 - 狡猾的雪吞虫", @@ -126,7 +129,7 @@ "tall_grass": "空之探险队「浓雾森林」", "temple": "空之探险队「守护洞穴」", "town": "空之探险队「随机迷宫3」", - "volcano": "空之探险队「热水洞窟」", + "volcano": "Firel - Twisturn Volcano", "wasteland": "空之探险队「梦幻高原」", "encounter_ace_trainer": "黑白 「视线!精英训练师」", "encounter_backpacker": "黑白 「视线!背包客」", diff --git a/src/locales/zh_CN/dialogue.json b/src/locales/zh_CN/dialogue.json index dd0fa3fb3cc..d23fd7379bb 100644 --- a/src/locales/zh_CN/dialogue.json +++ b/src/locales/zh_CN/dialogue.json @@ -715,12 +715,16 @@ "encounter": { "1": "你的对战生涯到此为止了。", "2": "你是一名训练师吧\n你没有干涉我们工作的权力!", - "3": "我是马洛科蒙集团的,要买马洛科蒙人寿保险吗。" + "3": "我是马洛科蒙集团的,要买马洛科蒙人寿保险吗。", + "4": "I found you! In that case, time for a Pokémon battle!", + "5": "An earful from Ms. Oleana is way worse than anything you can do!" }, "victory": { "1": "除了礼貌地撤退我似乎别无选择…", "2": "没法留住我的零花钱了,我又要财政赤字了…", - "3": "没人能比马洛科蒙集团的我们工作更卷!" + "3": "没人能比马洛科蒙集团的我们工作更卷!", + "4": "I even switched up my Pokémon...", + "5": "Battles didn't work... Only thing to do now is run!" } }, "oleana": { @@ -735,6 +739,73 @@ "3": "*叹气*奥利薇累累了……" } }, + "star_grunt": { + "encounter": { + "1": "We're Team Star, kid. We burn so bright, it hurts to look at us!", + "2": "We'll come at you full force - Hasta la vistaaar! ★", + "3": "If you don't clear out real quick-like, I'll hafta come at you in self-defense. You get me?", + "4": "Sorry, but if you don't turn yourself around here, amigo, we'll have to send you packing!", + "4_female": "Sorry, but if you don't turn yourself around here, amiga, we'll have to send you packing!", + "5": "Oh great. Here comes another rando to ruin my day." + }, + "victory": { + "1": "How come I'M the one seeing stars?!", + "2": "You're scary, kid. If you joined Team Star, you'd be looking down from the top in no time!", + "3": "I defended myself all right... But it wasn't enough!", + "4": "H-hasta la vistar... ★", + "5": "I didn't think grunt work for Team Star newbies would be this much of a chore..." + } + }, + "giacomo": { + "encounter": { + "1": "You don't really think things through, do ya? Declarin' war on Team Star is a real bad move.", + "2": "I'll play you a sick requiem as you crash and burn. Let's get this party staaarteeed!" + }, + "victory": { + "1": "Guess that's that...", + "2": "You turned my melody into a threnody..." + } + }, + "mela": { + "encounter": { + "1": "So you're the dope who picked a fight with Team Star... Prepare to get messed up.", + "2": "All riiight, BRING IT! I'll blow everythin' sky high!" + }, + "victory": { + "1": "Ugh. Is this really how it's gonna end? What a hassle...", + "2": "I burned through everythin' I had...and now I've sputtered out." + } + }, + "atticus": { + "encounter": { + "1": "You have some nerve baring your fangs at Team Star. Come, then, villainous wretch!", + "2": "Be warned—I shall spare thee no mercy! En garde!" + }, + "victory": { + "1": "Forgive me, my friends...", + "2": "You have utterly bested me. But thy victory stir'd no bitterness within me—such was its brilliance." + } + }, + "ortega": { + "encounter": { + "1": "I promise I'll play nice, so don't blame me when this battle sends you blubbering back home!", + "2": "I'll wipe that smug look off your face for sure! You're going down!" + }, + "victory": { + "1": "Ugh! How could I LOSE! What the HECK!", + "2": "Arrrrgggh! That strength of yours is SO. NOT. FAIR." + } + }, + "eri": { + "encounter": { + "1": "Doesn't matter who you are. I'll bury anyone who tries to take down Team Star!", + "2": "I give as good as I get—that's a promise! We'll see who's left standing in the end!" + }, + "victory": { + "1": "I'm so sorry, everyone...", + "2": "I gave it my all, but it wasn't enough—I wasn't enough..." + } + }, "rocket_boss_giovanni_1": { "encounter": { "1": "我不得不说,能来到这里,你的确很不简单!" @@ -922,6 +993,28 @@ "1": "你完全不理解!" } }, + "star_boss_penny_1": { + "encounter": { + "1": "I'm the big boss of Team Star. The name's Cassiopeia. \n$Now, bow down before the overwhelming might of Team Star's founder!" + }, + "victory": { + "1": "... ... .." + }, + "defeat": { + "1": "Heh..." + } + }, + "star_boss_penny_2": { + "encounter": { + "1": "I won't hold back in this battle! I'll stay true to Team Star's code! \n$My Veevee power will crush you into stardust!" + }, + "victory": { + "1": "...It's all over now." + }, + "defeat": { + "1": "I can't fault you on your battle skills at all... Considering how the bosses fell at your hands." + } + }, "macro_boss_rose_2": { "encounter": { "1": "我致力于解决伽勒尔的能源问题\n——当然也是全世界的能源问题。\n$我的经验与成果,造就了马洛科蒙集团,证明了我的正确与成功!\n$就算输了,我也不会改变主意的……" diff --git a/src/locales/zh_CN/move.json b/src/locales/zh_CN/move.json index 5974271abb2..d20b09f02be 100644 --- a/src/locales/zh_CN/move.json +++ b/src/locales/zh_CN/move.json @@ -3129,7 +3129,7 @@ }, "auraWheel": { "name": "气场轮", - "effect": "用储存在颊囊里的能量进行攻击,\n并提高自己的速度。其属性会随着\n莫鲁贝可的样子而改变" + "effect": "用储存在颊囊里的能量进行攻击,\n并提高自己的速度。如果由莫鲁贝可使用,\n其属性会随着它的样子而改变" }, "breakingSwipe": { "name": "广域破坏", diff --git a/src/locales/zh_CN/pokemon-form.json b/src/locales/zh_CN/pokemon-form.json index e77f9bdb9fa..c2ad12bbf16 100644 --- a/src/locales/zh_CN/pokemon-form.json +++ b/src/locales/zh_CN/pokemon-form.json @@ -1,4 +1,5 @@ { + "pikachu": "Normal", "pikachuCosplay": "换装", "pikachuCoolCosplay": "摇滚巨星", "pikachuBeautyCosplay": "贵妇", @@ -6,7 +7,9 @@ "pikachuSmartCosplay": "博士", "pikachuToughCosplay": "面罩摔跤手", "pikachuPartner": "搭档", + "eevee": "Normal", "eeveePartner": "搭档", + "pichu": "Normal", "pichuSpiky": "刺刺耳", "unownA": "A", "unownB": "B", @@ -36,36 +39,65 @@ "unownZ": "Z", "unownExclamation": "!", "unownQuestion": "?", + "castform": "Normal Form", "castformSunny": "晴天", "castformRainy": "雨天", "castformSnowy": "雪天", "deoxysNormal": "普通", + "deoxysAttack": "Attack", + "deoxysDefense": "Defense", + "deoxysSpeed": "Speed", "burmyPlant": "草木蓑衣", "burmySandy": "砂土蓑衣", "burmyTrash": "垃圾蓑衣", + "cherubiOvercast": "Overcast", + "cherubiSunshine": "Sunshine", "shellosEast": "东海", "shellosWest": "西海", + "rotom": "Normal", "rotomHeat": "加热", "rotomWash": "清洗", "rotomFrost": "结冰", "rotomFan": "旋转", "rotomMow": "切割", + "dialga": "Normal", + "dialgaOrigin": "Origin", + "palkia": "Normal", + "palkiaOrigin": "Origin", "giratinaAltered": "别种", + "giratinaOrigin": "Origin", "shayminLand": "陆上", + "shayminSky": "Sky", "basculinRedStriped": "红条纹", "basculinBlueStriped": "蓝条纹", "basculinWhiteStriped": "白条纹", + "darumaka": "Standard Mode", + "darumakaZen": "Zen", "deerlingSpring": "春天", "deerlingSummer": "夏天", "deerlingAutumn": "秋天", "deerlingWinter": "冬天", "tornadusIncarnate": "化身", + "tornadusTherian": "Therian", "thundurusIncarnate": "化身", + "thundurusTherian": "Therian", "landorusIncarnate": "化身", + "landorusTherian": "Therian", + "kyurem": "Normal", + "kyuremBlack": "Black", + "kyuremWhite": "White", "keldeoOrdinary": "通常", + "keldeoResolute": "Resolute", "meloettaAria": "歌声", "meloettaPirouette": "舞步形态", + "genesect": "Normal", + "genesectShock": "Shock Drive", + "genesectBurn": "Burn Drive", + "genesectChill": "Chill Drive", + "genesectDouse": "Douse Drive", + "froakie": "Normal", "froakieBattleBond": "牵绊变身", + "froakieAsh": "Ash", "scatterbugMeadow": "花园花纹", "scatterbugIcySnow": "冰雪花纹", "scatterbugPolar": "雪国花纹", @@ -91,6 +123,7 @@ "flabebeOrange": "橙花", "flabebeBlue": "蓝花", "flabebeWhite": "白花", + "furfrou": "Natural Form", "furfrouHeart": "心形造型", "furfrouStar": "星形造型", "furfrouDiamond": "菱形造型", @@ -100,6 +133,11 @@ "furfrouLaReine": "女王造型", "furfrouKabuki": "歌舞伎造型", "furfrouPharaoh": "国王造型", + "espurrMale": "Male", + "espurrFemale": "Female", + "honedgeShiled": "Shield", + "honedgeBlade": "Blade", + "pumpkaboo": "Average Size", "pumpkabooSmall": "小尺寸", "pumpkabooLarge": "大尺寸", "pumpkabooSuper": "特大尺寸", @@ -110,11 +148,37 @@ "zygarde50Pc": "50%形态 群聚变形", "zygarde10Pc": "10%形态 群聚变形", "zygardeComplete": "完全体形态", + "hoopa": "Confined", + "hoopaUnbound": "Unbound", "oricorioBaile": "热辣热辣风格", "oricorioPompom": "啪滋啪滋风格", "oricorioPau": "呼拉呼拉风格", "oricorioSensu": "轻盈轻盈风格", + "rockruff": "Normal", "rockruffOwnTempo": "特殊岩狗狗", + "rockruffMidday": "Midday", + "rockruffMidnight": "Midnight", + "rockruffDusk": "Dusk", + "wishiwashi": "Solo Form", + "wishiwashiSchool": "School", + "typeNullNormal": "Type: Normal", + "typeNullFighting": "Type: Fighting", + "typeNullFlying": "Type: Flying", + "typeNullPoison": "Type: Poison", + "typeNullGround": "Type: Ground", + "typeNullRock": "Type: Rock", + "typeNullBug": "Type: Bug", + "typeNullGhost": "Type: Ghost", + "typeNullSteel": "Type: Steel", + "typeNullFire": "Type: Fire", + "typeNullWater": "Type: Water", + "typeNullGrass": "Type: Grass", + "typeNullElectric": "Type: Electric", + "typeNullPsychic": "Type: Psychic", + "typeNullIce": "Type: Ice", + "typeNullDragon": "Type: Dragon", + "typeNullDark": "Type: Dark", + "typeNullFairy": "Type: Fairy", "miniorRedMeteor": "红色核心", "miniorOrangeMeteor": "橙色核心", "miniorYellowMeteor": "黄色核心", @@ -131,25 +195,66 @@ "miniorViolet": "紫色", "mimikyuDisguised": "化形", "mimikyuBusted": "现形", + "necrozma": "Normal", + "necrozmaDuskMane": "Dusk Mane", + "necrozmaDawnWings": "Dawn Wings", + "necrozmaUltra": "Ultra", + "magearna": "Normal", "magearnaOriginal": "500年前的颜色", + "marshadow": "Normal", "marshadowZenith": "全力", + "cramorant": "Normal", + "cramorantGulping": "Gulping Form", + "cramorantGorging": "Gorging Form", + "toxelAmped": "Amped Form", + "toxelLowkey": "Low-Key Form", "sinisteaPhony": "赝品", "sinisteaAntique": "真品", + "milceryVanillaCream": "Vanilla Cream", + "milceryRubyCream": "Ruby Cream", + "milceryMatchaCream": "Matcha Cream", + "milceryMintCream": "Mint Cream", + "milceryLemonCream": "Lemon Cream", + "milcerySaltedCream": "Salted Cream", + "milceryRubySwirl": "Ruby Swirl", + "milceryCaramelSwirl": "Caramel Swirl", + "milceryRainbowSwirl": "Rainbow Swirl", + "eiscue": "Ice Face", "eiscueNoIce": "解冻头", "indeedeeMale": "雄性", "indeedeeFemale": "雌性", "morpekoFullBelly": "满腹花纹", + "morpekoHangry": "Hangry", "zacianHeroOfManyBattles": "百战勇者", + "zacianCrowned": "Crowned", "zamazentaHeroOfManyBattles": "百战勇者", + "zamazentaCrowned": "Crowned", + "kubfuSingleStrike": "Single Strike", + "kubfuRapidStrike": "Rapid Strike", + "zarude": "Normal", "zarudeDada": "老爹", + "calyrex": "Normal", + "calyrexIce": "Ice Rider", + "calyrexShadow": "Shadow Rider", + "basculinMale": "Male", + "basculinFemale": "Female", "enamorusIncarnate": "化身", + "enamorusTherian": "Therian", + "lechonkMale": "Male", + "lechonkFemale": "Female", + "tandemausFour": "Family of Four", + "tandemausThree": "Family of Three", "squawkabillyGreenPlumage": "绿羽毛", "squawkabillyBluePlumage": "蓝羽毛", "squawkabillyYellowPlumage": "黄羽毛", "squawkabillyWhitePlumage": "白羽毛", + "finizenZero": "Zero", + "finizenHero": "Hero", "tatsugiriCurly": "上弓姿势", "tatsugiriDroopy": "下垂姿势", "tatsugiriStretchy": "平挺姿势", + "dunsparceTwo": "Two-Segment", + "dunsparceThree": "Three-Segment", "gimmighoulChest": "宝箱形态", "gimmighoulRoaming": "徒步形态", "koraidonApexBuild": "顶尖形态", @@ -164,6 +269,21 @@ "miraidonGlideMode": "滑翔模式", "poltchageistCounterfeit": "冒牌货", "poltchageistArtisan": "高档货", + "poltchageistUnremarkable": "Unremarkable", + "poltchageistMasterpiece": "Masterpiece", + "ogerponTealMask": "Teal Mask", + "ogerponTealMaskTera": "Teal Mask Terastallized", + "ogerponWellspringMask": "Wellspring Mask", + "ogerponWellspringMaskTera": "Wellspring Mask Terastallized", + "ogerponHearthflameMask": "Hearthflame Mask", + "ogerponHearthflameMaskTera": "Hearthflame Mask Terastallized", + "ogerponCornerstoneMask": "Cornerstone Mask", + "ogerponCornerstoneMaskTera": "Cornerstone Mask Terastallized", + "terpagos": "Normal Form", + "terpagosTerastal": "Terastal", + "terpagosStellar": "Stellar", + "galarDarumaka": "Standard Mode", + "galarDarumakaZen": "Zen", "paldeaTaurosCombat": "斗战种", "paldeaTaurosBlaze": "火炽种", "paldeaTaurosAqua": "水澜种" diff --git a/src/locales/zh_CN/splash-messages.json b/src/locales/zh_CN/splash-messages.json index 4d2d208edfd..24981513afe 100644 --- a/src/locales/zh_CN/splash-messages.json +++ b/src/locales/zh_CN/splash-messages.json @@ -1,5 +1,5 @@ { - "battlesWon": "场胜利!", + "battlesWon": "{{count, number}} 场胜利!", "joinTheDiscord": "加入Discord!", "infiniteLevels": "等级无限!", "everythingStacks": "道具全部叠加!", diff --git a/src/locales/zh_CN/trainer-classes.json b/src/locales/zh_CN/trainer-classes.json index 3a50acf5c01..ba126b9ec65 100644 --- a/src/locales/zh_CN/trainer-classes.json +++ b/src/locales/zh_CN/trainer-classes.json @@ -126,5 +126,8 @@ "skull_grunts": "骷髅队手下", "macro_grunt": "马洛科蒙训练师", "macro_grunt_female": "马洛科蒙训练师", - "macro_grunts": "马洛科蒙训练师" + "macro_grunts": "马洛科蒙训练师", + "star_grunt": "Star Grunt", + "star_grunt_female": "Star Grunt", + "star_grunts": "Star Grunts" } diff --git a/src/locales/zh_CN/trainer-names.json b/src/locales/zh_CN/trainer-names.json index 9e03a514ff7..33cf15e8304 100644 --- a/src/locales/zh_CN/trainer-names.json +++ b/src/locales/zh_CN/trainer-names.json @@ -30,10 +30,6 @@ "crasher_wake": "吉宪", "fantina": "梅丽莎", "byron": "东瓜", - "faba": "扎奥博", - "plumeria": "布尔美丽", - "oleana": "奥莉薇", - "candice": "小菘", "volkner": "电次", "cilan": "天桐", @@ -142,6 +138,15 @@ "rood": "罗德", "xerosic": "库瑟洛斯奇", "bryony": "芭菈", + "faba": "扎奥博", + "plumeria": "布尔美丽", + "oleana": "奥莉薇", + "giacomo": "Giacomo", + "mela": "Mela", + "atticus": "Atticus", + "ortega": "Ortega", + "eri": "Eri", + "maxie": "赤焰松", "archie": "水梧桐", "cyrus": "赤日", @@ -150,6 +155,7 @@ "lusamine": "露莎米奈", "guzma": "古兹马", "rose": "洛兹", + "cassiopeia": "牡丹", "blue_red_double": "青绿 & 赤红", "red_blue_double": "赤红 & 青绿", diff --git a/src/locales/zh_CN/trainer-titles.json b/src/locales/zh_CN/trainer-titles.json index 07654ec8fc3..9ef11aafbfa 100644 --- a/src/locales/zh_CN/trainer-titles.json +++ b/src/locales/zh_CN/trainer-titles.json @@ -19,6 +19,7 @@ "aether_boss": "以太基金会理事长", "skull_boss": "骷髅队老大", "macro_boss": "马洛科蒙总裁", + "star_boss": "Team Star Leader", "rocket_admin": "火箭队干部", "rocket_admin_female": "火箭队干部", @@ -34,5 +35,6 @@ "flare_admin_female": "闪焰队干部", "aether_admin": "以太基金会干部", "skull_admin": "骷髅队干部", - "macro_admin": "马洛科蒙干部" + "macro_admin": "马洛科蒙干部", + "star_admin": "Team Star Squad Boss" } diff --git a/src/locales/zh_TW/ability.json b/src/locales/zh_TW/ability.json index 21206c5362a..5d05a6c4e73 100644 --- a/src/locales/zh_TW/ability.json +++ b/src/locales/zh_TW/ability.json @@ -1237,6 +1237,6 @@ }, "poisonPuppeteer": { "name": "毒傀儡", - "description": "因為桃歹郎的招式而陷入中\n毒狀態的對手同時也會陷入\n混亂狀態。" + "description": "因為此寶可夢的招式而陷入中毒狀態的對手\n同時也會陷入混亂狀態。" } } diff --git a/src/locales/zh_TW/battler-tags.json b/src/locales/zh_TW/battler-tags.json index 9653db1077a..49b19f5efdc 100644 --- a/src/locales/zh_TW/battler-tags.json +++ b/src/locales/zh_TW/battler-tags.json @@ -70,5 +70,8 @@ "stockpilingOnAdd": "{{pokemonNameWithAffix}}蓄力了{{stockpiledCount}}次!", "disabledOnAdd": "封住了{{pokemonNameWithAffix}}的\n{moveName}}!", "disabledLapse": "{{pokemonNameWithAffix}}的\n定身法解除了!", - "tarShotOnAdd": "{{pokemonNameWithAffix}}\n變得怕火了!" + "tarShotOnAdd": "{{pokemonNameWithAffix}}\n變得怕火了!", + "substituteOnAdd": "{{pokemonNameWithAffix}}的\n替身出現了!", + "substituteOnHit": "替身代替{{pokemonNameWithAffix}}承受了攻擊!", + "substituteOnRemove": "{{pokemonNameWithAffix}}的\n替身消失了……" } diff --git a/src/locales/zh_TW/bgm-name.json b/src/locales/zh_TW/bgm-name.json index e8546750977..34c9ed6806e 100644 --- a/src/locales/zh_TW/bgm-name.json +++ b/src/locales/zh_TW/bgm-name.json @@ -83,6 +83,9 @@ "battle_galactic_boss": "晶燦鑽石·明亮珍珠「戰鬥!赤日」", "battle_plasma_boss": "黑2白2「戰鬥!魁奇思」", "battle_flare_boss": "XY「戰鬥!弗拉達利」", + "battle_star_grunt": "SV Team Star Battle", + "battle_star_admin": "SV Team Star Boss", + "battle_star_boss": "SV Cassiopeia Battle", "abyss": "空之探險隊「黑暗小丘」", "badlands": "空之探險隊「枯竭之谷」", @@ -97,17 +100,17 @@ "forest": "空之探險隊「黑暗森林」", "grass": "空之探險隊「蘋果森林」", "graveyard": "空之探險隊「神秘森林」", - "ice_cave": "空之探險隊「大冰山」", + "ice_cave": "Firel - -50°C", "island": "空之探險隊「沿岸岩地」", "jungle": "Lmz - 叢林", "laboratory": "Firel - 研究所", - "lake": "空之探險隊「水晶洞窟」", + "lake": "Lmz - Lake", "meadow": "空之探險隊「天空頂端(森林)」", "metropolis": "Firel - 城市", "mountain": "空之探險隊「角山」", - "plains": "空之探險隊「天空頂端(草原)」", - "power_plant": "空之探險隊「電氣平原 深處」", - "ruins": "空之探險隊「封印岩地 深處」", + "plains": "Firel - Route 888", + "power_plant": "Firel - The Klink", + "ruins": "Lmz - Ancient Ruins", "sea": "Andr06 - 海洋之秘", "seabed": "Firel - 海底", "slum": "Andr06 - 狡猾的雪吞蟲", @@ -117,7 +120,7 @@ "tall_grass": "空之探險隊「濃霧森林」", "temple": "空之探險隊「守護洞穴」", "town": "空之探險隊「隨機迷宮3」", - "volcano": "空之探險隊「熱水洞窟」", + "volcano": "Firel - Twisturn Volcano", "wasteland": "空之探險隊「夢幻高原」", "encounter_ace_trainer": "黑白 「視線!精英訓練師」", diff --git a/src/locales/zh_TW/move.json b/src/locales/zh_TW/move.json index b8c4ec05033..3a8956b1bf9 100644 --- a/src/locales/zh_TW/move.json +++ b/src/locales/zh_TW/move.json @@ -3129,7 +3129,7 @@ }, "auraWheel": { "name": "氣場輪", - "effect": "用儲存在頰囊裏的能量進行\n攻擊,並提高自己的速度。\n其屬性會隨着莫魯貝可的樣\n子而改變" + "effect": "用儲存在頰囊裏的能量進行\n攻擊,並提高自己的速度。\n如果由莫魯貝可使用,\n其屬性會隨着它的樣子而改變" }, "breakingSwipe": { "name": "廣域破壞", diff --git a/src/locales/zh_TW/pokemon-form.json b/src/locales/zh_TW/pokemon-form.json index f1fb4dff599..104f8f5ea49 100644 --- a/src/locales/zh_TW/pokemon-form.json +++ b/src/locales/zh_TW/pokemon-form.json @@ -1,4 +1,5 @@ { + "pikachu": "Normal", "pikachuCosplay": "換裝", "pikachuCoolCosplay": "搖滾巨星", "pikachuBeautyCosplay": "貴婦", @@ -6,7 +7,9 @@ "pikachuSmartCosplay": "博士", "pikachuToughCosplay": "面罩摔跤手", "pikachuPartner": "搭檔", + "eevee": "Normal", "eeveePartner": "搭檔", + "pichu": "Normal", "pichuSpiky": "刺刺耳", "unownA": "A", "unownB": "B", @@ -36,36 +39,65 @@ "unownZ": "Z", "unownExclamation": "!", "unownQuestion": "?", + "castform": "Normal Form", "castformSunny": "晴天", "castformRainy": "雨天", "castformSnowy": "雪天", "deoxysNormal": "普通", + "deoxysAttack": "Attack", + "deoxysDefense": "Defense", + "deoxysSpeed": "Speed", "burmyPlant": "草木蓑衣", "burmySandy": "砂土蓑衣", "burmyTrash": "垃圾蓑衣", + "cherubiOvercast": "Overcast", + "cherubiSunshine": "Sunshine", "shellosEast": "東海", "shellosWest": "西海", + "rotom": "Normal", "rotomHeat": "加熱", "rotomWash": "清洗", "rotomFrost": "結冰", "rotomFan": "旋轉", "rotomMow": "切割", + "dialga": "Normal", + "dialgaOrigin": "Origin", + "palkia": "Normal", + "palkiaOrigin": "Origin", "giratinaAltered": "別種", + "giratinaOrigin": "Origin", "shayminLand": "陸上", + "shayminSky": "Sky", "basculinRedStriped": "紅條紋", "basculinBlueStriped": "藍條紋", "basculinWhiteStriped": "白條紋", + "darumaka": "Standard Mode", + "darumakaZen": "Zen", "deerlingSpring": "春天", "deerlingSummer": "夏天", "deerlingAutumn": "秋天", "deerlingWinter": "冬天", "tornadusIncarnate": "化身", + "tornadusTherian": "Therian", "thundurusIncarnate": "化身", + "thundurusTherian": "Therian", "landorusIncarnate": "化身", + "landorusTherian": "Therian", + "kyurem": "Normal", + "kyuremBlack": "Black", + "kyuremWhite": "White", "keldeoOrdinary": "通常", + "keldeoResolute": "Resolute", "meloettaAria": "歌聲", "meloettaPirouette": "舞步形態", + "genesect": "Normal", + "genesectShock": "Shock Drive", + "genesectBurn": "Burn Drive", + "genesectChill": "Chill Drive", + "genesectDouse": "Douse Drive", + "froakie": "Normal", "froakieBattleBond": "牽絆變身", + "froakieAsh": "Ash", "scatterbugMeadow": "花園花紋", "scatterbugIcySnow": "冰雪花紋", "scatterbugPolar": "雪國花紋", @@ -91,6 +123,7 @@ "flabebeOrange": "橙花", "flabebeBlue": "藍花", "flabebeWhite": "白花", + "furfrou": "Natural Form", "furfrouHeart": "心形造型", "furfrouStar": "星形造型", "furfrouDiamond": "菱形造型", @@ -100,6 +133,11 @@ "furfrouLaReine": "女王造型", "furfrouKabuki": "歌舞伎造型", "furfrouPharaoh": "國王造型", + "espurrMale": "Male", + "espurrFemale": "Female", + "honedgeShiled": "Shield", + "honedgeBlade": "Blade", + "pumpkaboo": "Average Size", "pumpkabooSmall": "小尺寸", "pumpkabooLarge": "大尺寸", "pumpkabooSuper": "特大尺寸", @@ -110,11 +148,37 @@ "zygarde50Pc": "50%形態 群聚變形", "zygarde10Pc": "10%形態 群聚變形", "zygardeComplete": "完全體形態", + "hoopa": "Confined", + "hoopaUnbound": "Unbound", "oricorioBaile": "熱辣熱辣風格", "oricorioPompom": "啪滋啪滋風格", "oricorioPau": "呼拉呼拉風格", "oricorioSensu": "輕盈輕盈風格", + "rockruff": "Normal", "rockruffOwnTempo": "特殊岩狗狗", + "rockruffMidday": "Midday", + "rockruffMidnight": "Midnight", + "rockruffDusk": "Dusk", + "wishiwashi": "Solo Form", + "wishiwashiSchool": "School", + "typeNullNormal": "Type: Normal", + "typeNullFighting": "Type: Fighting", + "typeNullFlying": "Type: Flying", + "typeNullPoison": "Type: Poison", + "typeNullGround": "Type: Ground", + "typeNullRock": "Type: Rock", + "typeNullBug": "Type: Bug", + "typeNullGhost": "Type: Ghost", + "typeNullSteel": "Type: Steel", + "typeNullFire": "Type: Fire", + "typeNullWater": "Type: Water", + "typeNullGrass": "Type: Grass", + "typeNullElectric": "Type: Electric", + "typeNullPsychic": "Type: Psychic", + "typeNullIce": "Type: Ice", + "typeNullDragon": "Type: Dragon", + "typeNullDark": "Type: Dark", + "typeNullFairy": "Type: Fairy", "miniorRedMeteor": "紅色核心", "miniorOrangeMeteor": "橙色核心", "miniorYellowMeteor": "黃色核心", @@ -131,25 +195,66 @@ "miniorViolet": "紫色", "mimikyuDisguised": "化形", "mimikyuBusted": "現形", + "necrozma": "Normal", + "necrozmaDuskMane": "Dusk Mane", + "necrozmaDawnWings": "Dawn Wings", + "necrozmaUltra": "Ultra", + "magearna": "Normal", "magearnaOriginal": "500年前的顔色", + "marshadow": "Normal", "marshadowZenith": "全力", + "cramorant": "Normal", + "cramorantGulping": "Gulping Form", + "cramorantGorging": "Gorging Form", + "toxelAmped": "Amped Form", + "toxelLowkey": "Low-Key Form", "sinisteaPhony": "赝品", "sinisteaAntique": "真品", + "milceryVanillaCream": "Vanilla Cream", + "milceryRubyCream": "Ruby Cream", + "milceryMatchaCream": "Matcha Cream", + "milceryMintCream": "Mint Cream", + "milceryLemonCream": "Lemon Cream", + "milcerySaltedCream": "Salted Cream", + "milceryRubySwirl": "Ruby Swirl", + "milceryCaramelSwirl": "Caramel Swirl", + "milceryRainbowSwirl": "Rainbow Swirl", + "eiscue": "Ice Face", "eiscueNoIce": "解凍頭", "indeedeeMale": "雄性", "indeedeeFemale": "雌性", "morpekoFullBelly": "滿腹花紋", + "morpekoHangry": "Hangry", "zacianHeroOfManyBattles": "百戰勇者", + "zacianCrowned": "Crowned", "zamazentaHeroOfManyBattles": "百戰勇者", + "zamazentaCrowned": "Crowned", + "kubfuSingleStrike": "Single Strike", + "kubfuRapidStrike": "Rapid Strike", + "zarude": "Normal", "zarudeDada": "老爹", + "calyrex": "Normal", + "calyrexIce": "Ice Rider", + "calyrexShadow": "Shadow Rider", + "basculinMale": "Male", + "basculinFemale": "Female", "enamorusIncarnate": "化身", + "enamorusTherian": "Therian", + "lechonkMale": "Male", + "lechonkFemale": "Female", + "tandemausFour": "Family of Four", + "tandemausThree": "Family of Three", "squawkabillyGreenPlumage": "綠羽毛", "squawkabillyBluePlumage": "藍羽毛", "squawkabillyYellowPlumage": "黃羽毛", "squawkabillyWhitePlumage": "白羽毛", + "finizenZero": "Zero", + "finizenHero": "Hero", "tatsugiriCurly": "上弓姿勢", "tatsugiriDroopy": "下垂姿勢", "tatsugiriStretchy": "平挺姿勢", + "dunsparceTwo": "Two-Segment", + "dunsparceThree": "Three-Segment", "gimmighoulChest": "寶箱形態", "gimmighoulRoaming": "徒步形態", "koraidonApexBuild": "頂尖形態", @@ -164,6 +269,21 @@ "miraidonGlideMode":"滑翔模式", "poltchageistCounterfeit": "冒牌貨", "poltchageistArtisan": "高檔貨", + "poltchageistUnremarkable": "Unremarkable", + "poltchageistMasterpiece": "Masterpiece", + "ogerponTealMask": "Teal Mask", + "ogerponTealMaskTera": "Teal Mask Terastallized", + "ogerponWellspringMask": "Wellspring Mask", + "ogerponWellspringMaskTera": "Wellspring Mask Terastallized", + "ogerponHearthflameMask": "Hearthflame Mask", + "ogerponHearthflameMaskTera": "Hearthflame Mask Terastallized", + "ogerponCornerstoneMask": "Cornerstone Mask", + "ogerponCornerstoneMaskTera": "Cornerstone Mask Terastallized", + "terpagos": "Normal Form", + "terpagosTerastal": "Terastal", + "terpagosStellar": "Stellar", + "galarDarumaka": "Standard Mode", + "galarDarumakaZen": "Zen", "paldeaTaurosCombat": "鬥戰種", "paldeaTaurosBlaze": "火熾種", "paldeaTaurosAqua": "水瀾種" diff --git a/src/locales/zh_TW/pokemon-summary.json b/src/locales/zh_TW/pokemon-summary.json index ddbbea63a3a..331330f5bdd 100644 --- a/src/locales/zh_TW/pokemon-summary.json +++ b/src/locales/zh_TW/pokemon-summary.json @@ -12,7 +12,7 @@ "memoString": "{{natureFragment}} 性格,\n{{metFragment}}", "metFragment": { - "normal": "met at Lv{{level}},\n{{biome}}.", + "normal": "met at Lv{{level}},\n{{biome}}, Wave {{wave}}.", "apparently": "命中注定般地相遇于Lv.{{level}},\n{{biome}}。" }, "natureFragment": { diff --git a/src/locales/zh_TW/splash-messages.json b/src/locales/zh_TW/splash-messages.json index a25e7dab97b..60b03549c2f 100644 --- a/src/locales/zh_TW/splash-messages.json +++ b/src/locales/zh_TW/splash-messages.json @@ -1,5 +1,5 @@ { - "battlesWon": "勝利場數!", + "battlesWon": "{{count, number}} 勝利場數!", "joinTheDiscord": "加入Discord!", "infiniteLevels": "無限等級!", "everythingStacks": "所有效果都能疊加!", diff --git a/src/locales/zh_TW/trainer-classes.json b/src/locales/zh_TW/trainer-classes.json index d0b0fed7e5d..7469991171e 100644 --- a/src/locales/zh_TW/trainer-classes.json +++ b/src/locales/zh_TW/trainer-classes.json @@ -117,5 +117,8 @@ "plasma_grunts": "等离子队手下們", "flare_grunt": "闪焰队手下", "flare_grunt_female": "闪焰队手下", - "flare_grunts": "闪焰队手下們" + "flare_grunts": "闪焰队手下們", + "star_grunt": "Star Grunt", + "star_grunt_female": "Star Grunt", + "star_grunts": "Star Grunts" } diff --git a/src/locales/zh_TW/trainer-names.json b/src/locales/zh_TW/trainer-names.json index 04399cf19af..e3ce88d5a40 100644 --- a/src/locales/zh_TW/trainer-names.json +++ b/src/locales/zh_TW/trainer-names.json @@ -141,6 +141,11 @@ "faba": "扎奧博", "plumeria": "布爾美麗", "oleana": "奧利薇", + "giacomo": "Giacomo", + "mela": "Mela", + "atticus": "Atticus", + "ortega": "Ortega", + "eri": "Eri", "maxie": "赤焰松", "archie": "水梧桐", @@ -150,6 +155,7 @@ "lusamine": "露莎米奈", "guzma": "古茲馬", "rose": "洛茲", + "cassiopeia": "牡丹", "blue_red_double": "青綠 & 赤紅", "red_blue_double": "赤紅 & 青綠", diff --git a/src/locales/zh_TW/trainer-titles.json b/src/locales/zh_TW/trainer-titles.json index 80b2807e7b5..0931082acc0 100644 --- a/src/locales/zh_TW/trainer-titles.json +++ b/src/locales/zh_TW/trainer-titles.json @@ -16,6 +16,10 @@ "galactic_boss": "銀河隊老大", "plasma_boss": "等離子隊老大", "flare_boss": "閃焰隊老大", + "aether_boss": "Aether President", + "skull_boss": "Team Skull Boss", + "macro_boss": "Macro Cosmos President", + "star_boss": "Team Star Leader", "rocket_admin": "火箭隊幹部", "rocket_admin_female": "火箭隊幹部", @@ -28,5 +32,9 @@ "plasma_sage": "等離子隊賢人", "plasma_admin": "等離子隊幹部", "flare_admin": "閃焰隊幹部", - "flare_admin_female": "閃焰隊幹部" + "flare_admin_female": "閃焰隊幹部", + "aether_admin": "Aether Foundation Admin", + "skull_admin": "Team Skull Admin", + "macro_admin": "Macro Cosmos", + "star_admin": "Team Star Squad Boss" } diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index 48c0d66fc45..2b4f910034b 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -1,6 +1,7 @@ import * as Modifiers from "./modifier"; -import { AttackMove, allMoves, selfStatLowerMoves } from "../data/move"; -import { MAX_PER_TYPE_POKEBALLS, PokeballType, getPokeballCatchMultiplier, getPokeballName } from "../data/pokeball"; +import { MoneyMultiplierModifier } from "./modifier"; +import { allMoves, AttackMove, selfStatLowerMoves } from "../data/move"; +import { getPokeballCatchMultiplier, getPokeballName, MAX_PER_TYPE_POKEBALLS, PokeballType } from "../data/pokeball"; import Pokemon, { EnemyPokemon, PlayerPokemon, PokemonMove } from "../field/pokemon"; import { EvolutionItem, pokemonEvolutions } from "../data/pokemon-evolutions"; import { tmPoolTiers, tmSpecies } from "../data/tms"; @@ -9,17 +10,16 @@ import PartyUiHandler, { PokemonMoveSelectFilter, PokemonSelectFilter } from ".. import * as Utils from "../utils"; import { getBerryEffectDescription, getBerryName } from "../data/berry"; import { Unlockables } from "../system/unlockables"; -import { StatusEffect, getStatusEffectDescriptor } from "../data/status-effect"; +import { getStatusEffectDescriptor, StatusEffect } from "../data/status-effect"; import { SpeciesFormKey } from "../data/pokemon-species"; import BattleScene from "../battle-scene"; -import { VoucherType, getVoucherTypeIcon, getVoucherTypeName } from "../system/voucher"; -import { FormChangeItem, SpeciesFormChangeCondition, SpeciesFormChangeItemTrigger, pokemonFormChanges } from "../data/pokemon-forms"; +import { getVoucherTypeIcon, getVoucherTypeName, VoucherType } from "../system/voucher"; +import { FormChangeItem, pokemonFormChanges, SpeciesFormChangeCondition, SpeciesFormChangeItemTrigger } from "../data/pokemon-forms"; import { ModifierTier } from "./modifier-tier"; -import { Nature, getNatureName, getNatureStatMultiplier } from "#app/data/nature"; +import { getNatureName, getNatureStatMultiplier, Nature } from "#app/data/nature"; import i18next from "i18next"; import { getModifierTierTextTint } from "#app/ui/text"; import Overrides from "#app/overrides"; -import { MoneyMultiplierModifier } from "./modifier"; import { Abilities } from "#enums/abilities"; import { BattlerTagType } from "#enums/battler-tag-type"; import { BerryType } from "#enums/berry-type"; @@ -109,28 +109,31 @@ export class ModifierType { return null; } + /** + * Populates item id for ModifierType instance + * @param func + */ withIdFromFunc(func: ModifierTypeFunc): ModifierType { this.id = Object.keys(modifierTypes).find(k => modifierTypes[k] === func)!; // TODO: is this bang correct? return this; } /** - * Populates the tier field by performing a reverse lookup on the modifier pool specified by {@linkcode poolType} using the - * {@linkcode ModifierType}'s id. - * @param poolType the {@linkcode ModifierPoolType} to look into to derive the item's tier; defaults to {@linkcode ModifierPoolType.PLAYER} + * Populates item tier for ModifierType instance + * Tier is a necessary field for items that appear in player shop (determines the Pokeball visual they use) + * To find the tier, this function performs a reverse lookup of the item type in modifier pools + * @param poolType Default 'ModifierPoolType.PLAYER'. Which pool to lookup item tier from */ withTierFromPool(poolType: ModifierPoolType = ModifierPoolType.PLAYER): ModifierType { for (const tier of Object.values(getModifierPoolForType(poolType))) { for (const modifier of tier) { if (this.id === modifier.modifierType.id) { this.tier = modifier.modifierType.tier; - break; + return this; } } - if (this.tier) { - break; - } } + return this; } @@ -644,6 +647,55 @@ export class BaseStatBoosterModifierType extends PokemonHeldItemModifierType imp } } +/** + * Shuckle Juice item + */ +export class PokemonBaseStatTotalModifierType extends PokemonHeldItemModifierType implements GeneratedPersistentModifierType { + private readonly statModifier: integer; + + constructor(statModifier: integer) { + super("modifierType:ModifierType.MYSTERY_ENCOUNTER_SHUCKLE_JUICE", "berry_juice", (_type, args) => new Modifiers.PokemonBaseStatTotalModifier(this, (args[0] as Pokemon).id, this.statModifier)); + this.statModifier = statModifier; + } + + override getDescription(scene: BattleScene): string { + return i18next.t("modifierType:ModifierType.PokemonBaseStatTotalModifierType.description", { + increaseDecrease: i18next.t(this.statModifier >= 0 ? "modifierType:ModifierType.PokemonBaseStatTotalModifierType.extra.increase" : "modifierType:ModifierType.PokemonBaseStatTotalModifierType.extra.decrease"), + blessCurse: i18next.t(this.statModifier >= 0 ? "modifierType:ModifierType.PokemonBaseStatTotalModifierType.extra.blessed" : "modifierType:ModifierType.PokemonBaseStatTotalModifierType.extra.cursed"), + statValue: this.statModifier, + }); + } + + public getPregenArgs(): any[] { + return [ this.statModifier ]; + } +} + +/** + * Old Gateau item + */ +export class PokemonBaseStatFlatModifierType extends PokemonHeldItemModifierType implements GeneratedPersistentModifierType { + private readonly statModifier: integer; + private readonly stats: Stat[]; + + constructor(statModifier: integer, stats: Stat[]) { + super("modifierType:ModifierType.MYSTERY_ENCOUNTER_OLD_GATEAU", "old_gateau", (_type, args) => new Modifiers.PokemonBaseStatFlatModifier(this, (args[0] as Pokemon).id, this.statModifier, this.stats)); + this.statModifier = statModifier; + this.stats = stats; + } + + override getDescription(scene: BattleScene): string { + return i18next.t("modifierType:ModifierType.PokemonBaseStatFlatModifierType.description", { + stats: this.stats.map(stat => i18next.t(getStatKey(stat))).join("/"), + statValue: this.statModifier, + }); + } + + public getPregenArgs(): any[] { + return [ this.statModifier, this.stats ]; + } +} + class AllPokemonFullHpRestoreModifierType extends ModifierType { private descriptionKey: string; @@ -1057,11 +1109,11 @@ class EvolutionItemModifierTypeGenerator extends ModifierTypeGenerator { } const evolutionItemPool = [ - party.filter(p => pokemonEvolutions.hasOwnProperty(p.species.speciesId)).map(p => { + party.filter(p => pokemonEvolutions.hasOwnProperty(p.species.speciesId) && (!p.pauseEvolutions || p.species.speciesId === Species.SLOWPOKE || p.species.speciesId === Species.EEVEE)).map(p => { const evolutions = pokemonEvolutions[p.species.speciesId]; return evolutions.filter(e => e.item !== EvolutionItem.NONE && (e.evoFormKey === null || (e.preFormKey || "") === p.getFormKey()) && (!e.condition || e.condition.predicate(p))); }).flat(), - party.filter(p => p.isFusion() && p.fusionSpecies && pokemonEvolutions.hasOwnProperty(p.fusionSpecies.speciesId)).map(p => { + party.filter(p => p.isFusion() && p.fusionSpecies && pokemonEvolutions.hasOwnProperty(p.fusionSpecies.speciesId) && (!p.pauseEvolutions || p.fusionSpecies.speciesId === Species.SLOWPOKE || p.fusionSpecies.speciesId === Species.EEVEE)).map(p => { const evolutions = pokemonEvolutions[p.fusionSpecies!.speciesId]; return evolutions.filter(e => e.item !== EvolutionItem.NONE && (e.evoFormKey === null || (e.preFormKey || "") === p.getFusionFormKey()) && (!e.condition || e.condition.predicate(p))); }).flat() @@ -1236,6 +1288,21 @@ function skipInClassicAfterWave(wave: integer, defaultWeight: integer): Weighted function skipInLastClassicWaveOrDefault(defaultWeight: integer) : 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 (party: Pokemon[]) => { + const lures = party[0].scene.getModifiers(Modifiers.DoubleBattleChanceBoosterModifier); + return !(party[0].scene.gameMode.isClassic && party[0].scene.currentBattle.waveIndex === 199) && (lures.length === 0 || lures.filter(m => m.getMaxBattles() === maxBattles && m.getBattleCount() >= maxBattles * 0.6).length === 0) ? weight : 0; + }; +} + class WeightedModifierType { public modifierType: ModifierType; public weight: integer | WeightedModifierTypeWeightFunc; @@ -1320,6 +1387,8 @@ export const modifierTypes = { FORM_CHANGE_ITEM: () => new FormChangeItemModifierTypeGenerator(false), RARE_FORM_CHANGE_ITEM: () => new FormChangeItemModifierTypeGenerator(true), + EVOLUTION_TRACKER_GIMMIGHOUL: () => new PokemonHeldItemModifierType("modifierType:ModifierType.EVOLUTION_TRACKER_GIMMIGHOUL", "relic_gold", (type, _args) => new Modifiers.EvoTrackerModifier(type, (_args[0] as Pokemon).id, Species.GIMMIGHOUL, 10)), + MEGA_BRACELET: () => new ModifierType("modifierType:ModifierType.MEGA_BRACELET", "mega_bracelet", (type, _args) => new Modifiers.MegaEvolutionAccessModifier(type)), DYNAMAX_BAND: () => new ModifierType("modifierType:ModifierType.DYNAMAX_BAND", "dynamax_band", (type, _args) => new Modifiers.GigantamaxAccessModifier(type)), TERA_ORB: () => new ModifierType("modifierType:ModifierType.TERA_ORB", "tera_orb", (type, _args) => new Modifiers.TerastallizeAccessModifier(type)), @@ -1505,6 +1574,22 @@ export const modifierTypes = { ENEMY_STATUS_EFFECT_HEAL_CHANCE: () => new ModifierType("modifierType:ModifierType.ENEMY_STATUS_EFFECT_HEAL_CHANCE", "wl_full_heal", (type, _args) => new Modifiers.EnemyStatusEffectHealChanceModifier(type, 2.5, 10)), ENEMY_ENDURE_CHANCE: () => new EnemyEndureChanceModifierType("modifierType:ModifierType.ENEMY_ENDURE_CHANCE", "wl_reset_urge", 2), ENEMY_FUSED_CHANCE: () => new ModifierType("modifierType:ModifierType.ENEMY_FUSED_CHANCE", "wl_custom_spliced", (type, _args) => new Modifiers.EnemyFusionChanceModifier(type, 1)), + + MYSTERY_ENCOUNTER_SHUCKLE_JUICE: () => new ModifierTypeGenerator((party: Pokemon[], pregenArgs?: any[]) => { + if (pregenArgs) { + return new PokemonBaseStatTotalModifierType(pregenArgs[0] as integer); + } + return new PokemonBaseStatTotalModifierType(Utils.randSeedInt(20)); + }), + MYSTERY_ENCOUNTER_OLD_GATEAU: () => new ModifierTypeGenerator((party: Pokemon[], pregenArgs?: any[]) => { + if (pregenArgs) { + return new PokemonBaseStatFlatModifierType(pregenArgs[0] as integer, pregenArgs[1] as Stat[]); + } + return new PokemonBaseStatFlatModifierType(Utils.randSeedInt(20), [Stat.HP, Stat.ATK, Stat.DEF]); + }), + MYSTERY_ENCOUNTER_BLACK_SLUDGE: () => new ModifierType("modifierType:ModifierType.MYSTERY_ENCOUNTER_BLACK_SLUDGE", "black_sludge", (type, _args) => new Modifiers.HealShopCostModifier(type)), + MYSTERY_ENCOUNTER_MACHO_BRACE: () => new PokemonHeldItemModifierType("modifierType:ModifierType.MYSTERY_ENCOUNTER_MACHO_BRACE", "macho_brace", (type, args) => new Modifiers.PokemonIncrementingStatModifier(type, (args[0] as Pokemon).id)), + MYSTERY_ENCOUNTER_GOLDEN_BUG_NET: () => new ModifierType("modifierType:ModifierType.MYSTERY_ENCOUNTER_GOLDEN_BUG_NET", "golden_net", (type, _args) => new Modifiers.BoostBugSpawnModifier(type)), }; interface ModifierPool { @@ -1541,11 +1626,10 @@ const modifierPool: ModifierPool = { const thresholdPartyMemberCount = Math.min(party.filter(p => p.hp && 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, skipInLastClassicWaveOrDefault(2)), + 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), - new WeightedModifierType(modifierTypes.VOUCHER, (party: Pokemon[], rerollCount: integer) => !party[0].scene.gameMode.isDaily ? Math.max(1 - rerollCount, 0) : 0, 1), ].map(m => { m.setTier(ModifierTier.COMMON); return m; }), @@ -1599,7 +1683,7 @@ const modifierPool: ModifierPool = { return thresholdPartyMemberCount; }, 3), new WeightedModifierType(modifierTypes.DIRE_HIT, 4), - new WeightedModifierType(modifierTypes.SUPER_LURE, skipInLastClassicWaveOrDefault(4)), + new WeightedModifierType(modifierTypes.SUPER_LURE, lureWeightFunc(15, 4)), new WeightedModifierType(modifierTypes.NUGGET, skipInLastClassicWaveOrDefault(5)), new WeightedModifierType(modifierTypes.EVOLUTION_ITEM, (party: Pokemon[]) => { return Math.min(Math.ceil(party[0].scene.currentBattle.waveIndex / 15), 8); @@ -1616,13 +1700,13 @@ const modifierPool: ModifierPool = { new WeightedModifierType(modifierTypes.BASE_STAT_BOOSTER, 3), new WeightedModifierType(modifierTypes.TERA_SHARD, 1), new WeightedModifierType(modifierTypes.DNA_SPLICERS, (party: Pokemon[]) => party[0].scene.gameMode.isSplicedOnly && party.filter(p => !p.fusionSpecies).length > 1 ? 4 : 0), - new WeightedModifierType(modifierTypes.VOUCHER, (party: Pokemon[], rerollCount: integer) => !party[0].scene.gameMode.isDaily ? Math.max(3 - rerollCount * 3, 0) : 0, 3), + new WeightedModifierType(modifierTypes.VOUCHER, (party: Pokemon[], rerollCount: integer) => !party[0].scene.gameMode.isDaily ? Math.max(1 - rerollCount, 0) : 0, 1), ].map(m => { m.setTier(ModifierTier.GREAT); return m; }), [ModifierTier.ULTRA]: [ new WeightedModifierType(modifierTypes.ULTRA_BALL, (party: Pokemon[]) => (hasMaximumBalls(party, PokeballType.ULTRA_BALL)) ? 0 : 15, 15), - new WeightedModifierType(modifierTypes.MAX_LURE, skipInLastClassicWaveOrDefault(4)), + 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), @@ -1692,12 +1776,12 @@ const modifierPool: ModifierPool = { 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, skipInLastClassicWaveOrDefault(3)), + new WeightedModifierType(modifierTypes.LOCK_CAPSULE, (party: Pokemon[]) => party[0].scene.gameMode.isClassic ? 0 : 3), new WeightedModifierType(modifierTypes.SUPER_EXP_CHARM, skipInLastClassicWaveOrDefault(8)), new WeightedModifierType(modifierTypes.RARE_FORM_CHANGE_ITEM, (party: Pokemon[]) => Math.min(Math.ceil(party[0].scene.currentBattle.waveIndex / 50), 4) * 6, 24), new WeightedModifierType(modifierTypes.MEGA_BRACELET, (party: Pokemon[]) => Math.min(Math.ceil(party[0].scene.currentBattle.waveIndex / 50), 4) * 9, 36), new WeightedModifierType(modifierTypes.DYNAMAX_BAND, (party: Pokemon[]) => Math.min(Math.ceil(party[0].scene.currentBattle.waveIndex / 50), 4) * 9, 36), - new WeightedModifierType(modifierTypes.VOUCHER_PLUS, (party: Pokemon[], rerollCount: integer) => !party[0].scene.gameMode.isDaily ? Math.max(9 - rerollCount * 3, 0) : 0, 9), + new WeightedModifierType(modifierTypes.VOUCHER_PLUS, (party: Pokemon[], rerollCount: integer) => !party[0].scene.gameMode.isDaily ? Math.max(3 - rerollCount * 1, 0) : 0, 3), ].map(m => { m.setTier(ModifierTier.ROGUE); return m; }), @@ -1706,7 +1790,7 @@ const modifierPool: ModifierPool = { 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: integer) => !party[0].scene.gameMode.isDaily && !party[0].scene.gameMode.isEndless && !party[0].scene.gameMode.isSplicedOnly ? Math.max(15 - rerollCount * 5, 0) : 0, 15), + new WeightedModifierType(modifierTypes.VOUCHER_PREMIUM, (party: Pokemon[], rerollCount: integer) => !party[0].scene.gameMode.isDaily && !party[0].scene.gameMode.isEndless && !party[0].scene.gameMode.isSplicedOnly ? Math.max(5 - rerollCount * 2, 0) : 0, 5), new WeightedModifierType(modifierTypes.DNA_SPLICERS, (party: Pokemon[]) => !party[0].scene.gameMode.isSplicedOnly && party.filter(p => !p.fusionSpecies).length > 1 ? 24 : 0, 24), new WeightedModifierType(modifierTypes.MINI_BLACK_HOLE, (party: Pokemon[]) => (!party[0].scene.gameMode.isFreshStartChallenge() && party[0].scene.gameData.unlocks[Unlockables.MINI_BLACK_HOLE]) ? 1 : 0, 1), ].map(m => { @@ -1975,29 +2059,107 @@ export function regenerateModifierPoolThresholds(party: Pokemon[], poolType: Mod } } +export interface CustomModifierSettings { + guaranteedModifierTiers?: ModifierTier[]; + guaranteedModifierTypeOptions?: ModifierTypeOption[]; + guaranteedModifierTypeFuncs?: ModifierTypeFunc[]; + fillRemaining?: boolean; + /** Set to negative value to disable rerolls completely in shop */ + rerollMultiplier?: number; + allowLuckUpgrades?: boolean; +} + export function getModifierTypeFuncById(id: string): ModifierTypeFunc { return modifierTypes[id]; } -export function getPlayerModifierTypeOptions(count: integer, party: PlayerPokemon[], modifierTiers?: ModifierTier[]): ModifierTypeOption[] { +/** + * Generates modifier options for a {@linkcode SelectModifierPhase} + * @param count Determines the number of items to generate + * @param party Party is required for generating proper modifier pools + * @param modifierTiers (Optional) If specified, rolls items in the specified tiers. Commonly used for tier-locking with Lock Capsule. + * @param customModifierSettings (Optional) If specified, can customize the item shop rewards further. + * - `guaranteedModifierTypeOptions?: ModifierTypeOption[]` If specified, will override the first X items to be specific modifier options (these should be pre-genned). + * - `guaranteedModifierTypeFuncs?: ModifierTypeFunc[]` If specified, will override the next X items to be auto-generated from specific modifier functions (these don't have to be pre-genned). + * - `guaranteedModifierTiers?: ModifierTier[]` If specified, will override the next X items to be the specified tier. These can upgrade with luck. + * - `fillRemaining?: boolean` Default 'false'. If set to true, will fill the remainder of shop items that were not overridden by the 3 options above, up to the 'count' param value. + * - Example: `count = 4`, `customModifierSettings = { guaranteedModifierTiers: [ModifierTier.GREAT], fillRemaining: true }`, + * - The first item in the shop will be `GREAT` tier, and the remaining 3 items will be generated normally. + * - If `fillRemaining = false` in the same scenario, only 1 `GREAT` tier item will appear in the shop (regardless of `count` value). + * - `rerollMultiplier?: number` If specified, can adjust the amount of money required for a shop reroll. If set to a negative value, the shop will not allow rerolls at all. + * - `allowLuckUpgrades?: boolean` Default `true`, if `false` will prevent set item tiers from upgrading via luck + */ +export function getPlayerModifierTypeOptions(count: integer, party: PlayerPokemon[], modifierTiers?: ModifierTier[], customModifierSettings?: CustomModifierSettings): ModifierTypeOption[] { const options: ModifierTypeOption[] = []; const retryCount = Math.min(count * 5, 50); - new Array(count).fill(0).map((_, i) => { - let candidate = getNewModifierTypeOption(party, ModifierPoolType.PLAYER, modifierTiers && modifierTiers.length > i ? modifierTiers[i] : undefined); - let r = 0; - while (options.length && ++r < retryCount && options.filter(o => o.type?.name === candidate?.type?.name || o.type?.group === candidate?.type?.group).length) { - candidate = getNewModifierTypeOption(party, ModifierPoolType.PLAYER, candidate?.type?.tier, candidate?.upgradeCount); + if (!customModifierSettings) { + new Array(count).fill(0).map((_, i) => { + options.push(getModifierTypeOptionWithRetry(options, retryCount, party, modifierTiers && modifierTiers.length > i ? modifierTiers[i] : undefined)); + }); + } else { + // Guaranteed mod options first + if (customModifierSettings?.guaranteedModifierTypeOptions && customModifierSettings.guaranteedModifierTypeOptions.length > 0) { + options.push(...customModifierSettings.guaranteedModifierTypeOptions!); } - if (candidate) { - options.push(candidate); + + // Guaranteed mod functions second + if (customModifierSettings.guaranteedModifierTypeFuncs && 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]?.(); + + // Populates item id and tier + guaranteedMod = guaranteedMod + .withIdFromFunc(modifierTypes[modifierId]) + .withTierFromPool(); + + const modType = guaranteedMod instanceof ModifierTypeGenerator ? guaranteedMod.generateType(party) : guaranteedMod; + if (modType) { + const option = new ModifierTypeOption(modType, 0); + options.push(option); + } + }); } - }); + + // Guaranteed tiers third + if (customModifierSettings.guaranteedModifierTiers && customModifierSettings.guaranteedModifierTiers.length > 0) { + const allowLuckUpgrades = customModifierSettings.allowLuckUpgrades ?? true; + customModifierSettings.guaranteedModifierTiers.forEach((tier) => { + options.push(getModifierTypeOptionWithRetry(options, retryCount, party, tier, allowLuckUpgrades)); + }); + } + + // Fill remaining + if (options.length < count && customModifierSettings.fillRemaining) { + while (options.length < count) { + options.push(getModifierTypeOptionWithRetry(options, retryCount, party, undefined)); + } + } + } overridePlayerModifierTypeOptions(options, party); return options; } +/** + * Will generate a {@linkcode ModifierType} from the {@linkcode ModifierPoolType.PLAYER} pool, attempting to retry duplicated items up to retryCount + * @param existingOptions Currently generated options + * @param retryCount How many times to retry before allowing a dupe item + * @param party Current player party, used to calculate items in the pool + * @param tier If specified will generate item of tier + * @param allowLuckUpgrades `true` to allow items to upgrade tiers (the little animation that plays and is affected by luck) + */ +function getModifierTypeOptionWithRetry(existingOptions: ModifierTypeOption[], retryCount: integer, party: PlayerPokemon[], tier?: ModifierTier, allowLuckUpgrades?: boolean): ModifierTypeOption { + allowLuckUpgrades = allowLuckUpgrades ?? true; + let candidate = getNewModifierTypeOption(party, ModifierPoolType.PLAYER, tier, undefined, 0, allowLuckUpgrades); + let r = 0; + while (existingOptions.length && ++r < retryCount && existingOptions.filter(o => o.type.name === candidate?.type.name || o.type.group === candidate?.type.group).length) { + candidate = getNewModifierTypeOption(party, ModifierPoolType.PLAYER, candidate?.type.tier ?? tier, candidate?.upgradeCount, 0, allowLuckUpgrades); + } + return candidate!; +} + /** * Replaces the {@linkcode ModifierType} of the entries within {@linkcode options} with any * {@linkcode ModifierOverride} entries listed in {@linkcode Overrides.ITEM_REWARD_OVERRIDE} @@ -2123,7 +2285,16 @@ export function getDailyRunStarterModifiers(party: PlayerPokemon[]): Modifiers.P return ret; } -function getNewModifierTypeOption(party: Pokemon[], poolType: ModifierPoolType, tier?: ModifierTier, upgradeCount?: integer, retryCount: integer = 0): ModifierTypeOption | null { +/** + * Generates a ModifierType from the specified pool + * @param party party of the trainer using the item + * @param poolType PLAYER/WILD/TRAINER + * @param tier If specified, will override the initial tier of an item (can still upgrade with luck) + * @param upgradeCount If defined, means that this is a new ModifierType being generated to override another via luck upgrade. Used for recursive logic + * @param retryCount Max allowed tries before the next tier down is checked for a valid ModifierType + * @param allowLuckUpgrades Default true. If false, will not allow ModifierType to randomly upgrade to next tier + */ +function getNewModifierTypeOption(party: Pokemon[], poolType: ModifierPoolType, tier?: ModifierTier, upgradeCount?: integer, retryCount: integer = 0, allowLuckUpgrades: boolean = true): ModifierTypeOption | null { const player = !poolType; const pool = getModifierPoolForType(poolType); let thresholds: object; @@ -2149,7 +2320,7 @@ function getNewModifierTypeOption(party: Pokemon[], poolType: ModifierPoolType, if (!upgradeCount) { upgradeCount = 0; } - if (player && tierValue) { + if (player && tierValue && allowLuckUpgrades) { const partyLuckValue = getPartyLuckValue(party); const upgradeOdds = Math.floor(128 / ((partyLuckValue + 4) / 4)); let upgraded = false; @@ -2182,7 +2353,7 @@ function getNewModifierTypeOption(party: Pokemon[], poolType: ModifierPoolType, } } else if (upgradeCount === undefined && player) { upgradeCount = 0; - if (tier < ModifierTier.MASTER) { + if (tier < ModifierTier.MASTER && allowLuckUpgrades) { const partyShinyCount = party.filter(p => p.isShiny() && !p.isFainted()).length; const upgradeOdds = Math.floor(32 / ((partyShinyCount + 2) / 2)); while (modifierPool.hasOwnProperty(tier + upgradeCount + 1) && modifierPool[tier + upgradeCount + 1].length) { diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index 4b3d0a85280..d091648b1b0 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -29,7 +29,6 @@ import { Abilities } from "#app/enums/abilities"; import { LearnMovePhase } from "#app/phases/learn-move-phase"; import { LevelUpPhase } from "#app/phases/level-up-phase"; import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase"; -import { SpeciesFormKey } from "#app/data/pokemon-species"; export type ModifierPredicate = (modifier: Modifier) => boolean; @@ -414,7 +413,7 @@ export class DoubleBattleChanceBoosterModifier extends LapsingPersistentModifier } /** - * Modifies the chance of a double battle occurring + * Increases the chance of a double battle occurring * @param args [0] {@linkcode Utils.NumberHolder} for double battle chance * @returns true if the modifier was applied */ @@ -422,7 +421,7 @@ export class DoubleBattleChanceBoosterModifier extends LapsingPersistentModifier const doubleBattleChance = args[0] as Utils.NumberHolder; // This is divided because the chance is generated as a number from 0 to doubleBattleChance.value using Utils.randSeedInt // A double battle will initiate if the generated number is 0 - doubleBattleChance.value = Math.ceil(doubleBattleChance.value / 4); + doubleBattleChance.value = doubleBattleChance.value / 4; return true; } @@ -601,7 +600,7 @@ export class TerastallizeAccessModifier extends PersistentModifier { export abstract class PokemonHeldItemModifier extends PersistentModifier { public pokemonId: integer; - readonly isTransferrable: boolean = true; + public isTransferable: boolean = true; constructor(type: ModifierType, pokemonId: integer, stackCount?: integer) { super(type, stackCount); @@ -700,7 +699,7 @@ export abstract class PokemonHeldItemModifier extends PersistentModifier { export abstract class LapsingPokemonHeldItemModifier extends PokemonHeldItemModifier { protected battlesLeft: integer; - readonly isTransferrable: boolean = false; + public isTransferable: boolean = false; constructor(type: ModifierTypes.ModifierType, pokemonId: integer, battlesLeft?: integer, stackCount?: integer) { super(type, pokemonId, stackCount); @@ -737,7 +736,7 @@ export abstract class LapsingPokemonHeldItemModifier extends PokemonHeldItemModi export class TerastallizeModifier extends LapsingPokemonHeldItemModifier { public teraType: Type; - readonly isTransferrable: boolean = false; + public isTransferable: boolean = false; constructor(type: ModifierTypes.TerastallizeModifierType, pokemonId: integer, teraType: Type, battlesLeft?: integer, stackCount?: integer) { super(type, pokemonId, battlesLeft || 10, stackCount); @@ -800,7 +799,7 @@ export class TerastallizeModifier extends LapsingPokemonHeldItemModifier { */ export class BaseStatModifier extends PokemonHeldItemModifier { protected stat: PermanentStat; - readonly isTransferrable: boolean = false; + public isTransferable: boolean = false; constructor(type: ModifierType, pokemonId: integer, stat: PermanentStat, stackCount?: integer) { super(type, pokemonId, stackCount); @@ -841,6 +840,192 @@ export class BaseStatModifier extends PokemonHeldItemModifier { } } +export class EvoTrackerModifier extends PokemonHeldItemModifier { + protected species: Species; + protected required: integer; + public isTransferable: boolean = false; + + constructor(type: ModifierType, pokemonId: integer, species: Species, required: integer, stackCount?: integer) { + super(type, pokemonId, stackCount); + this.species = species; + this.required = required; + } + + matchType(modifier: Modifier): boolean { + if (modifier instanceof EvoTrackerModifier) { + return (modifier as EvoTrackerModifier).species === this.species; + } + return false; + } + + clone(): PersistentModifier { + return new EvoTrackerModifier(this.type, this.pokemonId, this.species, this.stackCount); + } + + getArgs(): any[] { + return super.getArgs().concat(this.species); + } + + apply(args: any[]): boolean { + return true; + } + + getMaxHeldItemCount(_pokemon: Pokemon): integer { + return this.required; + } +} + +/** + * Currently used by Shuckle Juice item + */ +export class PokemonBaseStatTotalModifier extends PokemonHeldItemModifier { + private statModifier: integer; + public isTransferable: boolean = false; + + constructor(type: ModifierTypes.PokemonBaseStatTotalModifierType, pokemonId: integer, statModifier: integer, stackCount?: integer) { + super(type, pokemonId, stackCount); + this.statModifier = statModifier; + } + + override matchType(modifier: Modifier): boolean { + return modifier instanceof PokemonBaseStatTotalModifier && this.statModifier === modifier.statModifier; + } + + override clone(): PersistentModifier { + return new PokemonBaseStatTotalModifier(this.type as ModifierTypes.PokemonBaseStatTotalModifierType, this.pokemonId, this.statModifier, this.stackCount); + } + + override getArgs(): any[] { + return super.getArgs().concat(this.statModifier); + } + + override shouldApply(args: any[]): boolean { + return super.shouldApply(args) && args.length === 2 && args[1] instanceof Array; + } + + override apply(args: any[]): boolean { + // Modifies the passed in baseStats[] array + args[1].forEach((v, i) => { + // HP is affected by half as much as other stats + const newVal = i === 0 ? Math.floor(v + this.statModifier / 2) : Math.floor(v + this.statModifier); + args[1][i] = Math.min(Math.max(newVal, 1), 999999); + }); + + return true; + } + + override getScoreMultiplier(): number { + return 1.2; + } + + override getMaxHeldItemCount(pokemon: Pokemon): integer { + return 2; + } +} + +/** + * Currently used by Old Gateau item + */ +export class PokemonBaseStatFlatModifier extends PokemonHeldItemModifier { + private statModifier: integer; + private stats: Stat[]; + public isTransferable: boolean = false; + + constructor (type: ModifierType, pokemonId: integer, statModifier: integer, stats: Stat[], stackCount?: integer) { + super(type, pokemonId, stackCount); + + this.statModifier = statModifier; + this.stats = stats; + } + + override matchType(modifier: Modifier): boolean { + return modifier instanceof PokemonBaseStatFlatModifier && modifier.statModifier === this.statModifier && this.stats.every(s => modifier.stats.some(stat => s === stat)); + } + + override clone(): PersistentModifier { + return new PokemonBaseStatFlatModifier(this.type, this.pokemonId, this.statModifier, this.stats, this.stackCount); + } + + override getArgs(): any[] { + return super.getArgs().concat(this.statModifier, this.stats); + } + + override shouldApply(args: any[]): boolean { + return super.shouldApply(args) && args.length === 2 && args[1] instanceof Array; + } + + override apply(args: any[]): boolean { + // Modifies the passed in baseStats[] array by a flat value, only if the stat is specified in this.stats + args[1].forEach((v, i) => { + if (this.stats.includes(i)) { + const newVal = Math.floor(v + this.statModifier); + args[1][i] = Math.min(Math.max(newVal, 1), 999999); + } + }); + + return true; + } + + override getScoreMultiplier(): number { + return 1.1; + } + + override getMaxHeldItemCount(pokemon: Pokemon): integer { + return 1; + } +} + +/** + * Currently used by Macho Brace item + */ +export class PokemonIncrementingStatModifier extends PokemonHeldItemModifier { + public isTransferable: boolean = false; + + constructor (type: ModifierType, pokemonId: integer, stackCount?: integer) { + super(type, pokemonId, stackCount); + } + + matchType(modifier: Modifier): boolean { + return modifier instanceof PokemonIncrementingStatModifier; + } + + clone(): PersistentModifier { + return new PokemonIncrementingStatModifier(this.type, this.pokemonId); + } + + getArgs(): any[] { + return super.getArgs(); + } + + shouldApply(args: any[]): boolean { + return super.shouldApply(args) && args.length === 2 && args[1] instanceof Array; + } + + apply(args: any[]): boolean { + // Modifies the passed in stats[] array by +1 per stack for HP, +2 per stack for other stats + // If the Macho Brace is at max stacks (50), adds additional 5% to total HP and 10% to other stats + args[1].forEach((v, i) => { + const isHp = i === 0; + let mult = 1; + if (this.stackCount === this.getMaxHeldItemCount()) { + mult = isHp ? 1.05 : 1.1; + } + const newVal = Math.floor((v + this.stackCount * (isHp ? 1 : 2)) * mult); + args[1][i] = Math.min(Math.max(newVal, 1), 999999); + }); + + return true; + } + + getScoreMultiplier(): number { + return 1.2; + } + + getMaxHeldItemCount(pokemon?: Pokemon): integer { + return 50; + } +} + /** * Modifier used for held items that apply {@linkcode Stat} boost(s) * using a multiplier. @@ -935,7 +1120,7 @@ export class EvolutionStatBoosterModifier extends StatBoosterModifier { * @returns true if the stat boosts can be applied, false otherwise */ shouldApply(args: any[]): boolean { - return super.shouldApply(args) && ((args[0] as Pokemon).getFormKey() !== SpeciesFormKey.GIGANTAMAX); + return super.shouldApply(args) && !(args[0] as Pokemon).isMax(); } /** @@ -1122,7 +1307,7 @@ export class SpeciesCritBoosterModifier extends CritBoosterModifier { * Applies Specific Type item boosts (e.g., Magnet) */ export class AttackTypeBoosterModifier extends PokemonHeldItemModifier { - private moveType: Type; + public moveType: Type; private boostMultiplier: number; constructor(type: ModifierType, pokemonId: integer, moveType: Type, boostPercent: number, stackCount?: integer) { @@ -2161,7 +2346,7 @@ export class PokemonMultiHitModifier extends PokemonHeldItemModifier { export class PokemonFormChangeItemModifier extends PokemonHeldItemModifier { public formChangeItem: FormChangeItem; public active: boolean; - readonly isTransferrable: boolean = false; + public isTransferable: boolean = false; constructor(type: ModifierTypes.FormChangeItemModifierType, pokemonId: integer, formChangeItem: FormChangeItem, active: boolean, stackCount?: integer) { super(type, pokemonId, stackCount); @@ -2222,6 +2407,15 @@ export class MoneyRewardModifier extends ConsumableModifier { scene.addMoney(moneyAmount.value); + scene.getParty().map(p => { + if (p.species?.speciesId === Species.GIMMIGHOUL || p.fusionSpecies?.speciesId === Species.GIMMIGHOUL) { + p.evoCounter++; + const modifierType: ModifierType = modifierTypes.EVOLUTION_TRACKER_GIMMIGHOUL(); + const modifier = modifierType!.newModifier(p); + scene.addModifier(modifier); + } + }); + return true; } } @@ -2378,6 +2572,55 @@ export class LockModifierTiersModifier extends PersistentModifier { } } +/** + * Black Sludge item + */ +export class HealShopCostModifier extends PersistentModifier { + constructor(type: ModifierType, stackCount?: integer) { + super(type, stackCount); + } + + match(modifier: Modifier): boolean { + return modifier instanceof HealShopCostModifier; + } + + clone(): HealShopCostModifier { + return new HealShopCostModifier(this.type, this.stackCount); + } + + apply(args: any[]): boolean { + (args[0] as Utils.IntegerHolder).value *= Math.pow(3, this.getStackCount()); + + return true; + } + + getMaxStackCount(scene: BattleScene): integer { + return 1; + } +} + +export class BoostBugSpawnModifier extends PersistentModifier { + constructor(type: ModifierType, stackCount?: integer) { + super(type, stackCount); + } + + match(modifier: Modifier): boolean { + return modifier instanceof BoostBugSpawnModifier; + } + + clone(): HealShopCostModifier { + return new BoostBugSpawnModifier(this.type, this.stackCount); + } + + apply(args: any[]): boolean { + return true; + } + + getMaxStackCount(scene: BattleScene): integer { + return 1; + } +} + export class SwitchEffectTransferModifier extends PokemonHeldItemModifier { constructor(type: ModifierType, pokemonId: integer, stackCount?: integer) { super(type, pokemonId, stackCount); @@ -2448,7 +2691,7 @@ export abstract class HeldItemTransferModifier extends PokemonHeldItemModifier { const transferredModifierTypes: ModifierTypes.ModifierType[] = []; const itemModifiers = pokemon.scene.findModifiers(m => m instanceof PokemonHeldItemModifier - && m.pokemonId === targetPokemon.id && m.isTransferrable, targetPokemon.isPlayer()) as PokemonHeldItemModifier[]; + && m.pokemonId === targetPokemon.id && m.isTransferable, targetPokemon.isPlayer()) as PokemonHeldItemModifier[]; let highestItemTier = itemModifiers.map(m => m.type.getOrInferTier(poolType)).reduce((highestTier, tier) => Math.max(tier!, highestTier), 0); // TODO: is this bang correct? let tierItemModifiers = itemModifiers.filter(m => m.type.getOrInferTier(poolType) === highestItemTier); @@ -2493,7 +2736,7 @@ export abstract class HeldItemTransferModifier extends PokemonHeldItemModifier { * @see {@linkcode modifierTypes[MINI_BLACK_HOLE]} */ export class TurnHeldItemTransferModifier extends HeldItemTransferModifier { - isTransferrable: boolean = true; + isTransferable: boolean = true; constructor(type: ModifierType, pokemonId: integer, stackCount?: integer) { super(type, pokemonId, stackCount); } @@ -2519,7 +2762,7 @@ export class TurnHeldItemTransferModifier extends HeldItemTransferModifier { } setTransferrableFalse(): void { - this.isTransferrable = false; + this.isTransferable = false; } } diff --git a/src/overrides.ts b/src/overrides.ts index d1597dfdee8..35ca299721b 100644 --- a/src/overrides.ts +++ b/src/overrides.ts @@ -6,13 +6,14 @@ import { PokeballType } from "#enums/pokeball"; import { Species } from "#enums/species"; import { StatusEffect } from "#enums/status-effect"; import { TimeOfDay } from "#enums/time-of-day"; -import { VariantTier } from "#enums/variant-tiers"; +import { VariantTier } from "#enums/variant-tier"; import { WeatherType } from "#enums/weather-type"; import { type PokeballCounts } from "./battle-scene"; import { Gender } from "./data/gender"; -import { allSpecies } from "./data/pokemon-species"; // eslint-disable-line @typescript-eslint/no-unused-vars import { Variant } from "./data/variant"; import { type ModifierOverride } from "./modifier/modifier-type"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; /** * Overrides that are using when testing different in game situations @@ -69,6 +70,8 @@ class DefaultOverrides { [PokeballType.MASTER_BALL]: 0, }, }; + /** Set to `true` to show all tutorials */ + readonly BYPASS_TUTORIAL_SKIP: boolean = false; // ---------------- // PLAYER OVERRIDES @@ -135,6 +138,15 @@ class DefaultOverrides { readonly EGG_FREE_GACHA_PULLS_OVERRIDE: boolean = false; readonly EGG_GACHA_PULL_COUNT_OVERRIDE: number = 0; + // ------------------------- + // MYSTERY ENCOUNTER OVERRIDES + // ------------------------- + + /** 1 to 256, set to null to ignore */ + readonly MYSTERY_ENCOUNTER_RATE_OVERRIDE: number | null = null; + readonly MYSTERY_ENCOUNTER_TIER_OVERRIDE: MysteryEncounterTier | null = null; + readonly MYSTERY_ENCOUNTER_OVERRIDE: MysteryEncounterType | null = null; + // ------------------------- // MODIFIER / ITEM OVERRIDES // ------------------------- diff --git a/src/phases/attempt-capture-phase.ts b/src/phases/attempt-capture-phase.ts index cf9ce997bfd..53723526c14 100644 --- a/src/phases/attempt-capture-phase.ts +++ b/src/phases/attempt-capture-phase.ts @@ -15,6 +15,7 @@ import { Mode } from "#app/ui/ui"; import i18next from "i18next"; import { PokemonPhase } from "./pokemon-phase"; import { VictoryPhase } from "./victory-phase"; +import { SubstituteTag } from "#app/data/battler-tags"; export class AttemptCapturePhase extends PokemonPhase { private pokeballType: PokeballType; @@ -36,6 +37,11 @@ export class AttemptCapturePhase extends PokemonPhase { return this.end(); } + const substitute = pokemon.getTag(SubstituteTag); + if (substitute) { + substitute.sprite.setVisible(false); + } + this.scene.pokeballCounts[this.pokeballType]--; this.originalY = pokemon.y; @@ -165,6 +171,11 @@ export class AttemptCapturePhase extends PokemonPhase { pokemon.setVisible(true); pokemon.untint(250, "Sine.easeOut"); + const substitute = pokemon.getTag(SubstituteTag); + if (substitute) { + substitute.sprite.setVisible(true); + } + const pokeballAtlasKey = getPokeballAtlasKey(this.pokeballType); this.pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`); this.scene.time.delayedCall(17, () => this.pokeball.setTexture("pb", `${pokeballAtlasKey}_open`)); diff --git a/src/phases/battle-end-phase.ts b/src/phases/battle-end-phase.ts index f08e04b443a..902a85325ad 100644 --- a/src/phases/battle-end-phase.ts +++ b/src/phases/battle-end-phase.ts @@ -2,19 +2,31 @@ import { applyPostBattleAbAttrs, PostBattleAbAttr } from "#app/data/ability"; import { LapsingPersistentModifier, LapsingPokemonHeldItemModifier } from "#app/modifier/modifier"; import { BattlePhase } from "./battle-phase"; import { GameOverPhase } from "./game-over-phase"; +import BattleScene from "#app/battle-scene"; export class BattleEndPhase extends BattlePhase { + /** If true, will increment battles won */ + isVictory: boolean; + + constructor(scene: BattleScene, isVictory: boolean = true) { + super(scene); + + this.isVictory = isVictory; + } + start() { super.start(); - this.scene.currentBattle.addBattleScore(this.scene); + if (this.isVictory) { + this.scene.currentBattle.addBattleScore(this.scene); - this.scene.gameData.gameStats.battles++; - if (this.scene.currentBattle.trainer) { - this.scene.gameData.gameStats.trainersDefeated++; - } - if (this.scene.gameMode.isEndless && this.scene.currentBattle.waveIndex + 1 > this.scene.gameData.gameStats.highestEndlessWave) { - this.scene.gameData.gameStats.highestEndlessWave = this.scene.currentBattle.waveIndex + 1; + this.scene.gameData.gameStats.battles++; + if (this.scene.currentBattle.trainer) { + this.scene.gameData.gameStats.trainersDefeated++; + } + if (this.scene.gameMode.isEndless && this.scene.currentBattle.waveIndex + 1 > this.scene.gameData.gameStats.highestEndlessWave) { + this.scene.gameData.gameStats.highestEndlessWave = this.scene.currentBattle.waveIndex + 1; + } } // Endless graceful end diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index 47d212aa598..66e39cf98a5 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -15,6 +15,8 @@ import { Mode } from "#app/ui/ui"; import i18next from "i18next"; import { FieldPhase } from "./field-phase"; import { SelectTargetPhase } from "./select-target-phase"; +import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; +import { isNullOrUndefined } from "#app/utils"; export class CommandPhase extends FieldPhase { protected fieldIndex: integer; @@ -68,7 +70,12 @@ export class CommandPhase extends FieldPhase { } } } else { - this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); + if (this.scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER && this.scene.currentBattle.mysteryEncounter?.skipToFightInput) { + this.scene.ui.clearText(); + this.scene.ui.setMode(Mode.FIGHT, this.fieldIndex); + } else { + this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); + } } } @@ -134,6 +141,13 @@ export class CommandPhase extends FieldPhase { this.scene.ui.showText("", 0); this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); }, null, true); + } else if (this.scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER && !this.scene.currentBattle.mysteryEncounter!.catchAllowed) { + this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); + this.scene.ui.setMode(Mode.MESSAGE); + this.scene.ui.showText(i18next.t("battle:noPokeballMysteryEncounter"), null, () => { + this.scene.ui.showText("", 0); + this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); + }, null, true); } else { const targets = this.scene.getEnemyField().filter(p => p.isActive(true)).map(p => p.getBattlerIndex()); if (targets.length > 1) { @@ -166,14 +180,16 @@ export class CommandPhase extends FieldPhase { case Command.POKEMON: case Command.RUN: const isSwitch = command === Command.POKEMON; - if (!isSwitch && this.scene.arena.biomeType === Biome.END) { + const { currentBattle, arena } = this.scene; + const mysteryEncounterFleeAllowed = currentBattle.mysteryEncounter?.fleeAllowed; + if (!isSwitch && (arena.biomeType === Biome.END || (!isNullOrUndefined(mysteryEncounterFleeAllowed) && !mysteryEncounterFleeAllowed))) { this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); this.scene.ui.setMode(Mode.MESSAGE); this.scene.ui.showText(i18next.t("battle:noEscapeForce"), null, () => { this.scene.ui.showText("", 0); this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); }, null, true); - } else if (!isSwitch && this.scene.currentBattle.battleType === BattleType.TRAINER) { + } else if (!isSwitch && (currentBattle.battleType === BattleType.TRAINER || currentBattle.mysteryEncounter?.encounterMode === MysteryEncounterMode.TRAINER_BATTLE)) { this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); this.scene.ui.setMode(Mode.MESSAGE); this.scene.ui.showText(i18next.t("battle:noEscapeTrainer"), null, () => { @@ -184,12 +200,12 @@ export class CommandPhase extends FieldPhase { const batonPass = isSwitch && args[0] as boolean; const trappedAbMessages: string[] = []; if (batonPass || !playerPokemon.isTrapped(trappedAbMessages)) { - this.scene.currentBattle.turnCommands[this.fieldIndex] = isSwitch + currentBattle.turnCommands[this.fieldIndex] = isSwitch ? { command: Command.POKEMON, cursor: cursor, args: args } : { command: Command.RUN }; success = true; if (!isSwitch && this.fieldIndex) { - this.scene.currentBattle.turnCommands[this.fieldIndex - 1]!.skip = true; + currentBattle.turnCommands[this.fieldIndex - 1]!.skip = true; } } else if (trappedAbMessages.length > 0) { if (!isSwitch) { @@ -206,7 +222,7 @@ export class CommandPhase extends FieldPhase { // trapTag should be defined at this point, but just in case... if (!trapTag) { - this.scene.currentBattle.turnCommands[this.fieldIndex] = isSwitch + currentBattle.turnCommands[this.fieldIndex] = isSwitch ? { command: Command.POKEMON, cursor: cursor, args: args } : { command: Command.RUN }; break; diff --git a/src/phases/common-anim-phase.ts b/src/phases/common-anim-phase.ts index a85cd7629d9..c4071488eef 100644 --- a/src/phases/common-anim-phase.ts +++ b/src/phases/common-anim-phase.ts @@ -6,12 +6,14 @@ import { PokemonPhase } from "./pokemon-phase"; export class CommonAnimPhase extends PokemonPhase { private anim: CommonAnim | null; private targetIndex: integer | undefined; + private playOnEmptyField: boolean; - constructor(scene: BattleScene, battlerIndex?: BattlerIndex, targetIndex?: BattlerIndex | undefined, anim?: CommonAnim) { + constructor(scene: BattleScene, battlerIndex?: BattlerIndex, targetIndex?: BattlerIndex | undefined, anim?: CommonAnim, playOnEmptyField: boolean = false) { super(scene, battlerIndex); this.anim = anim!; // TODO: is this bang correct? this.targetIndex = targetIndex; + this.playOnEmptyField = playOnEmptyField; } setAnimation(anim: CommonAnim) { @@ -19,7 +21,8 @@ export class CommonAnimPhase extends PokemonPhase { } start() { - new CommonBattleAnim(this.anim, this.getPokemon(), this.targetIndex !== undefined ? (this.player ? this.scene.getEnemyField() : this.scene.getPlayerField())[this.targetIndex] : this.getPokemon()).play(this.scene, () => { + const target = this.targetIndex !== undefined ? (this.player ? this.scene.getEnemyField() : this.scene.getPlayerField())[this.targetIndex] : this.getPokemon(); + new CommonBattleAnim(this.anim, this.getPokemon(), target).play(this.scene, false, () => { this.end(); }); } diff --git a/src/phases/damage-phase.ts b/src/phases/damage-phase.ts index 5add0345358..66b11512729 100644 --- a/src/phases/damage-phase.ts +++ b/src/phases/damage-phase.ts @@ -57,7 +57,7 @@ export class DamagePhase extends PokemonPhase { this.scene.damageNumberHandler.add(this.getPokemon(), this.amount, this.damageResult, this.critical); } - if (this.damageResult !== HitResult.OTHER) { + if (this.damageResult !== HitResult.OTHER && this.amount > 0) { const flashTimer = this.scene.time.addEvent({ delay: 100, repeat: 5, diff --git a/src/phases/egg-lapse-phase.ts b/src/phases/egg-lapse-phase.ts index 65426846bb3..c251819f331 100644 --- a/src/phases/egg-lapse-phase.ts +++ b/src/phases/egg-lapse-phase.ts @@ -40,7 +40,7 @@ export class EggLapsePhase extends Phase { this.showSummary(); }, () => { this.hatchEggsRegular(eggsToHatch); - this.showSummary(); + this.end(); } ); }, 100, true); diff --git a/src/phases/encounter-phase.ts b/src/phases/encounter-phase.ts index 3f37095569a..cead9de0fc6 100644 --- a/src/phases/encounter-phase.ts +++ b/src/phases/encounter-phase.ts @@ -1,5 +1,5 @@ import BattleScene from "#app/battle-scene"; -import { BattleType, BattlerIndex } from "#app/battle"; +import { BattlerIndex, BattleType } from "#app/battle"; import { applyAbAttrs, SyncEncounterNatureAbAttr } from "#app/data/ability"; import { getCharVariantFromDialogue } from "#app/data/dialogue"; import { TrainerSlot } from "#app/data/trainer-config"; @@ -10,14 +10,15 @@ import { Species } from "#app/enums/species"; import { EncounterPhaseEvent } from "#app/events/battle-scene"; import Pokemon, { FieldPosition } from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; -import { regenerateModifierPoolThresholds, ModifierPoolType } from "#app/modifier/modifier-type"; -import { IvScannerModifier, TurnHeldItemTransferModifier } from "#app/modifier/modifier"; +import { ModifierPoolType, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type"; +import { BoostBugSpawnModifier, IvScannerModifier, TurnHeldItemTransferModifier } from "#app/modifier/modifier"; import { achvs } from "#app/system/achv"; import { handleTutorial, Tutorial } from "#app/tutorial"; import { Mode } from "#app/ui/ui"; import i18next from "i18next"; import { BattlePhase } from "./battle-phase"; import * as Utils from "#app/utils"; +import { randSeedInt } from "#app/utils"; import { CheckSwitchPhase } from "./check-switch-phase"; import { GameOverPhase } from "./game-over-phase"; import { PostSummonPhase } from "./post-summon-phase"; @@ -27,6 +28,12 @@ import { ShinySparklePhase } from "./shiny-sparkle-phase"; import { SummonPhase } from "./summon-phase"; import { ToggleDoublePositionPhase } from "./toggle-double-position-phase"; import Overrides from "#app/overrides"; +import { initEncounterAnims, loadEncounterAnimAssets } from "#app/data/battle-anims"; +import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; +import { doTrainerExclamation } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; +import { getGoldenBugNetSpecies } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; export class EncounterPhase extends BattlePhase { private loaded: boolean; @@ -55,14 +62,45 @@ export class EncounterPhase extends BattlePhase { const battle = this.scene.currentBattle; + // Init Mystery Encounter if there is one + const mysteryEncounter = battle.mysteryEncounter; + if (mysteryEncounter) { + // If ME has an onInit() function, call it + // Usually used for calculating rand data before initializing anything visual + // Also prepopulates any dialogue tokens from encounter/option requirements + this.scene.executeWithSeedOffset(() => { + if (mysteryEncounter.onInit) { + mysteryEncounter.onInit(this.scene); + } + mysteryEncounter.populateDialogueTokensFromRequirements(this.scene); + }, this.scene.currentBattle.waveIndex); + + // Add any special encounter animations to load + if (mysteryEncounter.encounterAnimations && mysteryEncounter.encounterAnimations.length > 0) { + loadEnemyAssets.push(initEncounterAnims(this.scene, mysteryEncounter.encounterAnimations).then(() => loadEncounterAnimAssets(this.scene, true))); + } + + // Add intro visuals for mystery encounter + mysteryEncounter.initIntroVisuals(this.scene); + this.scene.field.add(mysteryEncounter.introVisuals!); + } + let totalBst = 0; - battle.enemyLevels?.forEach((level, e) => { + battle.enemyLevels?.every((level, e) => { + if (battle.battleType === BattleType.MYSTERY_ENCOUNTER) { + // Skip enemy loading for MEs, those are loaded elsewhere + return false; + } if (!this.loaded) { if (battle.battleType === BattleType.TRAINER) { battle.enemyParty[e] = battle.trainer?.genPartyMember(e)!; // TODO:: is the bang correct here? } else { - const enemySpecies = this.scene.randomSpecies(battle.waveIndex, level, true); + let enemySpecies = this.scene.randomSpecies(battle.waveIndex, level, true); + // If player has golden bug net, rolls 10% chance to replace with species from the golden bug net bug pool + if (this.scene.findModifier(m => m instanceof BoostBugSpawnModifier) && randSeedInt(10) === 0) { + enemySpecies = getGoldenBugNetSpecies(); + } battle.enemyParty[e] = this.scene.addEnemyPokemon(enemySpecies, level, TrainerSlot.NONE, !!this.scene.getEncounterBossSegments(battle.waveIndex, level, enemySpecies)); if (this.scene.currentBattle.battleSpec === BattleSpec.FINAL_BOSS) { battle.enemyParty[e].ivs = new Array(6).fill(31); @@ -79,7 +117,7 @@ export class EncounterPhase extends BattlePhase { } if (!this.loaded) { - this.scene.gameData.setPokemonSeen(enemyPokemon, true, battle.battleType === BattleType.TRAINER); + this.scene.gameData.setPokemonSeen(enemyPokemon, true, battle.battleType === BattleType.TRAINER || battle?.mysteryEncounter?.encounterMode === MysteryEncounterMode.TRAINER_BATTLE); } if (enemyPokemon.species.speciesId === Species.ETERNATUS) { @@ -104,6 +142,7 @@ export class EncounterPhase extends BattlePhase { loadEnemyAssets.push(enemyPokemon.loadAssets()); console.log(getPokemonNameWithAffix(enemyPokemon), enemyPokemon.species.speciesId, enemyPokemon.stats); + return true; }); if (this.scene.getParty().filter(p => p.isShiny()).length === 6) { @@ -112,6 +151,25 @@ export class EncounterPhase extends BattlePhase { if (battle.battleType === BattleType.TRAINER) { loadEnemyAssets.push(battle.trainer?.loadAssets().then(() => battle.trainer?.initSprite())!); // TODO: is this bang correct? + } else if (battle.battleType === BattleType.MYSTERY_ENCOUNTER) { + if (!battle.mysteryEncounter) { + battle.mysteryEncounter = this.scene.getMysteryEncounter(mysteryEncounter?.encounterType); + } + if (battle.mysteryEncounter.introVisuals) { + loadEnemyAssets.push(battle.mysteryEncounter.introVisuals.loadAssets().then(() => battle.mysteryEncounter!.introVisuals!.initSprite())); + } + if (battle.mysteryEncounter.loadAssets.length > 0) { + loadEnemyAssets.push(...battle.mysteryEncounter.loadAssets); + } + // Load Mystery Encounter Exclamation bubble and sfx + loadEnemyAssets.push(new Promise(resolve => { + this.scene.loadSe("GEN8- Exclaim", "battle_anims", "GEN8- Exclaim.wav"); + this.scene.loadImage("encounter_exclaim", "mystery-encounters"); + this.scene.load.once(Phaser.Loader.Events.COMPLETE, () => resolve()); + if (!this.scene.load.isLoading()) { + this.scene.load.start(); + } + })); } else { const overridedBossSegments = Overrides.OPP_HEALTH_SEGMENTS_OVERRIDE > 1; // for double battles, reduce the health segments for boss Pokemon unless there is an override @@ -127,7 +185,10 @@ export class EncounterPhase extends BattlePhase { } Promise.all(loadEnemyAssets).then(() => { - battle.enemyParty.forEach((enemyPokemon, e) => { + battle.enemyParty.every((enemyPokemon, e) => { + if (battle.battleType === BattleType.MYSTERY_ENCOUNTER) { + return false; + } if (e < (battle.double ? 2 : 1)) { if (battle.battleType === BattleType.WILD) { this.scene.field.add(enemyPokemon); @@ -145,17 +206,18 @@ export class EncounterPhase extends BattlePhase { enemyPokemon.setFieldPosition(e ? FieldPosition.RIGHT : FieldPosition.LEFT); } } + return true; }); - if (!this.loaded) { + if (!this.loaded && battle.battleType !== BattleType.MYSTERY_ENCOUNTER) { regenerateModifierPoolThresholds(this.scene.getEnemyField(), battle.battleType === BattleType.TRAINER ? ModifierPoolType.TRAINER : ModifierPoolType.WILD); this.scene.generateEnemyModifiers(); } this.scene.ui.setMode(Mode.MESSAGE).then(() => { if (!this.loaded) { - //@ts-ignore - this.scene.gameData.saveAll(this.scene, true, battle.waveIndex % 10 === 1 || this.scene.lastSavePlayTime >= 300).then(success => { // TODO: get rid of ts-ignore + this.trySetWeatherIfNewBiome(); // Set weather before session gets saved + this.scene.gameData.saveAll(this.scene, true, battle.waveIndex % 10 === 1 || (this.scene.lastSavePlayTime ?? 0) >= 300).then(success => { this.scene.disableMenu = false; if (!success) { return this.scene.reset(true); @@ -188,10 +250,6 @@ export class EncounterPhase extends BattlePhase { } } - if (!this.loaded) { - this.scene.arena.trySetWeather(getRandomWeatherType(this.scene.arena), false); - } - const enemyField = this.scene.getEnemyField(); this.scene.tweens.add({ targets: [this.scene.arenaEnemy, this.scene.currentBattle.trainer, enemyField, this.scene.arenaPlayer, this.scene.trainer].flat(), @@ -203,6 +261,19 @@ export class EncounterPhase extends BattlePhase { } } }); + + const encounterIntroVisuals = this.scene.currentBattle?.mysteryEncounter?.introVisuals; + if (encounterIntroVisuals) { + const enterFromRight = encounterIntroVisuals.enterFromRight; + if (enterFromRight) { + encounterIntroVisuals.x += 500; + } + this.scene.tweens.add({ + targets: encounterIntroVisuals, + x: enterFromRight ? "-=200" : "+=300", + duration: 2000 + }); + } } getEncounterMessage(): string { @@ -289,6 +360,63 @@ export class EncounterPhase extends BattlePhase { showDialogueAndSummon(); } } + } else if (this.scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER && this.scene.currentBattle.mysteryEncounter) { + const encounter = this.scene.currentBattle.mysteryEncounter; + const introVisuals = encounter.introVisuals; + introVisuals?.playAnim(); + + if (encounter.onVisualsStart) { + encounter.onVisualsStart(this.scene); + } + + const doEncounter = () => { + const doShowEncounterOptions = () => { + this.scene.ui.clearText(); + this.scene.ui.getMessageHandler().hideNameText(); + + this.scene.unshiftPhase(new MysteryEncounterPhase(this.scene)); + this.end(); + }; + + if (showEncounterMessage) { + const introDialogue = encounter.dialogue.intro; + if (!introDialogue) { + doShowEncounterOptions(); + } else { + const FIRST_DIALOGUE_PROMPT_DELAY = 750; + let i = 0; + const showNextDialogue = () => { + const nextAction = i === introDialogue.length - 1 ? doShowEncounterOptions : showNextDialogue; + const dialogue = introDialogue[i]; + const title = getEncounterText(this.scene, dialogue?.speaker); + const text = getEncounterText(this.scene, dialogue.text)!; + i++; + if (title) { + this.scene.ui.showDialogue(text, title, null, nextAction, 0, i === 1 ? FIRST_DIALOGUE_PROMPT_DELAY : 0); + } else { + this.scene.ui.showText(text, null, nextAction, i === 1 ? FIRST_DIALOGUE_PROMPT_DELAY : 0, true); + } + }; + + if (introDialogue.length > 0) { + showNextDialogue(); + } + } + } else { + doShowEncounterOptions(); + } + }; + + const encounterMessage = i18next.t("battle:mysteryEncounterAppeared"); + + if (!encounterMessage) { + doEncounter(); + } else { + doTrainerExclamation(this.scene); + this.scene.ui.showDialogue(encounterMessage, "???", null, () => { + this.scene.charSprite.hide().then(() => this.scene.hideFieldOverlay(250).then(() => doEncounter())); + }); + } } } @@ -301,7 +429,7 @@ export class EncounterPhase extends BattlePhase { } }); - if (this.scene.currentBattle.battleType !== BattleType.TRAINER) { + if (![BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER].includes(this.scene.currentBattle.battleType)) { enemyField.map(p => this.scene.pushConditionalPhase(new PostSummonPhase(this.scene, p.getBattlerIndex()), () => { // if there is not a player party, we can't continue if (!this.scene.getParty()?.length) { @@ -387,4 +515,18 @@ export class EncounterPhase extends BattlePhase { } return false; } + + /** + * Set biome weather if and only if this encounter is the start of a new biome. + * + * By using function overrides, this should happen if and only if this phase + * is exactly a NewBiomeEncounterPhase or an EncounterPhase (to account for + * Wave 1 of a Daily Run), but NOT NextEncounterPhase (which starts the next + * wave in the same biome). + */ + trySetWeatherIfNewBiome(): void { + if (!this.loaded) { + this.scene.arena.trySetWeather(getRandomWeatherType(this.scene.arena), false); + } + } } diff --git a/src/phases/enemy-command-phase.ts b/src/phases/enemy-command-phase.ts index 91ee0456cd4..b4503a7b059 100644 --- a/src/phases/enemy-command-phase.ts +++ b/src/phases/enemy-command-phase.ts @@ -14,11 +14,15 @@ import { FieldPhase } from "./field-phase"; */ export class EnemyCommandPhase extends FieldPhase { protected fieldIndex: integer; + protected skipTurn: boolean = false; constructor(scene: BattleScene, fieldIndex: integer) { super(scene); this.fieldIndex = fieldIndex; + if (this.scene.currentBattle.mysteryEncounter?.skipEnemyBattleTurns) { + this.skipTurn = true; + } } start() { @@ -57,7 +61,7 @@ export class EnemyCommandPhase extends FieldPhase { const index = trainer.getNextSummonIndex(enemyPokemon.trainerSlot, partyMemberScores); battle.turnCommands[this.fieldIndex + BattlerIndex.ENEMY] = - { command: Command.POKEMON, cursor: index, args: [false] }; + { command: Command.POKEMON, cursor: index, args: [false], skip: this.skipTurn }; battle.enemySwitchCounter++; @@ -71,7 +75,7 @@ export class EnemyCommandPhase extends FieldPhase { const nextMove = enemyPokemon.getNextMove(); this.scene.currentBattle.turnCommands[this.fieldIndex + BattlerIndex.ENEMY] = - { command: Command.FIGHT, move: nextMove }; + { command: Command.FIGHT, move: nextMove, skip: this.skipTurn }; this.scene.currentBattle.enemySwitchCounter = Math.max(this.scene.currentBattle.enemySwitchCounter - 1, 0); diff --git a/src/phases/faint-phase.ts b/src/phases/faint-phase.ts index 169d667113a..41384e5e491 100644 --- a/src/phases/faint-phase.ts +++ b/src/phases/faint-phase.ts @@ -110,7 +110,7 @@ export class FaintPhase extends PokemonPhase { } } else { this.scene.unshiftPhase(new VictoryPhase(this.scene, this.battlerIndex)); - if (this.scene.currentBattle.battleType === BattleType.TRAINER) { + if ([BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER].includes(this.scene.currentBattle.battleType)) { const hasReservePartyMember = !!this.scene.getEnemyParty().filter(p => p.isActive() && !p.isOnField() && p.trainerSlot === (pokemon as EnemyPokemon).trainerSlot).length; if (hasReservePartyMember) { this.scene.pushPhase(new SwitchSummonPhase(this.scene, this.fieldIndex, -1, false, false, false)); @@ -124,9 +124,6 @@ export class FaintPhase extends PokemonPhase { this.scene.redirectPokemonMoves(pokemon, allyPokemon); } - pokemon.lapseTags(BattlerTagLapseType.FAINT); - this.scene.getField(true).filter(p => p !== pokemon).forEach(p => p.removeTagsBySourceId(pokemon.id)); - pokemon.faintCry(() => { if (pokemon instanceof PlayerPokemon) { pokemon.addFriendship(-10); @@ -139,7 +136,10 @@ export class FaintPhase extends PokemonPhase { y: pokemon.y + 150, ease: "Sine.easeIn", onComplete: () => { - pokemon.setVisible(false); + pokemon.resetSprite(); + pokemon.lapseTags(BattlerTagLapseType.FAINT); + this.scene.getField(true).filter(p => p !== pokemon).forEach(p => p.removeTagsBySourceId(pokemon.id)); + pokemon.y -= 150; pokemon.trySetStatus(StatusEffect.FAINT); if (pokemon.isPlayer()) { diff --git a/src/phases/game-over-phase.ts b/src/phases/game-over-phase.ts index 17805e90f0f..b0cfa5c55e2 100644 --- a/src/phases/game-over-phase.ts +++ b/src/phases/game-over-phase.ts @@ -48,6 +48,14 @@ export class GameOverPhase extends BattlePhase { this.victory = true; } + // Handle Mystery Encounter special Game Over cases + // Situations such as when player lost a battle, but it isn't treated as full Game Over + if (!this.victory && this.scene.currentBattle.mysteryEncounter?.onGameOver && !this.scene.currentBattle.mysteryEncounter.onGameOver(this.scene)) { + // Do not end the game + return this.end(); + } + // Otherwise, continue standard Game Over logic + if (this.victory && this.scene.gameMode.isEndless) { const genderIndex = this.scene.gameData.gender ?? PlayerGender.UNSET; const genderStr = PlayerGender[genderIndex].toLowerCase(); @@ -237,7 +245,9 @@ export class GameOverPhase extends BattlePhase { trainer: this.scene.currentBattle.battleType === BattleType.TRAINER ? new TrainerData(this.scene.currentBattle.trainer) : null, gameVersion: this.scene.game.config.gameVersion, timestamp: new Date().getTime(), - challenges: this.scene.gameMode.challenges.map(c => new ChallengeData(c)) + challenges: this.scene.gameMode.challenges.map(c => new ChallengeData(c)), + mysteryEncounterType: this.scene.currentBattle.mysteryEncounter?.encounterType ?? -1, + mysteryEncounterSaveData: this.scene.mysteryEncounterSaveData } as SessionSaveData; } } diff --git a/src/phases/message-phase.ts b/src/phases/message-phase.ts index 2244980c899..1d953801178 100644 --- a/src/phases/message-phase.ts +++ b/src/phases/message-phase.ts @@ -6,14 +6,16 @@ export class MessagePhase extends Phase { private callbackDelay: integer | null; private prompt: boolean | null; private promptDelay: integer | null; + private speaker?: string; - constructor(scene: BattleScene, text: string, callbackDelay?: integer | null, prompt?: boolean | null, promptDelay?: integer | null) { + constructor(scene: BattleScene, text: string, callbackDelay?: integer | null, prompt?: boolean | null, promptDelay?: integer | null, speaker?: string) { super(scene); this.text = text; this.callbackDelay = callbackDelay!; // TODO: is this bang correct? this.prompt = prompt!; // TODO: is this bang correct? this.promptDelay = promptDelay!; // TODO: is this bang correct? + this.speaker = speaker; } start() { @@ -21,11 +23,15 @@ export class MessagePhase extends Phase { if (this.text.indexOf("$") > -1) { const pageIndex = this.text.indexOf("$"); - this.scene.unshiftPhase(new MessagePhase(this.scene, this.text.slice(pageIndex + 1), this.callbackDelay, this.prompt, this.promptDelay)); + this.scene.unshiftPhase(new MessagePhase(this.scene, this.text.slice(pageIndex + 1), this.callbackDelay, this.prompt, this.promptDelay, this.speaker)); this.text = this.text.slice(0, pageIndex).trim(); } - this.scene.ui.showText(this.text, null, () => this.end(), this.callbackDelay || (this.prompt ? 0 : 1500), this.prompt, this.promptDelay); + if (this.speaker) { + this.scene.ui.showDialogue(this.text, this.speaker, null, () => this.end(), this.callbackDelay || (this.prompt ? 0 : 1500), this.promptDelay ?? 0); + } else { + this.scene.ui.showText(this.text, null, () => this.end(), this.callbackDelay || (this.prompt ? 0 : 1500), this.prompt, this.promptDelay); + } } end() { diff --git a/src/phases/move-anim-test-phase.ts b/src/phases/move-anim-test-phase.ts index 2d3b54bfd9a..a6ab90464b8 100644 --- a/src/phases/move-anim-test-phase.ts +++ b/src/phases/move-anim-test-phase.ts @@ -31,7 +31,9 @@ export class MoveAnimTestPhase extends BattlePhase { initMoveAnim(this.scene, moveId).then(() => { loadMoveAnimAssets(this.scene, [moveId], true) .then(() => { - new MoveAnim(moveId, player ? this.scene.getPlayerPokemon()! : this.scene.getEnemyPokemon()!, (player !== (allMoves[moveId] instanceof SelfStatusMove) ? this.scene.getEnemyPokemon()! : this.scene.getPlayerPokemon()!).getBattlerIndex()).play(this.scene, () => { // TODO: are the bangs correct here? + const user = player ? this.scene.getPlayerPokemon()! : this.scene.getEnemyPokemon()!; + const target = (player !== (allMoves[moveId] instanceof SelfStatusMove)) ? this.scene.getEnemyPokemon()! : this.scene.getPlayerPokemon()!; + new MoveAnim(moveId, user, target.getBattlerIndex()).play(this.scene, allMoves[moveId].hitsSubstitute(user, target), () => { // TODO: are the bangs correct here? if (player) { this.playMoveAnim(moveQueue, false); } else { diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index 41fb03c4f4f..c3199166e84 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -3,7 +3,7 @@ import { BattlerIndex } from "#app/battle"; import { applyPreAttackAbAttrs, AddSecondStrikeAbAttr, IgnoreMoveEffectsAbAttr, applyPostDefendAbAttrs, PostDefendAbAttr, applyPostAttackAbAttrs, PostAttackAbAttr, MaxMultiHitAbAttr, AlwaysHitAbAttr } from "#app/data/ability"; import { ArenaTagSide, ConditionalProtectTag } from "#app/data/arena-tag"; import { MoveAnim } from "#app/data/battle-anims"; -import { BattlerTagLapseType, DamageProtectedTag, ProtectedTag, SemiInvulnerableTag } from "#app/data/battler-tags"; +import { BattlerTagLapseType, DamageProtectedTag, ProtectedTag, SemiInvulnerableTag, SubstituteTag } from "#app/data/battler-tags"; import { MoveTarget, applyMoveAttrs, OverrideMoveEffectAttr, MultiHitAttr, AttackMove, FixedDamageAttr, VariableTargetAttr, MissEffectAttr, MoveFlags, applyFilteredMoveAttrs, MoveAttr, MoveEffectAttr, MoveEffectTrigger, ChargeAttr, MoveCategory, NoEffectAttr, HitsTagAttr } from "#app/data/move"; import { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms"; import { BattlerTagType } from "#app/enums/battler-tag-type"; @@ -102,7 +102,7 @@ export class MoveEffectPhase extends PokemonPhase { * (and not random target) and failed the hit check against its target (MISS), log the move * as FAILed or MISSed (depending on the conditions above) and end this phase. */ - if (!hasActiveTargets || (!move.hasAttr(VariableTargetAttr) && !move.isMultiTarget() && !targetHitChecks[this.targets[0]])) { + if (!hasActiveTargets || (!move.hasAttr(VariableTargetAttr) && !move.isMultiTarget() && !targetHitChecks[this.targets[0]] && !targets[0].getTag(ProtectedTag))) { this.stopMultiHit(); if (hasActiveTargets) { this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: this.getTarget()? getPokemonNameWithAffix(this.getTarget()!) : "" })); @@ -119,25 +119,12 @@ export class MoveEffectPhase extends PokemonPhase { /** All move effect attributes are chained together in this array to be applied asynchronously. */ const applyAttrs: Promise[] = []; + const playOnEmptyField = this.scene.currentBattle?.mysteryEncounter?.hasBattleAnimationsWithoutTargets ?? false; // Move animation only needs one target - new MoveAnim(move.id as Moves, user, this.getTarget()?.getBattlerIndex()!).play(this.scene, () => { // TODO: is the bang correct here? + new MoveAnim(move.id as Moves, user, this.getTarget()!.getBattlerIndex()!, playOnEmptyField).play(this.scene, move.hitsSubstitute(user, this.getTarget()!), () => { /** Has the move successfully hit a target (for damage) yet? */ let hasHit: boolean = false; for (const target of targets) { - /** - * If the move missed a target, stop all future hits against that target - * and move on to the next target (if there is one). - */ - if (!targetHitChecks[target.getBattlerIndex()]) { - this.stopMultiHit(target); - this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: getPokemonNameWithAffix(target) })); - if (moveHistoryEntry.result === MoveResult.PENDING) { - moveHistoryEntry.result = MoveResult.MISS; - } - user.pushMoveHistory(moveHistoryEntry); - applyMoveAttrs(MissEffectAttr, user, null, move); - continue; - } /** The {@linkcode ArenaTagSide} to which the target belongs */ const targetSide = target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; @@ -155,6 +142,21 @@ export class MoveEffectPhase extends PokemonPhase { && (hasConditionalProtectApplied.value || (!target.findTags(t => t instanceof DamageProtectedTag).length && target.findTags(t => t instanceof ProtectedTag).find(t => target.lapseTag(t.tagType))) || (this.move.getMove().category !== MoveCategory.STATUS && target.findTags(t => t instanceof DamageProtectedTag).find(t => target.lapseTag(t.tagType)))); + /** + * If the move missed a target, stop all future hits against that target + * and move on to the next target (if there is one). + */ + if (!isProtected && !targetHitChecks[target.getBattlerIndex()]) { + this.stopMultiHit(target); + this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: getPokemonNameWithAffix(target) })); + if (moveHistoryEntry.result === MoveResult.PENDING) { + moveHistoryEntry.result = MoveResult.MISS; + } + user.pushMoveHistory(moveHistoryEntry); + applyMoveAttrs(MissEffectAttr, user, null, move); + continue; + } + /** Does this phase represent the invoked move's first strike? */ const firstHit = (user.turnData.hitsLeft === user.turnData.hitCount); @@ -246,7 +248,7 @@ export class MoveEffectPhase extends PokemonPhase { * If the move hit, and the target doesn't have Shield Dust, * apply the chance to flinch the target gained from King's Rock */ - if (dealsDamage && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr)) { + if (dealsDamage && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && !move.hitsSubstitute(user, target)) { const flinched = new Utils.BooleanHolder(false); user.scene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched); if (flinched.value) { @@ -258,14 +260,19 @@ export class MoveEffectPhase extends PokemonPhase { && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit) && (!attr.firstTargetOnly || firstTarget), user, target, this.move.getMove()).then(() => { // Apply the target's post-defend ability effects (as long as the target is active or can otherwise apply them) return Utils.executeIf(!target.isFainted() || target.canApplyAbility(), () => applyPostDefendAbAttrs(PostDefendAbAttr, target, user, this.move.getMove(), hitResult).then(() => { - // If the invoked move is an enemy attack, apply the enemy's status effect-inflicting tags and tokens + // Only apply the following effects if the move was not deflected by a substitute + if (move.hitsSubstitute(user, target)) { + return resolve(); + } + + // If the invoked move is an enemy attack, apply the enemy's status effect-inflicting tokens + if (!user.isPlayer() && this.move.getMove() instanceof AttackMove) { + user.scene.applyShuffledModifiers(this.scene, EnemyAttackStatusEffectChanceModifier, false, target); + } target.lapseTag(BattlerTagType.BEAK_BLAST_CHARGING); if (move.category === MoveCategory.PHYSICAL && user.isPlayer() !== target.isPlayer()) { target.lapseTag(BattlerTagType.SHELL_TRAP); } - if (!user.isPlayer() && this.move.getMove() instanceof AttackMove) { - user.scene.applyShuffledModifiers(this.scene, EnemyAttackStatusEffectChanceModifier, false, target); - } })).then(() => { // Apply the user's post-attack ability effects applyPostAttackAbAttrs(PostAttackAbAttr, user, target, this.move.getMove(), hitResult).then(() => { @@ -306,7 +313,20 @@ export class MoveEffectPhase extends PokemonPhase { } // Wait for all move effects to finish applying, then end this phase - Promise.allSettled(applyAttrs).then(() => this.end()); + Promise.allSettled(applyAttrs).then(() => { + /** + * Remove the target's substitute (if it exists and has expired) + * after all targeted effects have applied. + * This prevents blocked effects from applying until after this hit resolves. + */ + targets.forEach(target => { + const substitute = target.getTag(SubstituteTag); + if (!!substitute && substitute.hp <= 0) { + target.lapseTag(BattlerTagType.SUBSTITUTE); + } + }); + this.end(); + }); }); }); } diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index 6089e7d3202..e63096360dd 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -167,6 +167,7 @@ export class MovePhase extends BattlePhase { this.pokemon.pushMoveHistory({ move: Moves.NONE, result: MoveResult.FAIL }); this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT); // Remove any tags from moves like Fly/Dive/etc. + this.pokemon.lapseTags(BattlerTagLapseType.AFTER_MOVE); moveQueue.shift(); // Remove the second turn of charge moves return this.end(); } @@ -186,6 +187,7 @@ export class MovePhase extends BattlePhase { this.pokemon.pushMoveHistory({ move: Moves.NONE, result: MoveResult.FAIL }); this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT); // Remove any tags from moves like Fly/Dive/etc. + this.pokemon.lapseTags(BattlerTagLapseType.AFTER_MOVE); moveQueue.shift(); return this.end(); diff --git a/src/phases/mystery-encounter-phases.ts b/src/phases/mystery-encounter-phases.ts new file mode 100644 index 00000000000..007b69650b9 --- /dev/null +++ b/src/phases/mystery-encounter-phases.ts @@ -0,0 +1,605 @@ +import i18next from "i18next"; +import BattleScene from "../battle-scene"; +import { Phase } from "../phase"; +import { Mode } from "../ui/ui"; +import { transitionMysteryEncounterIntroVisuals, OptionSelectSettings } from "../data/mystery-encounters/utils/encounter-phase-utils"; +import MysteryEncounterOption, { OptionPhaseCallback } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { getCharVariantFromDialogue } from "../data/dialogue"; +import { TrainerSlot } from "../data/trainer-config"; +import { BattleSpec } from "#enums/battle-spec"; +import { IvScannerModifier } from "../modifier/modifier"; +import * as Utils from "../utils"; +import { isNullOrUndefined } from "../utils"; +import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { BattlerTagLapseType } from "#app/data/battler-tags"; +import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; +import { PostTurnStatusEffectPhase } from "#app/phases/post-turn-status-effect-phase"; +import { SummonPhase } from "#app/phases/summon-phase"; +import { ScanIvsPhase } from "#app/phases/scan-ivs-phase"; +import { ToggleDoublePositionPhase } from "#app/phases/toggle-double-position-phase"; +import { ReturnPhase } from "#app/phases/return-phase"; +import { CheckSwitchPhase } from "#app/phases/check-switch-phase"; +import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; +import { NewBattlePhase } from "#app/phases/new-battle-phase"; +import { GameOverPhase } from "#app/phases/game-over-phase"; +import { SwitchPhase } from "#app/phases/switch-phase"; +import { SeenEncounterData } from "#app/data/mystery-encounters/mystery-encounter-save-data"; + +/** + * Will handle (in order): + * - Clearing of phase queues to enter the Mystery Encounter game state + * - Management of session data related to MEs + * - Initialization of ME option select menu and UI + * - Execute {@linkcode MysteryEncounter.onPreOptionPhase} logic if it exists for the selected option + * - Display any `OptionTextDisplay.selected` type dialogue that is set in the {@linkcode MysteryEncounterDialogue} dialogue tree for selected option + * - Queuing of the {@linkcode MysteryEncounterOptionSelectedPhase} + */ +export class MysteryEncounterPhase extends Phase { + private readonly FIRST_DIALOGUE_PROMPT_DELAY = 300; + optionSelectSettings?: OptionSelectSettings; + + /** + * Mostly useful for having repeated queries during a single encounter, where the queries and options may differ each time + * @param scene + * @param optionSelectSettings allows overriding the typical options of an encounter with new ones + */ + constructor(scene: BattleScene, optionSelectSettings?: OptionSelectSettings) { + super(scene); + this.optionSelectSettings = optionSelectSettings; + } + + /** + * Updates seed offset, sets seen encounter session data, sets UI mode + */ + start() { + super.start(); + + // Clears out queued phases that are part of standard battle + this.scene.clearPhaseQueue(); + this.scene.clearPhaseQueueSplice(); + + const encounter = this.scene.currentBattle.mysteryEncounter!; + encounter.updateSeedOffset(this.scene); + + if (!this.optionSelectSettings) { + // Sets flag that ME was encountered, only if this is not a followup option select phase + // Can be used in later MEs to check for requirements to spawn, run history, etc. + this.scene.mysteryEncounterSaveData.encounteredEvents.push(new SeenEncounterData(encounter.encounterType, encounter.encounterTier, this.scene.currentBattle.waveIndex)); + } + + // Initiates encounter dialogue window and option select + this.scene.ui.setMode(Mode.MYSTERY_ENCOUNTER, this.optionSelectSettings); + } + + /** + * Triggers after a player selects an option for the encounter + * @param option + * @param index + */ + handleOptionSelect(option: MysteryEncounterOption, index: number): boolean { + // Set option selected flag + this.scene.currentBattle.mysteryEncounter!.selectedOption = option; + + if (!this.optionSelectSettings) { + // Saves the selected option in the ME save data, only if this is not a followup option select phase + // Can be used for analytics purposes to track what options are popular on certain encounters + const encounterSaveData = this.scene.mysteryEncounterSaveData.encounteredEvents[this.scene.mysteryEncounterSaveData.encounteredEvents.length - 1]; + if (encounterSaveData.type === this.scene.currentBattle.mysteryEncounter?.encounterType) { + encounterSaveData.selectedOption = index; + } + } + + if (!option.onOptionPhase) { + return false; + } + + // Populate dialogue tokens for option requirements + this.scene.currentBattle.mysteryEncounter!.populateDialogueTokensFromRequirements(this.scene); + + if (option.onPreOptionPhase) { + this.scene.executeWithSeedOffset(async () => { + return await option.onPreOptionPhase!(this.scene) + .then((result) => { + if (isNullOrUndefined(result) || result) { + this.continueEncounter(); + } + }); + }, this.scene.currentBattle.mysteryEncounter?.getSeedOffset()); + } else { + this.continueEncounter(); + } + + return true; + } + + /** + * Queues {@linkcode MysteryEncounterOptionSelectedPhase}, displays option.selected dialogue and ends phase + */ + continueEncounter() { + const endDialogueAndContinueEncounter = () => { + this.scene.pushPhase(new MysteryEncounterOptionSelectedPhase(this.scene)); + this.end(); + }; + + const optionSelectDialogue = this.scene.currentBattle?.mysteryEncounter?.selectedOption?.dialogue; + if (optionSelectDialogue?.selected && optionSelectDialogue.selected.length > 0) { + // Handle intermediate dialogue (between player selection event and the onOptionSelect logic) + this.scene.ui.setMode(Mode.MESSAGE); + const selectedDialogue = optionSelectDialogue.selected; + let i = 0; + const showNextDialogue = () => { + const nextAction = i === selectedDialogue.length - 1 ? endDialogueAndContinueEncounter : showNextDialogue; + const dialogue = selectedDialogue[i]; + let title: string | null = null; + const text: string | null = getEncounterText(this.scene, dialogue.text); + if (dialogue.speaker) { + title = getEncounterText(this.scene, dialogue.speaker); + } + + i++; + if (title) { + this.scene.ui.showDialogue(text ?? "", title, null, nextAction, 0, i === 1 ? this.FIRST_DIALOGUE_PROMPT_DELAY : 0); + } else { + this.scene.ui.showText(text ?? "", null, nextAction, i === 1 ? this.FIRST_DIALOGUE_PROMPT_DELAY : 0, true); + } + }; + + showNextDialogue(); + } else { + endDialogueAndContinueEncounter(); + } + } + + /** + * Ends phase + */ + end() { + this.scene.ui.setMode(Mode.MESSAGE).then(() => super.end()); + } +} + +/** + * Will handle (in order): + * - Execute {@linkcode MysteryEncounter.onOptionSelect} logic if it exists for the selected option + * + * It is important to point out that no phases are directly queued by any logic within this phase + * Any phase that is meant to follow this one MUST be queued via the onOptionSelect() logic of the selected option + */ +export class MysteryEncounterOptionSelectedPhase extends Phase { + onOptionSelect: OptionPhaseCallback; + + constructor(scene: BattleScene) { + super(scene); + this.onOptionSelect = this.scene.currentBattle.mysteryEncounter!.selectedOption!.onOptionPhase; + } + + /** + * Will handle (in order): + * - Execute {@linkcode MysteryEncounter.onOptionSelect} logic if it exists for the selected option + * + * It is important to point out that no phases are directly queued by any logic within this phase. + * Any phase that is meant to follow this one MUST be queued via the {@linkcode MysteryEncounter.onOptionSelect} logic of the selected option. + */ + start() { + super.start(); + if (this.scene.currentBattle.mysteryEncounter?.autoHideIntroVisuals) { + transitionMysteryEncounterIntroVisuals(this.scene).then(() => { + this.scene.executeWithSeedOffset(() => { + this.onOptionSelect(this.scene).finally(() => { + this.end(); + }); + }, this.scene.currentBattle.mysteryEncounter?.getSeedOffset() * 500); + }); + } else { + this.scene.executeWithSeedOffset(() => { + this.onOptionSelect(this.scene).finally(() => { + this.end(); + }); + }, this.scene.currentBattle.mysteryEncounter?.getSeedOffset() * 500); + } + } +} + +/** + * Runs at the beginning of an Encounter's battle + * Will clean up any residual flinches, Endure, etc. that are left over from {@linkcode MysteryEncounter.startOfBattleEffects} + * Will also handle Game Overs, switches, etc. that could happen from {@linkcode handleMysteryEncounterBattleStartEffects} + * See {@linkcode TurnEndPhase} for more details + */ +export class MysteryEncounterBattleStartCleanupPhase extends Phase { + constructor(scene: BattleScene) { + super(scene); + } + + /** + * Cleans up `TURN_END` tags, any {@linkcode PostTurnStatusEffectPhase}s, checks for Pokemon switches, then continues + */ + start() { + super.start(); + + const field = this.scene.getField(true).filter(p => p.summonData); + field.forEach(pokemon => { + pokemon.lapseTags(BattlerTagLapseType.TURN_END); + }); + + // Remove any status tick phases + while (!!this.scene.findPhase(p => p instanceof PostTurnStatusEffectPhase)) { + this.scene.tryRemovePhase(p => p instanceof PostTurnStatusEffectPhase); + } + + // The total number of Pokemon in the player's party that can legally fight + const legalPlayerPokemon = this.scene.getParty().filter(p => p.isAllowedInBattle()); + // The total number of legal player Pokemon that aren't currently on the field + const legalPlayerPartyPokemon = legalPlayerPokemon.filter(p => !p.isActive(true)); + if (!legalPlayerPokemon.length) { + this.scene.unshiftPhase(new GameOverPhase(this.scene)); + return this.end(); + } + + // Check for any KOd player mons and switch + // For each fainted mon on the field, if there is a legal replacement, summon it + const playerField = this.scene.getPlayerField(); + playerField.forEach((pokemon, i) => { + if (!pokemon.isAllowedInBattle() && legalPlayerPartyPokemon.length > i) { + this.scene.unshiftPhase(new SwitchPhase(this.scene, i, true, false)); + } + }); + + // THEN, if is a double battle, and player only has 1 summoned pokemon, center pokemon on field + if (this.scene.currentBattle.double && legalPlayerPokemon.length === 1 && legalPlayerPartyPokemon.length === 0) { + this.scene.unshiftPhase(new ToggleDoublePositionPhase(this.scene, true)); + } + + this.end(); + } +} + +/** + * Will handle (in order): + * - Setting BGM + * - Showing intro dialogue for an enemy trainer or wild Pokemon + * - Sliding in the visuals for enemy trainer or wild Pokemon, as well as handling summoning animations + * - Queue the {@linkcode SummonPhase}s, {@linkcode PostSummonPhase}s, etc., required to initialize the phase queue for a battle + */ +export class MysteryEncounterBattlePhase extends Phase { + disableSwitch: boolean; + + constructor(scene: BattleScene, disableSwitch = false) { + super(scene); + this.disableSwitch = disableSwitch; + } + + /** + * Sets up a ME battle + */ + start() { + super.start(); + + this.doMysteryEncounterBattle(this.scene); + } + + /** + * Gets intro battle message for new battle + * @param scene + * @private + */ + private getBattleMessage(scene: BattleScene): string { + const enemyField = scene.getEnemyField(); + const encounterMode = scene.currentBattle.mysteryEncounter!.encounterMode; + + if (scene.currentBattle.battleSpec === BattleSpec.FINAL_BOSS) { + return i18next.t("battle:bossAppeared", { bossName: enemyField[0].name }); + } + + if (encounterMode === MysteryEncounterMode.TRAINER_BATTLE) { + if (scene.currentBattle.double) { + return i18next.t("battle:trainerAppearedDouble", { trainerName: scene.currentBattle.trainer?.getName(TrainerSlot.NONE, true) }); + + } else { + return i18next.t("battle:trainerAppeared", { trainerName: scene.currentBattle.trainer?.getName(TrainerSlot.NONE, true) }); + } + } + + return enemyField.length === 1 + ? i18next.t("battle:singleWildAppeared", { pokemonName: enemyField[0].name }) + : i18next.t("battle:multiWildAppeared", { pokemonName1: enemyField[0].name, pokemonName2: enemyField[1].name }); + } + + /** + * Queues {@linkcode SummonPhase}s for the new battle, and handles trainer animations/dialogue if it's a Trainer battle + * @param scene + * @private + */ + private doMysteryEncounterBattle(scene: BattleScene) { + const encounterMode = scene.currentBattle.mysteryEncounter!.encounterMode; + if (encounterMode === MysteryEncounterMode.WILD_BATTLE || encounterMode === MysteryEncounterMode.BOSS_BATTLE) { + // Summons the wild/boss Pokemon + if (encounterMode === MysteryEncounterMode.BOSS_BATTLE) { + scene.playBgm(undefined); + } + const availablePartyMembers = scene.getEnemyParty().filter(p => !p.isFainted()).length; + scene.unshiftPhase(new SummonPhase(scene, 0, false)); + if (scene.currentBattle.double && availablePartyMembers > 1) { + scene.unshiftPhase(new SummonPhase(scene, 1, false)); + } + + if (!scene.currentBattle.mysteryEncounter?.hideBattleIntroMessage) { + scene.ui.showText(this.getBattleMessage(scene), null, () => this.endBattleSetup(scene), 0); + } else { + this.endBattleSetup(scene); + } + } else if (encounterMode === MysteryEncounterMode.TRAINER_BATTLE) { + this.showEnemyTrainer(); + const doSummon = () => { + scene.currentBattle.started = true; + scene.playBgm(undefined); + scene.pbTray.showPbTray(scene.getParty()); + scene.pbTrayEnemy.showPbTray(scene.getEnemyParty()); + const doTrainerSummon = () => { + this.hideEnemyTrainer(); + const availablePartyMembers = scene.getEnemyParty().filter(p => !p.isFainted()).length; + scene.unshiftPhase(new SummonPhase(scene, 0, false)); + if (scene.currentBattle.double && availablePartyMembers > 1) { + scene.unshiftPhase(new SummonPhase(scene, 1, false)); + } + this.endBattleSetup(scene); + }; + if (!scene.currentBattle.mysteryEncounter?.hideBattleIntroMessage) { + scene.ui.showText(this.getBattleMessage(scene), null, doTrainerSummon, 1000, true); + } else { + doTrainerSummon(); + } + }; + + const encounterMessages = scene.currentBattle.trainer?.getEncounterMessages(); + + if (!encounterMessages || !encounterMessages.length) { + doSummon(); + } else { + const trainer = this.scene.currentBattle.trainer; + let message: string; + scene.executeWithSeedOffset(() => message = Utils.randSeedItem(encounterMessages), this.scene.currentBattle.mysteryEncounter?.getSeedOffset()); + message = message!; // tell TS compiler it's defined now + const showDialogueAndSummon = () => { + scene.ui.showDialogue(message, trainer?.getName(TrainerSlot.NONE, true), null, () => { + scene.charSprite.hide().then(() => scene.hideFieldOverlay(250).then(() => doSummon())); + }); + }; + if (this.scene.currentBattle.trainer?.config.hasCharSprite && !this.scene.ui.shouldSkipDialogue(message)) { + this.scene.showFieldOverlay(500).then(() => this.scene.charSprite.showCharacter(trainer?.getKey()!, getCharVariantFromDialogue(encounterMessages[0])).then(() => showDialogueAndSummon())); // TODO: is this bang correct? + } else { + showDialogueAndSummon(); + } + } + } + } + + /** + * Initiate {@linkcode SummonPhase}s, {@linkcode ScanIvsPhase}, {@linkcode PostSummonPhase}s, etc. + * @param scene + * @private + */ + private endBattleSetup(scene: BattleScene) { + const enemyField = scene.getEnemyField(); + const encounterMode = scene.currentBattle.mysteryEncounter!.encounterMode; + + // PostSummon and ShinySparkle phases are handled by SummonPhase + + if (encounterMode !== MysteryEncounterMode.TRAINER_BATTLE) { + const ivScannerModifier = this.scene.findModifier(m => m instanceof IvScannerModifier); + if (ivScannerModifier) { + enemyField.map(p => this.scene.pushPhase(new ScanIvsPhase(this.scene, p.getBattlerIndex(), Math.min(ivScannerModifier.getStackCount() * 2, 6)))); + } + } + + const availablePartyMembers = scene.getParty().filter(p => !p.isFainted()); + + if (!availablePartyMembers[0].isOnField()) { + scene.pushPhase(new SummonPhase(scene, 0)); + } + + if (scene.currentBattle.double) { + if (availablePartyMembers.length > 1) { + scene.pushPhase(new ToggleDoublePositionPhase(scene, true)); + if (!availablePartyMembers[1].isOnField()) { + scene.pushPhase(new SummonPhase(scene, 1)); + } + } + } else { + if (availablePartyMembers.length > 1 && availablePartyMembers[1].isOnField()) { + scene.pushPhase(new ReturnPhase(scene, 1)); + } + scene.pushPhase(new ToggleDoublePositionPhase(scene, false)); + } + + if (encounterMode !== MysteryEncounterMode.TRAINER_BATTLE && !this.disableSwitch) { + const minPartySize = scene.currentBattle.double ? 2 : 1; + if (availablePartyMembers.length > minPartySize) { + scene.pushPhase(new CheckSwitchPhase(scene, 0, scene.currentBattle.double)); + if (scene.currentBattle.double) { + scene.pushPhase(new CheckSwitchPhase(scene, 1, scene.currentBattle.double)); + } + } + } + + this.end(); + } + + /** + * Ease in enemy trainer + * @private + */ + private showEnemyTrainer(): void { + // Show enemy trainer + const trainer = this.scene.currentBattle.trainer; + if (!trainer) { + return; + } + trainer.alpha = 0; + trainer.x += 16; + trainer.y -= 16; + trainer.setVisible(true); + this.scene.tweens.add({ + targets: trainer, + x: "-=16", + y: "+=16", + alpha: 1, + ease: "Sine.easeInOut", + duration: 750, + onComplete: () => { + trainer.untint(100, "Sine.easeOut"); + trainer.playAnim(); + } + }); + } + + private hideEnemyTrainer(): void { + this.scene.tweens.add({ + targets: this.scene.currentBattle.trainer, + x: "+=16", + y: "-=16", + alpha: 0, + ease: "Sine.easeInOut", + duration: 750 + }); + } +} + +/** + * Will handle (in order): + * - doContinueEncounter() callback for continuous encounters with back-to-back battles (this should push/shift its own phases as needed) + * + * OR + * + * - Any encounter reward logic that is set within {@linkcode MysteryEncounter.doEncounterExp} + * - Any encounter reward logic that is set within {@linkcode MysteryEncounter.doEncounterRewards} + * - Otherwise, can add a no-reward-item shop with only Potions, etc. if addHealPhase is true + * - Queuing of the {@linkcode PostMysteryEncounterPhase} + */ +export class MysteryEncounterRewardsPhase extends Phase { + addHealPhase: boolean; + + constructor(scene: BattleScene, addHealPhase: boolean = false) { + super(scene); + this.addHealPhase = addHealPhase; + } + + /** + * Runs {@linkcode MysteryEncounter.doContinueEncounter} and ends phase, OR {@linkcode MysteryEncounter.onRewards} then continues encounter + */ + start() { + super.start(); + const encounter = this.scene.currentBattle.mysteryEncounter!; + + if (encounter.doContinueEncounter) { + encounter.doContinueEncounter(this.scene).then(() => { + this.end(); + }); + } else { + this.scene.executeWithSeedOffset(() => { + if (encounter.onRewards) { + encounter.onRewards(this.scene).then(() => { + this.doEncounterRewardsAndContinue(); + }); + } else { + this.doEncounterRewardsAndContinue(); + } + // Do not use ME's seedOffset for rewards, these should always be consistent with waveIndex (once per wave) + }, this.scene.currentBattle.waveIndex * 1000); + } + } + + /** + * Queues encounter EXP and rewards phases, {@linkcode PostMysteryEncounterPhase}, and ends phase + */ + doEncounterRewardsAndContinue() { + const encounter = this.scene.currentBattle.mysteryEncounter!; + + if (encounter.doEncounterExp) { + encounter.doEncounterExp(this.scene); + } + + if (encounter.doEncounterRewards) { + encounter.doEncounterRewards(this.scene); + } else if (this.addHealPhase) { + this.scene.tryRemovePhase(p => p instanceof SelectModifierPhase); + this.scene.unshiftPhase(new SelectModifierPhase(this.scene, 0, undefined, { fillRemaining: false, rerollMultiplier: -1 })); + } + + this.scene.pushPhase(new PostMysteryEncounterPhase(this.scene)); + this.end(); + } +} + +/** + * Will handle (in order): + * - {@linkcode MysteryEncounter.onPostOptionSelect} logic (based on an option that was selected) + * - Showing any outro dialogue messages + * - Cleanup of any leftover intro visuals + * - Queuing of the next wave + */ +export class PostMysteryEncounterPhase extends Phase { + private readonly FIRST_DIALOGUE_PROMPT_DELAY = 750; + onPostOptionSelect?: OptionPhaseCallback; + + constructor(scene: BattleScene) { + super(scene); + this.onPostOptionSelect = this.scene.currentBattle.mysteryEncounter?.selectedOption?.onPostOptionPhase; + } + + /** + * Runs {@linkcode MysteryEncounter.onPostOptionSelect} then continues encounter + */ + start() { + super.start(); + + if (this.onPostOptionSelect) { + this.scene.executeWithSeedOffset(async () => { + return await this.onPostOptionSelect!(this.scene) + .then((result) => { + if (isNullOrUndefined(result) || result) { + this.continueEncounter(); + } + }); + }, this.scene.currentBattle.mysteryEncounter?.getSeedOffset() * 2000); + } else { + this.continueEncounter(); + } + } + + /** + * Queues {@linkcode NewBattlePhase}, plays outro dialogue and ends phase + */ + continueEncounter() { + const endPhase = () => { + this.scene.pushPhase(new NewBattlePhase(this.scene)); + this.end(); + }; + + const outroDialogue = this.scene.currentBattle?.mysteryEncounter?.dialogue?.outro; + if (outroDialogue && outroDialogue.length > 0) { + let i = 0; + const showNextDialogue = () => { + const nextAction = i === outroDialogue.length - 1 ? endPhase : showNextDialogue; + const dialogue = outroDialogue[i]; + let title: string | null = null; + const text: string | null = getEncounterText(this.scene, dialogue.text); + if (dialogue.speaker) { + title = getEncounterText(this.scene, dialogue.speaker); + } + + i++; + this.scene.ui.setMode(Mode.MESSAGE); + if (title) { + this.scene.ui.showDialogue(text ?? "", title, null, nextAction, 0, i === 1 ? this.FIRST_DIALOGUE_PROMPT_DELAY : 0); + } else { + this.scene.ui.showText(text ?? "", null, nextAction, i === 1 ? this.FIRST_DIALOGUE_PROMPT_DELAY : 0, true); + } + }; + + showNextDialogue(); + } else { + endPhase(); + } + } +} diff --git a/src/phases/new-biome-encounter-phase.ts b/src/phases/new-biome-encounter-phase.ts index 9f1209fb7ee..2a526a22ee2 100644 --- a/src/phases/new-biome-encounter-phase.ts +++ b/src/phases/new-biome-encounter-phase.ts @@ -17,15 +17,19 @@ export class NewBiomeEncounterPhase extends NextEncounterPhase { } } - this.scene.arena.trySetWeather(getRandomWeatherType(this.scene.arena), false); - for (const pokemon of this.scene.getParty().filter(p => p.isOnField())) { applyAbAttrs(PostBiomeChangeAbAttr, pokemon, null); } const enemyField = this.scene.getEnemyField(); + const moveTargets: any[] = [this.scene.arenaEnemy, enemyField]; + const mysteryEncounter = this.scene.currentBattle?.mysteryEncounter?.introVisuals; + if (mysteryEncounter) { + moveTargets.push(mysteryEncounter); + } + this.scene.tweens.add({ - targets: [this.scene.arenaEnemy, enemyField].flat(), + targets: moveTargets.flat(), x: "+=300", duration: 2000, onComplete: () => { @@ -35,4 +39,11 @@ export class NewBiomeEncounterPhase extends NextEncounterPhase { } }); } + + /** + * Set biome weather. + */ + trySetWeatherIfNewBiome(): void { + this.scene.arena.trySetWeather(getRandomWeatherType(this.scene.arena), false); + } } diff --git a/src/phases/next-encounter-phase.ts b/src/phases/next-encounter-phase.ts index d51aa374b6e..d63823e4167 100644 --- a/src/phases/next-encounter-phase.ts +++ b/src/phases/next-encounter-phase.ts @@ -23,8 +23,28 @@ export class NextEncounterPhase extends EncounterPhase { this.scene.arenaNextEnemy.setVisible(true); const enemyField = this.scene.getEnemyField(); + const moveTargets: any[] = [this.scene.arenaEnemy, this.scene.arenaNextEnemy, this.scene.currentBattle.trainer, enemyField, this.scene.lastEnemyTrainer]; + const lastEncounterVisuals = this.scene.lastMysteryEncounter?.introVisuals; + if (lastEncounterVisuals) { + moveTargets.push(lastEncounterVisuals); + } + const nextEncounterVisuals = this.scene.currentBattle.mysteryEncounter?.introVisuals; + if (nextEncounterVisuals) { + const enterFromRight = nextEncounterVisuals.enterFromRight; + if (enterFromRight) { + nextEncounterVisuals.x += 500; + this.scene.tweens.add({ + targets: nextEncounterVisuals, + x: "-=200", + duration: 2000 + }); + } else { + moveTargets.push(nextEncounterVisuals); + } + } + this.scene.tweens.add({ - targets: [this.scene.arenaEnemy, this.scene.arenaNextEnemy, this.scene.currentBattle.trainer, enemyField, this.scene.lastEnemyTrainer].flat(), + targets: moveTargets.flat(), x: "+=300", duration: 2000, onComplete: () => { @@ -36,6 +56,10 @@ export class NextEncounterPhase extends EncounterPhase { if (this.scene.lastEnemyTrainer) { this.scene.lastEnemyTrainer.destroy(); } + if (lastEncounterVisuals) { + this.scene.field.remove(lastEncounterVisuals, true); + this.scene.lastMysteryEncounter!.introVisuals = undefined; + } if (!this.tryOverrideForBattleSpec()) { this.doEncounterCommon(); @@ -43,4 +67,10 @@ export class NextEncounterPhase extends EncounterPhase { } }); } + + /** + * Do nothing (since this is simply the next wave in the same biome). + */ + trySetWeatherIfNewBiome(): void { + } } diff --git a/src/phases/obtain-status-effect-phase.ts b/src/phases/obtain-status-effect-phase.ts index bb06fafb1c9..93bf4cd41d5 100644 --- a/src/phases/obtain-status-effect-phase.ts +++ b/src/phases/obtain-status-effect-phase.ts @@ -31,7 +31,7 @@ export class ObtainStatusEffectPhase extends PokemonPhase { pokemon.status!.cureTurn = this.cureTurn; // TODO: is this bang correct? } pokemon.updateInfo(true); - new CommonBattleAnim(CommonAnim.POISON + (this.statusEffect! - 1), pokemon).play(this.scene, () => { + new CommonBattleAnim(CommonAnim.POISON + (this.statusEffect! - 1), pokemon).play(this.scene, false, () => { this.scene.queueMessage(getStatusEffectObtainText(this.statusEffect, getPokemonNameWithAffix(pokemon), this.sourceText ?? undefined)); if (pokemon.status?.isPostTurn()) { this.scene.pushPhase(new PostTurnStatusEffectPhase(this.scene, this.battlerIndex)); diff --git a/src/phases/party-exp-phase.ts b/src/phases/party-exp-phase.ts new file mode 100644 index 00000000000..c5a254871ca --- /dev/null +++ b/src/phases/party-exp-phase.ts @@ -0,0 +1,31 @@ +import BattleScene from "#app/battle-scene"; +import { Phase } from "#app/phase"; + +/** + * Provides EXP to the player's party *without* doing any Pokemon defeated checks or queueing extraneous post-battle phases + * Intended to be used as a more 1-off phase to provide exp to the party (such as during MEs), rather than cleanup a battle entirely + */ +export class PartyExpPhase extends Phase { + expValue: number; + useWaveIndexMultiplier?: boolean; + pokemonParticipantIds?: Set; + + constructor(scene: BattleScene, expValue: number, useWaveIndexMultiplier?: boolean, pokemonParticipantIds?: Set) { + super(scene); + + this.expValue = expValue; + this.useWaveIndexMultiplier = useWaveIndexMultiplier; + this.pokemonParticipantIds = pokemonParticipantIds; + } + + /** + * Gives EXP to the party + */ + start() { + super.start(); + + this.scene.applyPartyExp(this.expValue, false, this.useWaveIndexMultiplier, this.pokemonParticipantIds); + + this.end(); + } +} diff --git a/src/phases/pokemon-anim-phase.ts b/src/phases/pokemon-anim-phase.ts new file mode 100644 index 00000000000..50a62837f9c --- /dev/null +++ b/src/phases/pokemon-anim-phase.ts @@ -0,0 +1,237 @@ +import BattleScene from "#app/battle-scene"; +import { SubstituteTag } from "#app/data/battler-tags"; +import { PokemonAnimType } from "#enums/pokemon-anim-type"; +import Pokemon from "#app/field/pokemon"; +import { BattlePhase } from "#app/phases/battle-phase"; + + + +export class PokemonAnimPhase extends BattlePhase { + /** The type of animation to play in this phase */ + private key: PokemonAnimType; + /** The Pokemon to which this animation applies */ + private pokemon: Pokemon; + /** Any other field sprites affected by this animation */ + private fieldAssets: Phaser.GameObjects.Sprite[]; + + constructor(scene: BattleScene, key: PokemonAnimType, pokemon: Pokemon, fieldAssets?: Phaser.GameObjects.Sprite[]) { + super(scene); + + this.key = key; + this.pokemon = pokemon; + this.fieldAssets = fieldAssets ?? []; + } + + start(): void { + super.start(); + + switch (this.key) { + case PokemonAnimType.SUBSTITUTE_ADD: + this.doSubstituteAddAnim(); + break; + case PokemonAnimType.SUBSTITUTE_PRE_MOVE: + this.doSubstitutePreMoveAnim(); + break; + case PokemonAnimType.SUBSTITUTE_POST_MOVE: + this.doSubstitutePostMoveAnim(); + break; + case PokemonAnimType.SUBSTITUTE_REMOVE: + this.doSubstituteRemoveAnim(); + break; + default: + this.end(); + } + } + + doSubstituteAddAnim(): void { + const substitute = this.pokemon.getTag(SubstituteTag); + if (substitute === null) { + return this.end(); + } + + const getSprite = () => { + const sprite = this.scene.addFieldSprite( + this.pokemon.x + this.pokemon.getSprite().x, + this.pokemon.y + this.pokemon.getSprite().y, + `pkmn${this.pokemon.isPlayer() ? "__back": ""}__sub` + ); + sprite.setOrigin(0.5, 1); + this.scene.field.add(sprite); + return sprite; + }; + + const [ subSprite, subTintSprite ] = [ getSprite(), getSprite() ]; + const subScale = this.pokemon.getSpriteScale() * (this.pokemon.isPlayer() ? 0.5 : 1); + + subSprite.setVisible(false); + subSprite.setScale(subScale); + subTintSprite.setTintFill(0xFFFFFF); + subTintSprite.setScale(0.01); + + if (this.pokemon.isPlayer()) { + this.scene.field.bringToTop(this.pokemon); + } + + this.scene.playSound("PRSFX- Transform"); + + this.scene.tweens.add({ + targets: this.pokemon, + duration: 500, + x: this.pokemon.x + this.pokemon.getSubstituteOffset()[0], + y: this.pokemon.y + this.pokemon.getSubstituteOffset()[1], + alpha: 0.5, + ease: "Sine.easeIn" + }); + + this.scene.tweens.add({ + targets: subTintSprite, + delay: 250, + scale: subScale, + ease: "Cubic.easeInOut", + duration: 500, + onComplete: () => { + subSprite.setVisible(true); + this.pokemon.scene.tweens.add({ + targets: subTintSprite, + delay: 250, + alpha: 0, + ease: "Cubic.easeOut", + duration: 1000, + onComplete: () => { + subTintSprite.destroy(); + substitute.sprite = subSprite; + this.end(); + } + }); + } + }); + } + + doSubstitutePreMoveAnim(): void { + if (this.fieldAssets.length !== 1) { + return this.end(); + } + + const subSprite = this.fieldAssets[0]; + if (subSprite === undefined) { + return this.end(); + } + + this.scene.tweens.add({ + targets: subSprite, + alpha: 0, + ease: "Sine.easeInOut", + duration: 500 + }); + + this.scene.tweens.add({ + targets: this.pokemon, + x: subSprite.x, + y: subSprite.y, + alpha: 1, + ease: "Sine.easeInOut", + delay: 250, + duration: 500, + onComplete: () => this.end() + }); + } + + doSubstitutePostMoveAnim(): void { + if (this.fieldAssets.length !== 1) { + return this.end(); + } + + const subSprite = this.fieldAssets[0]; + if (subSprite === undefined) { + return this.end(); + } + + this.scene.tweens.add({ + targets: this.pokemon, + x: subSprite.x + this.pokemon.getSubstituteOffset()[0], + y: subSprite.y + this.pokemon.getSubstituteOffset()[1], + alpha: 0.5, + ease: "Sine.easeInOut", + duration: 500 + }); + + this.scene.tweens.add({ + targets: subSprite, + alpha: 1, + ease: "Sine.easeInOut", + delay: 250, + duration: 500, + onComplete: () => this.end() + }); + } + + doSubstituteRemoveAnim(): void { + if (this.fieldAssets.length !== 1) { + return this.end(); + } + + const subSprite = this.fieldAssets[0]; + if (subSprite === undefined) { + return this.end(); + } + + const getSprite = () => { + const sprite = this.scene.addFieldSprite( + subSprite.x, + subSprite.y, + `pkmn${this.pokemon.isPlayer() ? "__back": ""}__sub` + ); + sprite.setOrigin(0.5, 1); + this.scene.field.add(sprite); + return sprite; + }; + + const subTintSprite = getSprite(); + const subScale = this.pokemon.getSpriteScale() * (this.pokemon.isPlayer() ? 0.5 : 1); + subTintSprite.setAlpha(0); + subTintSprite.setTintFill(0xFFFFFF); + subTintSprite.setScale(subScale); + + this.scene.tweens.add({ + targets: subTintSprite, + alpha: 1, + ease: "Sine.easeInOut", + duration: 500, + onComplete: () => { + subSprite.destroy(); + const flashTimer = this.scene.time.addEvent({ + delay: 100, + repeat: 7, + startAt: 200, + callback: () => { + this.scene.playSound("PRSFX- Substitute2.wav"); + + subTintSprite.setVisible(flashTimer.repeatCount % 2 === 0); + if (!flashTimer.repeatCount) { + this.scene.tweens.add({ + targets: subTintSprite, + scale: 0.01, + ease: "Sine.cubicEaseIn", + duration: 500 + }); + + this.scene.tweens.add({ + targets: this.pokemon, + x: this.pokemon.x - this.pokemon.getSubstituteOffset()[0], + y: this.pokemon.y - this.pokemon.getSubstituteOffset()[1], + alpha: 1, + ease: "Sine.easeInOut", + delay: 250, + duration: 500, + onComplete: () => { + subTintSprite.destroy(); + this.end(); + } + }); + } + } + }); + } + }); + } +} diff --git a/src/phases/pokemon-heal-phase.ts b/src/phases/pokemon-heal-phase.ts index 49db2641e98..813d15ae87e 100644 --- a/src/phases/pokemon-heal-phase.ts +++ b/src/phases/pokemon-heal-phase.ts @@ -10,6 +10,8 @@ import { HealAchv } from "#app/system/achv"; import i18next from "i18next"; import * as Utils from "#app/utils"; import { CommonAnimPhase } from "./common-anim-phase"; +import { BattlerTagType } from "#app/enums/battler-tag-type"; +import { HealBlockTag } from "#app/data/battler-tags"; export class PokemonHealPhase extends CommonAnimPhase { private hpHealed: integer; @@ -50,9 +52,14 @@ export class PokemonHealPhase extends CommonAnimPhase { const hasMessage = !!this.message; const healOrDamage = (!pokemon.isFullHp() || this.hpHealed < 0); + const healBlock = pokemon.getTag(BattlerTagType.HEAL_BLOCK) as HealBlockTag; let lastStatusEffect = StatusEffect.NONE; - if (healOrDamage) { + if (healBlock && this.hpHealed > 0) { + this.scene.queueMessage(healBlock.onActivation(pokemon)); + this.message = null; + super.end(); + } else if (healOrDamage) { const hpRestoreMultiplier = new Utils.IntegerHolder(1); if (!this.revive) { this.scene.applyModifiers(HealingBoosterModifier, this.player, hpRestoreMultiplier); diff --git a/src/phases/post-summon-phase.ts b/src/phases/post-summon-phase.ts index 47a5513f0eb..e7f6c6ea3db 100644 --- a/src/phases/post-summon-phase.ts +++ b/src/phases/post-summon-phase.ts @@ -4,6 +4,9 @@ import { applyPostSummonAbAttrs, PostSummonAbAttr } from "#app/data/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 { BattleType } from "#app/battle"; export class PostSummonPhase extends PokemonPhase { constructor(scene: BattleScene, battlerIndex: BattlerIndex) { @@ -19,6 +22,12 @@ export class PostSummonPhase extends PokemonPhase { pokemon.status.turnCount = 0; } this.scene.arena.applyTags(ArenaTrapTag, pokemon); + + // If this is mystery encounter and has post summon phase tag, apply post summon effects + if (this.scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER && pokemon.findTags(t => t instanceof MysteryEncounterPostSummonTag).length > 0) { + pokemon.lapseTag(BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON); + } + applyPostSummonAbAttrs(PostSummonAbAttr, pokemon).then(() => this.end()); } } diff --git a/src/phases/post-turn-status-effect-phase.ts b/src/phases/post-turn-status-effect-phase.ts index 413f9eae65e..285bbddde88 100644 --- a/src/phases/post-turn-status-effect-phase.ts +++ b/src/phases/post-turn-status-effect-phase.ts @@ -42,7 +42,7 @@ export class PostTurnStatusEffectPhase extends PokemonPhase { this.scene.damageNumberHandler.add(this.getPokemon(), pokemon.damage(damage.value, false, true)); pokemon.updateInfo(); } - new CommonBattleAnim(CommonAnim.POISON + (pokemon.status.effect - 1), pokemon).play(this.scene, () => this.end()); + new CommonBattleAnim(CommonAnim.POISON + (pokemon.status.effect - 1), pokemon).play(this.scene, false, () => this.end()); } else { this.end(); } diff --git a/src/phases/return-phase.ts b/src/phases/return-phase.ts index dfc458eb817..19c73816b36 100644 --- a/src/phases/return-phase.ts +++ b/src/phases/return-phase.ts @@ -16,6 +16,7 @@ export class ReturnPhase extends SwitchSummonPhase { onEnd(): void { const pokemon = this.getPokemon(); + pokemon.resetSprite(); pokemon.resetTurnData(); pokemon.resetSummonData(); diff --git a/src/phases/scan-ivs-phase.ts b/src/phases/scan-ivs-phase.ts index ba27e4f1943..5ec61d5eec6 100644 --- a/src/phases/scan-ivs-phase.ts +++ b/src/phases/scan-ivs-phase.ts @@ -53,7 +53,7 @@ export class ScanIvsPhase extends PokemonPhase { this.scene.ui.setMode(Mode.CONFIRM, () => { this.scene.ui.setMode(Mode.MESSAGE); this.scene.ui.clearText(); - new CommonBattleAnim(CommonAnim.LOCK_ON, pokemon, pokemon).play(this.scene, () => { + new CommonBattleAnim(CommonAnim.LOCK_ON, pokemon, pokemon).play(this.scene, false, () => { this.scene.ui.getMessageHandler().promptIvs(pokemon.id, pokemon.ivs, this.shownIvs).then(() => this.end()); }); }, () => { diff --git a/src/phases/select-modifier-phase.ts b/src/phases/select-modifier-phase.ts index e14638c5dd2..58fb13ac466 100644 --- a/src/phases/select-modifier-phase.ts +++ b/src/phases/select-modifier-phase.ts @@ -1,7 +1,7 @@ import BattleScene from "#app/battle-scene"; import { ModifierTier } from "#app/modifier/modifier-tier"; import { regenerateModifierPoolThresholds, ModifierTypeOption, ModifierType, getPlayerShopModifierTypeOptionsForWave, PokemonModifierType, FusePokemonModifierType, PokemonMoveModifierType, TmModifierType, RememberMoveModifierType, PokemonPpRestoreModifierType, PokemonPpUpModifierType, ModifierPoolType, getPlayerModifierTypeOptions } from "#app/modifier/modifier-type"; -import { ExtraModifierModifier, Modifier, PokemonHeldItemModifier } from "#app/modifier/modifier"; +import { ExtraModifierModifier, HealShopCostModifier, Modifier, PokemonHeldItemModifier } from "#app/modifier/modifier"; import ModifierSelectUiHandler, { SHOP_OPTIONS_ROW_LIMIT } from "#app/ui/modifier-select-ui-handler"; import PartyUiHandler, { PartyUiMode, PartyOption } from "#app/ui/party-ui-handler"; import { Mode } from "#app/ui/ui"; @@ -9,16 +9,20 @@ import i18next from "i18next"; import * as Utils from "#app/utils"; import { BattlePhase } from "./battle-phase"; import Overrides from "#app/overrides"; +import { CustomModifierSettings } from "#app/modifier/modifier-type"; +import { isNullOrUndefined, NumberHolder } from "#app/utils"; export class SelectModifierPhase extends BattlePhase { private rerollCount: integer; - private modifierTiers: ModifierTier[]; + private modifierTiers?: ModifierTier[]; + private customModifierSettings?: CustomModifierSettings; - constructor(scene: BattleScene, rerollCount: integer = 0, modifierTiers?: ModifierTier[]) { + constructor(scene: BattleScene, rerollCount: integer = 0, modifierTiers?: ModifierTier[], customModifierSettings?: CustomModifierSettings) { super(scene); this.rerollCount = rerollCount; - this.modifierTiers = modifierTiers!; // TODO: is this bang correct? + this.modifierTiers = modifierTiers; + this.customModifierSettings = customModifierSettings; } start() { @@ -36,6 +40,20 @@ export class SelectModifierPhase extends BattlePhase { if (this.isPlayer()) { this.scene.applyModifiers(ExtraModifierModifier, true, modifierCount); } + + // If custom modifiers are specified, overrides default item count + if (!!this.customModifierSettings) { + const newItemCount = (this.customModifierSettings.guaranteedModifierTiers?.length || 0) + + (this.customModifierSettings.guaranteedModifierTypeOptions?.length || 0) + + (this.customModifierSettings.guaranteedModifierTypeFuncs?.length || 0); + if (this.customModifierSettings.fillRemaining) { + const originalCount = modifierCount.value; + modifierCount.value = originalCount > newItemCount ? originalCount : newItemCount; + } else { + modifierCount.value = newItemCount; + } + } + const typeOptions: ModifierTypeOption[] = this.getModifierTypeOptions(modifierCount.value); const modifierSelectCallback = (rowCursor: integer, cursor: integer) => { @@ -51,12 +69,12 @@ export class SelectModifierPhase extends BattlePhase { } let modifierType: ModifierType; let cost: integer; + const rerollCost = this.getRerollCost(typeOptions, this.scene.lockModifierTiers); switch (rowCursor) { case 0: switch (cursor) { case 0: - const rerollCost = this.getRerollCost(typeOptions, this.scene.lockModifierTiers); - if (this.scene.money < rerollCost) { + if (rerollCost < 0 || this.scene.money < rerollCost) { this.scene.ui.playError(); return false; } else { @@ -76,7 +94,7 @@ export class SelectModifierPhase extends BattlePhase { this.scene.ui.setModeWithoutClear(Mode.PARTY, PartyUiMode.MODIFIER_TRANSFER, -1, (fromSlotIndex: integer, itemIndex: integer, itemQuantity: integer, toSlotIndex: integer) => { if (toSlotIndex !== undefined && fromSlotIndex < 6 && toSlotIndex < 6 && fromSlotIndex !== toSlotIndex && itemIndex > -1) { const itemModifiers = this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier - && m.isTransferrable && m.pokemonId === party[fromSlotIndex].id) as PokemonHeldItemModifier[]; + && m.isTransferable && m.pokemonId === party[fromSlotIndex].id) as PokemonHeldItemModifier[]; const itemModifier = itemModifiers[itemIndex]; this.scene.tryTransferHeldItemModifier(itemModifier, party[toSlotIndex], true, itemQuantity); } else { @@ -90,6 +108,11 @@ export class SelectModifierPhase extends BattlePhase { }); break; case 3: + if (rerollCost < 0) { + // Reroll lock button is also disabled when reroll is disabled + this.scene.ui.playError(); + return false; + } this.scene.lockModifierTiers = !this.scene.lockModifierTiers; const uiHandler = this.scene.ui.getHandler() as ModifierSelectUiHandler; uiHandler.setRerollCost(this.getRerollCost(typeOptions, this.scene.lockModifierTiers)); @@ -99,6 +122,12 @@ export class SelectModifierPhase extends BattlePhase { } return true; case 1: + if (typeOptions.length === 0) { + this.scene.ui.clearText(); + this.scene.ui.setMode(Mode.MESSAGE); + super.end(); + return true; + } if (typeOptions[cursor].type) { modifierType = typeOptions[cursor].type; } @@ -109,7 +138,10 @@ export class SelectModifierPhase extends BattlePhase { if (shopOption.type) { modifierType = shopOption.type; } - cost = shopOption.cost; + // Apply Black Sludge to healing item cost + const healingItemCost = new NumberHolder(shopOption.cost); + this.scene.applyModifier(HealShopCostModifier, true, healingItemCost); + cost = healingItemCost.value; break; } @@ -217,7 +249,18 @@ export class SelectModifierPhase extends BattlePhase { } else { baseValue = 250; } - return Math.min(Math.ceil(this.scene.currentBattle.waveIndex / 10) * baseValue * Math.pow(2, this.rerollCount), Number.MAX_SAFE_INTEGER); + + let multiplier = 1; + if (!isNullOrUndefined(this.customModifierSettings?.rerollMultiplier)) { + if (this.customModifierSettings!.rerollMultiplier! < 0) { + // Completely overrides reroll cost to -1 and early exits + return -1; + } + + // Otherwise, continue with custom multiplier + multiplier = this.customModifierSettings!.rerollMultiplier!; + } + return Math.min(Math.ceil(this.scene.currentBattle.waveIndex / 10) * baseValue * Math.pow(2, this.rerollCount) * multiplier, Number.MAX_SAFE_INTEGER); } getPoolType(): ModifierPoolType { @@ -225,7 +268,7 @@ export class SelectModifierPhase extends BattlePhase { } getModifierTypeOptions(modifierCount: integer): ModifierTypeOption[] { - return getPlayerModifierTypeOptions(modifierCount, this.scene.getParty(), this.scene.lockModifierTiers ? this.modifierTiers : undefined); + return getPlayerModifierTypeOptions(modifierCount, this.scene.getParty(), this.scene.lockModifierTiers ? this.modifierTiers : undefined, this.customModifierSettings); } addModifier(modifier: Modifier): Promise { diff --git a/src/phases/select-target-phase.ts b/src/phases/select-target-phase.ts index 716d2737a6c..6f11f984c4b 100644 --- a/src/phases/select-target-phase.ts +++ b/src/phases/select-target-phase.ts @@ -4,6 +4,8 @@ import { Command } from "#app/ui/command-ui-handler"; import { Mode } from "#app/ui/ui"; import { CommandPhase } from "./command-phase"; import { PokemonPhase } from "./pokemon-phase"; +import i18next from "#app/plugins/i18n"; +import { allMoves } from "#app/data/move"; export class SelectTargetPhase extends PokemonPhase { constructor(scene: BattleScene, fieldIndex: integer) { @@ -17,6 +19,14 @@ export class SelectTargetPhase extends PokemonPhase { const move = turnCommand?.move?.move; this.scene.ui.setMode(Mode.TARGET_SELECT, this.fieldIndex, move, (targets: BattlerIndex[]) => { this.scene.ui.setMode(Mode.MESSAGE); + const fieldSide = this.scene.getField(); + const user = fieldSide[this.fieldIndex]; + const moveObject = allMoves[move!]; + if (moveObject && user.isMoveTargetRestricted(moveObject.id, user, fieldSide[targets[0]])) { + const errorMessage = user.getRestrictingTag(move!, user, fieldSide[targets[0]])!.selectionDeniedText(user, moveObject.id); + user.scene.queueMessage(i18next.t(errorMessage, { moveName: moveObject.name }), 0, true); + targets = []; + } if (targets.length < 1) { this.scene.currentBattle.turnCommands[this.fieldIndex] = null; this.scene.unshiftPhase(new CommandPhase(this.scene, this.fieldIndex)); diff --git a/src/phases/show-party-exp-bar-phase.ts b/src/phases/show-party-exp-bar-phase.ts index 9e019b202a5..f1783e7715f 100644 --- a/src/phases/show-party-exp-bar-phase.ts +++ b/src/phases/show-party-exp-bar-phase.ts @@ -1,4 +1,5 @@ import BattleScene from "#app/battle-scene"; +import { ExpGainsSpeed } from "#app/enums/exp-gains-speed"; import { ExpNotification } from "#app/enums/exp-notification"; import { ExpBoosterModifier } from "#app/modifier/modifier"; import * as Utils from "#app/utils"; @@ -44,7 +45,7 @@ export class ShowPartyExpBarPhase extends PlayerPartyMemberPokemonPhase { } else { this.end(); } - } else if (this.scene.expGainsSpeed < 3) { + } else if (this.scene.expGainsSpeed < ExpGainsSpeed.SKIP) { this.scene.partyExpBar.showPokemonExp(pokemon, exp.value, false, newLevel).then(() => { setTimeout(() => this.end(), 500 / Math.pow(2, this.scene.expGainsSpeed)); }); diff --git a/src/phases/stat-stage-change-phase.ts b/src/phases/stat-stage-change-phase.ts index 55faaa29903..4418c38c849 100644 --- a/src/phases/stat-stage-change-phase.ts +++ b/src/phases/stat-stage-change-phase.ts @@ -6,7 +6,7 @@ import Pokemon from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; import { ResetNegativeStatStageModifier } from "#app/modifier/modifier"; import { handleTutorial, Tutorial } from "#app/tutorial"; -import * as Utils from "#app/utils"; +import { NumberHolder, BooleanHolder } from "#app/utils"; import i18next from "i18next"; import { PokemonPhase } from "./pokemon-phase"; import { Stat, type BattleStat, getStatKey, getStatStageChangeDescriptionKey } from "#enums/stat"; @@ -42,17 +42,23 @@ export class StatStageChangePhase extends PokemonPhase { return this.end(); } + const stages = new NumberHolder(this.stages); + + if (!this.ignoreAbilities) { + applyAbAttrs(StatStageChangeMultiplierAbAttr, pokemon, null, false, stages); + } + let simulate = false; const filteredStats = this.stats.filter(stat => { - const cancelled = new Utils.BooleanHolder(false); + const cancelled = new BooleanHolder(false); - if (!this.selfTarget && this.stages < 0) { + if (!this.selfTarget && stages.value < 0) { // TODO: Include simulate boolean when tag applications can be simulated this.scene.arena.applyTagsForSide(MistTag, pokemon.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY, cancelled); } - if (!cancelled.value && !this.selfTarget && this.stages < 0) { + if (!cancelled.value && !this.selfTarget && stages.value < 0) { applyPreStatStageChangeAbAttrs(ProtectStatAbAttr, pokemon, stat, cancelled, simulate); } @@ -64,12 +70,6 @@ export class StatStageChangePhase extends PokemonPhase { return !cancelled.value; }); - const stages = new Utils.IntegerHolder(this.stages); - - if (!this.ignoreAbilities) { - applyAbAttrs(StatStageChangeMultiplierAbAttr, pokemon, null, false, stages); - } - const relLevels = filteredStats.map(s => (stages.value >= 1 ? Math.min(pokemon.getStatStage(s) + stages.value, 6) : Math.max(pokemon.getStatStage(s) + stages.value, -6)) - pokemon.getStatStage(s)); this.onChange && this.onChange(this.getPokemon(), filteredStats, relLevels); diff --git a/src/phases/summon-phase.ts b/src/phases/summon-phase.ts index 2645060c547..d909c5c3501 100644 --- a/src/phases/summon-phase.ts +++ b/src/phases/summon-phase.ts @@ -12,6 +12,7 @@ import { PartyMemberPokemonPhase } from "./party-member-pokemon-phase"; import { PostSummonPhase } from "./post-summon-phase"; import { GameOverPhase } from "./game-over-phase"; import { ShinySparklePhase } from "./shiny-sparkle-phase"; +import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; export class SummonPhase extends PartyMemberPokemonPhase { private loaded: boolean; @@ -33,8 +34,8 @@ export class SummonPhase extends PartyMemberPokemonPhase { */ preSummon(): void { const partyMember = this.getPokemon(); - // If the Pokemon about to be sent out is fainted or illegal under a challenge, switch to the first non-fainted legal Pokemon - if (!partyMember.isAllowedInBattle()) { + // If the Pokemon about to be sent out is fainted, illegal under a challenge, or no longer in the party for some reason, switch to the first non-fainted legal Pokemon + if (!partyMember.isAllowedInBattle() || (this.player && !this.getParty().some(p => p.id === partyMember.id))) { console.warn("The Pokemon about to be sent out is fainted or illegal under a challenge. Attempting to resolve..."); // First check if they're somehow still in play, if so remove them. @@ -79,16 +80,22 @@ export class SummonPhase extends PartyMemberPokemonPhase { onComplete: () => this.scene.trainer.setVisible(false) }); this.scene.time.delayedCall(750, () => this.summon()); - } else { + } else if (this.scene.currentBattle.battleType === BattleType.TRAINER || this.scene.currentBattle.mysteryEncounter?.encounterMode === MysteryEncounterMode.TRAINER_BATTLE) { const trainerName = this.scene.currentBattle.trainer?.getName(!(this.fieldIndex % 2) ? TrainerSlot.TRAINER : TrainerSlot.TRAINER_PARTNER); const pokemonName = this.getPokemon().getNameToRender(); const message = i18next.t("battle:trainerSendOut", { trainerName, pokemonName }); this.scene.pbTrayEnemy.hide(); this.scene.ui.showText(message, null, () => this.summon()); + } else if (this.scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER) { + this.scene.pbTrayEnemy.hide(); + this.summonWild(); } } + /** + * Enemy trainer or player trainer will do animations to throw Pokeball and summon a Pokemon to the field. + */ summon(): void { const pokemon = this.getPokemon(); @@ -167,6 +174,63 @@ export class SummonPhase extends PartyMemberPokemonPhase { }); } + /** + * Handles tweening and battle setup for a wild Pokemon that appears outside of the normal screen transition. + * Wild Pokemon will ease and fade in onto the field, then perform standard summon behavior. + * Currently only used by Mystery Encounters, as all other battle types pre-summon wild pokemon before screen transitions. + */ + summonWild(): void { + const pokemon = this.getPokemon(); + + if (this.fieldIndex === 1) { + pokemon.setFieldPosition(FieldPosition.RIGHT, 0); + } else { + const availablePartyMembers = this.getParty().filter(p => !p.isFainted()).length; + pokemon.setFieldPosition(!this.scene.currentBattle.double || availablePartyMembers === 1 ? FieldPosition.CENTER : FieldPosition.LEFT); + } + + this.scene.add.existing(pokemon); + this.scene.field.add(pokemon); + if (!this.player) { + const playerPokemon = this.scene.getPlayerPokemon() as Pokemon; + if (playerPokemon?.visible) { + this.scene.field.moveBelow(pokemon, playerPokemon); + } + this.scene.currentBattle.seenEnemyPartyMemberIds.add(pokemon.id); + } + this.scene.updateModifiers(this.player); + this.scene.updateFieldScale(); + pokemon.showInfo(); + pokemon.playAnim(); + pokemon.setVisible(true); + pokemon.getSprite().setVisible(true); + pokemon.setScale(0.75); + pokemon.tint(getPokeballTintColor(pokemon.pokeball)); + pokemon.untint(250, "Sine.easeIn"); + this.scene.updateFieldScale(); + pokemon.x += 16; + pokemon.y -= 20; + pokemon.alpha = 0; + + // Ease pokemon in + this.scene.tweens.add({ + targets: pokemon, + x: "-=16", + y: "+=16", + alpha: 1, + duration: 1000, + ease: "Sine.easeIn", + scale: pokemon.getSpriteScale(), + onComplete: () => { + pokemon.cry(pokemon.getHpRatio() > 0.25 ? undefined : { rate: 0.85 }); + pokemon.getSprite().clearTint(); + pokemon.resetSummonData(); + this.scene.updateFieldScale(); + this.scene.time.delayedCall(1000, () => this.end()); + } + }); + } + onEnd(): void { const pokemon = this.getPokemon(); @@ -176,7 +240,7 @@ export class SummonPhase extends PartyMemberPokemonPhase { pokemon.resetTurnData(); - if (!this.loaded || this.scene.currentBattle.battleType === BattleType.TRAINER || (this.scene.currentBattle.waveIndex % 10) === 1) { + if (!this.loaded || [BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER].includes(this.scene.currentBattle.battleType) || (this.scene.currentBattle.waveIndex % 10) === 1) { this.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true); this.queuePostSummon(); } diff --git a/src/phases/switch-summon-phase.ts b/src/phases/switch-summon-phase.ts index 2a5fd0cc3ac..525f74e896f 100644 --- a/src/phases/switch-summon-phase.ts +++ b/src/phases/switch-summon-phase.ts @@ -11,6 +11,7 @@ import { Command } from "#app/ui/command-ui-handler"; import i18next from "i18next"; import { PostSummonPhase } from "./post-summon-phase"; import { SummonPhase } from "./summon-phase"; +import { SubstituteTag } from "#app/data/battler-tags"; export class SwitchSummonPhase extends SummonPhase { private slotIndex: integer; @@ -65,6 +66,16 @@ export class SwitchSummonPhase extends SummonPhase { if (!this.batonPass) { (this.player ? this.scene.getEnemyField() : this.scene.getPlayerField()).forEach(enemyPokemon => enemyPokemon.removeTagsBySourceId(pokemon.id)); + const substitute = pokemon.getTag(SubstituteTag); + if (substitute) { + this.scene.tweens.add({ + targets: substitute.sprite, + duration: 250, + scale: substitute.sprite.scale * 0.5, + ease: "Sine.easeIn", + onComplete: () => substitute.sprite.destroy() + }); + } } this.scene.ui.showText(this.player ? @@ -115,8 +126,18 @@ export class SwitchSummonPhase extends SummonPhase { pokemonName: this.getPokemon().getNameToRender() }) ); - // Ensure improperly persisted summon data (such as tags) is cleared upon switching - if (!this.batonPass) { + /** + * If this switch is passing a Substitute, make the switched Pokemon match the returned Pokemon's state as it left. + * Otherwise, clear any persisting tags on the returned Pokemon. + */ + if (this.batonPass) { + const substitute = this.lastPokemon.getTag(SubstituteTag); + if (substitute) { + switchedInPokemon.x += this.lastPokemon.getSubstituteOffset()[0]; + switchedInPokemon.y += this.lastPokemon.getSubstituteOffset()[1]; + switchedInPokemon.setAlpha(0.5); + } + } else { switchedInPokemon.resetBattleData(); switchedInPokemon.resetSummonData(); } diff --git a/src/phases/trainer-victory-phase.ts b/src/phases/trainer-victory-phase.ts index 55b2a1608c0..e925f0c47d4 100644 --- a/src/phases/trainer-victory-phase.ts +++ b/src/phases/trainer-victory-phase.ts @@ -30,7 +30,7 @@ export class TrainerVictoryPhase extends BattlePhase { const trainerType = this.scene.currentBattle.trainer?.config.trainerType!; // TODO: is this bang correct? if (vouchers.hasOwnProperty(TrainerType[trainerType])) { if (!this.scene.validateVoucher(vouchers[TrainerType[trainerType]]) && this.scene.currentBattle.trainer?.config.isBoss) { - this.scene.unshiftPhase(new ModifierRewardPhase(this.scene, [modifierTypes.VOUCHER_PLUS, modifierTypes.VOUCHER_PLUS, modifierTypes.VOUCHER_PLUS, modifierTypes.VOUCHER_PREMIUM][vouchers[TrainerType[trainerType]].voucherType])); + this.scene.unshiftPhase(new ModifierRewardPhase(this.scene, [modifierTypes.VOUCHER, modifierTypes.VOUCHER, modifierTypes.VOUCHER_PLUS, modifierTypes.VOUCHER_PREMIUM][vouchers[TrainerType[trainerType]].voucherType])); } } diff --git a/src/phases/turn-init-phase.ts b/src/phases/turn-init-phase.ts index 568cfdc5714..92547878f12 100644 --- a/src/phases/turn-init-phase.ts +++ b/src/phases/turn-init-phase.ts @@ -9,6 +9,7 @@ import { CommandPhase } from "./command-phase"; import { EnemyCommandPhase } from "./enemy-command-phase"; import { GameOverPhase } from "./game-over-phase"; import { TurnStartPhase } from "./turn-start-phase"; +import { handleMysteryEncounterBattleStartEffects, handleMysteryEncounterTurnStartEffects } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; export class TurnInitPhase extends FieldPhase { constructor(scene: BattleScene) { @@ -46,6 +47,14 @@ export class TurnInitPhase extends FieldPhase { //this.scene.pushPhase(new MoveAnimTestPhase(this.scene)); this.scene.eventTarget.dispatchEvent(new TurnInitEvent()); + handleMysteryEncounterBattleStartEffects(this.scene); + + // If true, will skip remainder of current phase (and not queue CommandPhases etc.) + if (handleMysteryEncounterTurnStartEffects(this.scene)) { + this.end(); + return; + } + this.scene.getField().forEach((pokemon, i) => { if (pokemon?.isActive()) { if (pokemon.isPlayer()) { diff --git a/src/phases/victory-phase.ts b/src/phases/victory-phase.ts index 9679a79a37d..c10adc5683d 100644 --- a/src/phases/victory-phase.ts +++ b/src/phases/victory-phase.ts @@ -1,24 +1,25 @@ import BattleScene from "#app/battle-scene"; -import { BattlerIndex, BattleType } from "#app/battle"; -import { modifierTypes } from "#app/modifier/modifier-type"; -import { ExpShareModifier, ExpBalanceModifier, MultipleParticipantExpBonusModifier, PokemonExpBoosterModifier } from "#app/modifier/modifier"; -import * as Utils from "#app/utils"; -import Overrides from "#app/overrides"; +import { BattlerIndex, BattleType, ClassicFixedBossWaves } from "#app/battle"; +import { CustomModifierSettings, modifierTypes } from "#app/modifier/modifier-type"; import { BattleEndPhase } from "./battle-end-phase"; import { NewBattlePhase } from "./new-battle-phase"; import { PokemonPhase } from "./pokemon-phase"; import { AddEnemyBuffModifierPhase } from "./add-enemy-buff-modifier-phase"; import { EggLapsePhase } from "./egg-lapse-phase"; -import { ExpPhase } from "./exp-phase"; import { GameOverPhase } from "./game-over-phase"; import { ModifierRewardPhase } from "./modifier-reward-phase"; import { SelectModifierPhase } from "./select-modifier-phase"; -import { ShowPartyExpBarPhase } from "./show-party-exp-bar-phase"; import { TrainerVictoryPhase } from "./trainer-victory-phase"; +import { handleMysteryEncounterVictory } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; export class VictoryPhase extends PokemonPhase { - constructor(scene: BattleScene, battlerIndex: BattlerIndex) { + /** If true, indicates that the phase is intended for EXP purposes only, and not to continue a battle to next phase */ + isExpOnly: boolean; + + constructor(scene: BattleScene, battlerIndex: BattlerIndex | integer, isExpOnly: boolean = false) { super(scene, battlerIndex); + + this.isExpOnly = isExpOnly; } start() { @@ -26,96 +27,27 @@ export class VictoryPhase extends PokemonPhase { this.scene.gameData.gameStats.pokemonDefeated++; - const participantIds = this.scene.currentBattle.playerParticipantIds; - const party = this.scene.getParty(); - const expShareModifier = this.scene.findModifier(m => m instanceof ExpShareModifier) as ExpShareModifier; - const expBalanceModifier = this.scene.findModifier(m => m instanceof ExpBalanceModifier) as ExpBalanceModifier; - const multipleParticipantExpBonusModifier = this.scene.findModifier(m => m instanceof MultipleParticipantExpBonusModifier) as MultipleParticipantExpBonusModifier; - const nonFaintedPartyMembers = party.filter(p => p.hp); - const expPartyMembers = nonFaintedPartyMembers.filter(p => p.level < this.scene.getMaxExpLevel()); - const partyMemberExp: number[] = []; + const expValue = this.getPokemon().getExpValue(); + this.scene.applyPartyExp(expValue, true); - if (participantIds.size) { - let expValue = this.getPokemon().getExpValue(); - if (this.scene.currentBattle.battleType === BattleType.TRAINER) { - expValue = Math.floor(expValue * 1.5); - } - for (const partyMember of nonFaintedPartyMembers) { - const pId = partyMember.id; - const participated = participantIds.has(pId); - if (participated) { - partyMember.addFriendship(2); - } - if (!expPartyMembers.includes(partyMember)) { - continue; - } - if (!participated && !expShareModifier) { - partyMemberExp.push(0); - continue; - } - let expMultiplier = 0; - if (participated) { - expMultiplier += (1 / participantIds.size); - if (participantIds.size > 1 && multipleParticipantExpBonusModifier) { - expMultiplier += multipleParticipantExpBonusModifier.getStackCount() * 0.2; - } - } else if (expShareModifier) { - expMultiplier += (expShareModifier.getStackCount() * 0.2) / participantIds.size; - } - if (partyMember.pokerus) { - expMultiplier *= 1.5; - } - if (Overrides.XP_MULTIPLIER_OVERRIDE !== null) { - expMultiplier = Overrides.XP_MULTIPLIER_OVERRIDE; - } - const pokemonExp = new Utils.NumberHolder(expValue * expMultiplier); - this.scene.applyModifiers(PokemonExpBoosterModifier, true, partyMember, pokemonExp); - partyMemberExp.push(Math.floor(pokemonExp.value)); - } - - if (expBalanceModifier) { - let totalLevel = 0; - let totalExp = 0; - expPartyMembers.forEach((expPartyMember, epm) => { - totalExp += partyMemberExp[epm]; - totalLevel += expPartyMember.level; - }); - - const medianLevel = Math.floor(totalLevel / expPartyMembers.length); - - const recipientExpPartyMemberIndexes: number[] = []; - expPartyMembers.forEach((expPartyMember, epm) => { - if (expPartyMember.level <= medianLevel) { - recipientExpPartyMemberIndexes.push(epm); - } - }); - - const splitExp = Math.floor(totalExp / recipientExpPartyMemberIndexes.length); - - expPartyMembers.forEach((_partyMember, pm) => { - partyMemberExp[pm] = Phaser.Math.Linear(partyMemberExp[pm], recipientExpPartyMemberIndexes.indexOf(pm) > -1 ? splitExp : 0, 0.2 * expBalanceModifier.getStackCount()); - }); - } - - for (let pm = 0; pm < expPartyMembers.length; pm++) { - const exp = partyMemberExp[pm]; - - if (exp) { - const partyMemberIndex = party.indexOf(expPartyMembers[pm]); - this.scene.unshiftPhase(expPartyMembers[pm].isOnField() ? new ExpPhase(this.scene, partyMemberIndex, exp) : new ShowPartyExpBarPhase(this.scene, partyMemberIndex, exp)); - } - } + if (this.scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER) { + handleMysteryEncounterVictory(this.scene, false, this.isExpOnly); + return this.end(); } - if (!this.scene.getEnemyParty().find(p => this.scene.currentBattle.battleType ? !p?.isFainted(true) : p.isOnField())) { + if (!this.scene.getEnemyParty().find(p => this.scene.currentBattle.battleType === BattleType.WILD ? p.isOnField() : !p?.isFainted(true))) { this.scene.pushPhase(new BattleEndPhase(this.scene)); if (this.scene.currentBattle.battleType === BattleType.TRAINER) { this.scene.pushPhase(new TrainerVictoryPhase(this.scene)); } if (this.scene.gameMode.isEndless || !this.scene.gameMode.isWaveFinal(this.scene.currentBattle.waveIndex)) { this.scene.pushPhase(new EggLapsePhase(this.scene)); + if (this.scene.gameMode.isClassic && this.scene.currentBattle.waveIndex === ClassicFixedBossWaves.EVIL_BOSS_2) { + // Should get Lock Capsule on 165 before shop phase so it can be used in the rewards shop + this.scene.pushPhase(new ModifierRewardPhase(this.scene, modifierTypes.LOCK_CAPSULE)); + } if (this.scene.currentBattle.waveIndex % 10) { - this.scene.pushPhase(new SelectModifierPhase(this.scene)); + this.scene.pushPhase(new SelectModifierPhase(this.scene, undefined, undefined, this.getFixedBattleCustomModifiers())); } else if (this.scene.gameMode.isDaily) { this.scene.pushPhase(new ModifierRewardPhase(this.scene, modifierTypes.EXP_CHARM)); if (this.scene.currentBattle.waveIndex > 10 && !this.scene.gameMode.isWaveFinal(this.scene.currentBattle.waveIndex)) { @@ -148,4 +80,18 @@ export class VictoryPhase extends PokemonPhase { this.end(); } + + /** + * If this wave is a fixed battle with special custom modifier rewards, + * will pass those settings to the upcoming {@linkcode SelectModifierPhase}`. + */ + getFixedBattleCustomModifiers(): CustomModifierSettings | undefined { + const gameMode = this.scene.gameMode; + const waveIndex = this.scene.currentBattle.waveIndex; + if (gameMode.isFixedBattle(waveIndex)) { + return gameMode.getFixedBattle(waveIndex).customModifierRewardSettings; + } + + return undefined; + } } diff --git a/src/pipelines/sprite.ts b/src/pipelines/sprite.ts index c9a76dc50a4..88d6ce2d387 100644 --- a/src/pipelines/sprite.ts +++ b/src/pipelines/sprite.ts @@ -4,6 +4,7 @@ import Pokemon from "../field/pokemon"; import Trainer from "../field/trainer"; import FieldSpritePipeline from "./field-sprite"; import * as Utils from "../utils"; +import MysteryEncounterIntroVisuals from "../field/mystery-encounter-intro"; const spriteFragShader = ` #ifdef GL_FRAGMENT_PRECISION_HIGH @@ -37,6 +38,7 @@ uniform vec2 texFrameUv; uniform vec2 size; uniform vec2 texSize; uniform float yOffset; +uniform float yShadowOffset; uniform vec4 tone; uniform ivec4 baseVariantColors[32]; uniform vec4 variantColors[32]; @@ -251,7 +253,7 @@ void main() { float width = size.x - (yOffset / 2.0); float spriteX = ((floor(outPosition.x / fieldScale) - relPosition.x) / width) + 0.5; - float spriteY = ((floor(outPosition.y / fieldScale) - relPosition.y) / size.y); + float spriteY = ((floor(outPosition.y / fieldScale) - relPosition.y - yShadowOffset) / size.y); if (yCenter == 1) { spriteY += 0.5; @@ -338,6 +340,7 @@ export default class SpritePipeline extends FieldSpritePipeline { this.set2f("size", 0, 0); this.set2f("texSize", 0, 0); this.set1f("yOffset", 0); + this.set1f("yShadowOffset", 0); this.set4fv("tone", this._tone); } @@ -350,10 +353,11 @@ export default class SpritePipeline extends FieldSpritePipeline { const tone = data["tone"] as number[]; const teraColor = data["teraColor"] as integer[] ?? [ 0, 0, 0 ]; const hasShadow = data["hasShadow"] as boolean; + const yShadowOffset = data["yShadowOffset"] as number; const ignoreFieldPos = data["ignoreFieldPos"] as boolean; const ignoreOverride = data["ignoreOverride"] as boolean; - const isEntityObj = sprite.parentContainer instanceof Pokemon || sprite.parentContainer instanceof Trainer; + const isEntityObj = sprite.parentContainer instanceof Pokemon || sprite.parentContainer instanceof Trainer || sprite.parentContainer instanceof MysteryEncounterIntroVisuals; const field = isEntityObj ? sprite.parentContainer.parentContainer : sprite.parentContainer; const position = isEntityObj ? [ sprite.parentContainer.x, sprite.parentContainer.y ] @@ -376,6 +380,7 @@ export default class SpritePipeline extends FieldSpritePipeline { this.set2f("size", sprite.frame.width, sprite.height); this.set2f("texSize", sprite.texture.source[0].width, sprite.texture.source[0].height); this.set1f("yOffset", sprite.height - sprite.frame.height * (isEntityObj ? sprite.parentContainer.scale : sprite.scale)); + this.set1f("yShadowOffset", yShadowOffset ?? 0); this.set4fv("tone", tone); this.bindTexture(this.game.textures.get("tera").source[0].glTexture!, 1); // TODO: is this bang correct? @@ -447,14 +452,15 @@ export default class SpritePipeline extends FieldSpritePipeline { this.set1f("vCutoff", v1); const hasShadow = sprite.pipelineData["hasShadow"] as boolean; + const yShadowOffset = sprite.pipelineData["yShadowOffset"] as number ?? 0; if (hasShadow) { - const isEntityObj = sprite.parentContainer instanceof Pokemon || sprite.parentContainer instanceof Trainer; + const isEntityObj = sprite.parentContainer instanceof Pokemon || sprite.parentContainer instanceof Trainer || sprite.parentContainer instanceof MysteryEncounterIntroVisuals; const field = isEntityObj ? sprite.parentContainer.parentContainer : sprite.parentContainer; const fieldScaleRatio = field.scale / 6; const baseY = (isEntityObj ? sprite.parentContainer.y : sprite.y + sprite.height) * 6 / fieldScaleRatio; - const bottomPadding = Math.ceil(sprite.height * 0.05) * 6 / fieldScaleRatio; + const bottomPadding = Math.ceil(sprite.height * 0.05 + Math.max(yShadowOffset, 0)) * 6 / fieldScaleRatio; const yDelta = (baseY - y1) / field.scale; y2 = y1 = baseY + bottomPadding; const pixelHeight = (v1 - v0) / (sprite.frame.height * (isEntityObj ? sprite.parentContainer.scale : sprite.scale)); diff --git a/src/plugins/i18n.ts b/src/plugins/i18n.ts index 705fd5143a4..ec3fe93c765 100644 --- a/src/plugins/i18n.ts +++ b/src/plugins/i18n.ts @@ -167,6 +167,25 @@ export async function initI18n(): Promise { postProcess: ["korean-postposition"], }); + // Input: {{myMoneyValue, money}} + // Output: @[MONEY]{₽100,000,000} (useful for BBCode coloring of text) + // If you don't want the BBCode tag applied, just use 'number' formatter + i18next.services.formatter?.add("money", (value, lng, options) => { + const numberFormattedString = Intl.NumberFormat(lng, options).format(value); + switch (lng) { + case "ja": + return `@[MONEY]{${numberFormattedString}}円`; + case "de": + case "es": + case "fr": + case "it": + return `@[MONEY]{${numberFormattedString} ₽}`; + default: + // English and other languages that use same format + return `@[MONEY]{₽${numberFormattedString}}`; + } + }); + await initFonts(localStorage.getItem("prLang") ?? undefined); } diff --git a/src/system/achv.ts b/src/system/achv.ts index 6170fe23e1d..09ec74de50c 100644 --- a/src/system/achv.ts +++ b/src/system/achv.ts @@ -279,6 +279,8 @@ export function getAchievementDescription(localizationKey: string): string { return i18next.t("achv:FRESH_START.description", { context: genderStr }); case "INVERSE_BATTLE": return i18next.t("achv:INVERSE_BATTLE.description", { context: genderStr }); + case "BREEDERS_IN_SPACE": + return i18next.t("achv:BREEDERS_IN_SPACE.description", { context: genderStr }); default: return ""; } @@ -356,6 +358,7 @@ export const achvs = { MONO_FAIRY: new ChallengeAchv("MONO_FAIRY", "", "MONO_FAIRY.description", "fairy_feather", 100, (c, scene) => c instanceof SingleTypeChallenge && c.value === 18 && !scene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)), FRESH_START: new ChallengeAchv("FRESH_START", "", "FRESH_START.description", "reviver_seed", 100, (c, scene) => c instanceof FreshStartChallenge && c.value > 0 && !scene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)), INVERSE_BATTLE: new ChallengeAchv("INVERSE_BATTLE", "", "INVERSE_BATTLE.description", "inverse", 100, c => c instanceof InverseBattleChallenge && c.value > 0), + BREEDERS_IN_SPACE: new Achv("BREEDERS_IN_SPACE", "", "BREEDERS_IN_SPACE.description", "moon_stone", 100).setSecret(), }; export function initAchievements() { diff --git a/src/system/egg-data.ts b/src/system/egg-data.ts index 785ae364efe..1c9c903688a 100644 --- a/src/system/egg-data.ts +++ b/src/system/egg-data.ts @@ -1,6 +1,6 @@ import { EggTier } from "#enums/egg-type"; import { Species } from "#enums/species"; -import { VariantTier } from "#enums/variant-tiers"; +import { VariantTier } from "#enums/variant-tier"; import { EGG_SEED, Egg } from "../data/egg"; import { EggSourceType } from "#app/enums/egg-source-types"; diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 677bbe4add6..04fef4a81da 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -46,6 +46,8 @@ import { OutdatedPhase } from "#app/phases/outdated-phase"; import { ReloadSessionPhase } from "#app/phases/reload-session-phase"; import { RUN_HISTORY_LIMIT } from "#app/ui/run-history-ui-handler"; import { applySessionDataPatches, applySettingsDataPatches, applySystemDataPatches } from "./version-converter"; +import { MysteryEncounterSaveData } from "../data/mystery-encounters/mystery-encounter-save-data"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; export const defaultStarterSpecies: Species[] = [ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE, @@ -130,6 +132,8 @@ export interface SessionSaveData { gameVersion: string; timestamp: integer; challenges: ChallengeData[]; + mysteryEncounterType: MysteryEncounterType | -1; // Only defined when current wave is ME, + mysteryEncounterSaveData: MysteryEncounterSaveData; } interface Unlocks { @@ -947,7 +951,9 @@ export class GameData { trainer: scene.currentBattle.battleType === BattleType.TRAINER ? new TrainerData(scene.currentBattle.trainer) : null, gameVersion: scene.game.config.gameVersion, timestamp: new Date().getTime(), - challenges: scene.gameMode.challenges.map(c => new ChallengeData(c)) + challenges: scene.gameMode.challenges.map(c => new ChallengeData(c)), + mysteryEncounterType: scene.currentBattle.mysteryEncounter?.encounterType ?? -1, + mysteryEncounterSaveData: scene.mysteryEncounterSaveData } as SessionSaveData; } @@ -1038,11 +1044,14 @@ export class GameData { scene.score = sessionData.score; scene.updateScoreText(); + scene.mysteryEncounterSaveData = new MysteryEncounterSaveData(sessionData.mysteryEncounterSaveData); + scene.newArena(sessionData.arena.biome); const battleType = sessionData.battleType || 0; const trainerConfig = sessionData.trainer ? trainerConfigs[sessionData.trainer.trainerType] : null; - const battle = scene.newBattle(sessionData.waveIndex, battleType, sessionData.trainer, battleType === BattleType.TRAINER ? trainerConfig?.doubleOnly || sessionData.trainer?.variant === TrainerVariant.DOUBLE : sessionData.enemyParty.length > 1)!; // TODO: is this bang correct? + const mysteryEncounterType = sessionData.mysteryEncounterType !== -1 ? sessionData.mysteryEncounterType : undefined; + const battle = scene.newBattle(sessionData.waveIndex, battleType, sessionData.trainer, battleType === BattleType.TRAINER ? trainerConfig?.doubleOnly || sessionData.trainer?.variant === TrainerVariant.DOUBLE : sessionData.enemyParty.length > 1, mysteryEncounterType)!; // TODO: is this bang correct? battle.enemyLevels = sessionData.enemyParty.map(p => p.level); scene.arena.init(); @@ -1254,6 +1263,14 @@ export class GameData { return ret; } + if (k === "mysteryEncounterType") { + return v as MysteryEncounterType; + } + + if (k === "mysteryEncounterSaveData") { + return new MysteryEncounterSaveData(v); + } + return v; }) as SessionSaveData; @@ -1538,12 +1555,28 @@ export class GameData { } } - setPokemonCaught(pokemon: Pokemon, incrementCount: boolean = true, fromEgg: boolean = false, showMessage: boolean = true): Promise { + /** + * + * @param pokemon + * @param incrementCount + * @param fromEgg + * @param showMessage + * @returns `true` if Pokemon catch unlocked a new starter, `false` if Pokemon catch did not unlock a starter + */ + setPokemonCaught(pokemon: Pokemon, incrementCount: boolean = true, fromEgg: boolean = false, showMessage: boolean = true): Promise { return this.setPokemonSpeciesCaught(pokemon, pokemon.species, incrementCount, fromEgg, showMessage); } - setPokemonSpeciesCaught(pokemon: Pokemon, species: PokemonSpecies, incrementCount: boolean = true, fromEgg: boolean = false, showMessage: boolean = true): Promise { - return new Promise(resolve => { + /** + * + * @param pokemon + * @param incrementCount + * @param fromEgg + * @param showMessage + * @returns `true` if Pokemon catch unlocked a new starter, `false` if Pokemon catch did not unlock a starter + */ + setPokemonSpeciesCaught(pokemon: Pokemon, species: PokemonSpecies, incrementCount: boolean = true, fromEgg: boolean = false, showMessage: boolean = true): Promise { + return new Promise(resolve => { const dexEntry = this.dexData[species.speciesId]; const caughtAttr = dexEntry.caughtAttr; const formIndex = pokemon.formIndex; @@ -1598,24 +1631,24 @@ export class GameData { } } - const checkPrevolution = () => { + const checkPrevolution = (newStarter: boolean) => { if (hasPrevolution) { const prevolutionSpecies = pokemonPrevolutions[species.speciesId]; - this.setPokemonSpeciesCaught(pokemon, getPokemonSpecies(prevolutionSpecies), incrementCount, fromEgg, showMessage).then(() => resolve()); + this.setPokemonSpeciesCaught(pokemon, getPokemonSpecies(prevolutionSpecies), incrementCount, fromEgg, showMessage).then(result => resolve(result)); } else { - resolve(); + resolve(newStarter); } }; if (newCatch && speciesStarters.hasOwnProperty(species.speciesId)) { if (!showMessage) { - resolve(); + resolve(true); return; } this.scene.playSound("level_up_fanfare"); - this.scene.ui.showText(i18next.t("battle:addedAsAStarter", { pokemonName: species.name }), null, () => checkPrevolution(), null, true); + this.scene.ui.showText(i18next.t("battle:addedAsAStarter", { pokemonName: species.name }), null, () => checkPrevolution(true), null, true); } else { - checkPrevolution(); + checkPrevolution(false); } }); } @@ -1657,7 +1690,14 @@ export class GameData { this.starterData[species.speciesId].candyCount += count; } - setEggMoveUnlocked(species: PokemonSpecies, eggMoveIndex: integer, showMessage: boolean = true): Promise { + /** + * + * @param species + * @param eggMoveIndex + * @param showMessage Default true. If true, will display message for unlocked egg move + * @param prependSpeciesToMessage Default false. If true, will change message from "X Egg Move Unlocked!" to "Bulbasaur X Egg Move Unlocked!" + */ + setEggMoveUnlocked(species: PokemonSpecies, eggMoveIndex: integer, showMessage: boolean = true, prependSpeciesToMessage: boolean = false): Promise { return new Promise(resolve => { const speciesId = species.speciesId; if (!speciesEggMoves.hasOwnProperty(speciesId) || !speciesEggMoves[speciesId][eggMoveIndex]) { @@ -1683,9 +1723,10 @@ export class GameData { } this.scene.playSound("level_up_fanfare"); const moveName = allMoves[speciesEggMoves[speciesId][eggMoveIndex]].name; - this.scene.ui.showText(eggMoveIndex === 3 ? i18next.t("egg:rareEggMoveUnlock", { moveName: moveName }) : i18next.t("egg:eggMoveUnlock", { moveName: moveName }), null, (() => { - resolve(true); - }), null, true); + let message = prependSpeciesToMessage ? species.getName() + " " : ""; + message += eggMoveIndex === 3 ? i18next.t("egg:rareEggMoveUnlock", { moveName: moveName }) : i18next.t("egg:eggMoveUnlock", { moveName: moveName }); + + this.scene.ui.showText(message, null, () => resolve(true), null, true); }); } diff --git a/src/system/pokemon-data.ts b/src/system/pokemon-data.ts index 1fafcbf8acc..8240b6bcf84 100644 --- a/src/system/pokemon-data.ts +++ b/src/system/pokemon-data.ts @@ -12,6 +12,7 @@ import { loadBattlerTag } from "../data/battler-tags"; import { Biome } from "#enums/biome"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data"; export default class PokemonData { public id: integer; @@ -37,12 +38,14 @@ export default class PokemonData { public status: Status | null; public friendship: integer; public metLevel: integer; - public metBiome: Biome | -1; + public metBiome: Biome | -1; // -1 for starters public metSpecies: Species; + public metWave: number; // 0 for unknown (previous saves), -1 for starters public luck: integer; public pauseEvolutions: boolean; public pokerus: boolean; public usedTMs: Moves[]; + public evoCounter: integer; public fusionSpecies: Species; public fusionFormIndex: integer; @@ -51,11 +54,14 @@ export default class PokemonData { public fusionVariant: Variant; public fusionGender: Gender; public fusionLuck: integer; + public fusionMysteryEncounterPokemonData: MysteryEncounterPokemonData; public boss: boolean; public bossSegments?: integer; public summonData: PokemonSummonData; + /** Data that can customize a Pokemon in non-standard ways from its Species */ + public mysteryEncounterPokemonData: MysteryEncounterPokemonData; constructor(source: Pokemon | any, forHistory: boolean = false) { const sourcePokemon = source instanceof Pokemon ? source : null; @@ -86,9 +92,11 @@ export default class PokemonData { this.metLevel = source.metLevel || 5; this.metBiome = source.metBiome !== undefined ? source.metBiome : -1; this.metSpecies = source.metSpecies; + this.metWave = source.metWave ?? (this.metBiome === -1 ? -1 : 0); this.luck = source.luck !== undefined ? source.luck : (source.shiny ? (source.variant + 1) : 0); if (!forHistory) { this.pauseEvolutions = !!source.pauseEvolutions; + this.evoCounter = source.evoCounter ?? 0; } this.pokerus = !!source.pokerus; @@ -101,6 +109,8 @@ export default class PokemonData { this.fusionLuck = source.fusionLuck !== undefined ? source.fusionLuck : (source.fusionShiny ? source.fusionVariant + 1 : 0); this.usedTMs = source.usedTMs ?? []; + this.mysteryEncounterPokemonData = new MysteryEncounterPokemonData(source.mysteryEncounterPokemonData); + if (!forHistory) { this.boss = (source instanceof EnemyPokemon && !!source.bossSegments) || (!this.player && !!source.boss); this.bossSegments = source.bossSegments; diff --git a/src/test/abilities/arena_trap.test.ts b/src/test/abilities/arena_trap.test.ts new file mode 100644 index 00000000000..6b313fcc8db --- /dev/null +++ b/src/test/abilities/arena_trap.test.ts @@ -0,0 +1,59 @@ +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, it, expect } from "vitest"; + +describe("Abilities - Arena Trap", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + const TIMEOUT = 20 * 1000; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .moveset(Moves.SPLASH) + .ability(Abilities.ARENA_TRAP) + .enemySpecies(Species.RALTS) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.TELEPORT); + }); + + // TODO: Enable test when Issue #935 is addressed + it.todo("should not allow grounded Pokémon to flee", async () => { + game.override.battleType("single"); + + await game.classicMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon(); + + game.move.select(Moves.SPLASH); + + await game.toNextTurn(); + + expect(enemy).toBe(game.scene.getEnemyPokemon()); + }, TIMEOUT); + + it("should guarantee double battle with any one LURE", async () => { + game.override + .startingModifier([ + { name: "LURE" }, + ]) + .startingWave(2); + + await game.classicMode.startBattle(); + + expect(game.scene.getEnemyField().length).toBe(2); + }, TIMEOUT); +}); diff --git a/src/test/abilities/battle_bond.test.ts b/src/test/abilities/battle_bond.test.ts index 71e9438db8f..4882001cc8d 100644 --- a/src/test/abilities/battle_bond.test.ts +++ b/src/test/abilities/battle_bond.test.ts @@ -7,7 +7,7 @@ import { Species } from "#enums/species"; import GameManager from "#test/utils/gameManager"; import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest"; -const TIMEOUT = 20 * 1000; + describe("Abilities - BATTLE BOND", () => { let phaserGame: Phaser.Game; @@ -60,6 +60,5 @@ describe("Abilities - BATTLE BOND", () => { expect(greninja!.formIndex).toBe(baseForm); }, - TIMEOUT ); }); diff --git a/src/test/abilities/contrary.test.ts b/src/test/abilities/contrary.test.ts index 95a209395dc..5221e821e70 100644 --- a/src/test/abilities/contrary.test.ts +++ b/src/test/abilities/contrary.test.ts @@ -31,7 +31,7 @@ describe("Abilities - Contrary", () => { }); it("should invert stat changes when applied", async() => { - await game.startBattle([ + await game.classicMode.startBattle([ Species.SLOWBRO ]); @@ -39,4 +39,39 @@ describe("Abilities - Contrary", () => { expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1); }, 20000); + + describe("With Clear Body", () => { + it("should apply positive effects", async () => { + game.override + .enemyPassiveAbility(Abilities.CLEAR_BODY) + .moveset([Moves.TAIL_WHIP]); + await game.classicMode.startBattle([Species.SLOWBRO]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + + expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1); + + game.move.select(Moves.TAIL_WHIP); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(1); + }); + + it("should block negative effects", async () => { + game.override + .enemyPassiveAbility(Abilities.CLEAR_BODY) + .enemyMoveset([Moves.HOWL, Moves.HOWL, Moves.HOWL, Moves.HOWL]) + .moveset([Moves.SPLASH]); + await game.classicMode.startBattle([Species.SLOWBRO]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + + expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1); + + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1); + }); + }); }); diff --git a/src/test/abilities/costar.test.ts b/src/test/abilities/costar.test.ts index 794bed0d3cf..2fd1cb26408 100644 --- a/src/test/abilities/costar.test.ts +++ b/src/test/abilities/costar.test.ts @@ -8,7 +8,7 @@ import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest"; -const TIMEOUT = 20 * 1000; + describe("Abilities - COSTAR", () => { let phaserGame: Phaser.Game; @@ -59,7 +59,6 @@ describe("Abilities - COSTAR", () => { expect(leftPokemon.getStatStage(Stat.SPATK)).toBe(2); expect(rightPokemon.getStatStage(Stat.SPATK)).toBe(2); }, - TIMEOUT, ); test( @@ -83,6 +82,5 @@ describe("Abilities - COSTAR", () => { expect(leftPokemon.getStatStage(Stat.ATK)).toBe(-2); expect(rightPokemon.getStatStage(Stat.ATK)).toBe(-2); }, - TIMEOUT, ); }); diff --git a/src/test/abilities/dancer.test.ts b/src/test/abilities/dancer.test.ts index ec5ce53f4c3..7564a254dbe 100644 --- a/src/test/abilities/dancer.test.ts +++ b/src/test/abilities/dancer.test.ts @@ -7,7 +7,7 @@ import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; -const TIMEOUT = 20 * 1000; + describe("Abilities - Dancer", () => { let phaserGame: Phaser.Game; @@ -60,5 +60,5 @@ describe("Abilities - Dancer", () => { // doesn't use PP if copied move is also in moveset expect(oricorio.moveset[0]?.ppUsed).toBe(0); - }, TIMEOUT); + }); }); diff --git a/src/test/abilities/disguise.test.ts b/src/test/abilities/disguise.test.ts index fa7f26d2716..0268a738c0e 100644 --- a/src/test/abilities/disguise.test.ts +++ b/src/test/abilities/disguise.test.ts @@ -7,7 +7,7 @@ import { Stat } from "#enums/stat"; import GameManager from "#test/utils/gameManager"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; -const TIMEOUT = 20 * 1000; + describe("Abilities - Disguise", () => { let phaserGame: Phaser.Game; @@ -33,7 +33,7 @@ describe("Abilities - Disguise", () => { .enemyMoveset(Moves.SPLASH) .starterSpecies(Species.REGIELEKI) .moveset([Moves.SHADOW_SNEAK, Moves.VACUUM_WAVE, Moves.TOXIC_THREAD, Moves.SPLASH]); - }, TIMEOUT); + }); it("takes no damage from attacking move and transforms to Busted form, takes 1/8 max HP damage from the disguise breaking", async () => { await game.classicMode.startBattle(); @@ -50,7 +50,7 @@ describe("Abilities - Disguise", () => { expect(mimikyu.hp).equals(maxHp - disguiseDamage); expect(mimikyu.formIndex).toBe(bustedForm); - }, TIMEOUT); + }); it("doesn't break disguise when attacked with ineffective move", async () => { await game.classicMode.startBattle(); @@ -64,7 +64,7 @@ describe("Abilities - Disguise", () => { await game.phaseInterceptor.to("MoveEndPhase"); expect(mimikyu.formIndex).toBe(disguisedForm); - }, TIMEOUT); + }); 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([ Moves.SURGING_STRIKES ]); @@ -88,7 +88,7 @@ describe("Abilities - Disguise", () => { await game.phaseInterceptor.to("MoveEffectPhase"); expect(mimikyu.hp).lessThan(maxHp - disguiseDamage); expect(mimikyu.formIndex).toBe(bustedForm); - }, TIMEOUT); + }); it("takes effects from status moves and damage from status effects", async () => { await game.classicMode.startBattle(); @@ -104,7 +104,7 @@ describe("Abilities - Disguise", () => { expect(mimikyu.status?.effect).toBe(StatusEffect.POISON); expect(mimikyu.getStatStage(Stat.SPD)).toBe(-1); expect(mimikyu.hp).toBeLessThan(mimikyu.getMaxHp()); - }, TIMEOUT); + }); it("persists form change when switched out", async () => { game.override.enemyMoveset([Moves.SHADOW_SNEAK]); @@ -129,7 +129,7 @@ describe("Abilities - Disguise", () => { await game.phaseInterceptor.to("TurnEndPhase"); expect(mimikyu.formIndex).toBe(bustedForm); - }, TIMEOUT); + }); it("persists form change when wave changes with no arena reset", async () => { game.override.starterSpecies(0); @@ -146,7 +146,7 @@ describe("Abilities - Disguise", () => { await game.toNextWave(); expect(mimikyu.formIndex).toBe(bustedForm); - }, TIMEOUT); + }); it("reverts to Disguised form on arena reset", async () => { game.override.startingWave(4); @@ -166,7 +166,7 @@ describe("Abilities - Disguise", () => { await game.toNextWave(); expect(mimikyu.formIndex).toBe(disguisedForm); - }, TIMEOUT); + }); it("reverts to Disguised form on biome change when fainted", async () => { game.override.startingWave(10); @@ -190,7 +190,7 @@ describe("Abilities - Disguise", () => { await game.phaseInterceptor.to("PartyHealPhase"); expect(mimikyu1.formIndex).toBe(disguisedForm); - }, TIMEOUT); + }); it("doesn't faint twice when fainting due to Disguise break damage, nor prevent faint from Disguise break damage if using Endure", async () => { game.override.enemyMoveset([Moves.ENDURE]); @@ -204,7 +204,7 @@ describe("Abilities - Disguise", () => { expect(game.scene.getCurrentPhase()?.constructor.name).toBe("CommandPhase"); expect(game.scene.currentBattle.waveIndex).toBe(2); - }, TIMEOUT); + }); it("activates when Aerilate circumvents immunity to the move's base type", async () => { game.override.ability(Abilities.AERILATE); @@ -222,5 +222,5 @@ describe("Abilities - Disguise", () => { expect(mimikyu.formIndex).toBe(bustedForm); expect(mimikyu.hp).toBe(maxHp - disguiseDamage); - }, TIMEOUT); + }); }); diff --git a/src/test/abilities/galvanize.test.ts b/src/test/abilities/galvanize.test.ts index f81b854180a..1b7dde9ba60 100644 --- a/src/test/abilities/galvanize.test.ts +++ b/src/test/abilities/galvanize.test.ts @@ -9,7 +9,7 @@ import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -const TIMEOUT = 20 * 1000; + describe("Abilities - Galvanize", () => { let phaserGame: Phaser.Game; @@ -59,7 +59,7 @@ describe("Abilities - Galvanize", () => { expect(enemyPokemon.apply).toHaveReturnedWith(HitResult.EFFECTIVE); expect(move.calculateBattlePower).toHaveReturnedWith(48); expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp()); - }, TIMEOUT); + }); it("should cause Normal-type attacks to activate Volt Absorb", async () => { game.override.enemyAbility(Abilities.VOLT_ABSORB); @@ -81,7 +81,7 @@ describe("Abilities - Galvanize", () => { expect(playerPokemon.getMoveType).toHaveLastReturnedWith(Type.ELECTRIC); expect(enemyPokemon.apply).toHaveReturnedWith(HitResult.NO_EFFECT); expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); - }, TIMEOUT); + }); it("should not change the type of variable-type moves", async () => { game.override.enemySpecies(Species.MIGHTYENA); @@ -100,7 +100,7 @@ describe("Abilities - Galvanize", () => { expect(playerPokemon.getMoveType).not.toHaveLastReturnedWith(Type.ELECTRIC); expect(enemyPokemon.apply).toHaveReturnedWith(HitResult.NO_EFFECT); expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); - }, TIMEOUT); + }); it("should affect all hits of a Normal-type multi-hit move", async () => { await game.startBattle(); @@ -128,5 +128,5 @@ describe("Abilities - Galvanize", () => { } expect(enemyPokemon.apply).not.toHaveReturnedWith(HitResult.NO_EFFECT); - }, TIMEOUT); + }); }); diff --git a/src/test/abilities/gorilla_tactics.test.ts b/src/test/abilities/gorilla_tactics.test.ts index df698194323..5e92950526e 100644 --- a/src/test/abilities/gorilla_tactics.test.ts +++ b/src/test/abilities/gorilla_tactics.test.ts @@ -10,8 +10,6 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; describe("Abilities - Gorilla Tactics", () => { let phaserGame: Phaser.Game; let game: GameManager; - const TIMEOUT = 20 * 1000; - beforeAll(() => { phaserGame = new Phaser.Game({ type: Phaser.HEADLESS, @@ -49,7 +47,7 @@ describe("Abilities - Gorilla Tactics", () => { // Other moves should be restricted expect(darmanitan.isMoveRestricted(Moves.TACKLE)).toBe(true); expect(darmanitan.isMoveRestricted(Moves.SPLASH)).toBe(false); - }, TIMEOUT); + }); it("should struggle if the only usable move is disabled", async () => { await game.classicMode.startBattle([Species.GALAR_DARMANITAN]); @@ -79,5 +77,5 @@ describe("Abilities - Gorilla Tactics", () => { await game.phaseInterceptor.to("MoveEndPhase"); expect(darmanitan.hp).toBeLessThan(darmanitan.getMaxHp()); - }, TIMEOUT); + }); }); diff --git a/src/test/abilities/illuminate.test.ts b/src/test/abilities/illuminate.test.ts new file mode 100644 index 00000000000..73e4a49be95 --- /dev/null +++ b/src/test/abilities/illuminate.test.ts @@ -0,0 +1,59 @@ +import { Stat } from "#app/enums/stat"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, it, expect } from "vitest"; + +describe("Abilities - Illuminate", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + const TIMEOUT = 20 * 1000; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .moveset(Moves.SPLASH) + .ability(Abilities.ILLUMINATE) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SAND_ATTACK); + }); + + it("should prevent ACC stat stage from being lowered", async () => { + game.override.battleType("single"); + + await game.classicMode.startBattle(); + + const player = game.scene.getPlayerPokemon()!; + + expect(player.getStatStage(Stat.ACC)).toBe(0); + + game.move.select(Moves.SPLASH); + + await game.toNextTurn(); + + expect(player.getStatStage(Stat.ACC)).toBe(0); + }, TIMEOUT); + + it("should guarantee double battle with any one LURE", async () => { + game.override + .startingModifier([ + { name: "LURE" }, + ]) + .startingWave(2); + + await game.classicMode.startBattle(); + + expect(game.scene.getEnemyField().length).toBe(2); + }, TIMEOUT); +}); diff --git a/src/test/abilities/libero.test.ts b/src/test/abilities/libero.test.ts index 51f182d5401..f429d9ffc72 100644 --- a/src/test/abilities/libero.test.ts +++ b/src/test/abilities/libero.test.ts @@ -12,7 +12,7 @@ import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest"; -const TIMEOUT = 20 * 1000; + describe("Abilities - Libero", () => { let phaserGame: Phaser.Game; @@ -52,7 +52,6 @@ describe("Abilities - Libero", () => { testPokemonTypeMatchesDefaultMoveType(leadPokemon, Moves.SPLASH); }, - TIMEOUT, ); test.skip( @@ -92,7 +91,6 @@ describe("Abilities - Libero", () => { testPokemonTypeMatchesDefaultMoveType(leadPokemon, Moves.SPLASH); }, - TIMEOUT, ); test( @@ -115,7 +113,6 @@ describe("Abilities - Libero", () => { moveType = Type[Type.FIRE]; expect(leadPokemonType).toBe(moveType); }, - TIMEOUT, ); test( @@ -138,7 +135,6 @@ describe("Abilities - Libero", () => { moveType = Type[Type.ICE]; expect(leadPokemonType).toBe(moveType); }, - TIMEOUT, ); test( @@ -157,7 +153,6 @@ describe("Abilities - Libero", () => { testPokemonTypeMatchesDefaultMoveType(leadPokemon, Moves.AIR_SLASH); }, - TIMEOUT, ); test( @@ -175,7 +170,6 @@ describe("Abilities - Libero", () => { testPokemonTypeMatchesDefaultMoveType(leadPokemon, Moves.DIG); }, - TIMEOUT, ); test( @@ -197,7 +191,6 @@ describe("Abilities - Libero", () => { expect(enemyPokemon.isFullHp()).toBe(true); testPokemonTypeMatchesDefaultMoveType(leadPokemon, Moves.TACKLE); }, - TIMEOUT, ); test( @@ -216,7 +209,6 @@ describe("Abilities - Libero", () => { testPokemonTypeMatchesDefaultMoveType(leadPokemon, Moves.TACKLE); }, - TIMEOUT, ); test( @@ -235,7 +227,6 @@ describe("Abilities - Libero", () => { testPokemonTypeMatchesDefaultMoveType(leadPokemon, Moves.TACKLE); }, - TIMEOUT, ); test( @@ -254,7 +245,6 @@ describe("Abilities - Libero", () => { expect(leadPokemon.summonData.abilitiesApplied).not.toContain(Abilities.LIBERO); }, - TIMEOUT, ); test( @@ -274,7 +264,6 @@ describe("Abilities - Libero", () => { expect(leadPokemon.summonData.abilitiesApplied).not.toContain(Abilities.LIBERO); }, - TIMEOUT, ); test( @@ -292,7 +281,6 @@ describe("Abilities - Libero", () => { expect(leadPokemon.summonData.abilitiesApplied).not.toContain(Abilities.LIBERO); }, - TIMEOUT, ); test( @@ -310,7 +298,6 @@ describe("Abilities - Libero", () => { expect(leadPokemon.summonData.abilitiesApplied).not.toContain(Abilities.LIBERO); }, - TIMEOUT, ); test( @@ -329,7 +316,6 @@ describe("Abilities - Libero", () => { testPokemonTypeMatchesDefaultMoveType(leadPokemon, Moves.TRICK_OR_TREAT); }, - TIMEOUT, ); test( @@ -348,7 +334,6 @@ describe("Abilities - Libero", () => { testPokemonTypeMatchesDefaultMoveType(leadPokemon, Moves.CURSE); expect(leadPokemon.getTag(BattlerTagType.CURSED)).not.toBe(undefined); }, - TIMEOUT, ); }); diff --git a/src/test/abilities/magic_guard.test.ts b/src/test/abilities/magic_guard.test.ts index 4b3fb0ba985..dd8b83f7601 100644 --- a/src/test/abilities/magic_guard.test.ts +++ b/src/test/abilities/magic_guard.test.ts @@ -11,8 +11,6 @@ import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; -const TIMEOUT = 20 * 1000; // 20 sec timeout - describe("Abilities - Magic Guard", () => { let phaserGame: Phaser.Game; let game: GameManager; @@ -67,7 +65,7 @@ describe("Abilities - Magic Guard", () => { */ expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp()); expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp()); - }, TIMEOUT + } ); it( @@ -91,7 +89,7 @@ describe("Abilities - Magic Guard", () => { */ expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp()); expect(getStatusEffectCatchRateMultiplier(leadPokemon.status!.effect)).toBe(1.5); - }, TIMEOUT + } ); it( @@ -113,7 +111,7 @@ describe("Abilities - Magic Guard", () => { * - The player Pokemon (that just lost its Magic Guard ability) has taken damage from poison */ expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp()); - }, TIMEOUT + } ); @@ -138,7 +136,7 @@ describe("Abilities - Magic Guard", () => { */ expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); expect(getStatusEffectCatchRateMultiplier(enemyPokemon.status!.effect)).toBe(1.5); - }, TIMEOUT + } ); it("Magic Guard prevents damage caused by toxic but other non-damaging effects are still applied", @@ -166,7 +164,7 @@ describe("Abilities - Magic Guard", () => { expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); expect(enemyPokemon.status!.turnCount).toBeGreaterThan(toxicStartCounter); expect(getStatusEffectCatchRateMultiplier(enemyPokemon.status!.effect)).toBe(1.5); - }, TIMEOUT + } ); @@ -191,7 +189,7 @@ describe("Abilities - Magic Guard", () => { */ expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp()); expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp()); - }, TIMEOUT + } ); it("Magic Guard does not prevent poison from Toxic Spikes", async () => { @@ -220,7 +218,7 @@ describe("Abilities - Magic Guard", () => { expect(enemyPokemon.status!.effect).toBe(StatusEffect.POISON); expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp()); expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp()); - }, TIMEOUT + } ); it("Magic Guard prevents against damage from volatile status effects", @@ -246,7 +244,7 @@ describe("Abilities - Magic Guard", () => { expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp()); expect(enemyPokemon.getTag(BattlerTagType.CURSED)).not.toBe(undefined); expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); - }, TIMEOUT + } ); it("Magic Guard prevents crash damage", async () => { @@ -265,7 +263,7 @@ describe("Abilities - Magic Guard", () => { * - The player Pokemon (with Magic Guard) misses High Jump Kick but does not lose HP as a result */ expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp()); - }, TIMEOUT + } ); it("Magic Guard prevents damage from recoil", async () => { @@ -283,7 +281,7 @@ describe("Abilities - Magic Guard", () => { * - The player Pokemon (with Magic Guard) uses a recoil move but does not lose HP from recoil */ expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp()); - }, TIMEOUT + } ); it("Magic Guard does not prevent damage from Struggle's recoil", async () => { @@ -301,7 +299,7 @@ describe("Abilities - Magic Guard", () => { * - The player Pokemon (with Magic Guard) uses Struggle but does lose HP from Struggle's recoil */ expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp()); - }, TIMEOUT + } ); //This tests different move attributes than the recoil tests above @@ -320,7 +318,7 @@ describe("Abilities - Magic Guard", () => { * - The player Pokemon (with Magic Guard) uses a move with an HP cost but does not lose HP from using it */ expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp()); - }, TIMEOUT + } ); /* @@ -348,7 +346,7 @@ describe("Abilities - Magic Guard", () => { * - The player Pokemon (with Magic Guard) uses a non-attacking move with an HP cost and thus loses HP from using it */ expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp()); - }, TIMEOUT + } ); it("Magic Guard prevents damage from abilities with PostTurnHurtIfSleepingAbAttr", async () => { @@ -373,7 +371,7 @@ describe("Abilities - Magic Guard", () => { */ expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp()); expect(leadPokemon.status!.effect).toBe(StatusEffect.SLEEP); - }, TIMEOUT + } ); it("Magic Guard prevents damage from abilities with PostFaintContactDamageAbAttr", async () => { @@ -398,7 +396,7 @@ describe("Abilities - Magic Guard", () => { */ expect(enemyPokemon.hp).toBe(0); expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp()); - }, TIMEOUT + } ); it("Magic Guard prevents damage from abilities with PostDefendContactDamageAbAttr", async () => { @@ -422,7 +420,7 @@ describe("Abilities - Magic Guard", () => { */ expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp()); expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp()); - }, TIMEOUT + } ); it("Magic Guard prevents damage from abilities with ReverseDrainAbAttr", async () => { @@ -446,7 +444,7 @@ describe("Abilities - Magic Guard", () => { */ expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp()); expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp()); - }, TIMEOUT + } ); it("Magic Guard prevents HP loss from abilities with PostWeatherLapseDamageAbAttr", async () => { @@ -464,6 +462,6 @@ describe("Abilities - Magic Guard", () => { * - The player Pokemon (with Magic Guard) should not lose HP due to this ability attribute */ expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp()); - }, TIMEOUT + } ); }); diff --git a/src/test/abilities/no_guard.test.ts b/src/test/abilities/no_guard.test.ts new file mode 100644 index 00000000000..b793ede7099 --- /dev/null +++ b/src/test/abilities/no_guard.test.ts @@ -0,0 +1,68 @@ +import { BattlerIndex } from "#app/battle"; +import { MoveEffectPhase } from "#app/phases/move-effect-phase"; +import { MoveEndPhase } from "#app/phases/move-end-phase"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, it, expect, vi } from "vitest"; + +describe("Abilities - No Guard", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + const TIMEOUT = 20 * 1000; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .moveset(Moves.ZAP_CANNON) + .ability(Abilities.NO_GUARD) + .enemyLevel(200) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("should make moves always hit regardless of move accuracy", async () => { + game.override.battleType("single"); + + await game.classicMode.startBattle([ + Species.REGIELEKI + ]); + + game.move.select(Moves.ZAP_CANNON); + + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + + await game.phaseInterceptor.to(MoveEffectPhase, false); + + const moveEffectPhase = game.scene.getCurrentPhase() as MoveEffectPhase; + vi.spyOn(moveEffectPhase, "hitCheck"); + + await game.phaseInterceptor.to(MoveEndPhase); + + expect(moveEffectPhase.hitCheck).toHaveReturnedWith(true); + }, TIMEOUT); + + it("should guarantee double battle with any one LURE", async () => { + game.override + .startingModifier([ + { name: "LURE" }, + ]) + .startingWave(2); + + await game.classicMode.startBattle(); + + expect(game.scene.getEnemyField().length).toBe(2); + }, TIMEOUT); +}); diff --git a/src/test/abilities/parental_bond.test.ts b/src/test/abilities/parental_bond.test.ts index 2ad3f9e3f5c..22c9d8028be 100644 --- a/src/test/abilities/parental_bond.test.ts +++ b/src/test/abilities/parental_bond.test.ts @@ -10,7 +10,7 @@ import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; -const TIMEOUT = 20 * 1000; + describe("Abilities - Parental Bond", () => { let phaserGame: Phaser.Game; @@ -62,7 +62,7 @@ describe("Abilities - Parental Bond", () => { expect(leadPokemon.turnData.hitCount).toBe(2); expect(secondStrikeDamage).toBe(toDmgValue(0.25 * firstStrikeDamage)); - }, TIMEOUT + } ); it( @@ -81,7 +81,7 @@ describe("Abilities - Parental Bond", () => { expect(leadPokemon.turnData.hitCount).toBe(2); expect(leadPokemon.getStatStage(Stat.ATK)).toBe(2); - }, TIMEOUT + } ); it( @@ -98,7 +98,7 @@ describe("Abilities - Parental Bond", () => { await game.phaseInterceptor.to("BerryPhase", false); expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-1); - }, TIMEOUT + } ); it( @@ -116,7 +116,7 @@ describe("Abilities - Parental Bond", () => { await game.phaseInterceptor.to("BerryPhase", false); expect(leadPokemon.turnData.hitCount).toBe(2); - }, TIMEOUT + } ); it( @@ -133,7 +133,7 @@ describe("Abilities - Parental Bond", () => { await game.phaseInterceptor.to("DamagePhase", false); expect(leadPokemon.turnData.hitCount).toBe(1); - }, TIMEOUT + } ); it( @@ -151,7 +151,7 @@ describe("Abilities - Parental Bond", () => { await game.phaseInterceptor.to("DamagePhase", false); expect(leadPokemon.turnData.hitCount).toBe(1); - }, TIMEOUT + } ); it( @@ -167,7 +167,7 @@ describe("Abilities - Parental Bond", () => { await game.phaseInterceptor.to("BerryPhase", false); expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp() - 80); - }, TIMEOUT + } ); it( @@ -189,7 +189,7 @@ describe("Abilities - Parental Bond", () => { await game.phaseInterceptor.to("BerryPhase", false); expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp() - 4 * playerDamage); - }, TIMEOUT + } ); it( @@ -209,7 +209,7 @@ describe("Abilities - Parental Bond", () => { await game.phaseInterceptor.to("BerryPhase", false); playerPokemon.forEach(p => expect(p.turnData.hitCount).toBe(1)); - }, TIMEOUT + } ); it( @@ -225,7 +225,7 @@ describe("Abilities - Parental Bond", () => { await game.phaseInterceptor.to("DamagePhase", false); expect(leadPokemon.turnData.hitCount).toBe(2); - }, TIMEOUT + } ); it( @@ -247,7 +247,7 @@ describe("Abilities - Parental Bond", () => { await game.phaseInterceptor.to("BerryPhase", false); expect(leadPokemon.hp).toBe(Math.ceil(leadPokemon.getMaxHp() / 2)); - }, TIMEOUT + } ); it( @@ -271,7 +271,7 @@ describe("Abilities - Parental Bond", () => { await game.phaseInterceptor.to("BerryPhase", false); expect(leadPokemon.isOfType(Type.FIRE)).toBe(false); - }, TIMEOUT + } ); it( @@ -289,7 +289,7 @@ describe("Abilities - Parental Bond", () => { await game.phaseInterceptor.to("DamagePhase"); expect(leadPokemon.turnData.hitCount).toBe(4); - }, TIMEOUT + } ); it( @@ -313,7 +313,7 @@ describe("Abilities - Parental Bond", () => { await game.phaseInterceptor.to("MoveEndPhase", false); expect(enemyPokemon.hp).toBe(Math.ceil(enemyPokemon.getMaxHp() * 0.25)); - }, TIMEOUT + } ); it( @@ -339,7 +339,7 @@ describe("Abilities - Parental Bond", () => { await game.phaseInterceptor.to("MoveEndPhase", false); expect(enemyPokemon.hp).toBe(enemyStartingHp - 200); - }, TIMEOUT + } ); it( @@ -362,7 +362,7 @@ describe("Abilities - Parental Bond", () => { await game.phaseInterceptor.to("TurnEndPhase"); expect(leadPokemon.getTag(BattlerTagType.RECHARGING)).toBeDefined(); - }, TIMEOUT + } ); it( @@ -389,7 +389,7 @@ describe("Abilities - Parental Bond", () => { await game.phaseInterceptor.to("TurnEndPhase"); expect(enemyPokemon.getTag(BattlerTagType.TRAPPED)).toBeDefined(); - }, TIMEOUT + } ); it( @@ -413,7 +413,7 @@ describe("Abilities - Parental Bond", () => { await game.phaseInterceptor.to("TurnEndPhase"); expect(enemyPokemon.getTag(BattlerTagType.IGNORE_FLYING)).toBeDefined(); - }, TIMEOUT + } ); it( @@ -433,7 +433,7 @@ describe("Abilities - Parental Bond", () => { // This will cause this test to time out if the switch was forced on the first hit. await game.phaseInterceptor.to("MoveEffectPhase", false); - }, TIMEOUT + } ); it( @@ -457,7 +457,7 @@ describe("Abilities - Parental Bond", () => { await game.phaseInterceptor.to("BerryPhase", false); expect(enemyPokemon.status?.effect).toBeUndefined(); - }, TIMEOUT + } ); it( @@ -475,7 +475,7 @@ describe("Abilities - Parental Bond", () => { await game.phaseInterceptor.to("BerryPhase", false); expect(leadPokemon.getStatStage(Stat.ATK)).toBe(-1); - }, TIMEOUT + } ); it( @@ -493,7 +493,7 @@ describe("Abilities - Parental Bond", () => { await game.phaseInterceptor.to("BerryPhase", false); expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(1); - }, TIMEOUT + } ); it( @@ -519,6 +519,6 @@ describe("Abilities - Parental Bond", () => { await game.phaseInterceptor.to("BerryPhase", false); enemyPokemon.forEach((p, i) => expect(enemyStartingHp[i] - p.hp).toBe(2 * enemyFirstHitDamage[i])); - }, TIMEOUT + } ); }); diff --git a/src/test/abilities/power_construct.test.ts b/src/test/abilities/power_construct.test.ts index ec37bc96c2f..94cee82fb4a 100644 --- a/src/test/abilities/power_construct.test.ts +++ b/src/test/abilities/power_construct.test.ts @@ -7,7 +7,7 @@ import { Species } from "#enums/species"; import GameManager from "#test/utils/gameManager"; import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest"; -const TIMEOUT = 20 * 1000; + describe("Abilities - POWER CONSTRUCT", () => { let phaserGame: Phaser.Game; @@ -60,6 +60,5 @@ describe("Abilities - POWER CONSTRUCT", () => { expect(zygarde!.formIndex).toBe(baseForm); }, - TIMEOUT ); }); diff --git a/src/test/abilities/protean.test.ts b/src/test/abilities/protean.test.ts index 4be58a677a6..8479a293722 100644 --- a/src/test/abilities/protean.test.ts +++ b/src/test/abilities/protean.test.ts @@ -12,7 +12,7 @@ import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest"; -const TIMEOUT = 20 * 1000; + describe("Abilities - Protean", () => { let phaserGame: Phaser.Game; @@ -52,7 +52,6 @@ describe("Abilities - Protean", () => { testPokemonTypeMatchesDefaultMoveType(leadPokemon, Moves.SPLASH); }, - TIMEOUT, ); test.skip( @@ -92,7 +91,6 @@ describe("Abilities - Protean", () => { testPokemonTypeMatchesDefaultMoveType(leadPokemon, Moves.SPLASH); }, - TIMEOUT, ); test( @@ -115,7 +113,6 @@ describe("Abilities - Protean", () => { moveType = Type[Type.FIRE]; expect(leadPokemonType).toBe(moveType); }, - TIMEOUT, ); test( @@ -138,7 +135,6 @@ describe("Abilities - Protean", () => { moveType = Type[Type.ICE]; expect(leadPokemonType).toBe(moveType); }, - TIMEOUT, ); test( @@ -157,7 +153,6 @@ describe("Abilities - Protean", () => { testPokemonTypeMatchesDefaultMoveType(leadPokemon, Moves.AIR_SLASH); }, - TIMEOUT, ); test( @@ -175,7 +170,6 @@ describe("Abilities - Protean", () => { testPokemonTypeMatchesDefaultMoveType(leadPokemon, Moves.DIG); }, - TIMEOUT, ); test( @@ -197,7 +191,6 @@ describe("Abilities - Protean", () => { expect(enemyPokemon.isFullHp()).toBe(true); testPokemonTypeMatchesDefaultMoveType(leadPokemon, Moves.TACKLE); }, - TIMEOUT, ); test( @@ -216,7 +209,6 @@ describe("Abilities - Protean", () => { testPokemonTypeMatchesDefaultMoveType(leadPokemon, Moves.TACKLE); }, - TIMEOUT, ); test( @@ -235,7 +227,6 @@ describe("Abilities - Protean", () => { testPokemonTypeMatchesDefaultMoveType(leadPokemon, Moves.TACKLE); }, - TIMEOUT, ); test( @@ -254,7 +245,6 @@ describe("Abilities - Protean", () => { expect(leadPokemon.summonData.abilitiesApplied).not.toContain(Abilities.PROTEAN); }, - TIMEOUT, ); test( @@ -274,7 +264,6 @@ describe("Abilities - Protean", () => { expect(leadPokemon.summonData.abilitiesApplied).not.toContain(Abilities.PROTEAN); }, - TIMEOUT, ); test( @@ -292,7 +281,6 @@ describe("Abilities - Protean", () => { expect(leadPokemon.summonData.abilitiesApplied).not.toContain(Abilities.PROTEAN); }, - TIMEOUT, ); test( @@ -310,7 +298,6 @@ describe("Abilities - Protean", () => { expect(leadPokemon.summonData.abilitiesApplied).not.toContain(Abilities.PROTEAN); }, - TIMEOUT, ); test( @@ -329,7 +316,6 @@ describe("Abilities - Protean", () => { testPokemonTypeMatchesDefaultMoveType(leadPokemon, Moves.TRICK_OR_TREAT); }, - TIMEOUT, ); test( @@ -348,7 +334,6 @@ describe("Abilities - Protean", () => { testPokemonTypeMatchesDefaultMoveType(leadPokemon, Moves.CURSE); expect(leadPokemon.getTag(BattlerTagType.CURSED)).not.toBe(undefined); }, - TIMEOUT, ); }); diff --git a/src/test/abilities/quick_draw.test.ts b/src/test/abilities/quick_draw.test.ts index a02ee5cf56a..dbed6c822c4 100644 --- a/src/test/abilities/quick_draw.test.ts +++ b/src/test/abilities/quick_draw.test.ts @@ -55,7 +55,6 @@ describe("Abilities - Quick Draw", () => { }, 20000); test("does not triggered by non damage moves", { - timeout: 20000, retry: 5 }, async () => { await game.startBattle(); diff --git a/src/test/abilities/sand_veil.test.ts b/src/test/abilities/sand_veil.test.ts index da9fdcc01ab..201d8d89600 100644 --- a/src/test/abilities/sand_veil.test.ts +++ b/src/test/abilities/sand_veil.test.ts @@ -11,7 +11,7 @@ import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest"; -const TIMEOUT = 20 * 1000; + describe("Abilities - Sand Veil", () => { let phaserGame: Phaser.Game; @@ -75,6 +75,6 @@ describe("Abilities - Sand Veil", () => { expect(leadPokemon[0].isFullHp()).toBe(true); expect(leadPokemon[1].hp).toBeLessThan(leadPokemon[1].getMaxHp()); - }, TIMEOUT + } ); }); diff --git a/src/test/abilities/schooling.test.ts b/src/test/abilities/schooling.test.ts index ad9663bf8e5..4c5a66a41b7 100644 --- a/src/test/abilities/schooling.test.ts +++ b/src/test/abilities/schooling.test.ts @@ -7,7 +7,7 @@ import { Species } from "#enums/species"; import GameManager from "#test/utils/gameManager"; import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest"; -const TIMEOUT = 20 * 1000; + describe("Abilities - SCHOOLING", () => { let phaserGame: Phaser.Game; @@ -60,6 +60,5 @@ describe("Abilities - SCHOOLING", () => { expect(wishiwashi.formIndex).toBe(soloForm); }, - TIMEOUT ); }); diff --git a/src/test/abilities/shields_down.test.ts b/src/test/abilities/shields_down.test.ts index 9bfec23ddf1..411c23fc652 100644 --- a/src/test/abilities/shields_down.test.ts +++ b/src/test/abilities/shields_down.test.ts @@ -7,7 +7,7 @@ import { Species } from "#enums/species"; import GameManager from "#test/utils/gameManager"; import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest"; -const TIMEOUT = 20 * 1000; + describe("Abilities - SHIELDS DOWN", () => { let phaserGame: Phaser.Game; @@ -60,6 +60,5 @@ describe("Abilities - SHIELDS DOWN", () => { expect(minior.formIndex).toBe(meteorForm); }, - TIMEOUT ); }); diff --git a/src/test/abilities/sturdy.test.ts b/src/test/abilities/sturdy.test.ts index dc9f774cc5b..c329d0830d3 100644 --- a/src/test/abilities/sturdy.test.ts +++ b/src/test/abilities/sturdy.test.ts @@ -8,7 +8,7 @@ import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest"; -const TIMEOUT = 20 * 1000; + describe("Abilities - Sturdy", () => { let phaserGame: Phaser.Game; @@ -45,7 +45,6 @@ describe("Abilities - Sturdy", () => { await game.phaseInterceptor.to(MoveEndPhase); expect(game.scene.getEnemyParty()[0].hp).toBe(1); }, - TIMEOUT ); test( @@ -62,7 +61,6 @@ describe("Abilities - Sturdy", () => { expect(enemyPokemon.hp).toBe(0); expect(enemyPokemon.isFainted()).toBe(true); }, - TIMEOUT ); test( @@ -75,7 +73,6 @@ describe("Abilities - Sturdy", () => { const enemyPokemon: EnemyPokemon = game.scene.getEnemyParty()[0]; expect(enemyPokemon.isFullHp()).toBe(true); }, - TIMEOUT ); test( @@ -91,7 +88,6 @@ describe("Abilities - Sturdy", () => { expect(enemyPokemon.hp).toBe(0); expect(enemyPokemon.isFainted()).toBe(true); }, - TIMEOUT ); }); diff --git a/src/test/abilities/tera_shell.test.ts b/src/test/abilities/tera_shell.test.ts index 2826469f3bf..13df49136ca 100644 --- a/src/test/abilities/tera_shell.test.ts +++ b/src/test/abilities/tera_shell.test.ts @@ -6,8 +6,6 @@ import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -const TIMEOUT = 10 * 1000; // 10 second timeout - describe("Abilities - Tera Shell", () => { let phaserGame: Phaser.Game; let game: GameManager; @@ -54,7 +52,7 @@ describe("Abilities - Tera Shell", () => { await game.phaseInterceptor.to("MoveEndPhase"); expect(playerPokemon.getMoveEffectiveness).toHaveLastReturnedWith(2); - }, TIMEOUT + } ); it( @@ -71,7 +69,7 @@ describe("Abilities - Tera Shell", () => { await game.phaseInterceptor.to("MoveEndPhase"); expect(playerPokemon.getMoveEffectiveness).toHaveLastReturnedWith(0); - }, TIMEOUT + } ); it( @@ -88,7 +86,7 @@ describe("Abilities - Tera Shell", () => { await game.phaseInterceptor.to("MoveEndPhase"); expect(playerPokemon.getMoveEffectiveness).toHaveLastReturnedWith(0.25); - }, TIMEOUT + } ); it( @@ -106,6 +104,6 @@ describe("Abilities - Tera Shell", () => { await game.phaseInterceptor.to("BerryPhase", false); expect(playerPokemon.apply).toHaveLastReturnedWith(HitResult.EFFECTIVE); expect(playerPokemon.hp).toBe(playerPokemon.getMaxHp() - 40); - }, TIMEOUT + } ); }); diff --git a/src/test/abilities/unseen_fist.test.ts b/src/test/abilities/unseen_fist.test.ts index ea1996ec66b..0f285abd98f 100644 --- a/src/test/abilities/unseen_fist.test.ts +++ b/src/test/abilities/unseen_fist.test.ts @@ -4,9 +4,11 @@ import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { BattlerTagType } from "#app/enums/battler-tag-type"; +import { BerryPhase } from "#app/phases/berry-phase"; + -const TIMEOUT = 20 * 1000; describe("Abilities - Unseen Fist", () => { let phaserGame: Phaser.Game; @@ -32,36 +34,52 @@ describe("Abilities - Unseen Fist", () => { game.override.enemyLevel(100); }); - test( - "ability causes a contact move to ignore Protect", + it( + "should cause a contact move to ignore Protect", () => testUnseenFistHitResult(game, Moves.QUICK_ATTACK, Moves.PROTECT, true), - TIMEOUT ); - test( - "ability does not cause a non-contact move to ignore Protect", + it( + "should not cause a non-contact move to ignore Protect", () => testUnseenFistHitResult(game, Moves.ABSORB, Moves.PROTECT, false), - TIMEOUT ); - test( - "ability does not apply if the source has Long Reach", + it( + "should not apply if the source has Long Reach", () => { game.override.passiveAbility(Abilities.LONG_REACH); testUnseenFistHitResult(game, Moves.QUICK_ATTACK, Moves.PROTECT, false); - }, TIMEOUT + } ); - test( - "ability causes a contact move to ignore Wide Guard", + it( + "should cause a contact move to ignore Wide Guard", () => testUnseenFistHitResult(game, Moves.BREAKING_SWIPE, Moves.WIDE_GUARD, true), - TIMEOUT ); - test( - "ability does not cause a non-contact move to ignore Wide Guard", + it( + "should not cause a non-contact move to ignore Wide Guard", () => testUnseenFistHitResult(game, Moves.BULLDOZE, Moves.WIDE_GUARD, false), - TIMEOUT + ); + + it( + "should cause a contact move to ignore Protect, but not Substitute", + async () => { + game.override.enemyLevel(1); + game.override.moveset([Moves.TACKLE]); + + await game.startBattle(); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + enemyPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, enemyPokemon.id); + + game.move.select(Moves.TACKLE); + + await game.phaseInterceptor.to(BerryPhase, false); + + expect(enemyPokemon.getTag(BattlerTagType.SUBSTITUTE)).toBeUndefined(); + expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); + } ); }); diff --git a/src/test/abilities/zen_mode.test.ts b/src/test/abilities/zen_mode.test.ts index fd378647184..c7cbd9014e0 100644 --- a/src/test/abilities/zen_mode.test.ts +++ b/src/test/abilities/zen_mode.test.ts @@ -19,7 +19,7 @@ import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest"; import { Status, StatusEffect } from "#app/data/status-effect"; -const TIMEOUT = 20 * 1000; + describe("Abilities - ZEN MODE", () => { let phaserGame: Phaser.Game; @@ -67,7 +67,6 @@ describe("Abilities - ZEN MODE", () => { expect(game.scene.getParty()[0].hp).toBeLessThan(100); expect(game.scene.getParty()[0].formIndex).toBe(0); }, - TIMEOUT ); test( @@ -87,7 +86,6 @@ describe("Abilities - ZEN MODE", () => { expect(game.scene.getParty()[0].hp).not.toBe(100); expect(game.scene.getParty()[0].formIndex).not.toBe(0); }, - TIMEOUT ); test( @@ -125,7 +123,6 @@ describe("Abilities - ZEN MODE", () => { await game.phaseInterceptor.to(PostSummonPhase); expect(game.scene.getParty()[1].formIndex).toBe(1); }, - TIMEOUT ); test( @@ -156,6 +153,5 @@ describe("Abilities - ZEN MODE", () => { expect(darmanitan.formIndex).toBe(baseForm); }, - TIMEOUT ); }); diff --git a/src/test/abilities/zero_to_hero.test.ts b/src/test/abilities/zero_to_hero.test.ts index eafc32b4c79..a7f7c970218 100644 --- a/src/test/abilities/zero_to_hero.test.ts +++ b/src/test/abilities/zero_to_hero.test.ts @@ -7,7 +7,7 @@ import { Species } from "#enums/species"; import GameManager from "#test/utils/gameManager"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; -const TIMEOUT = 20 * 1000; + describe("Abilities - ZERO TO HERO", () => { let phaserGame: Phaser.Game; @@ -59,7 +59,7 @@ describe("Abilities - ZERO TO HERO", () => { expect(palafin1.formIndex).toBe(baseForm); expect(palafin2.formIndex).toBe(baseForm); - }, TIMEOUT); + }); it("should swap to Hero form when switching out during a battle", async () => { await game.startBattle([Species.PALAFIN, Species.FEEBAS]); @@ -70,7 +70,7 @@ describe("Abilities - ZERO TO HERO", () => { game.doSwitchPokemon(1); await game.phaseInterceptor.to(QuietFormChangePhase); expect(palafin.formIndex).toBe(heroForm); - }, TIMEOUT); + }); it("should not swap to Hero form if switching due to faint", async () => { await game.startBattle([Species.PALAFIN, Species.FEEBAS]); @@ -83,7 +83,7 @@ describe("Abilities - ZERO TO HERO", () => { game.doSelectPartyPokemon(1); await game.toNextTurn(); expect(palafin.formIndex).toBe(baseForm); - }, TIMEOUT); + }); it("should stay hero form if fainted and then revived", async () => { game.override.starterForms({ @@ -105,5 +105,5 @@ describe("Abilities - ZERO TO HERO", () => { await game.toNextTurn(); expect(palafin.formIndex).toBe(heroForm); - }, TIMEOUT); + }); }); diff --git a/src/test/arena/grassy_terrain.test.ts b/src/test/arena/grassy_terrain.test.ts new file mode 100644 index 00000000000..01bbc778ded --- /dev/null +++ b/src/test/arena/grassy_terrain.test.ts @@ -0,0 +1,69 @@ +import { allMoves } from "#app/data/move"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +describe("Arena - Grassy Terrain", () => { + 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 + .battleType("single") + .disableCrits() + .enemyLevel(1) + .enemySpecies(Species.SHUCKLE) + .enemyAbility(Abilities.STURDY) + .enemyMoveset(Moves.FLY) + .moveset([Moves.GRASSY_TERRAIN, Moves.EARTHQUAKE]) + .ability(Abilities.NO_GUARD); + }); + + it("halves the damage of Earthquake", async () => { + await game.classicMode.startBattle([Species.TAUROS]); + + const eq = allMoves[Moves.EARTHQUAKE]; + vi.spyOn(eq, "calculateBattlePower"); + + game.move.select(Moves.EARTHQUAKE); + await game.toNextTurn(); + + expect(eq.calculateBattlePower).toHaveReturnedWith(100); + + game.move.select(Moves.GRASSY_TERRAIN); + await game.toNextTurn(); + + game.move.select(Moves.EARTHQUAKE); + await game.phaseInterceptor.to("BerryPhase"); + + expect(eq.calculateBattlePower).toHaveReturnedWith(50); + }); + + it("Does not halve the damage of Earthquake if opponent is not grounded", async () => { + await game.classicMode.startBattle([Species.NINJASK]); + + const eq = allMoves[Moves.EARTHQUAKE]; + vi.spyOn(eq, "calculateBattlePower"); + + game.move.select(Moves.GRASSY_TERRAIN); + await game.toNextTurn(); + + game.move.select(Moves.EARTHQUAKE); + await game.phaseInterceptor.to("BerryPhase"); + + expect(eq.calculateBattlePower).toHaveReturnedWith(100); + }); +}); diff --git a/src/test/battle/battle.test.ts b/src/test/battle/battle.test.ts index 6e15bbd99d9..554692374d2 100644 --- a/src/test/battle/battle.test.ts +++ b/src/test/battle/battle.test.ts @@ -24,7 +24,8 @@ import { Moves } from "#enums/moves"; import { PlayerGender } from "#enums/player-gender"; import { Species } from "#enums/species"; import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { Biome } from "#app/enums/biome"; describe("Test Battle Phase", () => { let phaserGame: Phaser.Game; @@ -290,22 +291,27 @@ describe("Test Battle Phase", () => { expect(game.scene.currentBattle.turn).toBeGreaterThan(turn); }, 20000); - it("to next wave with pokemon killed, single", async () => { + it("does not set new weather if staying in same biome", async () => { const moveToUse = Moves.SPLASH; - game.override.battleType("single"); - game.override.starterSpecies(Species.MEWTWO); - game.override.enemySpecies(Species.RATTATA); - game.override.enemyAbility(Abilities.HYDRATION); - game.override.ability(Abilities.ZEN_MODE); - game.override.startingLevel(2000); - game.override.startingWave(3); - game.override.moveset([moveToUse]); + game.override + .battleType("single") + .starterSpecies(Species.MEWTWO) + .enemySpecies(Species.RATTATA) + .enemyAbility(Abilities.HYDRATION) + .ability(Abilities.ZEN_MODE) + .startingLevel(2000) + .startingWave(3) + .startingBiome(Biome.LAKE) + .moveset([moveToUse]); game.override.enemyMoveset([Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE]); - await game.startBattle(); + await game.classicMode.startBattle(); const waveIndex = game.scene.currentBattle.waveIndex; game.move.select(moveToUse); + + vi.spyOn(game.scene.arena, "trySetWeather"); await game.doKillOpponents(); await game.toNextWave(); + expect(game.scene.arena.trySetWeather).not.toHaveBeenCalled(); expect(game.scene.currentBattle.waveIndex).toBeGreaterThan(waveIndex); }, 20000); diff --git a/src/test/battle/damage_calculation.test.ts b/src/test/battle/damage_calculation.test.ts index 89f2bb4c269..a348df6c085 100644 --- a/src/test/battle/damage_calculation.test.ts +++ b/src/test/battle/damage_calculation.test.ts @@ -1,14 +1,13 @@ -import { DamagePhase } from "#app/phases/damage-phase"; -import { toDmgValue } from "#app/utils"; +import { allMoves } from "#app/data/move"; import { Abilities } from "#enums/abilities"; import { ArenaTagType } from "#enums/arena-tag-type"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -describe("Round Down and Minimun 1 test in Damage Calculation", () => { +describe("Battle Mechanics - Damage Calculation", () => { let phaserGame: Phaser.Game; let game: GameManager; @@ -24,24 +23,86 @@ describe("Round Down and Minimun 1 test in Damage Calculation", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleType("single"); - game.override.startingLevel(10); + game.override + .battleType("single") + .enemySpecies(Species.SNORLAX) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH) + .startingLevel(100) + .enemyLevel(100) + .disableCrits() + .moveset([Moves.TACKLE, Moves.DRAGON_RAGE, Moves.FISSURE, Moves.JUMP_KICK]); + }); + + it("Tackle deals expected base damage", async () => { + await game.classicMode.startBattle([Species.CHARIZARD]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + vi.spyOn(playerPokemon, "getEffectiveStat").mockReturnValue(80); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + vi.spyOn(enemyPokemon, "getEffectiveStat").mockReturnValue(90); + + // expected base damage = [(2*level/5 + 2) * power * playerATK / enemyDEF / 50] + 2 + // = 31.8666... + expect(enemyPokemon.getAttackDamage(playerPokemon, allMoves[Moves.TACKLE]).damage).toBeCloseTo(31); + }); + + it("Attacks deal 1 damage at minimum", async () => { + game.override + .startingLevel(1) + .enemySpecies(Species.AGGRON); + + await game.classicMode.startBattle([Species.MAGIKARP]); + + const aggron = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.TACKLE); + + await game.phaseInterceptor.to("BerryPhase", false); + + // Lvl 1 0 Atk Magikarp Tackle vs. 0 HP / 0 Def Aggron: 1-1 (0.3 - 0.3%) -- possibly the worst move ever + expect(aggron.hp).toBe(aggron.getMaxHp() - 1); + }); + + it("Fixed-damage moves ignore damage multipliers", async () => { + game.override + .enemySpecies(Species.DRAGONITE) + .enemyAbility(Abilities.MULTISCALE); + + await game.classicMode.startBattle([Species.MAGIKARP]); + + const magikarp = game.scene.getPlayerPokemon()!; + const dragonite = game.scene.getEnemyPokemon()!; + + expect(dragonite.getAttackDamage(magikarp, allMoves[Moves.DRAGON_RAGE]).damage).toBe(40); + }); + + it("One-hit KO moves ignore damage multipliers", async () => { + game.override + .enemySpecies(Species.AGGRON) + .enemyAbility(Abilities.MULTISCALE); + + await game.classicMode.startBattle([Species.MAGIKARP]); + + const magikarp = game.scene.getPlayerPokemon()!; + const aggron = game.scene.getEnemyPokemon()!; + + expect(aggron.getAttackDamage(magikarp, allMoves[Moves.FISSURE]).damage).toBe(aggron.hp); }); it("When the user fails to use Jump Kick with Wonder Guard ability, the damage should be 1.", async () => { - game.override.enemySpecies(Species.GASTLY); - game.override.enemyMoveset(Moves.SPLASH); - game.override.starterSpecies(Species.SHEDINJA); - game.override.moveset([Moves.JUMP_KICK]); - game.override.ability(Abilities.WONDER_GUARD); + game.override + .enemySpecies(Species.GASTLY) + .ability(Abilities.WONDER_GUARD); - await game.startBattle(); + await game.classicMode.startBattle([Species.SHEDINJA]); const shedinja = game.scene.getPlayerPokemon()!; game.move.select(Moves.JUMP_KICK); - await game.phaseInterceptor.to(DamagePhase); + await game.phaseInterceptor.to("DamagePhase"); expect(shedinja.hp).toBe(shedinja.getMaxHp() - 1); }); @@ -49,21 +110,19 @@ describe("Round Down and Minimun 1 test in Damage Calculation", () => { it("Charizard with odd HP survives Stealth Rock damage twice", async () => { game.scene.arena.addTag(ArenaTagType.STEALTH_ROCK, 1, Moves.STEALTH_ROCK, 0); - game.override.seed("Charizard Stealth Rock test"); - game.override.enemySpecies(Species.CHARIZARD); - game.override.enemyAbility(Abilities.BLAZE); - game.override.starterSpecies(Species.PIKACHU); - game.override.enemyLevel(100); + game.override + .seed("Charizard Stealth Rock test") + .enemySpecies(Species.CHARIZARD) + .enemyAbility(Abilities.BLAZE); - await game.startBattle(); + await game.classicMode.startBattle([Species.PIKACHU]); const charizard = game.scene.getEnemyPokemon()!; - const maxHp = charizard.getMaxHp(); - const damage_prediction = toDmgValue(charizard.getMaxHp() / 2); - const currentHp = charizard.hp; - const expectedHP = maxHp - damage_prediction; - - expect(currentHp).toBe(expectedHP); + if (charizard.getMaxHp() % 2 === 1) { + expect(charizard.hp).toBeGreaterThan(charizard.getMaxHp() / 2); + } else { + expect(charizard.hp).toBe(charizard.getMaxHp() / 2); + } }); }); diff --git a/src/test/battle/inverse_battle.test.ts b/src/test/battle/inverse_battle.test.ts index d808f71addb..01a0348e730 100644 --- a/src/test/battle/inverse_battle.test.ts +++ b/src/test/battle/inverse_battle.test.ts @@ -10,7 +10,7 @@ import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -const TIMEOUT = 20 * 1000; + describe("Inverse Battle", () => { let phaserGame: Phaser.Game; @@ -56,7 +56,7 @@ describe("Inverse Battle", () => { await game.phaseInterceptor.to("MoveEffectPhase"); expect(enemy.getMoveEffectiveness).toHaveLastReturnedWith(2); - }, TIMEOUT); + }); it("2x effective types are 0.5x effective - Thunderbolt against Flying Type", async () => { game.override @@ -73,7 +73,7 @@ describe("Inverse Battle", () => { await game.phaseInterceptor.to("MoveEffectPhase"); expect(enemy.getMoveEffectiveness).toHaveLastReturnedWith(0.5); - }, TIMEOUT); + }); it("0.5x effective types are 2x effective - Thunderbolt against Electric Type", async () => { game.override @@ -90,7 +90,7 @@ describe("Inverse Battle", () => { await game.phaseInterceptor.to("MoveEffectPhase"); expect(enemy.getMoveEffectiveness).toHaveLastReturnedWith(2); - }, TIMEOUT); + }); it("Stealth Rock follows the inverse matchups - Stealth Rock against Charizard deals 1/32 of max HP", async () => { game.scene.arena.addTag(ArenaTagType.STEALTH_ROCK, 1, Moves.STEALTH_ROCK, 0); @@ -110,7 +110,7 @@ describe("Inverse Battle", () => { console.log("Charizard's max HP: " + maxHp, "Damage: " + damage_prediction, "Current HP: " + currentHp, "Expected HP: " + expectedHP); expect(currentHp).toBeGreaterThan(maxHp * 31 / 32 - 1); - }, TIMEOUT); + }); it("Freeze Dry is 2x effective against Water Type like other Ice type Move - Freeze Dry against Squirtle", async () => { game.override @@ -127,7 +127,7 @@ describe("Inverse Battle", () => { await game.phaseInterceptor.to("MoveEffectPhase"); expect(enemy.getMoveEffectiveness).toHaveLastReturnedWith(2); - }, TIMEOUT); + }); it("Water Absorb should heal against water moves - Water Absorb against Water gun", async () => { game.override @@ -143,7 +143,7 @@ describe("Inverse Battle", () => { await game.phaseInterceptor.to("MoveEndPhase"); expect(enemy.hp).toBe(enemy.getMaxHp()); - }, TIMEOUT); + }); it("Fire type does not get burned - Will-O-Wisp against Charmander", async () => { game.override @@ -160,7 +160,7 @@ describe("Inverse Battle", () => { await game.phaseInterceptor.to("MoveEndPhase"); expect(enemy.status?.effect).not.toBe(StatusEffect.BURN); - }, TIMEOUT); + }); it("Electric type does not get paralyzed - Nuzzle against Pikachu", async () => { game.override @@ -177,7 +177,7 @@ describe("Inverse Battle", () => { await game.phaseInterceptor.to("MoveEndPhase"); expect(enemy.status?.effect).not.toBe(StatusEffect.PARALYSIS); - }, TIMEOUT); + }); it("Ground type is not immune to Thunder Wave - Thunder Wave against Sandshrew", async () => { game.override @@ -194,7 +194,7 @@ describe("Inverse Battle", () => { await game.phaseInterceptor.to("MoveEndPhase"); expect(enemy.status?.effect).toBe(StatusEffect.PARALYSIS); - }, TIMEOUT); + }); it("Anticipation should trigger on 2x effective moves - Anticipation against Thunderbolt", async () => { @@ -206,7 +206,7 @@ describe("Inverse Battle", () => { await game.challengeMode.startBattle(); expect(game.scene.getEnemyPokemon()?.summonData.abilitiesApplied[0]).toBe(Abilities.ANTICIPATION); - }, TIMEOUT); + }); it("Conversion 2 should change the type to the resistive type - Conversion 2 against Dragonite", async () => { game.override @@ -223,7 +223,7 @@ describe("Inverse Battle", () => { await game.phaseInterceptor.to("TurnEndPhase"); expect(player.getTypes()[0]).toBe(Type.DRAGON); - }, TIMEOUT); + }); it("Flying Press should be 0.25x effective against Grass + Dark Type - Flying Press against Meowscarada", async () => { game.override @@ -240,7 +240,7 @@ describe("Inverse Battle", () => { await game.phaseInterceptor.to("MoveEffectPhase"); expect(enemy.getMoveEffectiveness).toHaveLastReturnedWith(0.25); - }, TIMEOUT); + }); it("Scrappy ability has no effect - Tackle against Ghost Type still 2x effective with Scrappy", async () => { game.override @@ -258,7 +258,7 @@ describe("Inverse Battle", () => { await game.phaseInterceptor.to("MoveEffectPhase"); expect(enemy.getMoveEffectiveness).toHaveLastReturnedWith(2); - }, TIMEOUT); + }); it("FORESIGHT has no effect - Tackle against Ghost Type still 2x effective with Foresight", async () => { game.override @@ -279,5 +279,5 @@ describe("Inverse Battle", () => { await game.phaseInterceptor.to("MoveEffectPhase"); expect(enemy.getMoveEffectiveness).toHaveLastReturnedWith(2); - }, TIMEOUT); + }); }); diff --git a/src/test/battlerTags/octolock.test.ts b/src/test/battlerTags/octolock.test.ts index 7b1f9264370..ebd92dc6401 100644 --- a/src/test/battlerTags/octolock.test.ts +++ b/src/test/battlerTags/octolock.test.ts @@ -10,7 +10,7 @@ vi.mock("#app/battle-scene.js"); describe("BattlerTag - OctolockTag", () => { describe("lapse behavior", () => { - it("unshifts a StatStageChangePhase with expected stat stage changes", { timeout: 10000 }, async () => { + it("unshifts a StatStageChangePhase with expected stat stage changes", async () => { const mockPokemon = { scene: new BattleScene(), getBattlerIndex: () => 0, @@ -30,11 +30,11 @@ describe("BattlerTag - OctolockTag", () => { }); }); - it ("traps its target (extends TrappedTag)", { timeout: 2000 }, async () => { + it ("traps its target (extends TrappedTag)", async () => { expect(new OctolockTag(1)).toBeInstanceOf(TrappedTag); }); - it("can be added to pokemon who are not octolocked", { timeout: 2000 }, async => { + it("can be added to pokemon who are not octolocked", async => { const mockPokemon = { getTag: vi.fn().mockReturnValue(undefined) as Pokemon["getTag"], } as Pokemon; @@ -47,7 +47,7 @@ describe("BattlerTag - OctolockTag", () => { expect(mockPokemon.getTag).toHaveBeenCalledWith(BattlerTagType.OCTOLOCK); }); - it("cannot be added to pokemon who are octolocked", { timeout: 2000 }, async => { + it("cannot be added to pokemon who are octolocked", async => { const mockPokemon = { getTag: vi.fn().mockReturnValue(new BattlerTag(null!, null!, null!, null!)) as Pokemon["getTag"], } as Pokemon; diff --git a/src/test/battlerTags/stockpiling.test.ts b/src/test/battlerTags/stockpiling.test.ts index e568016dfef..ae2528e7b5f 100644 --- a/src/test/battlerTags/stockpiling.test.ts +++ b/src/test/battlerTags/stockpiling.test.ts @@ -12,7 +12,7 @@ beforeEach(() => { describe("BattlerTag - StockpilingTag", () => { describe("onAdd", () => { - it("unshifts a StatStageChangePhase with expected stat stage changes on add", { timeout: 10000 }, async () => { + it("unshifts a StatStageChangePhase with expected stat stage changes on add", async () => { const mockPokemon = { scene: vi.mocked(new BattleScene()) as BattleScene, getBattlerIndex: () => 0, @@ -35,7 +35,7 @@ describe("BattlerTag - StockpilingTag", () => { expect(mockPokemon.scene.unshiftPhase).toBeCalledTimes(1); }); - it("unshifts a StatStageChangePhase with expected stat changes on add (one stat maxed)", { timeout: 10000 }, async () => { + it("unshifts a StatStageChangePhase with expected stat changes on add (one stat maxed)", async () => { const mockPokemon = { scene: new BattleScene(), summonData: new PokemonSummonData(), @@ -64,7 +64,7 @@ describe("BattlerTag - StockpilingTag", () => { }); describe("onOverlap", () => { - it("unshifts a StatStageChangePhase with expected stat changes on overlap", { timeout: 10000 }, async () => { + it("unshifts a StatStageChangePhase with expected stat changes on overlap", async () => { const mockPokemon = { scene: new BattleScene(), getBattlerIndex: () => 0, @@ -89,7 +89,7 @@ describe("BattlerTag - StockpilingTag", () => { }); describe("stack limit, stat tracking, and removal", () => { - it("can be added up to three times, even when one stat does not change", { timeout: 10000 }, async () => { + it("can be added up to three times, even when one stat does not change", async () => { const mockPokemon = { scene: new BattleScene(), summonData: new PokemonSummonData(), diff --git a/src/test/battlerTags/substitute.test.ts b/src/test/battlerTags/substitute.test.ts new file mode 100644 index 00000000000..0802b549823 --- /dev/null +++ b/src/test/battlerTags/substitute.test.ts @@ -0,0 +1,231 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import Pokemon, { MoveResult, PokemonTurnData, TurnMove, PokemonMove } from "#app/field/pokemon"; +import BattleScene from "#app/battle-scene"; +import { BattlerTagLapseType, BindTag, SubstituteTag } from "#app/data/battler-tags"; +import { Moves } from "#app/enums/moves"; +import { PokemonAnimType } from "#app/enums/pokemon-anim-type"; +import * as messages from "#app/messages"; +import { allMoves } from "#app/data/move"; +import { MoveEffectPhase } from "#app/phases/move-effect-phase"; + +vi.mock("#app/battle-scene.js"); + +describe("BattlerTag - SubstituteTag", () => { + let mockPokemon: Pokemon; + + describe("onAdd behavior", () => { + beforeEach(() => { + mockPokemon = { + scene: new BattleScene(), + hp: 101, + id: 0, + getMaxHp: vi.fn().mockReturnValue(101) as Pokemon["getMaxHp"], + findAndRemoveTags: vi.fn().mockImplementation((tagFilter) => { + // simulate a Trapped tag set by another Pokemon, then expect the filter to catch it. + const trapTag = new BindTag(5, 0); + expect(tagFilter(trapTag)).toBeTruthy(); + return true; + }) as Pokemon["findAndRemoveTags"] + } as Pokemon; + + vi.spyOn(messages, "getPokemonNameWithAffix").mockReturnValue(""); + vi.spyOn(mockPokemon.scene, "getPokemonById").mockImplementation(pokemonId => mockPokemon.id === pokemonId ? mockPokemon : null); + }); + + it( + "sets the tag's HP to 1/4 of the source's max HP (rounded down)", + async () => { + const subject = new SubstituteTag(Moves.SUBSTITUTE, mockPokemon.id); + + vi.spyOn(mockPokemon.scene, "triggerPokemonBattleAnim").mockReturnValue(true); + vi.spyOn(mockPokemon.scene, "queueMessage").mockReturnValue(); + + subject.onAdd(mockPokemon); + + expect(subject.hp).toBe(25); + } + ); + + it( + "triggers on-add effects that bring the source out of focus", + async () => { + const subject = new SubstituteTag(Moves.SUBSTITUTE, mockPokemon.id); + + vi.spyOn(mockPokemon.scene, "triggerPokemonBattleAnim").mockImplementation( + (pokemon, battleAnimType, fieldAssets?, delayed?) => { + expect(battleAnimType).toBe(PokemonAnimType.SUBSTITUTE_ADD); + return true; + } + ); + + vi.spyOn(mockPokemon.scene, "queueMessage").mockReturnValue(); + + subject.onAdd(mockPokemon); + + expect(subject.sourceInFocus).toBeFalsy(); + expect(mockPokemon.scene.triggerPokemonBattleAnim).toHaveBeenCalledTimes(1); + expect(mockPokemon.scene.queueMessage).toHaveBeenCalledTimes(1); + } + ); + + it( + "removes effects that trap the source", + async () => { + const subject = new SubstituteTag(Moves.SUBSTITUTE, mockPokemon.id); + + vi.spyOn(mockPokemon.scene, "queueMessage").mockReturnValue(); + + subject.onAdd(mockPokemon); + expect(mockPokemon.findAndRemoveTags).toHaveBeenCalledTimes(1); + } + ); + }); + + describe("onRemove behavior", () => { + beforeEach(() => { + mockPokemon = { + scene: new BattleScene(), + hp: 101, + id: 0, + isFainted: vi.fn().mockReturnValue(false) as Pokemon["isFainted"] + } as Pokemon; + + vi.spyOn(messages, "getPokemonNameWithAffix").mockReturnValue(""); + }); + + it( + "triggers on-remove animation and message", + async () => { + const subject = new SubstituteTag(Moves.SUBSTITUTE, mockPokemon.id); + subject.sourceInFocus = false; + + vi.spyOn(mockPokemon.scene, "triggerPokemonBattleAnim").mockImplementation( + (pokemon, battleAnimType, fieldAssets?, delayed?) => { + expect(battleAnimType).toBe(PokemonAnimType.SUBSTITUTE_REMOVE); + return true; + } + ); + + vi.spyOn(mockPokemon.scene, "queueMessage").mockReturnValue(); + + subject.onRemove(mockPokemon); + + expect(mockPokemon.scene.triggerPokemonBattleAnim).toHaveBeenCalledTimes(1); + expect(mockPokemon.scene.queueMessage).toHaveBeenCalledTimes(1); + } + ); + }); + + describe("lapse behavior", () => { + beforeEach(() => { + mockPokemon = { + scene: new BattleScene(), + hp: 101, + id: 0, + turnData: {acted: true} as PokemonTurnData, + getLastXMoves: vi.fn().mockReturnValue([{move: Moves.TACKLE, result: MoveResult.SUCCESS} as TurnMove]) as Pokemon["getLastXMoves"], + } as Pokemon; + + vi.spyOn(messages, "getPokemonNameWithAffix").mockReturnValue(""); + }); + + it( + "PRE_MOVE lapse triggers pre-move animation", + async () => { + const subject = new SubstituteTag(Moves.SUBSTITUTE, mockPokemon.id); + + vi.spyOn(mockPokemon.scene, "triggerPokemonBattleAnim").mockImplementation( + (pokemon, battleAnimType, fieldAssets?, delayed?) => { + expect(battleAnimType).toBe(PokemonAnimType.SUBSTITUTE_PRE_MOVE); + return true; + } + ); + + vi.spyOn(mockPokemon.scene, "queueMessage").mockReturnValue(); + + expect(subject.lapse(mockPokemon, BattlerTagLapseType.PRE_MOVE)).toBeTruthy(); + + expect(subject.sourceInFocus).toBeTruthy(); + expect(mockPokemon.scene.triggerPokemonBattleAnim).toHaveBeenCalledTimes(1); + expect(mockPokemon.scene.queueMessage).not.toHaveBeenCalled(); + } + ); + + it( + "AFTER_MOVE lapse triggers post-move animation", + async () => { + const subject = new SubstituteTag(Moves.SUBSTITUTE, mockPokemon.id); + + vi.spyOn(mockPokemon.scene, "triggerPokemonBattleAnim").mockImplementation( + (pokemon, battleAnimType, fieldAssets?, delayed?) => { + expect(battleAnimType).toBe(PokemonAnimType.SUBSTITUTE_POST_MOVE); + return true; + } + ); + + vi.spyOn(mockPokemon.scene, "queueMessage").mockReturnValue(); + + expect(subject.lapse(mockPokemon, BattlerTagLapseType.AFTER_MOVE)).toBeTruthy(); + + expect(subject.sourceInFocus).toBeFalsy(); + expect(mockPokemon.scene.triggerPokemonBattleAnim).toHaveBeenCalledTimes(1); + expect(mockPokemon.scene.queueMessage).not.toHaveBeenCalled(); + } + ); + + /** TODO: Figure out how to mock a MoveEffectPhase correctly for this test */ + it.skip( + "HIT lapse triggers on-hit message", + async () => { + const subject = new SubstituteTag(Moves.SUBSTITUTE, mockPokemon.id); + + vi.spyOn(mockPokemon.scene, "triggerPokemonBattleAnim").mockReturnValue(true); + vi.spyOn(mockPokemon.scene, "queueMessage").mockReturnValue(); + + const pokemonMove = { + getMove: vi.fn().mockReturnValue(allMoves[Moves.TACKLE]) as PokemonMove["getMove"] + } as PokemonMove; + + const moveEffectPhase = { + move: pokemonMove, + getUserPokemon: vi.fn().mockReturnValue(undefined) as MoveEffectPhase["getUserPokemon"] + } as MoveEffectPhase; + + vi.spyOn(mockPokemon.scene, "getCurrentPhase").mockReturnValue(moveEffectPhase); + vi.spyOn(allMoves[Moves.TACKLE], "hitsSubstitute").mockReturnValue(true); + + expect(subject.lapse(mockPokemon, BattlerTagLapseType.HIT)).toBeTruthy(); + + expect(mockPokemon.scene.triggerPokemonBattleAnim).not.toHaveBeenCalled(); + expect(mockPokemon.scene.queueMessage).toHaveBeenCalledTimes(1); + } + ); + + it( + "CUSTOM lapse flags the tag for removal", + async () => { + const subject = new SubstituteTag(Moves.SUBSTITUTE, mockPokemon.id); + + vi.spyOn(mockPokemon.scene, "triggerPokemonBattleAnim").mockReturnValue(true); + vi.spyOn(mockPokemon.scene, "queueMessage").mockReturnValue(); + + expect(subject.lapse(mockPokemon, BattlerTagLapseType.CUSTOM)).toBeFalsy(); + } + ); + + it( + "Unsupported lapse type does nothing", + async () => { + const subject = new SubstituteTag(Moves.SUBSTITUTE, mockPokemon.id); + + vi.spyOn(mockPokemon.scene, "triggerPokemonBattleAnim").mockReturnValue(true); + vi.spyOn(mockPokemon.scene, "queueMessage").mockReturnValue(); + + expect(subject.lapse(mockPokemon, BattlerTagLapseType.TURN_END)).toBeTruthy(); + + expect(mockPokemon.scene.triggerPokemonBattleAnim).not.toHaveBeenCalled(); + expect(mockPokemon.scene.queueMessage).not.toHaveBeenCalled(); + } + ); + }); +}); diff --git a/src/test/boss-pokemon.test.ts b/src/test/boss-pokemon.test.ts index 8a0a0e01617..e316cac0cf6 100644 --- a/src/test/boss-pokemon.test.ts +++ b/src/test/boss-pokemon.test.ts @@ -9,8 +9,6 @@ import { EnemyPokemon } from "#app/field/pokemon"; import { toDmgValue } from "#app/utils"; describe("Boss Pokemon / Shields", () => { - const TIMEOUT = 20 * 1000; - let phaserGame: Phaser.Game; let game: GameManager; @@ -35,7 +33,7 @@ describe("Boss Pokemon / Shields", () => { .enemyMoveset(Moves.SPLASH) .enemyHeldItems([]) .startingLevel(1000) - .moveset([Moves.FALSE_SWIPE, Moves.SUPER_FANG, Moves.SPLASH]) + .moveset([Moves.FALSE_SWIPE, Moves.SUPER_FANG, Moves.SPLASH, Moves.PSYCHIC]) .ability(Abilities.NO_GUARD); }); @@ -62,7 +60,7 @@ describe("Boss Pokemon / Shields", () => { // Pokemon above level 100 get an extra shield level = 100; expect(game.scene.getEncounterBossSegments(wave, level, getPokemonSpecies(Species.RATTATA))).toBe(7); - }, TIMEOUT); + }); it("should reduce the number of shields if we are in a double battle", async () => { game.override @@ -77,7 +75,7 @@ describe("Boss Pokemon / Shields", () => { expect(boss1.bossSegments).toBe(2); expect(boss2.isBoss()).toBe(true); expect(boss2.bossSegments).toBe(2); - }, TIMEOUT); + }); it("shields should stop overflow damage and give stat stage boosts when broken", async () => { game.override.startingWave(150); // Floor 150 > 2 shields / 3 health segments @@ -107,7 +105,7 @@ describe("Boss Pokemon / Shields", () => { // Breaking the last shield gives a +2 boost to ATK, DEF, SP ATK, SP DEF or SPD expect(getTotalStatStageBoosts(enemyPokemon)).toBe(3); - }, TIMEOUT); + }); it("breaking multiple shields at once requires extra damage", async () => { game.override @@ -143,7 +141,7 @@ describe("Boss Pokemon / Shields", () => { expect(boss2.bossSegmentIndex).toBe(0); expect(boss2.hp).toBe(boss2.getMaxHp() - toDmgValue(boss2SegmentHp * 4)); - }, TIMEOUT); + }); it("the number of stat stage boosts is consistent when several shields are broken at once", async () => { const shieldsToBreak = 4; @@ -196,7 +194,29 @@ describe("Boss Pokemon / Shields", () => { await game.toNextTurn(); expect(getTotalStatStageBoosts(boss2)).toBe(totalStatStages); - }, TIMEOUT); + }); + + it("the boss enduring does not proc an extra stat boost", async () => { + game.override + .enemyHealthSegments(2) + .enemyAbility(Abilities.STURDY); + + await game.classicMode.startBattle([ Species.MEWTWO ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + expect(enemyPokemon.isBoss()).toBe(true); + expect(enemyPokemon.bossSegments).toBe(2); + expect(getTotalStatStageBoosts(enemyPokemon)).toBe(0); + + game.move.select(Moves.PSYCHIC); + await game.toNextTurn(); + + // Enemy survived with Sturdy + expect(enemyPokemon.bossSegmentIndex).toBe(0); + expect(enemyPokemon.hp).toBe(1); + expect(getTotalStatStageBoosts(enemyPokemon)).toBe(1); + + }); /** * Gets the sum of the effective stat stage boosts for the given Pokemon diff --git a/src/test/data/splash_messages.test.ts b/src/test/data/splash_messages.test.ts new file mode 100644 index 00000000000..7e07b9a6e77 --- /dev/null +++ b/src/test/data/splash_messages.test.ts @@ -0,0 +1,66 @@ +import { getSplashMessages } from "#app/data/splash-messages"; +import { describe, expect, it, vi, afterEach, beforeEach } from "vitest"; +import * as Constants from "#app/constants"; + +describe("Data - Splash Messages", () => { + it("should contain at least 15 splash messages", () => { + expect(getSplashMessages().length).toBeGreaterThanOrEqual(15); + }); + + // make sure to adjust this test if the weight it changed! + it("should add contain 10 `battlesWon` splash messages", () => { + const battlesWonMessages = getSplashMessages().filter((message) => message === "splashMessages:battlesWon"); + expect(battlesWonMessages).toHaveLength(10); + }); + + describe("Seasonal", () => { + beforeEach(() => { + vi.spyOn(Constants, "USE_SEASONAL_SPLASH_MESSAGES", "get").mockReturnValue(true); + }); + + afterEach(() => { + vi.useRealTimers(); // reset system time + }); + + it("should contain halloween messages from Sep 15 to Oct 31", () => { + testSeason(new Date("2024-09-15"), new Date("2024-10-31"), "halloween"); + }); + + it("should contain xmas messages from Dec 1 to Dec 26", () => { + testSeason(new Date("2024-12-01"), new Date("2024-12-26"), "xmas"); + }); + + it("should contain new years messages frm Jan 1 to Jan 31", () => { + testSeason(new Date("2024-01-01"), new Date("2024-01-31"), "newYears"); + }); + }); +}); + +/** + * Helpoer method to test seasonal messages + * @param startDate The seasons start date + * @param endDate The seasons end date + * @param prefix the splash message prefix (e.g. `newYears` or `xmas`) + */ +function testSeason(startDate: Date, endDate: Date, prefix: string) { + const filterFn = (message: string) => message.startsWith(`splashMessages:${prefix}.`); + + const beforeDate = new Date(startDate); + beforeDate.setDate(startDate.getDate() - 1); + + const afterDate = new Date(endDate); + afterDate.setDate(endDate.getDate() + 1); + + const dates: Date[] = [beforeDate, startDate, endDate, afterDate]; + const [before, start, end, after] = dates.map((date) => { + vi.setSystemTime(date); + console.log("System time set to", date); + const count = getSplashMessages().filter(filterFn).length; + return count; + }); + + expect(before).toBe(0); + expect(start).toBeGreaterThanOrEqual(10); // make sure to adjust if weight is changed! + expect(end).toBeGreaterThanOrEqual(10); // make sure to adjust if weight is changed! + expect(after).toBe(0); +} diff --git a/src/test/eggs/egg.test.ts b/src/test/eggs/egg.test.ts index 4f00e843b47..053ff8f1112 100644 --- a/src/test/eggs/egg.test.ts +++ b/src/test/eggs/egg.test.ts @@ -1,7 +1,7 @@ import { Egg, getLegendaryGachaSpeciesForTimestamp } from "#app/data/egg"; import { EggSourceType } from "#app/enums/egg-source-types"; import { EggTier } from "#app/enums/egg-type"; -import { VariantTier } from "#app/enums/variant-tiers"; +import { VariantTier } from "#app/enums/variant-tier"; import EggData from "#app/system/egg-data"; import * as Utils from "#app/utils"; import { Species } from "#enums/species"; @@ -136,9 +136,9 @@ describe("Egg Generation Tests", () => { expect(result).toBe(expectedResult); }); - it("should return a shiny common variant", () => { + it("should return a shiny standard variant", () => { const scene = game.scene; - const expectedVariantTier = VariantTier.COMMON; + const expectedVariantTier = VariantTier.STANDARD; const result = new Egg({ scene, isShiny: true, variantTier: expectedVariantTier, species: Species.BULBASAUR }).generatePlayerPokemon(scene).variant; diff --git a/src/test/enemy_command.test.ts b/src/test/enemy_command.test.ts new file mode 100644 index 00000000000..53cddc86efb --- /dev/null +++ b/src/test/enemy_command.test.ts @@ -0,0 +1,106 @@ +import { allMoves, MoveCategory } from "#app/data/move"; +import { Abilities } from "#app/enums/abilities"; +import { Moves } from "#app/enums/moves"; +import { Species } from "#app/enums/species"; +import { AiType, EnemyPokemon } from "#app/field/pokemon"; +import { randSeedInt } from "#app/utils"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + + +const NUM_TRIALS = 300; + +type MoveChoiceSet = { [key: number]: number }; + +function getEnemyMoveChoices(pokemon: EnemyPokemon, moveChoices: MoveChoiceSet): void { + // Use an unseeded random number generator in place of the mocked-out randBattleSeedInt + vi.spyOn(pokemon.scene, "randBattleSeedInt").mockImplementation((range, min?) => { + return randSeedInt(range, min); + }); + for (let i = 0; i < NUM_TRIALS; i++) { + const queuedMove = pokemon.getNextMove(); + moveChoices[queuedMove.move]++; + } + + for (const [moveId, count] of Object.entries(moveChoices)) { + console.log(`Move: ${allMoves[moveId].name} Count: ${count} (${count / NUM_TRIALS * 100}%)`); + } +} + +describe("Enemy Commands - Move Selection", () => { + 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(Abilities.BALL_FETCH) + .enemyAbility(Abilities.BALL_FETCH); + }); + + it( + "should never use Status moves if an attack can KO", + async () => { + game.override + .enemySpecies(Species.ETERNATUS) + .enemyMoveset([Moves.ETERNABEAM, Moves.SLUDGE_BOMB, Moves.DRAGON_DANCE, Moves.COSMIC_POWER]) + .startingLevel(1) + .enemyLevel(100); + + await game.classicMode.startBattle([Species.MAGIKARP]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + enemyPokemon.aiType = AiType.SMART_RANDOM; + + const moveChoices: MoveChoiceSet = {}; + const enemyMoveset = enemyPokemon.getMoveset(); + enemyMoveset.forEach(mv => moveChoices[mv!.moveId] = 0); + getEnemyMoveChoices(enemyPokemon, moveChoices); + + enemyMoveset.forEach(mv => { + if (mv?.getMove().category === MoveCategory.STATUS) { + expect(moveChoices[mv.moveId]).toBe(0); + } + }); + } + ); + + it( + "should not select Last Resort if it would fail, even if the move KOs otherwise", + async () => { + game.override + .enemySpecies(Species.KANGASKHAN) + .enemyMoveset([Moves.LAST_RESORT, Moves.GIGA_IMPACT, Moves.SPLASH, Moves.SWORDS_DANCE]) + .startingLevel(1) + .enemyLevel(100); + + await game.classicMode.startBattle([Species.MAGIKARP]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + enemyPokemon.aiType = AiType.SMART_RANDOM; + + const moveChoices: MoveChoiceSet = {}; + const enemyMoveset = enemyPokemon.getMoveset(); + enemyMoveset.forEach(mv => moveChoices[mv!.moveId] = 0); + getEnemyMoveChoices(enemyPokemon, moveChoices); + + enemyMoveset.forEach(mv => { + if (mv?.getMove().category === MoveCategory.STATUS || mv?.moveId === Moves.LAST_RESORT) { + expect(moveChoices[mv.moveId]).toBe(0); + } + }); + } + ); +}); diff --git a/src/test/evolution.test.ts b/src/test/evolution.test.ts index 16922babd7c..07865d7e64a 100644 --- a/src/test/evolution.test.ts +++ b/src/test/evolution.test.ts @@ -10,7 +10,6 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vite describe("Evolution", () => { let phaserGame: Phaser.Game; let game: GameManager; - const TIMEOUT = 1000 * 20; beforeAll(() => { phaserGame = new Phaser.Game({ @@ -46,7 +45,7 @@ describe("Evolution", () => { trapinch.evolve(pokemonEvolutions[Species.TRAPINCH][0], trapinch.getSpeciesForm()); expect(trapinch.abilityIndex).toBe(1); - }, TIMEOUT); + }); it("should keep same ability slot after evolving", async () => { await game.classicMode.runToSummon([Species.BULBASAUR, Species.CHARMANDER]); @@ -61,7 +60,7 @@ describe("Evolution", () => { charmander.evolve(pokemonEvolutions[Species.CHARMANDER][0], charmander.getSpeciesForm()); expect(charmander.abilityIndex).toBe(1); - }, TIMEOUT); + }); it("should handle illegal abilityIndex values", async () => { await game.classicMode.runToSummon([Species.SQUIRTLE]); @@ -71,7 +70,7 @@ describe("Evolution", () => { squirtle.evolve(pokemonEvolutions[Species.SQUIRTLE][0], squirtle.getSpeciesForm()); expect(squirtle.abilityIndex).toBe(0); - }, TIMEOUT); + }); it("should handle nincada's unique evolution", async () => { await game.classicMode.runToSummon([Species.NINCADA]); @@ -87,7 +86,7 @@ describe("Evolution", () => { expect(shedinja.abilityIndex).toBe(1); // Regression test for https://github.com/pagefaultgames/pokerogue/issues/3842 expect(shedinja.metBiome).toBe(-1); - }, TIMEOUT); + }); it("should set wild delay to NONE by default", () => { const speciesFormEvo = new SpeciesFormEvolution(Species.ABRA, null, null, 1000, null, null); @@ -120,7 +119,7 @@ describe("Evolution", () => { expect(totodile.hp).toBe(totodile.getMaxHp()); expect(totodile.hp).toBeGreaterThan(hpBefore); - }, TIMEOUT); + }); it("should not fully heal HP when evolving", async () => { game.override.moveset([Moves.SURF]) @@ -150,7 +149,7 @@ describe("Evolution", () => { expect(cyndaquil.getMaxHp()).toBeGreaterThan(maxHpBefore); expect(cyndaquil.hp).toBeGreaterThan(hpBefore); expect(cyndaquil.hp).toBeLessThan(cyndaquil.getMaxHp()); - }, TIMEOUT); + }); it("should handle rng-based split evolution", async () => { /* this test checks to make sure that tandemaus will @@ -174,5 +173,5 @@ describe("Evolution", () => { const fourForm = playerPokemon.getEvolution()!; expect(fourForm.evoFormKey).toBe(null); // meanwhile, according to the pokemon-forms, the evoFormKey for a 4 family maushold is null } - }, TIMEOUT); + }); }); diff --git a/src/test/items/double_battle_chance_booster.test.ts b/src/test/items/double_battle_chance_booster.test.ts index 1d5051fa9e9..8d2bd7c9179 100644 --- a/src/test/items/double_battle_chance_booster.test.ts +++ b/src/test/items/double_battle_chance_booster.test.ts @@ -12,8 +12,6 @@ import { Button } from "#app/enums/buttons"; describe("Items - Double Battle Chance Boosters", () => { let phaserGame: Phaser.Game; let game: GameManager; - const TIMEOUT = 20 * 1000; - beforeAll(() => { phaserGame = new Phaser.Game({ type: Phaser.HEADLESS, @@ -39,7 +37,7 @@ describe("Items - Double Battle Chance Boosters", () => { await game.classicMode.startBattle(); expect(game.scene.getEnemyField().length).toBe(2); - }, TIMEOUT); + }); it("should guarantee double boss battle with 3 unique tiers", async () => { game.override @@ -57,7 +55,7 @@ describe("Items - Double Battle Chance Boosters", () => { expect(enemyField.length).toBe(2); expect(enemyField[0].isBoss()).toBe(true); expect(enemyField[1].isBoss()).toBe(true); - }, TIMEOUT); + }); it("should renew how many battles are left of existing booster when picking up new booster of same tier", async() => { game.override @@ -100,5 +98,5 @@ describe("Items - Double Battle Chance Boosters", () => { } } expect(count).toBe(1); - }, TIMEOUT); + }); }); diff --git a/src/test/items/eviolite.test.ts b/src/test/items/eviolite.test.ts index d9991d47a89..7b2f9a15fce 100644 --- a/src/test/items/eviolite.test.ts +++ b/src/test/items/eviolite.test.ts @@ -9,8 +9,6 @@ import { StatBoosterModifier } from "#app/modifier/modifier"; describe("Items - Eviolite", () => { let phaserGame: Phaser.Game; let game: GameManager; - const TIMEOUT = 20 * 1000; - beforeAll(() => { phaserGame = new Phase.Game({ type: Phaser.HEADLESS, @@ -50,7 +48,7 @@ describe("Items - Eviolite", () => { expect(partyMember.getEffectiveStat(Stat.DEF)).toBe(Math.floor(defStat * 1.5)); expect(partyMember.getEffectiveStat(Stat.SPDEF)).toBe(Math.floor(spDefStat * 1.5)); - }, TIMEOUT); + }); it("should not provide a boost for fully evolved, unfused pokemon", async() => { await game.classicMode.startBattle([ @@ -74,7 +72,7 @@ describe("Items - Eviolite", () => { expect(partyMember.getEffectiveStat(Stat.DEF)).toBe(defStat); expect(partyMember.getEffectiveStat(Stat.SPDEF)).toBe(spDefStat); - }, TIMEOUT); + }); it("should provide 50% boost to DEF and SPDEF for completely unevolved, fused pokemon", async() => { await game.classicMode.startBattle([ @@ -107,7 +105,7 @@ describe("Items - Eviolite", () => { expect(partyMember.getEffectiveStat(Stat.DEF)).toBe(Math.floor(defStat * 1.5)); expect(partyMember.getEffectiveStat(Stat.SPDEF)).toBe(Math.floor(spDefStat * 1.5)); - }, TIMEOUT); + }); it("should provide 25% boost to DEF and SPDEF for partially unevolved (base), fused pokemon", async() => { await game.classicMode.startBattle([ @@ -140,7 +138,7 @@ describe("Items - Eviolite", () => { expect(partyMember.getEffectiveStat(Stat.DEF)).toBe(Math.floor(defStat * 1.25)); expect(partyMember.getEffectiveStat(Stat.SPDEF)).toBe(Math.floor(spDefStat * 1.25)); - }, TIMEOUT); + }); it("should provide 25% boost to DEF and SPDEF for partially unevolved (fusion), fused pokemon", async() => { await game.classicMode.startBattle([ @@ -173,7 +171,7 @@ describe("Items - Eviolite", () => { expect(partyMember.getEffectiveStat(Stat.DEF)).toBe(Math.floor(defStat * 1.25)); expect(partyMember.getEffectiveStat(Stat.SPDEF)).toBe(Math.floor(spDefStat * 1.25)); - }, TIMEOUT); + }); it("should not provide a boost for fully evolved, fused pokemon", async() => { await game.classicMode.startBattle([ @@ -206,7 +204,7 @@ describe("Items - Eviolite", () => { expect(partyMember.getEffectiveStat(Stat.DEF)).toBe(defStat); expect(partyMember.getEffectiveStat(Stat.SPDEF)).toBe(spDefStat); - }, TIMEOUT); + }); it("should not provide a boost for Gigantamax Pokémon", async() => { game.override.starterForms({ @@ -238,5 +236,5 @@ describe("Items - Eviolite", () => { expect(partyMember.getEffectiveStat(Stat.DEF)).toBe(defStat); expect(partyMember.getEffectiveStat(Stat.SPDEF)).toBe(spDefStat); - }, TIMEOUT); + }); }); diff --git a/src/test/items/grip_claw.test.ts b/src/test/items/grip_claw.test.ts index d9871616449..29d39cabc3e 100644 --- a/src/test/items/grip_claw.test.ts +++ b/src/test/items/grip_claw.test.ts @@ -9,7 +9,7 @@ import GameManager from "#test/utils/gameManager"; import Phase from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -const TIMEOUT = 20 * 1000; // 20 seconds +// 20 seconds describe("Items - Grip Claw", () => { let phaserGame: Phaser.Game; @@ -63,6 +63,6 @@ describe("Items - Grip Claw", () => { await game.phaseInterceptor.to(MoveEndPhase, false); expect(enemyPokemon[1].getHeldItems.length).toBe(enemyHeldItemCt[1]); - }, TIMEOUT + } ); }); diff --git a/src/test/moves/after_you.test.ts b/src/test/moves/after_you.test.ts index efce1b28a17..025b4804bf1 100644 --- a/src/test/moves/after_you.test.ts +++ b/src/test/moves/after_you.test.ts @@ -8,7 +8,7 @@ import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; -const TIMEOUT = 20 * 1000; + describe("Moves - After You", () => { let phaserGame: Phaser.Game; @@ -47,7 +47,7 @@ describe("Moves - After You", () => { const phase = game.scene.getCurrentPhase() as MovePhase; expect(phase.pokemon).toBe(game.scene.getPlayerField()[1]); await game.phaseInterceptor.to("MoveEndPhase"); - }, TIMEOUT); + }); it("fails if target already moved", async () => { game.override.enemySpecies(Species.SHUCKLE); @@ -61,5 +61,5 @@ describe("Moves - After You", () => { await game.phaseInterceptor.to(MovePhase); expect(game.scene.getPlayerField()[1].getLastXMoves(1)[0].result).toBe(MoveResult.FAIL); - }, TIMEOUT); + }); }); diff --git a/src/test/moves/alluring_voice.test.ts b/src/test/moves/alluring_voice.test.ts index b438d0f736a..3e86b46aa69 100644 --- a/src/test/moves/alluring_voice.test.ts +++ b/src/test/moves/alluring_voice.test.ts @@ -8,7 +8,7 @@ import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; -const TIMEOUT = 20 * 1000; + describe("Moves - Alluring Voice", () => { let phaserGame: Phaser.Game; @@ -50,5 +50,5 @@ describe("Moves - Alluring Voice", () => { await game.phaseInterceptor.to(BerryPhase); expect(enemy.getTag(BattlerTagType.CONFUSED)?.tagType).toBe("CONFUSED"); - }, TIMEOUT); + }); }); diff --git a/src/test/moves/astonish.test.ts b/src/test/moves/astonish.test.ts index b21e2a06051..694ad85803b 100644 --- a/src/test/moves/astonish.test.ts +++ b/src/test/moves/astonish.test.ts @@ -11,7 +11,7 @@ import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest"; -const TIMEOUT = 20 * 1000; + describe("Moves - Astonish", () => { let phaserGame: Phaser.Game; @@ -67,6 +67,6 @@ describe("Moves - Astonish", () => { await game.phaseInterceptor.to(BerryPhase, false); expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp()); - }, TIMEOUT + } ); }); diff --git a/src/test/moves/baddy_bad.test.ts b/src/test/moves/baddy_bad.test.ts index d1a221453a6..87a7e9e049d 100644 --- a/src/test/moves/baddy_bad.test.ts +++ b/src/test/moves/baddy_bad.test.ts @@ -8,8 +8,6 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; describe("Moves - Baddy Bad", () => { let phaserGame: Phaser.Game; let game: GameManager; - const TIMEOUT = 20 * 1000; - beforeAll(() => { phaserGame = new Phaser.Game({ type: Phaser.HEADLESS, @@ -39,5 +37,5 @@ describe("Moves - Baddy Bad", () => { await game.phaseInterceptor.to("BerryPhase"); expect(game.scene.arena.tags.length).toBe(0); - }, TIMEOUT); + }); }); diff --git a/src/test/moves/baneful_bunker.test.ts b/src/test/moves/baneful_bunker.test.ts new file mode 100644 index 00000000000..5f63e3b4313 --- /dev/null +++ b/src/test/moves/baneful_bunker.test.ts @@ -0,0 +1,93 @@ +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest"; +import GameManager from "../utils/gameManager"; +import { Species } from "#enums/species"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { BattlerIndex } from "#app/battle"; +import { StatusEffect } from "#app/enums/status-effect"; + + + +describe("Moves - Baneful Bunker", () => { + 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.battleType("single"); + + game.override.moveset(Moves.SLASH); + + game.override.enemySpecies(Species.SNORLAX); + game.override.enemyAbility(Abilities.INSOMNIA); + game.override.enemyMoveset(Moves.BANEFUL_BUNKER); + + game.override.startingLevel(100); + game.override.enemyLevel(100); + }); + test( + "should protect the user and poison attackers that make contact", + async () => { + await game.classicMode.startBattle([Species.CHARIZARD]); + + const leadPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.SLASH); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + await game.phaseInterceptor.to("BerryPhase", false); + expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); + expect(leadPokemon.status?.effect === StatusEffect.POISON).toBeTruthy(); + } + ); + test( + "should protect the user and poison attackers that make contact, regardless of accuracy checks", + async () => { + await game.classicMode.startBattle([Species.CHARIZARD]); + + const leadPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.SLASH); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + await game.phaseInterceptor.to("MoveEffectPhase"); + + await game.move.forceMiss(); + await game.phaseInterceptor.to("BerryPhase", false); + expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); + expect(leadPokemon.status?.effect === StatusEffect.POISON).toBeTruthy(); + } + ); + + test( + "should not poison attackers that don't make contact", + async () => { + game.override.moveset(Moves.FLASH_CANNON); + await game.classicMode.startBattle([Species.CHARIZARD]); + + const leadPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.FLASH_CANNON); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + await game.phaseInterceptor.to("MoveEffectPhase"); + + await game.move.forceMiss(); + await game.phaseInterceptor.to("BerryPhase", false); + expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); + expect(leadPokemon.status?.effect === StatusEffect.POISON).toBeFalsy(); + } + ); +}); diff --git a/src/test/moves/beak_blast.test.ts b/src/test/moves/beak_blast.test.ts index fe748c87826..3f4fe1d1d11 100644 --- a/src/test/moves/beak_blast.test.ts +++ b/src/test/moves/beak_blast.test.ts @@ -10,7 +10,7 @@ import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; -const TIMEOUT = 20 * 1000; + describe("Moves - Beak Blast", () => { let phaserGame: Phaser.Game; @@ -54,7 +54,7 @@ describe("Moves - Beak Blast", () => { await game.phaseInterceptor.to(BerryPhase, false); expect(enemyPokemon.status?.effect).toBe(StatusEffect.BURN); - }, TIMEOUT + } ); it( @@ -74,7 +74,7 @@ describe("Moves - Beak Blast", () => { await game.phaseInterceptor.to(BerryPhase, false); expect(enemyPokemon.status?.effect).toBe(StatusEffect.BURN); - }, TIMEOUT + } ); it( @@ -94,7 +94,7 @@ describe("Moves - Beak Blast", () => { await game.phaseInterceptor.to(BerryPhase, false); expect(enemyPokemon.status?.effect).not.toBe(StatusEffect.BURN); - }, TIMEOUT + } ); it( @@ -110,7 +110,7 @@ describe("Moves - Beak Blast", () => { await game.phaseInterceptor.to(BerryPhase, false); expect(leadPokemon.turnData.hitCount).toBe(2); - }, TIMEOUT + } ); it( @@ -131,6 +131,6 @@ describe("Moves - Beak Blast", () => { await game.phaseInterceptor.to(TurnEndPhase); expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); expect(leadPokemon.getTag(BattlerTagType.BEAK_BLAST_CHARGING)).toBeUndefined(); - }, TIMEOUT + } ); }); diff --git a/src/test/moves/beat_up.test.ts b/src/test/moves/beat_up.test.ts index 70b33f56583..51ec768084c 100644 --- a/src/test/moves/beat_up.test.ts +++ b/src/test/moves/beat_up.test.ts @@ -7,8 +7,6 @@ import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; -const TIMEOUT = 20 * 1000; // 20 sec timeout - describe("Moves - Beat Up", () => { let phaserGame: Phaser.Game; let game: GameManager; @@ -57,7 +55,7 @@ describe("Moves - Beat Up", () => { await game.phaseInterceptor.to(MoveEffectPhase); expect(enemyPokemon.hp).toBeLessThan(enemyStartingHp); } - }, TIMEOUT + } ); it( @@ -74,7 +72,7 @@ describe("Moves - Beat Up", () => { await game.phaseInterceptor.to(MoveEffectPhase); expect(playerPokemon.turnData.hitCount).toBe(5); - }, TIMEOUT + } ); it( @@ -99,6 +97,6 @@ describe("Moves - Beat Up", () => { await game.phaseInterceptor.to(MoveEffectPhase); expect(enemyPokemon.hp).toBeLessThan(enemyStartingHp); } - }, TIMEOUT + } ); }); diff --git a/src/test/moves/belly_drum.test.ts b/src/test/moves/belly_drum.test.ts index 3d85c59a2a5..494272089e2 100644 --- a/src/test/moves/belly_drum.test.ts +++ b/src/test/moves/belly_drum.test.ts @@ -8,7 +8,7 @@ import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest"; import { Abilities } from "#app/enums/abilities"; -const TIMEOUT = 20 * 1000; + // RATIO : HP Cost of Move const RATIO = 2; // PREDAMAGE : Amount of extra HP lost @@ -54,7 +54,7 @@ describe("Moves - BELLY DRUM", () => { expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp() - hpLost); expect(leadPokemon.getStatStage(Stat.ATK)).toBe(6); - }, TIMEOUT + } ); test("will still take effect if an uninvolved stat stage is at max", @@ -74,7 +74,7 @@ describe("Moves - BELLY DRUM", () => { expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp() - hpLost); expect(leadPokemon.getStatStage(Stat.ATK)).toBe(6); expect(leadPokemon.getStatStage(Stat.SPATK)).toBe(6); - }, TIMEOUT + } ); test("fails if the pokemon's ATK stat stage is at its maximum", @@ -90,7 +90,7 @@ describe("Moves - BELLY DRUM", () => { expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp()); expect(leadPokemon.getStatStage(Stat.ATK)).toBe(6); - }, TIMEOUT + } ); test("fails if the user's health is less than 1/2", @@ -106,6 +106,6 @@ describe("Moves - BELLY DRUM", () => { expect(leadPokemon.hp).toBe(hpLost - PREDAMAGE); expect(leadPokemon.getStatStage(Stat.ATK)).toBe(0); - }, TIMEOUT + } ); }); diff --git a/src/test/moves/burning_jealousy.test.ts b/src/test/moves/burning_jealousy.test.ts index 3f2bf453684..d6ebbf30bb1 100644 --- a/src/test/moves/burning_jealousy.test.ts +++ b/src/test/moves/burning_jealousy.test.ts @@ -8,7 +8,7 @@ import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -const TIMEOUT = 20 * 1000; + describe("Moves - Burning Jealousy", () => { let phaserGame: Phaser.Game; @@ -50,7 +50,7 @@ describe("Moves - Burning Jealousy", () => { await game.phaseInterceptor.to("BerryPhase"); expect(enemy.status?.effect).toBe(StatusEffect.BURN); - }, TIMEOUT); + }); it("should still burn the opponent if their stat stages were both raised and lowered in the same turn", async () => { game.override @@ -66,7 +66,7 @@ describe("Moves - Burning Jealousy", () => { await game.phaseInterceptor.to("BerryPhase"); expect(enemy.status?.effect).toBe(StatusEffect.BURN); - }, TIMEOUT); + }); it("should ignore stat stages raised by IMPOSTER", async () => { game.override @@ -81,11 +81,11 @@ describe("Moves - Burning Jealousy", () => { await game.phaseInterceptor.to("BerryPhase"); expect(enemy.status?.effect).toBeUndefined(); - }, TIMEOUT); + }); it.skip("should ignore weakness policy", async () => { // TODO: Make this test if WP is implemented await game.classicMode.startBattle(); - }, TIMEOUT); + }); it("should be boosted by Sheer Force even if opponent didn't raise stat stages", async () => { game.override @@ -98,5 +98,5 @@ describe("Moves - Burning Jealousy", () => { await game.phaseInterceptor.to("BerryPhase"); expect(allMoves[Moves.BURNING_JEALOUSY].calculateBattlePower).toHaveReturnedWith(allMoves[Moves.BURNING_JEALOUSY].power * 5461 / 4096); - }, TIMEOUT); + }); }); diff --git a/src/test/moves/ceaseless_edge.test.ts b/src/test/moves/ceaseless_edge.test.ts index 8511b3179c6..e98fe462c62 100644 --- a/src/test/moves/ceaseless_edge.test.ts +++ b/src/test/moves/ceaseless_edge.test.ts @@ -10,7 +10,7 @@ import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest"; -const TIMEOUT = 20 * 1000; + describe("Moves - Ceaseless Edge", () => { let phaserGame: Phaser.Game; @@ -61,7 +61,7 @@ describe("Moves - Ceaseless Edge", () => { expect(tagAfter instanceof ArenaTrapTag).toBeTruthy(); expect(tagAfter.layers).toBe(1); expect(enemyPokemon.hp).toBeLessThan(enemyStartingHp); - }, TIMEOUT + } ); test( @@ -86,7 +86,7 @@ describe("Moves - Ceaseless Edge", () => { expect(tagAfter instanceof ArenaTrapTag).toBeTruthy(); expect(tagAfter.layers).toBe(2); expect(enemyPokemon.hp).toBeLessThan(enemyStartingHp); - }, TIMEOUT + } ); test( @@ -114,6 +114,6 @@ describe("Moves - Ceaseless Edge", () => { game.move.select(Moves.SPLASH); await game.phaseInterceptor.to(TurnEndPhase, false); expect(game.scene.currentBattle.enemyParty[0].hp).toBeLessThan(hpBeforeSpikes); - }, TIMEOUT + } ); }); diff --git a/src/test/moves/clangorous_soul.test.ts b/src/test/moves/clangorous_soul.test.ts index 015b73b4dab..8f0bfb2549f 100644 --- a/src/test/moves/clangorous_soul.test.ts +++ b/src/test/moves/clangorous_soul.test.ts @@ -6,7 +6,7 @@ import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import { Stat } from "#enums/stat"; -const TIMEOUT = 20 * 1000; + /** HP Cost of Move */ const RATIO = 3; /** Amount of extra HP lost */ @@ -54,7 +54,7 @@ describe("Moves - Clangorous Soul", () => { expect(leadPokemon.getStatStage(Stat.SPATK)).toBe(1); expect(leadPokemon.getStatStage(Stat.SPDEF)).toBe(1); expect(leadPokemon.getStatStage(Stat.SPD)).toBe(1); - }, TIMEOUT + } ); it("will still take effect if one or more of the involved stat stages are not at max", @@ -79,7 +79,7 @@ describe("Moves - Clangorous Soul", () => { expect(leadPokemon.getStatStage(Stat.SPATK)).toBe(6); expect(leadPokemon.getStatStage(Stat.SPDEF)).toBe(5); expect(leadPokemon.getStatStage(Stat.SPD)).toBe(1); - }, TIMEOUT + } ); it("fails if all stat stages involved are at max", @@ -103,7 +103,7 @@ describe("Moves - Clangorous Soul", () => { expect(leadPokemon.getStatStage(Stat.SPATK)).toBe(6); expect(leadPokemon.getStatStage(Stat.SPDEF)).toBe(6); expect(leadPokemon.getStatStage(Stat.SPD)).toBe(6); - }, TIMEOUT + } ); it("fails if the user's health is less than 1/3", @@ -123,6 +123,6 @@ describe("Moves - Clangorous Soul", () => { expect(leadPokemon.getStatStage(Stat.SPATK)).toBe(0); expect(leadPokemon.getStatStage(Stat.SPDEF)).toBe(0); expect(leadPokemon.getStatStage(Stat.SPD)).toBe(0); - }, TIMEOUT + } ); }); diff --git a/src/test/moves/crafty_shield.test.ts b/src/test/moves/crafty_shield.test.ts index 7b962518944..63399c3a86a 100644 --- a/src/test/moves/crafty_shield.test.ts +++ b/src/test/moves/crafty_shield.test.ts @@ -9,7 +9,7 @@ import { BattlerTagType } from "#app/enums/battler-tag-type"; import { BerryPhase } from "#app/phases/berry-phase"; import { CommandPhase } from "#app/phases/command-phase"; -const TIMEOUT = 20 * 1000; + describe("Moves - Crafty Shield", () => { let phaserGame: Phaser.Game; @@ -56,7 +56,7 @@ describe("Moves - Crafty Shield", () => { await game.phaseInterceptor.to(BerryPhase, false); leadPokemon.forEach(p => expect(p.getStatStage(Stat.ATK)).toBe(0)); - }, TIMEOUT + } ); test( @@ -77,7 +77,7 @@ describe("Moves - Crafty Shield", () => { await game.phaseInterceptor.to(BerryPhase, false); expect(leadPokemon.some(p => p.hp < p.getMaxHp())).toBeTruthy(); - }, TIMEOUT + } ); test( @@ -99,7 +99,7 @@ describe("Moves - Crafty Shield", () => { await game.phaseInterceptor.to(BerryPhase, false); leadPokemon.forEach(p => expect(p.getTag(BattlerTagType.CURSED)).toBeUndefined()); - }, TIMEOUT + } ); test( diff --git a/src/test/moves/dragon_cheer.test.ts b/src/test/moves/dragon_cheer.test.ts index 0fc389ccfb6..beaf6ddb520 100644 --- a/src/test/moves/dragon_cheer.test.ts +++ b/src/test/moves/dragon_cheer.test.ts @@ -10,8 +10,6 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vite describe("Moves - Dragon Cheer", () => { let phaserGame: Phaser.Game; let game: GameManager; - const TIMEOUT = 20 * 1000; - beforeAll(() => { phaserGame = new Phaser.Game({ type: Phaser.HEADLESS, @@ -47,7 +45,7 @@ describe("Moves - Dragon Cheer", () => { // After Tackle await game.phaseInterceptor.to("TurnEndPhase"); expect(enemy.getCritStage).toHaveReturnedWith(1); // getCritStage is called on defender - }, TIMEOUT); + }); it("increases the user's Dragon-type allies' critical hit ratio by two stages", async () => { await game.classicMode.startBattle([Species.MAGIKARP, Species.DRAGONAIR]); @@ -64,7 +62,7 @@ describe("Moves - Dragon Cheer", () => { // After Tackle await game.phaseInterceptor.to("TurnEndPhase"); expect(enemy.getCritStage).toHaveReturnedWith(2); // getCritStage is called on defender - }, TIMEOUT); + }); it("applies the effect based on the allies' type upon use of the move, and do not change if the allies' type changes later in battle", async () => { await game.classicMode.startBattle([Species.DRAGONAIR, Species.MAGIKARP]); @@ -96,5 +94,5 @@ describe("Moves - Dragon Cheer", () => { await game.phaseInterceptor.to("MoveEndPhase"); expect(enemy.getCritStage).toHaveReturnedWith(1); // getCritStage is called on defender - }, TIMEOUT); + }); }); diff --git a/src/test/moves/dragon_tail.test.ts b/src/test/moves/dragon_tail.test.ts index e1af29b2db1..4b222a0c477 100644 --- a/src/test/moves/dragon_tail.test.ts +++ b/src/test/moves/dragon_tail.test.ts @@ -10,7 +10,7 @@ import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest"; import GameManager from "../utils/gameManager"; -const TIMEOUT = 20 * 1000; + describe("Moves - Dragon Tail", () => { let phaserGame: Phaser.Game; @@ -50,12 +50,12 @@ describe("Moves - Dragon Tail", () => { await game.phaseInterceptor.to(BerryPhase); const isVisible = enemyPokemon.visible; - const hasFled = enemyPokemon.wildFlee; + const hasFled = enemyPokemon.switchOutStatus; expect(!isVisible && hasFled).toBe(true); // simply want to test that the game makes it this far without crashing await game.phaseInterceptor.to(BattleEndPhase); - }, TIMEOUT + } ); test( @@ -72,10 +72,10 @@ describe("Moves - Dragon Tail", () => { await game.phaseInterceptor.to(BerryPhase); const isVisible = enemyPokemon.visible; - const hasFled = enemyPokemon.wildFlee; + const hasFled = enemyPokemon.switchOutStatus; expect(!isVisible && hasFled).toBe(true); expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp()); - }, TIMEOUT + } ); test( @@ -97,9 +97,9 @@ describe("Moves - Dragon Tail", () => { await game.phaseInterceptor.to(TurnEndPhase); const isVisibleLead = enemyLeadPokemon.visible; - const hasFledLead = enemyLeadPokemon.wildFlee; + const hasFledLead = enemyLeadPokemon.switchOutStatus; const isVisibleSec = enemySecPokemon.visible; - const hasFledSec = enemySecPokemon.wildFlee; + const hasFledSec = enemySecPokemon.switchOutStatus; expect(!isVisibleLead && hasFledLead && isVisibleSec && !hasFledSec).toBe(true); expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp()); @@ -109,7 +109,7 @@ describe("Moves - Dragon Tail", () => { await game.phaseInterceptor.to(BerryPhase); expect(enemySecPokemon.hp).toBeLessThan(enemySecPokemon.getMaxHp()); - }, TIMEOUT + } ); test( @@ -133,14 +133,14 @@ describe("Moves - Dragon Tail", () => { await game.phaseInterceptor.to(BerryPhase); const isVisibleLead = enemyLeadPokemon.visible; - const hasFledLead = enemyLeadPokemon.wildFlee; + const hasFledLead = enemyLeadPokemon.switchOutStatus; const isVisibleSec = enemySecPokemon.visible; - const hasFledSec = enemySecPokemon.wildFlee; + const hasFledSec = enemySecPokemon.switchOutStatus; expect(!isVisibleLead && hasFledLead && !isVisibleSec && hasFledSec).toBe(true); expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp()); expect(secPokemon.hp).toBeLessThan(secPokemon.getMaxHp()); expect(enemyLeadPokemon.hp).toBeLessThan(enemyLeadPokemon.getMaxHp()); expect(enemySecPokemon.hp).toBeLessThan(enemySecPokemon.getMaxHp()); - }, TIMEOUT + } ); }); diff --git a/src/test/moves/fillet_away.test.ts b/src/test/moves/fillet_away.test.ts index 68ace42c2ec..d8dd74a259c 100644 --- a/src/test/moves/fillet_away.test.ts +++ b/src/test/moves/fillet_away.test.ts @@ -7,7 +7,7 @@ import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest"; -const TIMEOUT = 20 * 1000; + /** HP Cost of Move */ const RATIO = 2; /** Amount of extra HP lost */ @@ -53,7 +53,7 @@ describe("Moves - FILLET AWAY", () => { expect(leadPokemon.getStatStage(Stat.ATK)).toBe(2); expect(leadPokemon.getStatStage(Stat.SPATK)).toBe(2); expect(leadPokemon.getStatStage(Stat.SPD)).toBe(2); - }, TIMEOUT + } ); test("still takes effect if one or more of the involved stat stages are not at max", @@ -74,7 +74,7 @@ describe("Moves - FILLET AWAY", () => { expect(leadPokemon.getStatStage(Stat.ATK)).toBe(6); expect(leadPokemon.getStatStage(Stat.SPATK)).toBe(5); expect(leadPokemon.getStatStage(Stat.SPD)).toBe(2); - }, TIMEOUT + } ); test("fails if all stat stages involved are at max", @@ -94,7 +94,7 @@ describe("Moves - FILLET AWAY", () => { expect(leadPokemon.getStatStage(Stat.ATK)).toBe(6); expect(leadPokemon.getStatStage(Stat.SPATK)).toBe(6); expect(leadPokemon.getStatStage(Stat.SPD)).toBe(6); - }, TIMEOUT + } ); test("fails if the user's health is less than 1/2", @@ -112,6 +112,6 @@ describe("Moves - FILLET AWAY", () => { expect(leadPokemon.getStatStage(Stat.ATK)).toBe(0); expect(leadPokemon.getStatStage(Stat.SPATK)).toBe(0); expect(leadPokemon.getStatStage(Stat.SPD)).toBe(0); - }, TIMEOUT + } ); }); diff --git a/src/test/moves/focus_punch.test.ts b/src/test/moves/focus_punch.test.ts index ca80c688169..b839c228b68 100644 --- a/src/test/moves/focus_punch.test.ts +++ b/src/test/moves/focus_punch.test.ts @@ -10,7 +10,7 @@ import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; -const TIMEOUT = 20 * 1000; + describe("Moves - Focus Punch", () => { let phaserGame: Phaser.Game; @@ -61,7 +61,7 @@ describe("Moves - Focus Punch", () => { expect(enemyPokemon.hp).toBeLessThan(enemyStartingHp); expect(leadPokemon.getMoveHistory().length).toBe(1); expect(leadPokemon.turnData.damageDealt).toBe(enemyStartingHp - enemyPokemon.hp); - }, TIMEOUT + } ); it( @@ -88,7 +88,7 @@ describe("Moves - Focus Punch", () => { expect(enemyPokemon.hp).toBe(enemyStartingHp); expect(leadPokemon.getMoveHistory().length).toBe(1); expect(leadPokemon.turnData.damageDealt).toBe(0); - }, TIMEOUT + } ); it( @@ -111,7 +111,7 @@ describe("Moves - Focus Punch", () => { expect(leadPokemon.getMoveHistory().length).toBe(1); expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); - }, TIMEOUT + } ); it( @@ -129,6 +129,6 @@ describe("Moves - Focus Punch", () => { expect(game.scene.getCurrentPhase() instanceof SwitchSummonPhase).toBeTruthy(); expect(game.scene.phaseQueue.find(phase => phase instanceof MoveHeaderPhase)).toBeDefined(); - }, TIMEOUT + } ); }); diff --git a/src/test/moves/follow_me.test.ts b/src/test/moves/follow_me.test.ts index 7d0c4fdb546..28fb1045a8c 100644 --- a/src/test/moves/follow_me.test.ts +++ b/src/test/moves/follow_me.test.ts @@ -8,7 +8,7 @@ import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest"; -const TIMEOUT = 20 * 1000; + describe("Moves - Follow Me", () => { let phaserGame: Phaser.Game; @@ -54,7 +54,7 @@ describe("Moves - Follow Me", () => { expect(playerPokemon[0].hp).toBeLessThan(playerPokemon[0].getMaxHp()); expect(playerPokemon[1].hp).toBe(playerPokemon[1].getMaxHp()); - }, TIMEOUT + } ); test( @@ -77,7 +77,7 @@ describe("Moves - Follow Me", () => { expect(playerPokemon[1].hp).toBeLessThan(playerPokemon[1].getMaxHp()); expect(playerPokemon[0].hp).toBe(playerPokemon[0].getMaxHp()); - }, TIMEOUT + } ); test( @@ -102,7 +102,7 @@ describe("Moves - Follow Me", () => { // If redirection was bypassed, both enemies should be damaged expect(enemyPokemon[0].hp).toBeLessThan(enemyPokemon[0].getMaxHp()); expect(enemyPokemon[1].hp).toBeLessThan(enemyPokemon[1].getMaxHp()); - }, TIMEOUT + } ); test( @@ -125,6 +125,6 @@ describe("Moves - Follow Me", () => { // If redirection was bypassed, both enemies should be damaged expect(enemyPokemon[0].hp).toBeLessThan(enemyPokemon[0].getMaxHp()); expect(enemyPokemon[1].hp).toBeLessThan(enemyPokemon[1].getMaxHp()); - }, TIMEOUT + } ); }); diff --git a/src/test/moves/freeze_dry.test.ts b/src/test/moves/freeze_dry.test.ts index ff9e2f07162..b901f04e6a1 100644 --- a/src/test/moves/freeze_dry.test.ts +++ b/src/test/moves/freeze_dry.test.ts @@ -9,8 +9,6 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vite describe("Moves - Freeze-Dry", () => { let phaserGame: Phaser.Game; let game: GameManager; - const TIMEOUT = 20 * 1000; - beforeAll(() => { phaserGame = new Phaser.Game({ type: Phaser.HEADLESS, @@ -44,7 +42,7 @@ describe("Moves - Freeze-Dry", () => { await game.phaseInterceptor.to("MoveEffectPhase"); expect(enemy.getMoveEffectiveness).toHaveReturnedWith(2); - }, TIMEOUT); + }); it("should deal 4x damage to water/flying types", async () => { game.override.enemySpecies(Species.WINGULL); @@ -58,7 +56,7 @@ describe("Moves - Freeze-Dry", () => { await game.phaseInterceptor.to("MoveEffectPhase"); expect(enemy.getMoveEffectiveness).toHaveReturnedWith(4); - }, TIMEOUT); + }); it("should deal 1x damage to water/fire types", async () => { game.override.enemySpecies(Species.VOLCANION); @@ -72,7 +70,7 @@ describe("Moves - Freeze-Dry", () => { await game.phaseInterceptor.to("MoveEffectPhase"); expect(enemy.getMoveEffectiveness).toHaveReturnedWith(1); - }, TIMEOUT); + }); // enable if this is ever fixed (lol) it.todo("should deal 2x damage to water types under Normalize", async () => { @@ -87,7 +85,7 @@ describe("Moves - Freeze-Dry", () => { await game.phaseInterceptor.to("MoveEffectPhase"); expect(enemy.getMoveEffectiveness).toHaveReturnedWith(2); - }, TIMEOUT); + }); // enable once Electrify is implemented (and the interaction is fixed, as above) it.todo("should deal 2x damage to water types under Electrify", async () => { @@ -102,5 +100,5 @@ describe("Moves - Freeze-Dry", () => { await game.phaseInterceptor.to("BerryPhase"); expect(enemy.getMoveEffectiveness).toHaveReturnedWith(2); - }, TIMEOUT); + }); }); diff --git a/src/test/moves/gastro_acid.test.ts b/src/test/moves/gastro_acid.test.ts index cfc458a908f..60b2bd80c05 100644 --- a/src/test/moves/gastro_acid.test.ts +++ b/src/test/moves/gastro_acid.test.ts @@ -6,7 +6,7 @@ import { MoveResult } from "#app/field/pokemon"; import GameManager from "#test/utils/gameManager"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; -const TIMEOUT = 20 * 1000; + describe("Moves - Gastro Acid", () => { let phaserGame: Phaser.Game; @@ -60,7 +60,7 @@ describe("Moves - Gastro Acid", () => { expect(enemyField[0].hp).toBeLessThan(enemyField[0].getMaxHp()); expect(enemyField[1].isFullHp()).toBe(true); - }, TIMEOUT); + }); it("fails if used on an enemy with an already-suppressed ability", async () => { game.override.battleType(null); @@ -78,5 +78,5 @@ describe("Moves - Gastro Acid", () => { await game.phaseInterceptor.to("TurnInitPhase"); expect(game.scene.getPlayerPokemon()!.getLastXMoves()[0].result).toBe(MoveResult.FAIL); - }, TIMEOUT); + }); }); diff --git a/src/test/moves/glaive_rush.test.ts b/src/test/moves/glaive_rush.test.ts index 9eed6868432..1a524b4aef6 100644 --- a/src/test/moves/glaive_rush.test.ts +++ b/src/test/moves/glaive_rush.test.ts @@ -6,7 +6,7 @@ import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; -const TIMEOUT = 20 * 1000; + describe("Moves - Glaive Rush", () => { let phaserGame: Phaser.Game; @@ -49,7 +49,7 @@ describe("Moves - Glaive Rush", () => { await game.phaseInterceptor.to("DamagePhase"); expect(enemy.hp).toBeLessThanOrEqual(1001 - (damageDealt * 3)); - }, TIMEOUT); + }); it("always gets hit by attacks", async () => { await game.classicMode.startBattle(); @@ -62,7 +62,7 @@ describe("Moves - Glaive Rush", () => { await game.phaseInterceptor.to("TurnEndPhase"); expect(enemy.hp).toBeLessThan(1000); - }, TIMEOUT); + }); it("interacts properly with multi-lens", async () => { game.override @@ -85,7 +85,7 @@ describe("Moves - Glaive Rush", () => { await game.phaseInterceptor.to("TurnEndPhase"); expect(player.hp).toBe(1000); - }, TIMEOUT); + }); it("secondary effects only last until next move", async () => { game.override.enemyMoveset([Moves.SHADOW_SNEAK]); @@ -111,7 +111,7 @@ describe("Moves - Glaive Rush", () => { await game.phaseInterceptor.to("TurnEndPhase"); expect(player.hp).toBe(damagedHp); - }, TIMEOUT); + }); it("secondary effects are removed upon switching", async () => { game.override @@ -135,7 +135,7 @@ describe("Moves - Glaive Rush", () => { await game.phaseInterceptor.to("TurnEndPhase"); expect(player.hp).toBe(player.getMaxHp()); - }, TIMEOUT); + }); it("secondary effects don't activate if move fails", async () => { game.override.moveset([Moves.SHADOW_SNEAK, Moves.PROTECT, Moves.SPLASH, Moves.GLAIVE_RUSH]); @@ -161,5 +161,5 @@ describe("Moves - Glaive Rush", () => { const damagedHP2 = 1000 - enemy.hp; expect(damagedHP2).toBeGreaterThanOrEqual((damagedHP1 * 2) - 1); - }, TIMEOUT); + }); }); diff --git a/src/test/moves/guard_swap.test.ts b/src/test/moves/guard_swap.test.ts index a27afaaa7ba..99769b32899 100644 --- a/src/test/moves/guard_swap.test.ts +++ b/src/test/moves/guard_swap.test.ts @@ -1,17 +1,16 @@ -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import Phaser from "phaser"; import GameManager from "#app/test/utils/gameManager"; import { Species } from "#enums/species"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { Moves } from "#enums/moves"; -import { Stat } from "#enums/stat"; +import { Stat, BATTLE_STATS } from "#enums/stat"; import { Abilities } from "#enums/abilities"; import { MoveEndPhase } from "#app/phases/move-end-phase"; describe("Moves - Guard Swap", () => { let phaserGame: Phaser.Game; let game: GameManager; - beforeAll(() => { phaserGame = new Phaser.Game({ type: Phaser.HEADLESS, @@ -27,37 +26,42 @@ describe("Moves - Guard Swap", () => { game.override .battleType("single") .enemyAbility(Abilities.BALL_FETCH) - .enemyMoveset([Moves.SHELL_SMASH]) - .enemySpecies(Species.MEW) + .enemyMoveset(Moves.SPLASH) + .enemySpecies(Species.INDEEDEE) .enemyLevel(200) .moveset([ Moves.GUARD_SWAP ]) .ability(Abilities.NONE); }); - it("should swap the user's DEF AND SPDEF stat stages with the target's", async () => { - await game.startBattle([ + it("should swap the user's DEF and SPDEF stat stages with the target's", async () => { + await game.classicMode.startBattle([ Species.INDEEDEE ]); - // Should start with no stat stages const player = game.scene.getPlayerPokemon()!; - // After Shell Smash, should have +2 in ATK and SPATK, -1 in DEF and SPDEF const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy.summonData, "statStages", "get").mockReturnValue(new Array(BATTLE_STATS.length).fill(1)); + game.move.select(Moves.GUARD_SWAP); await game.phaseInterceptor.to(MoveEndPhase); - expect(player.getStatStage(Stat.DEF)).toBe(0); - expect(player.getStatStage(Stat.SPDEF)).toBe(0); - expect(enemy.getStatStage(Stat.DEF)).toBe(-1); - expect(enemy.getStatStage(Stat.SPDEF)).toBe(-1); + for (const s of BATTLE_STATS) { + expect(player.getStatStage(s)).toBe(0); + expect(enemy.getStatStage(s)).toBe(1); + } await game.phaseInterceptor.to(TurnEndPhase); - expect(player.getStatStage(Stat.DEF)).toBe(-1); - expect(player.getStatStage(Stat.SPDEF)).toBe(-1); - expect(enemy.getStatStage(Stat.DEF)).toBe(0); - expect(enemy.getStatStage(Stat.SPDEF)).toBe(0); - }, 20000); + for (const s of BATTLE_STATS) { + if (s === Stat.DEF || s === Stat.SPDEF) { + expect(player.getStatStage(s)).toBe(1); + expect(enemy.getStatStage(s)).toBe(0); + } else { + expect(player.getStatStage(s)).toBe(0); + expect(enemy.getStatStage(s)).toBe(1); + } + } + }); }); diff --git a/src/test/moves/haze.test.ts b/src/test/moves/haze.test.ts index 211c1a41409..e5474801899 100644 --- a/src/test/moves/haze.test.ts +++ b/src/test/moves/haze.test.ts @@ -35,7 +35,7 @@ describe("Moves - Haze", () => { game.override.ability(Abilities.NONE); }); - it("should reset all stat changes of all Pokemon on field", { timeout: 10000 }, async () => { + it("should reset all stat changes of all Pokemon on field", async () => { await game.startBattle([Species.RATTATA]); const user = game.scene.getPlayerPokemon()!; const enemy = game.scene.getEnemyPokemon()!; diff --git a/src/test/moves/heal_block.test.ts b/src/test/moves/heal_block.test.ts new file mode 100644 index 00000000000..8549d6211a9 --- /dev/null +++ b/src/test/moves/heal_block.test.ts @@ -0,0 +1,153 @@ +import { BattlerIndex } from "#app/battle"; +import { ArenaTagSide } from "#app/data/arena-tag"; +import { WeatherType } from "#app/data/weather"; +import GameManager from "#app/test/utils/gameManager"; +import { Abilities } from "#enums/abilities"; +import { ArenaTagType } from "#enums/arena-tag-type"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +const TIMEOUT = 20 * 1000; + +// Bulbapedia Reference: https://bulbapedia.bulbagarden.net/wiki/Heal_Block_(move) +describe("Moves - Heal Block", () => { + 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.ABSORB, Moves.WISH, Moves.SPLASH, Moves.AQUA_RING]) + .enemyMoveset(Moves.HEAL_BLOCK) + .ability(Abilities.NO_GUARD) + .enemyAbility(Abilities.BALL_FETCH) + .enemySpecies(Species.BLISSEY) + .disableCrits(); + }); + + it("shouldn't stop damage from HP-drain attacks, just HP restoration", async() => { + + await game.classicMode.startBattle([Species.CHARIZARD]); + + const player = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; + + player.damageAndUpdate(enemy.getMaxHp() - 1); + + game.move.select(Moves.ABSORB); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(player.hp).toBe(1); + expect(enemy.hp).toBeLessThan(enemy.getMaxHp()); + }, TIMEOUT + ); + + it("shouldn't stop Liquid Ooze from dealing damage", async() => { + game.override.enemyAbility(Abilities.LIQUID_OOZE); + + await game.classicMode.startBattle([Species.CHARIZARD]); + + const player = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.ABSORB); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(player.isFullHp()).toBe(false); + expect(enemy.isFullHp()).toBe(false); + }, TIMEOUT); + + it("should stop delayed heals, such as from Wish", async() => { + await game.classicMode.startBattle([Species.CHARIZARD]); + + const player = game.scene.getPlayerPokemon()!; + + player.damageAndUpdate(player.getMaxHp() - 1); + + game.move.select(Moves.WISH); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(game.scene.arena.getTagOnSide(ArenaTagType.WISH, ArenaTagSide.PLAYER)).toBeDefined(); + while (game.scene.arena.getTagOnSide(ArenaTagType.WISH, ArenaTagSide.PLAYER)) { + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("TurnEndPhase"); + } + + expect(player.hp).toBe(1); + }, TIMEOUT); + + it("should prevent Grassy Terrain from restoring HP", async() => { + game.override.enemyAbility(Abilities.GRASSY_SURGE); + + await game.classicMode.startBattle([Species.CHARIZARD]); + + const player = game.scene.getPlayerPokemon()!; + + player.damageAndUpdate(player.getMaxHp() - 1); + + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(player.hp).toBe(1); + }, TIMEOUT); + + it("should prevent healing from heal-over-time moves", async() => { + await game.classicMode.startBattle([Species.CHARIZARD]); + + const player = game.scene.getPlayerPokemon()!; + + player.damageAndUpdate(player.getMaxHp() - 1); + + game.move.select(Moves.AQUA_RING); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(player.getTag(BattlerTagType.AQUA_RING)).toBeDefined(); + expect(player.hp).toBe(1); + }, TIMEOUT); + + it("should prevent abilities from restoring HP", async() => { + game.override + .weather(WeatherType.RAIN) + .ability(Abilities.RAIN_DISH); + + await game.classicMode.startBattle([Species.CHARIZARD]); + + const player = game.scene.getPlayerPokemon()!; + + player.damageAndUpdate(player.getMaxHp() - 1); + + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(player.hp).toBe(1); + }, TIMEOUT); + + it("should stop healing from items", async() => { + game.override.startingHeldItems([{name: "LEFTOVERS"}]); + + await game.classicMode.startBattle([Species.CHARIZARD]); + + const player = game.scene.getPlayerPokemon()!; + player.damageAndUpdate(player.getMaxHp() - 1); + + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(player.hp).toBe(1); + }, TIMEOUT); +}); diff --git a/src/test/moves/heart_swap.test.ts b/src/test/moves/heart_swap.test.ts new file mode 100644 index 00000000000..a128549c459 --- /dev/null +++ b/src/test/moves/heart_swap.test.ts @@ -0,0 +1,62 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import Phaser from "phaser"; +import GameManager from "#app/test/utils/gameManager"; +import { Species } from "#enums/species"; +import { TurnEndPhase } from "#app/phases/turn-end-phase"; +import { Moves } from "#enums/moves"; +import { BATTLE_STATS } from "#enums/stat"; +import { Abilities } from "#enums/abilities"; +import { MoveEndPhase } from "#app/phases/move-end-phase"; + +describe("Moves - Heart Swap", () => { + 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 + .battleType("single") + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH) + .enemySpecies(Species.INDEEDEE) + .enemyLevel(200) + .moveset([ Moves.HEART_SWAP ]) + .ability(Abilities.NONE); + }); + + it("should swap all of the user's stat stages with the target's", async () => { + await game.classicMode.startBattle([ + Species.MANAPHY + ]); + + const player = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; + + vi.spyOn(enemy.summonData, "statStages", "get").mockReturnValue(new Array(BATTLE_STATS.length).fill(1)); + + game.move.select(Moves.HEART_SWAP); + + await game.phaseInterceptor.to(MoveEndPhase); + + for (const s of BATTLE_STATS) { + expect(player.getStatStage(s)).toBe(0); + expect(enemy.getStatStage(s)).toBe(1); + } + + await game.phaseInterceptor.to(TurnEndPhase); + + for (const s of BATTLE_STATS) { + expect(enemy.getStatStage(s)).toBe(0); + expect(player.getStatStage(s)).toBe(1); + } + }); +}); diff --git a/src/test/moves/hyper_beam.test.ts b/src/test/moves/hyper_beam.test.ts index 7aa2dbfec2b..a6a471569ed 100644 --- a/src/test/moves/hyper_beam.test.ts +++ b/src/test/moves/hyper_beam.test.ts @@ -9,8 +9,6 @@ import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -const TIMEOUT = 20 * 1000; // 20 sec timeout for all tests - describe("Moves - Hyper Beam", () => { let phaserGame: Phaser.Game; let game: GameManager; @@ -67,6 +65,6 @@ describe("Moves - Hyper Beam", () => { await game.phaseInterceptor.to(BerryPhase, false); expect(enemyPokemon.hp).toBeLessThan(enemyPostAttackHp); - }, TIMEOUT + } ); }); diff --git a/src/test/moves/jaw_lock.test.ts b/src/test/moves/jaw_lock.test.ts index 75fd6f0ff32..3398ec00b3b 100644 --- a/src/test/moves/jaw_lock.test.ts +++ b/src/test/moves/jaw_lock.test.ts @@ -11,7 +11,7 @@ import { Species } from "#enums/species"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; -const TIMEOUT = 20 * 1000; + describe("Moves - Jaw Lock", () => { let phaserGame: Phaser.Game; @@ -61,7 +61,7 @@ describe("Moves - Jaw Lock", () => { expect(leadPokemon.getTag(BattlerTagType.TRAPPED)).toBeDefined(); expect(enemyPokemon.getTag(BattlerTagType.TRAPPED)).toBeDefined(); - }, TIMEOUT + } ); it( @@ -90,7 +90,7 @@ describe("Moves - Jaw Lock", () => { expect(leadPokemon.getTag(BattlerTagType.TRAPPED)).toBeUndefined(); expect(enemyPokemon.getTag(BattlerTagType.TRAPPED)).toBeUndefined(); - }, TIMEOUT + } ); it( @@ -114,7 +114,7 @@ describe("Moves - Jaw Lock", () => { await game.doKillOpponents(); expect(leadPokemon.getTag(BattlerTagType.TRAPPED)).toBeUndefined(); - }, TIMEOUT + } ); it( @@ -146,7 +146,7 @@ describe("Moves - Jaw Lock", () => { expect(enemyPokemon[1].getTag(BattlerTagType.TRAPPED)).toBeUndefined(); expect(playerPokemon[0].getTag(BattlerTagType.TRAPPED)).toBeDefined(); expect(playerPokemon[0].getTag(BattlerTagType.TRAPPED)?.sourceId).toBe(enemyPokemon[0].id); - }, TIMEOUT + } ); it( @@ -165,6 +165,6 @@ describe("Moves - Jaw Lock", () => { expect(playerPokemon.getTag(BattlerTagType.TRAPPED)).toBeUndefined(); expect(enemyPokemon.getTag(BattlerTagType.TRAPPED)).toBeUndefined(); - }, TIMEOUT + } ); }); diff --git a/src/test/moves/lash_out.test.ts b/src/test/moves/lash_out.test.ts index 8c414832f36..7a8ab6c5bb6 100644 --- a/src/test/moves/lash_out.test.ts +++ b/src/test/moves/lash_out.test.ts @@ -7,7 +7,7 @@ import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -const TIMEOUT = 20 * 1000; + describe("Moves - Lash Out", () => { let phaserGame: Phaser.Game; @@ -48,5 +48,5 @@ describe("Moves - Lash Out", () => { await game.phaseInterceptor.to("BerryPhase"); expect(allMoves[Moves.LASH_OUT].calculateBattlePower).toHaveReturnedWith(150); - }, TIMEOUT); + }); }); diff --git a/src/test/moves/lucky_chant.test.ts b/src/test/moves/lucky_chant.test.ts index 57e5ff80f1d..77ea751aee1 100644 --- a/src/test/moves/lucky_chant.test.ts +++ b/src/test/moves/lucky_chant.test.ts @@ -7,7 +7,7 @@ import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import GameManager from "../utils/gameManager"; -const TIMEOUT = 20 * 1000; + describe("Moves - Lucky Chant", () => { let phaserGame: Phaser.Game; @@ -55,7 +55,7 @@ describe("Moves - Lucky Chant", () => { const secondTurnDamage = playerPokemon.getMaxHp() - playerPokemon.hp - firstTurnDamage; expect(secondTurnDamage).toBeLessThan(firstTurnDamage); - }, TIMEOUT + } ); it( @@ -81,7 +81,7 @@ describe("Moves - Lucky Chant", () => { const secondTurnDamage = playerPokemon[0].getMaxHp() - playerPokemon[0].hp - firstTurnDamage; expect(secondTurnDamage).toBeLessThan(firstTurnDamage); - }, TIMEOUT + } ); it( @@ -108,6 +108,6 @@ describe("Moves - Lucky Chant", () => { const secondTurnDamage = playerPokemon.getMaxHp() - playerPokemon.hp - firstTurnDamage; expect(secondTurnDamage).toBeLessThan(firstTurnDamage); - }, TIMEOUT + } ); }); diff --git a/src/test/moves/make_it_rain.test.ts b/src/test/moves/make_it_rain.test.ts index 5ac35168f92..2b28a958ff0 100644 --- a/src/test/moves/make_it_rain.test.ts +++ b/src/test/moves/make_it_rain.test.ts @@ -8,7 +8,7 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { MoveEndPhase } from "#app/phases/move-end-phase"; import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; -const TIMEOUT = 20 * 1000; + describe("Moves - Make It Rain", () => { let phaserGame: Phaser.Game; @@ -46,7 +46,7 @@ describe("Moves - Make It Rain", () => { await game.phaseInterceptor.to(MoveEndPhase); expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(-1); - }, TIMEOUT); + }); it("should apply effects even if the target faints", async () => { game.override.enemyLevel(1); // ensures the enemy will faint @@ -63,7 +63,7 @@ describe("Moves - Make It Rain", () => { expect(enemyPokemon.isFainted()).toBe(true); expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(-1); - }, TIMEOUT); + }); it("should reduce Sp. Atk. once after KOing two enemies", async () => { game.override.enemyLevel(1); // ensures the enemy will faint @@ -80,7 +80,7 @@ describe("Moves - Make It Rain", () => { enemyPokemon.forEach(p => expect(p.isFainted()).toBe(true)); expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(-1); - }, TIMEOUT); + }); it("should lower SPATK stat stage by 1 if it only hits the second target", async () => { await game.startBattle([Species.CHARIZARD, Species.BLASTOISE]); @@ -96,5 +96,5 @@ describe("Moves - Make It Rain", () => { await game.phaseInterceptor.to(MoveEndPhase); expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(-1); - }, TIMEOUT); + }); }); diff --git a/src/test/moves/mat_block.test.ts b/src/test/moves/mat_block.test.ts index b759f49bf98..0746f9bcfa9 100644 --- a/src/test/moves/mat_block.test.ts +++ b/src/test/moves/mat_block.test.ts @@ -9,7 +9,7 @@ import { BerryPhase } from "#app/phases/berry-phase"; import { CommandPhase } from "#app/phases/command-phase"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; -const TIMEOUT = 20 * 1000; + describe("Moves - Mat Block", () => { let phaserGame: Phaser.Game; @@ -56,7 +56,7 @@ describe("Moves - Mat Block", () => { await game.phaseInterceptor.to(BerryPhase, false); leadPokemon.forEach(p => expect(p.hp).toBe(p.getMaxHp())); - }, TIMEOUT + } ); test( @@ -77,7 +77,7 @@ describe("Moves - Mat Block", () => { await game.phaseInterceptor.to(BerryPhase, false); leadPokemon.forEach(p => expect(p.getStatStage(Stat.ATK)).toBe(-2)); - }, TIMEOUT + } ); test( @@ -103,6 +103,6 @@ describe("Moves - Mat Block", () => { await game.phaseInterceptor.to(BerryPhase, false); expect(leadPokemon.some((p, i) => p.hp < leadStartingHp[i])).toBeTruthy(); - }, TIMEOUT + } ); }); diff --git a/src/test/moves/multi_target.test.ts b/src/test/moves/multi_target.test.ts index 5e830f23fc7..cd69482bd8e 100644 --- a/src/test/moves/multi_target.test.ts +++ b/src/test/moves/multi_target.test.ts @@ -7,7 +7,7 @@ import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; -const TIMEOUT = 20 * 1000; + describe("Multi-target damage reduction", () => { let phaserGame: Phaser.Game; @@ -75,7 +75,7 @@ describe("Multi-target damage reduction", () => { // Moves that target all enemies get reduced if there's more than one enemy expect(gleam1).toBeLessThanOrEqual(Utils.toDmgValue(gleam2 * 0.75) + 1); expect(gleam1).toBeGreaterThanOrEqual(Utils.toDmgValue(gleam2 * 0.75) - 1); - }, TIMEOUT); + }); it("should reduce earthquake when more than one pokemon other than user is not fainted", async () => { await game.startBattle([Species.MAGIKARP, Species.FEEBAS]); @@ -126,5 +126,5 @@ describe("Multi-target damage reduction", () => { // Turn 3: 1 target, should be no damage reduction expect(damageEnemy1Turn1).toBeLessThanOrEqual(Utils.toDmgValue(damageEnemy1Turn3 * 0.75) + 1); expect(damageEnemy1Turn1).toBeGreaterThanOrEqual(Utils.toDmgValue(damageEnemy1Turn3 * 0.75) - 1); - }, TIMEOUT); + }); }); diff --git a/src/test/moves/obstruct.test.ts b/src/test/moves/obstruct.test.ts index 539b11090de..43706a5a1d6 100644 --- a/src/test/moves/obstruct.test.ts +++ b/src/test/moves/obstruct.test.ts @@ -8,8 +8,6 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; describe("Moves - Obstruct", () => { let phaserGame: Phaser.Game; let game: GameManager; - const TIMEOUT = 20 * 1000; - beforeAll(() => { phaserGame = new Phaser.Game({ type: Phaser.HEADLESS, @@ -41,7 +39,24 @@ describe("Moves - Obstruct", () => { expect(player.isFullHp()).toBe(true); expect(enemy.getStatStage(Stat.DEF)).toBe(-2); - }, TIMEOUT); + }); + + it("bypasses accuracy checks when applying protection and defense reduction", async () => { + game.override.enemyMoveset(Array(4).fill(Moves.ICE_PUNCH)); + await game.classicMode.startBattle(); + + game.move.select(Moves.OBSTRUCT); + await game.phaseInterceptor.to("MoveEffectPhase"); + await game.move.forceMiss(); + + const player = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(player.isFullHp()).toBe(true); + expect(enemy.getStatStage(Stat.DEF)).toBe(-2); + } + ); it("protects from non-contact damaging moves and doesn't lower the opponent's defense by 2 stages", async () => { game.override.enemyMoveset(Array(4).fill(Moves.WATER_GUN)); @@ -55,7 +70,7 @@ describe("Moves - Obstruct", () => { expect(player.isFullHp()).toBe(true); expect(enemy.getStatStage(Stat.DEF)).toBe(0); - }, TIMEOUT); + }); it("doesn't protect from status moves", async () => { game.override.enemyMoveset(Array(4).fill(Moves.GROWL)); @@ -67,5 +82,5 @@ describe("Moves - Obstruct", () => { const player = game.scene.getPlayerPokemon()!; expect(player.getStatStage(Stat.ATK)).toBe(-1); - }, TIMEOUT); + }); }); diff --git a/src/test/moves/octolock.test.ts b/src/test/moves/octolock.test.ts index 7618b08e9fc..d80b71a51e1 100644 --- a/src/test/moves/octolock.test.ts +++ b/src/test/moves/octolock.test.ts @@ -36,7 +36,7 @@ describe("Moves - Octolock", () => { .ability(Abilities.BALL_FETCH); }); - it("lowers DEF and SPDEF stat stages of the target Pokemon by 1 each turn", { timeout: 10000 }, async () => { + it("lowers DEF and SPDEF stat stages of the target Pokemon by 1 each turn", async () => { await game.classicMode.startBattle([ Species.GRAPPLOCT ]); const enemyPokemon = game.scene.getEnemyPokemon()!; @@ -57,7 +57,7 @@ describe("Moves - Octolock", () => { expect(enemyPokemon.getStatStage(Stat.SPDEF)).toBe(-2); }); - it("if target pokemon has BIG_PECKS, should only lower SPDEF stat stage by 1", { timeout: 10000 }, async () => { + it("if target pokemon has BIG_PECKS, should only lower SPDEF stat stage by 1", async () => { game.override.enemyAbility(Abilities.BIG_PECKS); await game.classicMode.startBattle([ Species.GRAPPLOCT ]); @@ -71,7 +71,7 @@ describe("Moves - Octolock", () => { expect(enemyPokemon.getStatStage(Stat.SPDEF)).toBe(-1); }); - it("if target pokemon has WHITE_SMOKE, should not reduce any stat stages", { timeout: 10000 }, async () => { + it("if target pokemon has WHITE_SMOKE, should not reduce any stat stages", async () => { game.override.enemyAbility(Abilities.WHITE_SMOKE); await game.classicMode.startBattle([ Species.GRAPPLOCT ]); @@ -85,7 +85,7 @@ describe("Moves - Octolock", () => { expect(enemyPokemon.getStatStage(Stat.SPDEF)).toBe(0); }); - it("if target pokemon has CLEAR_BODY, should not reduce any stat stages", { timeout: 10000 }, async () => { + it("if target pokemon has CLEAR_BODY, should not reduce any stat stages", async () => { game.override.enemyAbility(Abilities.CLEAR_BODY); await game.classicMode.startBattle([ Species.GRAPPLOCT ]); @@ -99,7 +99,7 @@ describe("Moves - Octolock", () => { expect(enemyPokemon.getStatStage(Stat.SPDEF)).toBe(0); }); - it("traps the target pokemon", { timeout: 10000 }, async () => { + it("traps the target pokemon", async () => { await game.classicMode.startBattle([ Species.GRAPPLOCT ]); const enemyPokemon = game.scene.getEnemyPokemon()!; diff --git a/src/test/moves/parting_shot.test.ts b/src/test/moves/parting_shot.test.ts index 52cfaf98111..fa328e15a32 100644 --- a/src/test/moves/parting_shot.test.ts +++ b/src/test/moves/parting_shot.test.ts @@ -10,7 +10,7 @@ import { FaintPhase } from "#app/phases/faint-phase"; import { MessagePhase } from "#app/phases/message-phase"; import { TurnInitPhase } from "#app/phases/turn-init-phase"; -const TIMEOUT = 20 * 1000; + describe("Moves - Parting Shot", () => { let phaserGame: Phaser.Game; @@ -53,7 +53,7 @@ describe("Moves - Parting Shot", () => { expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0); expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(0); expect(game.scene.getPlayerField()[0].species.speciesId).toBe(Species.MURKROW); - }, TIMEOUT + } ); test( @@ -73,7 +73,7 @@ describe("Moves - Parting Shot", () => { expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0); expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(0); expect(game.scene.getPlayerField()[0].species.speciesId).toBe(Species.MURKROW); - }, TIMEOUT + } ); it.skip( // TODO: fix this bug to pass the test! @@ -115,7 +115,7 @@ describe("Moves - Parting Shot", () => { expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-6); expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(-6); expect(game.scene.getPlayerField()[0].species.speciesId).toBe(Species.MURKROW); - }, TIMEOUT + } ); it.skip( // TODO: fix this bug to pass the test! @@ -136,7 +136,7 @@ describe("Moves - Parting Shot", () => { expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0); expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(0); expect(game.scene.getPlayerField()[0].species.speciesId).toBe(Species.MURKROW); - }, TIMEOUT + } ); it.skip( // TODO: fix this bug to pass the test! @@ -156,7 +156,7 @@ describe("Moves - Parting Shot", () => { expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0); expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(0); expect(game.scene.getPlayerField()[0].species.speciesId).toBe(Species.MURKROW); - }, TIMEOUT + } ); it.skip( // TODO: fix this bug to pass the test! @@ -173,7 +173,7 @@ describe("Moves - Parting Shot", () => { expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-1); expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(-1); expect(game.scene.getPlayerField()[0].species.speciesId).toBe(Species.MURKROW); - }, TIMEOUT + } ); it.skip( // TODO: fix this bug to pass the test! @@ -196,6 +196,6 @@ describe("Moves - Parting Shot", () => { expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0); expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(0); expect(game.scene.getPlayerField()[0].species.speciesId).toBe(Species.MEOWTH); - }, TIMEOUT + } ); }); diff --git a/src/test/moves/power_shift.test.ts b/src/test/moves/power_shift.test.ts index 3fda315193e..f39759f278b 100644 --- a/src/test/moves/power_shift.test.ts +++ b/src/test/moves/power_shift.test.ts @@ -9,8 +9,6 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; describe("Moves - Power Shift", () => { let phaserGame: Phaser.Game; let game: GameManager; - const TIMEOUT = 20 * 1000; - beforeAll(() => { phaserGame = new Phaser.Game({ type: Phaser.HEADLESS, @@ -59,5 +57,5 @@ describe("Moves - Power Shift", () => { // Raw stats are swapped expect(playerPokemon.getStat(Stat.ATK, false)).toBe(20); expect(playerPokemon.getStat(Stat.DEF, false)).toBe(10); - }, TIMEOUT); + }); }); diff --git a/src/test/moves/power_swap.test.ts b/src/test/moves/power_swap.test.ts index a3d4bfca19a..e9a4b569c92 100644 --- a/src/test/moves/power_swap.test.ts +++ b/src/test/moves/power_swap.test.ts @@ -1,17 +1,16 @@ -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import Phaser from "phaser"; import GameManager from "#app/test/utils/gameManager"; import { Species } from "#enums/species"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { Moves } from "#enums/moves"; -import { Stat } from "#enums/stat"; +import { Stat, BATTLE_STATS } from "#enums/stat"; import { Abilities } from "#enums/abilities"; import { MoveEndPhase } from "#app/phases/move-end-phase"; describe("Moves - Power Swap", () => { let phaserGame: Phaser.Game; let game: GameManager; - beforeAll(() => { phaserGame = new Phaser.Game({ type: Phaser.HEADLESS, @@ -27,36 +26,42 @@ describe("Moves - Power Swap", () => { game.override .battleType("single") .enemyAbility(Abilities.BALL_FETCH) - .enemyMoveset([Moves.SHELL_SMASH]) - .enemySpecies(Species.MEW) + .enemyMoveset(Moves.SPLASH) + .enemySpecies(Species.INDEEDEE) .enemyLevel(200) .moveset([ Moves.POWER_SWAP ]) .ability(Abilities.NONE); }); - it("should swap the user's ATK AND SPATK stat stages with the target's", async () => { - await game.startBattle([ + it("should swap the user's ATK and SPATK stat stages with the target's", async () => { + await game.classicMode.startBattle([ Species.INDEEDEE ]); - // Should start with no stat stages const player = game.scene.getPlayerPokemon()!; - // After Shell Smash, should have +2 in ATK and SPATK, -1 in DEF and SPDEF const enemy = game.scene.getEnemyPokemon()!; + + vi.spyOn(enemy.summonData, "statStages", "get").mockReturnValue(new Array(BATTLE_STATS.length).fill(1)); + game.move.select(Moves.POWER_SWAP); await game.phaseInterceptor.to(MoveEndPhase); - expect(player.getStatStage(Stat.ATK)).toBe(0); - expect(player.getStatStage(Stat.SPATK)).toBe(0); - expect(enemy.getStatStage(Stat.ATK)).toBe(2); - expect(enemy.getStatStage(Stat.SPATK)).toBe(2); + for (const s of BATTLE_STATS) { + expect(player.getStatStage(s)).toBe(0); + expect(enemy.getStatStage(s)).toBe(1); + } await game.phaseInterceptor.to(TurnEndPhase); - expect(player.getStatStage(Stat.ATK)).toBe(2); - expect(player.getStatStage(Stat.SPATK)).toBe(2); - expect(enemy.getStatStage(Stat.ATK)).toBe(0); - expect(enemy.getStatStage(Stat.SPATK)).toBe(0); - }, 20000); + for (const s of BATTLE_STATS) { + if (s === Stat.ATK || s === Stat.SPATK) { + expect(player.getStatStage(s)).toBe(1); + expect(enemy.getStatStage(s)).toBe(0); + } else { + expect(player.getStatStage(s)).toBe(0); + expect(enemy.getStatStage(s)).toBe(1); + } + } + }); }); diff --git a/src/test/moves/protect.test.ts b/src/test/moves/protect.test.ts index 24bbcbb9d34..dcf4211ac7f 100644 --- a/src/test/moves/protect.test.ts +++ b/src/test/moves/protect.test.ts @@ -10,7 +10,7 @@ import { ArenaTagSide, ArenaTrapTag } from "#app/data/arena-tag"; import { BattlerIndex } from "#app/battle"; import { MoveResult } from "#app/field/pokemon"; -const TIMEOUT = 20 * 1000; + describe("Moves - Protect", () => { let phaserGame: Phaser.Game; @@ -53,7 +53,7 @@ describe("Moves - Protect", () => { await game.phaseInterceptor.to("BerryPhase", false); expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp()); - }, TIMEOUT + } ); test( @@ -72,7 +72,7 @@ describe("Moves - Protect", () => { expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp()); expect(game.scene.arena.getTagOnSide(ArenaTrapTag, ArenaTagSide.ENEMY)).toBeUndefined(); - }, TIMEOUT + } ); test( @@ -89,7 +89,7 @@ describe("Moves - Protect", () => { await game.phaseInterceptor.to("BerryPhase", false); expect(leadPokemon.getStatStage(Stat.ATK)).toBe(0); - }, TIMEOUT + } ); test( @@ -108,7 +108,7 @@ describe("Moves - Protect", () => { expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp()); expect(enemyPokemon.turnData.hitCount).toBe(1); - }, TIMEOUT + } ); test( @@ -129,6 +129,6 @@ describe("Moves - Protect", () => { expect(enemyPokemon.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); expect(leadPokemon.getLastXMoves()[0].result).toBe(MoveResult.FAIL); - }, TIMEOUT + } ); }); diff --git a/src/test/moves/purify.test.ts b/src/test/moves/purify.test.ts index 15d684b2d60..3ba9dfcbb65 100644 --- a/src/test/moves/purify.test.ts +++ b/src/test/moves/purify.test.ts @@ -8,7 +8,7 @@ import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest"; -const TIMEOUT = 20 * 1000; + describe("Moves - Purify", () => { let phaserGame: Phaser.Game; @@ -55,7 +55,6 @@ describe("Moves - Purify", () => { expect(enemyPokemon.status).toBeNull(); expect(playerPokemon.isFullHp()).toBe(true); }, - TIMEOUT ); test( @@ -74,7 +73,6 @@ describe("Moves - Purify", () => { expect(playerPokemon.hp).toBe(playerInitialHp); }, - TIMEOUT ); }); diff --git a/src/test/moves/quick_guard.test.ts b/src/test/moves/quick_guard.test.ts index 9ab0fe1509c..e03beeac06a 100644 --- a/src/test/moves/quick_guard.test.ts +++ b/src/test/moves/quick_guard.test.ts @@ -8,7 +8,7 @@ import { Stat } from "#enums/stat"; import { BattlerIndex } from "#app/battle"; import { MoveResult } from "#app/field/pokemon"; -const TIMEOUT = 20 * 1000; + describe("Moves - Quick Guard", () => { let phaserGame: Phaser.Game; @@ -52,7 +52,7 @@ describe("Moves - Quick Guard", () => { await game.phaseInterceptor.to("BerryPhase", false); playerPokemon.forEach(p => expect(p.hp).toBe(p.getMaxHp())); - }, TIMEOUT + } ); test( @@ -71,7 +71,7 @@ describe("Moves - Quick Guard", () => { await game.phaseInterceptor.to("BerryPhase", false); playerPokemon.forEach(p => expect(p.getStatStage(Stat.ATK)).toBe(0)); - }, TIMEOUT + } ); test( @@ -113,6 +113,6 @@ describe("Moves - Quick Guard", () => { expect(enemyPokemon.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); expect(playerPokemon.getLastXMoves()[0].result).toBe(MoveResult.FAIL); - }, TIMEOUT + } ); }); diff --git a/src/test/moves/rage_powder.test.ts b/src/test/moves/rage_powder.test.ts index 86bc48ef882..bb31a1f2194 100644 --- a/src/test/moves/rage_powder.test.ts +++ b/src/test/moves/rage_powder.test.ts @@ -6,7 +6,7 @@ import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest"; -const TIMEOUT = 20 * 1000; + describe("Moves - Rage Powder", () => { let phaserGame: Phaser.Game; @@ -50,7 +50,7 @@ describe("Moves - Rage Powder", () => { // If redirection was bypassed, both enemies should be damaged expect(enemyPokemon[0].hp).toBeLessThan(enemyPokemon[0].getMaxHp()); expect(enemyPokemon[1].hp).toBeLessThan(enemyPokemon[0].getMaxHp()); - }, TIMEOUT + } ); test( @@ -76,6 +76,6 @@ describe("Moves - Rage Powder", () => { // If redirection was bypassed, both enemies should be damaged expect(enemyPokemon[0].hp).toBeLessThan(enemyStartingHp[0]); expect(enemyPokemon[1].hp).toBeLessThan(enemyStartingHp[1]); - }, TIMEOUT + } ); }); diff --git a/src/test/moves/relic_song.test.ts b/src/test/moves/relic_song.test.ts new file mode 100644 index 00000000000..67fc557a318 --- /dev/null +++ b/src/test/moves/relic_song.test.ts @@ -0,0 +1,79 @@ +import { Type } from "#app/data/type"; +import { Challenges } from "#app/enums/challenges"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Moves - Relic Song", () => { + 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.RELIC_SONG, Moves.SPLASH]) + .battleType("single") + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH) + .enemySpecies(Species.MAGIKARP) + .enemyLevel(100); + }); + + it("swaps Meloetta's form between Aria and Pirouette", async () => { + await game.classicMode.startBattle([Species.MELOETTA]); + + const meloetta = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.RELIC_SONG); + await game.toNextTurn(); + + expect(meloetta.formIndex).toBe(1); + + game.move.select(Moves.RELIC_SONG); + await game.phaseInterceptor.to("BerryPhase"); + + expect(meloetta.formIndex).toBe(0); + }); + + it("doesn't swap Meloetta's form during a mono-type challenge", async () => { + game.challengeMode.addChallenge(Challenges.SINGLE_TYPE, Type.PSYCHIC + 1, 0); + await game.challengeMode.startBattle([Species.MELOETTA]); + + const meloetta = game.scene.getPlayerPokemon()!; + + expect(meloetta.formIndex).toBe(0); + + game.move.select(Moves.RELIC_SONG); + await game.phaseInterceptor.to("BerryPhase"); + await game.toNextTurn(); + + expect(meloetta.formIndex).toBe(0); + }); + + it("doesn't swap Meloetta's form during biome change (arena reset)", async () => { + game.override + .starterForms({[Species.MELOETTA]: 1}) + .startingWave(10); + await game.classicMode.startBattle([Species.MELOETTA]); + + const meloetta = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.SPLASH); + await game.doKillOpponents(); + await game.toNextWave(); + + expect(meloetta.formIndex).toBe(1); + }); +}); diff --git a/src/test/moves/roost.test.ts b/src/test/moves/roost.test.ts index df7fc7096b0..a1c473c0632 100644 --- a/src/test/moves/roost.test.ts +++ b/src/test/moves/roost.test.ts @@ -1,3 +1,4 @@ +import { BattlerIndex } from "#app/battle"; import { Type } from "#app/data/type"; import { BattlerTagType } from "#app/enums/battler-tag-type"; import { Moves } from "#app/enums/moves"; @@ -8,7 +9,7 @@ import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest"; -const TIMEOUT = 20 * 1000; + describe("Moves - Roost", () => { let phaserGame: Phaser.Game; @@ -29,10 +30,9 @@ describe("Moves - Roost", () => { game.override.battleType("single"); game.override.enemySpecies(Species.RELICANTH); game.override.startingLevel(100); - game.override.enemyLevel(60); + game.override.enemyLevel(100); game.override.enemyMoveset(Moves.EARTHQUAKE); game.override.moveset([Moves.ROOST, Moves.BURN_UP, Moves.DOUBLE_SHOCK]); - game.override.starterForms({ [Species.ROTOM]: 4 }); }); /** @@ -55,6 +55,7 @@ describe("Moves - Roost", () => { const playerPokemon = game.scene.getPlayerPokemon()!; const playerPokemonStartingHP = playerPokemon.hp; game.move.select(Moves.ROOST); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.phaseInterceptor.to(MoveEffectPhase); // Should only be normal type, and NOT flying type @@ -71,7 +72,7 @@ describe("Moves - Roost", () => { expect(playerPokemonTypes[0] === Type.NORMAL).toBeTruthy(); expect(playerPokemonTypes.length === 1).toBeTruthy(); expect(playerPokemon.isGrounded()).toBeTruthy(); - }, TIMEOUT + } ); test( @@ -81,6 +82,7 @@ describe("Moves - Roost", () => { const playerPokemon = game.scene.getPlayerPokemon()!; const playerPokemonStartingHP = playerPokemon.hp; game.move.select(Moves.ROOST); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.phaseInterceptor.to(MoveEffectPhase); // Should only be normal type, and NOT flying type @@ -98,7 +100,7 @@ describe("Moves - Roost", () => { expect(playerPokemonTypes[0] === Type.FLYING).toBeTruthy(); expect(playerPokemon.isGrounded()).toBeFalsy(); - }, TIMEOUT + } ); test( @@ -108,6 +110,7 @@ describe("Moves - Roost", () => { const playerPokemon = game.scene.getPlayerPokemon()!; const playerPokemonStartingHP = playerPokemon.hp; game.move.select(Moves.ROOST); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.phaseInterceptor.to(MoveEffectPhase); // Should only be pure fighting type and grounded @@ -125,19 +128,21 @@ describe("Moves - Roost", () => { expect(playerPokemonTypes[1] === Type.FLYING).toBeTruthy(); expect(playerPokemon.isGrounded()).toBeFalsy(); - }, TIMEOUT + } ); test( "Pokemon with levitate after using roost should lose flying type but still be unaffected by ground moves", async () => { + game.override.starterForms({ [Species.ROTOM]: 4 }); await game.classicMode.startBattle([Species.ROTOM]); const playerPokemon = game.scene.getPlayerPokemon()!; const playerPokemonStartingHP = playerPokemon.hp; game.move.select(Moves.ROOST); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.phaseInterceptor.to(MoveEffectPhase); - // Should only be pure fighting type and grounded + // Should only be pure eletric type and grounded let playerPokemonTypes = playerPokemon.getTypes(); expect(playerPokemonTypes[0] === Type.ELECTRIC).toBeTruthy(); expect(playerPokemonTypes.length === 1).toBeTruthy(); @@ -145,14 +150,14 @@ describe("Moves - Roost", () => { await game.phaseInterceptor.to(TurnEndPhase); - // Should have lost HP and is now back to being fighting/flying + // Should have lost HP and is now back to being electric/flying playerPokemonTypes = playerPokemon.getTypes(); expect(playerPokemon.hp).toBe(playerPokemonStartingHP); expect(playerPokemonTypes[0] === Type.ELECTRIC).toBeTruthy(); expect(playerPokemonTypes[1] === Type.FLYING).toBeTruthy(); expect(playerPokemon.isGrounded()).toBeFalsy(); - }, TIMEOUT + } ); test( @@ -162,6 +167,7 @@ describe("Moves - Roost", () => { const playerPokemon = game.scene.getPlayerPokemon()!; const playerPokemonStartingHP = playerPokemon.hp; game.move.select(Moves.BURN_UP); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.phaseInterceptor.to(MoveEffectPhase); // Should only be pure flying type after burn up @@ -171,6 +177,7 @@ describe("Moves - Roost", () => { await game.phaseInterceptor.to(TurnEndPhase); game.move.select(Moves.ROOST); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.phaseInterceptor.to(MoveEffectPhase); // Should only be typeless type after roost and is grounded @@ -189,7 +196,7 @@ describe("Moves - Roost", () => { expect(playerPokemonTypes.length === 1).toBeTruthy(); expect(playerPokemon.isGrounded()).toBeFalsy(); - }, TIMEOUT + } ); test( @@ -200,6 +207,7 @@ describe("Moves - Roost", () => { const playerPokemon = game.scene.getPlayerPokemon()!; const playerPokemonStartingHP = playerPokemon.hp; game.move.select(Moves.DOUBLE_SHOCK); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.phaseInterceptor.to(MoveEffectPhase); // Should only be pure flying type after burn up @@ -209,6 +217,7 @@ describe("Moves - Roost", () => { await game.phaseInterceptor.to(TurnEndPhase); game.move.select(Moves.ROOST); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.phaseInterceptor.to(MoveEffectPhase); // Should only be typeless type after roost and is grounded @@ -227,7 +236,7 @@ describe("Moves - Roost", () => { expect(playerPokemonTypes.length === 1).toBeTruthy(); expect(playerPokemon.isGrounded()).toBeFalsy(); - }, TIMEOUT + } ); test( @@ -254,7 +263,7 @@ describe("Moves - Roost", () => { expect(playerPokemonTypes.length === 3).toBeTruthy(); expect(playerPokemon.isGrounded()).toBeFalsy(); - }, TIMEOUT + } ); }); diff --git a/src/test/moves/safeguard.test.ts b/src/test/moves/safeguard.test.ts index 2caf698a73a..b21698d0298 100644 --- a/src/test/moves/safeguard.test.ts +++ b/src/test/moves/safeguard.test.ts @@ -8,7 +8,7 @@ import { Species } from "#enums/species"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -const TIMEOUT = 20 * 1000; + describe("Moves - Safeguard", () => { let phaserGame: Phaser.Game; @@ -46,7 +46,7 @@ describe("Moves - Safeguard", () => { await game.toNextTurn(); expect(enemy.status).toBeUndefined(); - }, TIMEOUT); + }); it("protects from status moves", async () => { await game.classicMode.startBattle(); @@ -57,7 +57,7 @@ describe("Moves - Safeguard", () => { await game.toNextTurn(); expect(enemyPokemon.status).toBeUndefined(); - }, TIMEOUT); + }); it("protects from confusion", async () => { game.override.moveset([Moves.CONFUSE_RAY]); @@ -69,7 +69,7 @@ describe("Moves - Safeguard", () => { await game.toNextTurn(); expect(enemyPokemon.summonData.tags).toEqual([]); - }, TIMEOUT); + }); it("protects ally from status", async () => { game.override.battleType("double"); @@ -87,7 +87,7 @@ describe("Moves - Safeguard", () => { expect(enemyPokemon[0].status).toBeUndefined(); expect(enemyPokemon[1].status).toBeUndefined(); - }, TIMEOUT); + }); it("protects from Yawn", async () => { await game.classicMode.startBattle(); @@ -98,7 +98,7 @@ describe("Moves - Safeguard", () => { await game.toNextTurn(); expect(enemyPokemon.summonData.tags).toEqual([]); - }, TIMEOUT); + }); it("doesn't protect from already existing Yawn", async () => { await game.classicMode.startBattle(); @@ -112,7 +112,7 @@ describe("Moves - Safeguard", () => { await game.toNextTurn(); expect(enemyPokemon.status?.effect).toEqual(StatusEffect.SLEEP); - }, TIMEOUT); + }); it("doesn't protect from self-inflicted via Rest or Flame Orb", async () => { game.override.enemyHeldItems([{name: "FLAME_ORB"}]); @@ -135,7 +135,7 @@ describe("Moves - Safeguard", () => { await game.toNextTurn(); expect(enemyPokemon.status?.effect).toEqual(StatusEffect.SLEEP); - }, TIMEOUT); + }); it("protects from ability-inflicted status", async () => { game.override.ability(Abilities.STATIC); @@ -151,5 +151,5 @@ describe("Moves - Safeguard", () => { await game.toNextTurn(); expect(enemyPokemon.status).toBeUndefined(); - }, TIMEOUT); + }); }); diff --git a/src/test/moves/shell_side_arm.test.ts b/src/test/moves/shell_side_arm.test.ts new file mode 100644 index 00000000000..ded7ed82fd1 --- /dev/null +++ b/src/test/moves/shell_side_arm.test.ts @@ -0,0 +1,85 @@ +import { BattlerIndex } from "#app/battle"; +import { allMoves, ShellSideArmCategoryAttr } from "#app/data/move"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, it, expect, vi } from "vitest"; + +describe("Moves - Shell Side Arm", () => { + 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.SHELL_SIDE_ARM]) + .battleType("single") + .startingLevel(100) + .enemyLevel(100) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("becomes a physical attack if forecasted to deal more damage as physical", async () => { + game.override.enemySpecies(Species.SNORLAX); + + await game.classicMode.startBattle([Species.MANAPHY]); + + const shellSideArm = allMoves[Moves.SHELL_SIDE_ARM]; + const shellSideArmAttr = shellSideArm.getAttrs(ShellSideArmCategoryAttr)[0]; + vi.spyOn(shellSideArmAttr, "apply"); + + game.move.select(Moves.SHELL_SIDE_ARM); + + await game.phaseInterceptor.to("MoveEffectPhase"); + + expect(shellSideArmAttr.apply).toHaveLastReturnedWith(true); + }); + + it("remains a special attack if forecasted to deal more damage as special", async () => { + game.override.enemySpecies(Species.SLOWBRO); + + await game.classicMode.startBattle([Species.MANAPHY]); + + const shellSideArm = allMoves[Moves.SHELL_SIDE_ARM]; + const shellSideArmAttr = shellSideArm.getAttrs(ShellSideArmCategoryAttr)[0]; + vi.spyOn(shellSideArmAttr, "apply"); + + game.move.select(Moves.SHELL_SIDE_ARM); + + await game.phaseInterceptor.to("MoveEffectPhase"); + + expect(shellSideArmAttr.apply).toHaveLastReturnedWith(false); + }); + + it("respects stat stage changes when forecasting base damage", async () => { + game.override + .enemySpecies(Species.SNORLAX) + .enemyMoveset(Moves.COTTON_GUARD); + + await game.classicMode.startBattle([Species.MANAPHY]); + + const shellSideArm = allMoves[Moves.SHELL_SIDE_ARM]; + const shellSideArmAttr = shellSideArm.getAttrs(ShellSideArmCategoryAttr)[0]; + vi.spyOn(shellSideArmAttr, "apply"); + + game.move.select(Moves.SHELL_SIDE_ARM); + + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + + await game.phaseInterceptor.to("BerryPhase", false); + + expect(shellSideArmAttr.apply).toHaveLastReturnedWith(false); + }); +}); diff --git a/src/test/moves/shell_trap.test.ts b/src/test/moves/shell_trap.test.ts index 213b9c3fd0a..1dae00e24a5 100644 --- a/src/test/moves/shell_trap.test.ts +++ b/src/test/moves/shell_trap.test.ts @@ -10,7 +10,7 @@ import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -const TIMEOUT = 20 * 1000; + describe("Moves - Shell Trap", () => { let phaserGame: Phaser.Game; @@ -60,7 +60,7 @@ describe("Moves - Shell Trap", () => { await game.phaseInterceptor.to(MoveEndPhase); enemyPokemon.forEach(p => expect(p.hp).toBeLessThan(p.getMaxHp())); - }, TIMEOUT + } ); it( @@ -86,7 +86,7 @@ describe("Moves - Shell Trap", () => { await game.phaseInterceptor.to(BerryPhase, false); enemyPokemon.forEach(p => expect(p.hp).toBe(p.getMaxHp())); - }, TIMEOUT + } ); it( @@ -112,7 +112,7 @@ describe("Moves - Shell Trap", () => { await game.phaseInterceptor.to(BerryPhase, false); enemyPokemon.forEach(p => expect(p.hp).toBe(p.getMaxHp())); - }, TIMEOUT + } ); it( @@ -138,7 +138,7 @@ describe("Moves - Shell Trap", () => { await game.phaseInterceptor.to(BerryPhase, false); enemyPokemon.forEach((p, i) => expect(p.hp).toBe(enemyStartingHp[i])); - }, TIMEOUT + } ); it( @@ -158,6 +158,6 @@ describe("Moves - Shell Trap", () => { expect(playerPokemon.getLastXMoves()[0].result).toBe(MoveResult.FAIL); expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); - }, TIMEOUT + } ); }); diff --git a/src/test/moves/spit_up.test.ts b/src/test/moves/spit_up.test.ts index acf7f01d991..412360c2664 100644 --- a/src/test/moves/spit_up.test.ts +++ b/src/test/moves/spit_up.test.ts @@ -43,7 +43,7 @@ describe("Moves - Spit Up", () => { }); describe("consumes all stockpile stacks to deal damage (scaling with stacks)", () => { - it("1 stack -> 100 power", { timeout: 10000 }, async () => { + it("1 stack -> 100 power", async () => { const stacksToSetup = 1; const expectedPower = 100; @@ -65,7 +65,7 @@ describe("Moves - Spit Up", () => { expect(pokemon.getTag(StockpilingTag)).toBeUndefined(); }); - it("2 stacks -> 200 power", { timeout: 10000 }, async () => { + it("2 stacks -> 200 power", async () => { const stacksToSetup = 2; const expectedPower = 200; @@ -88,7 +88,7 @@ describe("Moves - Spit Up", () => { expect(pokemon.getTag(StockpilingTag)).toBeUndefined(); }); - it("3 stacks -> 300 power", { timeout: 10000 }, async () => { + it("3 stacks -> 300 power", async () => { const stacksToSetup = 3; const expectedPower = 300; @@ -113,7 +113,7 @@ describe("Moves - Spit Up", () => { }); }); - it("fails without stacks", { timeout: 10000 }, async () => { + it("fails without stacks", async () => { await game.startBattle([Species.ABOMASNOW]); const pokemon = game.scene.getPlayerPokemon()!; @@ -130,7 +130,7 @@ describe("Moves - Spit Up", () => { }); describe("restores stat boosts granted by stacks", () => { - it("decreases stats based on stored values (both boosts equal)", { timeout: 10000 }, async () => { + it("decreases stats based on stored values (both boosts equal)", async () => { await game.startBattle([Species.ABOMASNOW]); const pokemon = game.scene.getPlayerPokemon()!; @@ -157,7 +157,7 @@ describe("Moves - Spit Up", () => { expect(pokemon.getTag(StockpilingTag)).toBeUndefined(); }); - it("decreases stats based on stored values (different boosts)", { timeout: 10000 }, async () => { + it("decreases stats based on stored values (different boosts)", async () => { await game.startBattle([Species.ABOMASNOW]); const pokemon = game.scene.getPlayerPokemon()!; diff --git a/src/test/moves/spotlight.test.ts b/src/test/moves/spotlight.test.ts index aef44369642..6324c3dc6ec 100644 --- a/src/test/moves/spotlight.test.ts +++ b/src/test/moves/spotlight.test.ts @@ -6,7 +6,7 @@ import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest"; -const TIMEOUT = 20 * 1000; + describe("Moves - Spotlight", () => { let phaserGame: Phaser.Game; @@ -50,7 +50,7 @@ describe("Moves - Spotlight", () => { expect(enemyPokemon[0].hp).toBeLessThan(enemyPokemon[0].getMaxHp()); expect(enemyPokemon[1].hp).toBe(enemyPokemon[1].getMaxHp()); - }, TIMEOUT + } ); test( @@ -70,6 +70,6 @@ describe("Moves - Spotlight", () => { expect(enemyPokemon[0].hp).toBeLessThan(enemyPokemon[0].getMaxHp()); expect(enemyPokemon[1].hp).toBe(enemyPokemon[1].getMaxHp()); - }, TIMEOUT + } ); }); diff --git a/src/test/moves/steamroller.test.ts b/src/test/moves/steamroller.test.ts new file mode 100644 index 00000000000..cbbb3a22593 --- /dev/null +++ b/src/test/moves/steamroller.test.ts @@ -0,0 +1,58 @@ +import { BattlerIndex } from "#app/battle"; +import { allMoves } from "#app/data/move"; +import { BattlerTagType } from "#app/enums/battler-tag-type"; +import { DamageCalculationResult } from "#app/field/pokemon"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +describe("Moves - Steamroller", () => { + 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.STEAMROLLER]).battleType("single").enemyAbility(Abilities.BALL_FETCH); + }); + + it("should always hit a minimzed target with double damage", async () => { + game.override.enemySpecies(Species.DITTO).enemyMoveset(Moves.MINIMIZE); + await game.classicMode.startBattle([Species.IRON_BOULDER]); + + const ditto = game.scene.getEnemyPokemon()!; + vi.spyOn(ditto, "getAttackDamage"); + ditto.hp = 5000; + const steamroller = allMoves[Moves.STEAMROLLER]; + vi.spyOn(steamroller, "calculateBattleAccuracy"); + const ironBoulder = game.scene.getPlayerPokemon()!; + vi.spyOn(ironBoulder, "getAccuracyMultiplier"); + // Turn 1 + game.move.select(Moves.STEAMROLLER); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); + await game.toNextTurn(); + // Turn 2 + game.move.select(Moves.STEAMROLLER); + await game.toNextTurn(); + + const [dmgCalcTurn1, dmgCalcTurn2]: DamageCalculationResult[] = vi + .mocked(ditto.getAttackDamage) + .mock.results.map((r) => r.value); + + expect(dmgCalcTurn2.damage).toBeGreaterThanOrEqual(dmgCalcTurn1.damage * 2); + expect(ditto.getTag(BattlerTagType.MINIMIZED)).toBeDefined(); + expect(steamroller.calculateBattleAccuracy).toHaveReturnedWith(-1); + }); +}); diff --git a/src/test/moves/stockpile.test.ts b/src/test/moves/stockpile.test.ts index 8e7a44d053b..141ce79eb33 100644 --- a/src/test/moves/stockpile.test.ts +++ b/src/test/moves/stockpile.test.ts @@ -37,7 +37,7 @@ describe("Moves - Stockpile", () => { game.override.ability(Abilities.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)", { timeout: 10000 }, async () => { + 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 () => { await game.startBattle([Species.ABOMASNOW]); const user = game.scene.getPlayerPokemon()!; @@ -76,7 +76,7 @@ describe("Moves - Stockpile", () => { } }); - it("gains a stockpile stack even if user's DEF and SPDEF stat stages are at +6", { timeout: 10000 }, async () => { + it("gains a stockpile stack even if user's DEF and SPDEF stat stages are at +6", async () => { await game.startBattle([Species.ABOMASNOW]); const user = game.scene.getPlayerPokemon()!; diff --git a/src/test/moves/substitute.test.ts b/src/test/moves/substitute.test.ts new file mode 100644 index 00000000000..6c18579e7f6 --- /dev/null +++ b/src/test/moves/substitute.test.ts @@ -0,0 +1,513 @@ +import { SubstituteTag, TrappedTag } from "#app/data/battler-tags"; +import { allMoves, StealHeldItemChanceAttr } from "#app/data/move"; +import { StatusEffect } from "#app/data/status-effect"; +import { Abilities } from "#app/enums/abilities"; +import { BattlerTagType } from "#app/enums/battler-tag-type"; +import { BerryType } from "#app/enums/berry-type"; +import { Moves } from "#app/enums/moves"; +import { Species } from "#app/enums/species"; +import { Stat } from "#app/enums/stat"; +import { MoveResult } from "#app/field/pokemon"; +import { CommandPhase } from "#app/phases/command-phase"; +import GameManager from "#app/test/utils/gameManager"; +import { Command } from "#app/ui/command-ui-handler"; +import { Mode } from "#app/ui/ui"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + + +describe("Moves - Substitute", () => { + 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 + .battleType("single") + .moveset([Moves.SUBSTITUTE, Moves.SWORDS_DANCE, Moves.TACKLE, Moves.SPLASH]) + .enemySpecies(Species.SNORLAX) + .enemyAbility(Abilities.INSOMNIA) + .enemyMoveset(Moves.SPLASH) + .startingLevel(100) + .enemyLevel(100); + }); + + it( + "should cause the user to take damage", + async () => { + await game.classicMode.startBattle([Species.MAGIKARP]); + + const leadPokemon = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.SUBSTITUTE); + + await game.phaseInterceptor.to("MoveEndPhase", false); + + expect(leadPokemon.hp).toBe(Math.ceil(leadPokemon.getMaxHp() * 3/4)); + } + ); + + it( + "should redirect enemy attack damage to the Substitute doll", + async () => { + game.override.enemyMoveset(Array(4).fill(Moves.TACKLE)); + + await game.classicMode.startBattle([Species.SKARMORY]); + + const leadPokemon = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.SUBSTITUTE); + + await game.phaseInterceptor.to("MoveEndPhase", false); + + expect(leadPokemon.hp).toBe(Math.ceil(leadPokemon.getMaxHp() * 3/4)); + expect(leadPokemon.getTag(BattlerTagType.SUBSTITUTE)).toBeDefined(); + const postSubHp = leadPokemon.hp; + + await game.phaseInterceptor.to("BerryPhase", false); + + expect(leadPokemon.hp).toBe(postSubHp); + expect(leadPokemon.getTag(BattlerTagType.SUBSTITUTE)).toBeDefined(); + } + ); + + it( + "should fade after redirecting more damage than its remaining HP", + async () => { + // Giga Impact OHKOs Magikarp if substitute isn't up + game.override.enemyMoveset(Array(4).fill(Moves.GIGA_IMPACT)); + vi.spyOn(allMoves[Moves.GIGA_IMPACT], "accuracy", "get").mockReturnValue(100); + + await game.classicMode.startBattle([Species.MAGIKARP]); + + const leadPokemon = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.SUBSTITUTE); + + await game.phaseInterceptor.to("MoveEndPhase", false); + + expect(leadPokemon.hp).toBe(Math.ceil(leadPokemon.getMaxHp() * 3/4)); + expect(leadPokemon.getTag(BattlerTagType.SUBSTITUTE)).toBeDefined(); + const postSubHp = leadPokemon.hp; + + await game.phaseInterceptor.to("BerryPhase", false); + + expect(leadPokemon.hp).toBe(postSubHp); + expect(leadPokemon.getTag(BattlerTagType.SUBSTITUTE)).toBeUndefined(); + } + ); + + it( + "should block stat changes from status moves", + async () => { + game.override.enemyMoveset(Array(4).fill(Moves.CHARM)); + + await game.classicMode.startBattle([Species.MAGIKARP]); + + const leadPokemon = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.SUBSTITUTE); + + await game.phaseInterceptor.to("BerryPhase", false); + + expect(leadPokemon.getStatStage(Stat.ATK)).toBe(0); + expect(leadPokemon.getTag(BattlerTagType.SUBSTITUTE)).toBeDefined(); + } + ); + + it( + "should be bypassed by sound-based moves", + async () => { + game.override.enemyMoveset(Array(4).fill(Moves.ECHOED_VOICE)); + + await game.classicMode.startBattle([Species.BLASTOISE]); + + const leadPokemon = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.SUBSTITUTE); + + await game.phaseInterceptor.to("MoveEndPhase"); + + expect(leadPokemon.getTag(BattlerTagType.SUBSTITUTE)).toBeDefined(); + const postSubHp = leadPokemon.hp; + + await game.phaseInterceptor.to("BerryPhase", false); + + expect(leadPokemon.getTag(BattlerTagType.SUBSTITUTE)).toBeDefined(); + expect(leadPokemon.hp).toBeLessThan(postSubHp); + } + ); + + it( + "should be bypassed by attackers with Infiltrator", + async () => { + game.override.enemyMoveset(Array(4).fill(Moves.TACKLE)); + game.override.enemyAbility(Abilities.INFILTRATOR); + + await game.classicMode.startBattle([Species.BLASTOISE]); + + const leadPokemon = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.SUBSTITUTE); + + await game.phaseInterceptor.to("MoveEndPhase"); + + expect(leadPokemon.getTag(BattlerTagType.SUBSTITUTE)).toBeDefined(); + const postSubHp = leadPokemon.hp; + + await game.phaseInterceptor.to("BerryPhase", false); + + expect(leadPokemon.getTag(BattlerTagType.SUBSTITUTE)).toBeDefined(); + expect(leadPokemon.hp).toBeLessThan(postSubHp); + } + ); + + it( + "shouldn't block the user's own status moves", + async () => { + await game.classicMode.startBattle([Species.BLASTOISE]); + + const leadPokemon = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.SUBSTITUTE); + + await game.phaseInterceptor.to("MoveEndPhase"); + await game.toNextTurn(); + + game.move.select(Moves.SWORDS_DANCE); + + await game.phaseInterceptor.to("MoveEndPhase", false); + + expect(leadPokemon.getStatStage(Stat.ATK)).toBe(2); + } + ); + + it( + "should protect the user from flinching", + async () => { + game.override.enemyMoveset(Array(4).fill(Moves.FAKE_OUT)); + game.override.startingLevel(1); // Ensures the Substitute will break + + await game.classicMode.startBattle([Species.BLASTOISE]); + + const leadPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + leadPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, leadPokemon.id); + + game.move.select(Moves.TACKLE); + + await game.phaseInterceptor.to("BerryPhase", false); + + expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp()); + } + ); + + it( + "should protect the user from being trapped", + async () => { + vi.spyOn(allMoves[Moves.SAND_TOMB], "accuracy", "get").mockReturnValue(100); + game.override.enemyMoveset(Array(4).fill(Moves.SAND_TOMB)); + + await game.classicMode.startBattle([Species.BLASTOISE]); + + const leadPokemon = game.scene.getPlayerPokemon()!; + + leadPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, leadPokemon.id); + + game.move.select(Moves.SPLASH); + + await game.phaseInterceptor.to("BerryPhase", false); + + expect(leadPokemon.getTag(TrappedTag)).toBeUndefined(); + } + ); + + it( + "should prevent the user's stats from being lowered", + async () => { + vi.spyOn(allMoves[Moves.LIQUIDATION], "chance", "get").mockReturnValue(100); + game.override.enemyMoveset(Array(4).fill(Moves.LIQUIDATION)); + + await game.classicMode.startBattle([Species.BLASTOISE]); + + const leadPokemon = game.scene.getPlayerPokemon()!; + + leadPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, leadPokemon.id); + + game.move.select(Moves.SPLASH); + + await game.phaseInterceptor.to("BerryPhase", false); + + expect(leadPokemon.getStatStage(Stat.DEF)).toBe(0); + } + ); + + it( + "should protect the user from being afflicted with status effects", + async () => { + game.override.enemyMoveset(Array(4).fill(Moves.NUZZLE)); + + await game.classicMode.startBattle([Species.BLASTOISE]); + + const leadPokemon = game.scene.getPlayerPokemon()!; + + leadPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, leadPokemon.id); + + game.move.select(Moves.SPLASH); + + await game.phaseInterceptor.to("BerryPhase", false); + + expect(leadPokemon.status?.effect).not.toBe(StatusEffect.PARALYSIS); + } + ); + + it( + "should prevent the user's items from being stolen", + async () => { + game.override.enemyMoveset(Array(4).fill(Moves.THIEF)); + vi.spyOn(allMoves[Moves.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([Species.BLASTOISE]); + + const leadPokemon = game.scene.getPlayerPokemon()!; + + leadPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, leadPokemon.id); + + game.move.select(Moves.SPLASH); + + await game.phaseInterceptor.to("BerryPhase", false); + + expect(leadPokemon.getHeldItems().length).toBe(1); + } + ); + + it( + "should prevent the user's items from being removed", + async () => { + game.override.moveset([Moves.KNOCK_OFF]); + game.override.enemyHeldItems([{name: "BERRY", type: BerryType.SITRUS}]); + + await game.classicMode.startBattle([Species.BLASTOISE]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + + enemyPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, enemyPokemon.id); + const enemyNumItems = enemyPokemon.getHeldItems().length; + + game.move.select(Moves.KNOCK_OFF); + + await game.phaseInterceptor.to("MoveEndPhase", false); + + expect(enemyPokemon.getHeldItems().length).toBe(enemyNumItems); + } + ); + + it( + "move effect should prevent the user's berries from being stolen and eaten", + async () => { + game.override.enemyMoveset(Array(4).fill(Moves.BUG_BITE)); + game.override.startingHeldItems([{name: "BERRY", type: BerryType.SITRUS}]); + + await game.classicMode.startBattle([Species.BLASTOISE]); + + const leadPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + leadPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, leadPokemon.id); + + game.move.select(Moves.TACKLE); + + await game.phaseInterceptor.to("MoveEndPhase", false); + const enemyPostAttackHp = enemyPokemon.hp; + + await game.phaseInterceptor.to("BerryPhase", false); + + expect(leadPokemon.getHeldItems().length).toBe(1); + expect(enemyPokemon.hp).toBe(enemyPostAttackHp); + } + ); + + it( + "should prevent the user's stats from being reset by Clear Smog", + async () => { + game.override.enemyMoveset(Array(4).fill(Moves.CLEAR_SMOG)); + + await game.classicMode.startBattle([Species.BLASTOISE]); + + const leadPokemon = game.scene.getPlayerPokemon()!; + + leadPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, leadPokemon.id); + + game.move.select(Moves.SWORDS_DANCE); + + await game.phaseInterceptor.to("BerryPhase", false); + + expect(leadPokemon.getStatStage(Stat.ATK)).toBe(2); + } + ); + + it( + "should prevent the user from becoming confused", + async () => { + game.override.enemyMoveset(Array(4).fill(Moves.MAGICAL_TORQUE)); + vi.spyOn(allMoves[Moves.MAGICAL_TORQUE], "chance", "get").mockReturnValue(100); + + await game.classicMode.startBattle([Species.BLASTOISE]); + + const leadPokemon = game.scene.getPlayerPokemon()!; + + leadPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, leadPokemon.id); + + game.move.select(Moves.SWORDS_DANCE); + + await game.phaseInterceptor.to("BerryPhase", false); + + expect(leadPokemon.getTag(BattlerTagType.CONFUSED)).toBeUndefined(); + expect(leadPokemon.getStatStage(Stat.ATK)).toBe(2); + } + ); + + it( + "should transfer to the switched in Pokemon when the source uses Baton Pass", + async () => { + game.override.moveset([Moves.SUBSTITUTE, Moves.BATON_PASS]); + + await game.classicMode.startBattle([Species.BLASTOISE, Species.CHARIZARD]); + + const leadPokemon = game.scene.getPlayerPokemon()!; + + leadPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, leadPokemon.id); + + // Simulate a Baton switch for the player this turn + game.onNextPrompt("CommandPhase", Mode.COMMAND, () => { + (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.POKEMON, 1, true); + }); + + await game.phaseInterceptor.to("MovePhase", false); + + const switchedPokemon = game.scene.getPlayerPokemon()!; + const subTag = switchedPokemon.getTag(SubstituteTag)!; + expect(subTag).toBeDefined(); + expect(subTag.hp).toBe(Math.floor(leadPokemon.getMaxHp() * 1/4)); + } + ); + + it( + "should prevent the source's Rough Skin from activating when hit", + async () => { + game.override.enemyMoveset(Array(4).fill(Moves.TACKLE)); + game.override.ability(Abilities.ROUGH_SKIN); + + await game.classicMode.startBattle([Species.BLASTOISE]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.SUBSTITUTE); + + await game.phaseInterceptor.to("BerryPhase", false); + + expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); + } + ); + + it( + "should prevent the source's Focus Punch from failing when hit", + async () => { + game.override.enemyMoveset(Array(4).fill(Moves.TACKLE)); + game.override.moveset([Moves.FOCUS_PUNCH]); + + // Make Focus Punch 40 power to avoid a KO + vi.spyOn(allMoves[Moves.FOCUS_PUNCH], "calculateBattlePower").mockReturnValue(40); + + await game.classicMode.startBattle([Species.BLASTOISE]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + playerPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, playerPokemon.id); + + game.move.select(Moves.FOCUS_PUNCH); + + await game.phaseInterceptor.to("BerryPhase", false); + + expect(playerPokemon.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); + expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp()); + } + ); + + it( + "should not allow Shell Trap to activate when attacked", + async () => { + game.override.enemyMoveset(Array(4).fill(Moves.TACKLE)); + game.override.moveset([Moves.SHELL_TRAP]); + + await game.classicMode.startBattle([Species.BLASTOISE]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + + playerPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, playerPokemon.id); + + game.move.select(Moves.SHELL_TRAP); + + await game.phaseInterceptor.to("BerryPhase", false); + + expect(playerPokemon.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + } + ); + + it( + "should not allow Beak Blast to burn opponents when hit", + async () => { + game.override.enemyMoveset(Array(4).fill(Moves.TACKLE)); + game.override.moveset([Moves.BEAK_BLAST]); + + await game.classicMode.startBattle([Species.BLASTOISE]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + playerPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, playerPokemon.id); + + game.move.select(Moves.BEAK_BLAST); + + await game.phaseInterceptor.to("MoveEndPhase"); + + expect(enemyPokemon.status?.effect).not.toBe(StatusEffect.BURN); + } + ); + + it( + "should cause incoming attacks to not activate Counter", + async() => { + game.override.enemyMoveset(Array(4).fill(Moves.TACKLE)); + game.override.moveset([Moves.COUNTER]); + + await game.classicMode.startBattle([Species.BLASTOISE]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + playerPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, playerPokemon.id); + + game.move.select(Moves.COUNTER); + + await game.phaseInterceptor.to("BerryPhase", false); + + expect(playerPokemon.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); + } + ); +}); diff --git a/src/test/moves/swallow.test.ts b/src/test/moves/swallow.test.ts index 5a0e63e6e78..b8ca941d0ee 100644 --- a/src/test/moves/swallow.test.ts +++ b/src/test/moves/swallow.test.ts @@ -38,7 +38,7 @@ describe("Moves - Swallow", () => { }); describe("consumes all stockpile stacks to heal (scaling with stacks)", () => { - it("1 stack -> 25% heal", { timeout: 10000 }, async () => { + it("1 stack -> 25% heal", async () => { const stacksToSetup = 1; const expectedHeal = 25; @@ -65,7 +65,7 @@ describe("Moves - Swallow", () => { expect(pokemon.getTag(StockpilingTag)).toBeUndefined(); }); - it("2 stacks -> 50% heal", { timeout: 10000 }, async () => { + it("2 stacks -> 50% heal", async () => { const stacksToSetup = 2; const expectedHeal = 50; @@ -93,7 +93,7 @@ describe("Moves - Swallow", () => { expect(pokemon.getTag(StockpilingTag)).toBeUndefined(); }); - it("3 stacks -> 100% heal", { timeout: 10000 }, async () => { + it("3 stacks -> 100% heal", async () => { const stacksToSetup = 3; const expectedHeal = 100; @@ -123,7 +123,7 @@ describe("Moves - Swallow", () => { }); }); - it("fails without stacks", { timeout: 10000 }, async () => { + it("fails without stacks", async () => { await game.startBattle([Species.ABOMASNOW]); const pokemon = game.scene.getPlayerPokemon()!; @@ -138,7 +138,7 @@ describe("Moves - Swallow", () => { }); describe("restores stat stage boosts granted by stacks", () => { - it("decreases stats based on stored values (both boosts equal)", { timeout: 10000 }, async () => { + it("decreases stats based on stored values (both boosts equal)", async () => { await game.startBattle([Species.ABOMASNOW]); const pokemon = game.scene.getPlayerPokemon()!; @@ -163,7 +163,7 @@ describe("Moves - Swallow", () => { expect(pokemon.getTag(StockpilingTag)).toBeUndefined(); }); - it("lower stat stages based on stored values (different boosts)", { timeout: 10000 }, async () => { + it("lower stat stages based on stored values (different boosts)", async () => { await game.startBattle([Species.ABOMASNOW]); const pokemon = game.scene.getPlayerPokemon()!; diff --git a/src/test/moves/tar_shot.test.ts b/src/test/moves/tar_shot.test.ts index 2963f061fc6..2385bd18265 100644 --- a/src/test/moves/tar_shot.test.ts +++ b/src/test/moves/tar_shot.test.ts @@ -11,8 +11,6 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vite describe("Moves - Tar Shot", () => { let phaserGame: Phaser.Game; let game: GameManager; - const TIMEOUT = 20 * 1000; - beforeAll(() => { phaserGame = new Phaser.Game({ type: Phaser.HEADLESS, @@ -54,7 +52,7 @@ describe("Moves - Tar Shot", () => { await game.phaseInterceptor.to("MoveEndPhase"); expect(enemy.getMoveEffectiveness).toHaveReturnedWith(4); - }, TIMEOUT); + }); it("will not double the effectiveness of Fire-type moves used on a target that is already under the effect of Tar Shot (but may still lower its Speed)", async () => { await game.classicMode.startBattle([Species.PIKACHU]); @@ -82,7 +80,7 @@ describe("Moves - Tar Shot", () => { await game.phaseInterceptor.to("MoveEndPhase"); expect(enemy.getMoveEffectiveness).toHaveReturnedWith(4); - }, TIMEOUT); + }); it("does not double the effectiveness of Fire-type moves against a Pokémon that is Terastallized", async () => { game.override.enemyHeldItems([{ name: "TERA_SHARD", type: Type.GRASS }]).enemySpecies(Species.SPRIGATITO); @@ -104,7 +102,7 @@ describe("Moves - Tar Shot", () => { await game.phaseInterceptor.to("MoveEndPhase"); expect(enemy.getMoveEffectiveness).toHaveReturnedWith(2); - }, TIMEOUT); + }); it("doubles the effectiveness of Fire-type moves against a Pokémon that is already under the effects of Tar Shot before it Terastallized", async () => { game.override.enemySpecies(Species.SPRIGATITO); @@ -128,5 +126,5 @@ describe("Moves - Tar Shot", () => { await game.phaseInterceptor.to("MoveEndPhase"); expect(enemy.getMoveEffectiveness).toHaveReturnedWith(4); - }, TIMEOUT); + }); }); diff --git a/src/test/moves/thousand_arrows.test.ts b/src/test/moves/thousand_arrows.test.ts index 8d1d6ee5f4a..ad9281dc45e 100644 --- a/src/test/moves/thousand_arrows.test.ts +++ b/src/test/moves/thousand_arrows.test.ts @@ -8,7 +8,7 @@ import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; -const TIMEOUT = 20 * 1000; + describe("Moves - Thousand Arrows", () => { let phaserGame: Phaser.Game; @@ -51,7 +51,7 @@ describe("Moves - Thousand Arrows", () => { expect(enemyPokemon.getTag(BattlerTagType.IGNORE_FLYING)).toBeDefined(); expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp()); - }, TIMEOUT + } ); it( @@ -74,7 +74,7 @@ describe("Moves - Thousand Arrows", () => { expect(enemyPokemon.getTag(BattlerTagType.IGNORE_FLYING)).toBeDefined(); expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp()); - }, TIMEOUT + } ); it( diff --git a/src/test/moves/throat_chop.test.ts b/src/test/moves/throat_chop.test.ts index cb34b4bafff..2a0ab675b25 100644 --- a/src/test/moves/throat_chop.test.ts +++ b/src/test/moves/throat_chop.test.ts @@ -10,8 +10,6 @@ import { afterEach, beforeAll, beforeEach, describe, it, expect } from "vitest"; describe("Moves - Throat Chop", () => { let phaserGame: Phaser.Game; let game: GameManager; - const TIMEOUT = 20 * 1000; - beforeAll(() => { phaserGame = new Phaser.Game({ type: Phaser.HEADLESS, @@ -53,5 +51,5 @@ describe("Moves - Throat Chop", () => { await game.phaseInterceptor.to("MoveEndPhase"); expect(enemy.isFullHp()).toBe(false); - }, TIMEOUT); + }); }); diff --git a/src/test/moves/thunder_wave.test.ts b/src/test/moves/thunder_wave.test.ts index 7ad59518013..28c5da4717b 100644 --- a/src/test/moves/thunder_wave.test.ts +++ b/src/test/moves/thunder_wave.test.ts @@ -7,7 +7,7 @@ import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; -const TIMEOUT = 20 * 1000; + describe("Moves - Thunder Wave", () => { let phaserGame: Phaser.Game; @@ -45,7 +45,7 @@ describe("Moves - Thunder Wave", () => { await game.phaseInterceptor.to("BerryPhase", false); expect(enemyPokemon.status?.effect).toBe(StatusEffect.PARALYSIS); - }, TIMEOUT); + }); it("does not paralyze if the Pokemon is a Ground-type", async () => { game.override.enemySpecies(Species.DIGLETT); @@ -58,7 +58,7 @@ describe("Moves - Thunder Wave", () => { await game.phaseInterceptor.to("BerryPhase", false); expect(enemyPokemon.status).toBeUndefined(); - }, TIMEOUT); + }); it("does not paralyze if the Pokemon already has a status effect", async () => { game.override.enemySpecies(Species.MAGIKARP).enemyStatusEffect(StatusEffect.BURN); @@ -71,7 +71,7 @@ describe("Moves - Thunder Wave", () => { await game.phaseInterceptor.to("BerryPhase", false); expect(enemyPokemon.status?.effect).not.toBe(StatusEffect.PARALYSIS); - }, TIMEOUT); + }); it("affects Ground types if the user has Normalize", async () => { game.override.ability(Abilities.NORMALIZE).enemySpecies(Species.DIGLETT); @@ -84,7 +84,7 @@ describe("Moves - Thunder Wave", () => { await game.phaseInterceptor.to("BerryPhase", false); expect(enemyPokemon.status?.effect).toBe(StatusEffect.PARALYSIS); - }, TIMEOUT); + }); it("does not affect Ghost types if the user has Normalize", async () => { game.override.ability(Abilities.NORMALIZE).enemySpecies(Species.HAUNTER); @@ -97,5 +97,5 @@ describe("Moves - Thunder Wave", () => { await game.phaseInterceptor.to("BerryPhase", false); expect(enemyPokemon.status).toBeUndefined(); - }, TIMEOUT); + }); }); diff --git a/src/test/moves/tidy_up.test.ts b/src/test/moves/tidy_up.test.ts index 255fe948447..8a3a0f3be76 100644 --- a/src/test/moves/tidy_up.test.ts +++ b/src/test/moves/tidy_up.test.ts @@ -8,6 +8,7 @@ import { Species } from "#enums/species"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { SubstituteTag } from "#app/data/battler-tags"; describe("Moves - Tidy Up", () => { @@ -39,7 +40,7 @@ describe("Moves - Tidy Up", () => { it("spikes are cleared", async () => { game.override.moveset([Moves.SPIKES, Moves.TIDY_UP]); game.override.enemyMoveset([Moves.SPIKES, Moves.SPIKES, Moves.SPIKES, Moves.SPIKES]); - await game.startBattle(); + await game.classicMode.startBattle(); game.move.select(Moves.SPIKES); await game.phaseInterceptor.to(TurnEndPhase); @@ -52,7 +53,7 @@ describe("Moves - Tidy Up", () => { it("stealth rocks are cleared", async () => { game.override.moveset([Moves.STEALTH_ROCK, Moves.TIDY_UP]); game.override.enemyMoveset([Moves.STEALTH_ROCK, Moves.STEALTH_ROCK, Moves.STEALTH_ROCK, Moves.STEALTH_ROCK]); - await game.startBattle(); + await game.classicMode.startBattle(); game.move.select(Moves.STEALTH_ROCK); await game.phaseInterceptor.to(TurnEndPhase); @@ -64,7 +65,7 @@ describe("Moves - Tidy Up", () => { it("toxic spikes are cleared", async () => { game.override.moveset([Moves.TOXIC_SPIKES, Moves.TIDY_UP]); game.override.enemyMoveset([Moves.TOXIC_SPIKES, Moves.TOXIC_SPIKES, Moves.TOXIC_SPIKES, Moves.TOXIC_SPIKES]); - await game.startBattle(); + await game.classicMode.startBattle(); game.move.select(Moves.TOXIC_SPIKES); await game.phaseInterceptor.to(TurnEndPhase); @@ -77,7 +78,7 @@ describe("Moves - Tidy Up", () => { game.override.moveset([Moves.STICKY_WEB, Moves.TIDY_UP]); game.override.enemyMoveset([Moves.STICKY_WEB, Moves.STICKY_WEB, Moves.STICKY_WEB, Moves.STICKY_WEB]); - await game.startBattle(); + await game.classicMode.startBattle(); game.move.select(Moves.STICKY_WEB); await game.phaseInterceptor.to(TurnEndPhase); @@ -86,21 +87,26 @@ describe("Moves - Tidy Up", () => { expect(game.scene.arena.getTag(ArenaTagType.STICKY_WEB)).toBeUndefined(); }, 20000); - it.skip("substitutes are cleared", async () => { + it("substitutes are cleared", async () => { game.override.moveset([Moves.SUBSTITUTE, Moves.TIDY_UP]); game.override.enemyMoveset([Moves.SUBSTITUTE, Moves.SUBSTITUTE, Moves.SUBSTITUTE, Moves.SUBSTITUTE]); - await game.startBattle(); + await game.classicMode.startBattle(); game.move.select(Moves.SUBSTITUTE); await game.phaseInterceptor.to(TurnEndPhase); game.move.select(Moves.TIDY_UP); await game.phaseInterceptor.to(MoveEndPhase); - // TODO: check for subs here once the move is implemented + + const pokemon = [ game.scene.getPlayerPokemon()!, game.scene.getEnemyPokemon()! ]; + pokemon.forEach(p => { + expect(p).toBeDefined(); + expect(p!.getTag(SubstituteTag)).toBeUndefined(); + }); }, 20000); it("user's stats are raised with no traps set", async () => { - await game.startBattle(); + await game.classicMode.startBattle(); const playerPokemon = game.scene.getPlayerPokemon()!; diff --git a/src/test/moves/whirlwind.test.ts b/src/test/moves/whirlwind.test.ts new file mode 100644 index 00000000000..a591a3cd6c5 --- /dev/null +++ b/src/test/moves/whirlwind.test.ts @@ -0,0 +1,53 @@ +import { BattlerIndex } from "#app/battle"; +import { allMoves } from "#app/data/move"; +import { BattlerTagType } from "#app/enums/battler-tag-type"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, it, expect, vi } from "vitest"; + +describe("Moves - Whirlwind", () => { + 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 + .battleType("single") + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.WHIRLWIND) + .enemySpecies(Species.PIDGEY); + }); + + it.each([ + { move: Moves.FLY, name: "Fly" }, + { move: Moves.BOUNCE, name: "Bounce" }, + { move: Moves.SKY_DROP, name: "Sky Drop" }, + ])("should not hit a flying target: $name (=$move)", async ({ move }) => { + game.override.moveset([move]); + await game.classicMode.startBattle([Species.STARAPTOR]); + + const staraptor = game.scene.getPlayerPokemon()!; + const whirlwind = allMoves[Moves.WHIRLWIND]; + vi.spyOn(whirlwind, "getFailedText"); + + game.move.select(move); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); + await game.toNextTurn(); + + expect(staraptor.findTag((t) => t.tagType === BattlerTagType.FLYING)).toBeDefined(); + expect(whirlwind.getFailedText).toHaveBeenCalledOnce(); + }); +}); diff --git a/src/test/moves/wide_guard.test.ts b/src/test/moves/wide_guard.test.ts index b4e6e305539..9ddd8905ff6 100644 --- a/src/test/moves/wide_guard.test.ts +++ b/src/test/moves/wide_guard.test.ts @@ -8,7 +8,7 @@ import { Stat } from "#enums/stat"; import { BerryPhase } from "#app/phases/berry-phase"; import { CommandPhase } from "#app/phases/command-phase"; -const TIMEOUT = 20 * 1000; + describe("Moves - Wide Guard", () => { let phaserGame: Phaser.Game; @@ -55,7 +55,7 @@ describe("Moves - Wide Guard", () => { await game.phaseInterceptor.to(BerryPhase, false); leadPokemon.forEach(p => expect(p.hp).toBe(p.getMaxHp())); - }, TIMEOUT + } ); test( @@ -76,7 +76,7 @@ describe("Moves - Wide Guard", () => { await game.phaseInterceptor.to(BerryPhase, false); leadPokemon.forEach(p => expect(p.getStatStage(Stat.ATK)).toBe(0)); - }, TIMEOUT + } ); test( @@ -97,7 +97,7 @@ describe("Moves - Wide Guard", () => { await game.phaseInterceptor.to(BerryPhase, false); expect(leadPokemon.some(p => p.hp < p.getMaxHp())).toBeTruthy(); - }, TIMEOUT + } ); test( @@ -120,6 +120,6 @@ describe("Moves - Wide Guard", () => { expect(leadPokemon[0].hp).toBe(leadPokemon[0].getMaxHp()); enemyPokemon.forEach(p => expect(p.hp).toBeLessThan(p.getMaxHp())); - }, TIMEOUT + } ); }); diff --git a/src/test/mystery-encounter/encounter-test-utils.ts b/src/test/mystery-encounter/encounter-test-utils.ts new file mode 100644 index 00000000000..a31ee150bfd --- /dev/null +++ b/src/test/mystery-encounter/encounter-test-utils.ts @@ -0,0 +1,176 @@ +import { Button } from "#app/enums/buttons"; +import { MysteryEncounterBattlePhase, MysteryEncounterOptionSelectedPhase, MysteryEncounterPhase, MysteryEncounterRewardsPhase } from "#app/phases/mystery-encounter-phases"; +import MysteryEncounterUiHandler from "#app/ui/mystery-encounter-ui-handler"; +import { Mode } from "#app/ui/ui"; +import GameManager from "../utils/gameManager"; +import MessageUiHandler from "#app/ui/message-ui-handler"; +import { Status, StatusEffect } from "#app/data/status-effect"; +import { expect, vi } from "vitest"; +import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import PartyUiHandler from "#app/ui/party-ui-handler"; +import OptionSelectUiHandler from "#app/ui/settings/option-select-ui-handler"; +import { isNullOrUndefined } from "#app/utils"; +import { CommandPhase } from "#app/phases/command-phase"; +import { VictoryPhase } from "#app/phases/victory-phase"; +import { MessagePhase } from "#app/phases/message-phase"; + +/** + * Runs a {@linkcode MysteryEncounter} to either the start of a battle, or to the {@linkcode MysteryEncounterRewardsPhase}, depending on the option selected + * @param game + * @param optionNo Human number, not index + * @param secondaryOptionSelect + * @param isBattle If selecting option should lead to battle, set to `true` + */ +export async function runMysteryEncounterToEnd(game: GameManager, optionNo: number, secondaryOptionSelect?: { pokemonNo: number, optionNo?: number }, isBattle: boolean = false) { + vi.spyOn(EncounterPhaseUtils, "selectPokemonForOption"); + await runSelectMysteryEncounterOption(game, optionNo, secondaryOptionSelect); + + // run the selected options phase + game.onNextPrompt("MysteryEncounterOptionSelectedPhase", Mode.MESSAGE, () => { + const uiHandler = game.scene.ui.getHandler(); + uiHandler.processInput(Button.ACTION); + }, () => game.isCurrentPhase(MysteryEncounterBattlePhase) || game.isCurrentPhase(MysteryEncounterRewardsPhase)); + + if (isBattle) { + game.onNextPrompt("DamagePhase", Mode.MESSAGE, () => { + game.setMode(Mode.MESSAGE); + game.endPhase(); + }, () => game.isCurrentPhase(CommandPhase)); + + game.onNextPrompt("CheckSwitchPhase", Mode.CONFIRM, () => { + game.setMode(Mode.MESSAGE); + game.endPhase(); + }, () => game.isCurrentPhase(CommandPhase)); + + game.onNextPrompt("CheckSwitchPhase", Mode.MESSAGE, () => { + game.setMode(Mode.MESSAGE); + game.endPhase(); + }, () => game.isCurrentPhase(CommandPhase)); + + // If a battle is started, fast forward to end of the battle + game.onNextPrompt("CommandPhase", Mode.COMMAND, () => { + game.scene.clearPhaseQueue(); + game.scene.clearPhaseQueueSplice(); + game.scene.unshiftPhase(new VictoryPhase(game.scene, 0)); + game.endPhase(); + }); + + // Handle end of battle trainer messages + game.onNextPrompt("TrainerVictoryPhase", Mode.MESSAGE, () => { + const uiHandler = game.scene.ui.getHandler(); + uiHandler.processInput(Button.ACTION); + }); + + // Handle egg hatch dialogue + game.onNextPrompt("EggLapsePhase", Mode.MESSAGE, () => { + const uiHandler = game.scene.ui.getHandler(); + uiHandler.processInput(Button.ACTION); + }); + + await game.phaseInterceptor.to(CommandPhase); + } else { + await game.phaseInterceptor.to(MysteryEncounterRewardsPhase); + } +} + +export async function runSelectMysteryEncounterOption(game: GameManager, optionNo: number, secondaryOptionSelect?: { pokemonNo: number, optionNo?: number }) { + // Handle any eventual queued messages (e.g. weather phase, etc.) + game.onNextPrompt("MessagePhase", Mode.MESSAGE, () => { + const uiHandler = game.scene.ui.getHandler(); + uiHandler.processInput(Button.ACTION); + }, () => game.isCurrentPhase(MysteryEncounterOptionSelectedPhase)); + + if (game.isCurrentPhase(MessagePhase)) { + await game.phaseInterceptor.run(MessagePhase); + } + + // dispose of intro messages + game.onNextPrompt("MysteryEncounterPhase", Mode.MESSAGE, () => { + const uiHandler = game.scene.ui.getHandler(); + uiHandler.processInput(Button.ACTION); + }, () => game.isCurrentPhase(MysteryEncounterOptionSelectedPhase)); + + await game.phaseInterceptor.to(MysteryEncounterPhase, true); + + // select the desired option + const uiHandler = game.scene.ui.getHandler(); + uiHandler.unblockInput(); // input are blocked by 1s to prevent accidental input. Tests need to handle that + + switch (optionNo) { + default: + case 1: + // no movement needed. Default cursor position + break; + case 2: + uiHandler.processInput(Button.RIGHT); + break; + case 3: + uiHandler.processInput(Button.DOWN); + break; + case 4: + uiHandler.processInput(Button.RIGHT); + uiHandler.processInput(Button.DOWN); + break; + } + + if (!isNullOrUndefined(secondaryOptionSelect?.pokemonNo)) { + await handleSecondaryOptionSelect(game, secondaryOptionSelect!.pokemonNo, secondaryOptionSelect!.optionNo); + } else { + uiHandler.processInput(Button.ACTION); + } +} + +async function handleSecondaryOptionSelect(game: GameManager, pokemonNo: number, optionNo?: number) { + // Handle secondary option selections + const partyUiHandler = game.scene.ui.handlers[Mode.PARTY] as PartyUiHandler; + vi.spyOn(partyUiHandler, "show"); + + const encounterUiHandler = game.scene.ui.getHandler(); + encounterUiHandler.processInput(Button.ACTION); + + await vi.waitFor(() => expect(partyUiHandler.show).toHaveBeenCalled()); + + for (let i = 1; i < pokemonNo; i++) { + partyUiHandler.processInput(Button.DOWN); + } + + // Open options on Pokemon + partyUiHandler.processInput(Button.ACTION); + // Click "Select" on Pokemon options + partyUiHandler.processInput(Button.ACTION); + + // If there is a second choice to make after selecting a Pokemon + if (!isNullOrUndefined(optionNo)) { + // Wait for Summary menu to close and second options to spawn + const secondOptionUiHandler = game.scene.ui.handlers[Mode.OPTION_SELECT] as OptionSelectUiHandler; + vi.spyOn(secondOptionUiHandler, "show"); + await vi.waitFor(() => expect(secondOptionUiHandler.show).toHaveBeenCalled()); + + // Navigate down to the correct option + for (let i = 1; i < optionNo!; i++) { + secondOptionUiHandler.processInput(Button.DOWN); + } + + // Select the option + secondOptionUiHandler.processInput(Button.ACTION); + } +} + +/** + * For any {@linkcode MysteryEncounter} that has a battle, can call this to skip battle and proceed to {@linkcode MysteryEncounterRewardsPhase} + * @param game + * @param runRewardsPhase + */ +export async function skipBattleRunMysteryEncounterRewardsPhase(game: GameManager, runRewardsPhase: boolean = true) { + game.scene.clearPhaseQueue(); + game.scene.clearPhaseQueueSplice(); + game.scene.getEnemyParty().forEach(p => { + p.hp = 0; + p.status = new Status(StatusEffect.FAINT); + game.scene.field.remove(p); + }); + game.scene.pushPhase(new VictoryPhase(game.scene, 0)); + game.phaseInterceptor.superEndPhase(); + game.setMode(Mode.MESSAGE); + await game.phaseInterceptor.to(MysteryEncounterRewardsPhase, runRewardsPhase); +} diff --git a/src/test/mystery-encounter/encounters/a-trainers-test-encounter.test.ts b/src/test/mystery-encounter/encounters/a-trainers-test-encounter.test.ts new file mode 100644 index 00000000000..3dc90427eb2 --- /dev/null +++ b/src/test/mystery-encounter/encounters/a-trainers-test-encounter.test.ts @@ -0,0 +1,189 @@ +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { HUMAN_TRANSITABLE_BIOMES } from "#app/data/mystery-encounters/mystery-encounters"; +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { runMysteryEncounterToEnd, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounter-test-utils"; +import BattleScene from "#app/battle-scene"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; +import { ATrainersTestEncounter } from "#app/data/mystery-encounters/encounters/a-trainers-test-encounter"; +import { EggTier } from "#enums/egg-type"; +import { CommandPhase } from "#app/phases/command-phase"; +import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; +import { PartyHealPhase } from "#app/phases/party-heal-phase"; + +const namespace = "mysteryEncounter:aTrainersTest"; +const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; +const defaultBiome = Biome.CAVE; +const defaultWave = 45; + +describe("A Trainer's Test - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(); + + const biomeMap = new Map([ + [Biome.VOLCANO, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]], + ]); + HUMAN_TRANSITABLE_BIOMES.forEach(biome => { + biomeMap.set(biome, [MysteryEncounterType.A_TRAINERS_TEST]); + }); + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue(biomeMap); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.A_TRAINERS_TEST, defaultParty); + + expect(ATrainersTestEncounter.encounterType).toBe(MysteryEncounterType.A_TRAINERS_TEST); + expect(ATrainersTestEncounter.encounterTier).toBe(MysteryEncounterTier.ROGUE); + expect(ATrainersTestEncounter.dialogue).toBeDefined(); + expect(ATrainersTestEncounter.dialogue.intro).toBeDefined(); + expect(ATrainersTestEncounter.dialogue.intro?.[0].speaker).toBeDefined(); + expect(ATrainersTestEncounter.dialogue.intro?.[0].text).toBeDefined(); + expect(ATrainersTestEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`); + expect(ATrainersTestEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`); + expect(ATrainersTestEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`); + expect(ATrainersTestEncounter.options.length).toBe(2); + }); + + it("should initialize fully ", async () => { + initSceneWithoutEncounterPhase(scene, defaultParty); + scene.currentBattle.mysteryEncounter = ATrainersTestEncounter; + + const { onInit } = ATrainersTestEncounter; + + expect(ATrainersTestEncounter.onInit).toBeDefined(); + + ATrainersTestEncounter.populateDialogueTokensFromRequirements(scene); + const onInitResult = onInit!(scene); + + expect(ATrainersTestEncounter.dialogueTokens?.statTrainerName).toBeDefined(); + expect(ATrainersTestEncounter.misc.trainerType).toBeDefined(); + expect(ATrainersTestEncounter.misc.trainerNameKey).toBeDefined(); + expect(ATrainersTestEncounter.misc.trainerEggDescription).toBeDefined(); + expect(ATrainersTestEncounter.dialogue.intro).toBeDefined(); + expect(ATrainersTestEncounter.options[1].dialogue?.selected).toBeDefined(); + expect(onInitResult).toBe(true); + }); + + describe("Option 1 - Accept the Challenge", () => { + it("should have the correct properties", () => { + const option = ATrainersTestEncounter.options[0]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue!.buttonLabel).toStrictEqual(`${namespace}.option.1.label`); + expect(option.dialogue!.buttonTooltip).toStrictEqual(`${namespace}.option.1.tooltip`); + }); + + it("Should start battle against the trainer", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.A_TRAINERS_TEST, defaultParty); + await runMysteryEncounterToEnd(game, 1, undefined, true); + + const enemyField = scene.getEnemyField(); + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(enemyField.length).toBe(1); + expect(scene.currentBattle.trainer).toBeDefined(); + expect(["buck", "cheryl", "marley", "mira", "riley"].includes(scene.currentBattle.trainer!.config.name.toLowerCase())).toBeTruthy(); + expect(enemyField[0]).toBeDefined(); + }); + + it("Should reward the player with an Epic or Legendary egg", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.A_TRAINERS_TEST, defaultParty); + + const eggsBefore = scene.gameData.eggs; + expect(eggsBefore).toBeDefined(); + const eggsBeforeLength = eggsBefore.length; + + await runMysteryEncounterToEnd(game, 1, undefined, true); + await skipBattleRunMysteryEncounterRewardsPhase(game); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + + const eggsAfter = scene.gameData.eggs; + expect(eggsAfter).toBeDefined(); + expect(eggsBeforeLength + 1).toBe(eggsAfter.length); + const eggTier = eggsAfter[eggsAfter.length - 1].tier; + expect(eggTier === EggTier.ULTRA || eggTier === EggTier.MASTER).toBeTruthy(); + }); + }); + + describe("Option 2 - Decline the Challenge", () => { + beforeEach(() => { + // Mock sound object + vi.spyOn(scene, "playSoundWithoutBgm").mockImplementation(() => { + return { + totalDuration: 1, + destroy: () => null + } as any; + }); + }); + + it("should have the correct properties", () => { + const option = ATrainersTestEncounter.options[1]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue?.buttonLabel).toStrictEqual(`${namespace}.option.2.label`); + expect(option.dialogue?.buttonTooltip).toStrictEqual(`${namespace}.option.2.tooltip`); + }); + + it("Should fully heal the party", async () => { + const phaseSpy = vi.spyOn(scene, "unshiftPhase"); + + await game.runToMysteryEncounter(MysteryEncounterType.A_TRAINERS_TEST, defaultParty); + await runMysteryEncounterToEnd(game, 2); + + const partyHealPhases = phaseSpy.mock.calls.filter(p => p[0] instanceof PartyHealPhase).map(p => p[0]); + expect(partyHealPhases.length).toBe(1); + }); + + it("Should reward the player with a Rare egg", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.A_TRAINERS_TEST, defaultParty); + + const eggsBefore = scene.gameData.eggs; + expect(eggsBefore).toBeDefined(); + const eggsBeforeLength = eggsBefore.length; + + await runMysteryEncounterToEnd(game, 2); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + + const eggsAfter = scene.gameData.eggs; + expect(eggsAfter).toBeDefined(); + expect(eggsBeforeLength + 1).toBe(eggsAfter.length); + const eggTier = eggsAfter[eggsAfter.length - 1].tier; + expect(eggTier).toBe(EggTier.GREAT); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.A_TRAINERS_TEST, defaultParty); + await runMysteryEncounterToEnd(game, 2); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); +}); diff --git a/src/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts b/src/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts new file mode 100644 index 00000000000..58c8e1fbc30 --- /dev/null +++ b/src/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts @@ -0,0 +1,254 @@ +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { runMysteryEncounterToEnd, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounter-test-utils"; +import BattleScene from "#app/battle-scene"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { BerryModifier, PokemonHeldItemModifier } from "#app/modifier/modifier"; +import { BerryType } from "#enums/berry-type"; +import { AbsoluteAvariceEncounter } from "#app/data/mystery-encounters/encounters/absolute-avarice-encounter"; +import { Moves } from "#enums/moves"; +import { CommandPhase } from "#app/phases/command-phase"; +import { MovePhase } from "#app/phases/move-phase"; +import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; + +const namespace = "mysteryEncounter:absoluteAvarice"; +const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; +const defaultBiome = Biome.PLAINS; +const defaultWave = 45; + +describe("Absolute Avarice - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(); + + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( + new Map([ + [Biome.PLAINS, [MysteryEncounterType.ABSOLUTE_AVARICE]], + [Biome.VOLCANO, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]], + ]) + ); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.ABSOLUTE_AVARICE, defaultParty); + + expect(AbsoluteAvariceEncounter.encounterType).toBe(MysteryEncounterType.ABSOLUTE_AVARICE); + expect(AbsoluteAvariceEncounter.encounterTier).toBe(MysteryEncounterTier.GREAT); + expect(AbsoluteAvariceEncounter.dialogue).toBeDefined(); + expect(AbsoluteAvariceEncounter.dialogue.intro).toStrictEqual([{ text: `${namespace}.intro` }]); + expect(AbsoluteAvariceEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`); + expect(AbsoluteAvariceEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`); + expect(AbsoluteAvariceEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`); + expect(AbsoluteAvariceEncounter.options.length).toBe(3); + }); + + it("should not spawn outside of proper biomes", async () => { + game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT); + game.override.startingBiome(Biome.VOLCANO); + await game.runToMysteryEncounter(); + + expect(game.scene.currentBattle.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.ABSOLUTE_AVARICE); + }); + + it("should not spawn if player does not have enough berries", async () => { + scene.modifiers = []; + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.ABSOLUTE_AVARICE); + }); + + it("should spawn if player has enough berries", async () => { + game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT); + game.override.startingHeldItems([{name: "BERRY", count: 2, type: BerryType.SITRUS}, {name: "BERRY", count: 3, type: BerryType.GANLON}]); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).toBe(MysteryEncounterType.ABSOLUTE_AVARICE); + }); + + it("should remove all player's berries at the start of the encounter", async () => { + game.override.startingHeldItems([{name: "BERRY", count: 2, type: BerryType.SITRUS}, {name: "BERRY", count: 3, type: BerryType.GANLON}]); + + await game.runToMysteryEncounter(MysteryEncounterType.ABSOLUTE_AVARICE, defaultParty); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).toBe(MysteryEncounterType.ABSOLUTE_AVARICE); + expect(scene.modifiers?.length).toBe(0); + }); + + describe("Option 1 - Fight the Greedent", () => { + it("should have the correct properties", () => { + const option1 = AbsoluteAvariceEncounter.options[0]; + expect(option1.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option1.dialogue).toBeDefined(); + expect(option1.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }); + }); + + it("should start battle against Greedent", async () => { + const phaseSpy = vi.spyOn(scene, "pushPhase"); + + await game.runToMysteryEncounter(MysteryEncounterType.ABSOLUTE_AVARICE, defaultParty); + await runMysteryEncounterToEnd(game, 1, undefined, true); + + const enemyField = scene.getEnemyField(); + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(enemyField.length).toBe(1); + expect(enemyField[0].species.speciesId).toBe(Species.GREEDENT); + const moveset = enemyField[0].moveset.map(m => m?.moveId); + expect(moveset?.length).toBe(4); + expect(moveset).toEqual([Moves.THRASH, Moves.BODY_PRESS, Moves.STUFF_CHEEKS, Moves.SLACK_OFF]); + + const movePhases = phaseSpy.mock.calls.filter(p => p[0] instanceof MovePhase).map(p => p[0]); + expect(movePhases.length).toBe(1); + expect(movePhases.filter(p => (p as MovePhase).move.moveId === Moves.STUFF_CHEEKS).length).toBe(1); // Stuff Cheeks used before battle + }); + + it("should give reviver seed to each pokemon after battle", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.ABSOLUTE_AVARICE, defaultParty); + await runMysteryEncounterToEnd(game, 1, undefined, true); + await skipBattleRunMysteryEncounterRewardsPhase(game); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + + for (const partyPokemon of scene.getParty()) { + const pokemonId = partyPokemon.id; + const pokemonItems = scene.findModifiers(m => m instanceof PokemonHeldItemModifier + && (m as PokemonHeldItemModifier).pokemonId === pokemonId, true) as PokemonHeldItemModifier[]; + const revSeed = pokemonItems.find(i => i.type.name === "Reviver Seed"); + expect(revSeed).toBeDefined; + expect(revSeed?.stackCount).toBe(1); + } + }); + }); + + describe("Option 2 - Reason with It", () => { + it("should have the correct properties", () => { + const option = AbsoluteAvariceEncounter.options[1]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected`, + }, + ], + }); + }); + + it("Should return 3 (2/5ths floored) berries if 8 were stolen", {retry: 5}, async () => { + game.override.startingHeldItems([{name: "BERRY", count: 2, type: BerryType.SITRUS}, {name: "BERRY", count: 3, type: BerryType.GANLON}, {name: "BERRY", count: 3, type: BerryType.APICOT}]); + + await game.runToMysteryEncounter(MysteryEncounterType.ABSOLUTE_AVARICE, defaultParty); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).toBe(MysteryEncounterType.ABSOLUTE_AVARICE); + expect(scene.modifiers?.length).toBe(0); + + await runMysteryEncounterToEnd(game, 2); + + const berriesAfter = scene.findModifiers(m => m instanceof BerryModifier); + const berryCountAfter = berriesAfter.reduce((a, b) => a + b.stackCount, 0); + expect(berriesAfter).toBeDefined(); + expect(berryCountAfter).toBe(3); + }); + + it("Should return 2 (2/5ths floored) berries if 7 were stolen", {retry: 5}, async () => { + game.override.startingHeldItems([{name: "BERRY", count: 2, type: BerryType.SITRUS}, {name: "BERRY", count: 3, type: BerryType.GANLON}, {name: "BERRY", count: 2, type: BerryType.APICOT}]); + + await game.runToMysteryEncounter(MysteryEncounterType.ABSOLUTE_AVARICE, defaultParty); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).toBe(MysteryEncounterType.ABSOLUTE_AVARICE); + expect(scene.modifiers?.length).toBe(0); + + await runMysteryEncounterToEnd(game, 2); + + const berriesAfter = scene.findModifiers(m => m instanceof BerryModifier); + const berryCountAfter = berriesAfter.reduce((a, b) => a + b.stackCount, 0); + expect(berriesAfter).toBeDefined(); + expect(berryCountAfter).toBe(2); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.ABSOLUTE_AVARICE, defaultParty); + await runMysteryEncounterToEnd(game, 2); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 3 - Let it have the food", () => { + it("should have the correct properties", () => { + const option = AbsoluteAvariceEncounter.options[2]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + selected: [ + { + text: `${namespace}.option.3.selected`, + }, + ], + }); + }); + + it("should add Greedent to the party", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.ABSOLUTE_AVARICE, defaultParty); + const partyCountBefore = scene.getParty().length; + + await runMysteryEncounterToEnd(game, 3); + const partyCountAfter = scene.getParty().length; + + expect(partyCountBefore + 1).toBe(partyCountAfter); + const greedent = scene.getParty()[scene.getParty().length - 1]; + expect(greedent.species.speciesId).toBe(Species.GREEDENT); + const moveset = greedent.moveset.map(m => m?.moveId); + expect(moveset?.length).toBe(4); + expect(moveset).toEqual([Moves.THRASH, Moves.BODY_PRESS, Moves.STUFF_CHEEKS, Moves.SLACK_OFF]); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.ABSOLUTE_AVARICE, defaultParty); + await runMysteryEncounterToEnd(game, 3); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); +}); diff --git a/src/test/mystery-encounter/encounters/an-offer-you-cant-refuse-encounter.test.ts b/src/test/mystery-encounter/encounters/an-offer-you-cant-refuse-encounter.test.ts new file mode 100644 index 00000000000..c39e636b462 --- /dev/null +++ b/src/test/mystery-encounter/encounters/an-offer-you-cant-refuse-encounter.test.ts @@ -0,0 +1,243 @@ +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { HUMAN_TRANSITABLE_BIOMES } from "#app/data/mystery-encounters/mystery-encounters"; +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { runMysteryEncounterToEnd } from "#test/mystery-encounter/encounter-test-utils"; +import BattleScene from "#app/battle-scene"; +import { PlayerPokemon, PokemonMove } from "#app/field/pokemon"; +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"; +import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { Moves } from "#enums/moves"; +import { ShinyRateBoosterModifier } from "#app/modifier/modifier"; +import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; + +const namespace = "mysteryEncounter:offerYouCantRefuse"; +/** Gyarados for Indimidate */ +const defaultParty = [Species.GYARADOS, Species.GENGAR, Species.ABRA]; +const defaultBiome = Biome.CAVE; +const defaultWave = 45; + +describe("An Offer You Can't Refuse - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(); + + const biomeMap = new Map([ + [Biome.VOLCANO, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]], + ]); + HUMAN_TRANSITABLE_BIOMES.forEach(biome => { + biomeMap.set(biome, [MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE]); + }); + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue(biomeMap); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE, defaultParty); + + expect(AnOfferYouCantRefuseEncounter.encounterType).toBe(MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE); + expect(AnOfferYouCantRefuseEncounter.encounterTier).toBe(MysteryEncounterTier.GREAT); + expect(AnOfferYouCantRefuseEncounter.dialogue).toBeDefined(); + expect(AnOfferYouCantRefuseEncounter.dialogue.intro).toStrictEqual([ + { text: `${namespace}.intro` }, + { speaker: `${namespace}.speaker`, text: `${namespace}.intro_dialogue` } + ]); + expect(AnOfferYouCantRefuseEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`); + expect(AnOfferYouCantRefuseEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`); + expect(AnOfferYouCantRefuseEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`); + expect(AnOfferYouCantRefuseEncounter.options.length).toBe(3); + }); + + it("should not spawn outside of HUMAN_TRANSITABLE_BIOMES", async () => { + game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT); + game.override.startingBiome(Biome.VOLCANO); + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE); + }); + + it("should initialize fully ", async () => { + initSceneWithoutEncounterPhase(scene, defaultParty); + scene.currentBattle.mysteryEncounter = AnOfferYouCantRefuseEncounter; + + const { onInit } = AnOfferYouCantRefuseEncounter; + + expect(AnOfferYouCantRefuseEncounter.onInit).toBeDefined(); + + AnOfferYouCantRefuseEncounter.populateDialogueTokensFromRequirements(scene); + const onInitResult = onInit!(scene); + + expect(AnOfferYouCantRefuseEncounter.dialogueTokens?.strongestPokemon).toBeDefined(); + expect(AnOfferYouCantRefuseEncounter.dialogueTokens?.price).toBeDefined(); + expect(AnOfferYouCantRefuseEncounter.dialogueTokens?.option2PrimaryAbility).toBe("Intimidate"); + expect(AnOfferYouCantRefuseEncounter.dialogueTokens?.moveOrAbility).toBe("Intimidate"); + expect(AnOfferYouCantRefuseEncounter.misc.pokemon instanceof PlayerPokemon).toBeTruthy(); + expect(AnOfferYouCantRefuseEncounter.misc?.price?.toString()).toBe(AnOfferYouCantRefuseEncounter.dialogueTokens?.price); + expect(onInitResult).toBe(true); + }); + + describe("Option 1 - Sell your Pokemon for money and a Shiny Charm", () => { + it("should have the correct properties", () => { + const option = AnOfferYouCantRefuseEncounter.options[0]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + speaker: `${namespace}.speaker`, + text: `${namespace}.option.1.selected`, + }, + ], + }); + }); + + it("Should update the player's money properly", async () => { + const initialMoney = 20000; + scene.money = initialMoney; + const updateMoneySpy = vi.spyOn(EncounterPhaseUtils, "updatePlayerMoney"); + + await game.runToMysteryEncounter(MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE, defaultParty); + await runMysteryEncounterToEnd(game, 1); + + const price = scene.currentBattle.mysteryEncounter!.misc.price; + + expect(updateMoneySpy).toHaveBeenCalledWith(scene, price); + expect(scene.money).toBe(initialMoney + price); + }); + + it("Should give the player a Shiny Charm", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE, defaultParty); + await runMysteryEncounterToEnd(game, 1); + + const itemModifier = scene.findModifier(m => m instanceof ShinyRateBoosterModifier) as ShinyRateBoosterModifier; + + expect(itemModifier).toBeDefined(); + expect(itemModifier?.stackCount).toBe(1); + }); + + it("Should remove the Pokemon from the party", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE, defaultParty); + + const initialPartySize = scene.getParty().length; + const pokemonName = scene.currentBattle.mysteryEncounter!.misc.pokemon.name; + + await runMysteryEncounterToEnd(game, 1); + + expect(scene.getParty().length).toBe(initialPartySize - 1); + expect(scene.getParty().find(p => p.name === pokemonName)).toBeUndefined(); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE, defaultParty); + await runMysteryEncounterToEnd(game, 1); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 2 - Extort the Kid", () => { + it("should have the correct properties", () => { + const option = AnOfferYouCantRefuseEncounter.options[1]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + disabledButtonTooltip: `${namespace}.option.2.tooltip_disabled`, + selected: [ + { + speaker: `${namespace}.speaker`, + text: `${namespace}.option.2.selected`, + }, + ], + }); + }); + + it("should award EXP to a pokemon with an ability in EXTORTION_ABILITIES", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE, defaultParty); + const party = scene.getParty(); + const gyarados = party.find((pkm) => pkm.species.speciesId === Species.GYARADOS)!; + const expBefore = gyarados.exp; + + await runMysteryEncounterToEnd(game, 2); + await game.phaseInterceptor.to(SelectModifierPhase, false); + + expect(gyarados.exp).toBe(expBefore + Math.floor(getPokemonSpecies(Species.LIEPARD).baseExp * defaultWave / 5 + 1)); + }); + + it("should award EXP to a pokemon with a move in EXTORTION_MOVES", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE, [Species.ABRA]); + const party = scene.getParty(); + const abra = party.find((pkm) => pkm.species.speciesId === Species.ABRA)!; + abra.moveset = [new PokemonMove(Moves.BEAT_UP)]; + const expBefore = abra.exp; + + await runMysteryEncounterToEnd(game, 2); + await game.phaseInterceptor.to(SelectModifierPhase, false); + + expect(abra.exp).toBe(expBefore + Math.floor(getPokemonSpecies(Species.LIEPARD).baseExp * defaultWave / 5 + 1)); + }); + + it("Should update the player's money properly", async () => { + const initialMoney = 20000; + scene.money = initialMoney; + const updateMoneySpy = vi.spyOn(EncounterPhaseUtils, "updatePlayerMoney"); + + await game.runToMysteryEncounter(MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE, defaultParty); + await runMysteryEncounterToEnd(game, 2); + + const price = scene.currentBattle.mysteryEncounter!.misc.price; + + expect(updateMoneySpy).toHaveBeenCalledWith(scene, price); + expect(scene.money).toBe(initialMoney + price); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE, defaultParty); + await runMysteryEncounterToEnd(game, 2); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 3 - Leave", () => { + it.each(Array.from({length: 30}))("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE, defaultParty); + await runMysteryEncounterToEnd(game, 3); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); +}); diff --git a/src/test/mystery-encounter/encounters/berries-abound-encounter.test.ts b/src/test/mystery-encounter/encounters/berries-abound-encounter.test.ts new file mode 100644 index 00000000000..78f4a477216 --- /dev/null +++ b/src/test/mystery-encounter/encounters/berries-abound-encounter.test.ts @@ -0,0 +1,229 @@ +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { runMysteryEncounterToEnd, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounter-test-utils"; +import BattleScene from "#app/battle-scene"; +import { Mode } from "#app/ui/ui"; +import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; +import { BerryModifier } from "#app/modifier/modifier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; +import { BerriesAboundEncounter } from "#app/data/mystery-encounters/encounters/berries-abound-encounter"; +import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import * as EncounterDialogueUtils from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { CommandPhase } from "#app/phases/command-phase"; +import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; + +const namespace = "mysteryEncounter:berriesAbound"; +const defaultParty = [Species.PYUKUMUKU, Species.MAGIKARP, Species.PIKACHU]; +const defaultBiome = Biome.CAVE; +const defaultWave = 45; + +describe("Berries Abound - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(); + game.override.startingModifier([]); + game.override.startingHeldItems([]); + + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( + new Map([ + [Biome.CAVE, [MysteryEncounterType.BERRIES_ABOUND]], + ]) + ); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.BERRIES_ABOUND, defaultParty); + + expect(BerriesAboundEncounter.encounterType).toBe(MysteryEncounterType.BERRIES_ABOUND); + expect(BerriesAboundEncounter.encounterTier).toBe(MysteryEncounterTier.COMMON); + expect(BerriesAboundEncounter.dialogue).toBeDefined(); + expect(BerriesAboundEncounter.dialogue.intro).toStrictEqual([{ text: `${namespace}.intro` }]); + expect(BerriesAboundEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`); + expect(BerriesAboundEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`); + expect(BerriesAboundEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`); + expect(BerriesAboundEncounter.options.length).toBe(3); + }); + + it("should initialize fully", async () => { + initSceneWithoutEncounterPhase(scene, defaultParty); + scene.currentBattle.mysteryEncounter = BerriesAboundEncounter; + + const { onInit } = BerriesAboundEncounter; + + expect(BerriesAboundEncounter.onInit).toBeDefined(); + + BerriesAboundEncounter.populateDialogueTokensFromRequirements(scene); + const onInitResult = onInit!(scene); + + const config = BerriesAboundEncounter.enemyPartyConfigs[0]; + expect(config).toBeDefined(); + expect(config.pokemonConfigs?.[0].isBoss).toBe(true); + expect(onInitResult).toBe(true); + }); + + describe("Option 1 - Fight", () => { + it("should have the correct properties", () => { + const option = BerriesAboundEncounter.options[0]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }); + }); + + it("should start a fight against the boss", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.BERRIES_ABOUND, defaultParty); + + const config = game.scene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0]; + const speciesToSpawn = config.pokemonConfigs?.[0].species.speciesId; + + await runMysteryEncounterToEnd(game, 1, undefined, true); + + const enemyField = scene.getEnemyField(); + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(enemyField.length).toBe(1); + expect(enemyField[0].species.speciesId).toBe(speciesToSpawn); + }); + + /** + * Related issue-comment: {@link https://github.com/pagefaultgames/pokerogue/issues/4300#issuecomment-2362849444} + */ + it("should reward the player with X berries based on wave", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.BERRIES_ABOUND, defaultParty); + + const numBerries = game.scene.currentBattle.mysteryEncounter!.misc.numBerries; + + // Clear out any pesky mods that slipped through test spin-up + scene.modifiers.forEach(mod => { + scene.removeModifier(mod); + }); + + await runMysteryEncounterToEnd(game, 1, undefined, true); + await skipBattleRunMysteryEncounterRewardsPhase(game); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + + const berriesAfter = scene.findModifiers(m => m instanceof BerryModifier) as BerryModifier[]; + const berriesAfterCount = berriesAfter.reduce((a, b) => a + b.stackCount, 0); + + expect(numBerries).toBe(berriesAfterCount); + }); + + it("should spawn a shop with 5 berries", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.BERRIES_ABOUND, defaultParty); + await runMysteryEncounterToEnd(game, 1, undefined, true); + await skipBattleRunMysteryEncounterRewardsPhase(game); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(5); + for (const option of modifierSelectHandler.options) { + expect(option.modifierTypeOption.type.id).toContain("BERRY"); + } + }); + }); + + describe("Option 2 - Race to the Bush", () => { + it("should have the correct properties", () => { + const option = BerriesAboundEncounter.options[1]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + }); + }); + + it("should start battle if fastest pokemon is slower than boss", async () => { + const encounterTextSpy = vi.spyOn(EncounterDialogueUtils, "showEncounterText"); + await game.runToMysteryEncounter(MysteryEncounterType.BERRIES_ABOUND, defaultParty); + + const config = game.scene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0]; + const speciesToSpawn = config.pokemonConfigs?.[0].species.speciesId; + // Setting enemy's level arbitrarily high to outspeed + config.pokemonConfigs![0].dataSource!.level = 1000; + + await runMysteryEncounterToEnd(game, 2, undefined, true); + + const enemyField = scene.getEnemyField(); + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(enemyField.length).toBe(1); + expect(enemyField[0].species.speciesId).toBe(speciesToSpawn); + + // Should be enraged + expect(enemyField[0].summonData.statStages).toEqual([1, 1, 1, 1, 1, 0, 0]); + expect(encounterTextSpy).toHaveBeenCalledWith(expect.any(BattleScene), `${namespace}.option.2.selected_bad`); + }); + + it("Should skip battle when fastest pokemon is faster than boss", async () => { + vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + vi.spyOn(EncounterDialogueUtils, "showEncounterText"); + + await game.runToMysteryEncounter(MysteryEncounterType.BERRIES_ABOUND, defaultParty); + + scene.getParty().forEach(pkm => { + vi.spyOn(pkm, "getStat").mockReturnValue(9999); // for ease return for every stat + }); + + await runMysteryEncounterToEnd(game, 2); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(5); + for (const option of modifierSelectHandler.options) { + expect(option.modifierTypeOption.type.id).toContain("BERRY"); + } + + expect(EncounterDialogueUtils.showEncounterText).toHaveBeenCalledWith(expect.any(BattleScene), `${namespace}.option.2.selected`); + expect(EncounterPhaseUtils.leaveEncounterWithoutBattle).toBeCalled(); + }); + }); + + describe("Option 3 - Leave", () => { + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.BERRIES_ABOUND, defaultParty); + await runMysteryEncounterToEnd(game, 3); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); +}); diff --git a/src/test/mystery-encounter/encounters/bug-type-superfan-encounter.test.ts b/src/test/mystery-encounter/encounters/bug-type-superfan-encounter.test.ts new file mode 100644 index 00000000000..247acc9e5b6 --- /dev/null +++ b/src/test/mystery-encounter/encounters/bug-type-superfan-encounter.test.ts @@ -0,0 +1,569 @@ +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { runMysteryEncounterToEnd, runSelectMysteryEncounterOption, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounter-test-utils"; +import { Moves } from "#enums/moves"; +import BattleScene from "#app/battle-scene"; +import { PokemonMove } from "#app/field/pokemon"; +import { Mode } from "#app/ui/ui"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; +import { TrainerType } from "#enums/trainer-type"; +import { MysteryEncounterPhase, MysteryEncounterRewardsPhase } from "#app/phases/mystery-encounter-phases"; +import { ContactHeldItemTransferChanceModifier } from "#app/modifier/modifier"; +import { CommandPhase } from "#app/phases/command-phase"; +import { BugTypeSuperfanEncounter } from "#app/data/mystery-encounters/encounters/bug-type-superfan-encounter"; +import * as encounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; +import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; + +const namespace = "mysteryEncounter:bugTypeSuperfan"; +const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.WEEDLE]; +const defaultBiome = Biome.CAVE; +const defaultWave = 24; + +const POOL_1_POKEMON = [ + Species.PARASECT, + Species.VENOMOTH, + Species.LEDIAN, + Species.ARIADOS, + Species.YANMA, + Species.BEAUTIFLY, + Species.DUSTOX, + Species.MASQUERAIN, + Species.NINJASK, + Species.VOLBEAT, + Species.ILLUMISE, + Species.ANORITH, + Species.KRICKETUNE, + Species.WORMADAM, + Species.MOTHIM, + Species.SKORUPI, + Species.JOLTIK, + Species.LARVESTA, + Species.VIVILLON, + Species.CHARJABUG, + Species.RIBOMBEE, + Species.SPIDOPS, + Species.LOKIX +]; + +const POOL_2_POKEMON = [ + Species.SCYTHER, + Species.PINSIR, + Species.HERACROSS, + Species.FORRETRESS, + Species.SCIZOR, + Species.SHUCKLE, + Species.SHEDINJA, + Species.ARMALDO, + Species.VESPIQUEN, + Species.DRAPION, + Species.YANMEGA, + Species.LEAVANNY, + Species.SCOLIPEDE, + Species.CRUSTLE, + Species.ESCAVALIER, + Species.ACCELGOR, + Species.GALVANTULA, + Species.VIKAVOLT, + Species.ARAQUANID, + Species.ORBEETLE, + Species.CENTISKORCH, + Species.FROSMOTH, + Species.KLEAVOR, +]; + +const POOL_3_POKEMON: { species: Species, formIndex?: number }[] = [ + { + species: Species.PINSIR, + formIndex: 1 + }, + { + species: Species.SCIZOR, + formIndex: 1 + }, + { + species: Species.HERACROSS, + formIndex: 1 + }, + { + species: Species.ORBEETLE, + formIndex: 1 + }, + { + species: Species.CENTISKORCH, + formIndex: 1 + }, + { + species: Species.DURANT, + }, + { + species: Species.VOLCARONA, + }, + { + species: Species.GOLISOPOD, + }, +]; + +const POOL_4_POKEMON = [ + Species.GENESECT, + Species.SLITHER_WING, + Species.BUZZWOLE, + Species.PHEROMOSA +]; + +const PHYSICAL_TUTOR_MOVES = [ + Moves.MEGAHORN, + Moves.X_SCISSOR, + Moves.ATTACK_ORDER, + Moves.PIN_MISSILE, + Moves.FIRST_IMPRESSION +]; + +const SPECIAL_TUTOR_MOVES = [ + Moves.SILVER_WIND, + Moves.BUG_BUZZ, + Moves.SIGNAL_BEAM, + Moves.POLLEN_PUFF +]; + +const STATUS_TUTOR_MOVES = [ + Moves.STRING_SHOT, + Moves.STICKY_WEB, + Moves.SILK_TRAP, + Moves.RAGE_POWDER, + Moves.HEAL_ORDER +]; + +const MISC_TUTOR_MOVES = [ + Moves.BUG_BITE, + Moves.LEECH_LIFE, + Moves.DEFEND_ORDER, + Moves.QUIVER_DANCE, + Moves.TAIL_GLOW, + Moves.INFESTATION, + Moves.U_TURN +]; + +describe("Bug-Type Superfan - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(); + + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( + new Map([ + [Biome.CAVE, [MysteryEncounterType.BUG_TYPE_SUPERFAN]], + ]) + ); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, defaultParty); + + expect(BugTypeSuperfanEncounter.encounterType).toBe(MysteryEncounterType.BUG_TYPE_SUPERFAN); + expect(BugTypeSuperfanEncounter.encounterTier).toBe(MysteryEncounterTier.GREAT); + expect(BugTypeSuperfanEncounter.dialogue).toBeDefined(); + expect(BugTypeSuperfanEncounter.dialogue.intro).toStrictEqual([ + { + text: `${namespace}.intro`, + }, + { + speaker: `${namespace}.speaker`, + text: `${namespace}.intro_dialogue`, + }, + ]); + expect(BugTypeSuperfanEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`); + expect(BugTypeSuperfanEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`); + expect(BugTypeSuperfanEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`); + expect(BugTypeSuperfanEncounter.options.length).toBe(3); + }); + + it("should initialize fully", async () => { + initSceneWithoutEncounterPhase(scene, defaultParty); + scene.currentBattle.mysteryEncounter = BugTypeSuperfanEncounter; + + const { onInit } = BugTypeSuperfanEncounter; + + expect(BugTypeSuperfanEncounter.onInit).toBeDefined(); + + BugTypeSuperfanEncounter.populateDialogueTokensFromRequirements(scene); + const onInitResult = onInit!(scene); + const config = BugTypeSuperfanEncounter.enemyPartyConfigs[0]; + + expect(config).toBeDefined(); + expect(config.trainerConfig?.trainerType).toBe(TrainerType.BUG_TYPE_SUPERFAN); + expect(config.trainerConfig?.partyTemplates).toBeDefined(); + expect(config.female).toBe(true); + expect(onInitResult).toBe(true); + }); + + describe("Option 1 - Battle the Bug-Type Superfan", () => { + it("should have the correct properties", () => { + const option = BugTypeSuperfanEncounter.options[0]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + speaker: `${namespace}.speaker`, + text: `${namespace}.option.1.selected`, + }, + ], + }); + }); + + it("should start battle against the Bug-Type Superfan with wave 30 party template", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, defaultParty); + await runMysteryEncounterToEnd(game, 1, undefined, true); + + const enemyParty = scene.getEnemyParty(); + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(enemyParty.length).toBe(2); + expect(scene.currentBattle.trainer?.config.trainerType).toBe(TrainerType.BUG_TYPE_SUPERFAN); + expect(enemyParty[0].species.speciesId).toBe(Species.BEEDRILL); + expect(enemyParty[1].species.speciesId).toBe(Species.BUTTERFREE); + }); + + it("should start battle against the Bug-Type Superfan with wave 50 party template", async () => { + game.override.startingWave(43); + await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, defaultParty); + await runMysteryEncounterToEnd(game, 1, undefined, true); + + const enemyParty = scene.getEnemyParty(); + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(enemyParty.length).toBe(3); + expect(scene.currentBattle.trainer?.config.trainerType).toBe(TrainerType.BUG_TYPE_SUPERFAN); + expect(enemyParty[0].species.speciesId).toBe(Species.BEEDRILL); + expect(enemyParty[1].species.speciesId).toBe(Species.BUTTERFREE); + expect(POOL_1_POKEMON.includes(enemyParty[2].species.speciesId)).toBe(true); + }); + + it("should start battle against the Bug-Type Superfan with wave 70 party template", async () => { + game.override.startingWave(61); + await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, defaultParty); + await runMysteryEncounterToEnd(game, 1, undefined, true); + + const enemyParty = scene.getEnemyParty(); + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(enemyParty.length).toBe(4); + expect(scene.currentBattle.trainer?.config.trainerType).toBe(TrainerType.BUG_TYPE_SUPERFAN); + expect(enemyParty[0].species.speciesId).toBe(Species.BEEDRILL); + expect(enemyParty[1].species.speciesId).toBe(Species.BUTTERFREE); + expect(POOL_1_POKEMON.includes(enemyParty[2].species.speciesId)).toBe(true); + expect(POOL_2_POKEMON.includes(enemyParty[3].species.speciesId)).toBe(true); + }); + + it("should start battle against the Bug-Type Superfan with wave 100 party template", async () => { + game.override.startingWave(81); + await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, defaultParty); + await runMysteryEncounterToEnd(game, 1, undefined, true); + + const enemyParty = scene.getEnemyParty(); + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(enemyParty.length).toBe(5); + expect(scene.currentBattle.trainer?.config.trainerType).toBe(TrainerType.BUG_TYPE_SUPERFAN); + expect(enemyParty[0].species.speciesId).toBe(Species.BEEDRILL); + expect(enemyParty[1].species.speciesId).toBe(Species.BUTTERFREE); + expect(POOL_1_POKEMON.includes(enemyParty[2].species.speciesId)).toBe(true); + expect(POOL_2_POKEMON.includes(enemyParty[3].species.speciesId)).toBe(true); + expect(POOL_2_POKEMON.includes(enemyParty[4].species.speciesId)).toBe(true); + }); + + it("should start battle against the Bug-Type Superfan with wave 120 party template", async () => { + game.override.startingWave(111); + await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, defaultParty); + await runMysteryEncounterToEnd(game, 1, undefined, true); + + const enemyParty = scene.getEnemyParty(); + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(enemyParty.length).toBe(5); + expect(scene.currentBattle.trainer?.config.trainerType).toBe(TrainerType.BUG_TYPE_SUPERFAN); + expect(enemyParty[0].species.speciesId).toBe(Species.BEEDRILL); + expect(enemyParty[0].formIndex).toBe(1); + expect(enemyParty[1].species.speciesId).toBe(Species.BUTTERFREE); + expect(enemyParty[1].formIndex).toBe(1); + expect(POOL_2_POKEMON.includes(enemyParty[2].species.speciesId)).toBe(true); + expect(POOL_2_POKEMON.includes(enemyParty[3].species.speciesId)).toBe(true); + expect(POOL_3_POKEMON.some(config => enemyParty[4].species.speciesId === config.species)).toBe(true); + }); + + it("should start battle against the Bug-Type Superfan with wave 140 party template", async () => { + game.override.startingWave(131); + await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, defaultParty); + await runMysteryEncounterToEnd(game, 1, undefined, true); + + const enemyParty = scene.getEnemyParty(); + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(enemyParty.length).toBe(5); + expect(scene.currentBattle.trainer?.config.trainerType).toBe(TrainerType.BUG_TYPE_SUPERFAN); + expect(enemyParty[0].species.speciesId).toBe(Species.BEEDRILL); + expect(enemyParty[0].formIndex).toBe(1); + expect(enemyParty[1].species.speciesId).toBe(Species.BUTTERFREE); + expect(enemyParty[1].formIndex).toBe(1); + expect(POOL_2_POKEMON.includes(enemyParty[2].species.speciesId)).toBe(true); + expect(POOL_3_POKEMON.some(config => enemyParty[3].species.speciesId === config.species)).toBe(true); + expect(POOL_3_POKEMON.some(config => enemyParty[4].species.speciesId === config.species)).toBe(true); + }); + + it("should start battle against the Bug-Type Superfan with wave 160 party template", async () => { + game.override.startingWave(151); + await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, defaultParty); + await runMysteryEncounterToEnd(game, 1, undefined, true); + + const enemyParty = scene.getEnemyParty(); + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(enemyParty.length).toBe(5); + expect(scene.currentBattle.trainer?.config.trainerType).toBe(TrainerType.BUG_TYPE_SUPERFAN); + expect(enemyParty[0].species.speciesId).toBe(Species.BEEDRILL); + expect(enemyParty[0].formIndex).toBe(1); + expect(enemyParty[1].species.speciesId).toBe(Species.BUTTERFREE); + expect(enemyParty[1].formIndex).toBe(1); + expect(POOL_2_POKEMON.includes(enemyParty[2].species.speciesId)).toBe(true); + expect(POOL_3_POKEMON.some(config => enemyParty[3].species.speciesId === config.species)).toBe(true); + expect(POOL_4_POKEMON.includes(enemyParty[4].species.speciesId)).toBe(true); + }); + + it("should start battle against the Bug-Type Superfan with wave 180 party template", async () => { + game.override.startingWave(171); + await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, defaultParty); + await runMysteryEncounterToEnd(game, 1, undefined, true); + + const enemyParty = scene.getEnemyParty(); + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(enemyParty.length).toBe(5); + expect(scene.currentBattle.trainer?.config.trainerType).toBe(TrainerType.BUG_TYPE_SUPERFAN); + expect(enemyParty[0].species.speciesId).toBe(Species.BEEDRILL); + expect(enemyParty[0].formIndex).toBe(1); + expect(enemyParty[0].isBoss()).toBe(true); + expect(enemyParty[0].bossSegments).toBe(2); + expect(enemyParty[1].species.speciesId).toBe(Species.BUTTERFREE); + expect(enemyParty[1].formIndex).toBe(1); + expect(enemyParty[1].isBoss()).toBe(true); + expect(enemyParty[1].bossSegments).toBe(2); + expect(POOL_3_POKEMON.some(config => enemyParty[2].species.speciesId === config.species)).toBe(true); + expect(POOL_3_POKEMON.some(config => enemyParty[3].species.speciesId === config.species)).toBe(true); + expect(POOL_4_POKEMON.includes(enemyParty[4].species.speciesId)).toBe(true); + }); + + it("should let the player learn a Bug move after battle ends", async () => { + const selectOptionSpy = vi.spyOn(encounterPhaseUtils, "selectOptionThenPokemon"); + await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, defaultParty); + await runMysteryEncounterToEnd(game, 1, undefined, true); + await skipBattleRunMysteryEncounterRewardsPhase(game, false); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterRewardsPhase.name); + game.phaseInterceptor["prompts"] = []; // Clear out prompt handlers + game.onNextPrompt("MysteryEncounterRewardsPhase", Mode.OPTION_SELECT, () => { + game.phaseInterceptor.superEndPhase(); + }); + await game.phaseInterceptor.run(MysteryEncounterRewardsPhase); + + expect(selectOptionSpy).toHaveBeenCalledTimes(1); + const optionData = selectOptionSpy.mock.calls[0][1]; + expect(PHYSICAL_TUTOR_MOVES.some(move => new PokemonMove(move).getName() === optionData[0].label)).toBe(true); + expect(SPECIAL_TUTOR_MOVES.some(move => new PokemonMove(move).getName() === optionData[1].label)).toBe(true); + expect(STATUS_TUTOR_MOVES.some(move => new PokemonMove(move).getName() === optionData[2].label)).toBe(true); + expect(MISC_TUTOR_MOVES.some(move => new PokemonMove(move).getName() === optionData[3].label)).toBe(true); + }); + }); + + describe("Option 2 - Show off Bug Types", () => { + it("should have the correct properties", () => { + const option = BugTypeSuperfanEncounter.options[1]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + disabledButtonTooltip: `${namespace}.option.2.disabled_tooltip` + }); + }); + + it("should NOT be selectable if the player doesn't have any Bug types", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, [Species.ABRA]); + await game.phaseInterceptor.to(MysteryEncounterPhase, false); + + const encounterPhase = scene.getCurrentPhase(); + expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name); + const mysteryEncounterPhase = encounterPhase as MysteryEncounterPhase; + vi.spyOn(mysteryEncounterPhase, "continueEncounter"); + vi.spyOn(mysteryEncounterPhase, "handleOptionSelect"); + vi.spyOn(scene.ui, "playError"); + + await runSelectMysteryEncounterOption(game, 2); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterPhase.name); + expect(scene.ui.playError).not.toHaveBeenCalled(); // No error sfx, option is disabled + expect(mysteryEncounterPhase.handleOptionSelect).not.toHaveBeenCalled(); + expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled(); + }); + + it("should proceed to rewards screen with 0-1 Bug Types reward options", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, defaultParty); + await runMysteryEncounterToEnd(game, 2); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(2); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toBe("SUPER_LURE"); + expect(modifierSelectHandler.options[1].modifierTypeOption.type.id).toBe("GREAT_BALL"); + }); + + it("should proceed to rewards screen with 2-3 Bug Types reward options", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, [Species.BUTTERFREE, Species.BEEDRILL]); + await runMysteryEncounterToEnd(game, 2); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(3); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toBe("QUICK_CLAW"); + expect(modifierSelectHandler.options[1].modifierTypeOption.type.id).toBe("MAX_LURE"); + expect(modifierSelectHandler.options[2].modifierTypeOption.type.id).toBe("ULTRA_BALL"); + }); + + it("should proceed to rewards screen with 4-5 Bug Types reward options", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, [Species.BUTTERFREE, Species.BEEDRILL, Species.GALVANTULA, Species.VOLCARONA]); + await runMysteryEncounterToEnd(game, 2); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(3); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toBe("GRIP_CLAW"); + expect(modifierSelectHandler.options[1].modifierTypeOption.type.id).toBe("MAX_LURE"); + expect(modifierSelectHandler.options[2].modifierTypeOption.type.id).toBe("ROGUE_BALL"); + }); + + it("should proceed to rewards screen with 6 Bug Types reward options (including form change item)", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, [Species.BUTTERFREE, Species.BEEDRILL, Species.GALVANTULA, Species.VOLCARONA, Species.ANORITH, Species.GENESECT]); + await runMysteryEncounterToEnd(game, 2); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(3); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toBe("MASTER_BALL"); + expect(modifierSelectHandler.options[1].modifierTypeOption.type.id).toBe("MAX_LURE"); + expect(modifierSelectHandler.options[2].modifierTypeOption.type.id).toBe("FORM_CHANGE_ITEM"); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(encounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, defaultParty); + await runMysteryEncounterToEnd(game, 2); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 3 - Give a Bug Item", () => { + it("should have the correct properties", () => { + const option = BugTypeSuperfanEncounter.options[2]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + disabledButtonTooltip: `${namespace}.option.3.disabled_tooltip`, + selected: [ + { + text: `${namespace}.option.3.selected`, + }, + { + speaker: `${namespace}.speaker`, + text: `${namespace}.option.3.selected_dialogue`, + }, + ], + secondOptionPrompt: `${namespace}.option.3.select_prompt`, + }); + }); + + it("should NOT be selectable if the player doesn't have any Bug items", async () => { + game.scene.modifiers = []; + await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, defaultParty); + await game.phaseInterceptor.to(MysteryEncounterPhase, false); + + game.scene.modifiers = []; + const encounterPhase = scene.getCurrentPhase(); + expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name); + const mysteryEncounterPhase = encounterPhase as MysteryEncounterPhase; + vi.spyOn(mysteryEncounterPhase, "continueEncounter"); + vi.spyOn(mysteryEncounterPhase, "handleOptionSelect"); + vi.spyOn(scene.ui, "playError"); + + await runSelectMysteryEncounterOption(game, 3); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterPhase.name); + expect(scene.ui.playError).not.toHaveBeenCalled(); // No error sfx, option is disabled + expect(mysteryEncounterPhase.handleOptionSelect).not.toHaveBeenCalled(); + expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled(); + }); + + it("should remove the gifted item and proceed to rewards screen", async () => { + game.override.startingHeldItems([{name: "GRIP_CLAW", count: 1}]); + await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, [Species.BUTTERFREE]); + + const gripClawCountBefore = scene.findModifier(m => m instanceof ContactHeldItemTransferChanceModifier)?.stackCount ?? 0; + + await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(2); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toBe("MYSTERY_ENCOUNTER_GOLDEN_BUG_NET"); + expect(modifierSelectHandler.options[1].modifierTypeOption.type.id).toBe("REVIVER_SEED"); + + const gripClawCountAfter = scene.findModifier(m => m instanceof ContactHeldItemTransferChanceModifier)?.stackCount ?? 0; + expect(gripClawCountBefore - 1).toBe(gripClawCountAfter); + }); + + it("should leave encounter without battle", async () => { + game.override.startingHeldItems([{name: "GRIP_CLAW", count: 1}]); + const leaveEncounterWithoutBattleSpy = vi.spyOn(encounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, [Species.BUTTERFREE]); + await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); +}); diff --git a/src/test/mystery-encounter/encounters/clowning-around-encounter.test.ts b/src/test/mystery-encounter/encounters/clowning-around-encounter.test.ts new file mode 100644 index 00000000000..5ed5a9487de --- /dev/null +++ b/src/test/mystery-encounter/encounters/clowning-around-encounter.test.ts @@ -0,0 +1,375 @@ +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import * as BattleAnims from "#app/data/battle-anims"; +import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { generateModifierType } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { runMysteryEncounterToEnd, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounter-test-utils"; +import { Moves } from "#enums/moves"; +import BattleScene from "#app/battle-scene"; +import Pokemon, { PokemonMove } from "#app/field/pokemon"; +import { Mode } from "#app/ui/ui"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; +import { ModifierTier } from "#app/modifier/modifier-tier"; +import { ClowningAroundEncounter } from "#app/data/mystery-encounters/encounters/clowning-around-encounter"; +import { TrainerType } from "#enums/trainer-type"; +import { Abilities } from "#enums/abilities"; +import { PostMysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; +import { Button } from "#enums/buttons"; +import PartyUiHandler from "#app/ui/party-ui-handler"; +import OptionSelectUiHandler from "#app/ui/settings/option-select-ui-handler"; +import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; +import { BerryType } from "#enums/berry-type"; +import { PokemonHeldItemModifier } from "#app/modifier/modifier"; +import { Type } from "#app/data/type"; +import { CommandPhase } from "#app/phases/command-phase"; +import { MovePhase } from "#app/phases/move-phase"; +import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; +import { NewBattlePhase } from "#app/phases/new-battle-phase"; + +const namespace = "mysteryEncounter:clowningAround"; +const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; +const defaultBiome = Biome.CAVE; +const defaultWave = 45; + +describe("Clowning Around - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(); + + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( + new Map([ + [Biome.CAVE, [MysteryEncounterType.CLOWNING_AROUND]], + ]) + ); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.CLOWNING_AROUND, defaultParty); + + expect(ClowningAroundEncounter.encounterType).toBe(MysteryEncounterType.CLOWNING_AROUND); + expect(ClowningAroundEncounter.encounterTier).toBe(MysteryEncounterTier.ULTRA); + expect(ClowningAroundEncounter.dialogue).toBeDefined(); + expect(ClowningAroundEncounter.dialogue.intro).toStrictEqual([ + { text: `${namespace}.intro` }, + { + speaker: `${namespace}.speaker`, + text: `${namespace}.intro_dialogue`, + }, + ]); + expect(ClowningAroundEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`); + expect(ClowningAroundEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`); + expect(ClowningAroundEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`); + expect(ClowningAroundEncounter.options.length).toBe(3); + }); + + it("should not run below wave 80", async () => { + game.override.startingWave(79); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.CLOWNING_AROUND); + }); + + it("should initialize fully", async () => { + initSceneWithoutEncounterPhase(scene, defaultParty); + scene.currentBattle.mysteryEncounter = ClowningAroundEncounter; + const moveInitSpy = vi.spyOn(BattleAnims, "initMoveAnim"); + const moveLoadSpy = vi.spyOn(BattleAnims, "loadMoveAnimAssets"); + + const { onInit } = ClowningAroundEncounter; + + expect(ClowningAroundEncounter.onInit).toBeDefined(); + + ClowningAroundEncounter.populateDialogueTokensFromRequirements(scene); + const onInitResult = onInit!(scene); + const config = ClowningAroundEncounter.enemyPartyConfigs[0]; + + expect(config.doubleBattle).toBe(true); + expect(config.trainerConfig?.trainerType).toBe(TrainerType.HARLEQUIN); + expect(config.pokemonConfigs?.[0]).toEqual({ + species: getPokemonSpecies(Species.MR_MIME), + isBoss: true, + moveSet: [Moves.TEETER_DANCE, Moves.ALLY_SWITCH, Moves.DAZZLING_GLEAM, Moves.PSYCHIC] + }); + expect(config.pokemonConfigs?.[1]).toEqual({ + species: getPokemonSpecies(Species.BLACEPHALON), + mysteryEncounterPokemonData: expect.anything(), + isBoss: true, + moveSet: [Moves.TRICK, Moves.HYPNOSIS, Moves.SHADOW_BALL, Moves.MIND_BLOWN] + }); + expect(config.pokemonConfigs?.[1].mysteryEncounterPokemonData?.types.length).toBe(2); + expect([ + Abilities.STURDY, + Abilities.PICKUP, + Abilities.INTIMIDATE, + Abilities.GUTS, + Abilities.DROUGHT, + Abilities.DRIZZLE, + Abilities.SNOW_WARNING, + Abilities.SAND_STREAM, + Abilities.ELECTRIC_SURGE, + Abilities.PSYCHIC_SURGE, + Abilities.GRASSY_SURGE, + Abilities.MISTY_SURGE, + Abilities.MAGICIAN, + Abilities.SHEER_FORCE, + Abilities.PRANKSTER + ]).toContain(config.pokemonConfigs?.[1].mysteryEncounterPokemonData?.ability); + expect(ClowningAroundEncounter.misc.ability).toBe(config.pokemonConfigs?.[1].mysteryEncounterPokemonData?.ability); + await vi.waitFor(() => expect(moveInitSpy).toHaveBeenCalled()); + await vi.waitFor(() => expect(moveLoadSpy).toHaveBeenCalled()); + expect(onInitResult).toBe(true); + }); + + describe("Option 1 - Battle the Clown", () => { + it("should have the correct properties", () => { + const option = ClowningAroundEncounter.options[0]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + speaker: `${namespace}.speaker`, + text: `${namespace}.option.1.selected`, + }, + ], + }); + }); + + it("should start double battle against the clown", async () => { + const phaseSpy = vi.spyOn(scene, "pushPhase"); + + await game.runToMysteryEncounter(MysteryEncounterType.CLOWNING_AROUND, defaultParty); + await runMysteryEncounterToEnd(game, 1, undefined, true); + + const enemyField = scene.getEnemyField(); + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(enemyField.length).toBe(2); + expect(enemyField[0].species.speciesId).toBe(Species.MR_MIME); + expect(enemyField[0].moveset).toEqual([new PokemonMove(Moves.TEETER_DANCE), new PokemonMove(Moves.ALLY_SWITCH), new PokemonMove(Moves.DAZZLING_GLEAM), new PokemonMove(Moves.PSYCHIC)]); + expect(enemyField[1].species.speciesId).toBe(Species.BLACEPHALON); + expect(enemyField[1].moveset).toEqual([new PokemonMove(Moves.TRICK), new PokemonMove(Moves.HYPNOSIS), new PokemonMove(Moves.SHADOW_BALL), new PokemonMove(Moves.MIND_BLOWN)]); + + // Should have used moves pre-battle + const movePhases = phaseSpy.mock.calls.filter(p => p[0] instanceof MovePhase).map(p => p[0]); + expect(movePhases.length).toBe(3); + expect(movePhases.filter(p => (p as MovePhase).move.moveId === Moves.ROLE_PLAY).length).toBe(1); + expect(movePhases.filter(p => (p as MovePhase).move.moveId === Moves.TAUNT).length).toBe(2); + }); + + it("should let the player gain the ability after battle completion", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.CLOWNING_AROUND, defaultParty); + await runMysteryEncounterToEnd(game, 1, undefined, true); + await skipBattleRunMysteryEncounterRewardsPhase(game); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + const abilityToTrain = scene.currentBattle.mysteryEncounter?.misc.ability; + + game.onNextPrompt("PostMysteryEncounterPhase", Mode.MESSAGE, () => { + game.scene.ui.getHandler().processInput(Button.ACTION); + }); + + // Run to ability train option selection + const optionSelectUiHandler = game.scene.ui.handlers[Mode.OPTION_SELECT] as OptionSelectUiHandler; + vi.spyOn(optionSelectUiHandler, "show"); + const partyUiHandler = game.scene.ui.handlers[Mode.PARTY] as PartyUiHandler; + vi.spyOn(partyUiHandler, "show"); + game.endPhase(); + await game.phaseInterceptor.to(PostMysteryEncounterPhase); + expect(scene.getCurrentPhase()?.constructor.name).toBe(PostMysteryEncounterPhase.name); + + // Wait for Yes/No confirmation to appear + await vi.waitFor(() => expect(optionSelectUiHandler.show).toHaveBeenCalled()); + // Select "Yes" on train ability + optionSelectUiHandler.processInput(Button.ACTION); + // Select first pokemon in party to train + await vi.waitFor(() => expect(partyUiHandler.show).toHaveBeenCalled()); + partyUiHandler.processInput(Button.ACTION); + // Click "Select" on Pokemon + partyUiHandler.processInput(Button.ACTION); + // Stop next battle before it runs + await game.phaseInterceptor.to(NewBattlePhase, false); + + const leadPokemon = scene.getParty()[0]; + expect(leadPokemon.mysteryEncounterPokemonData?.ability).toBe(abilityToTrain); + }); + }); + + describe("Option 2 - Remain Unprovoked", () => { + it("should have the correct properties", () => { + const option = ClowningAroundEncounter.options[1]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + speaker: `${namespace}.speaker`, + text: `${namespace}.option.2.selected`, + }, + { + text: `${namespace}.option.2.selected_2`, + }, + { + speaker: `${namespace}.speaker`, + text: `${namespace}.option.2.selected_3`, + }, + ], + }); + }); + + it("should randomize held items of the Pokemon with the most items, and not the held items of other pokemon", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.CLOWNING_AROUND, defaultParty); + + // Set some moves on party for attack type booster generation + scene.getParty()[0].moveset = [new PokemonMove(Moves.TACKLE), new PokemonMove(Moves.THIEF)]; + + // 2 Sitrus Berries on lead + scene.modifiers = []; + let itemType = generateModifierType(scene, modifierTypes.BERRY, [BerryType.SITRUS]) as PokemonHeldItemModifierType; + await addItemToPokemon(scene, scene.getParty()[0], 2, itemType); + // 2 Ganlon Berries on lead + itemType = generateModifierType(scene, modifierTypes.BERRY, [BerryType.GANLON]) as PokemonHeldItemModifierType; + await addItemToPokemon(scene, scene.getParty()[0], 2, itemType); + // 5 Golden Punch on lead (ultra) + itemType = generateModifierType(scene, modifierTypes.GOLDEN_PUNCH) as PokemonHeldItemModifierType; + await addItemToPokemon(scene, scene.getParty()[0], 5, itemType); + // 5 Lucky Egg on lead (ultra) + itemType = generateModifierType(scene, modifierTypes.LUCKY_EGG) as PokemonHeldItemModifierType; + await addItemToPokemon(scene, scene.getParty()[0], 5, itemType); + // 5 Soul Dew on lead (rogue) + itemType = generateModifierType(scene, modifierTypes.SOUL_DEW) as PokemonHeldItemModifierType; + await addItemToPokemon(scene, scene.getParty()[0], 5, itemType); + // 2 Golden Egg on lead (rogue) + itemType = generateModifierType(scene, modifierTypes.GOLDEN_EGG) as PokemonHeldItemModifierType; + await addItemToPokemon(scene, scene.getParty()[0], 2, itemType); + + // 5 Soul Dew on second party pokemon (these should not change) + itemType = generateModifierType(scene, modifierTypes.SOUL_DEW) as PokemonHeldItemModifierType; + await addItemToPokemon(scene, scene.getParty()[1], 5, itemType); + + await runMysteryEncounterToEnd(game, 2); + + const leadItemsAfter = scene.getParty()[0].getHeldItems(); + const ultraCountAfter = leadItemsAfter + .filter(m => m.type.tier === ModifierTier.ULTRA) + .reduce((a, b) => a + b.stackCount, 0); + const rogueCountAfter = leadItemsAfter + .filter(m => m.type.tier === ModifierTier.ROGUE) + .reduce((a, b) => a + b.stackCount, 0); + expect(ultraCountAfter).toBe(10); + expect(rogueCountAfter).toBe(7); + + const secondItemsAfter = scene.getParty()[1].getHeldItems(); + expect(secondItemsAfter.length).toBe(1); + expect(secondItemsAfter[0].type.id).toBe("SOUL_DEW"); + expect(secondItemsAfter[0]?.stackCount).toBe(5); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.CLOWNING_AROUND, defaultParty); + await runMysteryEncounterToEnd(game, 2); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 3 - Return the Insults", () => { + it("should have the correct properties", () => { + const option = ClowningAroundEncounter.options[2]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + selected: [ + { + speaker: `${namespace}.speaker`, + text: `${namespace}.option.3.selected`, + }, + { + text: `${namespace}.option.3.selected_2`, + }, + { + speaker: `${namespace}.speaker`, + text: `${namespace}.option.3.selected_3`, + }, + ], + }); + }); + + it("should randomize the pokemon types of the party", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.CLOWNING_AROUND, defaultParty); + + // Same type moves on lead + scene.getParty()[0].moveset = [new PokemonMove(Moves.ICE_BEAM), new PokemonMove(Moves.SURF)]; + // Different type moves on second + scene.getParty()[1].moveset = [new PokemonMove(Moves.GRASS_KNOT), new PokemonMove(Moves.ELECTRO_BALL)]; + // No moves on third + scene.getParty()[2].moveset = []; + await runMysteryEncounterToEnd(game, 3); + + const leadTypesAfter = scene.getParty()[0].mysteryEncounterPokemonData?.types; + const secondaryTypesAfter = scene.getParty()[1].mysteryEncounterPokemonData?.types; + const thirdTypesAfter = scene.getParty()[2].mysteryEncounterPokemonData?.types; + + expect(leadTypesAfter.length).toBe(2); + expect(leadTypesAfter[0]).toBe(Type.WATER); + expect([Type.WATER, Type.ICE].includes(leadTypesAfter[1])).toBeFalsy(); + expect(secondaryTypesAfter.length).toBe(2); + expect(secondaryTypesAfter[0]).toBe(Type.GHOST); + expect([Type.GHOST, Type.POISON].includes(secondaryTypesAfter[1])).toBeFalsy(); + expect([Type.GRASS, Type.ELECTRIC].includes(secondaryTypesAfter[1])).toBeTruthy(); + expect(thirdTypesAfter.length).toBe(2); + expect(thirdTypesAfter[0]).toBe(Type.PSYCHIC); + expect(secondaryTypesAfter[1]).not.toBe(Type.PSYCHIC); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.CLOWNING_AROUND, defaultParty); + await runMysteryEncounterToEnd(game, 3); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); +}); + +async function addItemToPokemon(scene: BattleScene, pokemon: Pokemon, stackCount: integer, itemType: PokemonHeldItemModifierType) { + const itemMod = itemType.newModifier(pokemon) as PokemonHeldItemModifier; + itemMod.stackCount = stackCount; + await scene.addModifier(itemMod, true, false, false, true); + await scene.updateModifiers(true); +} diff --git a/src/test/mystery-encounter/encounters/dancing-lessons-encounter.test.ts b/src/test/mystery-encounter/encounters/dancing-lessons-encounter.test.ts new file mode 100644 index 00000000000..cbf8251f2e7 --- /dev/null +++ b/src/test/mystery-encounter/encounters/dancing-lessons-encounter.test.ts @@ -0,0 +1,245 @@ +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { runMysteryEncounterToEnd, runSelectMysteryEncounterOption, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounter-test-utils"; +import BattleScene from "#app/battle-scene"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { Moves } from "#enums/moves"; +import { DancingLessonsEncounter } from "#app/data/mystery-encounters/encounters/dancing-lessons-encounter"; +import { Mode } from "#app/ui/ui"; +import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; +import { PokemonMove } from "#app/field/pokemon"; +import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; +import { CommandPhase } from "#app/phases/command-phase"; +import { MovePhase } from "#app/phases/move-phase"; +import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; +import { LearnMovePhase } from "#app/phases/learn-move-phase"; + +const namespace = "mysteryEncounter:dancingLessons"; +const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; +const defaultBiome = Biome.PLAINS; +const defaultWave = 45; + +describe("Dancing Lessons - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(); + + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( + new Map([ + [Biome.PLAINS, [MysteryEncounterType.DANCING_LESSONS]], + [Biome.SPACE, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]], + ]) + ); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DANCING_LESSONS, defaultParty); + + expect(DancingLessonsEncounter.encounterType).toBe(MysteryEncounterType.DANCING_LESSONS); + expect(DancingLessonsEncounter.encounterTier).toBe(MysteryEncounterTier.GREAT); + expect(DancingLessonsEncounter.dialogue).toBeDefined(); + expect(DancingLessonsEncounter.dialogue.intro).toStrictEqual([{ text: `${namespace}.intro` }]); + expect(DancingLessonsEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`); + expect(DancingLessonsEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`); + expect(DancingLessonsEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`); + expect(DancingLessonsEncounter.options.length).toBe(3); + }); + + it("should not spawn outside of proper biomes", async () => { + game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT); + game.override.startingBiome(Biome.SPACE); + await game.runToMysteryEncounter(); + + expect(game.scene.currentBattle.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.DANCING_LESSONS); + }); + + describe("Option 1 - Fight the Oricorio", () => { + it("should have the correct properties", () => { + const option1 = DancingLessonsEncounter.options[0]; + expect(option1.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option1.dialogue).toBeDefined(); + expect(option1.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }); + }); + + it("should start battle against Oricorio", async () => { + const phaseSpy = vi.spyOn(scene, "pushPhase"); + + await game.runToMysteryEncounter(MysteryEncounterType.DANCING_LESSONS, defaultParty); + // Make party lead's level arbitrarily high to not get KOed by move + const partyLead = scene.getParty()[0]; + partyLead.level = 1000; + partyLead.calculateStats(); + await runMysteryEncounterToEnd(game, 1, undefined, true); + + const enemyField = scene.getEnemyField(); + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(enemyField.length).toBe(1); + expect(enemyField[0].species.speciesId).toBe(Species.ORICORIO); + expect(enemyField[0].summonData.statStages).toEqual([1, 1, 1, 1, 0, 0, 0]); + const moveset = enemyField[0].moveset.map(m => m?.moveId); + expect(moveset.some(m => m === Moves.REVELATION_DANCE)).toBeTruthy(); + + const movePhases = phaseSpy.mock.calls.filter(p => p[0] instanceof MovePhase).map(p => p[0]); + expect(movePhases.length).toBe(1); + expect(movePhases.filter(p => (p as MovePhase).move.moveId === Moves.REVELATION_DANCE).length).toBe(1); // Revelation Dance used before battle + }); + + it("should have a Baton in the rewards after battle", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DANCING_LESSONS, defaultParty); + // Make party lead's level arbitrarily high to not get KOed by move + const partyLead = scene.getParty()[0]; + partyLead.level = 1000; + partyLead.calculateStats(); + await runMysteryEncounterToEnd(game, 1, undefined, true); + // For some reason updateModifiers breaks in this test and does not resolve promise + vi.spyOn(game.scene, "updateModifiers").mockImplementation(() => new Promise(resolve => resolve())); + await skipBattleRunMysteryEncounterRewardsPhase(game); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(3); // Should fill remaining + expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toContain("BATON"); + }); + }); + + describe("Option 2 - Learn its Dance", () => { + it("should have the correct properties", () => { + const option = DancingLessonsEncounter.options[1]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected`, + }, + ], + }); + }); + + it("Should select a pokemon to learn Revelation Dance", async () => { + const phaseSpy = vi.spyOn(scene, "unshiftPhase"); + + await game.runToMysteryEncounter(MysteryEncounterType.DANCING_LESSONS, defaultParty); + scene.getParty()[0].moveset = []; + await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1 }); + + const movePhases = phaseSpy.mock.calls.filter(p => p[0] instanceof LearnMovePhase).map(p => p[0]); + expect(movePhases.length).toBe(1); + expect(movePhases.filter(p => (p as LearnMovePhase)["moveId"] === Moves.REVELATION_DANCE).length).toBe(1); // Revelation Dance taught to pokemon + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.DANCING_LESSONS, defaultParty); + scene.getParty()[0].moveset = []; + await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1 }); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 3 - Teach it a Dance", () => { + it("should have the correct properties", () => { + const option = DancingLessonsEncounter.options[2]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + disabledButtonTooltip: `${namespace}.option.3.disabled_tooltip`, + secondOptionPrompt: `${namespace}.option.3.select_prompt`, + selected: [ + { + text: `${namespace}.option.3.selected`, + }, + ], + }); + }); + + it("should add Oricorio to the party", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DANCING_LESSONS, defaultParty); + const partyCountBefore = scene.getParty().length; + scene.getParty()[0].moveset = [new PokemonMove(Moves.DRAGON_DANCE)]; + await runMysteryEncounterToEnd(game, 3, {pokemonNo: 1, optionNo: 1}); + const partyCountAfter = scene.getParty().length; + + expect(partyCountBefore + 1).toBe(partyCountAfter); + const oricorio = scene.getParty()[scene.getParty().length - 1]; + expect(oricorio.species.speciesId).toBe(Species.ORICORIO); + const moveset = oricorio.moveset.map(m => m?.moveId); + expect(moveset?.some(m => m === Moves.REVELATION_DANCE)).toBeTruthy(); + expect(moveset?.some(m => m === Moves.DRAGON_DANCE)).toBeTruthy(); + }); + + it("should NOT be selectable if the player doesn't have a Dance type move", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DANCING_LESSONS, defaultParty); + const partyCountBefore = scene.getParty().length; + scene.getParty().forEach(p => p.moveset = []); + await game.phaseInterceptor.to(MysteryEncounterPhase, false); + + const encounterPhase = scene.getCurrentPhase(); + expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name); + const mysteryEncounterPhase = encounterPhase as MysteryEncounterPhase; + vi.spyOn(mysteryEncounterPhase, "continueEncounter"); + vi.spyOn(mysteryEncounterPhase, "handleOptionSelect"); + vi.spyOn(scene.ui, "playError"); + + await runSelectMysteryEncounterOption(game, 3); + const partyCountAfter = scene.getParty().length; + + expect(scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterPhase.name); + expect(scene.ui.playError).not.toHaveBeenCalled(); // No error sfx, option is disabled + expect(mysteryEncounterPhase.handleOptionSelect).not.toHaveBeenCalled(); + expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled(); + expect(partyCountBefore).toBe(partyCountAfter); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.DANCING_LESSONS, defaultParty); + scene.getParty()[0].moveset = [new PokemonMove(Moves.DRAGON_DANCE)]; + await runMysteryEncounterToEnd(game, 3, {pokemonNo: 1, optionNo: 1}); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); +}); diff --git a/src/test/mystery-encounter/encounters/delibirdy-encounter.test.ts b/src/test/mystery-encounter/encounters/delibirdy-encounter.test.ts new file mode 100644 index 00000000000..7e452fd90c7 --- /dev/null +++ b/src/test/mystery-encounter/encounters/delibirdy-encounter.test.ts @@ -0,0 +1,466 @@ +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { runMysteryEncounterToEnd, runSelectMysteryEncounterOption } from "#test/mystery-encounter/encounter-test-utils"; +import BattleScene from "#app/battle-scene"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { DelibirdyEncounter } from "#app/data/mystery-encounters/encounters/delibirdy-encounter"; +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { MoneyRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; +import { BerryModifier, HealingBoosterModifier, HiddenAbilityRateBoosterModifier, HitHealModifier, LevelIncrementBoosterModifier, PokemonInstantReviveModifier, PokemonNatureWeightModifier, PreserveBerryModifier } 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 { BerryType } from "#enums/berry-type"; + +const namespace = "mysteryEncounter:delibirdy"; +const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; +const defaultBiome = Biome.CAVE; +const defaultWave = 45; + +describe("Delibird-y - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(); + + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( + new Map([ + [Biome.CAVE, [MysteryEncounterType.DELIBIRDY]], + ]) + ); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + + expect(DelibirdyEncounter.encounterType).toBe(MysteryEncounterType.DELIBIRDY); + expect(DelibirdyEncounter.encounterTier).toBe(MysteryEncounterTier.GREAT); + expect(DelibirdyEncounter.dialogue).toBeDefined(); + expect(DelibirdyEncounter.dialogue.intro).toStrictEqual([{ text: `${namespace}.intro` }]); + expect(DelibirdyEncounter.dialogue.outro).toStrictEqual([{ text: `${namespace}.outro` }]); + expect(DelibirdyEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`); + expect(DelibirdyEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`); + expect(DelibirdyEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`); + expect(DelibirdyEncounter.options.length).toBe(3); + }); + + it("should not spawn if player does not have enough money", async () => { + scene.money = 0; + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.DELIBIRDY); + }); + + describe("Option 1 - Give them money", () => { + it("should have the correct properties", () => { + const option1 = DelibirdyEncounter.options[0]; + expect(option1.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT); + expect(option1.dialogue).toBeDefined(); + expect(option1.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }); + }); + + it("Should update the player's money properly", async () => { + const initialMoney = 20000; + scene.money = initialMoney; + const updateMoneySpy = vi.spyOn(EncounterPhaseUtils, "updatePlayerMoney"); + + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + await runMysteryEncounterToEnd(game, 1); + + const price = (scene.currentBattle.mysteryEncounter?.options[0].requirements[0] as MoneyRequirement).requiredMoney; + + expect(updateMoneySpy).toHaveBeenCalledWith(scene, -price, true, false); + expect(scene.money).toBe(initialMoney - price); + }); + + it("Should give the player a Hidden Ability Charm", async () => { + scene.money = 200000; + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + await runMysteryEncounterToEnd(game, 1); + + const itemModifier = scene.findModifier(m => m instanceof HiddenAbilityRateBoosterModifier) as HiddenAbilityRateBoosterModifier; + + expect(itemModifier).toBeDefined(); + expect(itemModifier?.stackCount).toBe(1); + }); + + it("Should give the player a Shell Bell if they have max stacks of Berry Pouches", async () => { + scene.money = 200000; + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + + // 5 Healing Charms + scene.modifiers = []; + const abilityCharm = generateModifierType(scene, modifierTypes.ABILITY_CHARM)!.newModifier() as HiddenAbilityRateBoosterModifier; + abilityCharm.stackCount = 4; + await scene.addModifier(abilityCharm, true, false, false, true); + await scene.updateModifiers(true); + + await runMysteryEncounterToEnd(game, 1); + + const abilityCharmAfter = scene.findModifier(m => m instanceof HiddenAbilityRateBoosterModifier); + const shellBellAfter = scene.findModifier(m => m instanceof HitHealModifier); + + expect(abilityCharmAfter).toBeDefined(); + expect(abilityCharmAfter?.stackCount).toBe(4); + expect(shellBellAfter).toBeDefined(); + expect(shellBellAfter?.stackCount).toBe(1); + }); + + it("should be disabled if player does not have enough money", async () => { + scene.money = 0; + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + await game.phaseInterceptor.to(MysteryEncounterPhase, false); + + const encounterPhase = scene.getCurrentPhase(); + expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name); + const mysteryEncounterPhase = encounterPhase as MysteryEncounterPhase; + vi.spyOn(mysteryEncounterPhase, "continueEncounter"); + vi.spyOn(mysteryEncounterPhase, "handleOptionSelect"); + vi.spyOn(scene.ui, "playError"); + + await runSelectMysteryEncounterOption(game, 1); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterPhase.name); + expect(scene.ui.playError).not.toHaveBeenCalled(); // No error sfx, option is disabled + expect(mysteryEncounterPhase.handleOptionSelect).not.toHaveBeenCalled(); + expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled(); + }); + + it("should leave encounter without battle", async () => { + scene.money = 200000; + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + await runMysteryEncounterToEnd(game, 1); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 2 - Give Food", () => { + it("should have the correct properties", () => { + const option = DelibirdyEncounter.options[1]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + secondOptionPrompt: `${namespace}.option.2.select_prompt`, + selected: [ + { + text: `${namespace}.option.2.selected`, + }, + ], + }); + }); + + it("Should decrease Berry stacks and give the player a Candy Jar", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + + // Set 2 Sitrus berries on party lead + scene.modifiers = []; + const sitrus = generateModifierType(scene, modifierTypes.BERRY, [BerryType.SITRUS])!; + const sitrusMod = sitrus.newModifier(scene.getParty()[0]) as BerryModifier; + sitrusMod.stackCount = 2; + await scene.addModifier(sitrusMod, true, false, false, true); + await scene.updateModifiers(true); + + await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1}); + + const sitrusAfter = scene.findModifier(m => m instanceof BerryModifier); + const candyJarAfter = scene.findModifier(m => m instanceof LevelIncrementBoosterModifier); + + expect(sitrusAfter?.stackCount).toBe(1); + expect(candyJarAfter).toBeDefined(); + expect(candyJarAfter?.stackCount).toBe(1); + }); + + it("Should remove Reviver Seed and give the player a Healing Charm", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + + // Set 1 Reviver Seed on party lead + scene.modifiers = []; + const revSeed = generateModifierType(scene, modifierTypes.REVIVER_SEED)!; + const modifier = revSeed.newModifier(scene.getParty()[0]) as PokemonInstantReviveModifier; + modifier.stackCount = 1; + await scene.addModifier(modifier, true, false, false, true); + await scene.updateModifiers(true); + + await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1}); + + const reviverSeedAfter = scene.findModifier(m => m instanceof PokemonInstantReviveModifier); + const healingCharmAfter = scene.findModifier(m => m instanceof HealingBoosterModifier); + + expect(reviverSeedAfter).toBeUndefined(); + expect(healingCharmAfter).toBeDefined(); + expect(healingCharmAfter?.stackCount).toBe(1); + }); + + it("Should give the player a Shell Bell if they have max stacks of Candy Jars", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + + // 99 Candy Jars + scene.modifiers = []; + const candyJar = generateModifierType(scene, modifierTypes.CANDY_JAR)!.newModifier() as LevelIncrementBoosterModifier; + candyJar.stackCount = 99; + await scene.addModifier(candyJar, true, false, false, true); + const sitrus = generateModifierType(scene, modifierTypes.BERRY, [BerryType.SITRUS])!; + + // Sitrus berries on party + const sitrusMod = sitrus.newModifier(scene.getParty()[0]) as BerryModifier; + sitrusMod.stackCount = 2; + await scene.addModifier(sitrusMod, true, false, false, true); + await scene.updateModifiers(true); + + await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1}); + + const sitrusAfter = scene.findModifier(m => m instanceof BerryModifier); + const candyJarAfter = scene.findModifier(m => m instanceof LevelIncrementBoosterModifier); + const shellBellAfter = scene.findModifier(m => m instanceof HitHealModifier); + + expect(sitrusAfter?.stackCount).toBe(1); + expect(candyJarAfter).toBeDefined(); + expect(candyJarAfter?.stackCount).toBe(99); + expect(shellBellAfter).toBeDefined(); + expect(shellBellAfter?.stackCount).toBe(1); + }); + + it("Should give the player a Shell Bell if they have max stacks of Healing Charms", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + + // 5 Healing Charms + scene.modifiers = []; + const healingCharm = generateModifierType(scene, modifierTypes.HEALING_CHARM)!.newModifier() as HealingBoosterModifier; + healingCharm.stackCount = 5; + await scene.addModifier(healingCharm, true, false, false, true); + + // Set 1 Reviver Seed on party lead + const revSeed = generateModifierType(scene, modifierTypes.REVIVER_SEED)!; + const modifier = revSeed.newModifier(scene.getParty()[0]) as PokemonInstantReviveModifier; + modifier.stackCount = 1; + await scene.addModifier(modifier, true, false, false, true); + await scene.updateModifiers(true); + + await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1}); + + const reviverSeedAfter = scene.findModifier(m => m instanceof PokemonInstantReviveModifier); + const healingCharmAfter = scene.findModifier(m => m instanceof HealingBoosterModifier); + const shellBellAfter = scene.findModifier(m => m instanceof HitHealModifier); + + expect(reviverSeedAfter).toBeUndefined(); + expect(healingCharmAfter).toBeDefined(); + expect(healingCharmAfter?.stackCount).toBe(5); + expect(shellBellAfter).toBeDefined(); + expect(shellBellAfter?.stackCount).toBe(1); + }); + + it("should be disabled if player does not have any proper items", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + + // Set 1 Soul Dew on party lead + scene.modifiers = []; + const soulDew = generateModifierType(scene, modifierTypes.SOUL_DEW)!; + const modifier = soulDew.newModifier(scene.getParty()[0]); + await scene.addModifier(modifier, true, false, false, true); + await scene.updateModifiers(true); + + await game.phaseInterceptor.to(MysteryEncounterPhase, false); + + const encounterPhase = scene.getCurrentPhase(); + expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name); + const mysteryEncounterPhase = encounterPhase as MysteryEncounterPhase; + vi.spyOn(mysteryEncounterPhase, "continueEncounter"); + vi.spyOn(mysteryEncounterPhase, "handleOptionSelect"); + vi.spyOn(scene.ui, "playError"); + + await runSelectMysteryEncounterOption(game, 2); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterPhase.name); + expect(scene.ui.playError).not.toHaveBeenCalled(); // No error sfx, option is disabled + expect(mysteryEncounterPhase.handleOptionSelect).not.toHaveBeenCalled(); + expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled(); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + + // Set 1 Reviver Seed on party lead + const revSeed = generateModifierType(scene, modifierTypes.REVIVER_SEED)!; + const modifier = revSeed.newModifier(scene.getParty()[0]) as PokemonInstantReviveModifier; + modifier.stackCount = 1; + await scene.addModifier(modifier, true, false, false, true); + await scene.updateModifiers(true); + + await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1}); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 3 - Give Item", () => { + it("should have the correct properties", () => { + const option = DelibirdyEncounter.options[2]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + secondOptionPrompt: `${namespace}.option.3.select_prompt`, + selected: [ + { + text: `${namespace}.option.3.selected`, + }, + ], + }); + }); + + it("Should decrease held item stacks and give the player a Berry Pouch", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + + // Set 2 Soul Dew on party lead + scene.modifiers = []; + const soulDew = generateModifierType(scene, modifierTypes.SOUL_DEW)!; + const modifier = soulDew.newModifier(scene.getParty()[0]) as PokemonNatureWeightModifier; + modifier.stackCount = 2; + await scene.addModifier(modifier, true, false, false, true); + await scene.updateModifiers(true); + + await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1}); + + const soulDewAfter = scene.findModifier(m => m instanceof PokemonNatureWeightModifier); + const berryPouchAfter = scene.findModifier(m => m instanceof PreserveBerryModifier); + + expect(soulDewAfter?.stackCount).toBe(1); + expect(berryPouchAfter).toBeDefined(); + expect(berryPouchAfter?.stackCount).toBe(1); + }); + + it("Should remove held item and give the player a Berry Pouch", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + + // Set 1 Soul Dew on party lead + scene.modifiers = []; + const soulDew = generateModifierType(scene, modifierTypes.SOUL_DEW)!; + const modifier = soulDew.newModifier(scene.getParty()[0]) as PokemonNatureWeightModifier; + modifier.stackCount = 1; + await scene.addModifier(modifier, true, false, false, true); + await scene.updateModifiers(true); + + await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1}); + + const soulDewAfter = scene.findModifier(m => m instanceof PokemonNatureWeightModifier); + const berryPouchAfter = scene.findModifier(m => m instanceof PreserveBerryModifier); + + expect(soulDewAfter).toBeUndefined(); + expect(berryPouchAfter).toBeDefined(); + expect(berryPouchAfter?.stackCount).toBe(1); + }); + + it("Should give the player a Shell Bell if they have max stacks of Berry Pouches", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + + // 5 Healing Charms + scene.modifiers = []; + const healingCharm = generateModifierType(scene, modifierTypes.BERRY_POUCH)!.newModifier() as PreserveBerryModifier; + healingCharm.stackCount = 3; + await scene.addModifier(healingCharm, true, false, false, true); + + // Set 1 Soul Dew on party lead + const soulDew = generateModifierType(scene, modifierTypes.SOUL_DEW)!; + const modifier = soulDew.newModifier(scene.getParty()[0]) as PokemonNatureWeightModifier; + modifier.stackCount = 1; + await scene.addModifier(modifier, true, false, false, true); + await scene.updateModifiers(true); + + await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1}); + + const soulDewAfter = scene.findModifier(m => m instanceof PokemonNatureWeightModifier); + const berryPouchAfter = scene.findModifier(m => m instanceof PreserveBerryModifier); + const shellBellAfter = scene.findModifier(m => m instanceof HitHealModifier); + + expect(soulDewAfter).toBeUndefined(); + expect(berryPouchAfter).toBeDefined(); + expect(berryPouchAfter?.stackCount).toBe(3); + expect(shellBellAfter).toBeDefined(); + expect(shellBellAfter?.stackCount).toBe(1); + }); + + it("should be disabled if player does not have any proper items", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + + // Set 1 Reviver Seed on party lead + scene.modifiers = []; + const revSeed = generateModifierType(scene, modifierTypes.REVIVER_SEED)!; + const modifier = revSeed.newModifier(scene.getParty()[0]); + await scene.addModifier(modifier, true, false, false, true); + await scene.updateModifiers(true); + + await game.phaseInterceptor.to(MysteryEncounterPhase, false); + + const encounterPhase = scene.getCurrentPhase(); + expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name); + const mysteryEncounterPhase = encounterPhase as MysteryEncounterPhase; + vi.spyOn(mysteryEncounterPhase, "continueEncounter"); + vi.spyOn(mysteryEncounterPhase, "handleOptionSelect"); + vi.spyOn(scene.ui, "playError"); + + await runSelectMysteryEncounterOption(game, 3); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterPhase.name); + expect(scene.ui.playError).not.toHaveBeenCalled(); // No error sfx, option is disabled + expect(mysteryEncounterPhase.handleOptionSelect).not.toHaveBeenCalled(); + expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled(); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + + // Set 1 Soul Dew on party lead + scene.modifiers = []; + const soulDew = generateModifierType(scene, modifierTypes.SOUL_DEW)!; + const modifier = soulDew.newModifier(scene.getParty()[0]) as PokemonNatureWeightModifier; + modifier.stackCount = 1; + await scene.addModifier(modifier, true, false, false, true); + await scene.updateModifiers(true); + + await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1}); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); +}); diff --git a/src/test/mystery-encounter/encounters/department-store-sale-encounter.test.ts b/src/test/mystery-encounter/encounters/department-store-sale-encounter.test.ts new file mode 100644 index 00000000000..0b2d66db20b --- /dev/null +++ b/src/test/mystery-encounter/encounters/department-store-sale-encounter.test.ts @@ -0,0 +1,223 @@ +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { runMysteryEncounterToEnd } from "#test/mystery-encounter/encounter-test-utils"; +import BattleScene from "#app/battle-scene"; +import { Mode } from "#app/ui/ui"; +import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; +import { DepartmentStoreSaleEncounter } from "#app/data/mystery-encounters/encounters/department-store-sale-encounter"; +import { CIVILIZATION_ENCOUNTER_BIOMES } from "#app/data/mystery-encounters/mystery-encounters"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; + +const namespace = "mysteryEncounter:departmentStoreSale"; +const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; +const defaultBiome = Biome.PLAINS; +const defaultWave = 37; + +describe("Department Store Sale - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(); + + const biomeMap = new Map([ + [Biome.VOLCANO, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]], + ]); + CIVILIZATION_ENCOUNTER_BIOMES.forEach(biome => { + biomeMap.set(biome, [MysteryEncounterType.DEPARTMENT_STORE_SALE]); + }); + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue(biomeMap); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty); + + expect(DepartmentStoreSaleEncounter.encounterType).toBe(MysteryEncounterType.DEPARTMENT_STORE_SALE); + expect(DepartmentStoreSaleEncounter.encounterTier).toBe(MysteryEncounterTier.COMMON); + expect(DepartmentStoreSaleEncounter.dialogue).toBeDefined(); + expect(DepartmentStoreSaleEncounter.dialogue.intro).toStrictEqual([ + { text: `${namespace}.intro` }, + { + speaker: `${namespace}.speaker`, + text: `${namespace}.intro_dialogue`, + } + ]); + expect(DepartmentStoreSaleEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`); + expect(DepartmentStoreSaleEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`); + expect(DepartmentStoreSaleEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`); + expect(DepartmentStoreSaleEncounter.options.length).toBe(4); + }); + + it("should not spawn outside of CIVILIZATION_ENCOUNTER_BIOMES", async () => { + game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON); + game.override.startingBiome(Biome.VOLCANO); + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.DEPARTMENT_STORE_SALE); + }); + + describe("Option 1 - TM Shop", () => { + it("should have the correct properties", () => { + const option = DepartmentStoreSaleEncounter.options[0]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + }); + }); + + it("should have shop with only TMs", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty); + await runMysteryEncounterToEnd(game, 1); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(4); + for (const option of modifierSelectHandler.options) { + expect(option.modifierTypeOption.type.id).toContain("TM_"); + } + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty); + await runMysteryEncounterToEnd(game, 1); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 2 - Vitamin Shop", () => { + it("should have the correct properties", () => { + const option = DepartmentStoreSaleEncounter.options[1]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + }); + }); + + it("should have shop with only Vitamins", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty); + await runMysteryEncounterToEnd(game, 2); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(3); + for (const option of modifierSelectHandler.options) { + expect(option.modifierTypeOption.type.id.includes("PP_UP") || + option.modifierTypeOption.type.id.includes("BASE_STAT_BOOSTER")).toBeTruthy(); + } + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty); + await runMysteryEncounterToEnd(game, 2); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 3 - X Item Shop", () => { + it("should have the correct properties", () => { + const option = DepartmentStoreSaleEncounter.options[2]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + }); + }); + + it("should have shop with only X Items", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty); + await runMysteryEncounterToEnd(game, 3); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(5); + for (const option of modifierSelectHandler.options) { + expect(option.modifierTypeOption.type.id.includes("DIRE_HIT") || + option.modifierTypeOption.type.id.includes("TEMP_STAT_STAGE_BOOSTER")).toBeTruthy(); + } + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty); + await runMysteryEncounterToEnd(game, 3); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 4 - Pokeball Shop", () => { + it("should have the correct properties", () => { + const option = DepartmentStoreSaleEncounter.options[3]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.4.label`, + buttonTooltip: `${namespace}.option.4.tooltip`, + }); + }); + + it("should have shop with only Pokeballs", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty); + await runMysteryEncounterToEnd(game, 4); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(4); + for (const option of modifierSelectHandler.options) { + expect(option.modifierTypeOption.type.id).toContain("BALL"); + } + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty); + await runMysteryEncounterToEnd(game, 4); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); +}); diff --git a/src/test/mystery-encounter/encounters/field-trip-encounter.test.ts b/src/test/mystery-encounter/encounters/field-trip-encounter.test.ts new file mode 100644 index 00000000000..13550abb97c --- /dev/null +++ b/src/test/mystery-encounter/encounters/field-trip-encounter.test.ts @@ -0,0 +1,215 @@ +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { runMysteryEncounterToEnd } from "#test/mystery-encounter/encounter-test-utils"; +import BattleScene from "#app/battle-scene"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { FieldTripEncounter } from "#app/data/mystery-encounters/encounters/field-trip-encounter"; +import { Moves } from "#enums/moves"; +import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; +import { Mode } from "#app/ui/ui"; +import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; + +const namespace = "mysteryEncounter:fieldTrip"; +const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; +const defaultBiome = Biome.CAVE; +const defaultWave = 45; + +describe("Field Trip - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + 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([Moves.TACKLE, Moves.UPROAR, Moves.SWORDS_DANCE]); + + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( + new Map([ + [Biome.CAVE, [MysteryEncounterType.FIELD_TRIP]], + ]) + ); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.FIELD_TRIP, defaultParty); + + expect(FieldTripEncounter.encounterType).toBe(MysteryEncounterType.FIELD_TRIP); + expect(FieldTripEncounter.encounterTier).toBe(MysteryEncounterTier.COMMON); + expect(FieldTripEncounter.dialogue).toBeDefined(); + expect(FieldTripEncounter.dialogue.intro).toStrictEqual([ + { + text: `${namespace}.intro` + }, + { + speaker: `${namespace}.speaker`, + text: `${namespace}.intro_dialogue` + } + ]); + expect(FieldTripEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`); + expect(FieldTripEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`); + expect(FieldTripEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`); + expect(FieldTripEncounter.options.length).toBe(3); + }); + + describe("Option 1 - Show off a physical move", () => { + it("should have the correct properties", () => { + const option = FieldTripEncounter.options[0]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + secondOptionPrompt: `${namespace}.second_option_prompt`, + }); + }); + + it("Should give no reward on incorrect option", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.FIELD_TRIP, defaultParty); + await runMysteryEncounterToEnd(game, 1, { pokemonNo: 1, optionNo: 2 }); + await game.phaseInterceptor.to(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(0); + }); + + it("Should give proper rewards on correct Physical move option", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.FIELD_TRIP, defaultParty); + await runMysteryEncounterToEnd(game, 1, { pokemonNo: 1, optionNo: 1 }); + await game.phaseInterceptor.to(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(5); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.name).toBe("X Attack"); + expect(modifierSelectHandler.options[1].modifierTypeOption.type.name).toBe("X Defense"); + expect(modifierSelectHandler.options[2].modifierTypeOption.type.name).toBe("X Speed"); + expect(modifierSelectHandler.options[3].modifierTypeOption.type.name).toBe("Dire Hit"); + expect(modifierSelectHandler.options[4].modifierTypeOption.type.name).toBe("Rarer Candy"); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.FIELD_TRIP, defaultParty); + await runMysteryEncounterToEnd(game, 1, { pokemonNo: 1, optionNo: 1 }); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 2 - Give Food", () => { + it("should have the correct properties", () => { + const option = FieldTripEncounter.options[1]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + secondOptionPrompt: `${namespace}.second_option_prompt`, + }); + }); + + it("Should give no reward on incorrect option", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.FIELD_TRIP, defaultParty); + await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1 }); + await game.phaseInterceptor.to(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(0); + }); + + it("Should give proper rewards on correct Special move option", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.FIELD_TRIP, defaultParty); + await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 2 }); + await game.phaseInterceptor.to(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(5); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.name).toBe("X Sp. Atk"); + expect(modifierSelectHandler.options[1].modifierTypeOption.type.name).toBe("X Sp. Def"); + expect(modifierSelectHandler.options[2].modifierTypeOption.type.name).toBe("X Speed"); + expect(modifierSelectHandler.options[3].modifierTypeOption.type.name).toBe("Dire Hit"); + expect(modifierSelectHandler.options[4].modifierTypeOption.type.name).toBe("Rarer Candy"); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.FIELD_TRIP, defaultParty); + await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 2 }); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 3 - Give Item", () => { + it("should have the correct properties", () => { + const option = FieldTripEncounter.options[2]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + secondOptionPrompt: `${namespace}.second_option_prompt`, + }); + }); + + it("Should give no reward on incorrect option", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.FIELD_TRIP, defaultParty); + await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); + await game.phaseInterceptor.to(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(0); + }); + + it("Should give proper rewards on correct Special move option", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.FIELD_TRIP, defaultParty); + await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 3 }); + await game.phaseInterceptor.to(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(5); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.name).toBe("X Accuracy"); + expect(modifierSelectHandler.options[1].modifierTypeOption.type.name).toBe("X Speed"); + expect(modifierSelectHandler.options[2].modifierTypeOption.type.name).toBe("5x Great Ball"); + expect(modifierSelectHandler.options[3].modifierTypeOption.type.name).toBe("IV Scanner"); + expect(modifierSelectHandler.options[4].modifierTypeOption.type.name).toBe("Rarer Candy"); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.FIELD_TRIP, defaultParty); + await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 3 }); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); +}); diff --git a/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts b/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts new file mode 100644 index 00000000000..cd11aa2628b --- /dev/null +++ b/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts @@ -0,0 +1,279 @@ +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { FieryFalloutEncounter } from "#app/data/mystery-encounters/encounters/fiery-fallout-encounter"; +import { Gender } from "#app/data/gender"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import * as BattleAnims from "#app/data/battle-anims"; +import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { runMysteryEncounterToEnd, runSelectMysteryEncounterOption, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounter-test-utils"; +import { Moves } from "#enums/moves"; +import BattleScene from "#app/battle-scene"; +import { PokemonHeldItemModifier } from "#app/modifier/modifier"; +import { Type } from "#app/data/type"; +import { Status, StatusEffect } from "#app/data/status-effect"; +import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; +import { CommandPhase } from "#app/phases/command-phase"; +import { MovePhase } from "#app/phases/move-phase"; +import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; + +const namespace = "mysteryEncounter:fieryFallout"; +/** Arcanine and Ninetails for 2 Fire types. Lapras, Gengar, Abra for burnable mon. */ +const defaultParty = [Species.ARCANINE, Species.NINETALES, Species.LAPRAS, Species.GENGAR, Species.ABRA]; +const defaultBiome = Biome.VOLCANO; +const defaultWave = 56; + +describe("Fiery Fallout - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(); + + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( + new Map([ + [Biome.VOLCANO, [MysteryEncounterType.FIERY_FALLOUT]], + [Biome.MOUNTAIN, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]], + ]) + ); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, defaultParty); + + expect(FieryFalloutEncounter.encounterType).toBe(MysteryEncounterType.FIERY_FALLOUT); + expect(FieryFalloutEncounter.encounterTier).toBe(MysteryEncounterTier.COMMON); + expect(FieryFalloutEncounter.dialogue).toBeDefined(); + expect(FieryFalloutEncounter.dialogue.intro).toStrictEqual([{ text: `${namespace}.intro` }]); + expect(FieryFalloutEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`); + expect(FieryFalloutEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`); + expect(FieryFalloutEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`); + expect(FieryFalloutEncounter.options.length).toBe(3); + }); + + it("should not spawn outside of volcano biome", async () => { + game.override.startingBiome(Biome.MOUNTAIN); + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.FIERY_FALLOUT); + }); + + it("should not run below wave 41", async () => { + game.override.startingWave(38); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.FIERY_FALLOUT); + }); + + it("should initialize fully ", async () => { + initSceneWithoutEncounterPhase(scene, defaultParty); + scene.currentBattle.mysteryEncounter = FieryFalloutEncounter; + const weatherSpy = vi.spyOn(scene.arena, "trySetWeather").mockReturnValue(true); + const moveInitSpy = vi.spyOn(BattleAnims, "initMoveAnim"); + const moveLoadSpy = vi.spyOn(BattleAnims, "loadMoveAnimAssets"); + + const { onInit } = FieryFalloutEncounter; + + expect(FieryFalloutEncounter.onInit).toBeDefined(); + + FieryFalloutEncounter.populateDialogueTokensFromRequirements(scene); + const onInitResult = onInit!(scene); + + expect(FieryFalloutEncounter.enemyPartyConfigs).toEqual([ + { + pokemonConfigs: [ + { + species: getPokemonSpecies(Species.VOLCARONA), + isBoss: false, + gender: Gender.MALE + }, + { + species: getPokemonSpecies(Species.VOLCARONA), + isBoss: false, + gender: Gender.FEMALE + } + ], + doubleBattle: true, + disableSwitch: true + } + ]); + expect(weatherSpy).toHaveBeenCalledTimes(1); + await vi.waitFor(() => expect(moveInitSpy).toHaveBeenCalled()); + await vi.waitFor(() => expect(moveLoadSpy).toHaveBeenCalled()); + expect(onInitResult).toBe(true); + }); + + describe("Option 1 - Fight 2 Volcarona", () => { + it("should have the correct properties", () => { + const option1 = FieryFalloutEncounter.options[0]; + expect(option1.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option1.dialogue).toBeDefined(); + expect(option1.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }); + }); + + it("should start battle against 2 Volcarona", async () => { + const phaseSpy = vi.spyOn(scene, "pushPhase"); + + await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, defaultParty); + await runMysteryEncounterToEnd(game, 1, undefined, true); + + const enemyField = scene.getEnemyField(); + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(enemyField.length).toBe(2); + expect(enemyField[0].species.speciesId).toBe(Species.VOLCARONA); + expect(enemyField[1].species.speciesId).toBe(Species.VOLCARONA); + expect(enemyField[0].gender).not.toEqual(enemyField[1].gender); // Should be opposite gender + + const movePhases = phaseSpy.mock.calls.filter(p => p[0] instanceof MovePhase).map(p => p[0]); + expect(movePhases.length).toBe(4); + expect(movePhases.filter(p => (p as MovePhase).move.moveId === Moves.FIRE_SPIN).length).toBe(2); // Fire spin used twice before battle + expect(movePhases.filter(p => (p as MovePhase).move.moveId === Moves.QUIVER_DANCE).length).toBe(2); // Quiver Dance used twice before battle + }); + + it("should give charcoal to lead pokemon", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, defaultParty); + await runMysteryEncounterToEnd(game, 1, undefined, true); + await skipBattleRunMysteryEncounterRewardsPhase(game); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + + const leadPokemonId = scene.getParty()?.[0].id; + const leadPokemonItems = scene.findModifiers(m => m instanceof PokemonHeldItemModifier + && (m as PokemonHeldItemModifier).pokemonId === leadPokemonId, true) as PokemonHeldItemModifier[]; + const charcoal = leadPokemonItems.find(i => i.type.name === "Charcoal"); + expect(charcoal).toBeDefined; + }); + }); + + describe("Option 2 - Suffer the weather", () => { + it("should have the correct properties", () => { + const option1 = FieryFalloutEncounter.options[1]; + expect(option1.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option1.dialogue).toBeDefined(); + expect(option1.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected`, + }, + ], + }); + }); + + it("should damage all non-fire party PKM by 20% and randomly burn 1", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, defaultParty); + + const party = scene.getParty(); + const lapras = party.find((pkm) => pkm.species.speciesId === Species.LAPRAS)!; + lapras.status = new Status(StatusEffect.POISON); + const abra = party.find((pkm) => pkm.species.speciesId === Species.ABRA)!; + vi.spyOn(abra, "isAllowedInBattle").mockReturnValue(false); + + await runMysteryEncounterToEnd(game, 2); + + const burnablePokemon = party.filter((pkm) => pkm.isAllowedInBattle() && !pkm.getTypes().includes(Type.FIRE)); + const notBurnablePokemon = party.filter((pkm) => !pkm.isAllowedInBattle() || pkm.getTypes().includes(Type.FIRE)); + expect(scene.currentBattle.mysteryEncounter?.dialogueTokens["burnedPokemon"]).toBe("Gengar"); + burnablePokemon.forEach((pkm) => { + expect(pkm.hp, `${pkm.name} should have received 20% damage: ${pkm.hp} / ${pkm.getMaxHp()} HP`).toBe(pkm.getMaxHp() - Math.floor(pkm.getMaxHp() * 0.2)); + }); + expect(burnablePokemon.some(pkm => pkm?.status?.effect === StatusEffect.BURN)).toBeTruthy(); + notBurnablePokemon.forEach((pkm) => expect(pkm.hp, `${pkm.name} should be full hp: ${pkm.hp} / ${pkm.getMaxHp()} HP`).toBe(pkm.getMaxHp())); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, defaultParty); + await runMysteryEncounterToEnd(game, 2); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 3 - use FIRE types", () => { + it("should have the correct properties", () => { + const option1 = FieryFalloutEncounter.options[2]; + expect(option1.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL); + expect(option1.dialogue).toBeDefined(); + expect(option1.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + disabledButtonTooltip: `${namespace}.option.3.disabled_tooltip`, + selected: [ + { + text: `${namespace}.option.3.selected`, + }, + ], + }); + }); + + it("should give charcoal to lead pokemon", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, defaultParty); + await runMysteryEncounterToEnd(game, 3); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + + const leadPokemonId = scene.getParty()?.[0].id; + const leadPokemonItems = scene.findModifiers(m => m instanceof PokemonHeldItemModifier + && (m as PokemonHeldItemModifier).pokemonId === leadPokemonId, true) as PokemonHeldItemModifier[]; + const charcoal = leadPokemonItems.find(i => i.type.name === "Charcoal"); + expect(charcoal).toBeDefined; + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, defaultParty); + await runMysteryEncounterToEnd(game, 3); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + + it("should be disabled if not enough FIRE types are in party", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, [Species.MAGIKARP, Species.ARCANINE]); + await game.phaseInterceptor.to(MysteryEncounterPhase, false); + + const encounterPhase = scene.getCurrentPhase(); + expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name); + const continueEncounterSpy = vi.spyOn((encounterPhase as MysteryEncounterPhase), "continueEncounter"); + + await runSelectMysteryEncounterOption(game, 3); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterPhase.name); + expect(continueEncounterSpy).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/test/mystery-encounter/encounters/fight-or-flight-encounter.test.ts b/src/test/mystery-encounter/encounters/fight-or-flight-encounter.test.ts new file mode 100644 index 00000000000..df2f32231ba --- /dev/null +++ b/src/test/mystery-encounter/encounters/fight-or-flight-encounter.test.ts @@ -0,0 +1,205 @@ +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { runMysteryEncounterToEnd, runSelectMysteryEncounterOption, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounter-test-utils"; +import { Moves } from "#enums/moves"; +import BattleScene from "#app/battle-scene"; +import { PokemonMove } from "#app/field/pokemon"; +import { Mode } from "#app/ui/ui"; +import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; +import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { FightOrFlightEncounter } from "#app/data/mystery-encounters/encounters/fight-or-flight-encounter"; +import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; +import { CommandPhase } from "#app/phases/command-phase"; +import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; + +const namespace = "mysteryEncounter:fightOrFlight"; +const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; +const defaultBiome = Biome.CAVE; +const defaultWave = 45; + +describe("Fight or Flight - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(); + + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( + new Map([ + [Biome.CAVE, [MysteryEncounterType.FIGHT_OR_FLIGHT]], + ]) + ); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.FIGHT_OR_FLIGHT, defaultParty); + + expect(FightOrFlightEncounter.encounterType).toBe(MysteryEncounterType.FIGHT_OR_FLIGHT); + expect(FightOrFlightEncounter.encounterTier).toBe(MysteryEncounterTier.COMMON); + expect(FightOrFlightEncounter.dialogue).toBeDefined(); + expect(FightOrFlightEncounter.dialogue.intro).toStrictEqual([{ text: `${namespace}.intro` }]); + expect(FightOrFlightEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`); + expect(FightOrFlightEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`); + expect(FightOrFlightEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`); + expect(FightOrFlightEncounter.options.length).toBe(3); + }); + + it("should initialize fully", async () => { + initSceneWithoutEncounterPhase(scene, defaultParty); + scene.currentBattle.mysteryEncounter = FightOrFlightEncounter; + + const { onInit } = FightOrFlightEncounter; + + expect(FightOrFlightEncounter.onInit).toBeDefined(); + + FightOrFlightEncounter.populateDialogueTokensFromRequirements(scene); + const onInitResult = onInit!(scene); + + const config = FightOrFlightEncounter.enemyPartyConfigs[0]; + expect(config).toBeDefined(); + expect(config.pokemonConfigs?.[0].isBoss).toBe(true); + expect(onInitResult).toBe(true); + }); + + describe("Option 1 - Fight", () => { + it("should have the correct properties", () => { + const option = FightOrFlightEncounter.options[0]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }); + }); + + it("should start a fight against the boss", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.FIGHT_OR_FLIGHT, defaultParty); + + const config = game.scene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0]; + const speciesToSpawn = config.pokemonConfigs?.[0].species.speciesId; + + await runMysteryEncounterToEnd(game, 1, undefined, true); + + const enemyField = scene.getEnemyField(); + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(enemyField.length).toBe(1); + expect(enemyField[0].species.speciesId).toBe(speciesToSpawn); + }); + + it("should reward the player with the item based on wave", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.FIGHT_OR_FLIGHT, defaultParty); + + const item = game.scene.currentBattle.mysteryEncounter?.misc; + + await runMysteryEncounterToEnd(game, 1, undefined, true); + await skipBattleRunMysteryEncounterRewardsPhase(game); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(1); + expect(item.type.name).toBe(modifierSelectHandler.options[0].modifierTypeOption.type.name); + }); + }); + + describe("Option 2 - Attempt to Steal", () => { + it("should have the correct properties", () => { + const option = FightOrFlightEncounter.options[1]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + disabledButtonTooltip: `${namespace}.option.2.disabled_tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected`, + } + ], + }); + }); + + it("should NOT be selectable if the player doesn't have a Stealing move", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.FIGHT_OR_FLIGHT, defaultParty); + scene.getParty().forEach(p => p.moveset = []); + await game.phaseInterceptor.to(MysteryEncounterPhase, false); + + const encounterPhase = scene.getCurrentPhase(); + expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name); + const mysteryEncounterPhase = encounterPhase as MysteryEncounterPhase; + vi.spyOn(mysteryEncounterPhase, "continueEncounter"); + vi.spyOn(mysteryEncounterPhase, "handleOptionSelect"); + vi.spyOn(scene.ui, "playError"); + + await runSelectMysteryEncounterOption(game, 2); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterPhase.name); + expect(scene.ui.playError).not.toHaveBeenCalled(); // No error sfx, option is disabled + expect(mysteryEncounterPhase.handleOptionSelect).not.toHaveBeenCalled(); + expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled(); + }); + + it("Should skip fight when player meets requirements", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.FIGHT_OR_FLIGHT, defaultParty); + + // Mock moveset + scene.getParty()[0].moveset = [new PokemonMove(Moves.KNOCK_OFF)]; + const item = game.scene.currentBattle.mysteryEncounter!.misc; + + await runMysteryEncounterToEnd(game, 2); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(1); + expect(item.type.name).toBe(modifierSelectHandler.options[0].modifierTypeOption.type.name); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 3 - Leave", () => { + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.FIGHT_OR_FLIGHT, defaultParty); + await runMysteryEncounterToEnd(game, 3); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); +}); diff --git a/src/test/mystery-encounter/encounters/fun-and-games-encounter.test.ts b/src/test/mystery-encounter/encounters/fun-and-games-encounter.test.ts new file mode 100644 index 00000000000..c337556728b --- /dev/null +++ b/src/test/mystery-encounter/encounters/fun-and-games-encounter.test.ts @@ -0,0 +1,288 @@ +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { HUMAN_TRANSITABLE_BIOMES } from "#app/data/mystery-encounters/mystery-encounters"; +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { runMysteryEncounterToEnd, runSelectMysteryEncounterOption } from "#test/mystery-encounter/encounter-test-utils"; +import BattleScene from "#app/battle-scene"; +import { Mode } from "#app/ui/ui"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; +import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; +import MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; +import { Nature } from "#enums/nature"; +import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; +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 { Moves } from "#enums/moves"; +import { Command } from "#app/ui/command-ui-handler"; +import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; + +const namespace = "mysteryEncounter:funAndGames"; +const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; +const defaultBiome = Biome.CAVE; +const defaultWave = 45; + +describe("Fun And Games! - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(); + + const biomeMap = new Map([ + [Biome.VOLCANO, [MysteryEncounterType.FIGHT_OR_FLIGHT]], + ]); + HUMAN_TRANSITABLE_BIOMES.forEach(biome => { + biomeMap.set(biome, [MysteryEncounterType.FUN_AND_GAMES]); + }); + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue(biomeMap); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.FUN_AND_GAMES, defaultParty); + + expect(FunAndGamesEncounter.encounterType).toBe(MysteryEncounterType.FUN_AND_GAMES); + expect(FunAndGamesEncounter.encounterTier).toBe(MysteryEncounterTier.GREAT); + expect(FunAndGamesEncounter.dialogue).toBeDefined(); + expect(FunAndGamesEncounter.dialogue.intro).toStrictEqual([ + { + speaker: `${namespace}.speaker`, + text: `${namespace}.intro_dialogue`, + } + ]); + expect(FunAndGamesEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`); + expect(FunAndGamesEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`); + expect(FunAndGamesEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`); + expect(FunAndGamesEncounter.options.length).toBe(2); + }); + + it("should not spawn outside of CIVILIZATIONN biomes", async () => { + game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT); + game.override.startingBiome(Biome.VOLCANO); + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.FUN_AND_GAMES); + }); + + it("should initialize fully", async () => { + initSceneWithoutEncounterPhase(scene, defaultParty); + scene.currentBattle.mysteryEncounter = new MysteryEncounter(FunAndGamesEncounter); + const encounter = scene.currentBattle.mysteryEncounter!; + scene.currentBattle.waveIndex = defaultWave; + + const { onInit } = encounter; + + expect(encounter.onInit).toBeDefined(); + + const onInitResult = onInit!(scene); + expect(onInitResult).toBe(true); + }); + + describe("Option 1 - Play the Wobbuffet game", () => { + it("should have the correct properties", () => { + const option = FunAndGamesEncounter.options[0]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }); + }); + + it("should NOT be selectable if the player doesn't have enough money", async () => { + game.scene.money = 0; + await game.runToMysteryEncounter(MysteryEncounterType.FUN_AND_GAMES, defaultParty); + await game.phaseInterceptor.to(MysteryEncounterPhase, false); + + const encounterPhase = scene.getCurrentPhase(); + expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name); + const mysteryEncounterPhase = encounterPhase as MysteryEncounterPhase; + vi.spyOn(mysteryEncounterPhase, "continueEncounter"); + vi.spyOn(mysteryEncounterPhase, "handleOptionSelect"); + vi.spyOn(scene.ui, "playError"); + + await runSelectMysteryEncounterOption(game, 1); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterPhase.name); + expect(scene.ui.playError).not.toHaveBeenCalled(); // No error sfx, option is disabled + expect(mysteryEncounterPhase.handleOptionSelect).not.toHaveBeenCalled(); + expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled(); + }); + + it("should get 3 turns to attack the Wobbuffet for a reward", async () => { + scene.money = 20000; + game.override.moveset([Moves.TACKLE]); + await game.runToMysteryEncounter(MysteryEncounterType.FUN_AND_GAMES, defaultParty); + await runMysteryEncounterToEnd(game, 1, { pokemonNo: 1 }, true); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(scene.getEnemyPokemon()?.species.speciesId).toBe(Species.WOBBUFFET); + expect(scene.getEnemyPokemon()?.ivs).toEqual([0, 0, 0, 0, 0, 0]); + expect(scene.getEnemyPokemon()?.nature).toBe(Nature.MILD); + + game.onNextPrompt("MessagePhase", Mode.MESSAGE, () => { + game.endPhase(); + }); + + // Turn 1 + (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, false); + await game.phaseInterceptor.to(CommandPhase); + + // Turn 2 + (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, false); + await game.phaseInterceptor.to(CommandPhase); + + // Turn 3 + (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, false); + await game.phaseInterceptor.to(SelectModifierPhase, false); + + // Rewards + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + }); + + it("should have no items in rewards if Wubboffet doesn't take enough damage", async () => { + scene.money = 20000; + await game.runToMysteryEncounter(MysteryEncounterType.FUN_AND_GAMES, defaultParty); + await runMysteryEncounterToEnd(game, 1, { pokemonNo: 1 }, true); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + game.onNextPrompt("MessagePhase", Mode.MESSAGE, () => { + game.endPhase(); + }); + + // Skip minigame + scene.currentBattle.mysteryEncounter!.misc.turnsRemaining = 0; + (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, false); + await game.phaseInterceptor.to(SelectModifierPhase, false); + + // Rewards + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(0); + }); + + it("should have Wide Lens item in rewards if Wubboffet is at 15-33% HP remaining", async () => { + scene.money = 20000; + game.override.moveset([Moves.SPLASH]); + await game.runToMysteryEncounter(MysteryEncounterType.FUN_AND_GAMES, defaultParty); + await runMysteryEncounterToEnd(game, 1, { pokemonNo: 1 }, true); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + game.onNextPrompt("MessagePhase", Mode.MESSAGE, () => { + game.endPhase(); + }); + + // Skip minigame + const wobbuffet = scene.getEnemyPokemon()!; + wobbuffet.hp = Math.floor(0.2 * wobbuffet.getMaxHp()); + scene.currentBattle.mysteryEncounter!.misc.turnsRemaining = 0; + (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, false); + await game.phaseInterceptor.to(SelectModifierPhase, false); + + // Rewards + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(1); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toEqual("WIDE_LENS"); + }); + + it("should have Scope Lens item in rewards if Wubboffet is at 3-15% HP remaining", async () => { + scene.money = 20000; + game.override.moveset([Moves.SPLASH]); + await game.runToMysteryEncounter(MysteryEncounterType.FUN_AND_GAMES, defaultParty); + await runMysteryEncounterToEnd(game, 1, { pokemonNo: 1 }, true); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + game.onNextPrompt("MessagePhase", Mode.MESSAGE, () => { + game.endPhase(); + }); + + // Skip minigame + const wobbuffet = scene.getEnemyPokemon()!; + wobbuffet.hp = Math.floor(0.1 * wobbuffet.getMaxHp()); + scene.currentBattle.mysteryEncounter!.misc.turnsRemaining = 0; + (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, false); + await game.phaseInterceptor.to(SelectModifierPhase, false); + + // Rewards + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(1); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toEqual("SCOPE_LENS"); + }); + + it("should have Multi Lens item in rewards if Wubboffet is at <3% HP remaining", async () => { + scene.money = 20000; + game.override.moveset([Moves.SPLASH]); + await game.runToMysteryEncounter(MysteryEncounterType.FUN_AND_GAMES, defaultParty); + await runMysteryEncounterToEnd(game, 1, { pokemonNo: 1 }, true); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + game.onNextPrompt("MessagePhase", Mode.MESSAGE, () => { + game.endPhase(); + }); + + // Skip minigame + const wobbuffet = scene.getEnemyPokemon()!; + wobbuffet.hp = 1; + scene.currentBattle.mysteryEncounter!.misc.turnsRemaining = 0; + (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, false); + await game.phaseInterceptor.to(SelectModifierPhase, false); + + // Rewards + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(1); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toEqual("MULTI_LENS"); + }); + }); + + describe("Option 2 - Leave", () => { + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.FUN_AND_GAMES, defaultParty); + await runMysteryEncounterToEnd(game, 2); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); +}); diff --git a/src/test/mystery-encounter/encounters/global-trade-system-encounter.test.ts b/src/test/mystery-encounter/encounters/global-trade-system-encounter.test.ts new file mode 100644 index 00000000000..ec35b338365 --- /dev/null +++ b/src/test/mystery-encounter/encounters/global-trade-system-encounter.test.ts @@ -0,0 +1,268 @@ +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { runMysteryEncounterToEnd } from "#test/mystery-encounter/encounter-test-utils"; +import BattleScene from "#app/battle-scene"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +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 { 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 { Mode } from "#app/ui/ui"; +import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; +import { ModifierTier } from "#app/modifier/modifier-tier"; + +const namespace = "mysteryEncounter:globalTradeSystem"; +const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; +const defaultBiome = Biome.CAVE; +const defaultWave = 45; + +describe("Global Trade System - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(); + + const biomeMap = new Map([ + [Biome.VOLCANO, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]], + ]); + CIVILIZATION_ENCOUNTER_BIOMES.forEach(biome => { + biomeMap.set(biome, [MysteryEncounterType.GLOBAL_TRADE_SYSTEM]); + }); + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue(biomeMap); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.GLOBAL_TRADE_SYSTEM, defaultParty); + + expect(GlobalTradeSystemEncounter.encounterType).toBe(MysteryEncounterType.GLOBAL_TRADE_SYSTEM); + expect(GlobalTradeSystemEncounter.encounterTier).toBe(MysteryEncounterTier.COMMON); + expect(GlobalTradeSystemEncounter.dialogue).toBeDefined(); + expect(GlobalTradeSystemEncounter.dialogue.intro).toStrictEqual([{ text: `${namespace}.intro` }]); + expect(GlobalTradeSystemEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`); + expect(GlobalTradeSystemEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`); + expect(GlobalTradeSystemEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`); + expect(GlobalTradeSystemEncounter.options.length).toBe(4); + }); + + it("should not loop infinitely when generating trade options for extreme BST non-legendaries", async () => { + const extremeBstTeam = [Species.SLAKING, Species.WISHIWASHI, Species.SUNKERN]; + await game.runToMysteryEncounter(MysteryEncounterType.GLOBAL_TRADE_SYSTEM, extremeBstTeam); + + expect(GlobalTradeSystemEncounter.encounterType).toBe(MysteryEncounterType.GLOBAL_TRADE_SYSTEM); + expect(GlobalTradeSystemEncounter.encounterTier).toBe(MysteryEncounterTier.COMMON); + expect(GlobalTradeSystemEncounter.dialogue).toBeDefined(); + expect(GlobalTradeSystemEncounter.dialogue.intro).toStrictEqual([{ text: `${namespace}.intro` }]); + expect(GlobalTradeSystemEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`); + expect(GlobalTradeSystemEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`); + expect(GlobalTradeSystemEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`); + expect(GlobalTradeSystemEncounter.options.length).toBe(4); + }); + + it("should not spawn outside of CIVILIZATION_ENCOUNTER_BIOMES", async () => { + game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON); + game.override.startingBiome(Biome.VOLCANO); + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.GLOBAL_TRADE_SYSTEM); + }); + + describe("Option 1 - Check Trade Offers", () => { + it("should have the correct properties", () => { + const option = GlobalTradeSystemEncounter.options[0]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + secondOptionPrompt: `${namespace}.option.1.trade_options_prompt`, + }); + }); + + it("Should trade a Pokemon from the player's party for the first of 3 Pokemon options", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.GLOBAL_TRADE_SYSTEM, defaultParty); + + const speciesBefore = scene.getParty()[0].species.speciesId; + await runMysteryEncounterToEnd(game, 1, { pokemonNo: 1, optionNo: 1 }); + + const speciesAfter = scene.getParty().at(-1)?.species.speciesId; + + expect(speciesAfter).toBeDefined(); + expect(speciesBefore).not.toBe(speciesAfter); + expect(defaultParty.includes(speciesAfter!)).toBeFalsy(); + }); + + it("Should trade a Pokemon from the player's party for the second of 3 Pokemon options", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.GLOBAL_TRADE_SYSTEM, defaultParty); + + const speciesBefore = scene.getParty()[1].species.speciesId; + await runMysteryEncounterToEnd(game, 1, { pokemonNo: 2, optionNo: 2 }); + + const speciesAfter = scene.getParty().at(-1)?.species.speciesId; + + expect(speciesAfter).toBeDefined(); + expect(speciesBefore).not.toBe(speciesAfter); + expect(defaultParty.includes(speciesAfter!)).toBeFalsy(); + }); + + it("Should trade a Pokemon from the player's party for the third of 3 Pokemon options", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.GLOBAL_TRADE_SYSTEM, defaultParty); + + const speciesBefore = scene.getParty()[2].species.speciesId; + await runMysteryEncounterToEnd(game, 1, { pokemonNo: 3, optionNo: 3 }); + + const speciesAfter = scene.getParty().at(-1)?.species.speciesId; + + expect(speciesAfter).toBeDefined(); + expect(speciesBefore).not.toBe(speciesAfter); + expect(defaultParty.includes(speciesAfter!)).toBeFalsy(); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.GLOBAL_TRADE_SYSTEM, defaultParty); + await runMysteryEncounterToEnd(game, 1, { pokemonNo: 1, optionNo: 1 }); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 2 - Wonder Trade", () => { + it("should have the correct properties", () => { + const option = GlobalTradeSystemEncounter.options[1]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip` + }); + }); + + it("Should trade a Pokemon from the player's party for a random wonder trade Pokemon", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.GLOBAL_TRADE_SYSTEM, defaultParty); + + const speciesBefore = scene.getParty()[2].species.speciesId; + await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1 }); + + const speciesAfter = scene.getParty().at(-1)?.species.speciesId; + + expect(speciesAfter).toBeDefined(); + expect(speciesBefore).not.toBe(speciesAfter); + expect(defaultParty.includes(speciesAfter!)).toBeFalsy(); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.GLOBAL_TRADE_SYSTEM, defaultParty); + await runMysteryEncounterToEnd(game, 2, { pokemonNo: 2 }); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 3 - Trade an Item", () => { + it("should have the correct properties", () => { + const option = GlobalTradeSystemEncounter.options[2]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + secondOptionPrompt: `${namespace}.option.3.trade_options_prompt`, + }); + }); + + it("should decrease item stacks of chosen item and have a tiered up item in rewards", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.GLOBAL_TRADE_SYSTEM, defaultParty); + + // Set 2 Soul Dew on party lead + scene.modifiers = []; + const soulDew = generateModifierType(scene, modifierTypes.SOUL_DEW)!; + const modifier = soulDew.newModifier(scene.getParty()[0]) as PokemonNatureWeightModifier; + modifier.stackCount = 2; + await scene.addModifier(modifier, true, false, false, true); + await scene.updateModifiers(true); + + await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1}); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(1); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.tier).toBe(ModifierTier.MASTER); + const soulDewAfter = scene.findModifier(m => m instanceof PokemonNatureWeightModifier); + expect(soulDewAfter?.stackCount).toBe(1); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.GLOBAL_TRADE_SYSTEM, defaultParty); + + // Set 1 Soul Dew on party lead + scene.modifiers = []; + const soulDew = generateModifierType(scene, modifierTypes.SOUL_DEW)!; + const modifier = soulDew.newModifier(scene.getParty()[0]) as PokemonNatureWeightModifier; + modifier.stackCount = 1; + await scene.addModifier(modifier, true, false, false, true); + await scene.updateModifiers(true); + + await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1}); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 4 - Leave", () => { + it("should have the correct properties", () => { + const option = GlobalTradeSystemEncounter.options[3]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.4.label`, + buttonTooltip: `${namespace}.option.4.tooltip`, + selected: [ + { + text: `${namespace}.option.4.selected`, + }, + ], + }); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.GLOBAL_TRADE_SYSTEM, defaultParty); + await runMysteryEncounterToEnd(game, 4); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); +}); diff --git a/src/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts b/src/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts new file mode 100644 index 00000000000..02872334fac --- /dev/null +++ b/src/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts @@ -0,0 +1,268 @@ +import { LostAtSeaEncounter } from "#app/data/mystery-encounters/encounters/lost-at-sea-encounter"; +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { runMysteryEncounterToEnd, runSelectMysteryEncounterOption } from "../encounter-test-utils"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; +import BattleScene from "#app/battle-scene"; +import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; +import { PartyExpPhase } from "#app/phases/party-exp-phase"; + + +const namespace = "mysteryEncounter:lostAtSea"; +/** Blastoise for surf. Pidgeot for fly. Abra for none. */ +const defaultParty = [Species.BLASTOISE, Species.PIDGEOT, Species.ABRA]; +const defaultBiome = Biome.SEA; +const defaultWave = 33; + +describe("Lost at Sea - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(); + + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( + new Map([ + [Biome.SEA, [MysteryEncounterType.LOST_AT_SEA]], + [Biome.MOUNTAIN, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]], + ]) + ); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.LOST_AT_SEA, defaultParty); + + expect(LostAtSeaEncounter.encounterType).toBe(MysteryEncounterType.LOST_AT_SEA); + expect(LostAtSeaEncounter.encounterTier).toBe(MysteryEncounterTier.COMMON); + expect(LostAtSeaEncounter.dialogue).toBeDefined(); + expect(LostAtSeaEncounter.dialogue.intro).toStrictEqual([{ text: `${namespace}.intro` }]); + expect(LostAtSeaEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`); + expect(LostAtSeaEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`); + expect(LostAtSeaEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`); + expect(LostAtSeaEncounter.options.length).toBe(3); + }); + + it("should not spawn outside of sea biome", async () => { + game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON); + game.override.startingBiome(Biome.MOUNTAIN); + await game.runToMysteryEncounter(); + + expect(game.scene.currentBattle.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.LOST_AT_SEA); + }); + + it("should initialize fully", () => { + initSceneWithoutEncounterPhase(scene, defaultParty); + scene.currentBattle.mysteryEncounter = LostAtSeaEncounter; + + const { onInit } = LostAtSeaEncounter; + + expect(LostAtSeaEncounter.onInit).toBeDefined(); + + LostAtSeaEncounter.populateDialogueTokensFromRequirements(scene); + const onInitResult = onInit!(scene); + + expect(LostAtSeaEncounter.dialogueTokens?.damagePercentage).toBe("25"); + expect(LostAtSeaEncounter.dialogueTokens?.option1RequiredMove).toBe("Surf"); + expect(LostAtSeaEncounter.dialogueTokens?.option2RequiredMove).toBe("Fly"); + expect(onInitResult).toBe(true); + }); + + describe("Option 1 - Surf", () => { + it("should have the correct properties", () => { + const option1 = LostAtSeaEncounter.options[0]; + expect(option1.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT); + expect(option1.dialogue).toBeDefined(); + expect(option1.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.1.label`, + disabledButtonLabel: `${namespace}.option.1.label_disabled`, + buttonTooltip: `${namespace}.option.1.tooltip`, + disabledButtonTooltip: `${namespace}.option.1.tooltip_disabled`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }); + }); + + it("should award exp to surfable PKM (Blastoise)", async () => { + const laprasSpecies = getPokemonSpecies(Species.LAPRAS); + + await game.runToMysteryEncounter(MysteryEncounterType.LOST_AT_SEA, defaultParty); + const party = game.scene.getParty(); + const blastoise = party.find((pkm) => pkm.species.speciesId === Species.BLASTOISE); + const expBefore = blastoise!.exp; + + await runMysteryEncounterToEnd(game, 1); + await game.phaseInterceptor.to(PartyExpPhase); + + expect(blastoise?.exp).toBe(expBefore + Math.floor(laprasSpecies.baseExp * defaultWave / 5 + 1)); + }); + + it("should leave encounter without battle", async () => { + game.override.startingWave(33); + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.LOST_AT_SEA, defaultParty); + await runMysteryEncounterToEnd(game, 1); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + + it("should be disabled if no surfable PKM is in party", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.LOST_AT_SEA, [Species.ARCANINE]); + await game.phaseInterceptor.to(MysteryEncounterPhase, false); + + const encounterPhase = scene.getCurrentPhase(); + expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name); + const mysteryEncounterPhase = encounterPhase as MysteryEncounterPhase; + vi.spyOn(mysteryEncounterPhase, "continueEncounter"); + vi.spyOn(mysteryEncounterPhase, "handleOptionSelect"); + vi.spyOn(scene.ui, "playError"); + + await runSelectMysteryEncounterOption(game, 1); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterPhase.name); + expect(scene.ui.playError).not.toHaveBeenCalled(); // No error sfx, option is disabled + expect(mysteryEncounterPhase.handleOptionSelect).not.toHaveBeenCalled(); + expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled(); + }); + }); + + describe("Option 2 - Fly", () => { + it("should have the correct properties", () => { + const option2 = LostAtSeaEncounter.options[1]; + + expect(option2.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT); + expect(option2.dialogue).toBeDefined(); + expect(option2.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.2.label`, + disabledButtonLabel: `${namespace}.option.2.label_disabled`, + buttonTooltip: `${namespace}.option.2.tooltip`, + disabledButtonTooltip: `${namespace}.option.2.tooltip_disabled`, + selected: [ + { + text: `${namespace}.option.2.selected`, + }, + ], + }); + }); + + it("should award exp to flyable PKM (Pidgeot)", async () => { + const laprasBaseExp = 187; + const wave = 33; + game.override.startingWave(wave); + + await game.runToMysteryEncounter(MysteryEncounterType.LOST_AT_SEA, defaultParty); + const party = game.scene.getParty(); + const pidgeot = party.find((pkm) => pkm.species.speciesId === Species.PIDGEOT); + const expBefore = pidgeot!.exp; + + await runMysteryEncounterToEnd(game, 2); + await game.phaseInterceptor.to(PartyExpPhase); + + expect(pidgeot!.exp).toBe(expBefore + Math.floor(laprasBaseExp * defaultWave / 5 + 1)); + }); + + it("should leave encounter without battle", async () => { + game.override.startingWave(33); + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.LOST_AT_SEA, defaultParty); + await runMysteryEncounterToEnd(game, 2); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + + it("should be disabled if no flyable PKM is in party", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.LOST_AT_SEA, [Species.ARCANINE]); + await game.phaseInterceptor.to(MysteryEncounterPhase, false); + + const encounterPhase = scene.getCurrentPhase(); + expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name); + const mysteryEncounterPhase = encounterPhase as MysteryEncounterPhase; + vi.spyOn(mysteryEncounterPhase, "continueEncounter"); + vi.spyOn(mysteryEncounterPhase, "handleOptionSelect"); + vi.spyOn(scene.ui, "playError"); + + await runSelectMysteryEncounterOption(game, 2); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterPhase.name); + expect(scene.ui.playError).not.toHaveBeenCalled(); // No error sfx, option is disabled + expect(mysteryEncounterPhase.handleOptionSelect).not.toHaveBeenCalled(); + expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled(); + }); + }); + + describe("Option 3 - Wander aimlessy", () => { + it("should have the correct properties", () => { + const option3 = LostAtSeaEncounter.options[2]; + + expect(option3.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option3.dialogue).toBeDefined(); + expect(option3.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + selected: [ + { + text: `${namespace}.option.3.selected`, + }, + ], + }); + }); + + it("should damage all (allowed in battle) party PKM by 25%", async () => { + game.override.startingWave(33); + + await game.runToMysteryEncounter(MysteryEncounterType.LOST_AT_SEA, defaultParty); + + const party = game.scene.getParty(); + const abra = party.find((pkm) => pkm.species.speciesId === Species.ABRA)!; + vi.spyOn(abra, "isAllowedInBattle").mockReturnValue(false); + + await runMysteryEncounterToEnd(game, 3); + + const allowedPkm = party.filter((pkm) => pkm.isAllowedInBattle()); + const notAllowedPkm = party.filter((pkm) => !pkm.isAllowedInBattle()); + allowedPkm.forEach((pkm) => + expect(pkm.hp, `${pkm.name} should have receivd 25% damage: ${pkm.hp} / ${pkm.getMaxHp()} HP`).toBe(pkm.getMaxHp() - Math.floor(pkm.getMaxHp() * 0.25)) + ); + + notAllowedPkm.forEach((pkm) => expect(pkm.hp, `${pkm.name} should be full hp: ${pkm.hp} / ${pkm.getMaxHp()} HP`).toBe(pkm.getMaxHp())); + }); + + it("should leave encounter without battle", async () => { + game.override.startingWave(33); + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.LOST_AT_SEA, defaultParty); + await runMysteryEncounterToEnd(game, 3); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); +}); diff --git a/src/test/mystery-encounter/encounters/mysterious-challengers-encounter.test.ts b/src/test/mystery-encounter/encounters/mysterious-challengers-encounter.test.ts new file mode 100644 index 00000000000..15cd3338fff --- /dev/null +++ b/src/test/mystery-encounter/encounters/mysterious-challengers-encounter.test.ts @@ -0,0 +1,254 @@ +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { HUMAN_TRANSITABLE_BIOMES } from "#app/data/mystery-encounters/mystery-encounters"; +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { runMysteryEncounterToEnd, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounter-test-utils"; +import BattleScene from "#app/battle-scene"; +import { Mode } from "#app/ui/ui"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; +import { ModifierTier } from "#app/modifier/modifier-tier"; +import { MysteriousChallengersEncounter } from "#app/data/mystery-encounters/encounters/mysterious-challengers-encounter"; +import { TrainerConfig, TrainerPartyCompoundTemplate, TrainerPartyTemplate } from "#app/data/trainer-config"; +import { PartyMemberStrength } from "#enums/party-member-strength"; +import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; +import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; +import MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; +import { CommandPhase } from "#app/phases/command-phase"; +import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; + +const namespace = "mysteryEncounter:mysteriousChallengers"; +const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; +const defaultBiome = Biome.CAVE; +const defaultWave = 45; + +describe("Mysterious Challengers - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(); + + const biomeMap = new Map([ + [Biome.VOLCANO, [MysteryEncounterType.FIGHT_OR_FLIGHT]], + ]); + HUMAN_TRANSITABLE_BIOMES.forEach(biome => { + biomeMap.set(biome, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]); + }); + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue(biomeMap); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS, defaultParty); + + expect(MysteriousChallengersEncounter.encounterType).toBe(MysteryEncounterType.MYSTERIOUS_CHALLENGERS); + expect(MysteriousChallengersEncounter.encounterTier).toBe(MysteryEncounterTier.GREAT); + expect(MysteriousChallengersEncounter.dialogue).toBeDefined(); + expect(MysteriousChallengersEncounter.dialogue.intro).toStrictEqual([{ text: `${namespace}.intro` }]); + expect(MysteriousChallengersEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`); + expect(MysteriousChallengersEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`); + expect(MysteriousChallengersEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`); + expect(MysteriousChallengersEncounter.options.length).toBe(3); + }); + + it("should not spawn outside of HUMAN_TRANSITABLE_BIOMES", async () => { + game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT); + game.override.startingBiome(Biome.VOLCANO); + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.MYSTERIOUS_CHALLENGERS); + }); + + it("should initialize fully", async () => { + initSceneWithoutEncounterPhase(scene, defaultParty); + scene.currentBattle.mysteryEncounter = new MysteryEncounter(MysteriousChallengersEncounter); + const encounter = scene.currentBattle.mysteryEncounter!; + scene.currentBattle.waveIndex = defaultWave; + + const { onInit } = encounter; + + expect(encounter.onInit).toBeDefined(); + + encounter.populateDialogueTokensFromRequirements(scene); + const onInitResult = onInit!(scene); + + expect(encounter.enemyPartyConfigs).toBeDefined(); + expect(encounter.enemyPartyConfigs.length).toBe(3); + expect(encounter.enemyPartyConfigs).toEqual([ + { + trainerConfig: expect.any(TrainerConfig), + female: expect.any(Boolean), + }, + { + trainerConfig: expect.any(TrainerConfig), + levelAdditiveModifier: 1, + female: expect.any(Boolean), + }, + { + trainerConfig: expect.any(TrainerConfig), + levelAdditiveModifier: 1.5, + female: expect.any(Boolean), + } + ]); + expect(encounter.enemyPartyConfigs[1].trainerConfig?.partyTemplates[0]).toEqual(new TrainerPartyCompoundTemplate( + new TrainerPartyTemplate(1, PartyMemberStrength.STRONGER, false, true), + new TrainerPartyTemplate(3, PartyMemberStrength.AVERAGE, false, true) + )); + expect(encounter.enemyPartyConfigs[2].trainerConfig?.partyTemplates[0]).toEqual(new TrainerPartyCompoundTemplate( + new TrainerPartyTemplate(2, PartyMemberStrength.AVERAGE), + new TrainerPartyTemplate(3, PartyMemberStrength.STRONG), + new TrainerPartyTemplate(1, PartyMemberStrength.STRONGER)) + ); + expect(encounter.spriteConfigs).toBeDefined(); + expect(encounter.spriteConfigs.length).toBe(3); + expect(onInitResult).toBe(true); + }); + + describe("Option 1 - Normal Battle", () => { + it("should have the correct properties", () => { + const option = MysteriousChallengersEncounter.options[0]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.selected`, + }, + ], + }); + }); + + it("should start battle against the trainer", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS, defaultParty); + await runMysteryEncounterToEnd(game, 1, undefined, true); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(scene.currentBattle.trainer).toBeDefined(); + expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); + }); + + it("should have normal trainer rewards after battle", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS, defaultParty); + await runMysteryEncounterToEnd(game, 1, undefined, true); + await skipBattleRunMysteryEncounterRewardsPhase(game); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(3); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toContain("TM_COMMON"); + expect(modifierSelectHandler.options[1].modifierTypeOption.type.id).toContain("TM_GREAT"); + expect(modifierSelectHandler.options[2].modifierTypeOption.type.id).toContain("MEMORY_MUSHROOM"); + }); + }); + + describe("Option 2 - Hard Battle", () => { + it("should have the correct properties", () => { + const option = MysteriousChallengersEncounter.options[1]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + text: `${namespace}.option.selected`, + }, + ], + }); + }); + + it("should start battle against the trainer", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS, defaultParty); + await runMysteryEncounterToEnd(game, 2, undefined, true); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(scene.currentBattle.trainer).toBeDefined(); + expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); + }); + + it("should have hard trainer rewards after battle", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS, defaultParty); + await runMysteryEncounterToEnd(game, 2, undefined, true); + await skipBattleRunMysteryEncounterRewardsPhase(game); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(4); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.tier - modifierSelectHandler.options[0].modifierTypeOption.upgradeCount).toBe(ModifierTier.ULTRA); + expect(modifierSelectHandler.options[1].modifierTypeOption.type.tier - modifierSelectHandler.options[1].modifierTypeOption.upgradeCount).toBe(ModifierTier.ULTRA); + expect(modifierSelectHandler.options[2].modifierTypeOption.type.tier - modifierSelectHandler.options[2].modifierTypeOption.upgradeCount).toBe(ModifierTier.GREAT); + expect(modifierSelectHandler.options[3].modifierTypeOption.type.tier - modifierSelectHandler.options[3].modifierTypeOption.upgradeCount).toBe(ModifierTier.GREAT); + }); + }); + + describe("Option 3 - Brutal Battle", () => { + it("should have the correct properties", () => { + const option = MysteriousChallengersEncounter.options[2]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + selected: [ + { + text: `${namespace}.option.selected`, + }, + ], + }); + }); + + it("should start battle against the trainer", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS, defaultParty); + await runMysteryEncounterToEnd(game, 3, undefined, true); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(scene.currentBattle.trainer).toBeDefined(); + expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); + }); + + it("should have brutal trainer rewards after battle", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS, defaultParty); + await runMysteryEncounterToEnd(game, 3, undefined, true); + await skipBattleRunMysteryEncounterRewardsPhase(game); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(4); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.tier - modifierSelectHandler.options[0].modifierTypeOption.upgradeCount).toBe(ModifierTier.ROGUE); + expect(modifierSelectHandler.options[1].modifierTypeOption.type.tier - modifierSelectHandler.options[1].modifierTypeOption.upgradeCount).toBe(ModifierTier.ROGUE); + expect(modifierSelectHandler.options[2].modifierTypeOption.type.tier - modifierSelectHandler.options[2].modifierTypeOption.upgradeCount).toBe(ModifierTier.ULTRA); + expect(modifierSelectHandler.options[3].modifierTypeOption.type.tier - modifierSelectHandler.options[3].modifierTypeOption.upgradeCount).toBe(ModifierTier.GREAT); + }); + }); +}); diff --git a/src/test/mystery-encounter/encounters/part-timer-encounter.test.ts b/src/test/mystery-encounter/encounters/part-timer-encounter.test.ts new file mode 100644 index 00000000000..061b6a61461 --- /dev/null +++ b/src/test/mystery-encounter/encounters/part-timer-encounter.test.ts @@ -0,0 +1,279 @@ +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { runMysteryEncounterToEnd, runSelectMysteryEncounterOption } from "#test/mystery-encounter/encounter-test-utils"; +import BattleScene from "#app/battle-scene"; +import { CIVILIZATION_ENCOUNTER_BIOMES } from "#app/data/mystery-encounters/mystery-encounters"; +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 { Moves } from "#enums/moves"; +import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; + +const namespace = "mysteryEncounter:partTimer"; +// Pyukumuku for lowest speed, Regieleki for highest speed, Feebas for lowest "bulk", Melmetal for highest "bulk" +const defaultParty = [Species.PYUKUMUKU, Species.REGIELEKI, Species.FEEBAS, Species.MELMETAL]; +const defaultBiome = Biome.PLAINS; +const defaultWave = 37; + +describe("Part-Timer - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(); + + const biomeMap = new Map([ + [Biome.VOLCANO, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]], + ]); + CIVILIZATION_ENCOUNTER_BIOMES.forEach(biome => { + biomeMap.set(biome, [MysteryEncounterType.PART_TIMER]); + }); + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue(biomeMap); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.PART_TIMER, defaultParty); + + expect(PartTimerEncounter.encounterType).toBe(MysteryEncounterType.PART_TIMER); + expect(PartTimerEncounter.encounterTier).toBe(MysteryEncounterTier.COMMON); + expect(PartTimerEncounter.dialogue).toBeDefined(); + expect(PartTimerEncounter.dialogue.intro).toStrictEqual([ + { text: `${namespace}.intro` }, + { + speaker: `${namespace}.speaker`, + text: `${namespace}.intro_dialogue`, + } + ]); + expect(PartTimerEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`); + expect(PartTimerEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`); + expect(PartTimerEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`); + expect(PartTimerEncounter.options.length).toBe(3); + }); + + it("should not spawn outside of CIVILIZATION_ENCOUNTER_BIOMES", async () => { + game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON); + game.override.startingBiome(Biome.VOLCANO); + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.PART_TIMER); + }); + + describe("Option 1 - Make Deliveries", () => { + it("should have the correct properties", () => { + const option = PartTimerEncounter.options[0]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected` + } + ] + }); + }); + + it("should give the player 1x money multiplier money with max slowest Pokemon", async () => { + vi.spyOn(EncounterPhaseUtils, "updatePlayerMoney"); + + await game.runToMysteryEncounter(MysteryEncounterType.PART_TIMER, defaultParty); + // Override party levels to 50 so stats can be fully reflective + scene.getParty().forEach(p => { + p.level = 50; + p.calculateStats(); + }); + await runMysteryEncounterToEnd(game, 1, { pokemonNo: 1 }); + + expect(EncounterPhaseUtils.updatePlayerMoney).toHaveBeenCalledWith(scene, scene.getWaveMoneyAmount(1), true, false); + // Expect PP of mon's moves to have been reduced to 2 + const moves = scene.getParty()[0].moveset; + for (const move of moves) { + expect((move?.getMovePp() ?? 0) - (move?.ppUsed ?? 0)).toBe(2); + } + }); + + it("should give the player 4x money multiplier money with max fastest Pokemon", async () => { + vi.spyOn(EncounterPhaseUtils, "updatePlayerMoney"); + + await game.runToMysteryEncounter(MysteryEncounterType.PART_TIMER, defaultParty); + // Override party levels to 50 so stats can be fully reflective + scene.getParty().forEach(p => { + p.level = 50; + p.ivs = [20, 20, 20, 20, 20, 20]; + p.calculateStats(); + }); + await runMysteryEncounterToEnd(game, 1, { pokemonNo: 2 }); + + expect(EncounterPhaseUtils.updatePlayerMoney).toHaveBeenCalledWith(scene, scene.getWaveMoneyAmount(4), true, false); + // Expect PP of mon's moves to have been reduced to 2 + const moves = scene.getParty()[1].moveset; + for (const move of moves) { + expect((move?.getMovePp() ?? 0) - (move?.ppUsed ?? 0)).toBe(2); + } + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.PART_TIMER, defaultParty); + await runMysteryEncounterToEnd(game, 1, { pokemonNo: 1 }); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 2 - Help in the Warehouse", () => { + it("should have the correct properties", () => { + const option = PartTimerEncounter.options[1]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected` + } + ] + }); + }); + + it("should give the player 1x money multiplier money with least bulky Pokemon", async () => { + vi.spyOn(EncounterPhaseUtils, "updatePlayerMoney"); + + await game.runToMysteryEncounter(MysteryEncounterType.PART_TIMER, defaultParty); + // Override party levels to 50 so stats can be fully reflective + scene.getParty().forEach(p => { + p.level = 50; + p.calculateStats(); + }); + await runMysteryEncounterToEnd(game, 2, { pokemonNo: 3 }); + + expect(EncounterPhaseUtils.updatePlayerMoney).toHaveBeenCalledWith(scene, scene.getWaveMoneyAmount(1), true, false); + // Expect PP of mon's moves to have been reduced to 2 + const moves = scene.getParty()[2].moveset; + for (const move of moves) { + expect((move?.getMovePp() ?? 0) - (move?.ppUsed ?? 0)).toBe(2); + } + }); + + it("should give the player 4x money multiplier money with bulkiest Pokemon", async () => { + vi.spyOn(EncounterPhaseUtils, "updatePlayerMoney"); + + await game.runToMysteryEncounter(MysteryEncounterType.PART_TIMER, defaultParty); + // Override party levels to 50 so stats can be fully reflective + scene.getParty().forEach(p => { + p.level = 50; + p.ivs = [20, 20, 20, 20, 20, 20]; + p.calculateStats(); + }); + await runMysteryEncounterToEnd(game, 2, { pokemonNo: 4 }); + + expect(EncounterPhaseUtils.updatePlayerMoney).toHaveBeenCalledWith(scene, scene.getWaveMoneyAmount(4), true, false); + // Expect PP of mon's moves to have been reduced to 2 + const moves = scene.getParty()[3].moveset; + for (const move of moves) { + expect((move?.getMovePp() ?? 0) - (move?.ppUsed ?? 0)).toBe(2); + } + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.PART_TIMER, defaultParty); + await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1 }); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 3 - Assist with Sales", () => { + it("should have the correct properties", () => { + const option = PartTimerEncounter.options[2]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + disabledButtonTooltip: `${namespace}.option.3.disabled_tooltip`, + selected: [ + { + text: `${namespace}.option.3.selected` + } + ] + }); + }); + + it("Should NOT be selectable when requirements are not met", async () => { + vi.spyOn(EncounterPhaseUtils, "updatePlayerMoney"); + + await game.runToMysteryEncounter(MysteryEncounterType.PART_TIMER, defaultParty); + // Mock movesets + scene.getParty().forEach(p => p.moveset = []); + await game.phaseInterceptor.to(MysteryEncounterPhase, false); + + const encounterPhase = scene.getCurrentPhase(); + expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name); + const mysteryEncounterPhase = encounterPhase as MysteryEncounterPhase; + vi.spyOn(mysteryEncounterPhase, "continueEncounter"); + vi.spyOn(mysteryEncounterPhase, "handleOptionSelect"); + vi.spyOn(scene.ui, "playError"); + + await runSelectMysteryEncounterOption(game, 3); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterPhase.name); + expect(scene.ui.playError).not.toHaveBeenCalled(); // No error sfx, option is disabled + expect(mysteryEncounterPhase.handleOptionSelect).not.toHaveBeenCalled(); + expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled(); + expect(EncounterPhaseUtils.updatePlayerMoney).not.toHaveBeenCalled(); + }); + + it("should be selectable and give the player 2.5x money multiplier money with requirements met", async () => { + vi.spyOn(EncounterPhaseUtils, "updatePlayerMoney"); + + await game.runToMysteryEncounter(MysteryEncounterType.PART_TIMER, defaultParty); + // Mock moveset + scene.getParty()[0].moveset = [new PokemonMove(Moves.ATTRACT)]; + await runMysteryEncounterToEnd(game, 3); + + expect(EncounterPhaseUtils.updatePlayerMoney).toHaveBeenCalledWith(scene, scene.getWaveMoneyAmount(2.5), true, false); + // Expect PP of mon's moves to have been reduced to 2 + const moves = scene.getParty()[0].moveset; + for (const move of moves) { + expect((move?.getMovePp() ?? 0) - (move?.ppUsed ?? 0)).toBe(2); + } + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.PART_TIMER, defaultParty); + await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1 }); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); +}); diff --git a/src/test/mystery-encounter/encounters/teleporting-hijinks-encounter.test.ts b/src/test/mystery-encounter/encounters/teleporting-hijinks-encounter.test.ts new file mode 100644 index 00000000000..44a5197a39e --- /dev/null +++ b/src/test/mystery-encounter/encounters/teleporting-hijinks-encounter.test.ts @@ -0,0 +1,306 @@ +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { runMysteryEncounterToEnd, runSelectMysteryEncounterOption, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounter-test-utils"; +import BattleScene from "#app/battle-scene"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; +import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; +import { CommandPhase } from "#app/phases/command-phase"; +import { TeleportingHijinksEncounter } from "#app/data/mystery-encounters/encounters/teleporting-hijinks-encounter"; +import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; +import { Mode } from "#app/ui/ui"; +import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; +import { Abilities } from "#app/enums/abilities"; + +const namespace = "mysteryEncounter:teleportingHijinks"; +const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; +const defaultBiome = Biome.CAVE; +const defaultWave = 45; + +const TRANSPORT_BIOMES = [Biome.SPACE, Biome.ISLAND, Biome.LABORATORY, Biome.FAIRY_CAVE, Biome.WASTELAND, Biome.DOJO]; + +describe("Teleporting Hijinks - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + scene.money = 20000; + game.override + .mysteryEncounterChance(100) + .startingWave(defaultWave) + .startingBiome(defaultBiome) + .disableTrainerWaves() + .enemyPassiveAbility(Abilities.BALL_FETCH); + + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( + new Map([ + [Biome.CAVE, [MysteryEncounterType.TELEPORTING_HIJINKS]], + ]) + ); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.TELEPORTING_HIJINKS, defaultParty); + + expect(TeleportingHijinksEncounter.encounterType).toBe(MysteryEncounterType.TELEPORTING_HIJINKS); + expect(TeleportingHijinksEncounter.encounterTier).toBe(MysteryEncounterTier.COMMON); + expect(TeleportingHijinksEncounter.dialogue).toBeDefined(); + expect(TeleportingHijinksEncounter.dialogue.intro).toStrictEqual([{ text: `${namespace}.intro` }]); + expect(TeleportingHijinksEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`); + expect(TeleportingHijinksEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`); + expect(TeleportingHijinksEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`); + expect(TeleportingHijinksEncounter.options.length).toBe(3); + }); + + it("should run in waves that are X1", async () => { + game.override.startingWave(11); + game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).toBe(MysteryEncounterType.TELEPORTING_HIJINKS); + }); + + it("should run in waves that are X2", async () => { + game.override.startingWave(32); + game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).toBe(MysteryEncounterType.TELEPORTING_HIJINKS); + }); + + it("should run in waves that are X3", async () => { + game.override.startingWave(23); + game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).toBe(MysteryEncounterType.TELEPORTING_HIJINKS); + }); + + it("should NOT run in waves that are not X1, X2, or X3", async () => { + game.override.startingWave(54); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle.mysteryEncounter).not.toBe(MysteryEncounterType.TELEPORTING_HIJINKS); + }); + + it("should initialize fully", async () => { + initSceneWithoutEncounterPhase(scene, defaultParty); + scene.currentBattle.mysteryEncounter = TeleportingHijinksEncounter; + + const { onInit } = TeleportingHijinksEncounter; + + expect(TeleportingHijinksEncounter.onInit).toBeDefined(); + + TeleportingHijinksEncounter.populateDialogueTokensFromRequirements(scene); + const onInitResult = onInit!(scene); + + expect(TeleportingHijinksEncounter.misc.price).toBeDefined(); + expect(TeleportingHijinksEncounter.dialogueTokens.price).toBeDefined(); + expect(onInitResult).toBe(true); + }); + + describe("Option 1 - Pay Money", () => { + it("should have the correct properties", () => { + const option = TeleportingHijinksEncounter.options[0]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }); + }); + + it("should NOT be selectable if the player doesn't have enough money", async () => { + game.scene.money = 0; + await game.runToMysteryEncounter(MysteryEncounterType.TELEPORTING_HIJINKS, defaultParty); + await game.phaseInterceptor.to(MysteryEncounterPhase, false); + + const encounterPhase = scene.getCurrentPhase(); + expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name); + const mysteryEncounterPhase = encounterPhase as MysteryEncounterPhase; + vi.spyOn(mysteryEncounterPhase, "continueEncounter"); + vi.spyOn(mysteryEncounterPhase, "handleOptionSelect"); + vi.spyOn(scene.ui, "playError"); + + await runSelectMysteryEncounterOption(game, 1); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterPhase.name); + expect(scene.ui.playError).not.toHaveBeenCalled(); // No error sfx, option is disabled + expect(mysteryEncounterPhase.handleOptionSelect).not.toHaveBeenCalled(); + expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled(); + }); + + it("should be selectable if the player has enough money", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.TELEPORTING_HIJINKS, defaultParty); + await runMysteryEncounterToEnd(game, 1, undefined, true); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + }); + + it("should transport to a new area", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.TELEPORTING_HIJINKS, defaultParty); + + const previousBiome = scene.arena.biomeType; + + await runMysteryEncounterToEnd(game, 1, undefined, true); + + expect(previousBiome).not.toBe(scene.arena.biomeType); + expect(TRANSPORT_BIOMES).toContain(scene.arena.biomeType); + }); + + it("should start a battle against an enraged boss below wave 50", { retry: 5 }, async () => { + await game.runToMysteryEncounter(MysteryEncounterType.TELEPORTING_HIJINKS, defaultParty); + await runMysteryEncounterToEnd(game, 1, undefined, true); + const enemyField = scene.getEnemyField(); + expect(enemyField[0].summonData.statStages).toEqual([0, 1, 0, 1, 1, 0, 0]); + expect(enemyField[0].isBoss()).toBe(true); + }); + + it("should start a battle against an extra enraged boss above wave 50", { retry: 5 }, async () => { + game.override.startingWave(56); + await game.runToMysteryEncounter(MysteryEncounterType.TELEPORTING_HIJINKS, defaultParty); + await runMysteryEncounterToEnd(game, 1, undefined, true); + const enemyField = scene.getEnemyField(); + expect(enemyField[0].summonData.statStages).toEqual([1, 1, 1, 1, 1, 0, 0]); + expect(enemyField[0].isBoss()).toBe(true); + }); + }); + + describe("Option 2 - Use Electric/Steel Typing", () => { + it("should have the correct properties", () => { + const option = TeleportingHijinksEncounter.options[1]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + disabledButtonTooltip: `${namespace}.option.2.disabled_tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected`, + } + ], + }); + }); + + it("should NOT be selectable if the player doesn't the right type pokemon", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.TELEPORTING_HIJINKS, [Species.BLASTOISE]); + await game.phaseInterceptor.to(MysteryEncounterPhase, false); + + const encounterPhase = scene.getCurrentPhase(); + expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name); + const mysteryEncounterPhase = encounterPhase as MysteryEncounterPhase; + vi.spyOn(mysteryEncounterPhase, "continueEncounter"); + vi.spyOn(mysteryEncounterPhase, "handleOptionSelect"); + vi.spyOn(scene.ui, "playError"); + + await runSelectMysteryEncounterOption(game, 2); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterPhase.name); + expect(scene.ui.playError).not.toHaveBeenCalled(); // No error sfx, option is disabled + expect(mysteryEncounterPhase.handleOptionSelect).not.toHaveBeenCalled(); + expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled(); + }); + + it("should be selectable if the player has the right type pokemon", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.TELEPORTING_HIJINKS, [Species.METAGROSS]); + await runMysteryEncounterToEnd(game, 2, undefined, true); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + }); + + it("should transport to a new area", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.TELEPORTING_HIJINKS, [Species.PIKACHU]); + + const previousBiome = scene.arena.biomeType; + + await runMysteryEncounterToEnd(game, 2, undefined, true); + + expect(previousBiome).not.toBe(scene.arena.biomeType); + expect(TRANSPORT_BIOMES).toContain(scene.arena.biomeType); + }); + + it("should start a battle against an enraged boss below wave 50", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.TELEPORTING_HIJINKS, [Species.PIKACHU]); + await runMysteryEncounterToEnd(game, 2, undefined, true); + const enemyField = scene.getEnemyField(); + expect(enemyField[0].summonData.statStages).toEqual([0, 1, 0, 1, 1, 0, 0]); + expect(enemyField[0].isBoss()).toBe(true); + }); + + it("should start a battle against an extra enraged boss above wave 50", { retry: 5 }, async () => { + game.override.startingWave(56); + await game.runToMysteryEncounter(MysteryEncounterType.TELEPORTING_HIJINKS, defaultParty); + await runMysteryEncounterToEnd(game, 1, undefined, true); + const enemyField = scene.getEnemyField(); + expect(enemyField[0].summonData.statStages).toEqual([1, 1, 1, 1, 1, 0, 0]); + expect(enemyField[0].isBoss()).toBe(true); + }); + }); + + describe("Option 3 - Inspect the Machine", () => { + it("should have the correct properties", () => { + const option = TeleportingHijinksEncounter.options[2]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + selected: [ + { + text: `${namespace}.option.3.selected`, + }, + ], + }); + }); + + it("should start a battle against a boss", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.TELEPORTING_HIJINKS, defaultParty); + await runMysteryEncounterToEnd(game, 3, undefined, true); + const enemyField = scene.getEnemyField(); + expect(enemyField[0].summonData.statStages).toEqual([0, 0, 0, 0, 0, 0, 0]); + expect(enemyField[0].isBoss()).toBe(true); + }); + + it("should have Magnet and Metal Coat in rewards after battle", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.TELEPORTING_HIJINKS, defaultParty); + await runMysteryEncounterToEnd(game, 3, undefined, true); + await skipBattleRunMysteryEncounterRewardsPhase(game); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.some(opt => opt.modifierTypeOption.type.name === "Metal Coat")).toBe(true); + expect(modifierSelectHandler.options.some(opt => opt.modifierTypeOption.type.name === "Magnet")).toBe(true); + }); + }); +}); diff --git a/src/test/mystery-encounter/encounters/the-expert-breeder-encounter.test.ts b/src/test/mystery-encounter/encounters/the-expert-breeder-encounter.test.ts new file mode 100644 index 00000000000..59765148ead --- /dev/null +++ b/src/test/mystery-encounter/encounters/the-expert-breeder-encounter.test.ts @@ -0,0 +1,283 @@ +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { HUMAN_TRANSITABLE_BIOMES } from "#app/data/mystery-encounters/mystery-encounters"; +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { runMysteryEncounterToEnd, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounter-test-utils"; +import BattleScene from "#app/battle-scene"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; +import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; +import MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; +import { CommandPhase } from "#app/phases/command-phase"; +import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; +import { TheExpertPokemonBreederEncounter } from "#app/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter"; +import { TrainerType } from "#enums/trainer-type"; +import { EggTier } from "#enums/egg-type"; +import { PostMysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; + +const namespace = "mysteryEncounter:expertPokemonBreeder"; +const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; +const defaultBiome = Biome.CAVE; +const defaultWave = 45; + +describe("The Expert Pokémon Breeder - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(); + + const biomeMap = new Map([ + [Biome.VOLCANO, [MysteryEncounterType.FIGHT_OR_FLIGHT]], + ]); + HUMAN_TRANSITABLE_BIOMES.forEach(biome => { + biomeMap.set(biome, [MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER]); + }); + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue(biomeMap); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER, defaultParty); + + expect(TheExpertPokemonBreederEncounter.encounterType).toBe(MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER); + expect(TheExpertPokemonBreederEncounter.encounterTier).toBe(MysteryEncounterTier.ULTRA); + expect(TheExpertPokemonBreederEncounter.dialogue).toBeDefined(); + expect(TheExpertPokemonBreederEncounter.dialogue.intro).toStrictEqual([ + { + text: `${namespace}.intro` + }, + { + speaker: "trainerNames:expert_pokemon_breeder", + text: `${namespace}.intro_dialogue` + }, + ]); + expect(TheExpertPokemonBreederEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`); + expect(TheExpertPokemonBreederEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`); + expect(TheExpertPokemonBreederEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`); + expect(TheExpertPokemonBreederEncounter.options.length).toBe(3); + }); + + it("should not spawn outside of HUMAN_TRANSITABLE_BIOMES", async () => { + game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT); + game.override.startingBiome(Biome.VOLCANO); + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER); + }); + + it("should initialize fully", async () => { + initSceneWithoutEncounterPhase(scene, defaultParty); + scene.currentBattle.mysteryEncounter = new MysteryEncounter(TheExpertPokemonBreederEncounter); + const encounter = scene.currentBattle.mysteryEncounter!; + scene.currentBattle.waveIndex = defaultWave; + + const { onInit } = encounter; + + expect(encounter.onInit).toBeDefined(); + + encounter.populateDialogueTokensFromRequirements(scene); + const onInitResult = onInit!(scene); + + expect(encounter.enemyPartyConfigs).toBeDefined(); + expect(encounter.enemyPartyConfigs.length).toBe(1); + expect(encounter.enemyPartyConfigs[0].trainerType).toBe(TrainerType.EXPERT_POKEMON_BREEDER); + expect(encounter.enemyPartyConfigs[0].pokemonConfigs?.length).toBe(3); + expect(encounter.spriteConfigs).toBeDefined(); + expect(encounter.spriteConfigs.length).toBe(2); + expect(onInitResult).toBe(true); + }); + + describe("Option 1 - Battle with Pokemon 1", () => { + it("should have the correct properties", () => { + const option = TheExpertPokemonBreederEncounter.options[0]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: expect.any(String), // Varies based on pokemon + selected: [ + { + speaker: "trainerNames:expert_pokemon_breeder", + text: `${namespace}.option.selected`, + }, + ], + }); + }); + + it("should start battle against the trainer", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER, defaultParty); + await runMysteryEncounterToEnd(game, 1, undefined, true); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(scene.currentBattle.trainer).toBeDefined(); + expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); + expect(scene.getParty().length).toBe(1); + }); + + it("Should reward the player with friendship and eggs based on pokemon selected", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER, defaultParty); + + const friendshipBefore = scene.currentBattle.mysteryEncounter!.misc.pokemon1.friendship; + + scene.gameData.eggs = []; + const eggsBefore = scene.gameData.eggs; + expect(eggsBefore).toBeDefined(); + const eggsBeforeLength = eggsBefore.length; + + await runMysteryEncounterToEnd(game, 1, undefined, true); + await skipBattleRunMysteryEncounterRewardsPhase(game); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + + const eggsAfter = scene.gameData.eggs; + const commonEggs = scene.currentBattle.mysteryEncounter!.misc.pokemon1CommonEggs; + const rareEggs = scene.currentBattle.mysteryEncounter!.misc.pokemon1RareEggs; + expect(eggsAfter).toBeDefined(); + expect(eggsBeforeLength + commonEggs + rareEggs).toBe(eggsAfter.length); + expect(eggsAfter.filter(egg => egg.tier === EggTier.COMMON).length).toBe(commonEggs); + expect(eggsAfter.filter(egg => egg.tier === EggTier.GREAT).length).toBe(rareEggs); + + game.phaseInterceptor.superEndPhase(); + await game.phaseInterceptor.to(PostMysteryEncounterPhase); + + const friendshipAfter = scene.currentBattle.mysteryEncounter!.misc.pokemon1.friendship; + expect(friendshipAfter).toBe(friendshipBefore + 20 + 2); // +2 extra for friendship gained from winning battle + }); + }); + + describe("Option 2 - Battle with Pokemon 2", () => { + it("should have the correct properties", () => { + const option = TheExpertPokemonBreederEncounter.options[1]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: expect.any(String), // Varies based on pokemon + selected: [ + { + speaker: "trainerNames:expert_pokemon_breeder", + text: `${namespace}.option.selected`, + }, + ], + }); + }); + + it("should start battle against the trainer", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER, defaultParty); + await runMysteryEncounterToEnd(game, 2, undefined, true); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(scene.currentBattle.trainer).toBeDefined(); + expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); + expect(scene.getParty().length).toBe(1); + }); + + it("Should reward the player with friendship and eggs based on pokemon selected", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER, defaultParty); + + const friendshipBefore = scene.currentBattle.mysteryEncounter!.misc.pokemon2.friendship; + + scene.gameData.eggs = []; + const eggsBefore = scene.gameData.eggs; + expect(eggsBefore).toBeDefined(); + const eggsBeforeLength = eggsBefore.length; + + await runMysteryEncounterToEnd(game, 2, undefined, true); + await skipBattleRunMysteryEncounterRewardsPhase(game); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + + const eggsAfter = scene.gameData.eggs; + const commonEggs = scene.currentBattle.mysteryEncounter!.misc.pokemon2CommonEggs; + const rareEggs = scene.currentBattle.mysteryEncounter!.misc.pokemon2RareEggs; + expect(eggsAfter).toBeDefined(); + expect(eggsBeforeLength + commonEggs + rareEggs).toBe(eggsAfter.length); + expect(eggsAfter.filter(egg => egg.tier === EggTier.COMMON).length).toBe(commonEggs); + expect(eggsAfter.filter(egg => egg.tier === EggTier.GREAT).length).toBe(rareEggs); + + game.phaseInterceptor.superEndPhase(); + await game.phaseInterceptor.to(PostMysteryEncounterPhase); + + const friendshipAfter = scene.currentBattle.mysteryEncounter!.misc.pokemon2.friendship; + expect(friendshipAfter).toBe(friendshipBefore + 20 + 2); // +2 extra for friendship gained from winning battle + }); + }); + + describe("Option 3 - Battle with Pokemon 3", () => { + it("should have the correct properties", () => { + const option = TheExpertPokemonBreederEncounter.options[2]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: expect.any(String), // Varies based on pokemon + selected: [ + { + speaker: "trainerNames:expert_pokemon_breeder", + text: `${namespace}.option.selected`, + }, + ], + }); + }); + + it("should start battle against the trainer", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER, defaultParty); + await runMysteryEncounterToEnd(game, 3, undefined, true); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(scene.currentBattle.trainer).toBeDefined(); + expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); + expect(scene.getParty().length).toBe(1); + }); + + it("Should reward the player with friendship and eggs based on pokemon selected", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER, defaultParty); + + const friendshipBefore = scene.currentBattle.mysteryEncounter!.misc.pokemon3.friendship; + + scene.gameData.eggs = []; + const eggsBefore = scene.gameData.eggs; + expect(eggsBefore).toBeDefined(); + const eggsBeforeLength = eggsBefore.length; + + await runMysteryEncounterToEnd(game, 3, undefined, true); + await skipBattleRunMysteryEncounterRewardsPhase(game); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + + const eggsAfter = scene.gameData.eggs; + const commonEggs = scene.currentBattle.mysteryEncounter!.misc.pokemon3CommonEggs; + const rareEggs = scene.currentBattle.mysteryEncounter!.misc.pokemon3RareEggs; + expect(eggsAfter).toBeDefined(); + expect(eggsBeforeLength + commonEggs + rareEggs).toBe(eggsAfter.length); + expect(eggsAfter.filter(egg => egg.tier === EggTier.COMMON).length).toBe(commonEggs); + expect(eggsAfter.filter(egg => egg.tier === EggTier.GREAT).length).toBe(rareEggs); + + game.phaseInterceptor.superEndPhase(); + await game.phaseInterceptor.to(PostMysteryEncounterPhase); + + const friendshipAfter = scene.currentBattle.mysteryEncounter!.misc.pokemon3.friendship; + expect(friendshipAfter).toBe(friendshipBefore + 20 + 2); // +2 extra for friendship gained from winning battle + }); + }); +}); diff --git a/src/test/mystery-encounter/encounters/the-pokemon-salesman-encounter.test.ts b/src/test/mystery-encounter/encounters/the-pokemon-salesman-encounter.test.ts new file mode 100644 index 00000000000..e2b1fe8309b --- /dev/null +++ b/src/test/mystery-encounter/encounters/the-pokemon-salesman-encounter.test.ts @@ -0,0 +1,196 @@ +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { runMysteryEncounterToEnd, runSelectMysteryEncounterOption } from "#test/mystery-encounter/encounter-test-utils"; +import BattleScene from "#app/battle-scene"; +import { PlayerPokemon } from "#app/field/pokemon"; +import { HUMAN_TRANSITABLE_BIOMES } from "#app/data/mystery-encounters/mystery-encounters"; +import { ThePokemonSalesmanEncounter } from "#app/data/mystery-encounters/encounters/the-pokemon-salesman-encounter"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; +import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; + +const namespace = "mysteryEncounter:pokemonSalesman"; +const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; +const defaultBiome = Biome.CAVE; +const defaultWave = 45; + +describe("The Pokemon Salesman - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(); + + const biomeMap = new Map([ + [Biome.VOLCANO, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]], + ]); + HUMAN_TRANSITABLE_BIOMES.forEach(biome => { + biomeMap.set(biome, [MysteryEncounterType.THE_POKEMON_SALESMAN]); + }); + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue(biomeMap); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + const { encounterType, encounterTier, dialogue, options } = ThePokemonSalesmanEncounter; + + await game.runToMysteryEncounter(MysteryEncounterType.THE_POKEMON_SALESMAN, defaultParty); + + expect(encounterType).toBe(MysteryEncounterType.THE_POKEMON_SALESMAN); + expect(encounterTier).toBe(MysteryEncounterTier.ULTRA); + expect(dialogue).toBeDefined(); + expect(dialogue.intro).toStrictEqual([ + { text: `${namespace}.intro` }, + { speaker: `${namespace}.speaker`, text: `${namespace}.intro_dialogue` } + ]); + const { title, description, query } = dialogue.encounterOptionsDialogue!; + expect(title).toBe(`${namespace}.title`); + expect(description).toMatch(new RegExp(`^${namespace}\\.description(_shiny)?$`)); + expect(query).toBe(`${namespace}.query`); + expect(options.length).toBe(2); + }); + + it("should not spawn outside of HUMAN_TRANSITABLE_BIOMES", async () => { + game.override.mysteryEncounterTier(MysteryEncounterTier.ULTRA); + game.override.startingBiome(Biome.VOLCANO); + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.THE_POKEMON_SALESMAN); + }); + + it("should initialize fully ", async () => { + initSceneWithoutEncounterPhase(scene, defaultParty); + scene.currentBattle.mysteryEncounter = ThePokemonSalesmanEncounter; + + const { onInit } = ThePokemonSalesmanEncounter; + + expect(ThePokemonSalesmanEncounter.onInit).toBeDefined(); + + ThePokemonSalesmanEncounter.populateDialogueTokensFromRequirements(scene); + const onInitResult = onInit!(scene); + + expect(ThePokemonSalesmanEncounter.dialogueTokens?.purchasePokemon).toBeDefined(); + expect(ThePokemonSalesmanEncounter.dialogueTokens?.price).toBeDefined(); + expect(ThePokemonSalesmanEncounter.misc.pokemon instanceof PlayerPokemon).toBeTruthy(); + expect(ThePokemonSalesmanEncounter.misc?.price?.toString()).toBe(ThePokemonSalesmanEncounter.dialogueTokens?.price); + expect(onInitResult).toBe(true); + }); + + it("should not spawn if player does not have enough money", async () => { + scene.money = 0; + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.THE_POKEMON_SALESMAN); + }); + + describe("Option 1 - Purchase the pokemon", () => { + it("should have the correct properties", () => { + const { optionMode, dialogue } = ThePokemonSalesmanEncounter.options[0]; + + expect(optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT); + expect(dialogue).toBeDefined(); + expect(dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: expect.stringMatching(new RegExp(`^${namespace}\\.option\\.1\\.tooltip(_shiny)?$`)), + selected: [ + { + text: `${namespace}.option.1.selected_message`, + }, + ], + }); + }); + + it("Should update the player's money properly", async () => { + const initialMoney = 20000; + scene.money = initialMoney; + const updateMoneySpy = vi.spyOn(EncounterPhaseUtils, "updatePlayerMoney"); + + await game.runToMysteryEncounter(MysteryEncounterType.THE_POKEMON_SALESMAN, defaultParty); + await runMysteryEncounterToEnd(game, 1); + + const price = scene.currentBattle.mysteryEncounter!.misc.price; + + expect(updateMoneySpy).toHaveBeenCalledWith(scene, -price, true, false); + expect(scene.money).toBe(initialMoney - price); + }); + + it("Should add the Pokemon to the party", async () => { + scene.money = 20000; + await game.runToMysteryEncounter(MysteryEncounterType.THE_POKEMON_SALESMAN, defaultParty); + + const initialPartySize = scene.getParty().length; + const pokemonName = scene.currentBattle.mysteryEncounter!.misc.pokemon.name; + + await runMysteryEncounterToEnd(game, 1); + + expect(scene.getParty().length).toBe(initialPartySize + 1); + + const newlyPurchasedPokemon = scene.getParty().find(p => p.name === pokemonName); + expect(newlyPurchasedPokemon).toBeDefined(); + expect(newlyPurchasedPokemon!.moveset.length > 0).toBeTruthy(); + }); + + it("should be disabled if player does not have enough money", async () => { + scene.money = 0; + await game.runToMysteryEncounter(MysteryEncounterType.THE_POKEMON_SALESMAN, defaultParty); + await game.phaseInterceptor.to(MysteryEncounterPhase, false); + + const encounterPhase = scene.getCurrentPhase(); + expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name); + const mysteryEncounterPhase = encounterPhase as MysteryEncounterPhase; + vi.spyOn(mysteryEncounterPhase, "continueEncounter"); + vi.spyOn(mysteryEncounterPhase, "handleOptionSelect"); + vi.spyOn(scene.ui, "playError"); + + await runSelectMysteryEncounterOption(game, 1); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterPhase.name); + expect(scene.ui.playError).not.toHaveBeenCalled(); // No error sfx, option is disabled + expect(mysteryEncounterPhase.handleOptionSelect).not.toHaveBeenCalled(); + expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled(); + }); + + it("should leave encounter without battle", async () => { + scene.money = 20000; + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.THE_POKEMON_SALESMAN, defaultParty); + await runMysteryEncounterToEnd(game, 1); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 2 - Leave", () => { + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.THE_POKEMON_SALESMAN, defaultParty); + await runMysteryEncounterToEnd(game, 2); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); +}); diff --git a/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts b/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts new file mode 100644 index 00000000000..5c1353ee337 --- /dev/null +++ b/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts @@ -0,0 +1,224 @@ +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import * as BattleAnims from "#app/data/battle-anims"; +import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { runMysteryEncounterToEnd, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounter-test-utils"; +import { Moves } from "#enums/moves"; +import BattleScene from "#app/battle-scene"; +import { TheStrongStuffEncounter } from "#app/data/mystery-encounters/encounters/the-strong-stuff-encounter"; +import { Nature } from "#app/data/nature"; +import { BerryType } from "#enums/berry-type"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { PokemonMove } from "#app/field/pokemon"; +import { Mode } from "#app/ui/ui"; +import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; +import { BerryModifier, PokemonBaseStatTotalModifier } from "#app/modifier/modifier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; +import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data"; +import { CommandPhase } from "#app/phases/command-phase"; +import { MovePhase } from "#app/phases/move-phase"; +import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; + +const namespace = "mysteryEncounter:theStrongStuff"; +const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; +const defaultBiome = Biome.CAVE; +const defaultWave = 45; + +describe("The Strong Stuff - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(); + + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( + new Map([ + [Biome.CAVE, [MysteryEncounterType.THE_STRONG_STUFF]], + [Biome.MOUNTAIN, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]], + ]) + ); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.THE_STRONG_STUFF, defaultParty); + + expect(TheStrongStuffEncounter.encounterType).toBe(MysteryEncounterType.THE_STRONG_STUFF); + expect(TheStrongStuffEncounter.encounterTier).toBe(MysteryEncounterTier.GREAT); + expect(TheStrongStuffEncounter.dialogue).toBeDefined(); + expect(TheStrongStuffEncounter.dialogue.intro).toStrictEqual([{ text: `${namespace}.intro` }]); + expect(TheStrongStuffEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`); + expect(TheStrongStuffEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`); + expect(TheStrongStuffEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`); + expect(TheStrongStuffEncounter.options.length).toBe(2); + }); + + it("should not spawn outside of CAVE biome", async () => { + game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON); + game.override.startingBiome(Biome.MOUNTAIN); + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.THE_STRONG_STUFF); + }); + + it("should initialize fully ", async () => { + initSceneWithoutEncounterPhase(scene, defaultParty); + scene.currentBattle.mysteryEncounter = TheStrongStuffEncounter; + const moveInitSpy = vi.spyOn(BattleAnims, "initMoveAnim"); + const moveLoadSpy = vi.spyOn(BattleAnims, "loadMoveAnimAssets"); + + const { onInit } = TheStrongStuffEncounter; + + expect(TheStrongStuffEncounter.onInit).toBeDefined(); + + TheStrongStuffEncounter.populateDialogueTokensFromRequirements(scene); + const onInitResult = onInit!(scene); + + expect(TheStrongStuffEncounter.enemyPartyConfigs).toEqual([ + { + levelAdditiveModifier: 1, + disableSwitch: true, + pokemonConfigs: [ + { + species: getPokemonSpecies(Species.SHUCKLE), + isBoss: true, + bossSegments: 5, + mysteryEncounterPokemonData: new MysteryEncounterPokemonData({ spriteScale: 1.25 }), + nature: Nature.BOLD, + moveSet: [Moves.INFESTATION, Moves.SALT_CURE, Moves.GASTRO_ACID, Moves.HEAL_ORDER], + modifierConfigs: expect.any(Array), + tags: [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON], + mysteryEncounterBattleEffects: expect.any(Function) + } + ], + } + ]); + await vi.waitFor(() => expect(moveInitSpy).toHaveBeenCalled()); + await vi.waitFor(() => expect(moveLoadSpy).toHaveBeenCalled()); + expect(onInitResult).toBe(true); + }); + + describe("Option 1 - Power Swap BSTs", () => { + it("should have the correct properties", () => { + const option1 = TheStrongStuffEncounter.options[0]; + expect(option1.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option1.dialogue).toBeDefined(); + expect(option1.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }); + }); + + it("should lower stats of 2 highest BST and raise stats for rest of party", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.THE_STRONG_STUFF, defaultParty); + + const bstsPrior = scene.getParty().map(p => p.getSpeciesForm().getBaseStatTotal()); + await runMysteryEncounterToEnd(game, 1); + + const bstsAfter = scene.getParty().map(p => { + const baseStats = p.getSpeciesForm().baseStats.slice(0); + scene.applyModifiers(PokemonBaseStatTotalModifier, true, p, baseStats); + return baseStats.reduce((a, b) => a + b); + }); + + // HP stat changes are halved compared to other values + expect(bstsAfter[0]).toEqual(bstsPrior[0] - 15 * 5 - 8); + expect(bstsAfter[1]).toEqual(bstsPrior[1] - 15 * 5 - 8); + expect(bstsAfter[2]).toEqual(bstsPrior[2] + 10 * 5 + 5); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.THE_STRONG_STUFF, defaultParty); + await runMysteryEncounterToEnd(game, 1); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 2 - battle the Shuckle", () => { + it("should have the correct properties", () => { + const option1 = TheStrongStuffEncounter.options[1]; + expect(option1.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option1.dialogue).toBeDefined(); + expect(option1.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected`, + }, + ], + }); + }); + + it("should start battle against Shuckle", async () => { + const phaseSpy = vi.spyOn(scene, "pushPhase"); + + await game.runToMysteryEncounter(MysteryEncounterType.THE_STRONG_STUFF, defaultParty); + await runMysteryEncounterToEnd(game, 2, undefined, true); + + const enemyField = scene.getEnemyField(); + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(enemyField.length).toBe(1); + expect(enemyField[0].species.speciesId).toBe(Species.SHUCKLE); + expect(enemyField[0].summonData.statStages).toEqual([0, 2, 0, 2, 0, 0, 0]); + const shuckleItems = enemyField[0].getHeldItems(); + expect(shuckleItems.length).toBe(5); + expect(shuckleItems.find(m => m instanceof BerryModifier && m.berryType === BerryType.SITRUS)?.stackCount).toBe(1); + expect(shuckleItems.find(m => m instanceof BerryModifier && m.berryType === BerryType.ENIGMA)?.stackCount).toBe(1); + expect(shuckleItems.find(m => m instanceof BerryModifier && m.berryType === BerryType.GANLON)?.stackCount).toBe(1); + expect(shuckleItems.find(m => m instanceof BerryModifier && m.berryType === BerryType.APICOT)?.stackCount).toBe(1); + expect(shuckleItems.find(m => m instanceof BerryModifier && m.berryType === BerryType.LUM)?.stackCount).toBe(2); + expect(enemyField[0].moveset).toEqual([new PokemonMove(Moves.INFESTATION), new PokemonMove(Moves.SALT_CURE), new PokemonMove(Moves.GASTRO_ACID), new PokemonMove(Moves.HEAL_ORDER)]); + + // Should have used moves pre-battle + const movePhases = phaseSpy.mock.calls.filter(p => p[0] instanceof MovePhase).map(p => p[0]); + expect(movePhases.length).toBe(2); + expect(movePhases.filter(p => (p as MovePhase).move.moveId === Moves.GASTRO_ACID).length).toBe(1); + expect(movePhases.filter(p => (p as MovePhase).move.moveId === Moves.STEALTH_ROCK).length).toBe(1); + }); + + it("should have Soul Dew in rewards", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.THE_STRONG_STUFF, defaultParty); + await runMysteryEncounterToEnd(game, 2, undefined, true); + await skipBattleRunMysteryEncounterRewardsPhase(game); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(3); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toEqual("SOUL_DEW"); + }); + }); +}); diff --git a/src/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts b/src/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts new file mode 100644 index 00000000000..1efe6dbd7f8 --- /dev/null +++ b/src/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts @@ -0,0 +1,372 @@ +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { HUMAN_TRANSITABLE_BIOMES } from "#app/data/mystery-encounters/mystery-encounters"; +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { runMysteryEncounterToEnd } from "#test/mystery-encounter/encounter-test-utils"; +import BattleScene from "#app/battle-scene"; +import { Mode } from "#app/ui/ui"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; +import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; +import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; +import MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; +import { TrainerType } from "#enums/trainer-type"; +import { Nature } from "#enums/nature"; +import { Moves } from "#enums/moves"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { TheWinstrateChallengeEncounter } from "#app/data/mystery-encounters/encounters/the-winstrate-challenge-encounter"; +import { Status, StatusEffect } from "#app/data/status-effect"; +import { MysteryEncounterRewardsPhase } from "#app/phases/mystery-encounter-phases"; +import { CommandPhase } from "#app/phases/command-phase"; +import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; +import { PartyHealPhase } from "#app/phases/party-heal-phase"; +import { VictoryPhase } from "#app/phases/victory-phase"; + +const namespace = "mysteryEncounter:theWinstrateChallenge"; +const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; +const defaultBiome = Biome.CAVE; +const defaultWave = 45; + +describe("The Winstrate Challenge - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(); + + const biomeMap = new Map([ + [Biome.VOLCANO, [MysteryEncounterType.FIGHT_OR_FLIGHT]], + ]); + HUMAN_TRANSITABLE_BIOMES.forEach(biome => { + biomeMap.set(biome, [MysteryEncounterType.THE_WINSTRATE_CHALLENGE]); + }); + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue(biomeMap); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.THE_WINSTRATE_CHALLENGE, defaultParty); + + expect(TheWinstrateChallengeEncounter.encounterType).toBe(MysteryEncounterType.THE_WINSTRATE_CHALLENGE); + expect(TheWinstrateChallengeEncounter.encounterTier).toBe(MysteryEncounterTier.ROGUE); + expect(TheWinstrateChallengeEncounter.dialogue).toBeDefined(); + expect(TheWinstrateChallengeEncounter.dialogue.intro).toStrictEqual([ + { text: `${namespace}.intro` }, + { + speaker: `${namespace}.speaker`, + text: `${namespace}.intro_dialogue`, + } + ]); + expect(TheWinstrateChallengeEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`); + expect(TheWinstrateChallengeEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`); + expect(TheWinstrateChallengeEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`); + expect(TheWinstrateChallengeEncounter.options.length).toBe(2); + }); + + it("should not spawn outside of HUMAN_TRANSITABLE_BIOMES", async () => { + game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT); + game.override.startingBiome(Biome.VOLCANO); + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.THE_WINSTRATE_CHALLENGE); + }); + + it("should initialize fully", async () => { + initSceneWithoutEncounterPhase(scene, defaultParty); + scene.currentBattle.mysteryEncounter = new MysteryEncounter(TheWinstrateChallengeEncounter); + const encounter = scene.currentBattle.mysteryEncounter!; + scene.currentBattle.waveIndex = defaultWave; + + const { onInit } = encounter; + + expect(encounter.onInit).toBeDefined(); + + encounter.populateDialogueTokensFromRequirements(scene); + const onInitResult = onInit!(scene); + + expect(encounter.enemyPartyConfigs).toBeDefined(); + expect(encounter.enemyPartyConfigs.length).toBe(5); + expect(encounter.enemyPartyConfigs).toEqual([ + { + trainerType: TrainerType.VITO, + pokemonConfigs: [ + { + species: getPokemonSpecies(Species.HISUI_ELECTRODE), + isBoss: false, + abilityIndex: 0, // Soundproof + nature: Nature.MODEST, + moveSet: [Moves.THUNDERBOLT, Moves.GIGA_DRAIN, Moves.FOUL_PLAY, Moves.THUNDER_WAVE], + modifierConfigs: expect.any(Array) + }, + { + species: getPokemonSpecies(Species.SWALOT), + isBoss: false, + abilityIndex: 2, // Gluttony + nature: Nature.QUIET, + moveSet: [Moves.SLUDGE_BOMB, Moves.GIGA_DRAIN, Moves.ICE_BEAM, Moves.EARTHQUAKE], + modifierConfigs: expect.any(Array) + }, + { + species: getPokemonSpecies(Species.DODRIO), + isBoss: false, + abilityIndex: 2, // Tangled Feet + nature: Nature.JOLLY, + moveSet: [Moves.DRILL_PECK, Moves.QUICK_ATTACK, Moves.THRASH, Moves.KNOCK_OFF], + modifierConfigs: expect.any(Array) + }, + { + species: getPokemonSpecies(Species.ALAKAZAM), + isBoss: false, + formIndex: 1, + nature: Nature.BOLD, + moveSet: [Moves.PSYCHIC, Moves.SHADOW_BALL, Moves.FOCUS_BLAST, Moves.THUNDERBOLT], + modifierConfigs: expect.any(Array) + }, + { + species: getPokemonSpecies(Species.DARMANITAN), + isBoss: false, + abilityIndex: 0, // Sheer Force + nature: Nature.IMPISH, + moveSet: [Moves.EARTHQUAKE, Moves.U_TURN, Moves.FLARE_BLITZ, Moves.ROCK_SLIDE], + modifierConfigs: expect.any(Array) + } + ] + }, + { + trainerType: TrainerType.VICKY, + pokemonConfigs: [ + { + species: getPokemonSpecies(Species.MEDICHAM), + isBoss: false, + formIndex: 1, + nature: Nature.IMPISH, + moveSet: [Moves.AXE_KICK, Moves.ICE_PUNCH, Moves.ZEN_HEADBUTT, Moves.BULLET_PUNCH], + modifierConfigs: expect.any(Array) + } + ] + }, + { + trainerType: TrainerType.VIVI, + pokemonConfigs: [ + { + species: getPokemonSpecies(Species.SEAKING), + isBoss: false, + abilityIndex: 3, // Lightning Rod + nature: Nature.ADAMANT, + moveSet: [Moves.WATERFALL, Moves.MEGAHORN, Moves.KNOCK_OFF, Moves.REST], + modifierConfigs: expect.any(Array) + }, + { + species: getPokemonSpecies(Species.BRELOOM), + isBoss: false, + abilityIndex: 1, // Poison Heal + nature: Nature.JOLLY, + moveSet: [Moves.SPORE, Moves.SWORDS_DANCE, Moves.SEED_BOMB, Moves.DRAIN_PUNCH], + modifierConfigs: expect.any(Array) + }, + { + species: getPokemonSpecies(Species.CAMERUPT), + isBoss: false, + formIndex: 1, + nature: Nature.CALM, + moveSet: [Moves.EARTH_POWER, Moves.FIRE_BLAST, Moves.YAWN, Moves.PROTECT], + modifierConfigs: expect.any(Array) + } + ] + }, + { + trainerType: TrainerType.VICTORIA, + pokemonConfigs: [ + { + species: getPokemonSpecies(Species.ROSERADE), + isBoss: false, + abilityIndex: 0, // Natural Cure + nature: Nature.CALM, + moveSet: [Moves.SYNTHESIS, Moves.SLUDGE_BOMB, Moves.GIGA_DRAIN, Moves.SLEEP_POWDER], + modifierConfigs: expect.any(Array) + }, + { + species: getPokemonSpecies(Species.GARDEVOIR), + isBoss: false, + formIndex: 1, + nature: Nature.TIMID, + moveSet: [Moves.PSYSHOCK, Moves.MOONBLAST, Moves.SHADOW_BALL, Moves.WILL_O_WISP], + modifierConfigs: expect.any(Array) + } + ] + }, + { + trainerType: TrainerType.VICTOR, + pokemonConfigs: [ + { + species: getPokemonSpecies(Species.SWELLOW), + isBoss: false, + abilityIndex: 0, // Guts + nature: Nature.ADAMANT, + moveSet: [Moves.FACADE, Moves.BRAVE_BIRD, Moves.PROTECT, Moves.QUICK_ATTACK], + modifierConfigs: expect.any(Array) + }, + { + species: getPokemonSpecies(Species.OBSTAGOON), + isBoss: false, + abilityIndex: 1, // Guts + nature: Nature.ADAMANT, + moveSet: [Moves.FACADE, Moves.OBSTRUCT, Moves.NIGHT_SLASH, Moves.FIRE_PUNCH], + modifierConfigs: expect.any(Array) + } + ] + } + ]); + expect(encounter.spriteConfigs).toBeDefined(); + expect(encounter.spriteConfigs.length).toBe(5); + expect(onInitResult).toBe(true); + }); + + describe("Option 1 - Normal Battle", () => { + it("should have the correct properties", () => { + const option = TheWinstrateChallengeEncounter.options[0]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + speaker: `${namespace}.speaker`, + text: `${namespace}.option.1.selected`, + }, + ], + }); + }); + + it("should battle all 5 trainers for a Macho Brace reward", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.THE_WINSTRATE_CHALLENGE, defaultParty); + await runMysteryEncounterToEnd(game, 1, undefined, true); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(scene.currentBattle.trainer).toBeDefined(); + expect(scene.currentBattle.trainer!.config.trainerType).toBe(TrainerType.VICTOR); + expect(scene.currentBattle.mysteryEncounter?.enemyPartyConfigs.length).toBe(4); + expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); + + await skipBattleToNextBattle(game); + expect(scene.currentBattle.trainer).toBeDefined(); + expect(scene.currentBattle.trainer!.config.trainerType).toBe(TrainerType.VICTORIA); + expect(scene.currentBattle.mysteryEncounter?.enemyPartyConfigs.length).toBe(3); + expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); + + await skipBattleToNextBattle(game); + expect(scene.currentBattle.trainer).toBeDefined(); + expect(scene.currentBattle.trainer!.config.trainerType).toBe(TrainerType.VIVI); + expect(scene.currentBattle.mysteryEncounter?.enemyPartyConfigs.length).toBe(2); + expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); + + await skipBattleToNextBattle(game); + expect(scene.currentBattle.trainer).toBeDefined(); + expect(scene.currentBattle.trainer!.config.trainerType).toBe(TrainerType.VICKY); + expect(scene.currentBattle.mysteryEncounter?.enemyPartyConfigs.length).toBe(1); + expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); + + await skipBattleToNextBattle(game); + expect(scene.currentBattle.trainer).toBeDefined(); + expect(scene.currentBattle.trainer!.config.trainerType).toBe(TrainerType.VITO); + expect(scene.currentBattle.mysteryEncounter?.enemyPartyConfigs.length).toBe(0); + expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); + + // Should have Macho Brace in the rewards + await skipBattleToNextBattle(game, true); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) 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", () => { + it("should have the correct properties", () => { + const option = TheWinstrateChallengeEncounter.options[1]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + speaker: `${namespace}.speaker`, + text: `${namespace}.option.2.selected`, + }, + ], + }); + }); + + it("Should fully heal the party", async () => { + const phaseSpy = vi.spyOn(scene, "unshiftPhase"); + + await game.runToMysteryEncounter(MysteryEncounterType.THE_WINSTRATE_CHALLENGE, defaultParty); + await runMysteryEncounterToEnd(game, 2); + + const partyHealPhases = phaseSpy.mock.calls.filter(p => p[0] instanceof PartyHealPhase).map(p => p[0]); + expect(partyHealPhases.length).toBe(1); + }); + + it("should have a Rarer Candy in the rewards", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.THE_WINSTRATE_CHALLENGE, defaultParty); + await runMysteryEncounterToEnd(game, 2); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(1); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toBe("RARER_CANDY"); + }); + }); +}); + +/** + * For any {@linkcode MysteryEncounter} that has a battle, can call this to skip battle and proceed to MysteryEncounterRewardsPhase + * @param game + * @param isFinalBattle + */ +async function skipBattleToNextBattle(game: GameManager, isFinalBattle: boolean = false) { + game.scene.clearPhaseQueue(); + game.scene.clearPhaseQueueSplice(); + const commandUiHandler = game.scene.ui.handlers[Mode.COMMAND]; + commandUiHandler.clear(); + game.scene.getEnemyParty().forEach(p => { + p.hp = 0; + p.status = new Status(StatusEffect.FAINT); + game.scene.field.remove(p); + }); + game.phaseInterceptor["onHold"] = []; + game.scene.pushPhase(new VictoryPhase(game.scene, 0)); + game.phaseInterceptor.superEndPhase(); + if (isFinalBattle) { + await game.phaseInterceptor.to(MysteryEncounterRewardsPhase); + } else { + await game.phaseInterceptor.to(CommandPhase); + } +} diff --git a/src/test/mystery-encounter/encounters/trash-to-treasure-encounter.test.ts b/src/test/mystery-encounter/encounters/trash-to-treasure-encounter.test.ts new file mode 100644 index 00000000000..bfeb249543f --- /dev/null +++ b/src/test/mystery-encounter/encounters/trash-to-treasure-encounter.test.ts @@ -0,0 +1,204 @@ +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import * as BattleAnims from "#app/data/battle-anims"; +import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { runMysteryEncounterToEnd, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounter-test-utils"; +import { Moves } from "#enums/moves"; +import BattleScene from "#app/battle-scene"; +import { PokemonMove } from "#app/field/pokemon"; +import { Mode } from "#app/ui/ui"; +import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; +import { HitHealModifier, HealShopCostModifier, TurnHealModifier } from "#app/modifier/modifier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; +import { TrashToTreasureEncounter } from "#app/data/mystery-encounters/encounters/trash-to-treasure-encounter"; +import { ModifierTier } from "#app/modifier/modifier-tier"; +import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; +import { CommandPhase } from "#app/phases/command-phase"; +import { MovePhase } from "#app/phases/move-phase"; + +const namespace = "mysteryEncounter:trashToTreasure"; +const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; +const defaultBiome = Biome.CAVE; +const defaultWave = 45; + +describe("Trash to Treasure - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(); + + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( + new Map([ + [Biome.CAVE, [MysteryEncounterType.TRASH_TO_TREASURE]], + ]) + ); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.TRASH_TO_TREASURE, defaultParty); + + expect(TrashToTreasureEncounter.encounterType).toBe(MysteryEncounterType.TRASH_TO_TREASURE); + expect(TrashToTreasureEncounter.encounterTier).toBe(MysteryEncounterTier.ULTRA); + expect(TrashToTreasureEncounter.dialogue).toBeDefined(); + expect(TrashToTreasureEncounter.dialogue.intro).toStrictEqual([{ text: `${namespace}.intro` }]); + expect(TrashToTreasureEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`); + expect(TrashToTreasureEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`); + expect(TrashToTreasureEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`); + expect(TrashToTreasureEncounter.options.length).toBe(2); + }); + + it("should initialize fully", async () => { + initSceneWithoutEncounterPhase(scene, defaultParty); + scene.currentBattle.mysteryEncounter = TrashToTreasureEncounter; + const moveInitSpy = vi.spyOn(BattleAnims, "initMoveAnim"); + const moveLoadSpy = vi.spyOn(BattleAnims, "loadMoveAnimAssets"); + + const { onInit } = TrashToTreasureEncounter; + + expect(TrashToTreasureEncounter.onInit).toBeDefined(); + + TrashToTreasureEncounter.populateDialogueTokensFromRequirements(scene); + const onInitResult = onInit!(scene); + + expect(TrashToTreasureEncounter.enemyPartyConfigs).toEqual([ + { + levelAdditiveModifier: 1, + disableSwitch: true, + pokemonConfigs: [ + { + species: getPokemonSpecies(Species.GARBODOR), + isBoss: true, + formIndex: 1, + bossSegmentModifier: 1, + moveSet: [Moves.PAYBACK, Moves.GUNK_SHOT, Moves.STOMPING_TANTRUM, Moves.DRAIN_PUNCH], + } + ], + } + ]); + await vi.waitFor(() => expect(moveInitSpy).toHaveBeenCalled()); + await vi.waitFor(() => expect(moveLoadSpy).toHaveBeenCalled()); + expect(onInitResult).toBe(true); + }); + + describe("Option 1 - Dig for Valuables", () => { + it("should have the correct properties", () => { + const option1 = TrashToTreasureEncounter.options[0]; + expect(option1.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option1.dialogue).toBeDefined(); + expect(option1.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }); + }); + + it("should give 2 Leftovers, 2 Shell Bell, and Black Sludge", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.TRASH_TO_TREASURE, defaultParty); + await runMysteryEncounterToEnd(game, 1); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + + const leftovers = scene.findModifier(m => m instanceof TurnHealModifier) as TurnHealModifier; + expect(leftovers).toBeDefined(); + expect(leftovers?.stackCount).toBe(2); + + const shellBell = scene.findModifier(m => m instanceof HitHealModifier) as HitHealModifier; + expect(shellBell).toBeDefined(); + expect(shellBell?.stackCount).toBe(2); + + const blackSludge = scene.findModifier(m => m instanceof HealShopCostModifier) as HealShopCostModifier; + expect(blackSludge).toBeDefined(); + expect(blackSludge?.stackCount).toBe(1); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.TRASH_TO_TREASURE, defaultParty); + await runMysteryEncounterToEnd(game, 1); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 2 - Battle Garbodor", () => { + it("should have the correct properties", () => { + const option1 = TrashToTreasureEncounter.options[1]; + expect(option1.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option1.dialogue).toBeDefined(); + expect(option1.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected`, + }, + ], + }); + }); + + it("should start battle against Garbodor", async () => { + const phaseSpy = vi.spyOn(scene, "pushPhase"); + + await game.runToMysteryEncounter(MysteryEncounterType.TRASH_TO_TREASURE, defaultParty); + await runMysteryEncounterToEnd(game, 2, undefined, true); + + const enemyField = scene.getEnemyField(); + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(enemyField.length).toBe(1); + expect(enemyField[0].species.speciesId).toBe(Species.GARBODOR); + expect(enemyField[0].moveset).toEqual([new PokemonMove(Moves.PAYBACK), new PokemonMove(Moves.GUNK_SHOT), new PokemonMove(Moves.STOMPING_TANTRUM), new PokemonMove(Moves.DRAIN_PUNCH)]); + + // Should have used moves pre-battle + const movePhases = phaseSpy.mock.calls.filter(p => p[0] instanceof MovePhase).map(p => p[0]); + expect(movePhases.length).toBe(2); + expect(movePhases.filter(p => (p as MovePhase).move.moveId === Moves.TOXIC).length).toBe(1); + expect(movePhases.filter(p => (p as MovePhase).move.moveId === Moves.AMNESIA).length).toBe(1); + }); + + it("should have 2 Rogue, 1 Ultra, 1 Great in rewards", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.TRASH_TO_TREASURE, defaultParty); + await runMysteryEncounterToEnd(game, 2, undefined, true); + await skipBattleRunMysteryEncounterRewardsPhase(game); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(4); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.tier - modifierSelectHandler.options[0].modifierTypeOption.upgradeCount).toEqual(ModifierTier.ROGUE); + expect(modifierSelectHandler.options[1].modifierTypeOption.type.tier - modifierSelectHandler.options[1].modifierTypeOption.upgradeCount).toEqual(ModifierTier.ROGUE); + expect(modifierSelectHandler.options[2].modifierTypeOption.type.tier - modifierSelectHandler.options[2].modifierTypeOption.upgradeCount).toEqual(ModifierTier.ULTRA); + expect(modifierSelectHandler.options[3].modifierTypeOption.type.tier - modifierSelectHandler.options[3].modifierTypeOption.upgradeCount).toEqual(ModifierTier.GREAT); + }); + }); +}); diff --git a/src/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts b/src/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts new file mode 100644 index 00000000000..2f8c4e5111a --- /dev/null +++ b/src/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts @@ -0,0 +1,249 @@ +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { runMysteryEncounterToEnd, runSelectMysteryEncounterOption } from "#test/mystery-encounter/encounter-test-utils"; +import { Moves } from "#enums/moves"; +import BattleScene from "#app/battle-scene"; +import { PokemonMove } from "#app/field/pokemon"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; +import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { generateModifierType } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; +import { CommandPhase } from "#app/phases/command-phase"; +import { UncommonBreedEncounter } from "#app/data/mystery-encounters/encounters/uncommon-breed-encounter"; +import { MovePhase } from "#app/phases/move-phase"; +import { speciesEggMoves } from "#app/data/egg-moves"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { BerryType } from "#enums/berry-type"; +import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; +import { Stat } from "#enums/stat"; +import { BerryModifier } from "#app/modifier/modifier"; +import { modifierTypes } from "#app/modifier/modifier-type"; + +const namespace = "mysteryEncounter:uncommonBreed"; +const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; +const defaultBiome = Biome.CAVE; +const defaultWave = 45; + +describe("Uncommon Breed - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(); + + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( + new Map([ + [Biome.CAVE, [MysteryEncounterType.UNCOMMON_BREED]], + ]) + ); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.UNCOMMON_BREED, defaultParty); + + expect(UncommonBreedEncounter.encounterType).toBe(MysteryEncounterType.UNCOMMON_BREED); + expect(UncommonBreedEncounter.encounterTier).toBe(MysteryEncounterTier.COMMON); + expect(UncommonBreedEncounter.dialogue).toBeDefined(); + expect(UncommonBreedEncounter.dialogue.intro).toStrictEqual([{ text: `${namespace}.intro` }]); + expect(UncommonBreedEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`); + expect(UncommonBreedEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`); + expect(UncommonBreedEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`); + expect(UncommonBreedEncounter.options.length).toBe(3); + }); + + it("should initialize fully", async () => { + initSceneWithoutEncounterPhase(scene, defaultParty); + scene.currentBattle.mysteryEncounter = UncommonBreedEncounter; + + const { onInit } = UncommonBreedEncounter; + + expect(UncommonBreedEncounter.onInit).toBeDefined(); + + UncommonBreedEncounter.populateDialogueTokensFromRequirements(scene); + const onInitResult = onInit!(scene); + + const config = UncommonBreedEncounter.enemyPartyConfigs[0]; + expect(config).toBeDefined(); + expect(config.pokemonConfigs?.[0].isBoss).toBe(false); + expect(onInitResult).toBe(true); + }); + + describe("Option 1 - Fight", () => { + it("should have the correct properties", () => { + const option = UncommonBreedEncounter.options[0]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }); + }); + + it.skip("should start a fight against the boss", async () => { + const phaseSpy = vi.spyOn(scene, "pushPhase"); + const unshiftPhaseSpy = vi.spyOn(scene, "unshiftPhase"); + await game.runToMysteryEncounter(MysteryEncounterType.UNCOMMON_BREED, defaultParty); + + const config = game.scene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0]; + const speciesToSpawn = config.pokemonConfigs?.[0].species.speciesId; + + await runMysteryEncounterToEnd(game, 1, undefined, true); + + const enemyField = scene.getEnemyField(); + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(enemyField.length).toBe(1); + expect(enemyField[0].species.speciesId).toBe(speciesToSpawn); + + const statStagePhases = unshiftPhaseSpy.mock.calls.filter(p => p[0] instanceof StatStageChangePhase)[0][0] as any; + expect(statStagePhases.stats).toEqual([Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD]); + + // Should have used its egg move pre-battle + const movePhases = phaseSpy.mock.calls.filter(p => p[0] instanceof MovePhase).map(p => p[0]); + expect(movePhases.length).toBe(1); + const eggMoves: Moves[] = speciesEggMoves[getPokemonSpecies(speciesToSpawn).getRootSpeciesId()]; + const usedMove = (movePhases[0] as MovePhase).move.moveId; + expect(eggMoves.includes(usedMove)).toBe(true); + }); + }); + + describe("Option 2 - Give it Food", () => { + it("should have the correct properties", () => { + const option = UncommonBreedEncounter.options[1]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + disabledButtonTooltip: `${namespace}.option.2.disabled_tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected`, + } + ], + }); + }); + + // TODO: there is some severe test flakiness occurring for this file, needs to be looked at/addressed in separate issue + it.skip("should NOT be selectable if the player doesn't have enough berries", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.UNCOMMON_BREED, defaultParty); + // Clear out any pesky mods that slipped through test spin-up + scene.modifiers.forEach(mod => { + scene.removeModifier(mod); + }); + await scene.updateModifiers(true); + await game.phaseInterceptor.to(MysteryEncounterPhase, false); + + const encounterPhase = scene.getCurrentPhase(); + expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name); + const mysteryEncounterPhase = encounterPhase as MysteryEncounterPhase; + vi.spyOn(mysteryEncounterPhase, "continueEncounter"); + vi.spyOn(mysteryEncounterPhase, "handleOptionSelect"); + vi.spyOn(scene.ui, "playError"); + + await runSelectMysteryEncounterOption(game, 2); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterPhase.name); + expect(scene.ui.playError).not.toHaveBeenCalled(); // No error sfx, option is disabled + expect(mysteryEncounterPhase.handleOptionSelect).not.toHaveBeenCalled(); + expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled(); + }); + + // TODO: there is some severe test flakiness occurring for this file, needs to be looked at/addressed in separate issue + it.skip("Should skip fight when player meets requirements", { retry: 5 }, async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.UNCOMMON_BREED, defaultParty); + + // Berries on party lead + const sitrus = generateModifierType(scene, modifierTypes.BERRY, [BerryType.SITRUS])!; + const sitrusMod = sitrus.newModifier(scene.getParty()[0]) as BerryModifier; + sitrusMod.stackCount = 2; + await scene.addModifier(sitrusMod, true, false, false, true); + const ganlon = generateModifierType(scene, modifierTypes.BERRY, [BerryType.GANLON])!; + const ganlonMod = ganlon.newModifier(scene.getParty()[0]) as BerryModifier; + ganlonMod.stackCount = 3; + await scene.addModifier(ganlonMod, true, false, false, true); + await scene.updateModifiers(true); + + await runMysteryEncounterToEnd(game, 2); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 3 - Use an Attracting Move", () => { + it("should have the correct properties", () => { + const option = UncommonBreedEncounter.options[2]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + disabledButtonTooltip: `${namespace}.option.3.disabled_tooltip`, + selected: [ + { + text: `${namespace}.option.3.selected`, + } + ], + }); + }); + + it("should NOT be selectable if the player doesn't have an Attracting move", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.UNCOMMON_BREED, defaultParty); + scene.getParty().forEach(p => p.moveset = []); + await game.phaseInterceptor.to(MysteryEncounterPhase, false); + + const encounterPhase = scene.getCurrentPhase(); + expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name); + const mysteryEncounterPhase = encounterPhase as MysteryEncounterPhase; + vi.spyOn(mysteryEncounterPhase, "continueEncounter"); + vi.spyOn(mysteryEncounterPhase, "handleOptionSelect"); + vi.spyOn(scene.ui, "playError"); + + await runSelectMysteryEncounterOption(game, 3); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterPhase.name); + expect(scene.ui.playError).not.toHaveBeenCalled(); // No error sfx, option is disabled + expect(mysteryEncounterPhase.handleOptionSelect).not.toHaveBeenCalled(); + expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled(); + }); + + it("Should skip fight when player meets requirements", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + await game.runToMysteryEncounter(MysteryEncounterType.UNCOMMON_BREED, defaultParty); + // Mock moveset + scene.getParty()[0].moveset = [new PokemonMove(Moves.CHARM)]; + await runMysteryEncounterToEnd(game, 3); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); +}); diff --git a/src/test/mystery-encounter/encounters/weird-dream-encounter.test.ts b/src/test/mystery-encounter/encounters/weird-dream-encounter.test.ts new file mode 100644 index 00000000000..d858d631596 --- /dev/null +++ b/src/test/mystery-encounter/encounters/weird-dream-encounter.test.ts @@ -0,0 +1,203 @@ +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { runMysteryEncounterToEnd } from "#test/mystery-encounter/encounter-test-utils"; +import BattleScene from "#app/battle-scene"; +import { Mode } from "#app/ui/ui"; +import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; +import { WeirdDreamEncounter } from "#app/data/mystery-encounters/encounters/weird-dream-encounter"; +import * as EncounterTransformationSequence from "#app/data/mystery-encounters/utils/encounter-transformation-sequence"; +import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; + +const namespace = "mysteryEncounter:weirdDream"; +const defaultParty = [Species.MAGBY, Species.HAUNTER, Species.ABRA]; +const defaultBiome = Biome.CAVE; +const defaultWave = 45; + +describe("Weird Dream - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(); + vi.spyOn(EncounterTransformationSequence, "doPokemonTransformationSequence").mockImplementation(() => new Promise(resolve => resolve())); + + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( + new Map([ + [Biome.CAVE, [MysteryEncounterType.WEIRD_DREAM]], + ]) + ); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty); + + expect(WeirdDreamEncounter.encounterType).toBe(MysteryEncounterType.WEIRD_DREAM); + expect(WeirdDreamEncounter.encounterTier).toBe(MysteryEncounterTier.ROGUE); + expect(WeirdDreamEncounter.dialogue).toBeDefined(); + expect(WeirdDreamEncounter.dialogue.intro).toStrictEqual([ + { + text: `${namespace}.intro` + }, + { + speaker: `${namespace}.speaker`, + text: `${namespace}.intro_dialogue`, + }, + ]); + expect(WeirdDreamEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`); + expect(WeirdDreamEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`); + expect(WeirdDreamEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`); + expect(WeirdDreamEncounter.options.length).toBe(2); + }); + + it("should initialize fully", async () => { + initSceneWithoutEncounterPhase(scene, defaultParty); + scene.currentBattle.mysteryEncounter = WeirdDreamEncounter; + const loadBgmSpy = vi.spyOn(scene, "loadBgm"); + + const { onInit } = WeirdDreamEncounter; + + expect(WeirdDreamEncounter.onInit).toBeDefined(); + + WeirdDreamEncounter.populateDialogueTokensFromRequirements(scene); + const onInitResult = onInit!(scene); + + expect(loadBgmSpy).toHaveBeenCalled(); + expect(onInitResult).toBe(true); + }); + + describe("Option 1 - Accept Transformation", () => { + it("should have the correct properties", () => { + const option = WeirdDreamEncounter.options[0]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }); + }); + + it("should transform the new party into new species, 2 at +90/+110, the rest at +40/50 BST", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty); + + const pokemonPrior = scene.getParty().map(pokemon => pokemon); + const bstsPrior = pokemonPrior.map(species => species.getSpeciesForm().getBaseStatTotal()); + + await runMysteryEncounterToEnd(game, 1); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + + const pokemonAfter = scene.getParty(); + const bstsAfter = pokemonAfter.map(pokemon => pokemon.getSpeciesForm().getBaseStatTotal()); + const bstDiff = bstsAfter.map((bst, index) => bst - bstsPrior[index]); + + for (let i = 0; i < pokemonAfter.length; i++) { + const newPokemon = pokemonAfter[i]; + expect(newPokemon.getSpeciesForm().speciesId).not.toBe(pokemonPrior[i].getSpeciesForm().speciesId); + expect(newPokemon.mysteryEncounterPokemonData?.types.length).toBe(2); + } + + const plus90To110 = bstDiff.filter(bst => bst > 80); + const plus40To50 = bstDiff.filter(bst => bst < 80); + + expect(plus90To110.length).toBe(2); + expect(plus40To50.length).toBe(1); + }); + + it("should have 1 Memory Mushroom, 5 Rogue Balls, and 2 Mints in rewards", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty); + await runMysteryEncounterToEnd(game, 1); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(4); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toEqual("MEMORY_MUSHROOM"); + expect(modifierSelectHandler.options[1].modifierTypeOption.type.id).toEqual("ROGUE_BALL"); + expect(modifierSelectHandler.options[2].modifierTypeOption.type.id).toEqual("MINT"); + expect(modifierSelectHandler.options[3].modifierTypeOption.type.id).toEqual("MINT"); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty); + await runMysteryEncounterToEnd(game, 1); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 2 - Leave", () => { + it("should have the correct properties", () => { + const option = WeirdDreamEncounter.options[1]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected`, + }, + ], + }); + }); + + it("should reduce party levels by 12.5%", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty); + const levelsPrior = scene.getParty().map(p => p.level); + await runMysteryEncounterToEnd(game, 2); + + const levelsAfter = scene.getParty().map(p => p.level); + + for (let i = 0; i < levelsPrior.length; i++) { + expect(Math.max(Math.ceil(0.8875 * levelsPrior[i]), 1)).toBe(levelsAfter[i]); + expect(scene.getParty()[i].levelExp).toBe(0); + } + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty); + await runMysteryEncounterToEnd(game, 2); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); +}); diff --git a/src/test/mystery-encounter/mystery-encounter-utils.test.ts b/src/test/mystery-encounter/mystery-encounter-utils.test.ts new file mode 100644 index 00000000000..7a13db512aa --- /dev/null +++ b/src/test/mystery-encounter/mystery-encounter-utils.test.ts @@ -0,0 +1,305 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import GameManager from "#app/test/utils/gameManager"; +import Phaser from "phaser"; +import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; +import { Species } from "#enums/species"; +import BattleScene from "#app/battle-scene"; +import { StatusEffect } from "#app/data/status-effect"; +import MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; +import { getPokemonSpecies, speciesStarters } from "#app/data/pokemon-species"; +import { Type } from "#app/data/type"; +import { getHighestLevelPlayerPokemon, getLowestLevelPlayerPokemon, getRandomPlayerPokemon, getRandomSpeciesByStarterTier, koPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { getEncounterText, queueEncounterMessage, showEncounterDialogue, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { MessagePhase } from "#app/phases/message-phase"; + +describe("Mystery Encounter Utils", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + scene = game.scene; + initSceneWithoutEncounterPhase(game.scene, [Species.ARCEUS, Species.MANAPHY]); + }); + + describe("getRandomPlayerPokemon", () => { + it("gets a random pokemon from player party", () => { + // Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal) + game.override.seed("random"); + + let result = getRandomPlayerPokemon(scene); + expect(result.species.speciesId).toBe(Species.MANAPHY); + + game.override.seed("random2"); + + result = getRandomPlayerPokemon(scene); + expect(result.species.speciesId).toBe(Species.ARCEUS); + }); + + it("gets a fainted pokemon from player party if isAllowedInBattle is false", () => { + // Both pokemon fainted + scene.getParty().forEach(p => { + p.hp = 0; + p.trySetStatus(StatusEffect.FAINT); + p.updateInfo(); + }); + + // Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal) + game.override.seed("random"); + + let result = getRandomPlayerPokemon(scene); + expect(result.species.speciesId).toBe(Species.MANAPHY); + + game.override.seed("random2"); + + result = getRandomPlayerPokemon(scene); + expect(result.species.speciesId).toBe(Species.ARCEUS); + }); + + it("gets an unfainted legal pokemon from player party if isAllowed is true and isFainted is false", () => { + // Only faint 1st pokemon + const party = scene.getParty(); + party[0].hp = 0; + party[0].trySetStatus(StatusEffect.FAINT); + party[0].updateInfo(); + + // Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal) + game.override.seed("random"); + + let result = getRandomPlayerPokemon(scene, true); + expect(result.species.speciesId).toBe(Species.MANAPHY); + + game.override.seed("random2"); + + result = getRandomPlayerPokemon(scene, true); + expect(result.species.speciesId).toBe(Species.MANAPHY); + }); + + it("returns last unfainted pokemon if doNotReturnLastAbleMon is false", () => { + // Only faint 1st pokemon + const party = scene.getParty(); + party[0].hp = 0; + party[0].trySetStatus(StatusEffect.FAINT); + party[0].updateInfo(); + + // Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal) + game.override.seed("random"); + + let result = getRandomPlayerPokemon(scene, true, false); + expect(result.species.speciesId).toBe(Species.MANAPHY); + + game.override.seed("random2"); + + result = getRandomPlayerPokemon(scene, true, false); + expect(result.species.speciesId).toBe(Species.MANAPHY); + }); + + it("never returns last unfainted pokemon if doNotReturnLastAbleMon is true", () => { + // Only faint 1st pokemon + const party = scene.getParty(); + party[0].hp = 0; + party[0].trySetStatus(StatusEffect.FAINT); + party[0].updateInfo(); + + // Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal) + game.override.seed("random"); + + let result = getRandomPlayerPokemon(scene, true, false, true); + expect(result.species.speciesId).toBe(Species.ARCEUS); + + game.override.seed("random2"); + + result = getRandomPlayerPokemon(scene, true, false, true); + expect(result.species.speciesId).toBe(Species.ARCEUS); + }); + }); + + describe("getHighestLevelPlayerPokemon", () => { + it("gets highest level pokemon", () => { + const party = scene.getParty(); + party[0].level = 100; + + const result = getHighestLevelPlayerPokemon(scene); + expect(result.species.speciesId).toBe(Species.ARCEUS); + }); + + it("gets highest level pokemon at different index", () => { + const party = scene.getParty(); + party[1].level = 100; + + const result = getHighestLevelPlayerPokemon(scene); + expect(result.species.speciesId).toBe(Species.MANAPHY); + }); + + it("breaks ties by getting returning lower index", () => { + const party = scene.getParty(); + party[0].level = 100; + party[1].level = 100; + + const result = getHighestLevelPlayerPokemon(scene); + expect(result.species.speciesId).toBe(Species.ARCEUS); + }); + + it("returns highest level unfainted if unfainted is true", () => { + const party = scene.getParty(); + party[0].level = 100; + party[0].hp = 0; + party[0].trySetStatus(StatusEffect.FAINT); + party[0].updateInfo(); + party[1].level = 10; + + const result = getHighestLevelPlayerPokemon(scene, true); + expect(result.species.speciesId).toBe(Species.MANAPHY); + }); + }); + + describe("getLowestLevelPokemon", () => { + it("gets lowest level pokemon", () => { + const party = scene.getParty(); + party[0].level = 100; + + const result = getLowestLevelPlayerPokemon(scene); + expect(result.species.speciesId).toBe(Species.MANAPHY); + }); + + it("gets lowest level pokemon at different index", () => { + const party = scene.getParty(); + party[1].level = 100; + + const result = getLowestLevelPlayerPokemon(scene); + expect(result.species.speciesId).toBe(Species.ARCEUS); + }); + + it("breaks ties by getting returning lower index", () => { + const party = scene.getParty(); + party[0].level = 100; + party[1].level = 100; + + const result = getLowestLevelPlayerPokemon(scene); + expect(result.species.speciesId).toBe(Species.ARCEUS); + }); + + it("returns lowest level unfainted if unfainted is true", () => { + const party = scene.getParty(); + party[0].level = 10; + party[0].hp = 0; + party[0].trySetStatus(StatusEffect.FAINT); + party[0].updateInfo(); + party[1].level = 100; + + const result = getLowestLevelPlayerPokemon(scene, true); + expect(result.species.speciesId).toBe(Species.MANAPHY); + }); + }); + + describe("getRandomSpeciesByStarterTier", () => { + it("gets species for a starter tier", () => { + const result = getRandomSpeciesByStarterTier(5); + const pokeSpecies = getPokemonSpecies(result); + + expect(pokeSpecies.speciesId).toBe(result); + expect(speciesStarters[result]).toBe(5); + }); + + it("gets species for a starter tier range", () => { + const result = getRandomSpeciesByStarterTier([5, 8]); + const pokeSpecies = getPokemonSpecies(result); + + expect(pokeSpecies.speciesId).toBe(result); + expect(speciesStarters[result]).toBeGreaterThanOrEqual(5); + expect(speciesStarters[result]).toBeLessThanOrEqual(8); + }); + + it("excludes species from search", () => { + // Only 9 tiers are: Koraidon, Miraidon, Arceus, Rayquaza, Kyogre, Groudon, Zacian + const result = getRandomSpeciesByStarterTier(9, [Species.KORAIDON, Species.MIRAIDON, Species.ARCEUS, Species.RAYQUAZA, Species.KYOGRE, Species.GROUDON]); + const pokeSpecies = getPokemonSpecies(result); + expect(pokeSpecies.speciesId).toBe(Species.ZACIAN); + }); + + it("gets species of specified types", () => { + // Only 9 tiers are: Koraidon, Miraidon, Arceus, Rayquaza, Kyogre, Groudon, Zacian + const result = getRandomSpeciesByStarterTier(9, undefined, [Type.GROUND]); + const pokeSpecies = getPokemonSpecies(result); + expect(pokeSpecies.speciesId).toBe(Species.GROUDON); + }); + }); + + describe("koPlayerPokemon", () => { + it("KOs a pokemon", () => { + const party = scene.getParty(); + const arceus = party[0]; + arceus.hp = 100; + expect(arceus.isAllowedInBattle()).toBe(true); + + koPlayerPokemon(scene, arceus); + expect(arceus.isAllowedInBattle()).toBe(false); + }); + }); + + describe("getTextWithEncounterDialogueTokens", () => { + it("injects dialogue tokens and color styling", () => { + scene.currentBattle.mysteryEncounter = new MysteryEncounter(null); + scene.currentBattle.mysteryEncounter.setDialogueToken("test", "value"); + + const result = getEncounterText(scene, "mysteryEncounter:unit_test_dialogue"); + expect(result).toEqual("valuevalue {{testvalue}} {{test1}} {{test}} {{test\\}} {{test\\}} {test}}"); + }); + + it("can perform nested dialogue token injection", () => { + scene.currentBattle.mysteryEncounter = new MysteryEncounter(null); + scene.currentBattle.mysteryEncounter.setDialogueToken("test", "value"); + scene.currentBattle.mysteryEncounter.setDialogueToken("testvalue", "new"); + + const result = getEncounterText(scene, "mysteryEncounter:unit_test_dialogue"); + expect(result).toEqual("valuevalue {{testvalue}} {{test1}} {{test}} {{test\\}} {{test\\}} {test}}"); + }); + }); + + describe("queueEncounterMessage", () => { + it("queues a message with encounter dialogue tokens", async () => { + scene.currentBattle.mysteryEncounter = new MysteryEncounter(null); + scene.currentBattle.mysteryEncounter.setDialogueToken("test", "value"); + const spy = vi.spyOn(game.scene, "queueMessage"); + const phaseSpy = vi.spyOn(game.scene, "unshiftPhase"); + + queueEncounterMessage(scene, "mysteryEncounter:unit_test_dialogue"); + expect(spy).toHaveBeenCalledWith("valuevalue {{testvalue}} {{test1}} {{test}} {{test\\}} {{test\\}} {test}}", null, true); + expect(phaseSpy).toHaveBeenCalledWith(expect.any(MessagePhase)); + }); + }); + + describe("showEncounterText", () => { + it("showText with dialogue tokens", async () => { + scene.currentBattle.mysteryEncounter = new MysteryEncounter(null); + scene.currentBattle.mysteryEncounter.setDialogueToken("test", "value"); + const spy = vi.spyOn(game.scene.ui, "showText"); + + await showEncounterText(scene, "mysteryEncounter:unit_test_dialogue"); + expect(spy).toHaveBeenCalledWith("valuevalue {{testvalue}} {{test1}} {{test}} {{test\\}} {{test\\}} {test}}", null, expect.any(Function), 0, true, null); + }); + }); + + describe("showEncounterDialogue", () => { + it("showText with dialogue tokens", async () => { + scene.currentBattle.mysteryEncounter = new MysteryEncounter(null); + scene.currentBattle.mysteryEncounter.setDialogueToken("test", "value"); + const spy = vi.spyOn(game.scene.ui, "showDialogue"); + + await showEncounterDialogue(scene, "mysteryEncounter:unit_test_dialogue", "mysteryEncounter:unit_test_dialogue"); + expect(spy).toHaveBeenCalledWith("valuevalue {{testvalue}} {{test1}} {{test}} {{test\\}} {{test\\}} {test}}", "valuevalue {{testvalue}} {{test1}} {{test}} {{test\\}} {{test\\}} {test}}", null, expect.any(Function), 0); + }); + }); +}); + diff --git a/src/test/mystery-encounter/mystery-encounter.test.ts b/src/test/mystery-encounter/mystery-encounter.test.ts new file mode 100644 index 00000000000..38c999f8aac --- /dev/null +++ b/src/test/mystery-encounter/mystery-encounter.test.ts @@ -0,0 +1,54 @@ +import { afterEach, beforeAll, beforeEach, expect, describe, it } from "vitest"; +import GameManager from "#app/test/utils/gameManager"; +import Phaser from "phaser"; +import { Species } from "#enums/species"; +import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import BattleScene from "#app/battle-scene"; + +describe("Mystery Encounters", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.startingWave(11); + game.override.mysteryEncounterChance(100); + }); + + it("Spawns a mystery encounter", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS, [Species.CHARIZARD, Species.VOLCARONA]); + + await game.phaseInterceptor.to(MysteryEncounterPhase, false); + expect(game.scene.getCurrentPhase()!.constructor.name).toBe(MysteryEncounterPhase.name); + }); + + it("Encounters should not run below wave 10", async () => { + game.override.startingWave(9); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.MYSTERIOUS_CHALLENGERS); + }); + + it("Encounters should not run above wave 180", async () => { + game.override.startingWave(181); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle.mysteryEncounter).toBeUndefined(); + }); +}); + diff --git a/src/test/phases/mystery-encounter-phase.test.ts b/src/test/phases/mystery-encounter-phase.test.ts new file mode 100644 index 00000000000..0a99cd00db3 --- /dev/null +++ b/src/test/phases/mystery-encounter-phase.test.ts @@ -0,0 +1,154 @@ +import {afterEach, beforeAll, beforeEach, expect, describe, it, vi } from "vitest"; +import GameManager from "#app/test/utils/gameManager"; +import Phaser from "phaser"; +import {Species} from "#enums/species"; +import { MysteryEncounterOptionSelectedPhase, MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; +import {Mode} from "#app/ui/ui"; +import {Button} from "#enums/buttons"; +import MysteryEncounterUiHandler from "#app/ui/mystery-encounter-ui-handler"; +import {MysteryEncounterType} from "#enums/mystery-encounter-type"; +import MessageUiHandler from "#app/ui/message-ui-handler"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; + +describe("Mystery Encounter Phases", () => { + 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.startingWave(11); + game.override.mysteryEncounterChance(100); + // Seed guarantees wild encounter to be replaced by ME + game.override.seed("test"); + }); + + describe("MysteryEncounterPhase", () => { + it("Runs to MysteryEncounterPhase", async() => { + await game.runToMysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS, [Species.CHARIZARD, Species.VOLCARONA]); + + await game.phaseInterceptor.to(MysteryEncounterPhase, false); + expect(game.scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterPhase.name); + }); + + it("Runs MysteryEncounterPhase", async() => { + await game.runToMysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS, [Species.CHARIZARD, Species.VOLCARONA]); + + game.onNextPrompt("MysteryEncounterPhase", Mode.MYSTERY_ENCOUNTER, () => { + // End phase early for test + game.phaseInterceptor.superEndPhase(); + }); + await game.phaseInterceptor.run(MysteryEncounterPhase); + + expect(game.scene.mysteryEncounterSaveData.encounteredEvents.length).toBeGreaterThan(0); + expect(game.scene.mysteryEncounterSaveData.encounteredEvents[0].type).toEqual(MysteryEncounterType.MYSTERIOUS_CHALLENGERS); + expect(game.scene.mysteryEncounterSaveData.encounteredEvents[0].tier).toEqual(MysteryEncounterTier.GREAT); + expect(game.scene.ui.getMode()).toBe(Mode.MYSTERY_ENCOUNTER); + }); + + it("Selects an option for MysteryEncounterPhase", async() => { + const dialogueSpy = vi.spyOn(game.scene.ui, "showDialogue"); + const messageSpy = vi.spyOn(game.scene.ui, "showText"); + await game.runToMysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS, [Species.CHARIZARD, Species.VOLCARONA]); + + game.onNextPrompt("MysteryEncounterPhase", Mode.MESSAGE, () => { + const handler = game.scene.ui.getHandler() as MessageUiHandler; + handler.processInput(Button.ACTION); + }); + + await game.phaseInterceptor.run(MysteryEncounterPhase); + + // Select option 1 for encounter + const handler = game.scene.ui.getHandler() as MysteryEncounterUiHandler; + handler.unblockInput(); + handler.processInput(Button.ACTION); + + // Waitfor required so that option select messages and preOptionPhase logic are handled + await vi.waitFor(() => expect(game.scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterOptionSelectedPhase.name)); + expect(game.scene.ui.getMode()).toBe(Mode.MESSAGE); + expect(dialogueSpy).toHaveBeenCalledTimes(1); + expect(messageSpy).toHaveBeenCalledTimes(2); + expect(dialogueSpy).toHaveBeenCalledWith("What's this?", "???", null, expect.any(Function)); + expect(messageSpy).toHaveBeenCalledWith("Mysterious challengers have appeared!", null, expect.any(Function), 750, true); + expect(messageSpy).toHaveBeenCalledWith("The trainer steps forward...", null, expect.any(Function), 300, true); + }); + }); + + describe("MysteryEncounterOptionSelectedPhase", () => { + it("runs phase", () => { + + }); + + it("handles onOptionSelect execution", () => { + + }); + + it("hides intro visuals", () => { + + }); + + it("does not hide intro visuals if option disabled", () => { + + }); + }); + + describe("MysteryEncounterBattlePhase", () => { + it("runs phase", () => { + + }); + + it("handles TRAINER_BATTLE variant", () => { + + }); + + it("handles BOSS_BATTLE variant", () => { + + }); + + it("handles WILD_BATTLE variant", () => { + + }); + + it("handles double battle", () => { + + }); + }); + + describe("MysteryEncounterRewardsPhase", () => { + it("runs phase", () => { + + }); + + it("handles doEncounterRewards", () => { + + }); + + it("handles heal phase if enabled", () => { + + }); + }); + + describe("PostMysteryEncounterPhase", () => { + it("runs phase", () => { + + }); + + it("handles onPostOptionSelect execution", () => { + + }); + + it("runs to next EncounterPhase", () => { + + }); + }); +}); + diff --git a/src/test/phases/select-modifier-phase.test.ts b/src/test/phases/select-modifier-phase.test.ts new file mode 100644 index 00000000000..d946c850ae3 --- /dev/null +++ b/src/test/phases/select-modifier-phase.test.ts @@ -0,0 +1,209 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import Phaser from "phaser"; +import GameManager from "#app/test/utils/gameManager"; +import { initSceneWithoutEncounterPhase } from "#app/test/utils/gameManagerUtils"; +import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; +import { ModifierTier } from "#app/modifier/modifier-tier"; +import * as Utils from "#app/utils"; +import { CustomModifierSettings, ModifierTypeOption, modifierTypes } from "#app/modifier/modifier-type"; +import BattleScene from "#app/battle-scene"; +import { Species } from "#enums/species"; +import { Mode } from "#app/ui/ui"; +import { PlayerPokemon } from "#app/field/pokemon"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; + +describe("SelectModifierPhase", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + scene = game.scene; + + initSceneWithoutEncounterPhase(scene, [Species.ABRA, Species.VOLCARONA]); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + + vi.clearAllMocks(); + }); + + it("should start a select modifier phase", async () => { + const selectModifierPhase = new SelectModifierPhase(scene); + scene.pushPhase(selectModifierPhase); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + }); + + it("should generate random modifiers", async () => { + const selectModifierPhase = new SelectModifierPhase(scene); + scene.pushPhase(selectModifierPhase); + await game.phaseInterceptor.run(SelectModifierPhase); + + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(3); + }); + + it("should modify reroll cost", async () => { + const options = [ + new ModifierTypeOption(modifierTypes.POTION(), 0, 100), + new ModifierTypeOption(modifierTypes.ETHER(), 0, 400), + new ModifierTypeOption(modifierTypes.REVIVE(), 0, 1000) + ]; + + const selectModifierPhase1 = new SelectModifierPhase(scene); + const selectModifierPhase2 = new SelectModifierPhase(scene, 0, undefined, { rerollMultiplier: 2 }); + + const cost1 = selectModifierPhase1.getRerollCost(options, false); + const cost2 = selectModifierPhase2.getRerollCost(options, false); + expect(cost2).toEqual(cost1 * 2); + }); + + it("should generate random modifiers from reroll", async () => { + let selectModifierPhase = new SelectModifierPhase(scene); + scene.pushPhase(selectModifierPhase); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(3); + + // Simulate selecting reroll + selectModifierPhase = new SelectModifierPhase(scene, 1, [ModifierTier.COMMON, ModifierTier.COMMON, ModifierTier.COMMON]); + scene.unshiftPhase(selectModifierPhase); + scene.ui.setMode(Mode.MESSAGE).then(() => game.endPhase()); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + expect(modifierSelectHandler.options.length).toEqual(3); + }); + + it("should generate random modifiers of same tier for reroll with reroll lock", async () => { + // Just use fully random seed for this test + vi.spyOn(scene, "resetSeed").mockImplementation(() => { + scene.waveSeed = Utils.shiftCharCodes(scene.seed, 5); + Phaser.Math.RND.sow([scene.waveSeed]); + console.log("Wave Seed:", scene.waveSeed, 5); + scene.rngCounter = 0; + }); + + let selectModifierPhase = new SelectModifierPhase(scene); + scene.pushPhase(selectModifierPhase); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(3); + const firstRollTiers: ModifierTier[] = modifierSelectHandler.options.map(o => o.modifierTypeOption.type.tier); + + // Simulate selecting reroll with lock + scene.lockModifierTiers = true; + scene.reroll = true; + selectModifierPhase = new SelectModifierPhase(scene, 1, firstRollTiers); + scene.unshiftPhase(selectModifierPhase); + scene.ui.setMode(Mode.MESSAGE).then(() => game.endPhase()); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + expect(modifierSelectHandler.options.length).toEqual(3); + // Reroll with lock can still upgrade + expect(modifierSelectHandler.options[0].modifierTypeOption.type.tier - modifierSelectHandler.options[0].modifierTypeOption.upgradeCount).toEqual(firstRollTiers[0]); + expect(modifierSelectHandler.options[1].modifierTypeOption.type.tier - modifierSelectHandler.options[1].modifierTypeOption.upgradeCount).toEqual(firstRollTiers[1]); + expect(modifierSelectHandler.options[2].modifierTypeOption.type.tier - modifierSelectHandler.options[2].modifierTypeOption.upgradeCount).toEqual(firstRollTiers[2]); + }); + + it("should generate custom modifiers", async () => { + const customModifiers: CustomModifierSettings = { + guaranteedModifierTypeFuncs: [modifierTypes.MEMORY_MUSHROOM, modifierTypes.TM_ULTRA, modifierTypes.LEFTOVERS, modifierTypes.AMULET_COIN, modifierTypes.GOLDEN_PUNCH] + }; + const selectModifierPhase = new SelectModifierPhase(scene, 0, undefined, customModifiers); + scene.pushPhase(selectModifierPhase); + await game.phaseInterceptor.run(SelectModifierPhase); + + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(5); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toEqual("MEMORY_MUSHROOM"); + expect(modifierSelectHandler.options[1].modifierTypeOption.type.id).toEqual("TM_ULTRA"); + expect(modifierSelectHandler.options[2].modifierTypeOption.type.id).toEqual("LEFTOVERS"); + expect(modifierSelectHandler.options[3].modifierTypeOption.type.id).toEqual("AMULET_COIN"); + expect(modifierSelectHandler.options[4].modifierTypeOption.type.id).toEqual("GOLDEN_PUNCH"); + }); + + it("should generate custom modifier tiers that can upgrade from luck", async () => { + const customModifiers: CustomModifierSettings = { + guaranteedModifierTiers: [ModifierTier.COMMON, ModifierTier.GREAT, ModifierTier.ULTRA, ModifierTier.ROGUE, ModifierTier.MASTER] + }; + const pokemon = new PlayerPokemon(scene, getPokemonSpecies(Species.BULBASAUR), 10, undefined, 0, undefined, true, 2, undefined, undefined, undefined); + + // Fill party with max shinies + while (scene.getParty().length > 0) { + scene.getParty().pop(); + } + scene.getParty().push(pokemon, pokemon, pokemon, pokemon, pokemon, pokemon); + + const selectModifierPhase = new SelectModifierPhase(scene, 0, undefined, customModifiers); + scene.pushPhase(selectModifierPhase); + await game.phaseInterceptor.run(SelectModifierPhase); + + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(5); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.tier - modifierSelectHandler.options[0].modifierTypeOption.upgradeCount).toEqual(ModifierTier.COMMON); + expect(modifierSelectHandler.options[1].modifierTypeOption.type.tier - modifierSelectHandler.options[1].modifierTypeOption.upgradeCount).toEqual(ModifierTier.GREAT); + expect(modifierSelectHandler.options[2].modifierTypeOption.type.tier - modifierSelectHandler.options[2].modifierTypeOption.upgradeCount).toEqual(ModifierTier.ULTRA); + expect(modifierSelectHandler.options[3].modifierTypeOption.type.tier - modifierSelectHandler.options[3].modifierTypeOption.upgradeCount).toEqual(ModifierTier.ROGUE); + expect(modifierSelectHandler.options[4].modifierTypeOption.type.tier - modifierSelectHandler.options[4].modifierTypeOption.upgradeCount).toEqual(ModifierTier.MASTER); + }); + + it("should generate custom modifiers and modifier tiers together", async () => { + const customModifiers: CustomModifierSettings = { + guaranteedModifierTypeFuncs: [modifierTypes.MEMORY_MUSHROOM, modifierTypes.TM_COMMON], + guaranteedModifierTiers: [ModifierTier.MASTER, ModifierTier.MASTER] + }; + const selectModifierPhase = new SelectModifierPhase(scene, 0, undefined, customModifiers); + scene.pushPhase(selectModifierPhase); + await game.phaseInterceptor.run(SelectModifierPhase); + + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(4); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toEqual("MEMORY_MUSHROOM"); + expect(modifierSelectHandler.options[1].modifierTypeOption.type.id).toEqual("TM_COMMON"); + expect(modifierSelectHandler.options[2].modifierTypeOption.type.tier).toEqual(ModifierTier.MASTER); + expect(modifierSelectHandler.options[3].modifierTypeOption.type.tier).toEqual(ModifierTier.MASTER); + }); + + it("should fill remaining modifiers if fillRemaining is true with custom modifiers", async () => { + const customModifiers: CustomModifierSettings = { + guaranteedModifierTypeFuncs: [modifierTypes.MEMORY_MUSHROOM], + guaranteedModifierTiers: [ModifierTier.MASTER], + fillRemaining: true + }; + const selectModifierPhase = new SelectModifierPhase(scene, 0, undefined, customModifiers); + scene.pushPhase(selectModifierPhase); + await game.phaseInterceptor.run(SelectModifierPhase); + + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(3); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toEqual("MEMORY_MUSHROOM"); + expect(modifierSelectHandler.options[1].modifierTypeOption.type.tier).toEqual(ModifierTier.MASTER); + }); +}); diff --git a/src/test/reload.test.ts b/src/test/reload.test.ts index a96a525ca2d..5009d76d1a7 100644 --- a/src/test/reload.test.ts +++ b/src/test/reload.test.ts @@ -35,19 +35,18 @@ describe("Reload", () => { expect(preReloadRngState).toBe(postReloadRngState); }, 20000); - it("should not have RNG inconsistencies after a biome switch", async () => { + it.each(Array.from({length: 100}))("should not have RNG inconsistencies after a biome switch", async () => { game.override .startingWave(10) - .startingBiome(Biome.CAVE) // Will lead to biomes with randomly generated weather .battleType("single") - .startingLevel(100) - .enemyLevel(1000) + .startingLevel(100) // Avoid levelling up + .enemyLevel(1000) // Avoid opponent dying before game.doKillOpponents() .disableTrainerWaves() .moveset([Moves.KOWTOW_CLEAVE]) .enemyMoveset(Moves.SPLASH); await game.dailyMode.startBattle(); - // Transition from Daily Run Wave 10 to Wave 11 in order to trigger biome switch + // Transition from Wave 10 to Wave 11 in order to trigger biome switch game.move.select(Moves.KOWTOW_CLEAVE); await game.phaseInterceptor.to("DamagePhase"); await game.doKillOpponents(); @@ -63,6 +62,34 @@ describe("Reload", () => { expect(preReloadRngState).toBe(postReloadRngState); }, 20000); + it("should not have weather inconsistencies after a biome switch", async () => { + game.override + .startingWave(10) + .startingBiome(Biome.ICE_CAVE) // Will lead to Snowy Forest with randomly generated weather + .battleType("single") + .startingLevel(100) // Avoid levelling up + .enemyLevel(1000) // Avoid opponent dying before game.doKillOpponents() + .disableTrainerWaves() + .moveset([Moves.KOWTOW_CLEAVE]) + .enemyMoveset(Moves.SPLASH); + await game.classicMode.startBattle(); // Apparently daily mode would override the biome + + // Transition from Wave 10 to Wave 11 in order to trigger biome switch + game.move.select(Moves.KOWTOW_CLEAVE); + await game.phaseInterceptor.to("DamagePhase"); + await game.doKillOpponents(); + await game.toNextWave(); + expect(game.phaseInterceptor.log).toContain("NewBiomeEncounterPhase"); + + const preReloadWeather = game.scene.arena.weather; + + await game.reload.reloadSession(); + + 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(); diff --git a/src/test/ui/battle_info.test.ts b/src/test/ui/battle_info.test.ts new file mode 100644 index 00000000000..4d511b75e6f --- /dev/null +++ b/src/test/ui/battle_info.test.ts @@ -0,0 +1,55 @@ +import { ExpGainsSpeed } from "#app/enums/exp-gains-speed"; +import { Species } from "#app/enums/species"; +import { ExpPhase } from "#app/phases/exp-phase"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("../data/exp", ({}) => { + return { + getLevelRelExp: vi.fn(() => 1), //consistent levelRelExp + }; +}); + +describe("UI - Battle Info", () => { + 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.GUILLOTINE, Moves.SPLASH]) + .battleType("single") + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH) + .enemySpecies(Species.CATERPIE); + }); + + it.each([ExpGainsSpeed.FAST, ExpGainsSpeed.FASTER, ExpGainsSpeed.SKIP])( + "should increase exp gains animation by 2^%i", + async (expGainsSpeed) => { + game.settings.expGainsSpeed(expGainsSpeed); + vi.spyOn(Math, "pow"); + + await game.classicMode.startBattle([Species.CHARIZARD]); + + game.move.select(Moves.SPLASH); + await game.doKillOpponents(); + await game.phaseInterceptor.to(ExpPhase, true); + + expect(Math.pow).not.toHaveBeenCalledWith(2, expGainsSpeed); + } + ); +}); diff --git a/src/test/utils/TextInterceptor.ts b/src/test/utils/TextInterceptor.ts index 507161eb6d0..466bcbf8052 100644 --- a/src/test/utils/TextInterceptor.ts +++ b/src/test/utils/TextInterceptor.ts @@ -1,3 +1,6 @@ +/** + * Class will intercept any text or dialogue message calls and log them for test purposes + */ export default class TextInterceptor { private scene; public logs: string[] = []; diff --git a/src/test/utils/gameManager.ts b/src/test/utils/gameManager.ts index cc364a74b83..36423c5e18f 100644 --- a/src/test/utils/gameManager.ts +++ b/src/test/utils/gameManager.ts @@ -31,7 +31,6 @@ import TargetSelectUiHandler from "#app/ui/target-select-ui-handler"; import { Mode } from "#app/ui/ui"; import { Button } from "#enums/buttons"; import { ExpNotification } from "#enums/exp-notification"; -import { GameDataType } from "#enums/game-data-type"; import { PlayerGender } from "#enums/player-gender"; import { Species } from "#enums/species"; import { generateStarter, waitUntil } from "#test/utils/gameManagerUtils"; @@ -49,6 +48,12 @@ import { OverridesHelper } from "./helpers/overridesHelper"; import { SettingsHelper } from "./helpers/settingsHelper"; import { ReloadHelper } from "./helpers/reloadHelper"; import { CheckSwitchPhase } from "#app/phases/check-switch-phase"; +import BattleMessageUiHandler from "#app/ui/battle-message-ui-handler"; +import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; +import { expect } from "vitest"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { isNullOrUndefined } from "#app/utils"; +import { ExpGainsSpeed } from "#app/enums/exp-gains-speed"; /** * Class to manage the game state and transitions between phases. @@ -88,6 +93,9 @@ export default class GameManager { this.challengeMode = new ChallengeModeHelper(this); this.settings = new SettingsHelper(this); this.reload = new ReloadHelper(this); + + // Disables Mystery Encounters on all tests (can be overridden at test level) + this.override.mysteryEncounterChance(0); } /** @@ -140,7 +148,7 @@ export default class GameManager { this.scene.gameSpeed = 5; this.scene.moveAnimations = false; this.scene.showLevelUpStats = false; - this.scene.expGainsSpeed = 3; + this.scene.expGainsSpeed = ExpGainsSpeed.SKIP; this.scene.expParty = ExpNotification.SKIP; this.scene.hpBarSpeed = 3; this.scene.enableTutorials = false; @@ -178,6 +186,39 @@ export default class GameManager { console.log("===finished run to final boss encounter==="); } + /** + * Runs the game to a mystery encounter phase. + * @param encounterType if specified, will expect encounter to have been spawned + * @param species Optional array of species for party. + * @returns A promise that resolves when the EncounterPhase ends. + */ + async runToMysteryEncounter(encounterType?: MysteryEncounterType, species?: Species[]) { + if (!isNullOrUndefined(encounterType)) { + this.override.disableTrainerWaves(); + this.override.mysteryEncounter(encounterType!); + } + + await this.runToTitle(); + + this.onNextPrompt("TitlePhase", Mode.TITLE, () => { + this.scene.gameMode = getGameMode(GameModes.CLASSIC); + const starters = generateStarter(this.scene, species); + const selectStarterPhase = new SelectStarterPhase(this.scene); + this.scene.pushPhase(new EncounterPhase(this.scene, false)); + selectStarterPhase.initBattle(starters); + }, () => this.isCurrentPhase(EncounterPhase)); + + this.onNextPrompt("EncounterPhase", Mode.MESSAGE, () => { + const handler = this.scene.ui.getHandler() as BattleMessageUiHandler; + handler.processInput(Button.ACTION); + }, () => this.isCurrentPhase(MysteryEncounterPhase), true); + + await this.phaseInterceptor.run(EncounterPhase); + if (!isNullOrUndefined(encounterType)) { + expect(this.scene.currentBattle?.mysteryEncounter?.encounterType).toBe(encounterType); + } + } + /** * @deprecated Use `game.classicMode.startBattle()` or `game.dailyMode.startBattle()` instead * @@ -329,13 +370,11 @@ export default class GameManager { * @returns A promise that resolves with the exported save data. */ exportSaveToTest(): Promise { + const saveKey = "x0i2O7WRiANTqPmZ"; return new Promise(async (resolve) => { - await this.scene.gameData.saveAll(this.scene, true, true, true, true); - this.scene.reset(true); - await waitUntil(() => this.scene.ui?.getMode() === Mode.TITLE); - await this.scene.gameData.tryExportData(GameDataType.SESSION, 0); - await waitUntil(() => localStorage.hasOwnProperty("toExport")); - return resolve(localStorage.getItem("toExport")!); // TODO: is this bang correct?; + const sessionSaveData = this.scene.gameData.getSessionSaveData(this.scene); + const encryptedSaveData = AES.encrypt(JSON.stringify(sessionSaveData), saveKey).toString(); + resolve(encryptedSaveData); }); } diff --git a/src/test/utils/gameManagerUtils.ts b/src/test/utils/gameManagerUtils.ts index 20a3fd179fd..700d93082d8 100644 --- a/src/test/utils/gameManagerUtils.ts +++ b/src/test/utils/gameManagerUtils.ts @@ -7,6 +7,7 @@ import { PlayerPokemon } from "#app/field/pokemon"; import { GameModes, getGameMode } from "#app/game-mode"; import { Starter } from "#app/ui/starter-select-ui-handler"; import { Species } from "#enums/species"; +import Battle, { BattleType } from "#app/battle"; /** Function to convert Blob to string */ export function blobToString(blob) { @@ -89,3 +90,23 @@ export function getMovePosition(scene: BattleScene, pokemonIndex: 0 | 1, move: M console.log(`Move position for ${Moves[move]} (=${move}):`, index); return index; } + +/** + * Useful for populating party, wave index, etc. without having to spin up and run through an entire EncounterPhase + * @param scene + * @param species + */ +export function initSceneWithoutEncounterPhase(scene: BattleScene, species?: Species[]) { + const starters = generateStarter(scene, species); + starters.forEach((starter) => { + const starterProps = scene.gameData.getSpeciesDexAttrProps(starter.species, starter.dexAttr); + const starterFormIndex = Math.min(starterProps.formIndex, Math.max(starter.species.forms.length - 1, 0)); + const starterGender = Gender.MALE; + const starterIvs = scene.gameData.dexData[starter.species.speciesId].ivs.slice(0); + const starterPokemon = scene.addPlayerPokemon(starter.species, scene.gameMode.getStartingLevel(), starter.abilityIndex, starterFormIndex, starterGender, starterProps.shiny, starterProps.variant, starterIvs, starter.nature); + starter.moveset && starterPokemon.tryPopulateMoveset(starter.moveset); + scene.getParty().push(starterPokemon); + }); + + scene.currentBattle = new Battle(getGameMode(GameModes.CLASSIC), 5, BattleType.WILD, undefined, false); +} diff --git a/src/test/utils/gameWrapper.ts b/src/test/utils/gameWrapper.ts index 7c0ecac7c12..0ef5c4d4611 100644 --- a/src/test/utils/gameWrapper.ts +++ b/src/test/utils/gameWrapper.ts @@ -229,7 +229,7 @@ export default class GameWrapper { }; this.scene.make = new MockGameObjectCreator(mockTextureManager); this.scene.time = new MockClock(this.scene); - this.scene.remove = vi.fn(); + this.scene.remove = vi.fn(); // TODO: this should be stubbed differently } } diff --git a/src/test/utils/helpers/overridesHelper.ts b/src/test/utils/helpers/overridesHelper.ts index a17b841b682..686de58e874 100644 --- a/src/test/utils/helpers/overridesHelper.ts +++ b/src/test/utils/helpers/overridesHelper.ts @@ -10,6 +10,8 @@ import { ModifierOverride } from "#app/modifier/modifier-type"; import Overrides from "#app/overrides"; import { vi } from "vitest"; import { GameManagerHelper } from "./gameManagerHelper"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; /** * Helper to handle overrides in tests @@ -323,6 +325,41 @@ export class OverridesHelper extends GameManagerHelper { return this; } + /** + * Override the encounter chance for a mystery encounter. + * @param percentage the encounter chance in % + * @returns spy instance + */ + mysteryEncounterChance(percentage: number) { + const maxRate: number = 256; // 100% + const rate = maxRate * (percentage / 100); + vi.spyOn(Overrides, "MYSTERY_ENCOUNTER_RATE_OVERRIDE", "get").mockReturnValue(rate); + this.log(`Mystery encounter chance set to ${percentage}% (=${rate})!`); + return this; + } + + /** + * Override the encounter chance for a mystery encounter. + * @returns spy instance + * @param tier + */ + mysteryEncounterTier(tier: MysteryEncounterTier) { + vi.spyOn(Overrides, "MYSTERY_ENCOUNTER_TIER_OVERRIDE", "get").mockReturnValue(tier); + this.log(`Mystery encounter tier set to ${tier}!`); + return this; + } + + /** + * Override the encounter that spawns for the scene + * @param encounterType + * @returns spy instance + */ + mysteryEncounter(encounterType: MysteryEncounterType) { + vi.spyOn(Overrides, "MYSTERY_ENCOUNTER_OVERRIDE", "get").mockReturnValue(encounterType); + this.log(`Mystery encounter override set to ${encounterType}!`); + return this; + } + private log(...params: any[]) { console.log("Overrides:", ...params); } diff --git a/src/test/utils/helpers/reloadHelper.ts b/src/test/utils/helpers/reloadHelper.ts index c15347b08c9..e0e538120cc 100644 --- a/src/test/utils/helpers/reloadHelper.ts +++ b/src/test/utils/helpers/reloadHelper.ts @@ -5,11 +5,27 @@ import { vi } from "vitest"; import { BattleStyle } from "#app/enums/battle-style"; import { CommandPhase } from "#app/phases/command-phase"; import { TurnInitPhase } from "#app/phases/turn-init-phase"; +import { SessionSaveData } from "#app/system/game-data"; +import GameManager from "../gameManager"; /** * Helper to allow reloading sessions in unit tests. */ export class ReloadHelper extends GameManagerHelper { + sessionData: SessionSaveData; + + constructor(game: GameManager) { + super(game); + + // Whenever the game saves the session, save it to the reloadHelper instead + vi.spyOn(game.scene.gameData, "saveAll").mockImplementation((scene) => { + return new Promise((resolve, reject) => { + this.sessionData = scene.gameData.getSessionSaveData(scene); + resolve(true); + }); + }); + } + /** * Simulate reloading the session from the title screen, until reaching the * beginning of the first turn (equivalent to running `startBattle()`) for @@ -17,7 +33,6 @@ export class ReloadHelper extends GameManagerHelper { */ async reloadSession() : Promise { const scene = this.game.scene; - const sessionData = scene.gameData.getSessionSaveData(scene); const titlePhase = new TitlePhase(scene); scene.clearPhaseQueue(); @@ -25,7 +40,7 @@ export class ReloadHelper extends GameManagerHelper { // Set the last saved session to the desired session data vi.spyOn(scene.gameData, "getSession").mockReturnValue( new Promise((resolve, reject) => { - resolve(sessionData); + resolve(this.sessionData); }) ); scene.unshiftPhase(titlePhase); diff --git a/src/test/utils/helpers/settingsHelper.ts b/src/test/utils/helpers/settingsHelper.ts index 8fca2a34d47..c611a705107 100644 --- a/src/test/utils/helpers/settingsHelper.ts +++ b/src/test/utils/helpers/settingsHelper.ts @@ -1,6 +1,7 @@ import { PlayerGender } from "#app/enums/player-gender"; import { BattleStyle } from "#app/enums/battle-style"; import { GameManagerHelper } from "./gameManagerHelper"; +import { ExpGainsSpeed } from "#app/enums/exp-gains-speed"; /** * Helper to handle settings for tests @@ -38,6 +39,15 @@ export class SettingsHelper extends GameManagerHelper { this.log(`Gender set to: ${PlayerGender[gender]} (=${gender})` ); } + /** + * Change the exp gains speed + * @param speed the {@linkcode ExpGainsSpeed} to set + */ + expGainsSpeed(speed: ExpGainsSpeed) { + this.game.scene.expGainsSpeed = speed; + this.log(`Exp Gains Speed set to: ${ExpGainsSpeed[speed]} (=${speed})` ); + } + private log(...params: any[]) { console.log("Settings:", ...params); } diff --git a/src/test/utils/mocks/mockGameObject.ts b/src/test/utils/mocks/mockGameObject.ts index 9138e0f687a..4c243ec9ca1 100644 --- a/src/test/utils/mocks/mockGameObject.ts +++ b/src/test/utils/mocks/mockGameObject.ts @@ -1,3 +1,3 @@ export interface MockGameObject { - + name: string; } diff --git a/src/test/utils/mocks/mockTextureManager.ts b/src/test/utils/mocks/mockTextureManager.ts index ca8065bef97..ce19d6b6432 100644 --- a/src/test/utils/mocks/mockTextureManager.ts +++ b/src/test/utils/mocks/mockTextureManager.ts @@ -7,7 +7,6 @@ import MockSprite from "#test/utils/mocks/mocksContainer/mockSprite"; import MockText from "#test/utils/mocks/mocksContainer/mockText"; import MockTexture from "#test/utils/mocks/mocksContainer/mockTexture"; import { MockGameObject } from "./mockGameObject"; -import { vi } from "vitest"; import { MockVideoGameObject } from "./mockVideoGameObject"; /** @@ -36,7 +35,7 @@ export default class MockTextureManager { text: this.text.bind(this), bitmapText: this.text.bind(this), displayList: this.displayList, - video: vi.fn(() => new MockVideoGameObject()), + video: () => new MockVideoGameObject(), }; } diff --git a/src/test/utils/mocks/mockVideoGameObject.ts b/src/test/utils/mocks/mockVideoGameObject.ts index d8155e23b6c..d11fb5a44ce 100644 --- a/src/test/utils/mocks/mockVideoGameObject.ts +++ b/src/test/utils/mocks/mockVideoGameObject.ts @@ -2,6 +2,8 @@ import { MockGameObject } from "./mockGameObject"; /** Mocks video-related stuff */ export class MockVideoGameObject implements MockGameObject { + public name: string; + constructor() {} public play = () => null; @@ -9,4 +11,5 @@ export class MockVideoGameObject implements MockGameObject { public setOrigin = () => null; public setScale = () => null; public setVisible = () => null; + public setLoop = () => null; } diff --git a/src/test/utils/mocks/mocksContainer/mockContainer.ts b/src/test/utils/mocks/mocksContainer/mockContainer.ts index e13cef0e43e..05dad327dc6 100644 --- a/src/test/utils/mocks/mocksContainer/mockContainer.ts +++ b/src/test/utils/mocks/mocksContainer/mockContainer.ts @@ -13,7 +13,7 @@ export default class MockContainer implements MockGameObject { public frame; protected textureManager; public list: MockGameObject[] = []; - private name?: string; + public name: string; constructor(textureManager: MockTextureManager, x, y) { this.x = x; @@ -35,6 +35,10 @@ export default class MockContainer implements MockGameObject { // same as remove or destroy } + removeBetween(startIndex, endIndex, destroyChild) { + // Removes multiple children across an index range + } + addedToScene() { // This callback is invoked when this Game Object is added to a Scene. } @@ -151,6 +155,10 @@ export default class MockContainer implements MockGameObject { // Sends this Game Object to the back of its parent's display list. } + moveTo(obj) { + // Moves this Game Object to the given index in the list. + } + moveAbove(obj) { // Moves this Game Object to be above the given Game Object in the display list. } @@ -159,9 +167,9 @@ export default class MockContainer implements MockGameObject { // Moves this Game Object to be below the given Game Object in the display list. } - setName = (name: string) => { + setName(name: string) { this.name = name; - }; + } bringToTop(obj) { // Brings this Game Object to the top of its parents display list. @@ -205,5 +213,9 @@ export default class MockContainer implements MockGameObject { return this.list; } + getByName(key: string) { + return this.list.find(v => v.name === key) ?? new MockContainer(this.textureManager, 0, 0); + } + disableInteractive = () => null; } diff --git a/src/test/utils/mocks/mocksContainer/mockGraphics.ts b/src/test/utils/mocks/mocksContainer/mockGraphics.ts index e026b212e16..b20faf4ed6a 100644 --- a/src/test/utils/mocks/mocksContainer/mockGraphics.ts +++ b/src/test/utils/mocks/mocksContainer/mockGraphics.ts @@ -3,6 +3,7 @@ import { MockGameObject } from "../mockGameObject"; export default class MockGraphics implements MockGameObject { private scene; public list: MockGameObject[] = []; + public name: string; constructor(textureManager, config) { this.scene = textureManager.scene; } diff --git a/src/test/utils/mocks/mocksContainer/mockRectangle.ts b/src/test/utils/mocks/mocksContainer/mockRectangle.ts index 26c2f74ea42..48cd2cb1380 100644 --- a/src/test/utils/mocks/mocksContainer/mockRectangle.ts +++ b/src/test/utils/mocks/mocksContainer/mockRectangle.ts @@ -4,6 +4,7 @@ export default class MockRectangle implements MockGameObject { private fillColor; private scene; public list: MockGameObject[] = []; + public name: string; constructor(textureManager, x, y, width, height, fillColor) { this.fillColor = fillColor; diff --git a/src/test/utils/mocks/mocksContainer/mockSprite.ts b/src/test/utils/mocks/mocksContainer/mockSprite.ts index 83ec3951151..a55b218d0c2 100644 --- a/src/test/utils/mocks/mocksContainer/mockSprite.ts +++ b/src/test/utils/mocks/mocksContainer/mockSprite.ts @@ -14,6 +14,7 @@ export default class MockSprite implements MockGameObject { public scene; public anims; public list: MockGameObject[] = []; + public name: string; constructor(textureManager, x, y, texture) { this.textureManager = textureManager; this.scene = textureManager.scene; diff --git a/src/test/utils/mocks/mocksContainer/mockText.ts b/src/test/utils/mocks/mocksContainer/mockText.ts index 5462056f1e5..f0854fcd90a 100644 --- a/src/test/utils/mocks/mocksContainer/mockText.ts +++ b/src/test/utils/mocks/mocksContainer/mockText.ts @@ -10,17 +10,18 @@ export default class MockText implements MockGameObject { public list: MockGameObject[] = []; public style; public text = ""; - private name?: string; + public name: string; public color?: string; constructor(textureManager, x, y, content, styleOptions) { this.scene = textureManager.scene; this.textureManager = textureManager; this.style = {}; - // Phaser.GameObjects.TextStyle.prototype.setStyle = () => null; + // Phaser.GameObjects.TextStyle.prototype.setStyle = () => this; // Phaser.GameObjects.Text.prototype.updateText = () => null; // Phaser.Textures.TextureManager.prototype.addCanvas = () => {}; UI.prototype.showText = this.showText; + UI.prototype.showDialogue = this.showDialogue; this.text = ""; this.phaserText = ""; // super(scene, x, y); @@ -78,13 +79,20 @@ export default class MockText implements MockGameObject { return result; } - showText(text, delay, callback, callbackDelay, prompt, promptDelay) { + showText(text: string, delay?: integer | null, callback?: Function | null, callbackDelay?: integer | null, prompt?: boolean | null, promptDelay?: integer | null) { this.scene.messageWrapper.showText(text, delay, callback, callbackDelay, prompt, promptDelay); if (callback) { callback(); } } + showDialogue(keyOrText: string, name: string | undefined, delay: integer | null = 0, callback: Function, callbackDelay?: integer, promptDelay?: integer) { + this.scene.messageWrapper.showDialogue(keyOrText, name, delay, callback, callbackDelay, promptDelay); + if (callback) { + callback(); + } + } + setScale(scale) { // return this.phaserText.setScale(scale); } @@ -192,9 +200,9 @@ export default class MockText implements MockGameObject { }; } - setColor = (color: string) => { + setColor(color: string) { this.color = color; - }; + } setInteractive = () => null; @@ -222,9 +230,9 @@ export default class MockText implements MockGameObject { // return this.phaserText.setAlpha(alpha); } - setName = (name: string) => { + setName(name: string) { this.name = name; - }; + } setAlign(align) { // return this.phaserText.setAlign(align); @@ -248,6 +256,14 @@ export default class MockText implements MockGameObject { }; } + disableInteractive() { + // Disables interaction with this Game Object. + } + + clearTint() { + // Clears tint on this Game Object. + } + add(obj) { // Adds a child to this Game Object. this.list.push(obj); diff --git a/src/test/utils/mocks/mocksContainer/mockTexture.ts b/src/test/utils/mocks/mocksContainer/mockTexture.ts index cb31480cc60..bedd1d2c84a 100644 --- a/src/test/utils/mocks/mocksContainer/mockTexture.ts +++ b/src/test/utils/mocks/mocksContainer/mockTexture.ts @@ -12,6 +12,7 @@ export default class MockTexture implements MockGameObject { public source; public frames: object; public firstFrame: string; + public name: string; constructor(manager, key: string, source) { this.manager = manager; diff --git a/src/test/utils/phaseInterceptor.ts b/src/test/utils/phaseInterceptor.ts index a89d1788be9..46bb757c867 100644 --- a/src/test/utils/phaseInterceptor.ts +++ b/src/test/utils/phaseInterceptor.ts @@ -43,6 +43,24 @@ import { UnavailablePhase } from "#app/phases/unavailable-phase"; import { VictoryPhase } from "#app/phases/victory-phase"; import { PartyHealPhase } from "#app/phases/party-heal-phase"; import UI, { Mode } from "#app/ui/ui"; +import { + MysteryEncounterBattlePhase, + MysteryEncounterOptionSelectedPhase, + MysteryEncounterPhase, + MysteryEncounterRewardsPhase, + PostMysteryEncounterPhase +} from "#app/phases/mystery-encounter-phases"; +import { ModifierRewardPhase } from "#app/phases/modifier-reward-phase"; +import { PartyExpPhase } from "#app/phases/party-exp-phase"; + +export interface PromptHandler { + phaseTarget?: string; + mode?: Mode; + callback?: () => void; + expireFn?: () => void; + awaitingActionInput?: boolean; +} +import { ExpPhase } from "#app/phases/exp-phase"; export default class PhaseInterceptor { public scene; @@ -52,7 +70,7 @@ export default class PhaseInterceptor { private interval; private promptInterval; private intervalRun; - private prompts; + private prompts: PromptHandler[]; private phaseFrom; private inProgress; private originalSetMode; @@ -104,10 +122,18 @@ export default class PhaseInterceptor { [EndEvolutionPhase, this.startPhase], [LevelCapPhase, this.startPhase], [AttemptRunPhase, this.startPhase], + [MysteryEncounterPhase, this.startPhase], + [MysteryEncounterOptionSelectedPhase, this.startPhase], + [MysteryEncounterBattlePhase, this.startPhase], + [MysteryEncounterRewardsPhase, this.startPhase], + [PostMysteryEncounterPhase, this.startPhase], + [ModifierRewardPhase, this.startPhase], + [PartyExpPhase, this.startPhase], + [ExpPhase, this.startPhase], ]; private endBySetMode = [ - TitlePhase, SelectGenderPhase, CommandPhase + TitlePhase, SelectGenderPhase, CommandPhase, SelectModifierPhase, MysteryEncounterPhase, PostMysteryEncounterPhase ]; /** @@ -320,7 +346,7 @@ export default class PhaseInterceptor { console.log("setMode", `${Mode[mode]} (=${mode})`, args); const ret = this.originalSetMode.apply(instance, [mode, ...args]); if (!this.phases[currentPhase.constructor.name]) { - throw new Error(`missing ${currentPhase.constructor.name} in phaseInterceptior PHASES list`); + throw new Error(`missing ${currentPhase.constructor.name} in phaseInterceptor PHASES list`); } if (this.phases[currentPhase.constructor.name].endBySetMode) { this.inProgress?.callback(); @@ -338,12 +364,15 @@ export default class PhaseInterceptor { const actionForNextPrompt = this.prompts[0]; const expireFn = actionForNextPrompt.expireFn && actionForNextPrompt.expireFn(); const currentMode = this.scene.ui.getMode(); - const currentPhase = this.scene.getCurrentPhase().constructor.name; + const currentPhase = this.scene.getCurrentPhase()?.constructor.name; const currentHandler = this.scene.ui.getHandler(); if (expireFn) { this.prompts.shift(); } else if (currentMode === actionForNextPrompt.mode && currentPhase === actionForNextPrompt.phaseTarget && currentHandler.active && (!actionForNextPrompt.awaitingActionInput || (actionForNextPrompt.awaitingActionInput && currentHandler.awaitingActionInput))) { - this.prompts.shift().callback(); + const prompt = this.prompts.shift(); + if (prompt?.callback) { + prompt.callback(); + } } } }); @@ -355,6 +384,7 @@ export default class PhaseInterceptor { * @param mode - The mode of the UI. * @param callback - The callback function to execute. * @param expireFn - The function to determine if the prompt has expired. + * @param awaitingActionInput */ addToNextPrompt(phaseTarget: string, mode: Mode, callback: () => void, expireFn?: () => void, awaitingActionInput: boolean = false) { this.prompts.push({ diff --git a/src/test/vitest.setup.ts b/src/test/vitest.setup.ts index bf806cd053a..74129f20d26 100644 --- a/src/test/vitest.setup.ts +++ b/src/test/vitest.setup.ts @@ -11,9 +11,11 @@ import { initSpecies } from "#app/data/pokemon-species"; import { initAchievements } from "#app/system/achv"; import { initVouchers } from "#app/system/voucher"; import { initStatsKeys } from "#app/ui/game-stats-ui-handler"; - +import { initMysteryEncounters } from "#app/data/mystery-encounters/mystery-encounters"; import { beforeAll, vi } from "vitest"; +process.env.TZ = "UTC"; + /** Mock the override import to always return default values, ignoring any custom overrides. */ vi.mock("#app/overrides", async (importOriginal) => { const { defaultOverrides } = await importOriginal(); @@ -35,6 +37,7 @@ initSpecies(); initMoves(); initAbilities(); initLoggedInUser(); +initMysteryEncounters(); global.testFailed = false; diff --git a/src/tutorial.ts b/src/tutorial.ts index c4482839779..18d8291d227 100644 --- a/src/tutorial.ts +++ b/src/tutorial.ts @@ -1,7 +1,9 @@ import BattleScene from "./battle-scene"; import AwaitableUiHandler from "./ui/awaitable-ui-handler"; +import UiHandler from "./ui/ui-handler"; import { Mode } from "./ui/ui"; import i18next from "i18next"; +import Overrides from "#app/overrides"; export enum Tutorial { Intro = "INTRO", @@ -39,7 +41,7 @@ const tutorialHandlers = { scene.ui.showText(i18next.t("tutorial:starterSelect"), null, () => scene.ui.showText("", null, () => resolve()), null, true); }); }, - [Tutorial.Pokerus]: (scene: BattleScene) => { + [Tutorial.Pokerus]: (scene: BattleScene) => { return new Promise(resolve => { scene.ui.showText(i18next.t("tutorial:pokerus"), null, () => scene.ui.showText("", null, () => resolve()), null, true); }); @@ -63,26 +65,87 @@ const tutorialHandlers = { }, }; -export function handleTutorial(scene: BattleScene, tutorial: Tutorial): Promise { - return new Promise(resolve => { - if (!scene.enableTutorials) { - return resolve(false); - } +/** + * Run through the specified tutorial if it hasn't been seen before and mark it as seen once done + * This will show a tutorial overlay if defined in the current {@linkcode AwaitableUiHandler} + * The main menu will also get disabled while the tutorial is running + * @param scene the current {@linkcode BattleScene} + * @param tutorial the {@linkcode Tutorial} to play + * @returns a promise with result `true` if the tutorial was run and finished, `false` otherwise + */ +export async function handleTutorial(scene: BattleScene, tutorial: Tutorial): Promise { + if (!scene.enableTutorials && !Overrides.BYPASS_TUTORIAL_SKIP) { + return false; + } - if (scene.gameData.getTutorialFlags()[tutorial]) { - return resolve(false); - } + if (scene.gameData.getTutorialFlags()[tutorial] && !Overrides.BYPASS_TUTORIAL_SKIP) { + return false; + } - const handler = scene.ui.getHandler(); - if (handler instanceof AwaitableUiHandler) { - handler.tutorialActive = true; - } - tutorialHandlers[tutorial](scene).then(() => { - scene.gameData.saveTutorialFlag(tutorial, true); - if (handler instanceof AwaitableUiHandler) { - handler.tutorialActive = false; - } - resolve(true); - }); - }); + const handler = scene.ui.getHandler(); + const isMenuDisabled = scene.disableMenu; + + // starting tutorial, disable menu + scene.disableMenu = true; + if (handler instanceof AwaitableUiHandler) { + handler.tutorialActive = true; + } + + await showTutorialOverlay(scene, handler); + await tutorialHandlers[tutorial](scene); + await hideTutorialOverlay(scene, handler); + + // tutorial finished and overlay gone, re-enable menu, save tutorial as seen + scene.disableMenu = isMenuDisabled; + scene.gameData.saveTutorialFlag(tutorial, true); + if (handler instanceof AwaitableUiHandler) { + handler.tutorialActive = false; + } + + return true; } + +/** + * Show the tutorial overlay if there is one + * @param scene the current BattleScene + * @param handler the current UiHandler + * @returns `true` once the overlay has finished appearing, or if there is no overlay + */ +async function showTutorialOverlay(scene: BattleScene, handler: UiHandler) { + if (handler instanceof AwaitableUiHandler && handler.tutorialOverlay) { + scene.tweens.add({ + targets: handler.tutorialOverlay, + alpha: 0.5, + duration: 750, + ease: "Sine.easeOut", + onComplete: () => { + return true; + } + }); + } else { + return true; + } +} + +/** + * Hide the tutorial overlay if there is one + * @param scene the current BattleScene + * @param handler the current UiHandler + * @returns `true` once the overlay has finished disappearing, or if there is no overlay + */ +async function hideTutorialOverlay(scene: BattleScene, handler: UiHandler) { + if (handler instanceof AwaitableUiHandler && handler.tutorialOverlay) { + scene.tweens.add({ + targets: handler.tutorialOverlay, + alpha: 0, + duration: 500, + ease: "Sine.easeOut", + onComplete: () => { + return true; + } + }); + } else { + return true; + } +} + diff --git a/src/ui-inputs.ts b/src/ui-inputs.ts index 5860702a15b..b9ecb55958c 100644 --- a/src/ui-inputs.ts +++ b/src/ui-inputs.ts @@ -169,12 +169,14 @@ export class UiInputs { } switch (this.scene.ui?.getMode()) { case Mode.MESSAGE: - if (!(this.scene.ui.getHandler() as MessageUiHandler).pendingPrompt) { + const messageHandler = this.scene.ui.getHandler(); + if (!messageHandler.pendingPrompt || messageHandler.isTextAnimationInProgress()) { return; } case Mode.TITLE: case Mode.COMMAND: case Mode.MODIFIER_SELECT: + case Mode.MYSTERY_ENCOUNTER: this.scene.ui.setOverlayMode(Mode.MENU); break; case Mode.STARTER_SELECT: diff --git a/src/ui/abstact-option-select-ui-handler.ts b/src/ui/abstact-option-select-ui-handler.ts index c6abecda4c0..740830595ed 100644 --- a/src/ui/abstact-option-select-ui-handler.ts +++ b/src/ui/abstact-option-select-ui-handler.ts @@ -344,6 +344,7 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler { super.clear(); this.config = null; this.optionSelectContainer.setVisible(false); + this.scrollCursor = 0; this.eraseCursor(); } diff --git a/src/ui/achvs-ui-handler.ts b/src/ui/achvs-ui-handler.ts index 605b8c538a9..491c4acb523 100644 --- a/src/ui/achvs-ui-handler.ts +++ b/src/ui/achvs-ui-handler.ts @@ -1,12 +1,13 @@ -import BattleScene from "../battle-scene"; +import BattleScene from "#app/battle-scene"; import { Button } from "#enums/buttons"; import i18next from "i18next"; -import { Achv, achvs, getAchievementDescription } from "../system/achv"; -import { Voucher, getVoucherTypeIcon, getVoucherTypeName, vouchers } from "../system/voucher"; -import MessageUiHandler from "./message-ui-handler"; -import { addTextObject, TextStyle } from "./text"; -import { Mode } from "./ui"; -import { addWindow } from "./ui-theme"; +import { Achv, achvs, getAchievementDescription } from "#app/system/achv"; +import { Voucher, getVoucherTypeIcon, getVoucherTypeName, vouchers } from "#app/system/voucher"; +import MessageUiHandler from "#app/ui/message-ui-handler"; +import { addTextObject, TextStyle } from "#app/ui/text"; +import { Mode } from "#app/ui/ui"; +import { addWindow } from "#app/ui/ui-theme"; +import { ScrollBar } from "#app/ui/scroll-bar"; import { PlayerGender } from "#enums/player-gender"; enum Page { @@ -49,6 +50,7 @@ export default class AchvsUiHandler extends MessageUiHandler { private vouchersTotal: number; private currentTotal: number; + private scrollBar: ScrollBar; private scrollCursor: number; private cursorObj: Phaser.GameObjects.NineSlice | null; private currentPage: Page; @@ -91,7 +93,10 @@ export default class AchvsUiHandler extends MessageUiHandler { this.iconsBg = addWindow(this.scene, 0, this.headerBg.height, (this.scene.game.canvas.width / 6) - 2, (this.scene.game.canvas.height / 6) - this.headerBg.height - 68); this.iconsBg.setOrigin(0, 0); - this.iconsContainer = this.scene.add.container(6, this.headerBg.height + 6); + const yOffset = 6; + this.scrollBar = new ScrollBar(this.scene, this.iconsBg.width - 9, this.iconsBg.y + yOffset, 4, this.iconsBg.height - yOffset * 2, this.ROWS); + + this.iconsContainer = this.scene.add.container(5, this.headerBg.height + 8); this.icons = []; @@ -148,6 +153,7 @@ export default class AchvsUiHandler extends MessageUiHandler { this.mainContainer.add(this.headerText); this.mainContainer.add(this.headerActionText); this.mainContainer.add(this.iconsBg); + this.mainContainer.add(this.scrollBar); this.mainContainer.add(this.iconsContainer); this.mainContainer.add(titleBg); this.mainContainer.add(this.titleText); @@ -162,6 +168,7 @@ export default class AchvsUiHandler extends MessageUiHandler { this.currentPage = Page.ACHIEVEMENTS; this.setCursor(0); + this.setScrollCursor(0); this.mainContainer.setVisible(false); } @@ -175,6 +182,8 @@ export default class AchvsUiHandler extends MessageUiHandler { this.mainContainer.setVisible(true); this.setCursor(0); this.setScrollCursor(0); + this.scrollBar.setTotalRows(Math.ceil(this.currentTotal / this.COLS)); + this.scrollBar.setScrollCursor(0); this.getUi().moveTo(this.mainContainer, this.getUi().length - 1); @@ -224,6 +233,8 @@ export default class AchvsUiHandler extends MessageUiHandler { this.updateAchvIcons(); } this.setCursor(0, true); + this.scrollBar.setTotalRows(Math.ceil(this.currentTotal / this.COLS)); + this.scrollBar.setScrollCursor(0); this.mainContainer.update(); } if (button === Button.CANCEL) { @@ -237,32 +248,44 @@ export default class AchvsUiHandler extends MessageUiHandler { if (this.cursor < this.COLS) { if (this.scrollCursor) { success = this.setScrollCursor(this.scrollCursor - 1); + } else { + // Wrap around to the last row + success = this.setScrollCursor(Math.ceil(this.currentTotal / this.COLS) - this.ROWS); + let newCursorIndex = this.cursor + (this.ROWS - 1) * this.COLS; + if (newCursorIndex > this.currentTotal - this.scrollCursor * this.COLS -1) { + newCursorIndex -= this.COLS; + } + success = success && this.setCursor(newCursorIndex); } } else { success = this.setCursor(this.cursor - this.COLS); } break; case Button.DOWN: - const canMoveDown = (this.cursor + itemOffset) + this.COLS < this.currentTotal; + const canMoveDown = itemOffset + 1 < this.currentTotal; if (rowIndex >= this.ROWS - 1) { if (this.scrollCursor < Math.ceil(this.currentTotal / this.COLS) - this.ROWS && canMoveDown) { + // scroll down one row success = this.setScrollCursor(this.scrollCursor + 1); + } else { + // wrap back to the first row + success = this.setScrollCursor(0) && this.setCursor(this.cursor % this.COLS); } } else if (canMoveDown) { - success = this.setCursor(this.cursor + this.COLS); + success = this.setCursor(Math.min(this.cursor + this.COLS, this.currentTotal - itemOffset - 1)); } break; case Button.LEFT: - if (!this.cursor && this.scrollCursor) { - success = this.setScrollCursor(this.scrollCursor - 1) && this.setCursor(this.cursor + (this.COLS - 1)); - } else if (this.cursor) { + if (this.cursor % this.COLS === 0) { + success = this.setCursor(Math.min(this.cursor + this.COLS - 1, this.currentTotal - itemOffset - 1)); + } else { success = this.setCursor(this.cursor - 1); } break; case Button.RIGHT: - if (this.cursor + 1 === this.ROWS * this.COLS && this.scrollCursor < Math.ceil(this.currentTotal / this.COLS) - this.ROWS) { - success = this.setScrollCursor(this.scrollCursor + 1) && this.setCursor(this.cursor - (this.COLS - 1)); - } else if (this.cursor + itemOffset < this.currentTotal - 1) { + if ((this.cursor + 1) % this.COLS === 0 || (this.cursor + itemOffset) === (this.currentTotal - 1)) { + success = this.setCursor(this.cursor - this.cursor % this.COLS); + } else { success = this.setCursor(this.cursor + 1); } break; @@ -315,15 +338,22 @@ export default class AchvsUiHandler extends MessageUiHandler { } this.scrollCursor = scrollCursor; + this.scrollBar.setScrollCursor(this.scrollCursor); + + // Cursor cannot go farther than the last element in the list + const maxCursor = Math.min(this.cursor, this.currentTotal - this.scrollCursor * this.COLS - 1); + if (maxCursor !== this.cursor) { + this.setCursor(maxCursor); + } switch (this.currentPage) { case Page.ACHIEVEMENTS: this.updateAchvIcons(); - this.showAchv(achvs[Object.keys(achvs)[Math.min(this.cursor + this.scrollCursor * this.COLS, Object.values(achvs).length - 1)]]); + this.showAchv(achvs[Object.keys(achvs)[this.cursor + this.scrollCursor * this.COLS]]); break; case Page.VOUCHERS: this.updateVoucherIcons(); - this.showVoucher(vouchers[Object.keys(vouchers)[Math.min(this.cursor + this.scrollCursor * this.COLS, Object.values(vouchers).length - 1)]]); + this.showVoucher(vouchers[Object.keys(vouchers)[this.cursor + this.scrollCursor * this.COLS]]); break; } return true; @@ -411,6 +441,7 @@ export default class AchvsUiHandler extends MessageUiHandler { super.clear(); this.currentPage = Page.ACHIEVEMENTS; this.mainContainer.setVisible(false); + this.setScrollCursor(0); this.eraseCursor(); } diff --git a/src/ui/awaitable-ui-handler.ts b/src/ui/awaitable-ui-handler.ts index 2052c6e2ade..c6dc717aa3a 100644 --- a/src/ui/awaitable-ui-handler.ts +++ b/src/ui/awaitable-ui-handler.ts @@ -7,6 +7,7 @@ export default abstract class AwaitableUiHandler extends UiHandler { protected awaitingActionInput: boolean; protected onActionInput: Function | null; public tutorialActive: boolean = false; + public tutorialOverlay: Phaser.GameObjects.Rectangle; constructor(scene: BattleScene, mode: Mode | null = null) { super(scene, mode); @@ -24,4 +25,21 @@ export default abstract class AwaitableUiHandler extends UiHandler { return false; } + + /** + * Create a semi transparent overlay that will get shown during tutorials + * @param container the container to add the overlay to + */ + initTutorialOverlay(container: Phaser.GameObjects.Container) { + if (!this.tutorialOverlay) { + this.tutorialOverlay = new Phaser.GameObjects.Rectangle(this.scene, -1, -1, this.scene.scaledCanvas.width, this.scene.scaledCanvas.height, 0x070707); + this.tutorialOverlay.setName("tutorial-overlay"); + this.tutorialOverlay.setOrigin(0, 0); + this.tutorialOverlay.setAlpha(0); + } + + if (container) { + container.add(this.tutorialOverlay); + } + } } diff --git a/src/ui/battle-info.ts b/src/ui/battle-info.ts index c7b82dc826e..b3474bed5cd 100644 --- a/src/ui/battle-info.ts +++ b/src/ui/battle-info.ts @@ -11,8 +11,11 @@ import { Stat } from "#enums/stat"; import BattleFlyout from "./battle-flyout"; import { WindowVariant, addWindow } from "./ui-theme"; import i18next from "i18next"; +import { ExpGainsSpeed } from "#app/enums/exp-gains-speed"; export default class BattleInfo extends Phaser.GameObjects.Container { + public static readonly EXP_GAINS_DURATION_BASE = 1650; + private baseY: number; private player: boolean; @@ -159,7 +162,7 @@ export default class BattleInfo extends Phaser.GameObjects.Container { this.splicedIcon.setInteractive(new Phaser.Geom.Rectangle(0, 0, 12, 15), Phaser.Geom.Rectangle.Contains); this.add(this.splicedIcon); - this.statusIndicator = this.scene.add.sprite(0, 0, "statuses"); + this.statusIndicator = this.scene.add.sprite(0, 0, Utils.getLocalizedSpriteKey("statuses")); this.statusIndicator.setName("icon_status"); this.statusIndicator.setVisible(false); this.statusIndicator.setOrigin(0, 0); @@ -378,17 +381,8 @@ export default class BattleInfo extends Phaser.GameObjects.Container { const ownedAbilityAttrs = pokemon.scene.gameData.starterData[pokemon.species.getRootSpeciesId()].abilityAttr; - let playerOwnsThisAbility = false; // Check if the player owns ability for the root form - if ((ownedAbilityAttrs & 1) > 0 && pokemon.hasSameAbilityInRootForm(0)) { - playerOwnsThisAbility = true; - } - if ((ownedAbilityAttrs & 2) > 0 && pokemon.hasSameAbilityInRootForm(1)) { - playerOwnsThisAbility = true; - } - if ((ownedAbilityAttrs & 4) > 0 && pokemon.hasSameAbilityInRootForm(2)) { - playerOwnsThisAbility = true; - } + const playerOwnsThisAbility = pokemon.checkIfPlayerHasAbilityOfStarter(ownedAbilityAttrs); if (missingDexAttrs || !playerOwnsThisAbility) { this.ownedIcon.setTint(0x808080); @@ -702,7 +696,11 @@ export default class BattleInfo extends Phaser.GameObjects.Container { instant = true; } const durationMultiplier = Phaser.Tweens.Builders.GetEaseFunction("Sine.easeIn")(1 - (Math.max(this.lastLevel - 100, 0) / 150)); - const duration = this.visible && !instant ? (((levelExp - this.lastLevelExp) / relLevelExp) * 1650) * durationMultiplier * levelDurationMultiplier : 0; + let duration = this.visible && !instant ? (((levelExp - this.lastLevelExp) / relLevelExp) * BattleInfo.EXP_GAINS_DURATION_BASE) * durationMultiplier * levelDurationMultiplier : 0; + const speed = (this.scene as BattleScene).expGainsSpeed; + if (speed && speed >= ExpGainsSpeed.DEFAULT) { + duration = speed >= ExpGainsSpeed.SKIP ? ExpGainsSpeed.DEFAULT : duration / Math.pow(2, speed); + } if (ratio === 1) { this.lastLevelExp = 0; this.lastLevel++; diff --git a/src/ui/battle-message-ui-handler.ts b/src/ui/battle-message-ui-handler.ts index 9a694d50b29..c27c6974192 100644 --- a/src/ui/battle-message-ui-handler.ts +++ b/src/ui/battle-message-ui-handler.ts @@ -83,12 +83,7 @@ export default class BattleMessageUiHandler extends MessageUiHandler { this.nameBoxContainer.add(this.nameText); messageContainer.add(this.nameBoxContainer); - const prompt = this.scene.add.sprite(0, 0, "prompt"); - prompt.setVisible(false); - prompt.setOrigin(0, 0); - messageContainer.add(prompt); - - this.prompt = prompt; + this.initPromptSprite(messageContainer); const levelUpStatsContainer = this.scene.add.container(0, 0); levelUpStatsContainer.setVisible(false); diff --git a/src/ui/egg-gacha-ui-handler.ts b/src/ui/egg-gacha-ui-handler.ts index 9497dfe58c6..b109eda5370 100644 --- a/src/ui/egg-gacha-ui-handler.ts +++ b/src/ui/egg-gacha-ui-handler.ts @@ -287,7 +287,6 @@ export default class EggGachaUiHandler extends MessageUiHandler { this.eggGachaContainer.add(this.eggGachaSummaryContainer); const gachaMessageBoxContainer = this.scene.add.container(0, 148); - this.eggGachaContainer.add(gachaMessageBoxContainer); const gachaMessageBox = addWindow(this.scene, 0, 0, 320, 32); gachaMessageBox.setOrigin(0, 0); @@ -301,8 +300,11 @@ export default class EggGachaUiHandler extends MessageUiHandler { this.message = gachaMessageText; + this.initTutorialOverlay(this.eggGachaContainer); this.eggGachaContainer.add(gachaMessageBoxContainer); + this.initPromptSprite(gachaMessageBoxContainer); + this.setCursor(0); } diff --git a/src/ui/evolution-scene-handler.ts b/src/ui/evolution-scene-handler.ts index ffbd06afde3..76d148d083e 100644 --- a/src/ui/evolution-scene-handler.ts +++ b/src/ui/evolution-scene-handler.ts @@ -45,12 +45,7 @@ export default class EvolutionSceneHandler extends MessageUiHandler { this.message = message; - const prompt = this.scene.add.sprite(0, 0, "prompt"); - prompt.setVisible(false); - prompt.setOrigin(0, 0); - this.messageContainer.add(prompt); - - this.prompt = prompt; + this.initPromptSprite(this.messageContainer); } show(_args: any[]): boolean { diff --git a/src/ui/fight-ui-handler.ts b/src/ui/fight-ui-handler.ts index 60db9d19eef..59d14ab3bc4 100644 --- a/src/ui/fight-ui-handler.ts +++ b/src/ui/fight-ui-handler.ts @@ -1,4 +1,4 @@ -import BattleScene from "../battle-scene"; +import BattleScene, { InfoToggle } from "../battle-scene"; import { addTextObject, TextStyle } from "./text"; import { getTypeDamageMultiplierColor, Type } from "../data/type"; import { Command } from "./command-ui-handler"; @@ -10,8 +10,10 @@ import i18next from "i18next"; import {Button} from "#enums/buttons"; import Pokemon, { PokemonMove } from "#app/field/pokemon"; import { CommandPhase } from "#app/phases/command-phase"; +import MoveInfoOverlay from "./move-info-overlay"; +import { BattleType } from "#app/battle"; -export default class FightUiHandler extends UiHandler { +export default class FightUiHandler extends UiHandler implements InfoToggle { public static readonly MOVES_CONTAINER_NAME = "moves"; private movesContainer: Phaser.GameObjects.Container; @@ -25,6 +27,7 @@ export default class FightUiHandler extends UiHandler { private accuracyText: Phaser.GameObjects.Text; private cursorObj: Phaser.GameObjects.Image | null; private moveCategoryIcon: Phaser.GameObjects.Sprite; + private moveInfoOverlay : MoveInfoOverlay; protected fieldIndex: integer = 0; protected cursor2: integer = 0; @@ -84,6 +87,24 @@ export default class FightUiHandler extends UiHandler { this.accuracyText.setOrigin(1, 0.5); this.accuracyText.setVisible(false); this.moveInfoContainer.add(this.accuracyText); + + // prepare move overlay + const overlayScale = 1; + this.moveInfoOverlay = new MoveInfoOverlay(this.scene, { + delayVisibility: true, + scale: overlayScale, + onSide: true, + right: true, + x: 0, + y: -MoveInfoOverlay.getHeight(overlayScale, true), + width: (this.scene.game.canvas.width / 6) + 4, + hideEffectBox: true, + hideBg: true + }); + ui.add(this.moveInfoOverlay); + // register the overlay to receive toggle events + this.scene.addInfoToggle(this.moveInfoOverlay); + this.scene.addInfoToggle(this); } show(args: any[]): boolean { @@ -102,6 +123,8 @@ export default class FightUiHandler extends UiHandler { this.setCursor(this.getCursor()); } this.displayMoves(); + this.toggleInfo(false); // in case cancel was pressed while info toggle is active + this.active = true; return true; } @@ -120,8 +143,12 @@ export default class FightUiHandler extends UiHandler { ui.playError(); } } else { - ui.setMode(Mode.COMMAND, this.fieldIndex); - success = true; + // Cannot back out of fight menu if skipToFightInput is enabled + const { battleType, mysteryEncounter } = this.scene.currentBattle; + if (battleType !== BattleType.MYSTERY_ENCOUNTER || !mysteryEncounter?.skipToFightInput) { + ui.setMode(Mode.COMMAND, this.fieldIndex); + success = true; + } } } else { switch (button) { @@ -155,6 +182,27 @@ export default class FightUiHandler extends UiHandler { return success; } + toggleInfo(visible: boolean): void { + if (visible) { + this.movesContainer.setVisible(false); + this.cursorObj?.setVisible(false); + } + this.scene.tweens.add({ + targets: [this.movesContainer, this.cursorObj], + duration: Utils.fixedInt(125), + ease: "Sine.easeInOut", + alpha: visible ? 0 : 1 + }); + if (!visible) { + this.movesContainer.setVisible(true); + this.cursorObj?.setVisible(true); + } + } + + isActive(): boolean { + return this.active; + } + getCursor(): integer { return !this.fieldIndex ? this.cursor : this.cursor2; } @@ -162,6 +210,7 @@ export default class FightUiHandler extends UiHandler { setCursor(cursor: integer): boolean { const ui = this.getUi(); + this.moveInfoOverlay.clear(); const changed = this.getCursor() !== cursor; if (changed) { if (!this.fieldIndex) { @@ -215,6 +264,7 @@ export default class FightUiHandler extends UiHandler { //** Changes the text color and shadow according to the determined TextStyle */ this.ppText.setColor(this.getTextColor(ppColorStyle, false)); this.ppText.setShadowColor(this.getTextColor(ppColorStyle, true)); + this.moveInfoOverlay.show(pokemonMove.getMove()); pokemon.getOpponents().forEach((opponent) => { opponent.updateEffectiveness(this.getEffectivenessText(pokemon, opponent, pokemonMove)); @@ -302,8 +352,10 @@ export default class FightUiHandler extends UiHandler { this.accuracyLabel.setVisible(false); this.accuracyText.setVisible(false); this.moveCategoryIcon.setVisible(false); + this.moveInfoOverlay.clear(); messageHandler.bg.setVisible(true); this.eraseCursor(); + this.active = false; } clearMoves() { diff --git a/src/ui/menu-ui-handler.ts b/src/ui/menu-ui-handler.ts index b8c3cfd1364..0af527e518f 100644 --- a/src/ui/menu-ui-handler.ts +++ b/src/ui/menu-ui-handler.ts @@ -157,6 +157,9 @@ export default class MenuUiHandler extends MessageUiHandler { menuMessageText.setOrigin(0, 0); this.menuMessageBoxContainer.add(menuMessageText); + this.initTutorialOverlay(this.menuContainer); + this.initPromptSprite(this.menuMessageBoxContainer); + this.message = menuMessageText; // By default we use the general purpose message window @@ -433,6 +436,9 @@ export default class MenuUiHandler extends MessageUiHandler { this.scene.playSound("ui/menu_open"); + // Make sure the tutorial overlay sits above everything, but below the message box + this.menuContainer.bringToTop(this.tutorialOverlay); + this.menuContainer.bringToTop(this.menuMessageBoxContainer); handleTutorial(this.scene, Tutorial.Menu); this.bgmBar.toggleBgmBar(true); diff --git a/src/ui/message-ui-handler.ts b/src/ui/message-ui-handler.ts index a78887e1581..f1b8ed981ee 100644 --- a/src/ui/message-ui-handler.ts +++ b/src/ui/message-ui-handler.ts @@ -17,6 +17,23 @@ export default abstract class MessageUiHandler extends AwaitableUiHandler { this.pendingPrompt = false; } + /** + * Add the sprite to be displayed at the end of messages with prompts + * @param container the container to add the sprite to + */ + initPromptSprite(container: Phaser.GameObjects.Container) { + if (!this.prompt) { + const promptSprite = this.scene.add.sprite(0, 0, "prompt"); + promptSprite.setVisible(false); + promptSprite.setOrigin(0, 0); + this.prompt = promptSprite; + } + + if (container) { + container.add(this.prompt); + } + } + showText(text: string, delay?: integer | null, callback?: Function | null, callbackDelay?: integer | null, prompt?: boolean | null, promptDelay?: integer | null) { this.showTextInternal(text, delay, callback, callbackDelay, prompt, promptDelay); } @@ -29,10 +46,13 @@ export default abstract class MessageUiHandler extends AwaitableUiHandler { if (delay === null || delay === undefined) { delay = 20; } + + // Pattern matching regex that checks for @c{}, @f{}, @s{}, and @f{} patterns within message text and parses them to their respective behaviors. const charVarMap = new Map(); const delayMap = new Map(); const soundMap = new Map(); - const actionPattern = /@(c|d|s)\{(.*?)\}/; + const fadeMap = new Map(); + const actionPattern = /@(c|d|s|f)\{(.*?)\}/; let actionMatch: RegExpExecArray | null; while ((actionMatch = actionPattern.exec(text))) { switch (actionMatch[1]) { @@ -45,6 +65,9 @@ export default abstract class MessageUiHandler extends AwaitableUiHandler { case "s": soundMap.set(actionMatch.index, actionMatch[2]); break; + case "f": + fadeMap.set(actionMatch.index, parseInt(actionMatch[2])); + break; } text = text.slice(0, actionMatch.index) + text.slice(actionMatch.index + actionMatch[2].length + 4); } @@ -103,6 +126,7 @@ export default abstract class MessageUiHandler extends AwaitableUiHandler { const charVar = charVarMap.get(charIndex); const charSound = soundMap.get(charIndex); const charDelay = delayMap.get(charIndex); + const charFade = fadeMap.get(charIndex); this.message.setText(text.slice(0, charIndex)); const advance = () => { if (charVar) { @@ -134,6 +158,19 @@ export default abstract class MessageUiHandler extends AwaitableUiHandler { advance(); } }); + } else if (charFade) { + this.textTimer!.paused = true; + this.scene.time.delayedCall(150, () => { + this.scene.ui.fadeOut(750).then(() => { + const delay = Utils.getFrameMs(charFade); + this.scene.time.delayedCall(delay, () => { + this.scene.ui.fadeIn(500).then(() => { + this.textTimer!.paused = false; + advance(); + }); + }); + }); + }); } else { advance(); } @@ -160,7 +197,7 @@ export default abstract class MessageUiHandler extends AwaitableUiHandler { const lastLineWidth = lastLineTest.displayWidth; lastLineTest.destroy(); if (this.prompt) { - this.prompt.setPosition(lastLineWidth + 2, (textLinesCount - 1) * 18 + 2); + this.prompt.setPosition(this.message.x + lastLineWidth + 2, this.message.y + (textLinesCount - 1) * 18 + 2); this.prompt.play("prompt"); } this.pendingPrompt = false; @@ -186,6 +223,14 @@ export default abstract class MessageUiHandler extends AwaitableUiHandler { }; } + isTextAnimationInProgress() { + if (this.textTimer) { + return this.textTimer.repeatCount < this.textTimer.repeat; + } + + return false; + } + clearText() { this.message.setText(""); this.pendingPrompt = false; diff --git a/src/ui/modifier-select-ui-handler.ts b/src/ui/modifier-select-ui-handler.ts index ca5d27f96a4..a1e10d74c64 100644 --- a/src/ui/modifier-select-ui-handler.ts +++ b/src/ui/modifier-select-ui-handler.ts @@ -4,15 +4,17 @@ import { getPokeballAtlasKey, PokeballType } from "../data/pokeball"; import { addTextObject, getTextStyleOptions, getModifierTierTextTint, getTextColor, TextStyle } from "./text"; import AwaitableUiHandler from "./awaitable-ui-handler"; import { Mode } from "./ui"; -import { LockModifierTiersModifier, PokemonHeldItemModifier } from "../modifier/modifier"; +import { LockModifierTiersModifier, PokemonHeldItemModifier, HealShopCostModifier } from "../modifier/modifier"; import { handleTutorial, Tutorial } from "../tutorial"; -import {Button} from "#enums/buttons"; +import { Button } from "#enums/buttons"; import MoveInfoOverlay from "./move-info-overlay"; import { allMoves } from "../data/move"; import * as Utils from "./../utils"; import Overrides from "#app/overrides"; import i18next from "i18next"; import { ShopCursorTarget } from "#app/enums/shop-cursor-target"; +import { IntegerHolder } from "./../utils"; +import Phaser from "phaser"; export const SHOP_OPTIONS_ROW_LIMIT = 6; @@ -22,13 +24,18 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { private lockRarityButtonContainer: Phaser.GameObjects.Container; private transferButtonContainer: Phaser.GameObjects.Container; private checkButtonContainer: Phaser.GameObjects.Container; + private continueButtonContainer: Phaser.GameObjects.Container; private rerollCostText: Phaser.GameObjects.Text; private lockRarityButtonText: Phaser.GameObjects.Text; - private moveInfoOverlay : MoveInfoOverlay; - private moveInfoOverlayActive : boolean = false; + private moveInfoOverlay: MoveInfoOverlay; + private moveInfoOverlayActive: boolean = false; private rowCursor: integer = 0; private player: boolean; + /** + * If reroll cost is negative, it is assumed there are 0 items in the shop. + * It will cause reroll button to be disabled, and a "Continue" button to show in the place of shop items + */ private rerollCost: integer; private transferButtonWidth: integer; private checkButtonWidth: integer; @@ -105,6 +112,15 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { this.lockRarityButtonText.setOrigin(0, 0); this.lockRarityButtonContainer.add(this.lockRarityButtonText); + this.continueButtonContainer = this.scene.add.container((this.scene.game.canvas.width / 12), -(this.scene.game.canvas.height / 12)); + this.continueButtonContainer.setVisible(false); + ui.add(this.continueButtonContainer); + + // Create continue button + const continueButtonText = addTextObject(this.scene, -24, 5, i18next.t("modifierSelectUiHandler:continueNextWaveButton"), TextStyle.MESSAGE); + continueButtonText.setName("text-continue-btn"); + this.continueButtonContainer.add(continueButtonText); + // prepare move overlay const overlayScale = 1; this.moveInfoOverlay = new MoveInfoOverlay(this.scene, { @@ -113,7 +129,7 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { onSide: true, right: true, x: 1, - y: -MoveInfoOverlay.getHeight(overlayScale, true) -1, + y: -MoveInfoOverlay.getHeight(overlayScale, true) - 1, width: (this.scene.game.canvas.width / 6) - 2, }); ui.add(this.moveInfoOverlay); @@ -134,7 +150,7 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { return false; } - if (args.length !== 4 || !(args[1] instanceof Array) || !args[1].length || !(args[2] instanceof Function)) { + if (args.length !== 4 || !(args[1] instanceof Array) || !(args[2] instanceof Function)) { return false; } @@ -144,7 +160,7 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { this.player = args[0]; - const partyHasHeldItem = this.player && !!this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier && m.isTransferrable).length; + const partyHasHeldItem = this.player && !!this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier && m.isTransferable).length; const canLockRarities = !!this.scene.findModifier(m => m instanceof LockModifierTiersModifier); this.transferButtonContainer.setVisible(false); @@ -159,6 +175,9 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { this.lockRarityButtonContainer.setVisible(false); this.lockRarityButtonContainer.setAlpha(0); + this.continueButtonContainer.setVisible(false); + this.continueButtonContainer.setAlpha(0); + this.rerollButtonContainer.setPositionRelative(this.lockRarityButtonContainer, 0, canLockRarities ? -12 : 0); this.rerollCost = args[3] as integer; @@ -166,8 +185,11 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { this.updateRerollCostText(); const typeOptions = args[1] as ModifierTypeOption[]; - const shopTypeOptions = !this.scene.gameMode.hasNoShop - ? getPlayerShopModifierTypeOptionsForWave(this.scene.currentBattle.waveIndex, this.scene.getWaveMoneyAmount(1)) + const removeHealShop = this.scene.gameMode.hasNoShop; + const baseShopCost = new IntegerHolder(this.scene.getWaveMoneyAmount(1)); + this.scene.applyModifier(HealShopCostModifier, true, baseShopCost); + const shopTypeOptions = !removeHealShop + ? getPlayerShopModifierTypeOptionsForWave(this.scene.currentBattle.waveIndex, baseShopCost.value) : []; const optionsYOffset = shopTypeOptions.length >= SHOP_OPTIONS_ROW_LIMIT ? -8 : -24; @@ -180,6 +202,11 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { this.options.push(option); } + // Set "Continue" button height based on number of rows in healing items shop + const continueButton = this.continueButtonContainer.getAt(0); + continueButton.y = optionsYOffset - 5; + continueButton.setVisible(this.options.length === 0); + for (let m = 0; m < shopTypeOptions.length; m++) { const row = m < SHOP_OPTIONS_ROW_LIMIT ? 0 : 1; const col = m < SHOP_OPTIONS_ROW_LIMIT ? m : m - SHOP_OPTIONS_ROW_LIMIT; @@ -243,16 +270,24 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { this.rerollButtonContainer.setAlpha(0); this.checkButtonContainer.setAlpha(0); this.lockRarityButtonContainer.setAlpha(0); + this.continueButtonContainer.setAlpha(0); this.rerollButtonContainer.setVisible(true); this.checkButtonContainer.setVisible(true); + this.continueButtonContainer.setVisible(this.rerollCost < 0); this.lockRarityButtonContainer.setVisible(canLockRarities); this.scene.tweens.add({ - targets: [ this.rerollButtonContainer, this.lockRarityButtonContainer, this.checkButtonContainer ], + targets: [ this.checkButtonContainer, this.continueButtonContainer ], alpha: 1, duration: 250 }); + this.scene.tweens.add({ + targets: [this.rerollButtonContainer, this.lockRarityButtonContainer], + alpha: this.rerollCost < 0 ? 0.5 : 1, + duration: 250 + }); + const updateCursorTarget = () => { if (this.scene.shopCursorTarget === ShopCursorTarget.CHECK_TEAM) { this.setRowCursor(0); @@ -418,6 +453,14 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { // the modifier selection has been updated, always hide the overlay this.moveInfoOverlay.clear(); if (this.rowCursor) { + if (this.rowCursor === 1 && options.length === 0) { + // Continue button when no shop items + this.cursorObj.setScale(1.25); + this.cursorObj.setPosition((this.scene.game.canvas.width / 18) + 23, (-this.scene.game.canvas.height / 12) - (this.shopOptionsRows.length > 1 ? 6 : 22)); + ui.showText(i18next.t("modifierSelectUiHandler:continueNextWaveDescription")); + return ret; + } + const sliceWidth = (this.scene.game.canvas.width / 6) / (options.length + 2); if (this.rowCursor < 2) { this.cursorObj.setPosition(sliceWidth * (cursor + 1) + (sliceWidth * 0.5) - 20, (-this.scene.game.canvas.height / 12) - (this.shopOptionsRows.length > 1 ? 6 : 22)); @@ -435,10 +478,10 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { this.cursorObj.setPosition(6, this.lockRarityButtonContainer.visible ? -72 : -60); ui.showText(i18next.t("modifierSelectUiHandler:rerollDesc")); } else if (cursor === 1) { - this.cursorObj.setPosition((this.scene.game.canvas.width - this.transferButtonWidth - this.checkButtonWidth)/6 - 30, -60); + this.cursorObj.setPosition((this.scene.game.canvas.width - this.transferButtonWidth - this.checkButtonWidth) / 6 - 30, -60); ui.showText(i18next.t("modifierSelectUiHandler:transferDesc")); } else if (cursor === 2) { - this.cursorObj.setPosition((this.scene.game.canvas.width - this.checkButtonWidth)/6 - 10, -60); + this.cursorObj.setPosition((this.scene.game.canvas.width - this.checkButtonWidth) / 6 - 10, -60); ui.showText(i18next.t("modifierSelectUiHandler:checkTeamDesc")); } else { this.cursorObj.setPosition(6, -60); @@ -454,7 +497,14 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { if (rowCursor !== lastRowCursor) { this.rowCursor = rowCursor; let newCursor = Math.round(this.cursor / Math.max(this.getRowItems(lastRowCursor) - 1, 1) * (this.getRowItems(rowCursor) - 1)); + if (rowCursor === 1 && this.options.length === 0) { + // Handle empty shop + newCursor = 0; + } if (rowCursor === 0) { + if (this.options.length === 0) { + newCursor = 1; + } if (newCursor === 0 && !this.rerollButtonContainer.visible) { newCursor = 1; } @@ -495,6 +545,13 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { } updateRerollCostText(): void { + const rerollDisabled = this.rerollCost < 0; + if (rerollDisabled) { + this.rerollCostText.setVisible(false); + return; + } else { + this.rerollCostText.setVisible(true); + } const canReroll = this.scene.money >= this.rerollCost; const formattedMoney = Utils.formatMoney(this.scene.moneyFormat, this.rerollCost); @@ -539,7 +596,7 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { onComplete: () => options.forEach(o => o.destroy()) }); - [ this.rerollButtonContainer, this.checkButtonContainer, this.transferButtonContainer, this.lockRarityButtonContainer ].forEach(container => { + [ this.rerollButtonContainer, this.checkButtonContainer, this.transferButtonContainer, this.lockRarityButtonContainer, this.continueButtonContainer ].forEach(container => { if (container.visible) { this.scene.tweens.add({ targets: container, diff --git a/src/ui/move-info-overlay.ts b/src/ui/move-info-overlay.ts index 77010f84528..42026082b36 100644 --- a/src/ui/move-info-overlay.ts +++ b/src/ui/move-info-overlay.ts @@ -15,12 +15,16 @@ export interface MoveInfoOverlaySettings { //location and width of the component; unaffected by scaling x?: number; y?: number; - width?: number; // default is always half the screen, regardless of scale + /** Default is always half the screen, regardless of scale */ + width?: number; + /** Determines whether to display the small secondary box */ + hideEffectBox?: boolean; + hideBg?: boolean; } -const EFF_HEIGHT = 46; +const EFF_HEIGHT = 48; const EFF_WIDTH = 82; -const DESC_HEIGHT = 46; +const DESC_HEIGHT = 48; const BORDER = 8; const GLOBAL_SCALE = 6; @@ -38,6 +42,7 @@ export default class MoveInfoOverlay extends Phaser.GameObjects.Container implem private acc: Phaser.GameObjects.Text; private typ: Phaser.GameObjects.Sprite; private cat: Phaser.GameObjects.Sprite; + private descBg: Phaser.GameObjects.NineSlice; private options : MoveInfoOverlaySettings; @@ -52,9 +57,9 @@ export default class MoveInfoOverlay extends Phaser.GameObjects.Container implem // prepare the description box const width = (options?.width || MoveInfoOverlay.getWidth(scale, scene)) / scale; // divide by scale as we always want this to be half a window wide - const descBg = addWindow(scene, (options?.onSide && !options?.right ? EFF_WIDTH : 0), options?.top ? EFF_HEIGHT : 0, width - (options?.onSide ? EFF_WIDTH : 0), DESC_HEIGHT); - descBg.setOrigin(0, 0); - this.add(descBg); + this.descBg = addWindow(scene, (options?.onSide && !options?.right ? EFF_WIDTH : 0), options?.top ? EFF_HEIGHT : 0, width - (options?.onSide ? EFF_WIDTH : 0), DESC_HEIGHT); + this.descBg.setOrigin(0, 0); + this.add(this.descBg); // set up the description; wordWrap uses true pixels, unaffected by any scaling, while other values are affected this.desc = addTextObject(scene, (options?.onSide && !options?.right ? EFF_WIDTH : 0) + BORDER, (options?.top ? EFF_HEIGHT : 0) + BORDER - 2, "", TextStyle.BATTLE_INFO, { wordWrap: { width: (width - (BORDER - 2) * 2 - (options?.onSide ? EFF_WIDTH : 0)) * GLOBAL_SCALE } }); @@ -91,7 +96,7 @@ export default class MoveInfoOverlay extends Phaser.GameObjects.Container implem valuesBg.setOrigin(0, 0); this.val.add(valuesBg); - this.typ = this.scene.add.sprite(25, EFF_HEIGHT - 35, `types${Utils.verifyLang(i18next.language) ? `_${i18next.language}` : ""}`, "unknown"); + this.typ = this.scene.add.sprite(25, EFF_HEIGHT - 35, Utils.getLocalizedSpriteKey("types"), "unknown"); this.typ.setScale(0.8); this.val.add(this.typ); @@ -125,6 +130,14 @@ export default class MoveInfoOverlay extends Phaser.GameObjects.Container implem this.acc.setOrigin(1, 0.5); this.val.add(this.acc); + if (options?.hideEffectBox) { + this.val.setVisible(false); + } + + if (options?.hideBg) { + this.descBg.setVisible(false); + } + // hide this component for now this.setVisible(false); } @@ -138,7 +151,7 @@ export default class MoveInfoOverlay extends Phaser.GameObjects.Container implem this.pow.setText(move.power >= 0 ? move.power.toString() : "---"); this.acc.setText(move.accuracy >= 0 ? move.accuracy.toString() : "---"); this.pp.setText(move.pp >= 0 ? move.pp.toString() : "---"); - this.typ.setTexture(`types${Utils.verifyLang(i18next.language) ? `_${i18next.language}` : ""}`, Type[move.type].toLowerCase()); + this.typ.setTexture(Utils.getLocalizedSpriteKey("types"), Type[move.type].toLowerCase()); this.cat.setFrame(MoveCategory[move.category].toLowerCase()); this.desc.setText(move?.effect || ""); @@ -176,8 +189,19 @@ export default class MoveInfoOverlay extends Phaser.GameObjects.Container implem this.active = false; } - toggleInfo(force?: boolean): void { - this.setVisible(force ?? !this.visible); + toggleInfo(visible: boolean): void { + if (visible) { + this.setVisible(true); + } + this.scene.tweens.add({ + targets: this.desc, + duration: Utils.fixedInt(125), + ease: "Sine.easeInOut", + alpha: visible ? 1 : 0 + }); + if (!visible) { + this.setVisible(false); + } } isActive(): boolean { diff --git a/src/ui/mystery-encounter-ui-handler.ts b/src/ui/mystery-encounter-ui-handler.ts new file mode 100644 index 00000000000..08de740e3ec --- /dev/null +++ b/src/ui/mystery-encounter-ui-handler.ts @@ -0,0 +1,626 @@ +import BattleScene from "../battle-scene"; +import { addBBCodeTextObject, getBBCodeFrag, TextStyle } from "./text"; +import { Mode } from "./ui"; +import UiHandler from "./ui-handler"; +import { Button } from "#enums/buttons"; +import { addWindow, WindowVariant } from "./ui-theme"; +import { MysteryEncounterPhase } from "../phases/mystery-encounter-phases"; +import { PartyUiMode } from "./party-ui-handler"; +import MysteryEncounterOption from "#app/data/mystery-encounters/mystery-encounter-option"; +import * as Utils from "../utils"; +import { isNullOrUndefined } from "../utils"; +import { getPokeballAtlasKey } from "../data/pokeball"; +import { OptionSelectSettings } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import i18next from "i18next"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext"; + +export default class MysteryEncounterUiHandler extends UiHandler { + private cursorContainer: Phaser.GameObjects.Container; + private cursorObj?: Phaser.GameObjects.Image; + + private optionsContainer: Phaser.GameObjects.Container; + // Length = max number of allowable options (4) + private optionScrollTweens: (Phaser.Tweens.Tween | null)[] = new Array(4).fill(null); + + private tooltipWindow: Phaser.GameObjects.NineSlice; + private tooltipContainer: Phaser.GameObjects.Container; + private tooltipScrollTween?: Phaser.Tweens.Tween; + + private descriptionWindow: Phaser.GameObjects.NineSlice; + private descriptionContainer: Phaser.GameObjects.Container; + private descriptionScrollTween?: Phaser.Tweens.Tween; + private rarityBall: Phaser.GameObjects.Sprite; + + private dexProgressWindow: Phaser.GameObjects.NineSlice; + private dexProgressContainer: Phaser.GameObjects.Container; + private showDexProgress: boolean = false; + + private overrideSettings?: OptionSelectSettings; + private encounterOptions: MysteryEncounterOption[] = []; + private optionsMeetsReqs: boolean[]; + + protected viewPartyIndex: number = 0; + protected viewPartyXPosition: number = 0; + + protected blockInput: boolean = true; + + constructor(scene: BattleScene) { + super(scene, Mode.MYSTERY_ENCOUNTER); + } + + override setup() { + const ui = this.getUi(); + + this.cursorContainer = this.scene.add.container(18, -38.7); + this.cursorContainer.setVisible(false); + ui.add(this.cursorContainer); + this.optionsContainer = this.scene.add.container(12, -38.7); + this.optionsContainer.setVisible(false); + ui.add(this.optionsContainer); + this.dexProgressContainer = this.scene.add.container(214, -43); + this.dexProgressContainer.setVisible(false); + ui.add(this.dexProgressContainer); + this.descriptionContainer = this.scene.add.container(0, -152); + this.descriptionContainer.setVisible(false); + ui.add(this.descriptionContainer); + this.tooltipContainer = this.scene.add.container(210, -48); + this.tooltipContainer.setVisible(false); + ui.add(this.tooltipContainer); + + this.setCursor(this.getCursor()); + + this.descriptionWindow = addWindow(this.scene, 0, 0, 150, 105, false, false, 0, 0, WindowVariant.THIN); + this.descriptionContainer.add(this.descriptionWindow); + + this.tooltipWindow = addWindow(this.scene, 0, 0, 110, 48, false, false, 0, 0, WindowVariant.THIN); + this.tooltipContainer.add(this.tooltipWindow); + + this.dexProgressWindow = addWindow(this.scene, 0, 0, 24, 28, false, false, 0, 0, WindowVariant.THIN); + this.dexProgressContainer.add(this.dexProgressWindow); + + this.rarityBall = this.scene.add.sprite(141, 9, "pb"); + this.rarityBall.setScale(0.75); + this.descriptionContainer.add(this.rarityBall); + + const dexProgressIndicator = this.scene.add.sprite(12, 10, "encounter_radar"); + dexProgressIndicator.setScale(0.80); + this.dexProgressContainer.add(dexProgressIndicator); + this.dexProgressContainer.setInteractive(new Phaser.Geom.Rectangle(0, 0, 24, 28), Phaser.Geom.Rectangle.Contains); + } + + override show(args: any[]): boolean { + super.show(args); + + this.overrideSettings = args[0] as OptionSelectSettings ?? {}; + const showDescriptionContainer = isNullOrUndefined(this.overrideSettings?.hideDescription) ? true : !this.overrideSettings?.hideDescription; + const slideInDescription = isNullOrUndefined(this.overrideSettings?.slideInDescription) ? true : this.overrideSettings?.slideInDescription; + const startingCursorIndex = this.overrideSettings?.startingCursorIndex ?? 0; + + this.cursorContainer.setVisible(true); + this.descriptionContainer.setVisible(showDescriptionContainer); + this.optionsContainer.setVisible(true); + this.dexProgressContainer.setVisible(true); + this.displayEncounterOptions(slideInDescription); + const cursor = this.getCursor(); + if (cursor === (this.optionsContainer?.length || 0) - 1) { + // Always resets cursor on view party button if it was last there + this.setCursor(cursor); + } else { + this.setCursor(startingCursorIndex); + } + if (this.blockInput) { + setTimeout(() => { + this.unblockInput(); + }, 1000); + } + this.displayOptionTooltip(); + + return true; + } + + override processInput(button: Button): boolean { + const ui = this.getUi(); + + let success = false; + + const cursor = this.getCursor(); + + if (button === Button.CANCEL || button === Button.ACTION) { + if (button === Button.ACTION) { + const selected = this.encounterOptions[cursor]; + if (cursor === this.viewPartyIndex) { + // Handle view party + success = true; + const overrideSettings: OptionSelectSettings = { + ...this.overrideSettings, + slideInDescription: false + }; + this.scene.ui.setMode(Mode.PARTY, PartyUiMode.CHECK, -1, () => { + this.scene.ui.setMode(Mode.MYSTERY_ENCOUNTER, overrideSettings); + setTimeout(() => { + this.setCursor(this.viewPartyIndex); + this.unblockInput(); + }, 300); + }); + } else if (this.blockInput || (!this.optionsMeetsReqs[cursor] && (selected.optionMode === MysteryEncounterOptionMode.DISABLED_OR_DEFAULT || selected.optionMode === MysteryEncounterOptionMode.DISABLED_OR_SPECIAL))) { + success = false; + } else { + if ((this.scene.getCurrentPhase() as MysteryEncounterPhase).handleOptionSelect(selected, cursor)) { + success = true; + } else { + ui.playError(); + } + } + } else { + // TODO: If we need to handle cancel option? Maybe default logic to leave/run from encounter idk + } + } else { + switch (this.optionsContainer.getAll()?.length) { + default: + case 3: + success = this.handleTwoOptionMoveInput(button); + break; + case 4: + success = this.handleThreeOptionMoveInput(button); + break; + case 5: + success = this.handleFourOptionMoveInput(button); + break; + } + + this.displayOptionTooltip(); + } + + if (success) { + ui.playSelect(); + } + + return success; + } + + private handleTwoOptionMoveInput(button: Button): boolean { + let success = false; + const cursor = this.getCursor(); + switch (button) { + case Button.UP: + if (cursor < this.viewPartyIndex) { + success = this.setCursor(this.viewPartyIndex); + } + break; + case Button.DOWN: + if (cursor === this.viewPartyIndex) { + success = this.setCursor(1); + } + break; + case Button.LEFT: + if (cursor > 0) { + success = this.setCursor(cursor - 1); + } + break; + case Button.RIGHT: + if (cursor < this.viewPartyIndex) { + success = this.setCursor(cursor + 1); + } + break; + } + + return success; + } + + private handleThreeOptionMoveInput(button: Button): boolean { + let success = false; + const cursor = this.getCursor(); + switch (button) { + case Button.UP: + if (cursor === 2) { + success = this.setCursor(cursor - 2); + } else { + success = this.setCursor(this.viewPartyIndex); + } + break; + case Button.DOWN: + if (cursor === this.viewPartyIndex) { + success = this.setCursor(1); + } else { + success = this.setCursor(2); + } + break; + case Button.LEFT: + if (cursor === this.viewPartyIndex) { + success = this.setCursor(1); + } else if (cursor === 1) { + success = this.setCursor(cursor - 1); + } + break; + case Button.RIGHT: + if (cursor === 1) { + success = this.setCursor(this.viewPartyIndex); + } else if (cursor < 1) { + success = this.setCursor(cursor + 1); + } + break; + } + + return success; + } + + private handleFourOptionMoveInput(button: Button): boolean { + let success = false; + const cursor = this.getCursor(); + switch (button) { + case Button.UP: + if (cursor >= 2 && cursor !== this.viewPartyIndex) { + success = this.setCursor(cursor - 2); + } else { + success = this.setCursor(this.viewPartyIndex); + } + break; + case Button.DOWN: + if (cursor <= 1) { + success = this.setCursor(cursor + 2); + } else if (cursor === this.viewPartyIndex) { + success = this.setCursor(1); + } + break; + case Button.LEFT: + if (cursor === this.viewPartyIndex) { + success = this.setCursor(1); + } else if (cursor % 2 === 1) { + success = this.setCursor(cursor - 1); + } + break; + case Button.RIGHT: + if (cursor === 1) { + success = this.setCursor(this.viewPartyIndex); + } else if (cursor % 2 === 0 && cursor !== this.viewPartyIndex) { + success = this.setCursor(cursor + 1); + } + break; + } + + return success; + } + + /** + * When ME UI first displays, the option buttons will be disabled temporarily to prevent player accidentally clicking through hastily + * This method is automatically called after a short delay but can also be called manually + */ + unblockInput() { + if (this.blockInput) { + this.blockInput = false; + for (let i = 0; i < this.optionsContainer.length - 1; i++) { + const optionMode = this.encounterOptions[i].optionMode; + if (!this.optionsMeetsReqs[i] && (optionMode === MysteryEncounterOptionMode.DISABLED_OR_DEFAULT || optionMode === MysteryEncounterOptionMode.DISABLED_OR_SPECIAL)) { + continue; + } + (this.optionsContainer.getAt(i) as Phaser.GameObjects.Text).setAlpha(1); + } + } + } + + override getCursor(): number { + return this.cursor ? this.cursor : 0; + } + + override setCursor(cursor: number): boolean { + const prevCursor = this.getCursor(); + const changed = prevCursor !== cursor; + if (changed) { + this.cursor = cursor; + } + + this.viewPartyIndex = this.optionsContainer.getAll()?.length - 1; + + if (!this.cursorObj) { + this.cursorObj = this.scene.add.image(0, 0, "cursor"); + this.cursorContainer.add(this.cursorObj); + } + + if (cursor === this.viewPartyIndex) { + this.cursorObj.setPosition(this.viewPartyXPosition, -17); + } else if (this.optionsContainer.getAll()?.length === 3) { // 2 Options + this.cursorObj.setPosition(-10.5 + (cursor % 2 === 1 ? 100 : 0), 15); + } else if (this.optionsContainer.getAll()?.length === 4) { // 3 Options + this.cursorObj.setPosition(-10.5 + (cursor % 2 === 1 ? 100 : 0), 7 + (cursor > 1 ? 16 : 0)); + } else if (this.optionsContainer.getAll()?.length === 5) { // 4 Options + this.cursorObj.setPosition(-10.5 + (cursor % 2 === 1 ? 100 : 0), 7 + (cursor > 1 ? 16 : 0)); + } + + return changed; + } + + displayEncounterOptions(slideInDescription: boolean = true): void { + this.getUi().clearText(); + const mysteryEncounter = this.scene.currentBattle.mysteryEncounter!; + this.encounterOptions = this.overrideSettings?.overrideOptions ?? mysteryEncounter.options; + this.optionsMeetsReqs = []; + + const titleText: string | null = getEncounterText(this.scene, mysteryEncounter.dialogue.encounterOptionsDialogue?.title, TextStyle.TOOLTIP_TITLE); + const descriptionText: string | null = getEncounterText(this.scene, mysteryEncounter.dialogue.encounterOptionsDialogue?.description, TextStyle.TOOLTIP_CONTENT); + const queryText: string | null = getEncounterText(this.scene, mysteryEncounter.dialogue.encounterOptionsDialogue?.query, TextStyle.TOOLTIP_CONTENT); + + // Clear options container (except cursor) + this.optionsContainer.removeAll(true); + + // Options Window + for (let i = 0; i < this.encounterOptions.length; i++) { + const option = this.encounterOptions[i]; + + let optionText: BBCodeText; + switch (this.encounterOptions.length) { + default: + case 2: + optionText = addBBCodeTextObject(this.scene, i % 2 === 0 ? 0 : 100, 8, "-", TextStyle.WINDOW, { fontSize: "80px", lineSpacing: -8 }); + break; + case 3: + optionText = addBBCodeTextObject(this.scene, i % 2 === 0 ? 0 : 100, i < 2 ? 0 : 16, "-", TextStyle.WINDOW, { fontSize: "80px", lineSpacing: -8 }); + break; + case 4: + optionText = addBBCodeTextObject(this.scene, i % 2 === 0 ? 0 : 100, i < 2 ? 0 : 16, "-", TextStyle.WINDOW, { fontSize: "80px", lineSpacing: -8 }); + break; + } + + this.optionsMeetsReqs.push(option.meetsRequirements(this.scene)); + const optionDialogue = option.dialogue!; + const label = !this.optionsMeetsReqs[i] && optionDialogue.disabledButtonLabel ? optionDialogue.disabledButtonLabel : optionDialogue.buttonLabel; + let text: string | null; + if (option.hasRequirements() && this.optionsMeetsReqs[i] && (option.optionMode === MysteryEncounterOptionMode.DEFAULT_OR_SPECIAL || option.optionMode === MysteryEncounterOptionMode.DISABLED_OR_SPECIAL)) { + // Options with special requirements that are met are automatically colored green + text = getEncounterText(this.scene, label, TextStyle.SUMMARY_GREEN); + } else { + text = getEncounterText(this.scene, label, optionDialogue.style ? optionDialogue.style : TextStyle.WINDOW); + } + + if (text) { + optionText.setText(text); + } + + if (!this.optionsMeetsReqs[i] && (option.optionMode === MysteryEncounterOptionMode.DISABLED_OR_DEFAULT || option.optionMode === MysteryEncounterOptionMode.DISABLED_OR_SPECIAL)) { + optionText.setAlpha(0.5); + } + if (this.blockInput) { + optionText.setAlpha(0.5); + } + + // Sets up the mask that hides the option text to give an illusion of scrolling + const nonScrollWidth = 90; + const optionTextMaskRect = this.scene.make.graphics({}); + optionTextMaskRect.setScale(6); + optionTextMaskRect.fillStyle(0xFFFFFF); + optionTextMaskRect.beginPath(); + optionTextMaskRect.fillRect(optionText.x + 11, optionText.y + 140, nonScrollWidth, 18); + + const optionTextMask = optionTextMaskRect.createGeometryMask(); + optionText.setMask(optionTextMask); + + const optionTextWidth = optionText.displayWidth; + + const tween = this.optionScrollTweens[i]; + if (tween) { + tween.remove(); + this.optionScrollTweens[i] = null; + } + + // Animates the option text scrolling sideways + if (optionTextWidth > nonScrollWidth) { + this.optionScrollTweens[i] = this.scene.tweens.add({ + targets: optionText, + delay: Utils.fixedInt(2000), + loop: -1, + hold: Utils.fixedInt(2000), + duration: Utils.fixedInt((optionTextWidth - nonScrollWidth) / 15 * 2000), + x: `-=${(optionTextWidth - nonScrollWidth)}` + }); + } + + this.optionsContainer.add(optionText); + } + + // View Party Button + const viewPartyText = addBBCodeTextObject(this.scene, (this.scene.game.canvas.width) / 6, -24, getBBCodeFrag(i18next.t("mysteryEncounterMessages:view_party_button"), TextStyle.PARTY), TextStyle.PARTY); + this.optionsContainer.add(viewPartyText); + viewPartyText.x -= (viewPartyText.displayWidth + 16); + this.viewPartyXPosition = viewPartyText.x - 10; + + // Description Window + const titleTextObject = addBBCodeTextObject(this.scene, 0, 0, titleText ?? "", TextStyle.TOOLTIP_TITLE, { wordWrap: { width: 750 }, align: "center", lineSpacing: -8 }); + this.descriptionContainer.add(titleTextObject); + titleTextObject.setPosition(72 - titleTextObject.displayWidth / 2, 5.5); + + // Rarity of encounter + const index = mysteryEncounter.encounterTier === MysteryEncounterTier.COMMON ? 0 : + mysteryEncounter.encounterTier === MysteryEncounterTier.GREAT ? 1 : + mysteryEncounter.encounterTier === MysteryEncounterTier.ULTRA ? 2 : + mysteryEncounter.encounterTier === MysteryEncounterTier.ROGUE ? 3 : 4; + const ballType = getPokeballAtlasKey(index); + this.rarityBall.setTexture("pb", ballType); + + const descriptionTextObject = addBBCodeTextObject(this.scene, 6, 25, descriptionText ?? "", TextStyle.TOOLTIP_CONTENT, { wordWrap: { width: 830 } }); + + // Sets up the mask that hides the description text to give an illusion of scrolling + const descriptionTextMaskRect = this.scene.make.graphics({}); + descriptionTextMaskRect.setScale(6); + descriptionTextMaskRect.fillStyle(0xFFFFFF); + descriptionTextMaskRect.beginPath(); + descriptionTextMaskRect.fillRect(6, 53, 206, 57); + + const abilityDescriptionTextMask = descriptionTextMaskRect.createGeometryMask(); + + descriptionTextObject.setMask(abilityDescriptionTextMask); + + const descriptionLineCount = Math.floor(descriptionTextObject.displayHeight / 10); + + if (this.descriptionScrollTween) { + this.descriptionScrollTween.remove(); + this.descriptionScrollTween = undefined; + } + + // Animates the description text moving upwards + if (descriptionLineCount > 6) { + this.descriptionScrollTween = this.scene.tweens.add({ + targets: descriptionTextObject, + delay: Utils.fixedInt(2000), + loop: -1, + hold: Utils.fixedInt(2000), + duration: Utils.fixedInt((descriptionLineCount - 6) * 2000), + y: `-=${10 * (descriptionLineCount - 6)}` + }); + } + + this.descriptionContainer.add(descriptionTextObject); + + const queryTextObject = addBBCodeTextObject(this.scene, 0, 0, queryText ?? "", TextStyle.TOOLTIP_CONTENT, { wordWrap: { width: 830 } }); + this.descriptionContainer.add(queryTextObject); + queryTextObject.setPosition(75 - queryTextObject.displayWidth / 2, 90); + + // Slide in description container + if (slideInDescription) { + this.descriptionContainer.x -= 150; + this.scene.tweens.add({ + targets: this.descriptionContainer, + x: "+=150", + ease: "Sine.easeInOut", + duration: 1000 + }); + } + } + + /** + * Updates and displays the tooltip for a given option + * The tooltip will auto wrap and scroll if it is too long + */ + private displayOptionTooltip() { + const cursor = this.getCursor(); + // Clear tooltip box + if (this.tooltipContainer.length > 1) { + this.tooltipContainer.removeBetween(1, this.tooltipContainer.length, true); + } + this.tooltipContainer.setVisible(true); + + if (isNullOrUndefined(cursor) || cursor > this.optionsContainer.length - 2) { + // Ignore hovers on view party button + // Hide dex progress if visible + this.showHideDexProgress(false); + return; + } + + let text: string | null; + const cursorOption = this.encounterOptions[cursor]; + const optionDialogue = cursorOption.dialogue!; + if (!this.optionsMeetsReqs[cursor] && (cursorOption.optionMode === MysteryEncounterOptionMode.DISABLED_OR_DEFAULT || cursorOption.optionMode === MysteryEncounterOptionMode.DISABLED_OR_SPECIAL) && optionDialogue.disabledButtonTooltip) { + text = getEncounterText(this.scene, optionDialogue.disabledButtonTooltip, TextStyle.TOOLTIP_CONTENT); + } else { + text = getEncounterText(this.scene, optionDialogue.buttonTooltip, TextStyle.TOOLTIP_CONTENT); + } + + // Auto-color options green/blue for good/bad by looking for (+)/(-) + if (text) { + const primaryStyleString = [...text.match(new RegExp(/\[color=[^\[]*\]\[shadow=[^\[]*\]/i))!][0]; + text = text.replace(/(\(\+\)[^\(\[]*)/gi, substring => "[/color][/shadow]" + getBBCodeFrag(substring, TextStyle.SUMMARY_GREEN) + "[/color][/shadow]" + primaryStyleString); + text = text.replace(/(\(\-\)[^\(\[]*)/gi, substring => "[/color][/shadow]" + getBBCodeFrag(substring, TextStyle.SUMMARY_BLUE) + "[/color][/shadow]" + primaryStyleString); + } + + if (text) { + const tooltipTextObject = addBBCodeTextObject(this.scene, 6, 7, text, TextStyle.TOOLTIP_CONTENT, { wordWrap: { width: 600 }, fontSize: "72px" }); + this.tooltipContainer.add(tooltipTextObject); + + // Sets up the mask that hides the description text to give an illusion of scrolling + const tooltipTextMaskRect = this.scene.make.graphics({}); + tooltipTextMaskRect.setScale(6); + tooltipTextMaskRect.fillStyle(0xFFFFFF); + tooltipTextMaskRect.beginPath(); + tooltipTextMaskRect.fillRect(this.tooltipContainer.x, this.tooltipContainer.y + 188.5, 150, 32); + + const textMask = tooltipTextMaskRect.createGeometryMask(); + tooltipTextObject.setMask(textMask); + + const tooltipLineCount = Math.floor(tooltipTextObject.displayHeight / 11.2); + + if (this.tooltipScrollTween) { + this.tooltipScrollTween.remove(); + this.tooltipScrollTween = undefined; + } + + // Animates the tooltip text moving upwards + if (tooltipLineCount > 3) { + this.tooltipScrollTween = this.scene.tweens.add({ + targets: tooltipTextObject, + delay: Utils.fixedInt(1200), + loop: -1, + hold: Utils.fixedInt(1200), + duration: Utils.fixedInt((tooltipLineCount - 3) * 1200), + y: `-=${11.2 * (tooltipLineCount - 3)}` + }); + } + } + + // Dex progress indicator + if (cursorOption.hasDexProgress && !this.showDexProgress) { + this.showHideDexProgress(true); + } else if (!cursorOption.hasDexProgress) { + this.showHideDexProgress(false); + } + } + + override clear(): void { + super.clear(); + this.overrideSettings = undefined; + this.optionsContainer.setVisible(false); + this.optionsContainer.removeAll(true); + this.dexProgressContainer.setVisible(false); + this.descriptionContainer.setVisible(false); + this.tooltipContainer.setVisible(false); + // Keeps container background and pokeball + this.descriptionContainer.removeBetween(2, this.descriptionContainer.length, true); + this.getUi().getMessageHandler().clearText(); + this.eraseCursor(); + } + + private eraseCursor(): void { + if (this.cursorObj) { + this.cursorObj.destroy(); + } + this.cursorObj = undefined; + } + + /** + * Will show or hide the Dex progress icon for an option that has dex progress + * @param show - if true does show, if false does hide + */ + private showHideDexProgress(show: boolean) { + if (show && !this.showDexProgress) { + this.showDexProgress = true; + this.scene.tweens.killTweensOf(this.dexProgressContainer); + this.scene.tweens.add({ + targets: this.dexProgressContainer, + y: -63, + ease: "Sine.easeInOut", + duration: 750, + onComplete: () => { + this.dexProgressContainer.on("pointerover", () => { + (this.scene as BattleScene).ui.showTooltip("", i18next.t("mysteryEncounterMessages:affects_pokedex"), true); + }); + this.dexProgressContainer.on("pointerout", () => { + (this.scene as BattleScene).ui.hideTooltip(); + }); + } + }); + } else if (!show && this.showDexProgress) { + this.showDexProgress = false; + this.scene.tweens.killTweensOf(this.dexProgressContainer); + this.scene.tweens.add({ + targets: this.dexProgressContainer, + y: -43, + ease: "Sine.easeInOut", + duration: 750, + onComplete: () => { + this.dexProgressContainer.off("pointerover"); + this.dexProgressContainer.off("pointerout"); + } + }); + } + } +} diff --git a/src/ui/party-ui-handler.ts b/src/ui/party-ui-handler.ts index 9e025dbe086..6b6ce2aa789 100644 --- a/src/ui/party-ui-handler.ts +++ b/src/ui/party-ui-handler.ts @@ -90,7 +90,12 @@ export enum PartyUiMode { * Indicates that the party UI is open to check the team. This * type of selection can be cancelled. */ - CHECK + CHECK, + /** + * Indicates that the party UI is open to select a party member for an arbitrary effect. + * This is generally used in for Mystery Encounter or special effects that require the player to select a Pokemon + */ + SELECT } export enum PartyOption { @@ -107,6 +112,7 @@ export enum PartyOption { UNSPLICE, RELEASE, RENAME, + SELECT, SCROLL_UP = 1000, SCROLL_DOWN = 1001, FORM_CHANGE_ITEM = 2000, @@ -210,7 +216,7 @@ export default class PartyUiHandler extends MessageUiHandler { public static NoEffectMessage = i18next.t("partyUiHandler:anyEffect"); - private localizedOptions = [PartyOption.SEND_OUT, PartyOption.SUMMARY, PartyOption.CANCEL, PartyOption.APPLY, PartyOption.RELEASE, PartyOption.TEACH, PartyOption.SPLICE, PartyOption.UNSPLICE, PartyOption.REVIVE, PartyOption.TRANSFER, PartyOption.UNPAUSE_EVOLUTION, PartyOption.PASS_BATON, PartyOption.RENAME]; + private localizedOptions = [PartyOption.SEND_OUT, PartyOption.SUMMARY, PartyOption.CANCEL, PartyOption.APPLY, PartyOption.RELEASE, PartyOption.TEACH, PartyOption.SPLICE, PartyOption.UNSPLICE, PartyOption.REVIVE, PartyOption.TRANSFER, PartyOption.UNPAUSE_EVOLUTION, PartyOption.PASS_BATON, PartyOption.RENAME, PartyOption.SELECT]; constructor(scene: BattleScene) { super(scene, Mode.PARTY); @@ -349,7 +355,7 @@ export default class PartyUiHandler extends MessageUiHandler { const newPokemon = this.scene.getParty()[p]; // this next line gets all of the transferable items from pokemon [p]; it does this by getting all the held modifiers that are transferable and checking to see if they belong to pokemon [p] const getTransferrableItemsFromPokemon = (newPokemon: PlayerPokemon) => - this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier && (m as PokemonHeldItemModifier).isTransferrable && (m as PokemonHeldItemModifier).pokemonId === newPokemon.id) as PokemonHeldItemModifier[]; + this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier && (m as PokemonHeldItemModifier).isTransferable && (m as PokemonHeldItemModifier).pokemonId === newPokemon.id) as PokemonHeldItemModifier[]; // this next bit checks to see if the the selected item from the original transfer pokemon exists on the new pokemon [p]; this returns undefined if the new pokemon doesn't have the item at all, otherwise it returns the pokemonHeldItemModifier for that item const matchingModifier = newPokemon.scene.findModifier(m => m instanceof PokemonHeldItemModifier && m.pokemonId === newPokemon.id && m.matchType(getTransferrableItemsFromPokemon(pokemon)[this.transferOptionCursor])) as PokemonHeldItemModifier; const partySlot = this.partySlots.filter(m => m.getPokemon() === newPokemon)[0]; // this gets pokemon [p] for us @@ -393,7 +399,7 @@ export default class PartyUiHandler extends MessageUiHandler { || (option === PartyOption.RELEASE && this.partyUiMode === PartyUiMode.RELEASE)) { let filterResult: string | null; const getTransferrableItemsFromPokemon = (pokemon: PlayerPokemon) => - this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier && m.isTransferrable && m.pokemonId === pokemon.id) as PokemonHeldItemModifier[]; + this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier && m.isTransferable && m.pokemonId === pokemon.id) as PokemonHeldItemModifier[]; if (option !== PartyOption.TRANSFER && option !== PartyOption.SPLICE) { filterResult = (this.selectFilter as PokemonSelectFilter)(pokemon); if (filterResult === null && (option === PartyOption.SEND_OUT || option === PartyOption.PASS_BATON)) { @@ -461,8 +467,8 @@ export default class PartyUiHandler extends MessageUiHandler { } else if (option === PartyOption.UNPAUSE_EVOLUTION) { this.clearOptions(); ui.playSelect(); - pokemon.pauseEvolutions = false; - this.showText(i18next.t("partyUiHandler:unpausedEvolutions", { pokemonName: getPokemonNameWithAffix(pokemon) }), undefined, () => this.showText("", 0), null, true); + pokemon.pauseEvolutions = !pokemon.pauseEvolutions; + this.showText(i18next.t(pokemon.pauseEvolutions? "partyUiHandler:pausedEvolutions" : "partyUiHandler:unpausedEvolutions", { pokemonName: getPokemonNameWithAffix(pokemon) }), undefined, () => this.showText("", 0), null, true); } else if (option === PartyOption.UNSPLICE) { this.clearOptions(); ui.playSelect(); @@ -523,6 +529,9 @@ export default class PartyUiHandler extends MessageUiHandler { return true; } else if (option === PartyOption.CANCEL) { return this.processInput(Button.CANCEL); + } else if (option === PartyOption.SELECT) { + ui.playSelect(); + return true; } } else if (button === Button.CANCEL) { this.clearOptions(); @@ -587,7 +596,7 @@ export default class PartyUiHandler extends MessageUiHandler { if (this.partyUiMode === PartyUiMode.MODIFIER_TRANSFER && !this.transferMode) { /** Initialize item quantities for the selected Pokemon */ const itemModifiers = this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier - && m.isTransferrable && m.pokemonId === this.scene.getParty()[this.cursor].id) as PokemonHeldItemModifier[]; + && m.isTransferable && m.pokemonId === this.scene.getParty()[this.cursor].id) as PokemonHeldItemModifier[]; this.transferQuantities = itemModifiers.map(item => item.getStackCount()); this.transferQuantitiesMax = itemModifiers.map(item => item.getStackCount()); } @@ -804,7 +813,7 @@ export default class PartyUiHandler extends MessageUiHandler { const itemModifiers = this.partyUiMode === PartyUiMode.MODIFIER_TRANSFER ? this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier - && m.isTransferrable && m.pokemonId === pokemon.id) as PokemonHeldItemModifier[] + && m.isTransferable && m.pokemonId === pokemon.id) as PokemonHeldItemModifier[] : []; if (this.options.length) { @@ -872,12 +881,15 @@ export default class PartyUiHandler extends MessageUiHandler { } } break; + case PartyUiMode.SELECT: + this.options.push(PartyOption.SELECT); + break; } this.options.push(PartyOption.SUMMARY); this.options.push(PartyOption.RENAME); - if (pokemon.pauseEvolutions && pokemonEvolutions.hasOwnProperty(pokemon.species.speciesId)) { + if ((pokemonEvolutions.hasOwnProperty(pokemon.species.speciesId) || (pokemon.isFusion() && pokemon.fusionSpecies && pokemonEvolutions.hasOwnProperty(pokemon.fusionSpecies.speciesId)))) { this.options.push(PartyOption.UNPAUSE_EVOLUTION); } @@ -964,6 +976,8 @@ export default class PartyUiHandler extends MessageUiHandler { if (formChangeItemModifiers && option >= PartyOption.FORM_CHANGE_ITEM) { const modifier = formChangeItemModifiers[option - PartyOption.FORM_CHANGE_ITEM]; optionName = `${modifier.active ? i18next.t("partyUiHandler:DEACTIVATE") : i18next.t("partyUiHandler:ACTIVATE")} ${modifier.type.name}`; + } else if (option === PartyOption.UNPAUSE_EVOLUTION) { + optionName = `${pokemon.pauseEvolutions ? i18next.t("partyUiHandler:UNPAUSE_EVOLUTION") : i18next.t("partyUiHandler:PAUSE_EVOLUTION")}`; } else { if (this.localizedOptions.includes(option)) { optionName = i18next.t(`partyUiHandler:${PartyOption[option]}`); @@ -1258,7 +1272,7 @@ class PartySlot extends Phaser.GameObjects.Container { } if (this.pokemon.status) { - const statusIndicator = this.scene.add.sprite(0, 0, "statuses"); + const statusIndicator = this.scene.add.sprite(0, 0, Utils.getLocalizedSpriteKey("statuses")); statusIndicator.setFrame(StatusEffect[this.pokemon.status?.effect].toLowerCase()); statusIndicator.setOrigin(0, 0); statusIndicator.setPositionRelative(slotLevelLabel, this.slotIndex >= battlerCount ? 43 : 55, 0); diff --git a/src/ui/pokemon-info-container.ts b/src/ui/pokemon-info-container.ts index 3c54e529d43..242e59c599b 100644 --- a/src/ui/pokemon-info-container.ts +++ b/src/ui/pokemon-info-container.ts @@ -12,6 +12,7 @@ import ConfirmUiHandler from "./confirm-ui-handler"; import { StatsContainer } from "./stats-container"; import { TextStyle, addBBCodeTextObject, addTextObject, getTextColor } from "./text"; import { addWindow } from "./ui-theme"; +import { Species } from "#enums/species"; interface LanguageSetting { infoContainerTextSize: string; @@ -234,7 +235,19 @@ export default class PokemonInfoContainer extends Phaser.GameObjects.Container { this.pokemonGenderText.setVisible(false); } - if (pokemon.species.forms?.[pokemon.formIndex]?.formName) { + const formKey = (pokemon.species?.forms?.[pokemon.formIndex!]?.formKey); + const formText = Utils.capitalizeString(formKey, "-", false, false) || ""; + const speciesName = Utils.capitalizeString(Species[pokemon.species.getRootSpeciesId()], "_", true, false); + + let formName = ""; + if (pokemon.species.speciesId === Species.ARCEUS) { + formName = i18next.t(`pokemonInfo:Type.${formText?.toUpperCase()}`); + } else { + const i18key = `pokemonForm:${speciesName}${formText}`; + formName = i18next.exists(i18key) ? i18next.t(i18key) : formText; + } + + if (formName) { this.pokemonFormLabelText.setVisible(true); this.pokemonFormText.setVisible(true); const newForm = BigInt(1 << pokemon.formIndex) * DexAttr.DEFAULT_FORM; @@ -247,11 +260,10 @@ export default class PokemonInfoContainer extends Phaser.GameObjects.Container { this.pokemonFormLabelText.setShadowColor(getTextColor(TextStyle.WINDOW, true, this.scene.uiTheme)); } - const formName = pokemon.species.forms?.[pokemon.formIndex]?.formName; this.pokemonFormText.setText(formName.length > this.numCharsBeforeCutoff ? formName.substring(0, this.numCharsBeforeCutoff - 3) + "..." : formName); if (formName.length > this.numCharsBeforeCutoff) { this.pokemonFormText.setInteractive(new Phaser.Geom.Rectangle(0, 0, this.pokemonFormText.width, this.pokemonFormText.height), Phaser.Geom.Rectangle.Contains); - this.pokemonFormText.on("pointerover", () => (this.scene as BattleScene).ui.showTooltip("", pokemon.species.forms?.[pokemon.formIndex]?.formName, true)); + this.pokemonFormText.on("pointerover", () => (this.scene as BattleScene).ui.showTooltip("", formName, true)); this.pokemonFormText.on("pointerout", () => (this.scene as BattleScene).ui.hideTooltip()); } else { this.pokemonFormText.disableInteractive(); @@ -267,18 +279,13 @@ export default class PokemonInfoContainer extends Phaser.GameObjects.Container { this.pokemonAbilityText.setColor(getTextColor(abilityTextStyle, false, this.scene.uiTheme)); this.pokemonAbilityText.setShadowColor(getTextColor(abilityTextStyle, true, this.scene.uiTheme)); - /** - * If the opposing Pokemon only has 1 normal ability and is using the hidden ability it should have the same behavior - * if it had 2 normal abilities. This code checks if that is the case and uses the correct opponent Pokemon abilityIndex (2) - * for calculations so it aligns with where the hidden ability is stored in the starter data's abilityAttr (4) - */ - const opponentPokemonOneNormalAbility = (pokemon.species.getAbilityCount() === 2); - const opponentPokemonAbilityIndex = (opponentPokemonOneNormalAbility && pokemon.abilityIndex === 1) ? 2 : pokemon.abilityIndex; - const opponentPokemonAbilityAttr = 1 << opponentPokemonAbilityIndex; - const rootFormHasHiddenAbility = starterEntry.abilityAttr & opponentPokemonAbilityAttr; + const ownedAbilityAttrs = pokemon.scene.gameData.starterData[pokemon.species.getRootSpeciesId()].abilityAttr; - if (!rootFormHasHiddenAbility) { + // Check if the player owns ability for the root form + const playerOwnsThisAbility = pokemon.checkIfPlayerHasAbilityOfStarter(ownedAbilityAttrs); + + if (!playerOwnsThisAbility) { this.pokemonAbilityLabelText.setColor(getTextColor(TextStyle.SUMMARY_BLUE, false, this.scene.uiTheme)); this.pokemonAbilityLabelText.setShadowColor(getTextColor(TextStyle.SUMMARY_BLUE, true, this.scene.uiTheme)); } else { diff --git a/src/ui/run-history-ui-handler.ts b/src/ui/run-history-ui-handler.ts index 8f132a1ab1c..d983fb0b0b8 100644 --- a/src/ui/run-history-ui-handler.ts +++ b/src/ui/run-history-ui-handler.ts @@ -13,7 +13,7 @@ import { RunEntry } from "../system/game-data"; import { PlayerGender } from "#enums/player-gender"; import { TrainerVariant } from "../field/trainer"; -export type RunSelectCallback = (cursor: integer) => void; +export type RunSelectCallback = (cursor: number) => void; export const RUN_HISTORY_LIMIT: number = 25; @@ -25,15 +25,15 @@ export const RUN_HISTORY_LIMIT: number = 25; */ export default class RunHistoryUiHandler extends MessageUiHandler { + private readonly maxRows = 3; + private runSelectContainer: Phaser.GameObjects.Container; private runsContainer: Phaser.GameObjects.Container; - private runSelectMessageBox: Phaser.GameObjects.NineSlice; - private runSelectMessageBoxContainer: Phaser.GameObjects.Container; private runs: RunEntryContainer[]; private runSelectCallback: RunSelectCallback | null; - private scrollCursor: integer = 0; + private scrollCursor: number = 0; private cursorObj: Phaser.GameObjects.NineSlice | null; @@ -74,15 +74,15 @@ export default class RunHistoryUiHandler extends MessageUiHandler { this.getUi().bringToTop(this.runSelectContainer); this.runSelectContainer.setVisible(true); - this.populateRuns(this.scene); + this.populateRuns(this.scene).then(() => { + this.setScrollCursor(0); + this.setCursor(0); - this.setScrollCursor(0); - this.setCursor(0); - - //Destroys the cursor if there are no runs saved so far. - if (this.runs.length === 0) { - this.clearCursor(); - } + //Destroys the cursor if there are no runs saved so far. + if (this.runs.length === 0) { + this.clearCursor(); + } + }); return true; } @@ -122,13 +122,21 @@ export default class RunHistoryUiHandler extends MessageUiHandler { success = this.setCursor(this.cursor - 1); } else if (this.scrollCursor) { success = this.setScrollCursor(this.scrollCursor - 1); + } else if (this.runs.length > 1) { + // wrap around to the bottom + success = this.setCursor(Math.min(this.runs.length - 1, this.maxRows - 1)); + success = this.setScrollCursor(Math.max(0, this.runs.length - this.maxRows)) || success; } break; case Button.DOWN: - if (this.cursor < 2) { + if (this.cursor < Math.min(this.maxRows - 1, this.runs.length - this.scrollCursor - 1)) { success = this.setCursor(this.cursor + 1); - } else if (this.scrollCursor < this.runs.length - 3) { + } else if (this.scrollCursor < this.runs.length - this.maxRows) { success = this.setScrollCursor(this.scrollCursor + 1); + } else if (this.runs.length > 1) { + // wrap around to the top + success = this.setCursor(0); + success = this.setScrollCursor(0) || success; } break; } @@ -218,6 +226,7 @@ export default class RunHistoryUiHandler extends MessageUiHandler { override clear() { super.clear(); this.runSelectContainer.setVisible(false); + this.setScrollCursor(0); this.clearCursor(); this.runSelectCallback = null; this.clearRuns(); @@ -281,7 +290,7 @@ class RunEntryContainer extends Phaser.GameObjects.Container { const genderIndex = this.scene.gameData.gender ?? PlayerGender.UNSET; const genderStr = PlayerGender[genderIndex].toLowerCase(); // Defeats from wild Pokemon battles will show the Pokemon responsible by the text of the run result. - if (data.battleType === BattleType.WILD) { + if (data.battleType === BattleType.WILD || (data.battleType === BattleType.MYSTERY_ENCOUNTER && !data.trainer)) { const enemyContainer = this.scene.add.container(8, 5); const gameOutcomeLabel = addTextObject(this.scene, 0, 0, `${i18next.t("runHistory:defeatedWild", { context: genderStr })}`, TextStyle.WINDOW); enemyContainer.add(gameOutcomeLabel); @@ -302,7 +311,7 @@ class RunEntryContainer extends Phaser.GameObjects.Container { enemy.destroy(); }); this.add(enemyContainer); - } else if (data.battleType === BattleType.TRAINER) { // Defeats from Trainers show the trainer's title and name + } else if (data.battleType === BattleType.TRAINER || (data.battleType === BattleType.MYSTERY_ENCOUNTER && data.trainer)) { // Defeats from Trainers show the trainer's title and name const tObj = data.trainer.toTrainer(this.scene); // Because of the interesting mechanics behind rival names, the rival name and title have to be retrieved differently const RIVAL_TRAINER_ID_THRESHOLD = 375; @@ -360,7 +369,7 @@ class RunEntryContainer extends Phaser.GameObjects.Container { // The code here does not account for icon weirdness. const pokemonIconsContainer = this.scene.add.container(140, 17); - data.party.forEach((p: PokemonData, i: integer) => { + data.party.forEach((p: PokemonData, i: number) => { const iconContainer = this.scene.add.container(26 * i, 0); iconContainer.setScale(0.75); const pokemon = p.toPokemon(this.scene); diff --git a/src/ui/run-info-ui-handler.ts b/src/ui/run-info-ui-handler.ts index b7ad5f5adec..119b7bc9c4a 100644 --- a/src/ui/run-info-ui-handler.ts +++ b/src/ui/run-info-ui-handler.ts @@ -49,15 +49,11 @@ export default class RunInfoUiHandler extends UiHandler { private runResultContainer: Phaser.GameObjects.Container; private runInfoContainer: Phaser.GameObjects.Container; private partyContainer: Phaser.GameObjects.Container; - private partyHeldItemsContainer: Phaser.GameObjects.Container; private statsBgWidth: integer; - private partyContainerHeight: integer; - private partyContainerWidth: integer; private hallofFameContainer: Phaser.GameObjects.Container; private endCardContainer: Phaser.GameObjects.Container; - private partyInfo: Phaser.GameObjects.Container[]; private partyVisibility: Boolean; private modifiersModule: any; @@ -180,9 +176,9 @@ export default class RunInfoUiHandler extends UiHandler { private async parseRunResult() { const genderIndex = this.scene.gameData.gender ?? PlayerGender.UNSET; const genderStr = PlayerGender[genderIndex]; - const runResultTextStyle = this.isVictory ? TextStyle.SUMMARY : TextStyle.SUMMARY_RED; + const runResultTextStyle = this.isVictory ? TextStyle.PERFECT_IV : TextStyle.SUMMARY_RED; const runResultTitle = this.isVictory ? i18next.t("runHistory:victory") : i18next.t("runHistory:defeated", { context: genderStr }); - const runResultText = addBBCodeTextObject(this.scene, 6, 5, `${runResultTitle} - ${i18next.t("saveSlotSelectUiHandler:wave")} ${this.runInfo.waveIndex}`, runResultTextStyle, {fontSize : "65px", lineSpacing: 0.1}); + const runResultText = addTextObject(this.scene, 6, 5, `${runResultTitle} - ${i18next.t("saveSlotSelectUiHandler:wave")} ${this.runInfo.waveIndex}`, runResultTextStyle, {fontSize : "65px", lineSpacing: 0.1}); if (this.isVictory) { const hallofFameInstructionContainer = this.scene.add.container(0, 0); @@ -211,7 +207,7 @@ export default class RunInfoUiHandler extends UiHandler { if (!this.isVictory) { const enemyContainer = this.scene.add.container(0, 0); // Wild - Single and Doubles - if (this.runInfo.battleType === BattleType.WILD) { + if (this.runInfo.battleType === BattleType.WILD || (this.runInfo.battleType === BattleType.MYSTERY_ENCOUNTER && !this.runInfo.trainer)) { switch (this.runInfo.enemyParty.length) { case 1: // Wild - Singles @@ -222,7 +218,7 @@ export default class RunInfoUiHandler extends UiHandler { this.parseWildDoubleDefeat(enemyContainer); break; } - } else if (this.runInfo.battleType === BattleType.TRAINER) { + } else if (this.runInfo.battleType === BattleType.TRAINER || (this.runInfo.battleType === BattleType.MYSTERY_ENCOUNTER && this.runInfo.trainer)) { this.parseTrainerDefeat(enemyContainer); } this.runResultContainer.add(enemyContainer); @@ -381,10 +377,6 @@ export default class RunInfoUiHandler extends UiHandler { break; case GameModes.SPLICED_ENDLESS: modeText.appendText(`${i18next.t("gameMode:endlessSpliced")}`, false); - if (this.runInfo.waveIndex === this.scene.gameData.gameStats.highestEndlessWave) { - modeText.appendText(` [${i18next.t("runHistory:personalBest")}]`, false); - modeText.setTint(0xffef5c, 0x47ff69, 0x6b6bff, 0xff6969); - } break; case GameModes.CHALLENGE: modeText.appendText(`${i18next.t("gameMode:challenge")}`, false); @@ -403,17 +395,18 @@ export default class RunInfoUiHandler extends UiHandler { break; case GameModes.ENDLESS: modeText.appendText(`${i18next.t("gameMode:endless")}`, false); - // If the player achieves a personal best in Endless, the mode text will be tinted similarly to SSS luck to celebrate their achievement. - if (this.runInfo.waveIndex === this.scene.gameData.gameStats.highestEndlessWave) { - modeText.appendText(` [${i18next.t("runHistory:personalBest")}]`, false); - modeText.setTint(0xffef5c, 0x47ff69, 0x6b6bff, 0xff6969); - } break; case GameModes.CLASSIC: modeText.appendText(`${i18next.t("gameMode:classic")}`, false); break; } + // If the player achieves a personal best in Endless, the mode text will be tinted similarly to SSS luck to celebrate their achievement. + if ((this.runInfo.gameMode === GameModes.ENDLESS || this.runInfo.gameMode === GameModes.SPLICED_ENDLESS) && this.runInfo.waveIndex === this.scene.gameData.gameStats.highestEndlessWave) { + modeText.appendText(` [${i18next.t("runHistory:personalBest")}]`); + modeText.setTint(0xffef5c, 0x47ff69, 0x6b6bff, 0xff6969); + } + // Duration + Money const runInfoTextContainer = this.scene.add.container(0, 0); // Japanese is set to a greater line spacing of 35px in addBBCodeTextObject() if lineSpacing < 12. @@ -866,7 +859,7 @@ export default class RunInfoUiHandler extends UiHandler { private buttonCycleOption(button: Button) { switch (button) { case Button.CYCLE_FORM: - if (this.isVictory) { + if (this.isVictory && this.pageMode !== RunInfoUiMode.HALL_OF_FAME) { if (!this.endCardContainer || !this.endCardContainer.visible) { this.createVictorySplash(); this.endCardContainer.setVisible(true); @@ -880,7 +873,7 @@ export default class RunInfoUiHandler extends UiHandler { } break; case Button.CYCLE_SHINY: - if (this.isVictory) { + if (this.isVictory && this.pageMode !== RunInfoUiMode.ENDING_ART) { if (!this.hallofFameContainer.visible) { this.hallofFameContainer.setVisible(true); this.pageMode = RunInfoUiMode.HALL_OF_FAME; @@ -891,7 +884,7 @@ export default class RunInfoUiHandler extends UiHandler { } break; case Button.CYCLE_ABILITY: - if (this.runInfo.modifiers.length !== 0) { + if (this.runInfo.modifiers.length !== 0 && this.pageMode === RunInfoUiMode.MAIN) { if (this.partyVisibility) { this.showParty(false); } else { diff --git a/src/ui/scroll-bar.ts b/src/ui/scroll-bar.ts index e756393ae1a..5ed79d0cdad 100644 --- a/src/ui/scroll-bar.ts +++ b/src/ui/scroll-bar.ts @@ -1,36 +1,65 @@ +/** + * A vertical scrollbar element that resizes dynamically based on the current scrolling + * and number of elements that can be shown on screen + */ export class ScrollBar extends Phaser.GameObjects.Container { - private bg: Phaser.GameObjects.Image; + private bg: Phaser.GameObjects.NineSlice; private handleBody: Phaser.GameObjects.Rectangle; - private handleBottom: Phaser.GameObjects.Image; - private pages: number; - private page: number; + private handleBottom: Phaser.GameObjects.NineSlice; + private currentRow: number; + private totalRows: number; + private maxRows: number; - constructor(scene: Phaser.Scene, x: number, y: number, pages: number) { + /** + * @param scene the current scene + * @param x the scrollbar's x position (origin: top left) + * @param y the scrollbar's y position (origin: top left) + * @param width the scrollbar's width + * @param height the scrollbar's height + * @param maxRows the maximum number of rows that can be shown at once + */ + constructor(scene: Phaser.Scene, x: number, y: number, width: number, height: number, maxRows: number) { super(scene, x, y); - this.bg = scene.add.image(0, 0, "scroll_bar"); + this.maxRows = maxRows; + + const borderSize = 2; + width = Math.max(width, 4); + + this.bg = scene.add.nineslice(0, 0, "scroll_bar", undefined, width, height, borderSize, borderSize, borderSize, borderSize); this.bg.setOrigin(0, 0); this.add(this.bg); - this.handleBody = scene.add.rectangle(1, 1, 3, 4, 0xaaaaaa); + this.handleBody = scene.add.rectangle(1, 1, width - 2, 4, 0xaaaaaa); this.handleBody.setOrigin(0, 0); this.add(this.handleBody); - this.handleBottom = scene.add.image(1, 1, "scroll_bar_handle"); + this.handleBottom = scene.add.nineslice(1, 1, "scroll_bar_handle", undefined, width - 2, 2, 2, 0, 0, 0); this.handleBottom.setOrigin(0, 0); this.add(this.handleBottom); } - setPage(page: number): void { - this.page = page; - this.handleBody.y = 1 + (this.bg.displayHeight - 1 - this.handleBottom.displayHeight) / this.pages * page; + /** + * Set the current row that is displayed + * Moves the bar handle up or down accordingly + * @param scrollCursor how many times the view was scrolled down + */ + setScrollCursor(scrollCursor: number): void { + this.currentRow = scrollCursor; + this.handleBody.y = 1 + (this.bg.displayHeight - 1 - this.handleBottom.displayHeight) / this.totalRows * this.currentRow; this.handleBottom.y = this.handleBody.y + this.handleBody.displayHeight; } - setPages(pages: number): void { - this.pages = pages; - this.handleBody.height = (this.bg.displayHeight - 1 - this.handleBottom.displayHeight) * 9 / this.pages; + /** + * Set the total number of rows to display + * If it's smaller than the maximum number of rows on screen the bar will get hidden + * Otherwise the scrollbar handle gets resized based on the ratio to the maximum number of rows + * @param rows how many rows of data there are in total + */ + setTotalRows(rows: number): void { + this.totalRows = rows; + this.handleBody.height = (this.bg.displayHeight - 1 - this.handleBottom.displayHeight) * this.maxRows / this.totalRows; - this.setVisible(this.pages > 9); + this.setVisible(this.totalRows > this.maxRows); } } diff --git a/src/ui/settings/abstract-control-settings-ui-handler.ts b/src/ui/settings/abstract-control-settings-ui-handler.ts index f8dab1bf7cc..efa262bb2e9 100644 --- a/src/ui/settings/abstract-control-settings-ui-handler.ts +++ b/src/ui/settings/abstract-control-settings-ui-handler.ts @@ -1,11 +1,12 @@ -import UiHandler from "../ui-handler"; -import BattleScene from "../../battle-scene"; -import {Mode} from "../ui"; -import {InterfaceConfig} from "../../inputs-controller"; -import {addWindow} from "../ui-theme"; -import {addTextObject, TextStyle} from "../text"; -import {getIconWithSettingName} from "#app/configs/inputs/configHandler"; -import NavigationMenu, {NavigationManager} from "#app/ui/settings/navigationMenu"; +import UiHandler from "#app/ui/ui-handler"; +import BattleScene from "#app/battle-scene"; +import { Mode } from "#app/ui/ui"; +import { InterfaceConfig } from "#app/inputs-controller"; +import { addWindow } from "#app/ui/ui-theme"; +import { addTextObject, TextStyle } from "#app/ui/text"; +import { ScrollBar } from "#app/ui/scroll-bar"; +import { getIconWithSettingName } from "#app/configs/inputs/configHandler"; +import NavigationMenu, { NavigationManager } from "#app/ui/settings/navigationMenu"; import { Device } from "#enums/devices"; import { Button } from "#enums/buttons"; import i18next from "i18next"; @@ -19,7 +20,7 @@ export interface LayoutConfig { inputsIcons: InputsIcons; settingLabels: Phaser.GameObjects.Text[]; optionValueLabels: Phaser.GameObjects.Text[][]; - optionCursors: integer[]; + optionCursors: number[]; keys: string[]; bindingSettings: Array; } @@ -31,8 +32,9 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler protected optionsContainer: Phaser.GameObjects.Container; protected navigationContainer: NavigationMenu; - protected scrollCursor: integer; - protected optionCursors: integer[]; + protected scrollBar: ScrollBar; + protected scrollCursor: number; + protected optionCursors: number[]; protected cursorObj: Phaser.GameObjects.NineSlice | null; protected optionsBg: Phaser.GameObjects.NineSlice; @@ -65,7 +67,7 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler protected device: Device; abstract saveSettingToLocalStorage(setting, cursor): void; - abstract setSetting(scene: BattleScene, setting, value: integer): boolean; + abstract setSetting(scene: BattleScene, setting, value: number): boolean; /** * Constructor for the AbstractSettingsUiHandler. @@ -241,7 +243,7 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler // Calculate the total available space for placing option labels next to their setting label // We reserve space for the setting label and then distribute the remaining space evenly - const totalSpace = (300 - labelWidth) - totalWidth / 6; + const totalSpace = (297 - labelWidth) - totalWidth / 6; // Calculate the spacing between options based on the available space divided by the number of gaps between labels const optionSpacing = Math.floor(totalSpace / (optionValueLabels[s].length - 1)); @@ -269,6 +271,11 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler // Add the options container to the overall settings container to be displayed in the UI. this.settingsContainer.add(optionsContainer); } + + // Add vertical scrollbar + this.scrollBar = new ScrollBar(this.scene, this.optionsBg.width - 9, this.optionsBg.y + 5, 4, this.optionsBg.height - 11, this.rowsToDisplay); + this.settingsContainer.add(this.scrollBar); + // Add the settings container to the UI. ui.add(this.settingsContainer); @@ -413,6 +420,8 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler this.optionCursors = layout.optionCursors; this.inputsIcons = layout.inputsIcons; this.bindingSettings = layout.bindingSettings; + this.scrollBar.setTotalRows(layout.settingLabels.length); + this.scrollBar.setScrollCursor(0); // Return true indicating the layout was successfully applied. return true; @@ -538,7 +547,7 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler * @param cursor - The cursor position to set. * @returns `true` if the cursor was set successfully. */ - setCursor(cursor: integer): boolean { + setCursor(cursor: number): boolean { const ret = super.setCursor(cursor); // If the optionsContainer is not initialized, return the result from the parent class directly. if (!this.optionsContainer) { @@ -547,7 +556,8 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler // Check if the cursor object exists, if not, create it. if (!this.cursorObj) { - this.cursorObj = this.scene.add.nineslice(0, 0, "summary_moves_cursor", undefined, (this.scene.game.canvas.width / 6) - 10, 16, 1, 1, 1, 1); + const cursorWidth = (this.scene.game.canvas.width / 6) - (this.scrollBar.visible? 16 : 10); + this.cursorObj = this.scene.add.nineslice(0, 0, "summary_moves_cursor", undefined, cursorWidth, 16, 1, 1, 1, 1); this.cursorObj.setOrigin(0, 0); // Set the origin to the top-left corner. this.optionsContainer.add(this.cursorObj); // Add the cursor to the options container. } @@ -564,7 +574,7 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler * @param scrollCursor - The scroll cursor position to set. * @returns `true` if the scroll cursor was set successfully. */ - setScrollCursor(scrollCursor: integer): boolean { + setScrollCursor(scrollCursor: number): boolean { // Check if the new scroll position is the same as the current one; if so, do not update. if (scrollCursor === this.scrollCursor) { return false; @@ -572,6 +582,7 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler // Update the internal scroll cursor state this.scrollCursor = scrollCursor; + this.scrollBar.setScrollCursor(this.scrollCursor); // Apply the new scroll position to the settings UI. this.updateSettingsScroll(); @@ -590,7 +601,7 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler * @param save - Whether to save the setting to local storage. * @returns `true` if the option cursor was set successfully. */ - setOptionCursor(settingIndex: integer, cursor: integer, save?: boolean): boolean { + setOptionCursor(settingIndex: number, cursor: number, save?: boolean): boolean { // Retrieve the specific setting using the settingIndex from the settingDevice enumeration. const setting = this.setting[Object.keys(this.setting)[settingIndex]]; diff --git a/src/ui/settings/abstract-settings-ui-handler.ts b/src/ui/settings/abstract-settings-ui-handler.ts index 570377eab43..975a32127ff 100644 --- a/src/ui/settings/abstract-settings-ui-handler.ts +++ b/src/ui/settings/abstract-settings-ui-handler.ts @@ -1,12 +1,13 @@ -import BattleScene from "../../battle-scene"; -import { hasTouchscreen, isMobile } from "../../touch-controls"; -import { TextStyle, addTextObject } from "../text"; -import { Mode } from "../ui"; -import UiHandler from "../ui-handler"; -import { addWindow } from "../ui-theme"; -import {Button} from "#enums/buttons"; -import {InputsIcons} from "#app/ui/settings/abstract-control-settings-ui-handler"; -import NavigationMenu, {NavigationManager} from "#app/ui/settings/navigationMenu"; +import BattleScene from "#app/battle-scene"; +import { hasTouchscreen, isMobile } from "#app/touch-controls"; +import { TextStyle, addTextObject } from "#app/ui/text"; +import { Mode } from "#app/ui/ui"; +import UiHandler from "#app/ui/ui-handler"; +import { addWindow } from "#app/ui/ui-theme"; +import { ScrollBar } from "#app/ui/scroll-bar"; +import { Button } from "#enums/buttons"; +import { InputsIcons } from "#app/ui/settings/abstract-control-settings-ui-handler"; +import NavigationMenu, { NavigationManager } from "#app/ui/settings/navigationMenu"; import { Setting, SettingKeys, SettingType } from "#app/system/settings/settings"; import i18next from "i18next"; @@ -19,11 +20,12 @@ export default class AbstractSettingsUiHandler extends UiHandler { private optionsContainer: Phaser.GameObjects.Container; private navigationContainer: NavigationMenu; - private scrollCursor: integer; + private scrollCursor: number; + private scrollBar: ScrollBar; private optionsBg: Phaser.GameObjects.NineSlice; - private optionCursors: integer[]; + private optionCursors: number[]; private settingLabels: Phaser.GameObjects.Text[]; private optionValueLabels: Phaser.GameObjects.Text[][]; @@ -117,7 +119,7 @@ export default class AbstractSettingsUiHandler extends UiHandler { const labelWidth = Math.max(78, this.settingLabels[s].displayWidth + 8); - const totalSpace = (300 - labelWidth) - totalWidth / 6; + const totalSpace = (297 - labelWidth) - totalWidth / 6; const optionSpacing = Math.floor(totalSpace / (this.optionValueLabels[s].length - 1)); let xOffset = 0; @@ -130,7 +132,11 @@ export default class AbstractSettingsUiHandler extends UiHandler { this.optionCursors = this.settings.map(setting => setting.default); + this.scrollBar = new ScrollBar(this.scene, this.optionsBg.width - 9, this.optionsBg.y + 5, 4, this.optionsBg.height - 11, this.rowsToDisplay); + this.scrollBar.setTotalRows(this.settings.length); + this.settingsContainer.add(this.optionsBg); + this.settingsContainer.add(this.scrollBar); this.settingsContainer.add(this.navigationContainer); this.settingsContainer.add(actionsBg); this.settingsContainer.add(this.optionsContainer); @@ -186,6 +192,7 @@ export default class AbstractSettingsUiHandler extends UiHandler { this.settingsContainer.setVisible(true); this.setCursor(0); + this.setScrollCursor(0); this.getUi().moveTo(this.settingsContainer, this.getUi().length - 1); @@ -301,11 +308,12 @@ export default class AbstractSettingsUiHandler extends UiHandler { * @param cursor - The cursor position to set. * @returns `true` if the cursor was set successfully. */ - setCursor(cursor: integer): boolean { + setCursor(cursor: number): boolean { const ret = super.setCursor(cursor); if (!this.cursorObj) { - this.cursorObj = this.scene.add.nineslice(0, 0, "summary_moves_cursor", undefined, (this.scene.game.canvas.width / 6) - 10, 16, 1, 1, 1, 1); + const cursorWidth = (this.scene.game.canvas.width / 6) - (this.scrollBar.visible? 16 : 10); + this.cursorObj = this.scene.add.nineslice(0, 0, "summary_moves_cursor", undefined, cursorWidth, 16, 1, 1, 1, 1); this.cursorObj.setOrigin(0, 0); this.optionsContainer.add(this.cursorObj); } @@ -323,7 +331,7 @@ export default class AbstractSettingsUiHandler extends UiHandler { * @param save - Whether to save the setting to local storage. * @returns `true` if the option cursor was set successfully. */ - setOptionCursor(settingIndex: integer, cursor: integer, save?: boolean): boolean { + setOptionCursor(settingIndex: number, cursor: number, save?: boolean): boolean { const setting = this.settings[settingIndex]; if (setting.key === SettingKeys.Touch_Controls && cursor && hasTouchscreen() && isMobile()) { @@ -359,12 +367,13 @@ export default class AbstractSettingsUiHandler extends UiHandler { * @param scrollCursor - The scroll cursor position to set. * @returns `true` if the scroll cursor was set successfully. */ - setScrollCursor(scrollCursor: integer): boolean { + setScrollCursor(scrollCursor: number): boolean { if (scrollCursor === this.scrollCursor) { return false; } this.scrollCursor = scrollCursor; + this.scrollBar.setScrollCursor(this.scrollCursor); this.updateSettingsScroll(); @@ -394,6 +403,7 @@ export default class AbstractSettingsUiHandler extends UiHandler { clear() { super.clear(); this.settingsContainer.setVisible(false); + this.setScrollCursor(0); this.eraseCursor(); this.getUi().bgmBar.toggleBgmBar(this.scene.showBgmBar); if (this.reloadRequired) { diff --git a/src/ui/starter-select-ui-handler.ts b/src/ui/starter-select-ui-handler.ts index d99c25bc612..5ef26d1ba88 100644 --- a/src/ui/starter-select-ui-handler.ts +++ b/src/ui/starter-select-ui-handler.ts @@ -13,7 +13,7 @@ import { allMoves } from "../data/move"; import { Nature, getNatureName } from "../data/nature"; import { pokemonFormChanges } from "../data/pokemon-forms"; import { LevelMoves, pokemonFormLevelMoves, pokemonSpeciesLevelMoves } from "../data/pokemon-level-moves"; -import PokemonSpecies, { allSpecies, getPokemonSpeciesForm, getStarterValueFriendshipCap, speciesStarters, starterPassiveAbilities, getPokerusStarters } from "../data/pokemon-species"; +import PokemonSpecies, { allSpecies, getPokemonSpeciesForm, getStarterValueFriendshipCap, speciesStarters, starterPassiveAbilities, POKERUS_STARTER_COUNT, getPokerusStarters } from "../data/pokemon-species"; import { Type } from "../data/type"; import { GameModes } from "../game-mode"; import { AbilityAttr, DexAttr, DexAttrProps, DexEntry, StarterMoveset, StarterAttributes, StarterPreferences, StarterPrefs } from "../system/game-data"; @@ -83,7 +83,7 @@ const languageSettings: { [key: string]: LanguageSetting } = { }, "fr":{ starterInfoTextSize: "54px", - instructionTextSize: "35px", + instructionTextSize: "38px", }, "it":{ starterInfoTextSize: "56px", @@ -627,11 +627,11 @@ export default class StarterSelectUiHandler extends MessageUiHandler { const starterBoxContainer = this.scene.add.container(speciesContainerX + 6, 9); //115 - this.starterSelectScrollBar = new ScrollBar(this.scene, 161, 12, 0); + this.starterSelectScrollBar = new ScrollBar(this.scene, 161, 12, 5, starterContainerWindow.height - 6, 9); starterBoxContainer.add(this.starterSelectScrollBar); - this.pokerusCursorObjs = new Array(3).fill(null).map(() => { + this.pokerusCursorObjs = new Array(POKERUS_STARTER_COUNT).fill(null).map(() => { const cursorObj = this.scene.add.image(0, 0, "select_cursor_pokerus"); cursorObj.setVisible(false); cursorObj.setOrigin(0, 0); @@ -760,7 +760,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.pokemonCaughtHatchedContainer.add(this.pokemonHatchedCountText); this.pokemonMovesContainer = this.scene.add.container(102, 16); - this.pokemonMovesContainer.setScale(0.5); + this.pokemonMovesContainer.setScale(0.375); for (let m = 0; m < 4; m++) { const moveContainer = this.scene.add.container(0, 14 * m); @@ -894,6 +894,9 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.message.setOrigin(0, 0); this.starterSelectMessageBoxContainer.add(this.message); + // arrow icon for the message box + this.initPromptSprite(this.starterSelectMessageBoxContainer); + this.statsContainer = new StatsContainer(this.scene, 6, 16); this.scene.add.existing(this.statsContainer); @@ -911,7 +914,11 @@ export default class StarterSelectUiHandler extends MessageUiHandler { y: this.scene.game.canvas.height / 6 - MoveInfoOverlay.getHeight(overlayScale) - 29, }); this.starterSelectContainer.add(this.moveInfoOverlay); + + // Filter bar sits above everything, except the tutorial overlay and message box this.starterSelectContainer.bringToTop(this.filterBarContainer); + this.initTutorialOverlay(this.starterSelectContainer); + this.starterSelectContainer.bringToTop(this.starterSelectMessageBoxContainer); this.scene.eventTarget.addEventListener(BattleSceneEventType.CANDY_UPGRADE_NOTIFICATION_CHANGED, (e) => this.onCandyUpgradeDisplayChanged(e)); @@ -995,15 +1002,14 @@ export default class StarterSelectUiHandler extends MessageUiHandler { delete starterAttributes.shiny; } - if (starterAttributes.variant !== undefined && !isNaN(starterAttributes.variant)) { + if (starterAttributes.variant !== undefined) { const unlockedVariants = [ - hasNonShiny, hasShiny && caughtAttr & DexAttr.DEFAULT_VARIANT, hasShiny && caughtAttr & DexAttr.VARIANT_2, hasShiny && caughtAttr & DexAttr.VARIANT_3 ]; - if (!unlockedVariants[starterAttributes.variant + 1]) { // add 1 as -1 = non-shiny - // requested variant wasn't unlocked, purging setting + if (isNaN(starterAttributes.variant) || starterAttributes.variant < 0 || !unlockedVariants[starterAttributes.variant]) { + // variant value is invalid or requested variant wasn't unlocked, purging setting delete starterAttributes.variant; } } @@ -2534,8 +2540,8 @@ export default class StarterSelectUiHandler extends MessageUiHandler { } }); - this.starterSelectScrollBar.setPages(Math.max(Math.ceil(this.filteredStarterContainers.length / 9), 1)); - this.starterSelectScrollBar.setPage(0); + this.starterSelectScrollBar.setTotalRows(Math.max(Math.ceil(this.filteredStarterContainers.length / 9), 1)); + this.starterSelectScrollBar.setScrollCursor(0); // sort const sort = this.filterBar.getVals(DropDownColumn.SORT)[0]; @@ -2570,7 +2576,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { const onScreenFirstIndex = this.scrollCursor * maxColumns; const onScreenLastIndex = Math.min(this.filteredStarterContainers.length - 1, onScreenFirstIndex + maxRows * maxColumns -1); - this.starterSelectScrollBar.setPage(this.scrollCursor); + this.starterSelectScrollBar.setScrollCursor(this.scrollCursor); let pokerusCursorIndex = 0; this.filteredStarterContainers.forEach((container, i) => { diff --git a/src/ui/stats-container.ts b/src/ui/stats-container.ts index c6e0ea3a71c..06dd6e7c035 100644 --- a/src/ui/stats-container.ts +++ b/src/ui/stats-container.ts @@ -4,12 +4,15 @@ import { TextStyle, addBBCodeTextObject, addTextObject, getTextColor } from "./t import { PERMANENT_STATS, getStatKey } from "#app/enums/stat"; import i18next from "i18next"; + const ivChartSize = 24; const ivChartStatCoordMultipliers = [[0, -1], [0.825, -0.5], [0.825, 0.5], [-0.825, -0.5], [-0.825, 0.5], [0, 1]]; const speedLabelOffset = -3; const sideLabelOffset = 1; const ivLabelOffset = [0, sideLabelOffset, -sideLabelOffset, sideLabelOffset, -sideLabelOffset, speedLabelOffset]; +const ivChartLabelyOffset= [0, 5, 0, 5, 0, 0]; // doing this so attack does not overlap with (+N) const ivChartStatIndexes = [0, 1, 2, 5, 4, 3]; // swap special attack and speed + const defaultIvChartData = new Array(12).fill(null).map(() => 0); export class StatsContainer extends Phaser.GameObjects.Container { @@ -29,7 +32,6 @@ export class StatsContainer extends Phaser.GameObjects.Container { setup() { this.setName("stats"); const ivChartBgData = new Array(6).fill(null).map((_, i: integer) => [ ivChartSize * ivChartStatCoordMultipliers[ivChartStatIndexes[i]][0], ivChartSize * ivChartStatCoordMultipliers[ivChartStatIndexes[i]][1] ] ).flat(); - const ivChartBg = this.scene.add.polygon(48, 44, ivChartBgData, 0xd8e0f0, 0.625); ivChartBg.setOrigin(0, 0); @@ -55,12 +57,19 @@ export class StatsContainer extends Phaser.GameObjects.Container { this.ivStatValueTexts = []; for (const s of PERMANENT_STATS) { - const statLabel = addTextObject(this.scene, ivChartBg.x + (ivChartSize) * ivChartStatCoordMultipliers[s][0] * 1.325, ivChartBg.y + (ivChartSize) * ivChartStatCoordMultipliers[s][1] * 1.325 - 4 + ivLabelOffset[s], i18next.t(getStatKey(s)), TextStyle.TOOLTIP_CONTENT); + const statLabel = addTextObject( + this.scene, + ivChartBg.x + (ivChartSize) * ivChartStatCoordMultipliers[s][0] * 1.325 + (this.showDiff ? 0 : ivLabelOffset[s]), + ivChartBg.y + (ivChartSize) * ivChartStatCoordMultipliers[s][1] * 1.325 - 4 + (this.showDiff ? 0 : ivChartLabelyOffset[s]), + i18next.t(getStatKey(s)), + TextStyle.TOOLTIP_CONTENT + ); statLabel.setOrigin(0.5); - this.ivStatValueTexts[s] = addBBCodeTextObject(this.scene, statLabel.x, statLabel.y + 8, "0", TextStyle.TOOLTIP_CONTENT); + this.ivStatValueTexts[s] = addBBCodeTextObject(this.scene, statLabel.x - (this.showDiff ? 0 : ivLabelOffset[s]), statLabel.y + 8, "0", TextStyle.TOOLTIP_CONTENT); this.ivStatValueTexts[s].setOrigin(0.5); + this.add(statLabel); this.add(this.ivStatValueTexts[s]); } diff --git a/src/ui/summary-ui-handler.ts b/src/ui/summary-ui-handler.ts index 8ae72f08edd..e93fa0713c0 100644 --- a/src/ui/summary-ui-handler.ts +++ b/src/ui/summary-ui-handler.ts @@ -214,7 +214,7 @@ export default class SummaryUiHandler extends UiHandler { this.statusContainer.add(statusLabel); - this.status = this.scene.add.sprite(91, 4, "statuses"); + this.status = this.scene.add.sprite(91, 4, Utils.getLocalizedSpriteKey("statuses")); this.status.setOrigin(0.5, 0); this.statusContainer.add(this.status); @@ -701,6 +701,7 @@ export default class SummaryUiHandler extends UiHandler { const profileContainer = this.scene.add.container(0, -pageBg.height); pageContainer.add(profileContainer); + // TODO: should add field for original trainer name to Pokemon object, to support gift/traded Pokemon from MEs const trainerText = addBBCodeTextObject(this.scene, 7, 12, `${i18next.t("pokemonSummary:ot")}/${getBBCodeFrag(loggedInUser?.username || i18next.t("pokemonSummary:unknown"), this.scene.gameData.gender === PlayerGender.FEMALE ? TextStyle.SUMMARY_PINK : TextStyle.SUMMARY_BLUE)}`, TextStyle.SUMMARY_ALT); trainerText.setOrigin(0, 0); profileContainer.add(trainerText); @@ -824,6 +825,7 @@ export default class SummaryUiHandler extends UiHandler { metFragment: i18next.t(`pokemonSummary:metFragment.${this.pokemon?.metBiome === -1? "apparently": "normal"}`, { biome: `${getBBCodeFrag(getBiomeName(this.pokemon?.metBiome!), TextStyle.SUMMARY_RED)}${closeFragment}`, // TODO: is this bang correct? level: `${getBBCodeFrag(this.pokemon?.metLevel.toString()!, TextStyle.SUMMARY_RED)}${closeFragment}`, // TODO: is this bang correct? + wave: `${getBBCodeFrag((this.pokemon?.metWave ? this.pokemon.metWave.toString()! : i18next.t("pokemonSummary:unknownTrainer")), TextStyle.SUMMARY_RED)}${closeFragment}`, }), natureFragment: i18next.t(`pokemonSummary:natureFragment.${rawNature}`, { nature: nature }) }); diff --git a/src/ui/target-select-ui-handler.ts b/src/ui/target-select-ui-handler.ts index 6ca580dc2b2..3cdda984d3c 100644 --- a/src/ui/target-select-ui-handler.ts +++ b/src/ui/target-select-ui-handler.ts @@ -8,6 +8,7 @@ import {Button} from "#enums/buttons"; import { Moves } from "#enums/moves"; import Pokemon from "#app/field/pokemon"; import { ModifierBar } from "#app/modifier/modifier"; +import { SubstituteTag } from "#app/data/battler-tags"; export type TargetSelectCallback = (targets: BattlerIndex[]) => void; @@ -111,7 +112,7 @@ export default class TargetSelectUiHandler extends UiHandler { if (this.targetFlashTween) { this.targetFlashTween.stop(); for (const pokemon of multipleTargets) { - pokemon.setAlpha(1); + pokemon.setAlpha(!!pokemon.getTag(SubstituteTag) ? 0.5 : 1); this.highlightItems(pokemon.id, 1); } } @@ -162,7 +163,7 @@ export default class TargetSelectUiHandler extends UiHandler { } for (const pokemon of this.targetsHighlighted) { - pokemon.setAlpha(1); + pokemon.setAlpha(!!pokemon.getTag(SubstituteTag) ? 0.5 : 1); this.highlightItems(pokemon.id, 1); } diff --git a/src/ui/text.ts b/src/ui/text.ts index 99a0436bba3..58b6343144a 100644 --- a/src/ui/text.ts +++ b/src/ui/text.ts @@ -226,6 +226,34 @@ export function getBBCodeFrag(content: string, textStyle: TextStyle, uiTheme: Ui return `[color=${getTextColor(textStyle, false, uiTheme)}][shadow=${getTextColor(textStyle, true, uiTheme)}]${content}`; } +/** + * Should only be used with BBCodeText (see {@linkcode addBBCodeTextObject()}) + * This does NOT work with UI showText() or showDialogue() methods. + * Method will do pattern match/replace and apply BBCode color/shadow styling to substrings within the content: + * @[]{} + * + * Example: passing a content string of "@[SUMMARY_BLUE]{blue text} primaryStyle text @[SUMMARY_RED]{red text}" will result in: + * - "blue text" with TextStyle.SUMMARY_BLUE applied + * - " primaryStyle text " with primaryStyle TextStyle applied + * - "red text" with TextStyle.SUMMARY_RED applied + * @param content string with styling that need to be applied for BBCodeTextObject + * @param primaryStyle Primary style is required in order to escape BBCode styling properly. + * @param uiTheme + */ +export function getTextWithColors(content: string, primaryStyle: TextStyle, uiTheme: UiTheme = UiTheme.DEFAULT): string { + // Apply primary styling before anything else + let text = getBBCodeFrag(content, primaryStyle, uiTheme) + "[/color][/shadow]"; + const primaryStyleString = [...text.match(new RegExp(/\[color=[^\[]*\]\[shadow=[^\[]*\]/i))!][0]; + + // Set custom colors + text = text.replace(/@\[([^{]*)\]{([^}]*)}/gi, (substring, textStyle: string, textToColor: string) => { + return "[/color][/shadow]" + getBBCodeFrag(textToColor, TextStyle[textStyle], uiTheme) + "[/color][/shadow]" + primaryStyleString; + }); + + // Remove extra style block at the end + return text.replace(/\[color=[^\[]*\]\[shadow=[^\[]*\]\[\/color\]\[\/shadow\]/gi, ""); +} + export function getTextColor(textStyle: TextStyle, shadow?: boolean, uiTheme: UiTheme = UiTheme.DEFAULT): string { const isLegacyTheme = uiTheme === UiTheme.LEGACY; switch (textStyle) { diff --git a/src/ui/title-ui-handler.ts b/src/ui/title-ui-handler.ts index 67a4f7260e6..4087b397ff7 100644 --- a/src/ui/title-ui-handler.ts +++ b/src/ui/title-ui-handler.ts @@ -3,11 +3,14 @@ import OptionSelectUiHandler from "./settings/option-select-ui-handler"; import { Mode } from "./ui"; import * as Utils from "../utils"; import { TextStyle, addTextObject, getTextStyleOptions } from "./text"; -import { getBattleCountSplashMessage, getSplashMessages } from "../data/splash-messages"; +import { getSplashMessages } from "../data/splash-messages"; import i18next from "i18next"; import { TimedEventDisplay } from "#app/timed-event-manager"; export default class TitleUiHandler extends OptionSelectUiHandler { + /** If the stats can not be retrieved, use this fallback value */ + private static readonly BATTLES_WON_FALLBACK: number = -99999999; + private titleContainer: Phaser.GameObjects.Container; private playerCountLabel: Phaser.GameObjects.Text; private splashMessage: string; @@ -72,8 +75,8 @@ export default class TitleUiHandler extends OptionSelectUiHandler { .then(request => request.json()) .then(stats => { this.playerCountLabel.setText(`${stats.playerCount} ${i18next.t("menu:playersOnline")}`); - if (this.splashMessage === getBattleCountSplashMessage()) { - this.splashMessageText.setText(getBattleCountSplashMessage().replace("{COUNT}", stats.battleCount.toLocaleString("en-US"))); + if (this.splashMessage === "splashMessages:battlesWon") { + this.splashMessageText.setText(i18next.t(this.splashMessage, { count: stats.battleCount })); } }) .catch(err => { @@ -86,7 +89,7 @@ export default class TitleUiHandler extends OptionSelectUiHandler { if (ret) { this.splashMessage = Utils.randItem(getSplashMessages()); - this.splashMessageText.setText(this.splashMessage.replace("{COUNT}", "?")); + this.splashMessageText.setText(i18next.t(this.splashMessage, { count: TitleUiHandler.BATTLES_WON_FALLBACK })); const ui = this.getUi(); diff --git a/src/ui/ui.ts b/src/ui/ui.ts index 82b3ee6b4fa..7e00c87cc5f 100644 --- a/src/ui/ui.ts +++ b/src/ui/ui.ts @@ -53,6 +53,7 @@ import EggSummaryUiHandler from "./egg-summary-ui-handler"; import TestDialogueUiHandler from "#app/ui/test-dialogue-ui-handler"; import AutoCompleteUiHandler from "./autocomplete-ui-handler"; import { Device } from "#enums/devices"; +import MysteryEncounterUiHandler from "./mystery-encounter-ui-handler"; export enum Mode { MESSAGE, @@ -97,6 +98,7 @@ export enum Mode { TEST_DIALOGUE, AUTO_COMPLETE, ADMIN, + MYSTERY_ENCOUNTER } const transitionModes = [ @@ -137,6 +139,8 @@ const noTransitionModes = [ Mode.TEST_DIALOGUE, Mode.AUTO_COMPLETE, Mode.ADMIN, + Mode.MYSTERY_ENCOUNTER, + Mode.RUN_INFO ]; export default class UI extends Phaser.GameObjects.Container { @@ -204,6 +208,7 @@ export default class UI extends Phaser.GameObjects.Container { new TestDialogueUiHandler(scene, Mode.TEST_DIALOGUE), new AutoCompleteUiHandler(scene), new AdminUiHandler(scene), + new MysteryEncounterUiHandler(scene), ]; } diff --git a/src/utils.ts b/src/utils.ts index 7decf9bb4c0..e526d086316 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,5 @@ import { MoneyFormat } from "#enums/money-format"; +import { Moves } from "#enums/moves"; import i18next from "i18next"; export const MissingTextureKey = "__MISSING"; @@ -587,6 +588,14 @@ export function isNullOrUndefined(object: any): boolean { return null === object || undefined === object; } +/** + * Capitalizes the first letter of a string + * @param str + */ +export function capitalizeFirstLetter(str: string) { + return str.charAt(0).toUpperCase() + str.slice(1); +} + /** * This function is used in the context of a Pokémon battle game to calculate the actual integer damage value from a float result. * Many damage calculation formulas involve various parameters and result in float values. @@ -620,3 +629,12 @@ export function getLocalizedSpriteKey(baseKey: string) { export function isBetween(num: number, min: number, max: number): boolean { return num >= min && num <= max; } + +/** + * Helper method to return the animation filename for a given move + * + * @param move the move for which the animation filename is needed + */ +export function animationFileName(move: Moves): string { + return Moves[move].toLowerCase().replace(/\_/g, "-"); +} diff --git a/vite.config.ts b/vite.config.ts index 1fd85e2572f..946315c4b7b 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -31,6 +31,7 @@ export default defineConfig(({mode}) => { return ({ ...defaultConfig, + base: '', esbuild: { pure: mode === 'production' ? ['console.log'] : [], keepNames: true, diff --git a/vitest.config.ts b/vitest.config.ts index 54462675704..9f9245687a1 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -4,6 +4,7 @@ import { defaultConfig } from "./vite.config"; export default defineProject(({ mode }) => ({ ...defaultConfig, test: { + testTimeout: 20000, setupFiles: ["./src/test/fontFace.setup.ts", "./src/test/vitest.setup.ts"], server: { deps: {