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/865.json b/public/images/pokemon/exp/865.json index 16dcd426cb1..b204a053406 100644 --- a/public/images/pokemon/exp/865.json +++ b/public/images/pokemon/exp/865.json @@ -4,639 +4,1143 @@ "image": "865.png", "format": "RGBA8888", "size": { - "w": 400, - "h": 400 + "w": 285, + "h": 285 }, "scale": 1, "frames": [ - { - "filename": "0001.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 80, - "h": 111 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 80, - "h": 111 - }, - "frame": { - "x": 0, - "y": 0, - "w": 80, - "h": 111 - } - }, - { - "filename": "0011.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 80, - "h": 111 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 80, - "h": 111 - }, - "frame": { - "x": 0, - "y": 0, - "w": 80, - "h": 111 - } - }, - { - "filename": "0021.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 80, - "h": 111 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 80, - "h": 111 - }, - "frame": { - "x": 0, - "y": 0, - "w": 80, - "h": 111 - } - }, - { - "filename": "0002.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 80, - "h": 111 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 80, - "h": 111 - }, - "frame": { - "x": 0, - "y": 111, - "w": 80, - "h": 111 - } - }, - { - "filename": "0003.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 80, - "h": 111 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 80, - "h": 111 - }, - "frame": { - "x": 0, - "y": 222, - "w": 80, - "h": 111 - } - }, - { - "filename": "0013.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 80, - "h": 111 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 80, - "h": 111 - }, - "frame": { - "x": 0, - "y": 222, - "w": 80, - "h": 111 - } - }, { "filename": "0004.png", "rotated": false, - "trimmed": false, + "trimmed": true, "sourceSize": { - "w": 80, - "h": 111 + "w": 82, + "h": 96 }, "spriteSourceSize": { - "x": 0, + "x": 5, "y": 0, - "w": 80, - "h": 111 + "w": 77, + "h": 96 }, "frame": { - "x": 80, - "y": 0, - "w": 80, - "h": 111 - } - }, - { - "filename": "0014.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 80, - "h": 111 - }, - "spriteSourceSize": { "x": 0, "y": 0, - "w": 80, - "h": 111 - }, - "frame": { - "x": 80, - "y": 0, - "w": 80, - "h": 111 - } - }, - { - "filename": "0024.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 80, - "h": 111 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 80, - "h": 111 - }, - "frame": { - "x": 80, - "y": 0, - "w": 80, - "h": 111 - } - }, - { - "filename": "0005.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 80, - "h": 111 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 80, - "h": 111 - }, - "frame": { - "x": 160, - "y": 0, - "w": 80, - "h": 111 - } - }, - { - "filename": "0006.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 80, - "h": 111 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 80, - "h": 111 - }, - "frame": { - "x": 240, - "y": 0, - "w": 80, - "h": 111 - } - }, - { - "filename": "0007.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 80, - "h": 111 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 80, - "h": 111 - }, - "frame": { - "x": 320, - "y": 0, - "w": 80, - "h": 111 - } - }, - { - "filename": "0017.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 80, - "h": 111 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 80, - "h": 111 - }, - "frame": { - "x": 320, - "y": 0, - "w": 80, - "h": 111 - } - }, - { - "filename": "0027.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 80, - "h": 111 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 80, - "h": 111 - }, - "frame": { - "x": 320, - "y": 0, - "w": 80, - "h": 111 - } - }, - { - "filename": "0008.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 80, - "h": 111 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 80, - "h": 111 - }, - "frame": { - "x": 80, - "y": 111, - "w": 80, - "h": 111 - } - }, - { - "filename": "0018.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 80, - "h": 111 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 80, - "h": 111 - }, - "frame": { - "x": 80, - "y": 111, - "w": 80, - "h": 111 - } - }, - { - "filename": "0028.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 80, - "h": 111 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 80, - "h": 111 - }, - "frame": { - "x": 80, - "y": 111, - "w": 80, - "h": 111 - } - }, - { - "filename": "0009.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 80, - "h": 111 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 80, - "h": 111 - }, - "frame": { - "x": 80, - "y": 222, - "w": 80, - "h": 111 - } - }, - { - "filename": "0019.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 80, - "h": 111 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 80, - "h": 111 - }, - "frame": { - "x": 80, - "y": 222, - "w": 80, - "h": 111 - } - }, - { - "filename": "0029.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 80, - "h": 111 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 80, - "h": 111 - }, - "frame": { - "x": 80, - "y": 222, - "w": 80, - "h": 111 - } - }, - { - "filename": "0010.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 80, - "h": 111 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 80, - "h": 111 - }, - "frame": { - "x": 160, - "y": 111, - "w": 80, - "h": 111 - } - }, - { - "filename": "0020.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 80, - "h": 111 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 80, - "h": 111 - }, - "frame": { - "x": 160, - "y": 111, - "w": 80, - "h": 111 - } - }, - { - "filename": "0030.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 80, - "h": 111 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 80, - "h": 111 - }, - "frame": { - "x": 160, - "y": 111, - "w": 80, - "h": 111 - } - }, - { - "filename": "0012.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 80, - "h": 111 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 80, - "h": 111 - }, - "frame": { - "x": 160, - "y": 222, - "w": 80, - "h": 111 - } - }, - { - "filename": "0022.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 80, - "h": 111 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 80, - "h": 111 - }, - "frame": { - "x": 160, - "y": 222, - "w": 80, - "h": 111 - } - }, - { - "filename": "0015.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 80, - "h": 111 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 80, - "h": 111 - }, - "frame": { - "x": 240, - "y": 111, - "w": 80, - "h": 111 - } - }, - { - "filename": "0025.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 80, - "h": 111 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 80, - "h": 111 - }, - "frame": { - "x": 240, - "y": 111, - "w": 80, - "h": 111 + "w": 77, + "h": 96 } }, { "filename": "0016.png", "rotated": false, - "trimmed": false, + "trimmed": true, "sourceSize": { - "w": 80, - "h": 111 + "w": 82, + "h": 96 }, "spriteSourceSize": { - "x": 0, + "x": 5, "y": 0, - "w": 80, - "h": 111 + "w": 77, + "h": 96 }, "frame": { - "x": 320, - "y": 111, - "w": 80, - "h": 111 + "x": 0, + "y": 0, + "w": 77, + "h": 96 } }, { - "filename": "0026.png", + "filename": "0022.png", "rotated": false, - "trimmed": false, + "trimmed": true, "sourceSize": { - "w": 80, - "h": 111 + "w": 82, + "h": 96 }, "spriteSourceSize": { - "x": 0, + "x": 5, "y": 0, - "w": 80, - "h": 111 + "w": 77, + "h": 96 }, "frame": { - "x": 320, - "y": 111, - "w": 80, - "h": 111 + "x": 0, + "y": 0, + "w": 77, + "h": 96 + } + }, + { + "filename": "0040.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 82, + "h": 96 + }, + "spriteSourceSize": { + "x": 5, + "y": 0, + "w": 77, + "h": 96 + }, + "frame": { + "x": 0, + "y": 0, + "w": 77, + "h": 96 + } + }, + { + "filename": "0052.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 82, + "h": 96 + }, + "spriteSourceSize": { + "x": 5, + "y": 0, + "w": 77, + "h": 96 + }, + "frame": { + "x": 0, + "y": 0, + "w": 77, + "h": 96 + } + }, + { + "filename": "0034.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 82, + "h": 96 + }, + "spriteSourceSize": { + "x": 5, + "y": 0, + "w": 77, + "h": 96 + }, + "frame": { + "x": 0, + "y": 96, + "w": 77, + "h": 96 + } + }, + { + "filename": "0002.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 82, + "h": 96 + }, + "spriteSourceSize": { + "x": 1, + "y": 4, + "w": 79, + "h": 92 + }, + "frame": { + "x": 0, + "y": 192, + "w": 79, + "h": 92 + } + }, + { + "filename": "0018.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 82, + "h": 96 + }, + "spriteSourceSize": { + "x": 1, + "y": 4, + "w": 79, + "h": 92 + }, + "frame": { + "x": 0, + "y": 192, + "w": 79, + "h": 92 + } + }, + { + "filename": "0020.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 82, + "h": 96 + }, + "spriteSourceSize": { + "x": 1, + "y": 4, + "w": 79, + "h": 92 + }, + "frame": { + "x": 0, + "y": 192, + "w": 79, + "h": 92 + } + }, + { + "filename": "0036.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 82, + "h": 96 + }, + "spriteSourceSize": { + "x": 1, + "y": 4, + "w": 79, + "h": 92 + }, + "frame": { + "x": 0, + "y": 192, + "w": 79, + "h": 92 + } + }, + { + "filename": "0038.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 82, + "h": 96 + }, + "spriteSourceSize": { + "x": 1, + "y": 4, + "w": 79, + "h": 92 + }, + "frame": { + "x": 0, + "y": 192, + "w": 79, + "h": 92 + } + }, + { + "filename": "0054.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 82, + "h": 96 + }, + "spriteSourceSize": { + "x": 1, + "y": 4, + "w": 79, + "h": 92 + }, + "frame": { + "x": 0, + "y": 192, + "w": 79, + "h": 92 + } + }, + { + "filename": "0005.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 82, + "h": 96 + }, + "spriteSourceSize": { + "x": 10, + "y": 0, + "w": 72, + "h": 96 + }, + "frame": { + "x": 77, + "y": 0, + "w": 72, + "h": 96 + } + }, + { + "filename": "0015.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 82, + "h": 96 + }, + "spriteSourceSize": { + "x": 10, + "y": 0, + "w": 72, + "h": 96 + }, + "frame": { + "x": 77, + "y": 0, + "w": 72, + "h": 96 } }, { "filename": "0023.png", "rotated": false, - "trimmed": false, + "trimmed": true, "sourceSize": { - "w": 80, - "h": 111 + "w": 82, + "h": 96 + }, + "spriteSourceSize": { + "x": 10, + "y": 0, + "w": 72, + "h": 96 + }, + "frame": { + "x": 77, + "y": 0, + "w": 72, + "h": 96 + } + }, + { + "filename": "0033.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 82, + "h": 96 + }, + "spriteSourceSize": { + "x": 10, + "y": 0, + "w": 72, + "h": 96 + }, + "frame": { + "x": 77, + "y": 0, + "w": 72, + "h": 96 + } + }, + { + "filename": "0041.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 82, + "h": 96 + }, + "spriteSourceSize": { + "x": 10, + "y": 0, + "w": 72, + "h": 96 + }, + "frame": { + "x": 77, + "y": 0, + "w": 72, + "h": 96 + } + }, + { + "filename": "0051.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 82, + "h": 96 + }, + "spriteSourceSize": { + "x": 10, + "y": 0, + "w": 72, + "h": 96 + }, + "frame": { + "x": 77, + "y": 0, + "w": 72, + "h": 96 + } + }, + { + "filename": "0006.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 82, + "h": 96 + }, + "spriteSourceSize": { + "x": 19, + "y": 0, + "w": 63, + "h": 96 + }, + "frame": { + "x": 77, + "y": 96, + "w": 63, + "h": 96 + } + }, + { + "filename": "0014.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 82, + "h": 96 + }, + "spriteSourceSize": { + "x": 19, + "y": 0, + "w": 63, + "h": 96 + }, + "frame": { + "x": 77, + "y": 96, + "w": 63, + "h": 96 + } + }, + { + "filename": "0024.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 82, + "h": 96 + }, + "spriteSourceSize": { + "x": 19, + "y": 0, + "w": 63, + "h": 96 + }, + "frame": { + "x": 77, + "y": 96, + "w": 63, + "h": 96 + } + }, + { + "filename": "0032.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 82, + "h": 96 + }, + "spriteSourceSize": { + "x": 19, + "y": 0, + "w": 63, + "h": 96 + }, + "frame": { + "x": 77, + "y": 96, + "w": 63, + "h": 96 + } + }, + { + "filename": "0042.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 82, + "h": 96 + }, + "spriteSourceSize": { + "x": 19, + "y": 0, + "w": 63, + "h": 96 + }, + "frame": { + "x": 77, + "y": 96, + "w": 63, + "h": 96 + } + }, + { + "filename": "0050.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 82, + "h": 96 + }, + "spriteSourceSize": { + "x": 19, + "y": 0, + "w": 63, + "h": 96 + }, + "frame": { + "x": 77, + "y": 96, + "w": 63, + "h": 96 + } + }, + { + "filename": "0001.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 82, + "h": 96 }, "spriteSourceSize": { "x": 0, - "y": 0, - "w": 80, - "h": 111 + "y": 7, + "w": 79, + "h": 89 }, "frame": { - "x": 240, - "y": 222, - "w": 80, - "h": 111 + "x": 79, + "y": 192, + "w": 79, + "h": 89 + } + }, + { + "filename": "0019.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 82, + "h": 96 + }, + "spriteSourceSize": { + "x": 0, + "y": 7, + "w": 79, + "h": 89 + }, + "frame": { + "x": 79, + "y": 192, + "w": 79, + "h": 89 + } + }, + { + "filename": "0037.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 82, + "h": 96 + }, + "spriteSourceSize": { + "x": 0, + "y": 7, + "w": 79, + "h": 89 + }, + "frame": { + "x": 79, + "y": 192, + "w": 79, + "h": 89 + } + }, + { + "filename": "0007.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 82, + "h": 96 + }, + "spriteSourceSize": { + "x": 21, + "y": 0, + "w": 59, + "h": 96 + }, + "frame": { + "x": 140, + "y": 96, + "w": 59, + "h": 96 + } + }, + { + "filename": "0013.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 82, + "h": 96 + }, + "spriteSourceSize": { + "x": 21, + "y": 0, + "w": 59, + "h": 96 + }, + "frame": { + "x": 140, + "y": 96, + "w": 59, + "h": 96 + } + }, + { + "filename": "0025.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 82, + "h": 96 + }, + "spriteSourceSize": { + "x": 21, + "y": 0, + "w": 59, + "h": 96 + }, + "frame": { + "x": 140, + "y": 96, + "w": 59, + "h": 96 + } + }, + { + "filename": "0031.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 82, + "h": 96 + }, + "spriteSourceSize": { + "x": 21, + "y": 0, + "w": 59, + "h": 96 + }, + "frame": { + "x": 140, + "y": 96, + "w": 59, + "h": 96 + } + }, + { + "filename": "0043.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 82, + "h": 96 + }, + "spriteSourceSize": { + "x": 21, + "y": 0, + "w": 59, + "h": 96 + }, + "frame": { + "x": 140, + "y": 96, + "w": 59, + "h": 96 + } + }, + { + "filename": "0049.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 82, + "h": 96 + }, + "spriteSourceSize": { + "x": 21, + "y": 0, + "w": 59, + "h": 96 + }, + "frame": { + "x": 140, + "y": 96, + "w": 59, + "h": 96 + } + }, + { + "filename": "0008.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 82, + "h": 96 + }, + "spriteSourceSize": { + "x": 20, + "y": 0, + "w": 58, + "h": 96 + }, + "frame": { + "x": 149, + "y": 0, + "w": 58, + "h": 96 + } + }, + { + "filename": "0012.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 82, + "h": 96 + }, + "spriteSourceSize": { + "x": 20, + "y": 0, + "w": 58, + "h": 96 + }, + "frame": { + "x": 149, + "y": 0, + "w": 58, + "h": 96 + } + }, + { + "filename": "0026.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 82, + "h": 96 + }, + "spriteSourceSize": { + "x": 20, + "y": 0, + "w": 58, + "h": 96 + }, + "frame": { + "x": 149, + "y": 0, + "w": 58, + "h": 96 + } + }, + { + "filename": "0030.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 82, + "h": 96 + }, + "spriteSourceSize": { + "x": 20, + "y": 0, + "w": 58, + "h": 96 + }, + "frame": { + "x": 149, + "y": 0, + "w": 58, + "h": 96 + } + }, + { + "filename": "0044.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 82, + "h": 96 + }, + "spriteSourceSize": { + "x": 20, + "y": 0, + "w": 58, + "h": 96 + }, + "frame": { + "x": 149, + "y": 0, + "w": 58, + "h": 96 + } + }, + { + "filename": "0048.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 82, + "h": 96 + }, + "spriteSourceSize": { + "x": 20, + "y": 0, + "w": 58, + "h": 96 + }, + "frame": { + "x": 149, + "y": 0, + "w": 58, + "h": 96 + } + }, + { + "filename": "0003.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 82, + "h": 96 + }, + "spriteSourceSize": { + "x": 2, + "y": 2, + "w": 79, + "h": 94 + }, + "frame": { + "x": 199, + "y": 96, + "w": 79, + "h": 94 + } + }, + { + "filename": "0017.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 82, + "h": 96 + }, + "spriteSourceSize": { + "x": 2, + "y": 2, + "w": 79, + "h": 94 + }, + "frame": { + "x": 199, + "y": 96, + "w": 79, + "h": 94 + } + }, + { + "filename": "0021.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 82, + "h": 96 + }, + "spriteSourceSize": { + "x": 2, + "y": 2, + "w": 79, + "h": 94 + }, + "frame": { + "x": 199, + "y": 96, + "w": 79, + "h": 94 + } + }, + { + "filename": "0035.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 82, + "h": 96 + }, + "spriteSourceSize": { + "x": 2, + "y": 2, + "w": 79, + "h": 94 + }, + "frame": { + "x": 199, + "y": 96, + "w": 79, + "h": 94 + } + }, + { + "filename": "0039.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 82, + "h": 96 + }, + "spriteSourceSize": { + "x": 2, + "y": 2, + "w": 79, + "h": 94 + }, + "frame": { + "x": 199, + "y": 96, + "w": 79, + "h": 94 + } + }, + { + "filename": "0053.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 82, + "h": 96 + }, + "spriteSourceSize": { + "x": 2, + "y": 2, + "w": 79, + "h": 94 + }, + "frame": { + "x": 199, + "y": 96, + "w": 79, + "h": 94 + } + }, + { + "filename": "0009.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 82, + "h": 96 + }, + "spriteSourceSize": { + "x": 22, + "y": 0, + "w": 52, + "h": 96 + }, + "frame": { + "x": 207, + "y": 0, + "w": 52, + "h": 96 + } + }, + { + "filename": "0011.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 82, + "h": 96 + }, + "spriteSourceSize": { + "x": 22, + "y": 0, + "w": 52, + "h": 96 + }, + "frame": { + "x": 207, + "y": 0, + "w": 52, + "h": 96 + } + }, + { + "filename": "0027.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 82, + "h": 96 + }, + "spriteSourceSize": { + "x": 22, + "y": 0, + "w": 52, + "h": 96 + }, + "frame": { + "x": 207, + "y": 0, + "w": 52, + "h": 96 + } + }, + { + "filename": "0029.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 82, + "h": 96 + }, + "spriteSourceSize": { + "x": 22, + "y": 0, + "w": 52, + "h": 96 + }, + "frame": { + "x": 207, + "y": 0, + "w": 52, + "h": 96 + } + }, + { + "filename": "0045.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 82, + "h": 96 + }, + "spriteSourceSize": { + "x": 22, + "y": 0, + "w": 52, + "h": 96 + }, + "frame": { + "x": 207, + "y": 0, + "w": 52, + "h": 96 + } + }, + { + "filename": "0047.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 82, + "h": 96 + }, + "spriteSourceSize": { + "x": 22, + "y": 0, + "w": 52, + "h": 96 + }, + "frame": { + "x": 207, + "y": 0, + "w": 52, + "h": 96 + } + }, + { + "filename": "0010.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 82, + "h": 96 + }, + "spriteSourceSize": { + "x": 21, + "y": 1, + "w": 53, + "h": 95 + }, + "frame": { + "x": 199, + "y": 190, + "w": 53, + "h": 95 + } + }, + { + "filename": "0028.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 82, + "h": 96 + }, + "spriteSourceSize": { + "x": 21, + "y": 1, + "w": 53, + "h": 95 + }, + "frame": { + "x": 199, + "y": 190, + "w": 53, + "h": 95 + } + }, + { + "filename": "0046.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 82, + "h": 96 + }, + "spriteSourceSize": { + "x": 21, + "y": 1, + "w": 53, + "h": 95 + }, + "frame": { + "x": 199, + "y": 190, + "w": 53, + "h": 95 } } ] @@ -645,6 +1149,6 @@ "meta": { "app": "https://www.codeandweb.com/texturepacker", "version": "3.0", - "smartupdate": "$TexturePacker:SmartUpdate:dbbc7976911621e1d876d7e8ad805633:03f0391b0ea7e652408b22d3fc88c1f3:c7693032eafd40ba9028aebf1b6abbfa$" + "smartupdate": "$TexturePacker:SmartUpdate:a1464f05009f1be2ce8d1c3fd2ff65e3:98bcf35a71d7d1dceea57ad6e2a92474:c7693032eafd40ba9028aebf1b6abbfa$" } } diff --git a/public/images/pokemon/exp/865.png b/public/images/pokemon/exp/865.png index bf1e4a11d9f..8326e41ccc6 100644 Binary files a/public/images/pokemon/exp/865.png and b/public/images/pokemon/exp/865.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 f06ae607b0e..bbae01bf2f6 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, randSeedInt } 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"; @@ -86,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"; @@ -161,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 @@ -246,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; @@ -758,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} @@ -775,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} @@ -884,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`); @@ -1021,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 @@ -1051,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) { @@ -1086,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; @@ -1135,14 +1198,21 @@ 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 + if (this.isWaveMysteryEncounter(newBattleType, newWaveIndex, mysteryEncounterType) || newBattleType === BattleType.MYSTERY_ENCOUNTER || !isNullOrUndefined(mysteryEncounterType)) { + newBattleType = BattleType.MYSTERY_ENCOUNTER; + // Reset base spawn weight + this.mysteryEncounterSaveData.encounterSpawnChance = BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT; + } else if (newBattleType === BattleType.WILD) { + this.mysteryEncounterSaveData.encounterSpawnChance += 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; } @@ -1167,12 +1237,20 @@ 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; + // Will generate the actual Mystery Encounter during NextEncounterPhase, to ensure it uses proper biome + this.currentBattle.mysteryEncounterType = mysteryEncounterType; + } + //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) { @@ -1181,7 +1259,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) { @@ -1189,14 +1267,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()) { @@ -1290,7 +1375,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: @@ -1316,6 +1400,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) { @@ -1815,6 +1906,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 ?? {}; @@ -2039,12 +2143,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 @@ -2061,6 +2169,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; @@ -2485,7 +2605,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(); @@ -2507,29 +2627,45 @@ 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; + } + modifier.stackCount = mt.stackCount ?? 1; + 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()); }); @@ -2837,4 +2973,271 @@ 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)); + } + } + } + } + + /** + * Determines whether a wave should randomly generate a {@linkcode MysteryEncounter}. + * Currently, the only modes that MEs are allowed in are Classic and Challenge. + * Additionally, MEs cannot spawn outside of waves 10-180 in those modes + * + * @param newBattleType + * @param waveIndex + * @param sessionDataEncounterType + */ + private isWaveMysteryEncounter(newBattleType: BattleType, waveIndex: number, sessionDataEncounterType?: MysteryEncounterType): boolean { + const [lowestMysteryEncounterWave, highestMysteryEncounterWave] = this.gameMode.getMysteryEncounterLegalWaves(); + if (this.gameMode.hasMysteryEncounters && newBattleType === BattleType.WILD && !this.gameMode.isBoss(waveIndex) && waveIndex < highestMysteryEncounterWave && waveIndex > lowestMysteryEncounterWave) { + // If ME type is already defined in session data, no need to roll RNG check + if (!isNullOrUndefined(sessionDataEncounterType)) { + return true; + } + + // 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) * (waveIndex - 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 || (waveIndex - encounteredEvents[encounteredEvents.length - 1].waveIndex) > 3 || !isNullOrUndefined(Overrides.MYSTERY_ENCOUNTER_RATE_OVERRIDE); + + if (canSpawn) { + let roll = MYSTERY_ENCOUNTER_SPAWN_MAX_WEIGHT; + // Always rolls the check on the same offset to ensure no RNG changes from reloading session + this.executeWithSeedOffset(() => { + roll = randSeedInt(MYSTERY_ENCOUNTER_SPAWN_MAX_WEIGHT); + }, waveIndex * 3 * 1000); + return roll < successRate; + } + } + + return false; + } + + /** + * 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..f9c16d0189d 100644 --- a/src/battle.ts +++ b/src/battle.ts @@ -14,28 +14,40 @@ 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"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; + +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 +56,7 @@ export interface FaintLogEntry { } interface TurnCommands { - [key: number]: TurnCommand | null + [key: number]: TurnCommand | null } export default class Battle { @@ -77,6 +89,10 @@ export default class Battle { public playerFaintsHistory: FaintLogEntry[] = []; public enemyFaintsHistory: FaintLogEntry[] = []; + public mysteryEncounterType?: MysteryEncounterType; + /** 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 +115,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 +167,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 +213,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 +429,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 +455,11 @@ export class FixedBattleConfig { this.seedOffsetWaveIndex = seedOffsetWaveIndex; return this; } + + setCustomModifierRewards(customModifierRewardSettings: CustomModifierSettings) { + this.customModifierRewardSettings = customModifierRewardSettings; + return this; + } } @@ -493,29 +519,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 +560,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 old mode 100755 new mode 100644 index b38d9ea0fb3..9e9c423623d --- 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; } } @@ -353,6 +366,10 @@ export class TypeImmunityAbAttr extends PreDefendAbAttr { return false; } + getImmuneType(): Type | null { + return this.immuneType; + } + override getCondition(): AbAttrCondition | null { return this.condition; } @@ -1086,7 +1103,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 +1687,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 => { @@ -1763,7 +1780,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 => { @@ -1785,6 +1802,61 @@ export class PostDefendStealHeldItemAbAttr extends PostDefendAbAttr { } } +/** + * Base class for defining all {@linkcode Ability} Attributes after a status effect has been set. + * @see {@linkcode applyPostSetStatus()}. + */ +export class PostSetStatusAbAttr extends AbAttr { + /** + * Does nothing after a status condition is set. + * @param pokemon {@linkcode Pokemon} that status condition was set on. + * @param sourcePokemon {@linkcode Pokemon} that that set the status condition. Is `null` if status was not set by a Pokemon. + * @param passive Whether this ability is a passive. + * @param effect {@linkcode StatusEffect} that was set. + * @param args Set of unique arguments needed by this attribute. + * @returns `true` if application of the ability succeeds. + */ + applyPostSetStatus(pokemon: Pokemon, sourcePokemon: Pokemon | null = null, passive: boolean, effect: StatusEffect, simulated: boolean, args: any[]) : boolean | Promise { + return false; + } +} + +/** + * If another Pokemon burns, paralyzes, poisons, or badly poisons this Pokemon, + * that Pokemon receives the same non-volatile status condition as part of this + * ability attribute. For Synchronize ability. + */ +export class SynchronizeStatusAbAttr extends PostSetStatusAbAttr { + /** + * If the `StatusEffect` that was set is Burn, Paralysis, Poison, or Toxic, and the status + * was set by a source Pokemon, set the source Pokemon's status to the same `StatusEffect`. + * @param pokemon {@linkcode Pokemon} that status condition was set on. + * @param sourcePokemon {@linkcode Pokemon} that that set the status condition. Is null if status was not set by a Pokemon. + * @param passive Whether this ability is a passive. + * @param effect {@linkcode StatusEffect} that was set. + * @param args Set of unique arguments needed by this attribute. + * @returns `true` if application of the ability succeeds. + */ + override applyPostSetStatus(pokemon: Pokemon, sourcePokemon: Pokemon | null = null, passive: boolean, effect: StatusEffect, simulated: boolean, args: any[]): boolean { + /** Synchronizable statuses */ + const syncStatuses = new Set([ + StatusEffect.BURN, + StatusEffect.PARALYSIS, + StatusEffect.POISON, + StatusEffect.TOXIC + ]); + + if (sourcePokemon && syncStatuses.has(effect)) { + if (!simulated) { + sourcePokemon.trySetStatus(effect, true, pokemon); + } + return true; + } + + return false; + } +} + export class PostVictoryAbAttr extends AbAttr { applyPostVictory(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean | Promise { return false; @@ -2625,7 +2697,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) { @@ -2634,7 +2710,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; @@ -3757,7 +3843,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; @@ -4214,6 +4300,10 @@ export class ReduceBerryUseThresholdAbAttr extends AbAttr { } } +/** + * Ability attribute used for abilites that change the ability owner's weight + * Used for Heavy Metal (doubling weight) and Light Metal (halving weight) + */ export class WeightMultiplierAbAttr extends AbAttr { private multiplier: integer; @@ -4650,6 +4740,10 @@ export function applyStatMultiplierAbAttrs(attrType: Constructor { return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyStatStage(pokemon, passive, simulated, stat, statValue, args), args); } +export function applyPostSetStatusAbAttrs(attrType: Constructor, + pokemon: Pokemon, effect: StatusEffect, sourcePokemon?: Pokemon | null, simulated: boolean = false, ...args: any[]): Promise { + return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyPostSetStatus(pokemon, sourcePokemon, passive, effect, simulated, args), args, false, simulated); +} /** * Applies a field Stat multiplier attribute @@ -4817,11 +4911,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) @@ -4882,7 +4974,8 @@ export function initAbilities() { .attr(EffectSporeAbAttr), new Ability(Abilities.SYNCHRONIZE, 3) .attr(SyncEncounterNatureAbAttr) - .unimplemented(), + .attr(SynchronizeStatusAbAttr) + .partial(), // interaction with psycho shift needs work, keeping to old Gen interaction for now new Ability(Abilities.CLEAR_BODY, 3) .attr(ProtectStatAbAttr) .ignorable(), @@ -4934,8 +5027,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), @@ -5066,7 +5158,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), @@ -5147,8 +5238,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(), @@ -5318,8 +5408,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 @@ -5858,6 +5947,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 102d435fc60..62ef8112e6c 100644 --- a/src/data/battle-anims.ts +++ b/src/data/battle-anims.ts @@ -7,6 +7,9 @@ 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 { @@ -304,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; } @@ -322,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 { @@ -384,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; @@ -414,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(); } @@ -425,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); } @@ -446,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 => { @@ -482,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(); @@ -509,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); @@ -516,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)) { @@ -570,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(); @@ -679,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; @@ -761,9 +832,9 @@ export abstract class BattleAnim { 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(); } @@ -830,7 +901,7 @@ export abstract class BattleAnim { } }; - if (!scene.moveAnimations) { + if (!scene.moveAnimations && !this.playRegardlessOfIssues) { return cleanUpAndComplete(); } @@ -844,12 +915,12 @@ export abstract class BattleAnim { this.srcLine = [ userFocusX, userFocusY, targetFocusX, targetFocusY ]; this.dstLine = [ userInitialX, userInitialY, targetInitialX, targetInitialY ]; - let r = anim!.frames.length; // TODO: is this bang correct? + let r = anim?.frames.length ?? 0; let f = 0; scene.tweens.addCounter({ duration: Utils.getFrameMs(3), - repeat: anim!.frames.length, // TODO: is this bang correct? + repeat: anim?.frames.length ?? 0, onRepeat: () => { if (!f) { userSprite.setVisible(false); @@ -866,11 +937,13 @@ 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) { - if (!isUser && !!targetSubstitute) { + 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()); @@ -921,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); @@ -1017,19 +1090,181 @@ 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; } getAnim(): AnimConfig | null { - return this.commonAnim ? commonAnims.get(this.commonAnim)! : null; // TODO: is this bang correct? + return this.commonAnim ? commonAnims.get(this.commonAnim) ?? null : null; } isOppAnim(): boolean { @@ -1040,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; } @@ -1049,7 +1284,7 @@ export class MoveAnim extends BattleAnim { getAnim(): AnimConfig { return moveAnims.get(this.move) instanceof AnimConfig ? moveAnims.get(this.move) as AnimConfig - : moveAnims.get(this.move)![this.user?.isPlayer() ? 0 : 1] as AnimConfig; // TODO: is this bang correct? + : moveAnims.get(this.move)?.[this.user?.isPlayer() ? 0 : 1] as AnimConfig; } isOppAnim(): boolean { @@ -1081,7 +1316,27 @@ export class MoveChargeAnim extends MoveAnim { getAnim(): AnimConfig { return chargeAnims.get(this.chargeAnim) instanceof AnimConfig ? chargeAnims.get(this.chargeAnim) as AnimConfig - : chargeAnims.get(this.chargeAnim)![this.user?.isPlayer() ? 0 : 1] as AnimConfig; // TODO: is this bang correct? + : chargeAnims.get(this.chargeAnim)?.[this.user?.isPlayer() ? 0 : 1] as AnimConfig; + } +} + +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; } } diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index a43fa58ba1d..e92446ef5a2 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"; @@ -141,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. * @@ -524,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 @@ -878,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; @@ -1060,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); } @@ -2194,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 @@ -2217,6 +2281,36 @@ export class TarShotTag extends BattlerTag { } } +/** + * Battler Tag that keeps track of how many times the user has Autotomized + * Each count of Autotomization reduces the weight by 100kg + */ +export class AutotomizedTag extends BattlerTag { + public autotomizeCount: number = 0; + constructor(sourceMove: Moves = Moves.AUTOTOMIZE) { + super(BattlerTagType.AUTOTOMIZED, BattlerTagLapseType.CUSTOM, 1, sourceMove); + } + + /** + * Adds an autotomize count to the Pokemon. Each stack reduces weight by 100kg + * If the Pokemon is over 0.1kg it also displays a message. + * @param pokemon The Pokemon that is being autotomized + */ + onAdd(pokemon: Pokemon): void { + const minWeight = 0.1; + if (pokemon.getWeight() > minWeight) { + pokemon.scene.queueMessage(i18next.t("battlerTags:autotomizeOnAdd", { + pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) + })); + } + this.autotomizeCount += 1; + } + + onOverlap(pokemon: Pokemon): void { + this.onAdd(pokemon); + } +} + export class SubstituteTag extends BattlerTag { /** The substitute's remaining HP. If HP is depleted, the Substitute fades. */ public hp: number; @@ -2238,8 +2332,8 @@ export class SubstituteTag extends BattlerTag { pokemon.scene.triggerPokemonBattleAnim(pokemon, PokemonAnimType.SUBSTITUTE_ADD); pokemon.scene.queueMessage(i18next.t("battlerTags:substituteOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }), 1500); - // Remove any trapping effects from the user - pokemon.findAndRemoveTags(tag => tag instanceof TrappedTag); + // 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. */ @@ -2304,6 +2398,45 @@ export class SubstituteTag extends BattlerTag { } } +/** + * 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. * @@ -2465,6 +2598,12 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source return new GorillaTacticsTag(); case BattlerTagType.SUBSTITUTE: return new SubstituteTag(sourceMove, sourceId); + case BattlerTagType.AUTOTOMIZED: + return new AutotomizedTag(); + 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 2205519c532..1afbfc932dc 100644 --- a/src/data/challenge.ts +++ b/src/data/challenge.ts @@ -172,11 +172,9 @@ export abstract class Challenge { * @param overrideValue {@link integer} The value to check for. If undefined, gets the current value. * @returns {@link string} The localised name for the current value. */ - getValue(overrideValue?: integer): string { - if (overrideValue === undefined) { - overrideValue = this.value; - } - return i18next.t(`challenges:${this.geti18nKey()}.value.${this.value}`); + getValue(overrideValue?: number): string { + const value = overrideValue ?? this.value; + return i18next.t(`challenges:${this.geti18nKey()}.value.${value}`); } /** @@ -184,11 +182,9 @@ export abstract class Challenge { * @param overrideValue {@link integer} The value to check for. If undefined, gets the current value. * @returns {@link string} The localised description for the current value. */ - getDescription(overrideValue?: integer): string { - if (overrideValue === undefined) { - overrideValue = this.value; - } - return `${i18next.t([`challenges:${this.geti18nKey()}.desc.${this.value}`, `challenges:${this.geti18nKey()}.desc`])}`; + getDescription(overrideValue?: number): string { + const value = overrideValue ?? this.value; + return `${i18next.t([`challenges:${this.geti18nKey()}.desc.${value}`, `challenges:${this.geti18nKey()}.desc`])}`; } /** @@ -487,14 +483,12 @@ export class SingleGenerationChallenge extends Challenge { * @param {value} overrideValue The value to check for. If undefined, gets the current value. * @returns {string} The localised name for the current value. */ - getValue(overrideValue?: integer): string { - if (overrideValue === undefined) { - overrideValue = this.value; - } - if (this.value === 0) { + getValue(overrideValue?: number): string { + const value = overrideValue ?? this.value; + if (value === 0) { return i18next.t("settings:off"); } - return i18next.t(`starterSelectUiHandler:gen${this.value}`); + return i18next.t(`starterSelectUiHandler:gen${value}`); } /** @@ -502,14 +496,12 @@ export class SingleGenerationChallenge extends Challenge { * @param {value} overrideValue The value to check for. If undefined, gets the current value. * @returns {string} The localised description for the current value. */ - getDescription(overrideValue?: integer): string { - if (overrideValue === undefined) { - overrideValue = this.value; - } - if (this.value === 0) { + getDescription(overrideValue?: number): string { + const value = overrideValue ?? this.value; + if (value === 0) { return i18next.t("challenges:singleGeneration.desc_default"); } - return i18next.t("challenges:singleGeneration.desc", { gen: i18next.t(`challenges:singleGeneration.gen_${this.value}`) }); + return i18next.t("challenges:singleGeneration.desc", { gen: i18next.t(`challenges:singleGeneration.gen_${value}`) }); } 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 1d1a788e768..8866e86f708 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, SubstituteTag, 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"; @@ -652,7 +650,7 @@ export default class Move implements Localizable { } /** - * Applies each {@linkcode MoveCondition} of this move to the params + * Applies each {@linkcode MoveCondition} function of this move to the params, determines if the move can be used prior to calling each attribute's apply() * @param user {@linkcode Pokemon} to apply conditions to * @param target {@linkcode Pokemon} to apply conditions to * @param move {@linkcode Move} to apply conditions to @@ -813,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); @@ -2097,21 +2091,20 @@ export class PsychoShiftEffectAttr extends MoveEffectAttr { if (target.status) { return false; - } - //@ts-ignore - how can target.status.effect be checked when we return `false` before when it's defined? - if (!target.status || (target.status.effect === statusToApply && move.chance < 0)) { // TODO: resolve ts-ignore - const statusAfflictResult = target.trySetStatus(statusToApply, true, user); - if (statusAfflictResult) { + } else { + const canSetStatus = target.canSetStatus(statusToApply, true, false, user); + + if (canSetStatus) { if (user.status) { user.scene.queueMessage(getStatusEffectHealText(user.status.effect, getPokemonNameWithAffix(user))); } user.resetStatus(); user.updateInfo(); + target.trySetStatus(statusToApply, true, user); } - return statusAfflictResult; - } - return false; + return canSetStatus; + } } getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { @@ -2140,7 +2133,7 @@ export class StealHeldItemChanceAttr extends MoveEffectAttr { 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? @@ -2217,7 +2210,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()); @@ -2421,6 +2414,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; + } } /** @@ -3843,7 +3846,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 @@ -3977,19 +3980,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; } } @@ -4536,6 +4538,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: @@ -4856,7 +4859,9 @@ export class RemoveAllSubstitutesAttr extends MoveEffectAttr { } /** - * Attribute used when a move hits a {@linkcode BattlerTagType} for double damage + * 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 { @@ -4865,7 +4870,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; @@ -4877,6 +4882,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; @@ -5158,31 +5174,29 @@ export class RevivalBlessingAttr extends MoveEffectAttr { } export class ForceSwitchOutAttr extends MoveEffectAttr { - private user: boolean; - private batonPass: boolean; - - constructor(user?: boolean, batonPass?: boolean) { + constructor( + private selfSwitch: boolean = false, + private batonPass: boolean = false + ) { super(false, MoveEffectTrigger.POST_APPLY, false, true); - this.user = !!user; - this.batonPass = !!batonPass; } isBatonPass() { return this.batonPass; } + // TODO: Why is this a Promise? apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise { return new Promise(resolve => { - // Check if the move category is not STATUS or if the switch out condition is not met if (!this.getSwitchOutCondition()(user, target, move)) { return resolve(false); } - // Move the switch out logic inside the conditional block - // This ensures that the switch out only happens when the conditions are met - const switchOutTarget = this.user ? user : target; - if (switchOutTarget instanceof PlayerPokemon) { + // Move the switch out logic inside the conditional block + // This ensures that the switch out only happens when the conditions are met + const switchOutTarget = this.selfSwitch ? user : target; + if (switchOutTarget instanceof PlayerPokemon) { switchOutTarget.leaveField(!this.batonPass); if (switchOutTarget.hp > 0) { @@ -5191,42 +5205,43 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { } else { resolve(false); } - return; - } else if (user.scene.currentBattle.battleType !== BattleType.WILD) { - // Switch out logic for trainer battles + return; + } else if (user.scene.currentBattle.battleType !== BattleType.WILD) { + // Switch out logic for trainer battles switchOutTarget.leaveField(!this.batonPass); - if (switchOutTarget.hp > 0) { - // for opponent switching out - user.scene.prependToPhase(new SwitchSummonPhase(user.scene, switchOutTarget.getFieldIndex(), (user.scene.currentBattle.trainer ? user.scene.currentBattle.trainer.getNextSummonIndex((switchOutTarget as EnemyPokemon).trainerSlot) : 0), false, this.batonPass, false), MoveEndPhase); + if (switchOutTarget.hp > 0) { + // for opponent switching out + user.scene.prependToPhase(new SwitchSummonPhase(user.scene, switchOutTarget.getFieldIndex(), + (user.scene.currentBattle.trainer ? user.scene.currentBattle.trainer.getNextSummonIndex((switchOutTarget as EnemyPokemon).trainerSlot) : 0), + false, this.batonPass, false), MoveEndPhase); } - } else { - // Switch out logic for everything else (eg: WILD battles) - switchOutTarget.leaveField(false); + } else { + // Switch out logic for everything else (eg: WILD battles) + switchOutTarget.leaveField(false); - if (switchOutTarget.hp) { - switchOutTarget.setWildFlee(true); - user.scene.queueMessage(i18next.t("moveTriggers:fled", {pokemonName: getPokemonNameWithAffix(switchOutTarget)}), null, true, 500); + if (switchOutTarget.hp) { + user.scene.queueMessage(i18next.t("moveTriggers:fled", {pokemonName: getPokemonNameWithAffix(switchOutTarget)}), null, true, 500); // in double battles redirect potential moves off fled pokemon if (switchOutTarget.scene.currentBattle.double) { const allyPokemon = switchOutTarget.getAlly(); switchOutTarget.scene.redirectPokemonMoves(switchOutTarget, allyPokemon); } - } + } - if (!switchOutTarget.getAlly()?.isActive(true)) { - user.scene.clearEnemyHeldItemModifiers(); + if (!switchOutTarget.getAlly()?.isActive(true)) { + user.scene.clearEnemyHeldItemModifiers(); - if (switchOutTarget.hp) { - user.scene.pushPhase(new BattleEndPhase(user.scene)); - user.scene.pushPhase(new NewBattlePhase(user.scene)); - } - } - } + if (switchOutTarget.hp) { + user.scene.pushPhase(new BattleEndPhase(user.scene)); + user.scene.pushPhase(new NewBattlePhase(user.scene)); + } + } + } - resolve(true); - }); + resolve(true); + }); } getCondition(): MoveConditionFunc { @@ -5241,29 +5256,33 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { getSwitchOutCondition(): MoveConditionFunc { return (user, target, move) => { - const switchOutTarget = (this.user ? user : target); + const switchOutTarget = (this.selfSwitch ? user : target); const player = switchOutTarget instanceof PlayerPokemon; - if (!this.user && move.hitsSubstitute(user, target)) { - return false; + if (!this.selfSwitch) { + if (move.hitsSubstitute(user, target)) { + return false; + } + + const blockedByAbility = new Utils.BooleanHolder(false); + applyAbAttrs(ForceSwitchOutImmunityAbAttr, target, blockedByAbility); + return !blockedByAbility.value; } - if (!this.user && move.category === MoveCategory.STATUS && (target.hasAbilityWithAttr(ForceSwitchOutImmunityAbAttr) || target.isMax())) { - return false; - } - - if (!player && !user.scene.currentBattle.battleType) { + if (!player && user.scene.currentBattle.battleType === BattleType.WILD) { if (this.batonPass) { return false; } // Don't allow wild opponents to flee on the boss stage since it can ruin a run early on - if (!(user.scene.currentBattle.waveIndex % 10)) { + if (user.scene.currentBattle.waveIndex % 10 === 0) { return false; } } const party = player ? user.scene.getParty() : user.scene.getEnemyParty(); - return (!player && !user.scene.currentBattle.battleType) || party.filter(p => p.isAllowedInBattle() && (player || (p as EnemyPokemon).trainerSlot === (switchOutTarget as EnemyPokemon).trainerSlot)).length > user.scene.currentBattle.getBattlerCount(); + return (!player && !user.scene.currentBattle.battleType) + || party.filter(p => p.isAllowedInBattle() + && (player || (p as EnemyPokemon).trainerSlot === (switchOutTarget as EnemyPokemon).trainerSlot)).length > user.scene.currentBattle.getBattlerCount(); }; } @@ -5271,8 +5290,8 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { if (!user.scene.getEnemyParty().find(p => p.isActive() && !p.isOnField())) { return -20; } - let ret = this.user ? Math.floor((1 - user.getHpRatio()) * 20) : super.getUserBenefitScore(user, target, move); - if (this.user && this.batonPass) { + let ret = this.selfSwitch ? Math.floor((1 - user.getHpRatio()) * 20) : super.getUserBenefitScore(user, target, move); + if (this.selfSwitch && this.batonPass) { const statStageTotal = user.getStatStages().reduce((s: integer, total: integer) => total += s, 0); ret = ret / 2 + (Phaser.Tweens.Builders.GetEaseFunction("Sine.easeOut")(Math.min(Math.abs(statStageTotal), 10) / 10) * (statStageTotal >= 0 ? 10 : -10)); } @@ -5280,6 +5299,21 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { } } + +export class ChillyReceptionAttr extends ForceSwitchOutAttr { + + // using inherited constructor + + apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise { + user.scene.arena.trySetWeather(WeatherType.SNOW, true); + return super.apply(user, target, move, args); + } + + getCondition(): MoveConditionFunc { + // chilly reception move will go through if the weather is change-able to snow, or the user can switch out, else move will fail + return (user, target, move) => user.scene.arena.trySetWeather(WeatherType.SNOW, true) || super.getSwitchOutCondition()(user, target, move); + } +} export class RemoveTypeAttr extends MoveEffectAttr { private removedType: Type; @@ -6397,7 +6431,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; } @@ -6458,8 +6492,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) => { @@ -6758,12 +6790,11 @@ 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(), @@ -6776,8 +6807,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), @@ -6801,8 +6832,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), @@ -6855,8 +6886,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) - .ignoresSubstitute() - .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), @@ -6871,7 +6901,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), @@ -6894,8 +6924,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), @@ -6955,17 +6984,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) @@ -7353,7 +7383,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) @@ -7409,7 +7440,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), @@ -7441,7 +7472,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) @@ -7539,7 +7570,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) @@ -7814,8 +7845,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(), @@ -7895,8 +7926,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) @@ -8006,8 +8037,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(), @@ -8079,7 +8109,7 @@ export function initMoves() { .attr(MovePowerMultiplierAttr, (user, target, move) => target.status && (target.status.effect === StatusEffect.POISON || target.status.effect === StatusEffect.TOXIC) ? 2 : 1), new SelfStatusMove(Moves.AUTOTOMIZE, Type.STEEL, -1, 15, -1, 0, 5) .attr(StatStageChangeAttr, [ Stat.SPD ], 2, true) - .partial(), + .attr(AddBattlerTagAttr, BattlerTagType.AUTOTOMIZED, true), new SelfStatusMove(Moves.RAGE_POWDER, Type.BUG, -1, 20, -1, 2, 5) .powderMove() .attr(AddBattlerTagAttr, BattlerTagType.CENTER_OF_ATTENTION, true), @@ -8094,7 +8124,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), @@ -8107,10 +8137,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) @@ -8193,7 +8222,7 @@ 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), @@ -8229,6 +8258,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) @@ -8260,13 +8290,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), @@ -8279,7 +8310,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) @@ -8333,9 +8364,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) @@ -8506,8 +8537,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) @@ -8764,6 +8795,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) @@ -9058,8 +9091,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), @@ -9111,7 +9143,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) @@ -9119,7 +9151,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) @@ -9471,10 +9503,9 @@ export function initMoves() { .makesContact(), new SelfStatusMove(Moves.SHED_TAIL, Type.NORMAL, -1, 10, -1, 0, 9) .unimplemented(), - new StatusMove(Moves.CHILLY_RECEPTION, Type.ICE, -1, 10, -1, 0, 9) - .attr(WeatherChangeAttr, WeatherType.SNOW) - .attr(ForceSwitchOutAttr, true, false) - .target(MoveTarget.BOTH_SIDES), + new SelfStatusMove(Moves.CHILLY_RECEPTION, Type.ICE, -1, 10, -1, 0, 9) + .attr(PreMoveMessageAttr, (user, move) => i18next.t("moveTriggers:chillyReception", {pokemonName: getPokemonNameWithAffix(user)})) + .attr(ChillyReceptionAttr, true, false), 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) @@ -9595,7 +9626,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..88fdadf8588 --- /dev/null +++ b/src/data/mystery-encounters/encounters/a-trainers-test-encounter.ts @@ -0,0 +1,186 @@ +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"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; + +/** 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, CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES[1]) + .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..efcd41054ea --- /dev/null +++ b/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts @@ -0,0 +1,537 @@ +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, PokemonInstantReviveModifier } 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) + .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 }); + } + }); + + // Do NOT remove the real berries yet or else it will be persisted in the session data + + // SpDef buff below wave 50, +1 to all stats otherwise + const statChangesForBattle: (Stat.ATK | Stat.DEF | Stat.SPATK | Stat.SPDEF | Stat.SPD | Stat.ACC | Stat.EVA)[] = scene.currentBattle.waveIndex < 50 ? + [Stat.SPDEF] : + [Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD]; + + // 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.CRUNCH], + 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, statChangesForBattle, 1)); + } + } + ], + }; + + encounter.enemyPartyConfigs = [config]; + encounter.setDialogueToken("greedentName", getPokemonSpecies(Species.GREEDENT).getName()); + + return true; + }) + .withOnVisualsStart((scene: BattleScene) => { + doGreedentSpriteSteal(scene); + doBerrySpritePile(scene); + + // Remove the berries from the party + // Session has been safely saved at this point, so data won't be lost + const berryItems = scene.findModifiers(m => m instanceof BerryModifier) as BerryModifier[]; + berryItems.forEach(berryMod => { + scene.removeModifier(berryMod); + }); + + scene.updateModifiers(true); + + 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 => { + const heldItems = p.getHeldItems(); + if (revSeed && !heldItems.some(item => item instanceof PokemonInstantReviveModifier)) { + 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..919cd1df5ca --- /dev/null +++ b/src/data/mystery-encounters/encounters/an-offer-you-cant-refuse-encounter.ts @@ -0,0 +1,177 @@ +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, speciesStarters } 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"; + +/** + * Money offered starts at base value of Relic Gold, increasing linearly up to 3x Relic Gold based on the starter tier of the Pokemon being purchased + * Starter value 1-3 -> Relic Gold + * Starter value 10 -> 3 * Relic Gold + */ +const MONEY_MINIMUM_MULTIPLIER = 10; +const MONEY_MAXIMUM_MULTIPLIER = 30; + +/** + * 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 baseSpecies = pokemon.getSpeciesForm().getRootSpeciesId(true); + const starterValue: number = speciesStarters[baseSpecies] ?? 1; + const multiplier = Math.max(MONEY_MAXIMUM_MULTIPLIER / 10 * starterValue, MONEY_MINIMUM_MULTIPLIER); + const price = scene.getWaveMoneyAmount(multiplier); + + 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..4cc5edc5208 --- /dev/null +++ b/src/data/mystery-encounters/encounters/berries-abound-encounter.ts @@ -0,0 +1,277 @@ +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 = () => { + 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++) { + 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 = () => { + 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++) { + tryGiveBerry(scene); + } + }; + + // Defense/Spd buffs below wave 50, +1 to all stats 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 = 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, statChangesForBattle, 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 = () => { + 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++) { + 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(); + +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)) { + 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)) { + 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..35858b2e6db --- /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..2b0f7b0722e --- /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, LevelIncrementBoosterModifier, MoneyMultiplierModifier, 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 = 2; + +/** + * 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 Amulet Coin + // Check if the player has max stacks of that item already + const existing = scene.findModifier(m => m instanceof MoneyMultiplierModifier) as MoneyMultiplierModifier; + + 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.AMULET_COIN)); + } + + 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..52890f0ffed --- /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 < 5) { + // 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..49936b67efe --- /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[0], 100) + .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..13f9d926345 --- /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..f3765c9acba --- /dev/null +++ b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts @@ -0,0 +1,859 @@ +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 * as Utils from "#app/utils"; +import { IntegerHolder, isNullOrUndefined, randInt, randSeedInt, randSeedShuffle } from "#app/utils"; +import Pokemon, { EnemyPokemon, PlayerPokemon, PokemonMove } from "#app/field/pokemon"; +import { HiddenAbilityRateBoosterModifier, PokemonFormChangeItemModifier, PokemonHeldItemModifier, ShinyRateBoosterModifier, 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"; + +/** Base shiny chance of 512/65536 -> 1/128 odds, affected by events and Shiny Charms. Cannot exceed 1/16 odds. */ +const WONDER_TRADE_SHINY_CHANCE = 512; +/** Max shiny chance of 4096/65536 -> 1/16 odds. */ +const MAX_WONDER_TRADE_SHINY_CHANCE = 4096; + +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 && tradePokemon.species.forms.length > tradePokemon.formIndex ? tradePokemon.species.forms[pokemon.formIndex].formName : null; + 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) { + const shinyThreshold = new Utils.IntegerHolder(WONDER_TRADE_SHINY_CHANCE); + if (scene.eventManager.isEventActive()) { + shinyThreshold.value *= scene.eventManager.getShinyMultiplier(); + } + scene.applyModifiers(ShinyRateBoosterModifier, true, shinyThreshold); + + // Base shiny chance of 512/65536 -> 1/128, affected by events and Shiny Charms + // Maximum shiny chance of 4096/65536 -> 1/16, cannot improve further after that + const shinyChance = Math.min(shinyThreshold.value, MAX_WONDER_TRADE_SHINY_CHANCE); + + tradePokemon.trySetShinySeed(shinyChance, false); + } + + // Extra HA roll at base 1/64 odds (boosted by events and charms) + const hiddenIndex = tradePokemon.species.ability2 ? 2 : 1; + if (tradePokemon.species.abilityHidden) { + if (tradePokemon.abilityIndex < hiddenIndex) { + const hiddenAbilityChance = new IntegerHolder(64); + scene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance); + + const hasHiddenAbility = !randSeedInt(hiddenAbilityChance.value); + + if (hasHiddenAbility) { + tradePokemon.abilityIndex = hiddenIndex; + } + } + } + + // If Pokemon is still not shiny or with HA, give the Pokemon a random Common egg move in its moveset + if (!tradePokemon.shiny && (!tradePokemon.species.abilityHidden || tradePokemon.abilityIndex < hiddenIndex)) { + const eggMoves = tradePokemon.getEggMoves(); + if (eggMoves) { + // Cannot gen the rare egg move, only 1 of the first 3 common moves + const eggMove = eggMoves[randSeedInt(3)]; + if (!tradePokemon.moveset.some(m => m?.moveId === eggMove)) { + if (tradePokemon.moveset.length < 4) { + tradePokemon.moveset.push(new PokemonMove(eggMove)); + } else { + const eggMoveIndex = randSeedInt(4); + tradePokemon.moveset[eggMoveIndex] = new PokemonMove(eggMove); + } + } + } + } + + 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..4117de14fc4 --- /dev/null +++ b/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts @@ -0,0 +1,210 @@ +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 TRAP_PERCENT = 35; +const COMMON_REWARDS_PERCENT = 20; +const ULTRA_REWARDS_PERCENT = 30; +const ROGUE_REWARDS_PERCENT = 10; +const MASTER_REWARDS_PERCENT = 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()); + encounter.setDialogueToken("trapPercent", TRAP_PERCENT.toString()); + encounter.setDialogueToken("commonPercent", COMMON_REWARDS_PERCENT.toString()); + encounter.setDialogueToken("ultraPercent", ULTRA_REWARDS_PERCENT.toString()); + encounter.setDialogueToken("roguePercent", ROGUE_REWARDS_PERCENT.toString()); + encounter.setDialogueToken("masterPercent", MASTER_REWARDS_PERCENT.toString()); + + 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 < TRAP_PERCENT) { + // 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 >= RAND_LENGTH - COMMON_REWARDS_PERCENT) { + // 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 >= RAND_LENGTH - COMMON_REWARDS_PERCENT - ULTRA_REWARDS_PERCENT) { + // 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 >= RAND_LENGTH - COMMON_REWARDS_PERCENT - ULTRA_REWARDS_PERCENT - ROGUE_REWARDS_PERCENT) { + // 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 >= RAND_LENGTH - COMMON_REWARDS_PERCENT - ULTRA_REWARDS_PERCENT - ROGUE_REWARDS_PERCENT - MASTER_REWARDS_PERCENT) { + // 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..8b8fc5f73be --- /dev/null +++ b/src/data/mystery-encounters/encounters/safari-zone-encounter.ts @@ -0,0 +1,524 @@ +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; + +const NUM_SAFARI_ENCOUNTERS = 3; + +/** + * 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`) + .withOnInit((scene: BattleScene) => { + scene.currentBattle.mysteryEncounter?.setDialogueToken("numEncounters", NUM_SAFARI_ENCOUNTERS.toString()); + return true; + }) + .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: NUM_SAFARI_ENCOUNTERS + }; + 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..abea725f113 --- /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(0, 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(0, 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, +1 to all stats 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..53e27022195 --- /dev/null +++ b/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts @@ -0,0 +1,164 @@ +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(0, 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)); + let 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)); + tries++; + } + + 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, 0); + } 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(0, 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..56328695128 --- /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.COMMON) + .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..33864c00143 --- /dev/null +++ b/src/data/mystery-encounters/encounters/training-session-encounter.ts @@ -0,0 +1,415 @@ +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..83ec3e1e4e9 --- /dev/null +++ b/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts @@ -0,0 +1,232 @@ +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 { 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; + +// Items will cost 2.5x as much for remainder of the run +const SHOP_ITEM_COST_MULTIPLIER = 2.5; + +/** + * 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"); + + encounter.setDialogueToken("costMultiplier", SHOP_ITEM_COST_MULTIPLIER.toString()); + + 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); + + const blackSludge = generateModifierType(scene, modifierTypes.MYSTERY_ENCOUNTER_BLACK_SLUDGE, [SHOP_ITEM_COST_MULTIPLIER]); + const modifier = blackSludge?.newModifier(); + if (modifier) { + await scene.addModifier(modifier, false, false, false, true); + scene.playSound("battle_anims/PRSFX- Venom Drench", { volume: 2 }); + await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: modifier.type.name }), null, undefined, true); + } + + 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: "2x " + 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: "2x " + 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..24298a633df --- /dev/null +++ b/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts @@ -0,0 +1,260 @@ +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 { 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); + + // Pokemon will always have one of its egg moves in its moveset + const eggMoves = pokemon.getEggMoves(); + if (eggMoves) { + 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; + + // Defense/Spd buffs below wave 50, +1 to all stats 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: 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, statChangesForBattle, 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 + givePokemonExtraEggMove(pokemon, encounter.misc.eggMove); + + 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 + givePokemonExtraEggMove(pokemon, encounter.misc.eggMove); + + // 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(); + +function givePokemonExtraEggMove(pokemon: EnemyPokemon, previousEggMove: Moves) { + const eggMoves = pokemon.getEggMoves(); + if (eggMoves) { + 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); + } + } +} 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..8fa60774a72 --- /dev/null +++ b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts @@ -0,0 +1,624 @@ +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 { 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, Challenges.SINGLE_GENERATION) + .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(); + await 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; + + // Transfer previous Pokemon's luck value + newPokemon.luck = previousPokemon.getLuck(); + + // 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(); + // Store a copy of a "standard" generated moveset for the new pokemon, will be used later for finding a favored move + const newPokemonGeneratedMoveset = newPokemon.moveset; + + newPokemon.moveset = previousPokemon.moveset; + + const newEggMoveIndex = await addEggMoveToNewPokemonMoveset(scene, newPokemon, speciesRootForm); + + // Try to add a favored STAB move (might fail if Pokemon already knows a bunch of moves from newPokemonGeneratedMoveset) + addFavoredMoveToNewPokemonMoveset(scene, newPokemon, newPokemonGeneratedMoveset, newEggMoveIndex); + + // 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; + await 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 modType = modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU() + .generateType(scene.getParty(), [20, stats]) + ?.withIdFromFunc(modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU); + const modifier = modType?.newModifier(newPokemon); + if (modifier) { + await scene.addModifier(modifier, false, false, false, true); + } + } + + // Enable passive if previous had it + newPokemon.passive = previousPokemon.passive; + + newPokemon.calculateStats(); + await newPokemon.updateInfo(); + } + + // One random pokemon will get its passive unlocked + const passiveDisabledPokemon = scene.getParty().filter(p => !p.passive); + if (passiveDisabledPokemon?.length > 0) { + const enablePassiveMon = passiveDisabledPokemon[randSeedInt(passiveDisabledPokemon.length)]; + enablePassiveMon.passive = true; + await enablePassiveMon.updateInfo(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(); + } + } + }); +} + +/** + * Returns index of the new egg move within the Pokemon's moveset (not the index of the move in `speciesEggMoves`) + * @param scene + * @param newPokemon + * @param speciesRootForm + */ +async function addEggMoveToNewPokemonMoveset(scene: BattleScene, newPokemon: PlayerPokemon, speciesRootForm: Species): Promise { + let eggMoveIndex: null | number = null; + const eggMoves = newPokemon.getEggMoves()?.slice(0); + if (eggMoves) { + const eggMoveIndices = [0, 1, 2, 3]; + randSeedShuffle(eggMoveIndices); + let randomEggMoveIndex = eggMoveIndices.pop(); + let randomEggMove = !isNullOrUndefined(randomEggMoveIndex) ? eggMoves[randomEggMoveIndex] : null; + let retries = 0; + while (retries < 3 && (!randomEggMove || newPokemon.moveset.some(m => m?.moveId === randomEggMove))) { + // If Pokemon already knows this move, roll for another egg move + randomEggMoveIndex = eggMoveIndices.pop(); + randomEggMove = !isNullOrUndefined(randomEggMoveIndex) ? eggMoves[randomEggMoveIndex] : null; + retries++; + } + + if (randomEggMove) { + if (!newPokemon.moveset.some(m => m?.moveId === randomEggMove)) { + 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 (!isNullOrUndefined(randomEggMoveIndex) && !!scene.gameData.dexData[speciesRootForm].caughtAttr) { + await scene.gameData.setEggMoveUnlocked(getPokemonSpecies(speciesRootForm), randomEggMoveIndex, true); + } + } + } + + return eggMoveIndex; +} + +/** + * Returns index of the new egg move within the Pokemon's moveset (not the index of the move in `speciesEggMoves`) + * @param scene + * @param newPokemon + * @param newPokemonGeneratedMoveset + * @param newEggMoveIndex + */ +function addFavoredMoveToNewPokemonMoveset(scene: BattleScene, newPokemon: PlayerPokemon, newPokemonGeneratedMoveset: (PokemonMove | null)[], newEggMoveIndex: number | null) { + let favoredMove: PokemonMove | null = null; + for (const move of newPokemonGeneratedMoveset) { + // Needs to match first type, second type will be replaced + if (move?.getMove().type === newPokemon.getTypes()[0] && !newPokemon.moveset.some(m => m?.moveId === move?.moveId)) { + favoredMove = move; + break; + } + } + // If was unable to find a favored move, uses first move in moveset that isn't already known (typically a high power STAB move) + // Otherwise, it gains no favored move + if (!favoredMove) { + for (const move of newPokemonGeneratedMoveset) { + // Needs to match first type, second type will be replaced + if (!newPokemon.moveset.some(m => m?.moveId === move?.moveId)) { + favoredMove = move; + break; + } + } + } + // Finally, assign favored move to random index that isn't the new egg move index + if (favoredMove) { + let favoredMoveIndex = randSeedInt(4); + while (newEggMoveIndex !== null && favoredMoveIndex === newEggMoveIndex) { + favoredMoveIndex = randSeedInt(4); + } + + newPokemon.moveset[favoredMoveIndex] = favoredMove; + } +} 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..ffae71b9555 --- /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..1b14aa91847 --- /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) => { + 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] { + const hpRatio = pokemon?.getHpRatio(); + if (!isNullOrUndefined(hpRatio)) { + return ["healthRatio", Math.floor(hpRatio * 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..0ce4a5c2506 --- /dev/null +++ b/src/data/mystery-encounters/mystery-encounters.ts @@ -0,0 +1,378 @@ +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"; +import { getBiomeName } from "#app/data/biomes"; + +/** + * Spawn chance: (BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT + WIGHT_INCREMENT_ON_SPAWN_MISS * ) / MYSTERY_ENCOUNTER_SPAWN_MAX_WEIGHT + */ +export const BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT = 3; +/** + * The divisor for determining ME spawns, defines the "maximum" weight required for a spawn + * If spawn_weight === MYSTERY_ENCOUNTER_SPAWN_MAX_WEIGHT, 100% chance to spawn a ME + */ +export const MYSTERY_ENCOUNTER_SPAWN_MAX_WEIGHT = 256; +/** + * When an ME spawn roll fails, WEIGHT_INCREMENT_ON_SPAWN_MISS is added to future rolls for ME spawn checks. + * These values are cleared whenever the next ME spawns, and spawn weight returns to BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT + */ +export const WEIGHT_INCREMENT_ON_SPAWN_MISS = 3; +/** + * Specifies the target average for total ME spawns in a single Classic run. + * Used by anti-variance mechanic to check whether a run is above or below the target on a given wave. + */ +export const AVERAGE_ENCOUNTERS_PER_RUN_TARGET = 12; +/** + * Will increase/decrease the chance of spawning a ME based on the current run's total MEs encountered vs AVERAGE_ENCOUNTERS_PER_RUN_TARGET + * Example: + * AVERAGE_ENCOUNTERS_PER_RUN_TARGET = 17 (expects avg 1 ME every 10 floors) + * ANTI_VARIANCE_WEIGHT_MODIFIER = 15 + * + * On wave 20, if 1 ME has been encountered, the difference from expected average is 0 MEs. + * So anti-variance adds 0/256 to the spawn weight check for ME spawn. + * + * On wave 20, if 0 MEs have been encountered, the difference from expected average is 1 ME. + * So anti-variance adds 15/256 to the spawn weight check for ME spawn. + * + * On wave 20, if 2 MEs have been encountered, the difference from expected average is -1 ME. + * So anti-variance adds -15/256 to the spawn weight check for ME spawn. + */ +export const ANTI_VARIANCE_WEIGHT_MODIFIER = 15; + +export const EXTREME_ENCOUNTER_BIOMES = [ + 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 + let encounterBiomeTableLog = ""; + mysteryEncountersByBiome.forEach((biomeEncounters, biome) => { + anyBiomeEncounters.forEach(encounter => { + if (!biomeEncounters.includes(encounter)) { + biomeEncounters.push(encounter); + } + }); + + encounterBiomeTableLog += `${getBiomeName(biome).toUpperCase()}: [${biomeEncounters.map(type => MysteryEncounterType[type].toString().toLowerCase()).sort().join(", ")}]\n`; + }); + + console.debug("All Mystery Encounters by Biome:\n" + encounterBiomeTableLog); +} 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..c4d5e47cb05 --- /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..ea04241e663 --- /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 = partyTrainerConfig ? partyTrainerConfig : 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(`Pokemon: ${enemyPokemon.name}`, `Species ID: ${enemyPokemon.species.speciesId}`, `Stats: ${enemyPokemon.stats}`, `Ability: ${enemyPokemon.getAbility().name}`, `Passive Ability: ${enemyPokemon.getPassiveAbility().name}`); + }); + + 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..d7e596af879 --- /dev/null +++ b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts @@ -0,0 +1,832 @@ +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]) + ?.withIdFromFunc(modifierTypes.MYSTERY_ENCOUNTER_SHUCKLE_JUICE); + 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; + const baseLevel = 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 4fc833939e4..a904f497b0f 100644 --- a/src/data/pokemon-forms.ts +++ b/src/data/pokemon-forms.ts @@ -684,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)), 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 aa85c29f551..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), @@ -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/status-effect.ts b/src/data/status-effect.ts index 4381db5c2c6..ffe32a02aeb 100644 --- a/src/data/status-effect.ts +++ b/src/data/status-effect.ts @@ -44,6 +44,10 @@ function getStatusEffectMessageKey(statusEffect: StatusEffect | undefined): stri } export function getStatusEffectObtainText(statusEffect: StatusEffect | undefined, pokemonNameWithAffix: string, sourceText?: string): string { + if (statusEffect === StatusEffect.NONE) { + return ""; + } + if (!sourceText) { const i18nKey = `${getStatusEffectMessageKey(statusEffect)}.obtain`as ParseKeys; return i18next.t(i18nKey, { pokemonNameWithAffix: pokemonNameWithAffix }); @@ -53,21 +57,33 @@ export function getStatusEffectObtainText(statusEffect: StatusEffect | undefined } export function getStatusEffectActivationText(statusEffect: StatusEffect, pokemonNameWithAffix: string): string { + if (statusEffect === StatusEffect.NONE) { + return ""; + } const i18nKey = `${getStatusEffectMessageKey(statusEffect)}.activation` as ParseKeys; return i18next.t(i18nKey, { pokemonNameWithAffix: pokemonNameWithAffix }); } export function getStatusEffectOverlapText(statusEffect: StatusEffect, pokemonNameWithAffix: string): string { + if (statusEffect === StatusEffect.NONE) { + return ""; + } const i18nKey = `${getStatusEffectMessageKey(statusEffect)}.overlap` as ParseKeys; return i18next.t(i18nKey, { pokemonNameWithAffix: pokemonNameWithAffix }); } export function getStatusEffectHealText(statusEffect: StatusEffect, pokemonNameWithAffix: string): string { + if (statusEffect === StatusEffect.NONE) { + return ""; + } const i18nKey = `${getStatusEffectMessageKey(statusEffect)}.heal` as ParseKeys; return i18next.t(i18nKey, { pokemonNameWithAffix: pokemonNameWithAffix }); } export function getStatusEffectDescriptor(statusEffect: StatusEffect): string { + if (statusEffect === StatusEffect.NONE) { + return ""; + } const i18nKey = `${getStatusEffectMessageKey(statusEffect)}.description` as ParseKeys; return i18next.t(i18nKey); } diff --git a/src/data/trainer-config.ts b/src/data/trainer-config.ts index ac33f26de9e..82593a4e08b 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 @@ -592,7 +597,7 @@ export class TrainerConfig { case "flare": { return { [TrainerPoolTier.COMMON]: [Species.FLETCHLING, Species.LITLEO, Species.INKAY, Species.HELIOPTILE, Species.ELECTRIKE, Species.SKORUPI, Species.PURRLOIN, Species.CLAWITZER, Species.PANCHAM, Species.ESPURR, Species.BUNNELBY], - [TrainerPoolTier.UNCOMMON]: [Species.LITWICK, Species.SNEASEL, Species.PUMPKABOO, Species.PHANTUMP, Species.HONEDGE, Species.BINACLE, Species.BERGMITE, Species.HOUNDOUR, Species.SKRELP, Species.SLIGGOO], + [TrainerPoolTier.UNCOMMON]: [Species.LITWICK, Species.SNEASEL, Species.PUMPKABOO, Species.PHANTUMP, Species.HONEDGE, Species.BINACLE, Species.HOUNDOUR, Species.SKRELP, Species.SLIGGOO], [TrainerPoolTier.RARE]: [Species.NOIVERN, Species.HISUI_AVALUGG, Species.HISUI_SLIGGOO] }; } @@ -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.GLIMMET, Species.BULBASAUR ] + }; + } + 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.GALAR_PONYTA, Species.POPPLIO ] + }; + } + 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 @@ -1350,7 +1509,7 @@ export const trainerConfigs: TrainerConfigs = { .setSpeciesPools({ [TrainerPoolTier.COMMON]: [Species.CARVANHA, Species.WAILMER, Species.ZIGZAGOON, Species.LOTAD, Species.CORPHISH, Species.SPHEAL, Species.REMORAID, Species.QWILFISH, Species.BARBOACH], [TrainerPoolTier.UNCOMMON]: [Species.CLAMPERL, Species.CHINCHOU, Species.WOOPER, Species.WINGULL, Species.TENTACOOL, Species.AZURILL, Species.CLOBBOPUS, Species.HORSEA], - [TrainerPoolTier.RARE]: [Species.MANTINE, Species.DHELMISE, Species.HISUI_QWILFISH, Species.ARROKUDA, Species.PALDEA_WOOPER, Species.SKRELP], + [TrainerPoolTier.RARE]: [Species.MANTYKE, Species.DHELMISE, Species.HISUI_QWILFISH, Species.ARROKUDA, Species.PALDEA_WOOPER, Species.SKRELP], [TrainerPoolTier.SUPER_RARE]: [Species.DONDOZO, Species.BASCULEGION] }), [TrainerType.MATT]: new TrainerConfig(++t).setMoneyMultiplier(1.5).initForEvilTeamAdmin("aqua_admin", "aqua", [Species.SHARPEDO]).setEncounterBgm(TrainerType.PLASMA_GRUNT).setBattleBgm("battle_plasma_grunt").setMixedBattleBgm("battle_aqua_magma_grunt").setVictoryBgm("victory_team_plasma").setPartyTemplateFunc(scene => getEvilGruntPartyTemplate(scene)), @@ -1368,8 +1527,8 @@ export const trainerConfigs: TrainerConfigs = { [TrainerType.PLASMA_GRUNT]: new TrainerConfig(++t).setHasGenders("Plasma Grunt Female").setHasDouble("Plasma Grunts").setMoneyMultiplier(1.0).setEncounterBgm(TrainerType.PLASMA_GRUNT).setBattleBgm("battle_plasma_grunt").setMixedBattleBgm("battle_plasma_grunt").setVictoryBgm("victory_team_plasma").setPartyTemplateFunc(scene => getEvilGruntPartyTemplate(scene)) .setSpeciesPools({ [TrainerPoolTier.COMMON]: [Species.PATRAT, Species.LILLIPUP, Species.PURRLOIN, Species.SCRAFTY, Species.WOOBAT, Species.VANILLITE, Species.SANDILE, Species.TRUBBISH, Species.TYMPOLE], - [TrainerPoolTier.UNCOMMON]: [Species.FRILLISH, Species.VENIPEDE, Species.GOLETT, Species.TIMBURR, Species.DARUMAKA, Species.FOONGUS, Species.JOLTIK], - [TrainerPoolTier.RARE]: [Species.PAWNIARD, Species.RUFFLET, Species.VULLABY, Species.ZORUA, Species.DRILBUR, Species.KLINK, Species.CUBCHOO, Species.MIENFOO, Species.DURANT, Species.BOUFFALANT], + [TrainerPoolTier.UNCOMMON]: [Species.FRILLISH, Species.VENIPEDE, Species.GOLETT, Species.TIMBURR, Species.DARUMAKA, Species.FOONGUS, Species.JOLTIK, Species.CUBCHOO, Species.KLINK], + [TrainerPoolTier.RARE]: [Species.PAWNIARD, Species.RUFFLET, Species.VULLABY, Species.ZORUA, Species.DRILBUR, Species.MIENFOO, Species.DURANT, Species.BOUFFALANT], [TrainerPoolTier.SUPER_RARE]: [Species.DRUDDIGON, Species.HISUI_ZORUA, Species.AXEW, Species.DEINO] }), [TrainerType.ZINZOLIN]: new TrainerConfig(++t).setMoneyMultiplier(1.5).initForEvilTeamAdmin("plasma_sage", "plasma", [Species.CRYOGONAL]).setEncounterBgm(TrainerType.PLASMA_GRUNT).setBattleBgm("battle_plasma_grunt").setMixedBattleBgm("battle_plasma_grunt").setVictoryBgm("victory_team_plasma").setPartyTemplateFunc(scene => getEvilGruntPartyTemplate(scene)), @@ -1378,7 +1537,7 @@ export const trainerConfigs: TrainerConfigs = { .setSpeciesPools({ [TrainerPoolTier.COMMON]: [Species.FLETCHLING, Species.LITLEO, Species.PONYTA, Species.INKAY, Species.HOUNDOUR, Species.SKORUPI, Species.SCRAFTY, Species.CROAGUNK, Species.SCATTERBUG, Species.ESPURR], [TrainerPoolTier.UNCOMMON]: [Species.HELIOPTILE, Species.ELECTRIKE, Species.SKRELP, Species.PANCHAM, Species.PURRLOIN, Species.POOCHYENA, Species.BINACLE, Species.CLAUNCHER, Species.PUMPKABOO, Species.PHANTUMP], - [TrainerPoolTier.RARE]: [Species.LITWICK, Species.SNEASEL, Species.PAWNIARD, Species.BERGMITE, Species.SLIGGOO], + [TrainerPoolTier.RARE]: [Species.LITWICK, Species.SNEASEL, Species.PAWNIARD, Species.SLIGGOO], [TrainerPoolTier.SUPER_RARE]: [Species.NOIVERN, Species.HISUI_SLIGGOO, Species.HISUI_AVALUGG] }), [TrainerType.BRYONY]: new TrainerConfig(++t).setMoneyMultiplier(1.5).initForEvilTeamAdmin("flare_admin_female", "flare", [Species.LIEPARD]).setEncounterBgm(TrainerType.PLASMA_GRUNT).setBattleBgm("battle_plasma_grunt").setMixedBattleBgm("battle_flare_grunt").setVictoryBgm("victory_team_plasma").setPartyTemplateFunc(scene => getEvilGruntPartyTemplate(scene)), @@ -1386,15 +1545,15 @@ export const trainerConfigs: TrainerConfigs = { [TrainerType.AETHER_GRUNT]: new TrainerConfig(++t).setHasGenders("Aether Grunt Female").setHasDouble("Aether Grunts").setMoneyMultiplier(1.0).setEncounterBgm(TrainerType.PLASMA_GRUNT).setBattleBgm("battle_plasma_grunt").setMixedBattleBgm("battle_aether_grunt").setVictoryBgm("victory_team_plasma").setPartyTemplateFunc(scene => getEvilGruntPartyTemplate(scene)) .setSpeciesPools({ [TrainerPoolTier.COMMON]: [ Species.PIKIPEK, Species.ROCKRUFF, Species.ALOLA_DIGLETT, Species.ALOLA_EXEGGUTOR, Species.YUNGOOS, Species.CORSOLA, Species.ALOLA_GEODUDE, Species.ALOLA_RAICHU, Species.BOUNSWEET, Species.LILLIPUP, Species.KOMALA, Species.MORELULL, Species.COMFEY, Species.TOGEDEMARU], - [TrainerPoolTier.UNCOMMON]: [ Species.POLIWAG, Species.STUFFUL, Species.ORANGURU, Species.PASSIMIAN, Species.BRUXISH, Species.MINIOR, Species.WISHIWASHI, Species.CRABRAWLER, Species.CUTIEFLY, Species.ORICORIO, Species.MUDBRAY, Species.PYUKUMUKU, Species.ALOLA_MAROWAK], - [TrainerPoolTier.RARE]: [ Species.GALAR_CORSOLA, Species.ALOLA_SANDSHREW, Species.ALOLA_VULPIX, Species.TURTONATOR, Species.DRAMPA], + [TrainerPoolTier.UNCOMMON]: [ Species.POLIWAG, Species.STUFFUL, Species.ORANGURU, Species.PASSIMIAN, Species.BRUXISH, Species.MINIOR, Species.WISHIWASHI, Species.ALOLA_SANDSHREW, Species.ALOLA_VULPIX, Species.CRABRAWLER, Species.CUTIEFLY, Species.ORICORIO, Species.MUDBRAY, Species.PYUKUMUKU, Species.ALOLA_MAROWAK], + [TrainerPoolTier.RARE]: [ Species.GALAR_CORSOLA, Species.TURTONATOR, Species.MIMIKYU, Species.MAGNEMITE, Species.DRAMPA], [TrainerPoolTier.SUPER_RARE]: [Species.JANGMO_O, Species.PORYGON] }), [TrainerType.FABA]: new TrainerConfig(++t).setMoneyMultiplier(1.5).initForEvilTeamAdmin("aether_admin", "aether", [Species.HYPNO]).setEncounterBgm(TrainerType.PLASMA_GRUNT).setBattleBgm("battle_plasma_grunt").setMixedBattleBgm("battle_aether_grunt").setVictoryBgm("victory_team_plasma").setPartyTemplateFunc(scene => getEvilGruntPartyTemplate(scene)), [TrainerType.SKULL_GRUNT]: new TrainerConfig(++t).setHasGenders("Skull Grunt Female").setHasDouble("Skull Grunts").setMoneyMultiplier(1.0).setEncounterBgm(TrainerType.PLASMA_GRUNT).setBattleBgm("battle_plasma_grunt").setMixedBattleBgm("battle_skull_grunt").setVictoryBgm("victory_team_plasma").setPartyTemplateFunc(scene => getEvilGruntPartyTemplate(scene)) .setSpeciesPools({ - [TrainerPoolTier.COMMON]: [ Species.SALANDIT, Species.ALOLA_RATTATA, Species.EKANS, Species.ALOLA_MEOWTH, Species.SCRAGGY, Species.KOFFING, Species.ALOLA_GRIMER, Species.MAREANIE, Species.SPINARAK, Species.TRUBBISH], - [TrainerPoolTier.UNCOMMON]: [ Species.FOMANTIS, Species.SABLEYE, Species.SANDILE, Species.HOUNDOUR, Species.ALOLA_MAROWAK, Species.GASTLY, Species.PANCHAM, Species.DROWZEE, Species.ZUBAT, Species.VENIPEDE, Species.VULLABY], + [TrainerPoolTier.COMMON]: [ Species.SALANDIT, Species.ALOLA_RATTATA, Species.EKANS, Species.ALOLA_MEOWTH, Species.SCRAGGY, Species.KOFFING, Species.ALOLA_GRIMER, Species.MAREANIE, Species.SPINARAK, Species.TRUBBISH, Species.DROWZEE], + [TrainerPoolTier.UNCOMMON]: [ Species.FOMANTIS, Species.SABLEYE, Species.SANDILE, Species.HOUNDOUR, Species.ALOLA_MAROWAK, Species.GASTLY, Species.PANCHAM, Species.ZUBAT, Species.VENIPEDE, Species.VULLABY], [TrainerPoolTier.RARE]: [Species.SANDYGAST, Species.PAWNIARD, Species.MIMIKYU, Species.DHELMISE, Species.WISHIWASHI, Species.NYMBLE], [TrainerPoolTier.SUPER_RARE]: [Species.GRUBBIN, Species.DEWPIDER] }), @@ -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"), @@ -1725,7 +1916,14 @@ export const trainerConfigs: TrainerConfigs = { p.formIndex = 1; // Mega Kangaskhan p.generateName(); })) - .setPartyMemberFunc(4, getRandomPartyMemberFunc([Species.GASTRODON, Species.SEISMITOAD])) + .setPartyMemberFunc(4, getRandomPartyMemberFunc([Species.GASTRODON, Species.SEISMITOAD], TrainerSlot.TRAINER, true, p => { + //Storm Drain Gastrodon, Water Absorb Seismitoad + if (p.species.speciesId === Species.GASTRODON) { + p.abilityIndex = 0; + } else if (p.species.speciesId === Species.SEISMITOAD) { + p.abilityIndex = 2; + } + })) .setPartyMemberFunc(5, getRandomPartyMemberFunc([Species.MEWTWO], TrainerSlot.TRAINER, true, p => { p.setBoss(true, 2); p.generateAndPopulateMoveset(); @@ -1869,7 +2067,7 @@ export const trainerConfigs: TrainerConfigs = { p.setBoss(true, 2); p.generateAndPopulateMoveset(); p.pokeball = PokeballType.ULTRA_BALL; - p.formIndex = Utils.randSeedInt(5, 1); // Shock, Burn, Chill, or Douse Drive + p.formIndex = Utils.randSeedInt(4, 1); // Shock, Burn, Chill, or Douse Drive })) .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.BASCULEGION, Species.JELLICENT ], TrainerSlot.TRAINER, true, p => { p.generateAndPopulateMoveset(); @@ -1962,9 +2160,23 @@ export const trainerConfigs: TrainerConfigs = { p.pokeball = PokeballType.MASTER_BALL; })), [TrainerType.GUZMA]: new TrainerConfig(++t).setName("Guzma").initForEvilTeamLeader("Skull Boss", []).setMixedBattleBgm("battle_skull_boss").setVictoryBgm("victory_team_plasma") - .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.LOKIX, Species.YANMEGA ])) + .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.LOKIX, Species.YANMEGA ], TrainerSlot.TRAINER, true, p => { + //Tinted Lens Lokix, Tinted Lens Yanmega + if (p.species.speciesId === Species.LOKIX) { + p.abilityIndex = 2; + } else if (p.species.speciesId === Species.YANMEGA) { + p.abilityIndex = 1; + } + })) .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.HERACROSS ])) - .setPartyMemberFunc(2, getRandomPartyMemberFunc([ Species.SCIZOR, Species.KLEAVOR ])) + .setPartyMemberFunc(2, getRandomPartyMemberFunc([ Species.SCIZOR, Species.KLEAVOR ], TrainerSlot.TRAINER, true, p => { + //Technician Scizor, Sharpness Kleavor + if (p.species.speciesId === Species.SCIZOR) { + p.abilityIndex = 1; + } else if (p.species.speciesId === Species.KLEAVOR) { + p.abilityIndex = 2; + } + })) .setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.GALVANTULA, Species.VIKAVOLT])) .setPartyMemberFunc(4, getRandomPartyMemberFunc([ Species.PINSIR ], TrainerSlot.TRAINER, true, p => { p.generateAndPopulateMoveset(); @@ -1984,25 +2196,32 @@ export const trainerConfigs: TrainerConfigs = { p.abilityIndex = 2; //Anticipation p.pokeball = PokeballType.ULTRA_BALL; })) - .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.HISUI_SAMUROTT, Species.CRAWDAUNT ], TrainerSlot.TRAINER, true, p => { + .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.SCIZOR, Species.KLEAVOR ], TrainerSlot.TRAINER, true, p => { + //Technician Scizor, Sharpness Kleavor + if (p.species.speciesId === Species.SCIZOR) { + p.abilityIndex = 1; + } else if (p.species.speciesId === Species.KLEAVOR) { + p.abilityIndex = 2; + } + })) + .setPartyMemberFunc(2, getRandomPartyMemberFunc([ Species.HISUI_SAMUROTT, Species.CRAWDAUNT ], TrainerSlot.TRAINER, true, p => { p.abilityIndex = 2; //Sharpness Hisui Samurott, Adaptability Crawdaunt })) - .setPartyMemberFunc(2, getRandomPartyMemberFunc([ Species.SCIZOR, Species.KLEAVOR ])) - .setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.PINSIR ], TrainerSlot.TRAINER, true, p => { + .setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.BUZZWOLE ], TrainerSlot.TRAINER, true, p => { p.generateAndPopulateMoveset(); + p.pokeball = PokeballType.ROGUE_BALL; + })) + .setPartyMemberFunc(4, getRandomPartyMemberFunc([ Species.XURKITREE ], TrainerSlot.TRAINER, true, p => { + p.setBoss(true, 2); + p.generateAndPopulateMoveset(); + p.pokeball = PokeballType.ROGUE_BALL; + })) + .setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.PINSIR ], TrainerSlot.TRAINER, true, p => { + p.setBoss(true, 2); p.formIndex = 1; + p.generateAndPopulateMoveset(); 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.ROGUE_BALL; - })) - .setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.XURKITREE ], TrainerSlot.TRAINER, true, p => { - p.setBoss(true, 2); - p.generateAndPopulateMoveset(); - 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 ])) @@ -2018,17 +2237,16 @@ export const trainerConfigs: TrainerConfigs = { 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.MELMETAL ], TrainerSlot.TRAINER, true, p => { + .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.ARCHALUDON ], 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 Dracovish, Hustle Dracozolt })) - .setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.ARCHALUDON ])) + .setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.MELMETAL ])) .setPartyMemberFunc(4, getRandomPartyMemberFunc([ Species.GALAR_ARTICUNO, Species.GALAR_ZAPDOS, Species.GALAR_MOLTRES ], TrainerSlot.TRAINER, true, p => { p.setBoss(true, 2); p.generateAndPopulateMoveset(); @@ -2041,4 +2259,214 @@ export const trainerConfigs: TrainerConfigs = { 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 657f0d47375..d606ae319f7 100644 --- a/src/enums/battler-tag-type.ts +++ b/src/enums/battler-tag-type.ts @@ -79,4 +79,7 @@ export enum BattlerTagType { TAR_SHOT = "TAR_SHOT", BURNED_UP = "BURNED_UP", DOUBLE_SHOCKED = "DOUBLE_SHOCKED", + AUTOTOMIZED = "AUTOTOMIZED", + 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/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..dc9ad84f09d 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"; @@ -33,6 +33,7 @@ export class Arena { public tags: ArenaTag[]; public bgm: string; public ignoreAbilities: boolean; + public ignoringEffectSource: BattlerIndex | null; private lastTimeOfDay: TimeOfDay; @@ -76,21 +77,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 +150,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 +158,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 +321,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? @@ -569,8 +570,9 @@ export class Arena { } } - setIgnoreAbilities(ignoreAbilities: boolean = true): void { + setIgnoreAbilities(ignoreAbilities: boolean, ignoringEffectSource: BattlerIndex | null = null): void { this.ignoreAbilities = ignoreAbilities; + this.ignoringEffectSource = ignoreAbilities ? ignoringEffectSource : null; } /** @@ -746,7 +748,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 +764,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 +776,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 +790,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..70588da5d44 --- /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 c648ff485b7..14f93809414 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -3,24 +3,24 @@ 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, SubstituteTag, 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, AutotomizedTag } 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"; +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, PostSetStatusAbAttr, applyPostSetStatusAbAttrs } from "../data/ability"; import PokemonData from "../system/pokemon-data"; import { BattlerIndex } from "../battle"; import { Mode } from "../ui/ui"; @@ -60,6 +60,7 @@ import { ToggleDoublePositionPhase } from "#app/phases/toggle-double-position-ph 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, @@ -94,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; @@ -106,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; @@ -113,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; @@ -138,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) { @@ -188,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; @@ -197,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); @@ -218,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 { @@ -230,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) { @@ -319,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 { @@ -532,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 { @@ -561,8 +586,10 @@ 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; } @@ -572,8 +599,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { // Resetting properties should not be shown on the field this.setVisible(false); - // Reset field position - this.setFieldPosition(FieldPosition.CENTER); + // Remove the offset from having a Substitute active if (this.isOffsetBySubstitute()) { this.x -= this.getSubstituteOffset()[0]; this.y -= this.getSubstituteOffset()[1]; @@ -850,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: @@ -915,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); @@ -955,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 { @@ -1122,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); @@ -1131,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); @@ -1184,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) { @@ -1207,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)) { @@ -1222,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)) { @@ -1280,7 +1364,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (this.isFusion() && ability.hasAttr(NoFusionAbilityAbAttr)) { return false; } - if (this.scene?.arena.ignoreAbilities && ability.isIgnorable) { + const arena = this.scene?.arena; + if (arena.ignoreAbilities && arena.ignoringEffectSource !== this.getBattlerIndex() && ability.isIgnorable) { return false; } if (this.summonData?.abilitySuppressed && !ability.hasAttr(UnsuppressableAbilityAbAttr)) { @@ -1342,11 +1427,23 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return false; } + /** + * Gets the weight of the Pokemon with subtractive modifiers (Autotomize) happening first + * and then multiplicative modifiers happening after (Heavy Metal and Light Metal) + * @returns the kg of the Pokemon (minimum of 0.1) + */ getWeight(): number { - const weight = new Utils.NumberHolder(this.species.weight); + const autotomizedTag = this.getTag(AutotomizedTag); + let weightRemoved = 0; + if (!Utils.isNullOrUndefined(autotomizedTag)) { + weightRemoved = 100 * autotomizedTag!.autotomizeCount; + } + const minWeight = 0.1; + const weight = new Utils.NumberHolder(this.species.weight - weightRemoved); + // This will trigger the ability overlay so only call this function when necessary applyAbAttrs(WeightMultiplierAbAttr, this, null, false, weight); - return weight.value; + return Math.max(minWeight, weight.value); } /** @@ -1697,6 +1794,15 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } + /** + * Get a list of all egg moves + * + * @returns list of egg moves + */ + getEggMoves() : Moves[] | undefined { + return speciesEggMoves[this.getSpeciesForm().getRootSpeciesId(true)]; + } + setMove(moveIndex: integer, moveId: Moves): void { const move = moveId ? new PokemonMove(moveId) : null; this.moveset[moveIndex] = move; @@ -1751,6 +1857,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) @@ -1831,6 +1973,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(); @@ -2034,7 +2177,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}`, @@ -2056,11 +2199,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 { @@ -2138,10 +2281,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); @@ -2158,7 +2303,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); } @@ -2223,13 +2370,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; @@ -2247,291 +2449,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) { - this.lapseTags(BattlerTagLapseType.HIT); - - const substitute = this.getTag(SubstituteTag); - if (substitute && move.hitsSubstitute(source, this)) { - substitute.hp -= damage.value; - damage.value = 0; - } - 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; - } - } - - if (damage.value > 0) { - 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.destroySubstitute(); - 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 = (cancelled.value || 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; + } } /** @@ -2777,16 +3018,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; @@ -3113,7 +3378,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } if (asPhase) { - this.scene.unshiftPhase(new ObtainStatusEffectPhase(this.scene, this.getBattlerIndex(), effect, cureTurn, sourceText!, sourcePokemon!)); // TODO: are these bangs correct? + this.scene.unshiftPhase(new ObtainStatusEffectPhase(this.scene, this.getBattlerIndex(), effect, cureTurn, sourceText, sourcePokemon)); return true; } @@ -3147,6 +3412,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (effect !== StatusEffect.FAINT) { this.scene.triggerPokemonFormChange(this, SpeciesFormChangeStatusEffectTrigger, true); + applyPostSetStatusAbAttrs(PostSetStatusAbAttr, this, effect, sourcePokemon); } return true; @@ -3190,6 +3456,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.updateFusionPalette(); } this.summonData = new PokemonSummonData(); + this.setSwitchOutStatus(false); if (!this.battleData) { this.resetBattleData(); } @@ -3595,6 +3862,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.hideInfo(); } this.scene.field.remove(this); + this.setSwitchOutStatus(true); this.scene.triggerPokemonFormChange(this, SpeciesFormChangeActiveTrigger, true); } @@ -3618,6 +3886,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 { @@ -3883,6 +4170,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); @@ -3910,6 +4203,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; @@ -3917,6 +4211,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); @@ -3984,6 +4279,7 @@ 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; } @@ -4202,7 +4498,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. @@ -4223,6 +4519,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}). @@ -4493,8 +4822,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)); @@ -4575,6 +4911,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); @@ -4626,6 +4963,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 ]; @@ -4693,6 +5031,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..b577ba542bd 100644 --- a/src/loading-scene.ts +++ b/src/loading-scene.ts @@ -1,5 +1,4 @@ import { GachaType } from "./enums/gacha-types"; -import { trainerConfigs } from "./data/trainer-config"; import { getBiomeHasProps } from "./field/arena"; import CacheBustedLoaderPlugin from "./plugins/cache-busted-loader-plugin"; import { SceneBase } from "./scene-base"; @@ -21,7 +20,7 @@ import i18next from "i18next"; 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"; @@ -207,14 +206,6 @@ export class LoadingScene extends SceneBase { this.loadAtlas("trainer_f_back", "trainer"); this.loadAtlas("trainer_f_back_pb", "trainer"); - Utils.getEnumValues(TrainerType).map(tt => { - const config = trainerConfigs[tt]; - this.loadAtlas(config.getSpriteKey(), "trainer"); - if (config.doubleOnly || config.hasDouble) { - this.loadAtlas(config.getSpriteKey(true), "trainer"); - } - }); - // Load character sprites this.loadAtlas("c_rival_m", "character", "rival_m"); this.loadAtlas("c_rival_f", "character", "rival_f"); @@ -240,12 +231,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 +280,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 +359,7 @@ export class LoadingScene extends SceneBase { initMoves(); initAbilities(); initChallenges(); + initMysteryEncounters(); } loadLoadingScreen() { diff --git a/src/locales/ca_ES/config.ts b/src/locales/ca_ES/config.ts index 4d8f6c9dc59..b9f92bb8f49 100644 --- a/src/locales/ca_ES/config.ts +++ b/src/locales/ca_ES/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 "./mystery-encounters/mysterious-chest-dialogue.json"; +import mysteriousChallengers from "./mystery-encounters/mysterious-challengers-dialogue.json"; +import darkDeal from "./mystery-encounters/dark-deal-dialogue.json"; +import departmentStoreSale from "./mystery-encounters/department-store-sale-dialogue.json"; +import fieldTrip from "./mystery-encounters/field-trip-dialogue.json"; +import fieryFallout from "./mystery-encounters/fiery-fallout-dialogue.json"; +import fightOrFlight from "./mystery-encounters/fight-or-flight-dialogue.json"; +import safariZone from "./mystery-encounters/safari-zone-dialogue.json"; +import shadyVitaminDealer from "./mystery-encounters/shady-vitamin-dealer-dialogue.json"; +import slumberingSnorlax from "./mystery-encounters/slumbering-snorlax-dialogue.json"; +import trainingSession from "./mystery-encounters/training-session-dialogue.json"; +import theStrongStuff from "./mystery-encounters/the-strong-stuff-dialogue.json"; +import pokemonSalesman from "./mystery-encounters/the-pokemon-salesman-dialogue.json"; +import offerYouCantRefuse from "./mystery-encounters/an-offer-you-cant-refuse-dialogue.json"; +import delibirdy from "./mystery-encounters/delibirdy-dialogue.json"; +import absoluteAvarice from "./mystery-encounters/absolute-avarice-dialogue.json"; +import aTrainersTest from "./mystery-encounters/a-trainers-test-dialogue.json"; +import trashToTreasure from "./mystery-encounters/trash-to-treasure-dialogue.json"; +import berriesAbound from "./mystery-encounters/berries-abound-dialogue.json"; +import clowningAround from "./mystery-encounters/clowning-around-dialogue.json"; +import partTimer from "./mystery-encounters/part-timer-dialogue.json"; +import dancingLessons from "./mystery-encounters/dancing-lessons-dialogue.json"; +import weirdDream from "./mystery-encounters/weird-dream-dialogue.json"; +import theWinstrateChallenge from "./mystery-encounters/the-winstrate-challenge-dialogue.json"; +import teleportingHijinks from "./mystery-encounters/teleporting-hijinks-dialogue.json"; +import bugTypeSuperfan from "./mystery-encounters/bug-type-superfan-dialogue.json"; +import funAndGames from "./mystery-encounters/fun-and-games-dialogue.json"; +import uncommonBreed from "./mystery-encounters/uncommon-breed-dialogue.json"; +import globalTradeSystem from "./mystery-encounters/global-trade-system-dialogue.json"; +import expertPokemonBreeder from "./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 caEsConfig = { ability, abilityTriggers, @@ -110,4 +152,40 @@ export const caEsConfig = { modifierSelectUiHandler, moveTriggers, 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/ca_ES/mystery-encounter-messages.json b/src/locales/ca_ES/mystery-encounter-messages.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/src/locales/ca_ES/mystery-encounter-messages.json @@ -0,0 +1 @@ +{} diff --git a/src/locales/ca_ES/mystery-encounters/a-trainers-test-dialogue.json b/src/locales/ca_ES/mystery-encounters/a-trainers-test-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/ca_ES/mystery-encounters/a-trainers-test-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/ca_ES/mystery-encounters/absolute-avarice-dialogue.json b/src/locales/ca_ES/mystery-encounters/absolute-avarice-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/ca_ES/mystery-encounters/absolute-avarice-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/ca_ES/mystery-encounters/an-offer-you-cant-refuse-dialogue.json b/src/locales/ca_ES/mystery-encounters/an-offer-you-cant-refuse-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/ca_ES/mystery-encounters/an-offer-you-cant-refuse-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/ca_ES/mystery-encounters/berries-abound-dialogue.json b/src/locales/ca_ES/mystery-encounters/berries-abound-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/ca_ES/mystery-encounters/berries-abound-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/ca_ES/mystery-encounters/bug-type-superfan-dialogue.json b/src/locales/ca_ES/mystery-encounters/bug-type-superfan-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/ca_ES/mystery-encounters/bug-type-superfan-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/ca_ES/mystery-encounters/clowning-around-dialogue.json b/src/locales/ca_ES/mystery-encounters/clowning-around-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/ca_ES/mystery-encounters/clowning-around-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/ca_ES/mystery-encounters/dancing-lessons-dialogue.json b/src/locales/ca_ES/mystery-encounters/dancing-lessons-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/ca_ES/mystery-encounters/dancing-lessons-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/ca_ES/mystery-encounters/dark-deal-dialogue.json b/src/locales/ca_ES/mystery-encounters/dark-deal-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/ca_ES/mystery-encounters/dark-deal-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/ca_ES/mystery-encounters/delibirdy-dialogue.json b/src/locales/ca_ES/mystery-encounters/delibirdy-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/ca_ES/mystery-encounters/delibirdy-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/ca_ES/mystery-encounters/department-store-sale-dialogue.json b/src/locales/ca_ES/mystery-encounters/department-store-sale-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/ca_ES/mystery-encounters/department-store-sale-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/ca_ES/mystery-encounters/field-trip-dialogue.json b/src/locales/ca_ES/mystery-encounters/field-trip-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/ca_ES/mystery-encounters/field-trip-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/ca_ES/mystery-encounters/fiery-fallout-dialogue.json b/src/locales/ca_ES/mystery-encounters/fiery-fallout-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/ca_ES/mystery-encounters/fiery-fallout-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/ca_ES/mystery-encounters/fight-or-flight-dialogue.json b/src/locales/ca_ES/mystery-encounters/fight-or-flight-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/ca_ES/mystery-encounters/fight-or-flight-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/ca_ES/mystery-encounters/fun-and-games-dialogue.json b/src/locales/ca_ES/mystery-encounters/fun-and-games-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/ca_ES/mystery-encounters/fun-and-games-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/ca_ES/mystery-encounters/global-trade-system-dialogue.json b/src/locales/ca_ES/mystery-encounters/global-trade-system-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/ca_ES/mystery-encounters/global-trade-system-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/ca_ES/mystery-encounters/lost-at-sea-dialogue.json b/src/locales/ca_ES/mystery-encounters/lost-at-sea-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/ca_ES/mystery-encounters/lost-at-sea-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/ca_ES/mystery-encounters/mysterious-challengers-dialogue.json b/src/locales/ca_ES/mystery-encounters/mysterious-challengers-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/ca_ES/mystery-encounters/mysterious-challengers-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/ca_ES/mystery-encounters/mysterious-chest-dialogue.json b/src/locales/ca_ES/mystery-encounters/mysterious-chest-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/ca_ES/mystery-encounters/mysterious-chest-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/ca_ES/mystery-encounters/part-timer-dialogue.json b/src/locales/ca_ES/mystery-encounters/part-timer-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/ca_ES/mystery-encounters/part-timer-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/ca_ES/mystery-encounters/safari-zone-dialogue.json b/src/locales/ca_ES/mystery-encounters/safari-zone-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/ca_ES/mystery-encounters/safari-zone-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/ca_ES/mystery-encounters/shady-vitamin-dealer-dialogue.json b/src/locales/ca_ES/mystery-encounters/shady-vitamin-dealer-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/ca_ES/mystery-encounters/shady-vitamin-dealer-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/ca_ES/mystery-encounters/slumbering-snorlax-dialogue.json b/src/locales/ca_ES/mystery-encounters/slumbering-snorlax-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/ca_ES/mystery-encounters/slumbering-snorlax-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/ca_ES/mystery-encounters/teleporting-hijinks-dialogue.json b/src/locales/ca_ES/mystery-encounters/teleporting-hijinks-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/ca_ES/mystery-encounters/teleporting-hijinks-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/ca_ES/mystery-encounters/the-expert-pokemon-breeder-dialogue.json b/src/locales/ca_ES/mystery-encounters/the-expert-pokemon-breeder-dialogue.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/src/locales/ca_ES/mystery-encounters/the-expert-pokemon-breeder-dialogue.json @@ -0,0 +1 @@ +{} diff --git a/src/locales/ca_ES/mystery-encounters/the-pokemon-salesman-dialogue.json b/src/locales/ca_ES/mystery-encounters/the-pokemon-salesman-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/ca_ES/mystery-encounters/the-pokemon-salesman-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/ca_ES/mystery-encounters/the-strong-stuff-dialogue.json b/src/locales/ca_ES/mystery-encounters/the-strong-stuff-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/ca_ES/mystery-encounters/the-strong-stuff-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/ca_ES/mystery-encounters/the-winstrate-challenge-dialogue.json b/src/locales/ca_ES/mystery-encounters/the-winstrate-challenge-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/ca_ES/mystery-encounters/the-winstrate-challenge-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/ca_ES/mystery-encounters/training-session-dialogue.json b/src/locales/ca_ES/mystery-encounters/training-session-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/ca_ES/mystery-encounters/training-session-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/ca_ES/mystery-encounters/trash-to-treasure-dialogue.json b/src/locales/ca_ES/mystery-encounters/trash-to-treasure-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/ca_ES/mystery-encounters/trash-to-treasure-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/ca_ES/mystery-encounters/uncommon-breed-dialogue.json b/src/locales/ca_ES/mystery-encounters/uncommon-breed-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/ca_ES/mystery-encounters/uncommon-breed-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/ca_ES/mystery-encounters/weird-dream-dialogue.json b/src/locales/ca_ES/mystery-encounters/weird-dream-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/ca_ES/mystery-encounters/weird-dream-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file 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/achv.json b/src/locales/de/achv.json index 21a1d89f9d6..4fa48d8cfdc 100644 --- a/src/locales/de/achv.json +++ b/src/locales/de/achv.json @@ -273,5 +273,9 @@ "INVERSE_BATTLE": { "name": "Spieglein, Spieglein an der Wand", "description": "Schließe die 'Umkehrkampf' Herausforderung ab" + }, + "BREEDERS_IN_SPACE": { + "name": "Züchter im Weltall!", + "description": "Besiege die Pokémon-Züchter-Expertin im Stratosphären Biome." } } diff --git a/src/locales/de/battle.json b/src/locales/de/battle.json index 38e36d4b2da..93b30e10fe5 100644 --- a/src/locales/de/battle.json +++ b/src/locales/de/battle.json @@ -14,6 +14,10 @@ "moneyWon": "Du gewinnst {{moneyAmount}} ₽!", "moneyPickedUp": "Du hebst {{moneyAmount}} ₽ auf!", "pokemonCaught": "{{pokemonName}} wurde gefangen!", + "pokemonObtained": "Du erhältst {{pokemonName}}!", + "pokemonBrokeFree": "Mist!\nDas Pokémon hat sich befreit!", + "pokemonFled": "Das wilde {{pokemonName}} ist geflohen!", + "playerFled": "Du bist vor dem wilden {{pokemonName}} geflohen!", "addedAsAStarter": "{{pokemonName}} wurde als Starterpokémon hinzugefügt!", "partyFull": "Dein Team ist voll. Möchtest du ein Pokémon durch {{pokemonName}} ersetzen?", "pokemon": "Pokémon", @@ -49,6 +53,7 @@ "noPokeballTrainer": "Du kannst das Pokémon eines anderen Trainers nicht fangen!", "noPokeballMulti": "Du kannst erst einen Pokéball werfen, wenn nur noch ein Pokémon übrig ist!", "noPokeballStrong": "Das Ziel-Pokémon ist zu stark, um gefangen zu werden! Du musst es zuerst schwächen!", + "noPokeballMysteryEncounter": "You aren't able to\ncatch this Pokémon!", "noEscapeForce": "Eine unsichtbare Kraft verhindert die Flucht.", "noEscapeTrainer": "Du kannst nicht aus einem Trainerkampf fliehen!", "noEscapePokemon": "{{moveName}} von {{pokemonName}} verhindert {{escapeVerb}}!", @@ -96,5 +101,8 @@ "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?", + "mysteryEncounterAppeared": "Was ist das?", + "battlerTagsHealBlock": "{{pokemonNameWithAffix}} kann nicht geheilt werden, da die Heilung blockiert wird!", + "battlerTagsHealBlockOnRemove": "{{pokemonNameWithAffix}} kann wieder geheilt werden!" } diff --git a/src/locales/de/bgm-name.json b/src/locales/de/bgm-name.json index 1eab276a70a..dbcb6ee6929 100644 --- a/src/locales/de/bgm-name.json +++ b/src/locales/de/bgm-name.json @@ -77,23 +77,26 @@ "end_summit": "PMDDX Gipfel des Himmelturms", "battle_rocket_grunt": "HGSS Vs. Team Rocket Rüpel", "battle_aqua_magma_grunt": "ORAS Vs. Team Aqua & Magma", - "battle_galactic_grunt": "BDSP Vs. Team Galaktik Rüpel", + "battle_galactic_grunt": "SDLP Vs. Team Galaktik Rüpel", "battle_plasma_grunt": "SW Vs. Team Plasma Rüpel", "battle_flare_grunt": "XY Vs. Team Flare Rüpel", "battle_aether_grunt": "SM Vs. Æther Foundation", "battle_skull_grunt": "SM Vs. Team Skull Rüpel", "battle_macro_grunt": "SWSH Vs. Trainer", - "battle_galactic_admin": "BDSP Vs. Team Galactic Commander", + "battle_star_grunt": "KAPU Vs. Team Star", + "battle_galactic_admin": "SDLP 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", + "battle_galactic_boss": "SDLP Vs. Zyrus", "battle_plasma_boss": "S2W2 Vs. G-Cis", "battle_flare_boss": "XY Vs. Flordelis", "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)", @@ -146,5 +149,11 @@ "encounter_youngster": "SW Trainerblicke treffen sich (Knirps)", "heal": "SW Pokémon-Heilung", "menu": "PMD Erkundungsteam Himmel Willkommen in der Welt der Pokémon!", - "title": "PMD Erkundungsteam Himmel Top-Menü-Thema" + "title": "PMD Erkundungsteam Himmel Top-Menü-Thema", + + "mystery_encounter_weird_dream": "PMD Erkundungsteam Himmel Zeitturmspitze", + "mystery_encounter_fun_and_games": "PMD Erkundungsteam Himmel Gildenmeister Knuddeluff", + "mystery_encounter_gen_5_gts": "SW GTS", + "mystery_encounter_gen_6_gts": "XY GTS", + "mystery_encounter_delibirdy": "Firel - DeliDelivery!" } diff --git a/src/locales/de/config.ts b/src/locales/de/config.ts index 772bfb6d1d5..582272d4087 100644 --- a/src/locales/de/config.ts +++ b/src/locales/de/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 "./mystery-encounters/mysterious-chest-dialogue.json"; +import mysteriousChallengers from "./mystery-encounters/mysterious-challengers-dialogue.json"; +import darkDeal from "./mystery-encounters/dark-deal-dialogue.json"; +import departmentStoreSale from "./mystery-encounters/department-store-sale-dialogue.json"; +import fieldTrip from "./mystery-encounters/field-trip-dialogue.json"; +import fieryFallout from "./mystery-encounters/fiery-fallout-dialogue.json"; +import fightOrFlight from "./mystery-encounters/fight-or-flight-dialogue.json"; +import safariZone from "./mystery-encounters/safari-zone-dialogue.json"; +import shadyVitaminDealer from "./mystery-encounters/shady-vitamin-dealer-dialogue.json"; +import slumberingSnorlax from "./mystery-encounters/slumbering-snorlax-dialogue.json"; +import trainingSession from "./mystery-encounters/training-session-dialogue.json"; +import theStrongStuff from "./mystery-encounters/the-strong-stuff-dialogue.json"; +import pokemonSalesman from "./mystery-encounters/the-pokemon-salesman-dialogue.json"; +import offerYouCantRefuse from "./mystery-encounters/an-offer-you-cant-refuse-dialogue.json"; +import delibirdy from "./mystery-encounters/delibirdy-dialogue.json"; +import absoluteAvarice from "./mystery-encounters/absolute-avarice-dialogue.json"; +import aTrainersTest from "./mystery-encounters/a-trainers-test-dialogue.json"; +import trashToTreasure from "./mystery-encounters/trash-to-treasure-dialogue.json"; +import berriesAbound from "./mystery-encounters/berries-abound-dialogue.json"; +import clowningAround from "./mystery-encounters/clowning-around-dialogue.json"; +import partTimer from "./mystery-encounters/part-timer-dialogue.json"; +import dancingLessons from "./mystery-encounters/dancing-lessons-dialogue.json"; +import weirdDream from "./mystery-encounters/weird-dream-dialogue.json"; +import theWinstrateChallenge from "./mystery-encounters/the-winstrate-challenge-dialogue.json"; +import teleportingHijinks from "./mystery-encounters/teleporting-hijinks-dialogue.json"; +import bugTypeSuperfan from "./mystery-encounters/bug-type-superfan-dialogue.json"; +import funAndGames from "./mystery-encounters/fun-and-games-dialogue.json"; +import uncommonBreed from "./mystery-encounters/uncommon-breed-dialogue.json"; +import globalTradeSystem from "./mystery-encounters/global-trade-system-dialogue.json"; +import expertPokemonBreeder from "./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 deConfig = { ability, abilityTriggers, @@ -110,4 +152,40 @@ export const deConfig = { modifierSelectUiHandler, moveTriggers, 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/de/dialogue.json b/src/locales/de/dialogue.json index 8a3dbb8880e..493ccef2976 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,138 @@ "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..." + } + }, + "stat_trainer_buck": { + "encounter": { + "1": "...Ich sag dir jetzt mal was. Ich bin echt stark. Tue überrascht!", + "2": "Ich fühle, wie meine Pokémon in ihren Pokébällen zittern!" + }, + "victory": { + "1": "Hehehehe! So heiß bist du!", + "2": "Hehehehe! So heiß bist du!" + }, + "defeat": { + "1": "Whoa! Du scheinst ja wirklich erschöpft zu sein.", + "2": "Whoa! Du scheinst ja wirklich erschöpft zu sein." + } + }, + "stat_trainer_cheryl": { + "encounter": { + "1": "Meine Pokémon können es kaum erwarten, zu kämpfen.", + "2": "Ich sollte dich warnen, meine Pokémon können ziemlich wild sein." + }, + "victory": { + "1": "Ein gutes Verhältnis von Angriff und Verteidigung... Das ist nicht einfach.", + "2": "Ein gutes Verhältnis von Angriff und Verteidigung... Das ist nicht einfach." + }, + "defeat": { + "1": "Brauchen deine Pokémon Heilung?", + "2": "Brauchen deine Pokémon Heilung?" + } + }, + "stat_trainer_marley": { + "encounter": { + "1": "...OK. Ich werde mein Bestes geben.", + "2": "...OK. Ich werde nicht verlieren...!" + }, + "victory": { + "1": "... Awww.", + "2": "... Awww." + }, + "defeat": { + "1": "... Auf Wiedersehen.", + "2": "... Auf Wiedersehen." + } + }, + "stat_trainer_mira": { + "encounter": { + "1": "Du wirst von Mira schockiert sein!", + "2": "Mira wird dir zeigen, dass Mira sich nicht mehr verirrt!" + }, + "victory": { + "1": "Mira wundern, ob sie in diesem Land weit kommen kann.", + "2": "Mira wundern, ob sie in diesem Land weit kommen kann." + }, + "defeat": { + "1": "Mira wuss, dass sie gewinnen würde!", + "2": "Mira wuss, dass sie gewinnen würde!" + } + }, + "stat_trainer_riley": { + "encounter": { + "1": "Kämpfe sind unsere Art der Begrüßung.", + "2": "Wir setzen alles daran, deine Pokémon zu besiegen." + }, + "victory": { + "1": "Manchmal kämpfen wir, und manchmal schließen wir uns zusammen...\n$Es ist großartig, wie Trainer interagieren können.", + "2": "Manchmal kämpfen wir, und manchmal schließen wir uns zusammen...\n$Es ist großartig, wie Trainer interagieren können." + }, + "defeat": { + "1": "Du hast dich gut geschlagen. Bis zum nächsten Mal.", + "2": "Du hast dich gut geschlagen. Bis zum nächsten Mal." + } + }, + "winstrates_victor": { + "encounter": { + "1": "Das ist der Kampfgeist den ich sehen will! Ich mag dich!" + }, + "victory": { + "1": "Ahh! Du bist stärker als ich dachte!" + } + }, + "winstrates_victoria": { + "encounter": { + "1": "Mein Gott! Bist du nicht etwas jung?\n$Du musst ein ziemlich guter Trainer sein, um meinen Mann zu besiegen.\n$Jetzt bin ich wohl an der Reihe!" + }, + "victory": { + "1": "Waas? Wie stark bist du denn?" + } + }, + "winstrates_vivi": { + "encounter": { + "1": "Du bist stärker als Mama? Wow! Aber ich bin auch stark! Wirklich! Ehrlich!" + }, + "victory": { + "1": "Huh? Habe ich wirklich verloren?\nSchnief... Omaaa!" + } + }, + "winstrates_vicky": { + "encounter": { + "1": "Wie kannst du es wagen, meine kostbare Enkelin zum Weinen zu bringen!\n$Ich sehe, ich muss dir eine Lektion erteilen.\n$Mach dich bereit, eine Niederlage zu erleiden!" + }, + "victory": { + "1": "Wow! So stark!\nMeine Enkelin hat nicht gelogen." + } + }, + "winstrates_vito": { + "encounter": { + "1": "Ich habe zusammen mit meiner ganzen Familie trainiert, mit jedem von uns!\n$Ich verliere gegen niemanden!" + }, + "victory": { + "1": "Ich war besser als jeder in meiner Familie. Ich habe noch nie verloren..." + } + }, "brock": { "encounter": { "1": "Meine Expertise in Bezug auf Gesteins-Pokémon wird dich besiegen! Komm schon!", diff --git a/src/locales/de/egg.json b/src/locales/de/egg.json index dbece7e81f3..4097ac17578 100644 --- a/src/locales/de/egg.json +++ b/src/locales/de/egg.json @@ -11,6 +11,7 @@ "gachaTypeLegendary": "Erhöhte Chance auf legendäre Eier.", "gachaTypeMove": "Erhöhte Chance auf Eier mit seltenen Attacken.", "gachaTypeShiny": "Erhöhte Chance auf schillernde Eier.", + "eventType": "Geheimnisvolles Ereignis", "selectMachine": "Wähle eine Maschine.", "notEnoughVouchers": "Du hast nicht genug Ei-Gutscheine!", "tooManyEggs": "Du hast schon zu viele Eier!", @@ -23,4 +24,4 @@ "moveUPGacha": "Mehr\nEi-Attacken!", "shinyUPGacha": "Mehr\nSchillernde!", "legendaryUPGacha": "erscheint\nöfter!" -} \ No newline at end of file +} diff --git a/src/locales/de/modifier-select-ui-handler.json b/src/locales/de/modifier-select-ui-handler.json index 3de1222c3b4..fbc6820244a 100644 --- a/src/locales/de/modifier-select-ui-handler.json +++ b/src/locales/de/modifier-select-ui-handler.json @@ -8,5 +8,7 @@ "lockRaritiesDesc": "Setze die Seltenheit der Items fest. (Beeinflusst die Rollkosten).", "checkTeamDesc": "Überprüfe dein Team or nutze Formänderungsitems.", "rerollCost": "{{formattedMoney}}₽", - "itemCost": "{{formattedMoney}}₽" -} \ No newline at end of file + "itemCost": "{{formattedMoney}}₽", + "continueNextWaveButton": "Fortfahren", + "continueNextWaveDescription": "Zur nächsten Welle fortfahren." +} diff --git a/src/locales/de/modifier-type.json b/src/locales/de/modifier-type.json index 7c7972343d6..4f08727f9fd 100644 --- a/src/locales/de/modifier-type.json +++ b/src/locales/de/modifier-type.json @@ -68,6 +68,20 @@ "BaseStatBoosterModifierType": { "description": "Erhöht den {{stat}} Basiswert des Trägers um 10%. Das Stapellimit erhöht sich, je höher dein IS-Wert ist." }, + "PokemonBaseStatTotalModifierType": { + "name": "Pottrottsaft", + "description": "{{increaseDecrease}} alle Basiswerte des Trägers um {{statValue}}. Du wurdest von Pottrott {{blessCurse}}.", + "extra": { + "increase": "Erhöht", + "decrease": "Verringert", + "blessed": "gesegnet", + "cursed": "verflucht" + } + }, + "PokemonBaseStatFlatModifierType": { + "name": "Spezialität", + "description": "Erhöht den {{stats}}-Wert des Trägers um {{statValue}}. Nach einem komischen Traum gefunden." + }, "AllPokemonFullHpRestoreModifierType": { "description": "Stellt 100% der KP aller Pokémon her." }, @@ -401,7 +415,13 @@ "ENEMY_FUSED_CHANCE": { "name": "Fusionsmarke", "description": "Fügt eine 1%ige Chance hinzu, dass ein wildes Pokémon eine Fusion ist." - } + }, + + "MYSTERY_ENCOUNTER_SHUCKLE_JUICE": { "name": "Pottrottsaft" }, + "MYSTERY_ENCOUNTER_BLACK_SLUDGE": { "name": "Giftschleim", "description": "Der Geruch ist so stark, dass die Geschäfte ihre Items nur zu einem stark erhöhten Preis verkaufen." }, + "MYSTERY_ENCOUNTER_MACHO_BRACE": { "name": "Machoschiene", "description": "Das Besiegen eines Pokémon gewährt dem Besitzer einen Machoschiene-Stapel. Jeder Stapel steigert die Werte leicht, mit einem zusätzlichen Bonus bei maximalen Stapeln." }, + "MYSTERY_ENCOUNTER_OLD_GATEAU": { "name": "Spezialität", "description": "Erhöht den {{stats}}-Wert des Trägers um {{statValue}}." }, + "MYSTERY_ENCOUNTER_GOLDEN_BUG_NET": { "name": "Golden Bug Net", "description": "Erhöht die Chance, dass der Besitzer mehr Pokémon vom Typ Käfer findet. Hat ein seltsames Gewicht." } }, "SpeciesBoosterItem": { "LIGHT_BALL": { diff --git a/src/locales/de/move-trigger.json b/src/locales/de/move-trigger.json index 01b22429fb3..9b59c4b79ed 100644 --- a/src/locales/de/move-trigger.json +++ b/src/locales/de/move-trigger.json @@ -65,6 +65,7 @@ "suppressAbilities": "Die Fähigkeit von {{pokemonName}} wirkt nicht mehr!", "revivalBlessing": "{{pokemonName}} ist wieder fit und kampfbereit!", "swapArenaTags": "{{pokemonName}} hat die Effekte, die auf den beiden Seiten des Kampffeldes wirken, miteinander getauscht!", + "chillyReception": "{{pokemonName}} erzählt einen schlechten Witz, der nicht besonders gut ankommt...", "exposedMove": "{{pokemonName}} erkennt {{targetPokemonName}}!", "safeguard": "{{targetName}} wird durch Bodyguard geschützt!", "afterYou": "{{targetName}} lässt sich auf Galanterie ein!" 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/mystery-encounter-messages.json b/src/locales/de/mystery-encounter-messages.json new file mode 100644 index 00000000000..5c10b06a355 --- /dev/null +++ b/src/locales/de/mystery-encounter-messages.json @@ -0,0 +1,7 @@ +{ + "paid_money": "Du bezahlst {{amount, number}} ₽.", + "receive_money": "Du erhältst {{amount, number}} ₽!", + "affects_pokedex": "Beeinflusst Pokédex-Daten", + "cancel_option": "Zurück zur Auswahl der Begegnungsoptionen.", + "view_party_button": "Team überprüfen" +} diff --git a/src/locales/de/mystery-encounters/a-trainers-test-dialogue.json b/src/locales/de/mystery-encounters/a-trainers-test-dialogue.json new file mode 100644 index 00000000000..50413e24bc2 --- /dev/null +++ b/src/locales/de/mystery-encounters/a-trainers-test-dialogue.json @@ -0,0 +1,47 @@ +{ + "intro": "Ein sehr starker Trainer kommt auf dich zu...", + "buck": { + "intro_dialogue": "Yo, Trainer! Mein Name ist Avenaro.$Ich habe ein super Angebot für einen starken Trainer wie dich!$Ich trage zwei seltene Pokémon-Eier bei mir, aber ich möchte, dass sich jemand anderes um eines kümmert.$Wenn du mir beweisen kannst, dass du ein starker Trainer bist, werde ich dir das seltenere Ei geben!", + "accept": "Wohooo! Ich bin Feuer und Flamme!", + "decline": "Manno, es sieht so aus, als wäre dein Team nicht in Bestform.$Hier, lass mich dir helfen." + }, + "cheryl": { + "intro_dialogue": "Hallo mein Name ist Raissa, ich habe eine besondere Bitte an dich, einen starken Trainer.$Ich trage zwei seltene Pokémon-Eier bei mir, aber ich möchte, dass sich jemand anderes um eines kümmert.$Wenn du mir beweisen kannst, dass du ein starker Trainer bist, werde ich dir das seltenere Ei geben!", + "accept": "Ich hoffe, du bist bereit!", + "decline": "Ich verstehe, es sieht so aus, als wäre dein Team nicht in der besten Verfassung.$Hier, lass mich dir helfen." + }, + "marley": { + "intro_dialogue": "...@d{64} Ich bin Charlie.$Ich habe ein Angebot für dich...$Ich trage zwei Pokémon-Eier bei mir, aber ich möchte, dass sich jemand anderes um eines kümmert.$Wenn du stärker bist als ich, werde ich dir das seltenere Ei geben.", + "accept": "...So ist das also.", + "decline": "...Deine Pokémon sehen verletzt aus...Lass mich helfen." + }, + "mira": { + "intro_dialogue": "Hi, ich bin Orisa!$Ich habe eine Bitte an dich, einen starken Trainer.$Ich trage zwei seltene Pokémon-Eier bei mir, aber ich möchte, dass sich jemand anderes um eines kümmert.$Wenn du mir beweisen kannst, dass du ein starker Trainer bist, werde ich dir das seltenere Ei geben!", + "accept": "Du wirst Orisa herausfordern? Juhu!", + "decline": "Aww, kein Kampf? Das ist okay!$Hier, Orisa wird dein Team heilen!" + }, + "riley": { + "intro_dialogue": "Ich Urs, ich habe eine Bitte an dich, einen starken Trainer.$Ich trage zwei seltene Pokémon-Eier bei mir, aber ich möchte, dass sich jemand anderes um eines kümmert.$Wenn du mir beweisen kannst, dass du ein starker Trainer bist, werde ich dir das seltenere Ei geben!", + "accept": "Dieser Blick...Lass uns das machen.", + "decline": "Ich verstehe, dein Team sieht geschlagen aus.$Hier, lass mich dir helfen." + }, + "title": "Ein Trainer-Test", + "description": "Es scheint als würde dieser Trainer dir ein Ei geben, egal wie du dich entscheidest. Wenn du es jedoch schaffst, diesen starken Trainer zu besiegen, wirst du ein viel selteneres Ei erhalten.", + "query": "Was wirst du tun?", + "option": { + "1": { + "label": "Die Herausforderung annehmen", + "tooltip": "(-) Schwerer Kampf\n(+) Erhalte ein @[TOOLTIP_TITLE]{Sehr seltenes Ei}" + }, + "2": { + "label": "Die Herausforderung ablehnen", + "tooltip": "(+) Team wird geheilt\n(+) Erhalte ein @[TOOLTIP_TITLE]{Ei}" + } + }, + "eggTypes": { + "rare": "seltenes Ei", + "epic": "episches Ei", + "legendary": "legendäres Ei" + }, + "outro": "{{statTrainerName}} gibt dir ein {{eggType}}!" +} diff --git a/src/locales/de/mystery-encounters/absolute-avarice-dialogue.json b/src/locales/de/mystery-encounters/absolute-avarice-dialogue.json new file mode 100644 index 00000000000..eb4d06fc527 --- /dev/null +++ b/src/locales/de/mystery-encounters/absolute-avarice-dialogue.json @@ -0,0 +1,25 @@ +{ + "intro": "Ein {{greedentName}} überfällt dich und stiehlt die Beeren deines Teams!", + "title": "Absoluter Geiz", + "description": "Der {{greedentName}} hat dich total überrascht und all deine Beeren gestohlen!\nEs sieht so aus, als ob das {{greedentName}} sie gleich essen würde, aber dann hält es inne und sieht dich interessiert an.", + "query": "Was wirst du tun?", + "option": { + "1": { + "label": "Kampf beginnen", + "tooltip": "(-) Schwerer Kampf\n(+) Belohnungen aus seinem Beerenversteck", + "selected": "Der {{greedentName}} füllt seine Backen und bereitet sich auf den Kampf vor!", + "boss_enraged": "{{greedentName}} Liebe für Essen hat es aufgebracht!", + "food_stash": "Es scheint, als ob das {{greedentName}} ein riesiges Nahrungslager bewacht hat!$Jedes Pokémon in deinem Team erhält {{foodReward}}!" + }, + "2": { + "label": "Verhandeln", + "tooltip": "(+) Einige Beeren zurückbekommen", + "selected": "Deine Bitte berührt das {{greedentName}}.$Es gibt dir nicht alle Beeren zurück, aber wirft dir trotzdem ein paar zu." + }, + "3": { + "label": "Beeren überlassen", + "tooltip": "(-) Alle Beeren verlieren\n(?) Das {{greedentName}} wird dich mögen", + "selected": "Das {{greedentName}} verschlingt den gesamten Beerenversteck in einem Blitz!$Es klopft sich auf den Bauch und sieht dich dankbar an.$Vielleicht könntest du ihm auf deinem Abenteuer mehr Beeren geben...$@s{level_up_fanfare}Das {{greedentName}} möchte sich deiner Gruppe anschließen!" + } + } +} \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/an-offer-you-cant-refuse-dialogue.json b/src/locales/de/mystery-encounters/an-offer-you-cant-refuse-dialogue.json new file mode 100644 index 00000000000..67ac2b1e7f3 --- /dev/null +++ b/src/locales/de/mystery-encounters/an-offer-you-cant-refuse-dialogue.json @@ -0,0 +1,26 @@ +{ + "intro": "Du wirst von einem reich aussehenden Jungen aufgehalten.", + "speaker": "Reicher Junge", + "intro_dialogue": "Guten Tag!$Ich kann nicht anders, als zu bemerken, dass dein\n{{strongestPokemon}} einfach göttlich aussieht!$Ich habe schon immer ein Pokémon wie dieses haben wollen!$Ich würde es dir großzügig bezahlen, und dir auch diesen alten Kram geben!", + "title": "Ein Angebot das du nicht ablehnen kannst", + "description": "Dir wird ein @[TOOLTIP_TITLE]{Schillerpin} und {{price, money}} für dein {{strongestPokemon}} angeboten!\nEs ist ein extrem gutes Angebot, aber kannst du es wirklich ertragen, dich von einem so starken Teammitglied zu trennen?", + "query": "Was wirst du tun?", + "option": { + "1": { + "label": "Den Deal annehmen", + "tooltip": "(-) Verliere {{strongestPokemon}}\n(+) Erhalte einen @[TOOLTIP_TITLE]{Schillerpin}\n(+) Erhalte {{price, money}}", + "selected": "Wunderbar!@d{32} Komm mit, {{strongestPokemon}}!$Es ist Zeit, dich allen im Yachtclub zu zeigen!$Die werden so neidisch sein!" + }, + "2": { + "label": "Das Kind erpressen", + "tooltip": "(+) {{option2PrimaryName}} setzt {{moveOrAbility}} ein\n(+) Erhalte {{price, money}}", + "tooltip_disabled": "Dein Pokémon muss bestimmte Attacken oder Fähigkeiten haben, um diese Option zu wählen", + "selected": "Mein Gott, wir werden ausgeraubt, {{liepardName}}!$Du wirst von meinen Anwälten hören!" + }, + "3": { + "label": "Weggehen", + "tooltip": "(-) Keine Belohnung", + "selected": "Was ein beschissener Tag...$Ach, was solls. Lass uns zurück zum Yachtclub gehen, {{liepardName}}." + } + } +} \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/berries-abound-dialogue.json b/src/locales/de/mystery-encounters/berries-abound-dialogue.json new file mode 100644 index 00000000000..e7ff57a32ef --- /dev/null +++ b/src/locales/de/mystery-encounters/berries-abound-dialogue.json @@ -0,0 +1,26 @@ +{ + "intro": "Da ist ein riesiger Beerenstrauch in der Nähe dieses Pokémons!", + "title": "Überall Beeren", + "description": "Es scheint, als ob ein starkes Pokémon einen Beerenstrauch bewacht. Ein Kampf wäre der direkte Weg, aber es sieht stark aus. Vielleicht könnte ein schnelles Pokémon ein paar Beeren schnappen, ohne erwischt zu werden?", + "query": "Was wirst du tun?", + "berries": "Berren!", + "option": { + "1": { + "label": "Kampf beginnen", + "tooltip": "(-) Schwerer Kampf\n(+) Beeren erhalten", + "selected": "Du trittst dem Pokémon ohne Furcht entgegen." + }, + "2": { + "label": "Zum Strauch rennen", + "tooltip": "(-) {{fastestPokemon}} nutzt seine Geschwindigkeit\n(+) Beeren erhalten", + "selected": "Dein {{fastestPokemon}} rennt zum Strauch!$Es schafft es, {{numBerries}} zu schnappen, bevor das {{enemyPokemon}} reagieren kann!$Du ziehst dich schnell mit deiner neuen Beute zurück.", + "selected_bad": "Dein {{fastestPokemon}} rennt zum Strauch!$Oh nein! Das {{enemyPokemon}} war schneller und hat den Weg blockiert!", + "boss_enraged": "Das gegnerische {{enemyPokemon}} ist wütend geworden!" + }, + "3": { + "label": "Verlassen", + "tooltip": "(-) Keine Belohnung", + "selected": "Du lässt das starke Pokémon mit seinem Item zurück und gehst weiter." + } + } +} \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/bug-type-superfan-dialogue.json b/src/locales/de/mystery-encounters/bug-type-superfan-dialogue.json new file mode 100644 index 00000000000..b298cff3dd7 --- /dev/null +++ b/src/locales/de/mystery-encounters/bug-type-superfan-dialogue.json @@ -0,0 +1,40 @@ +{ + "intro": "Ein ungewöhnlicher Trainer mit allerlei Käfer-Schnickschnack versperrt dir den Weg!", + "intro_dialogue": "Hey, Trainer! Ich bin auf einer Mission, um die seltensten Käfer-Pokémon zu finden!$Du musst Käfer-Pokémon auch lieben, oder? Jeder liebt Käfer-Pokémon!", + "title": "Der Käfersammler-Superfan", + "speaker": "Käfersammler-Superfan", + "description": "Der Trainer plappert drauf los, ohne auf eine Antwort zu warten...\nEs scheint, als gäbe es nur einen Weg, um aus dieser Situation herauszukommen... Die Aufmerksamkeit des Trainers zu erregen!", + "query": "Was wirst du tun?", + "option": { + "1": { + "label": "Pokémon-Kampf", + "tooltip": "(-) Herausfordernder Kampf\n(+) Einem Pokémon eine Käfer-Attacke beibringen", + "selected": "Ein Pokémon-Kampf? Meine Käfer-Pokémon sind mehr als bereit für dich!" + }, + "2": { + "label": "Käfer-Pokémon zeigen", + "tooltip": "(+) Erhalte ein Geschenk", + "disabled_tooltip": "Du brauchst mindestens 1 Käfer-Pokémon in deinem Team, um das auszuwählen.", + "selected": "Du zeigst dem Trainer all deine Käfer-Pokémon...", + "selected_0_to_1": "Huh? Du hast nur {{numBugTypes}} Käfer-Pokémon...$Ich verschwende hier meine Zeit...", + "selected_2_to_3": "Hey, du hast {{numBugTypes}}! Nicht schlecht.$Hier, das könnte dir auf deiner Reise helfen, mehr zu fangen!", + "selected_4_to_5": "Was? Du hast {{numBugTypes}}? Nicht schlecht!$Du bist noch nicht ganz auf meinem Level, aber ich kann mich in dir erkennen! $Nimm das, mein junger Padawan!", + "selected_6": "Wow! {{numBugTypes}}!$Du musst Käfer-Pokémon fast so sehr lieben wie ich!$Hier, nimm das als Zeichen unserer Kameradschaft!" + }, + "3": { + "label": "Verschenke ein Käfer-Item", + "tooltip": "(-) Du gibst dem Trainer ein {{requiredBugItems}}\n(+) Erhalte ein Geschenk", + "disabled_tooltip": "Du brauchst ein {{requiredBugItems}}, um das auszuwählen.", + "select_prompt": "Wählen Sie ein Item aus, um es zu verschenken.", + "invalid_selection": "Das Pokémon hat kein solches Item.", + "selected": "Du gibst {{selectedItem}} an dem Trainer .", + "selected_dialogue": "Wow! {{selectedItem}}, für mich? Du bist nicht so schlecht, Junge!$Als Zeichen meiner Anerkennung möchte ich, dass du dieses besondere Geschenk bekommst!$Es wurde in meiner Familie weitergegeben, und jetzt möchte ich, dass du es hast!" + } + }, + "battle_won": "Dein Wissen und Können waren perfekt, um unsere Schwächen auszunutzen!$Als Gegenleistung für die wertvolle Lektion, erlaube mir, einem deiner Pokémon eine Käfer-Attacke beizubringen!", + "teach_move_prompt": "Wähle eine Attacke aus die du deinem Pokémon beibringen möchtest.", + "confirm_no_teach": "Bist du sicher, dass du keine dieser großartigen Attacken lernen möchtest?", + "outro": "Ich sehe großartige Käfer-Pokémon in deiner Zukunft! Mögen sich unsere Wege wieder kreuzen!$Mach's gut!", + "numBugTypes_one": "{{count}} Käfer-Pokémon", + "numBugTypes_other": "{{count}} Käfer-Pokémon" +} diff --git a/src/locales/de/mystery-encounters/clowning-around-dialogue.json b/src/locales/de/mystery-encounters/clowning-around-dialogue.json new file mode 100644 index 00000000000..5dce7b515a9 --- /dev/null +++ b/src/locales/de/mystery-encounters/clowning-around-dialogue.json @@ -0,0 +1,35 @@ +{ + "intro": "Es ist...@d{64} ein Clown?", + "speaker": "Clown", + "intro_dialogue": "Du tollpatschiger Trottel, bereite dich auf einen brillanten Kampf vor!\nDu wirst von diesem prügelnden Straßenmusikanten besiegt!", + "title": "Rumgeblödel", + "description": "Irgendwas stimmt nicht mit dieser Begegnung. Der Clown scheint darauf aus zu sein, dich zu einem Kampf zu provozieren, aber zu welchem Zweck?\n\nDas {{blacephalonName}} ist besonders seltsam, als hätte es @[TOOLTIP_TITLE]{seltsame Typen} und eine @[TOOLTIP_TITLE]{Fähigkeit.}", + "query": "Was wirst du tun?", + "option": { + "1": { + "label": "Kampf beginnen", + "tooltip": "(-) Komischer Kampf\n(?) Beeinflusst Pokémon-Fähigkeiten", + "selected": "Deine erbärmlichen Pokémon sind bereit für eine erbärmliche Vorstellung!", + "apply_ability_dialogue": "Eine sensationelle Vorstellung! Dein Können passt zu einer sensationellen Fähigkeit als Beute!", + "apply_ability_message": "Der Clown bietet an, die Fähigkeit eines deiner Pokémon dauerhaft auf {{ability}} zu wechseln!", + "ability_prompt": "Soll eines deiner Pokémon die Fähigkeit {{ability}} dauerhaft erlangen?", + "ability_gained": "@s{level_up_fanfare}{{chosenPokemon}} hat die Fähigkeit {{ability}} erhalten!" + }, + "2": { + "label": "Nicht provozieren lassen", + "tooltip": "(-) Der Clown ist beleidigt\n(?) Beeinflusst Pokémon-Items", + "selected": "Du erbärmlicher Feigling, du verweigerst einen wunderbaren Kampf? Fühle meinen Zorn!", + "selected_2": "Das {{blacephalonName}} des Clowns verwendet Trickbetrug! Alle Items deines {{switchPokemon}} wurden zufällig vertauscht!", + "selected_3": "Meine perfekte List hat dich in die Irre geführt!" + }, + "3": { + "label": "Die Beleidigungen erwidern", + "tooltip": "(-) Den Clown verärgern\n(?) Beeinflusst Pokémon-Typen", + "selected": "Du erbärmlicher Feigling verweigerst einen wunderbaren Kampf? Fühle meinen Zorn!", + "selected_2": "Das {{blacephalonName}} des Clowns verwendet eine seltsame Attacke! Alle Typen deines Teams wurden zufällig vertauscht!", + "selected_3": "Meine perfekte List hat dich in die Irre geführt!" + } + + }, + "outro": "Der Clown und seine Kumpanen verschwinden in einer Rauchwolke." +} \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/dancing-lessons-dialogue.json b/src/locales/de/mystery-encounters/dancing-lessons-dialogue.json new file mode 100644 index 00000000000..3ea02955309 --- /dev/null +++ b/src/locales/de/mystery-encounters/dancing-lessons-dialogue.json @@ -0,0 +1,27 @@ +{ + "intro": "Ein {{oricorioName}} tanzt traurig allein, ohne einen Partner.", + "title": "Tanzstunden", + "description": "Das {{oricorioName}} scheint nicht aggressiv zu sein, im Gegenteil, es scheint traurig zu sein.\nVielleicht möchte es einfach nur mit jemandem tanzen...", + "query": "Was wirst du tun?", + "option": { + "1": { + "label": "Kampf beginnen", + "tooltip": "(-) Schwerer Kampf\n(+) Erhalte ein Stab", + "selected": "Das {{oricorioName}} ist verstört und verteidigt sich!", + "boss_enraged": "Das {{oricorioName}} ist wütend und steigert seine Werte!" + }, + "2": { + "label": "Lerne den Tanz", + "tooltip": "(+) Bringe einem Pokémon Wecktanz bei", + "selected": "Du schaust dem {{oricorioName}} genau zu, wie es seinen Tanz aufführt...$@s{level_up_fanfare}Dein {{selectedPokemon}} hat von {{oricorioName}} gelernt!" + }, + "3": { + "label": "Zeig einen Tanz", + "tooltip": "(-) Bringe dem {{oricorioName}} einen Tanz bei\n(+) Das {{oricorioName}} wird dich mögen", + "disabled_tooltip": "Dein Pokémon muss einen Tanz beherrschen, um diese Option zu wählen.", + "select_prompt": "Wählen Sie eine Tanzattacke aus, die verwendet werden soll.", + "selected": "Das {{oricorioName}} schaut fasziniert zu, wie {{selectedPokemon}} {{selectedMove}} vorführt!$Es liebt die Vorführung!$@s{level_up_fanfare}Das {{oricorioName}} möchte sich dir anschließen!" + } + }, + "invalid_selection": "Das Pokémon kennt keine Tanzattacke" +} \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/dark-deal-dialogue.json b/src/locales/de/mystery-encounters/dark-deal-dialogue.json new file mode 100644 index 00000000000..85c8d4565bb --- /dev/null +++ b/src/locales/de/mystery-encounters/dark-deal-dialogue.json @@ -0,0 +1,24 @@ + + +{ + "intro": "Ein seltsamer Mann in einem zerrissenen Mantel steht dir im Weg...", + "speaker": "Seltsamer Mann", + "intro_dialogue": "Hey, du!$Ich habe an einem neuen Gerät gearbeitet, um die verborgene Kraft eines Pokémon zum Vorschein zu bringen!$Es bindet die Atome des Pokémon auf molekularer Ebene vollständig neu und bringt sie in eine$weitaus mächtigere Form.$Hehe...@d{64} Ich brauche nur ein paar Opf-@d{32} Ähm, Testpersonen, um zu beweisen, dass es funktioniert.", + "title": "Dunkler Handel", + "description": "Der verstörende Typ hält einige Pokébälle hoch.\n\"Es wird such für dich lohnen! Du kannst diese tollen Pokébälle als Bezahlung haben, alles was ich brauche ist ein Pokémon aus deinem Team! Hehe...\"", + "query": "Was wirst du tun?", + "option": { + "1": { + "label": "Aktzeptieren", + "tooltip": "(+) 5 Roguebälle\n(?) Ein zufälliges Pokémon wird verbessert", + "selected_dialogue": "Lass mich mal sehen...${{pokeName}} ist eine gute Wahl!$Denk dran, ich bin nicht verantwortlich, wenn etwas schief geht!@d{32} Hehe...", + "selected_message": "Der Mann übergibt dir 5 Roguebälle.${{pokeName}} springt in die seltsame Maschine...$Blinkende Lichter und seltsame Geräusche kommen aus der Maschine!$...@d{96} Etwas kommt aus der Maschine,\nwütend und wild!" + }, + "2": { + "label": "Ablehnen", + "tooltip": "(-) Keine Belohnung", + "selected": "Du willst einem armen Kerl nicht helfen? Pah!" + } + }, + "outro": "Nach der schrecklichen Begegnung, sammelst du dich und gehst weiter." +} \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/delibirdy-dialogue.json b/src/locales/de/mystery-encounters/delibirdy-dialogue.json new file mode 100644 index 00000000000..34ca9666d09 --- /dev/null +++ b/src/locales/de/mystery-encounters/delibirdy-dialogue.json @@ -0,0 +1,29 @@ + + +{ + "intro": "Ein Schwarm {{delibirdName}} ist aufgetaucht!", + "title": "Botogel-Bande", + "description": "Die {{delibirdName}} schauen dich erwartungsvoll an, als ob sie etwas wollen. Vielleicht würde es sie zufriedenstellen, wenn du ihnen ein Item oder etwas Geld gibst?", + "query": "Was möchtest du ihnen geben?", + "invalid_selection": "Das Pokémon hat kein solches Item.", + "option": { + "1": { + "label": "Geld geben", + "tooltip": "(-) Den {{delibirdName}} {{money, money}} geben\n(+) Erhalte ein Geschenk", + "selected": "Du wirfst das Geld zu den {{delibirdName}}, die aufgeregt miteinander schnattern.$Sie drehen sich zu dir um und geben dir glücklich ein Geschenk!" + }, + "2": { + "label": "Futter geben", + "tooltip": "(-) Gib den {{delibirdName}} eine Beere oder einen Belebersamen\n(+) Erhalte ein Geschenk", + "select_prompt": "Wähle ein Item aus.", + "selected": "Du wirfst {{chosenItem}} zu den {{delibirdName}}, die aufgeregt miteinander schnattern.$Sie drehen sich zu dir um und geben dir glücklich ein Geschenk!" + }, + "3": { + "label": "Ein Item geben", + "tooltip": "(-) Gebe den {{delibirdName}} ein Item\n(+) Erhalte ein Geschenk", + "select_prompt": "Wähle ein Item aus.", + "selected": "Du wirfst {{chosenItem}} zu den {{delibirdName}}, die aufgeregt miteinander schnattern.$Sie drehen sich zu dir um und geben dir glücklich ein Geschenk!" + } + }, + "outro": "Die {{delibirdName}} watscheln glücklich davon.$Was für ein seltsamer kleiner Austausch!" +} \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/department-store-sale-dialogue.json b/src/locales/de/mystery-encounters/department-store-sale-dialogue.json new file mode 100644 index 00000000000..66e41975a31 --- /dev/null +++ b/src/locales/de/mystery-encounters/department-store-sale-dialogue.json @@ -0,0 +1,27 @@ +{ + "intro": "Es ist eine Dame mit vielen Einkaufstüten.", + "speaker": "Einkäuferin", + "intro_dialogue": "Hallo! Bist du auch wegen der tollen Angebote hier?$Es gibt einen speziellen Gutschein, den du während des Verkaufs einlösen kannst!$Ich habe einen zusätzlichen. Hier, bitte!", + "title": "Einkaufszentrum-Verkauf", + "description": "Es gibt Angebote in jede Richtung! Es sieht so aus, als ob es 4 Kassen gibt, an denen du den Gutschein gegen verschiedene Artikel eintauschen kannst. Die Möglichkeiten sind endlos!", + "query": "Welche Kasse wählst du?", + "option": { + "1": { + "label": "TM-Kasse", + "tooltip": "(+) TM Shop" + }, + "2": { + "label": "Nährstoff-Kasse", + "tooltip": "(+) Nährstoff Shop" + }, + "3": { + "label": "Kampf-Item-Kasse", + "tooltip": "(+) X-Item Shop" + }, + "4": { + "label": "Pokéball-Kasse", + "tooltip": "(+) Pokéball Shop" + } + }, + "outro": "Was für ein Schnäppchen! Du solltest öfter hier einkaufen." +} \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/field-trip-dialogue.json b/src/locales/de/mystery-encounters/field-trip-dialogue.json new file mode 100644 index 00000000000..61e6d4d9367 --- /dev/null +++ b/src/locales/de/mystery-encounters/field-trip-dialogue.json @@ -0,0 +1,31 @@ +{ + "intro": "Eine Lehrerin und ein paar Schulkinder stehen auf einmal vor dir!", + "speaker": "Lehrerin", + "intro_dialogue": "Hallo! Könntest du eine Minute für meine Schüler erübrigen?$Ich bringe ihnen gerade bei, wie Pokémon-Attacken funktionieren und würde ihnen gerne$eine Demonstration zeigen.$Würdest du uns eine Attacke deines Pokémon vorführen?", + "title": "Exkursion", + "description": "Eine Lehrerin fragt nach einer Attackenvorführung eines Pokémon. Je nachdem, welche Attacke du wählst, hat sie vielleicht etwas Nützliches für dich als Belohnung.", + "query": "Welchen Attacken-Typ wählst du?", + "option": { + "1": { + "label": "Physische Attacke", + "tooltip": "(+) Physische Item-Belohnungen" + }, + "2": { + "label": "Spezielle Attacke", + "tooltip": "(+) Spezielle Item-Belohnungen" + }, + "3": { + "label": "Status-Attacke", + "tooltip": "(+) Status Item-Belohnungen" + }, + "selected": "{{pokeName}} zeigt eine beeindruckende Vorführung von {{move}}!" + }, + "second_option_prompt": "Wähle eine Attacke die dein Pokémon einsetzen soll.", + "incorrect": "...$Das ist keine {{moveCategory}}Attacke!\nEs tut mir leid, aber ich kann dir nichts geben.$Kommt Kinder, wir suchen uns woanders einen besseren Trainer.", + "incorrect_exp": "Es scheint, als hättest du eine wertvolle Lektion gelernt?$Dein Pokémon hat auch etwas Erfahrung gesammelt.", + "correct": "Ich dank dir vielmals für deine Freundlichkeit!$Ich hoffe, diese Items sind nützlich für dich.", + "correct_exp": "{{pokeName}} hat auch etwas wertvolle Erfahrung gesammelt!", + "status": "Status-", + "physical": "physische ", + "special": "spezielle " +} \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/fiery-fallout-dialogue.json b/src/locales/de/mystery-encounters/fiery-fallout-dialogue.json new file mode 100644 index 00000000000..167efd48b53 --- /dev/null +++ b/src/locales/de/mystery-encounters/fiery-fallout-dialogue.json @@ -0,0 +1,26 @@ +{ + "intro": "Du hast einen Sturm aus Rauch und Asche entdeckt!", + "title": "Feurige Folgen", + "description": "Die umherwirbelnde Asche und Glut haben die Sicht auf fast Null reduziert. Es scheint, als könnte es eine... Quelle geben, die diese Bedingungen verursacht. Aber was könnte hinter einem Phänomen dieser Größe stecken?", + "query": "Was wirst du tun?", + "option": { + "1": { + "label": "Finde die Quelle", + "tooltip": "(?) Entdecke die Quelle\n(-) Schwieriger Kampf", + "selected": "Du hast die Quelle des Sturms gefunden!$Es sind zwei {{volcaronaName}}, die in der Mitte eines Paarungstanzes sind!$Sie nehmen die Unterbrechung nicht gut auf und greifen an!" + }, + "2": { + "label": "Sich einigeln", + "tooltip": "(-) Die Folgen des Wetters erleiden", + "selected": "Die Folgen des Wetters sind verheerend!$Deine Pokémon nehmen 20% ihrer maximalen KP als Schaden!", + "target_burned": "Dein {{burnedPokemon}} wurde auch verbrannt!" + }, + "3": { + "label": "Dein Feuer-Pokémon hilft", + "tooltip": "(+) Das Wetter klärt auf\n(+) Erhalte ein Holzkohle", + "disabled_tooltip": "Du benötigst mindestens 2 Feuer-Pokémon, um diese Option auszuwählen", + "selected": "Dein {{option3PrimaryName}} und {{option3SecondaryName}} führen dich zu zwei {{volcaronaName}}, die in der Mitte eines Paarungstanzes sind!$Zum Glück können deine Pokémon sie beruhigen,und sie ziehen ohne Probleme ab." + } + }, + "found_charcoal": "Nachdem das Wetter aufklart, entdeckt dein {{leadPokemon}} etwas auf dem Boden.$@s{item_fanfare}{{leadPokemon}} erhält eine Holzkohle!" +} \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/fight-or-flight-dialogue.json b/src/locales/de/mystery-encounters/fight-or-flight-dialogue.json new file mode 100644 index 00000000000..33ed0d3f95a --- /dev/null +++ b/src/locales/de/mystery-encounters/fight-or-flight-dialogue.json @@ -0,0 +1,25 @@ +{ + "intro": "Etwas Glänzendes liegt auf dem Boden in der Nähe dieses Pokémons!", + "title": "Kampf oder Flucht", + "description": "Es scheint, als würde ein starkes Pokémon ein Item bewachen. Ein Kampf wäre der direkte Weg, aber es sieht stark aus. Vielleicht könntest du das Item stehlen, wenn du das richtige Pokémon für den Job hast.", + "query": "Was wirst du tun?", + "option": { + "1": { + "label": "Kampf beginnen", + "tooltip": "(-) Schwerer Kampf\n(+) Neues Item", + "selected": "Du trittst dem Pokémon ohne Furcht entgegen.", + "stat_boost": "Die Stärke von {{enemyPokemon}} erhöht einen seiner Werte!" + }, + "2": { + "label": "Das Item stehlen", + "disabled_tooltip": "Dein Pokémon muss eine bestimmte Attacken beherrschen, um diese Option zu wählen.", + "tooltip": "(+) {{option2PrimaryName}} setzt {{option2PrimaryMove}} ein", + "selected": ".@d{32}.@d{32}.@d{32}$Dein {{option2PrimaryName}} hilft dir und setzt {{option2PrimaryMove}} ein!$Du hast das Item gestohlen!" + }, + "3": { + "label": "Verlassen", + "tooltip": "(-) Keine Belohnung", + "selected": "Du lässt das starke Pokémon mit seinem Item zurück und gehst weiter." + } + } +} \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/fun-and-games-dialogue.json b/src/locales/de/mystery-encounters/fun-and-games-dialogue.json new file mode 100644 index 00000000000..9e38d0a4599 --- /dev/null +++ b/src/locales/de/mystery-encounters/fun-and-games-dialogue.json @@ -0,0 +1,30 @@ +{ + "intro_dialogue": "Kommen Sie näher, meine Damen und Herren!$Versuchen Sie Ihr Glück mit dem brandneuen {{wobbuffetName}}-Hau-den-Lukas!", + "speaker": "Animateur", + "title": "Spaß und Spiele!", + "description": "Du hast ein {{wobbuffetName}} gefunden, das ein Spiel spielt! Du hast @[TOOLTIP_TITLE]{3 Züge}, um das {{wobbuffetName}} so nah wie möglich an @[TOOLTIP_TITLE]{1 KP} heranzubringen, @[TOOLTIP_TITLE]{ohne es zu besiegen}, damit es eine riesige Gegenattacke auf der Glockenmaschine ausführen kann.\nAber sei vorsichtig! Wenn du das {{wobbuffetName}} besiegst, musst du die Kosten für die Wiederbelebung bezahlen!", + "query": "Möchtest du spielen?", + "option": { + "1": { + "label": "Das Spiel spielen", + "tooltip": "(-) Zahle {{option1Money, money}}\n(+) Spiele {{wobbuffetName}} Hau-den-Lukas", + "selected": "Zeit dein Glück herauszufordern!" + }, + "2": { + "label": "Weggehen", + "tooltip": "(-) Keine Belohnung", + "selected": "Du beeilst dich auf deinem Weg, mit einem leichten Gefühl der Reue." + } + }, + "ko": "Oh nein! Das {{wobbuffetName}} ist ohnmächtig geworden!$Du verlierst das Spiel und musst die Kosten für die Wiederbelebung bezahlen...", + "charging_continue": "Das {{wobbuffetName}} lädt seine Gegenattacke auf!", + "turn_remaining_3": "Drei Runden verbleiben!", + "turn_remaining_2": "Zwei Runden verbleiben!", + "turn_remaining_1": "Nur noch eine Runde!", + "end_game": "Die Zeit ist um!$Das {{wobbuffetName}} holt zum Gegenangriff aus und@d{16}.@d{16}.@d{16}.", + "best_result": "Das {{wobbuffetName}} schlägt so hart auf den Knopf, dass die Glocke vom oberen Teil abbricht!$Du gewinnst den Hauptpreis!", + "great_result": "Das {{wobbuffetName}} schlägt den Knopf so hart, dass die Glocke fast getroffen wird!$So nah! Du gewinnst den zweiten Preis!", + "good_result": "Das {{wobbuffetName}} trifft den Knopf stark genug, um die Hälfte der Skala zu erreichen!$Du verdienst den dritten Preis!", + "bad_result": "Das {{wobbuffetName}} trifft den Knopf kaum und nichts passiert...$Oh nein! Du gewinnst nichts!", + "outro": "Das war ein lustiges kleines Spiel!" +} \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/global-trade-system-dialogue.json b/src/locales/de/mystery-encounters/global-trade-system-dialogue.json new file mode 100644 index 00000000000..f9b2ac605bf --- /dev/null +++ b/src/locales/de/mystery-encounters/global-trade-system-dialogue.json @@ -0,0 +1,32 @@ +{ + "intro": "Es ist eine Schnittstelle für die Globale Tauschstation, das GTS.", + "title": "Das GTS", + "description": "Ah, das GTS! Ein technologisches Wunder, mit dem du dich mit jedem auf der Welt verbinden kannst, um Pokémon mit ihnen zu tauschen! Wird das Glück dir heute hold sein?", + "query": "Was wirst du tun?", + "option": { + "1": { + "label": "Tauschangebote prüfen", + "tooltip": "(+) Wähle ein Tauschangebot für eines deiner Pokémon aus", + "trade_options_prompt": "Wähle ein Pokémon aus, das du erhalten möchtest." + }, + "2": { + "label": "Zaubertausch", + "tooltip": "(+) Seine eine deiner Pokémon an die GTS und erhalte ein zufälliges Pokémon im Austausch" + }, + "3": { + "label": "Tausche ein Item", + "trade_options_prompt": "Wähle ein Item aus, das du senden möchtest.", + "invalid_selection": "Dieses Pokémon hat keine Items die getauscht werden können.", + "tooltip": "(+) Sende eines deiner Items an die GTS und erhalte ein zufälliges Item im Austausch" + }, + "4": { + "label": "Weggehen", + "tooltip": "(-) Keine Belohnung", + "selected": "Heute ist keine Zeit zum Tauschen! Du gehst weiter." + } + }, + "pokemon_trade_selected": "{{tradedPokemon}} wird an {{tradeTrainerName}} gesendet.", + "pokemon_trade_goodbye": "Machs gut, {{tradedPokemon}}!", + "item_trade_selected": "{{chosenItem}} wird an {{tradeTrainerName}} gesendet.$.@d{64}.@d{64}.@d{64}\n@s{level_up_fanfare}Tausch abgeschlossen!$Du hast {{itemName}} von {{tradeTrainerName}} erhalten!", + "trade_received": "@s{evolution_fanfare}{{tradeTrainerName}} hat dir {{received}} geschickt!" +} \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/lost-at-sea-dialogue.json b/src/locales/de/mystery-encounters/lost-at-sea-dialogue.json new file mode 100644 index 00000000000..3ce269fbee8 --- /dev/null +++ b/src/locales/de/mystery-encounters/lost-at-sea-dialogue.json @@ -0,0 +1,28 @@ +{ + "intro": "Du warst auf dem Meer umhergeirrt und effektiv nirgendwohin gekommen.", + "title": "Verloren auf See", + "description": "Die See ist in diesem Gebiet stürmisch und du hast kaum noch Energie. Das ist schlecht. Gibt es einen Ausweg aus der Situation?", + "query": "Was wirst du tun?", + "option": { + "1": { + "label": "{{option1PrimaryName}} kann helfen", + "label_disabled": "Kein {{option1RequiredMove}}", + "tooltip": "(+) {{option1PrimaryName}} rettet dich\n(+) {{option1PrimaryName}} erhält etwas EP", + "tooltip_disabled": "Du hast kein Pokémon, das {{option1RequiredMove}} erlernen kann", + "selected": "{{option1PrimaryName}} schwimmt voraus und führt dich zurück auf den richtigen Weg.${{option1PrimaryName}} scheint auch stärker geworden zu sein in dieser Zeit der Not!" + }, + "2": { + "label": "{{option2PrimaryName}} kann helfen", + "label_disabled": "Kein {{option2RequiredMove}}", + "tooltip": "(+) {{option2PrimaryName}} rettet dich\n(+) {{option2PrimaryName}} erhält etwas EP", + "tooltip_disabled": "Du hast kein Pokémon, das {{option2RequiredMove}} erlernen kann", + "selected": "{{option2PrimaryName}} fliegt vor deinem Boot und führt dich zurück auf den richtigen Weg.${{option2PrimaryName}} scheint auch stärker geworden zu sein in dieser Zeit der Not!" + }, + "3": { + "label": "Umherirren", + "tooltip": "(-) Jedes deiner Pokémon verliert {{damagePercentage}}% seiner maximalen KP", + "selected": "Du treibst im Boot umher, steuerst ohne Richtung, bis du endlich ein Wahrzeichen siehst, das du wiedererkennst.$Du und deine Pokémon sind erschöpft von dem ganzen Vorfall." + } + }, + "outro": "Du bist wieder auf dem richtigen Weg." +} \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/mysterious-challengers-dialogue.json b/src/locales/de/mystery-encounters/mysterious-challengers-dialogue.json new file mode 100644 index 00000000000..040a8c269e0 --- /dev/null +++ b/src/locales/de/mystery-encounters/mysterious-challengers-dialogue.json @@ -0,0 +1,22 @@ +{ + "intro": "Mysteriöse Herausforderer sind aufgetaucht!", + "title": "Mysteriöse Herausforderer", + "description": "Wenn du einen Herausforderer besiegst, könntest du sie beeindrucken und eine Belohnung erhalten. Aber manche sehen ziemlich stark aus. Bist du bereit für die Herausforderung?", + "query": "Wen wirst du bekämpfen?", + "option": { + "1": { + "label": "Schlauer Trainer", + "tooltip": "(-) Standardkampf\n(+) TM Belohnungen" + }, + "2": { + "label": "Starker Trainer", + "tooltip": "(-) Harter Kampf\n(+) Gute Belohnungen" + }, + "3": { + "label": "Mächtigster Trainer", + "tooltip": "(-) Brutaler Kampf\n(+) Großartige Belohnungen" + }, + "selected": "Der Herausforderer tritt vor..." + }, + "outro": "Der mysteriöse Herausforderer wurde besiegt!" +} \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/mysterious-chest-dialogue.json b/src/locales/de/mystery-encounters/mysterious-chest-dialogue.json new file mode 100644 index 00000000000..4e575b955d4 --- /dev/null +++ b/src/locales/de/mystery-encounters/mysterious-chest-dialogue.json @@ -0,0 +1,23 @@ +{ + "intro": "Du hast...@d{32} eine Truhe gefunden?", + "title": "Die mysteriöse Truhe", + "description": "Eine wunderschön verzierte Truhe steht auf dem Boden. Da muss doch etwas Gutes drin sein... oder?", + "query": "Wirst du sie öffnen?", + "option": { + "1": { + "label": "Öffnen", + "tooltip": "@[SUMMARY_BLUE]{({{trapPercent}}%) Etwas Schreckliches}\n@[SUMMARY_GREEN]{({{commonPercent}}%) Standard Belohnung}\n@[SUMMARY_GREEN]{({{ultraPercent}}%) Gute Belohnung}\n@[SUMMARY_GREEN]{({{roguePercent}}%) Großartige Belohnung}\n@[SUMMARY_GREEN]{({{masterPercent}}%) Erstaunliche Belohnung}", + "selected": "Du öffnest die Truhe und findest...", + "normal": "Einfach ein paar normale Werkzeuge und Gegenstände.", + "good": "Ein paar ziemlich gute Werkzeuge und Gegenstände.", + "great": "Ein paar großartige Werkzeuge und Gegenstände.", + "amazing": "Ein erstaunlichen Gegenstand!", + "bad": "Oh nein!@d{32}\nDie Truhe war tatsächlich ein {{gimmighoulName}}!$Dein {{pokeName}} springt schützend vor dich aber wird dabei besiegt!" + }, + "2": { + "label": "Zu riskant, weggehen", + "tooltip": "(-) Keine Belohnung", + "selected": "Du gehst schnell weiter, mit einem leichten Gefühl der Reue." + } + } +} diff --git a/src/locales/de/mystery-encounters/part-timer-dialogue.json b/src/locales/de/mystery-encounters/part-timer-dialogue.json new file mode 100644 index 00000000000..dd86449092c --- /dev/null +++ b/src/locales/de/mystery-encounters/part-timer-dialogue.json @@ -0,0 +1,31 @@ +{ + "intro": "Eine geschäftige Person spricht dich an.", + "speaker": "Arbeitende Person", + "intro_dialogue": "Du siehst aus, als hättest du viele fähige Pokémon!$Wir können dich bezahlen, wenn du uns bei einigen Teilzeitjobs hilfst!", + "title": "Teilzeitjob", + "description": "Es scheint, als gäbe es viele Aufgaben, die erledigt werden müssen. Je besser dein Pokémon für eine Aufgabe geeignet ist, desto mehr Geld kann es verdienen.", + "query": "Welchen Job wählst du?", + "invalid_selection": "Das Pokémon muss genug KP haben.", + "option": { + "1": { + "label": "Lieferdienst", + "tooltip": "(-) Dein Pokémon nutzt seine Geschwindigkeit\n(+) Verdiene @[MONEY]{Geld}", + "selected": "Dein {{selectedPokemon}} arbeitet eine Schicht lang damit, Bestellungen an Kunden auszuliefern." + }, + "2": { + "label": "Lagerarbeit", + "tooltip": "(-) Dein Pokémon nutzt seine Stärke und Ausdauer\n(+) Verdiene @[MONEY]{Geld}", + "selected": "Dein {{selectedPokemon}} arbeitet eine Schicht lang damit, Gegenstände im Lager zu bewegen." + }, + "3": { + "label": "Verkäufer", + "tooltip": "(-) Dein {{option3PrimaryName}} nutzt {{option3PrimaryMove}}\n(+) Verdiene @[MONEY]{Geld}", + "disabled_tooltip": "Dein Pokémon muss bestimmte Attacken kennen, um diesen Job zu erledigen", + "selected": "Dein {{option3PrimaryName}} verbringt den Tag damit, {{option3PrimaryMove}} einzusetzen, um Kunden in den Laden zu locken!" + } + }, + "job_complete_good": "Danke für die Hilfe! Dein {{selectedPokemon}} war unglaublich hilfreich!$Hier ist dein Gehalt für den Tag.", + "job_complete_bad": "Dein {{selectedPokemon}} hat uns ein wenig geholfen!$Hier ist dein Gehalt für den Tag.", + "pokemon_tired": "Dein {{selectedPokemon}} ist erschöpft! Die AP aller seiner Attacken wurden auf 2 reduziert!", + "outro": "Komm doch bald wieder und hilf uns erneut!" +} \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/safari-zone-dialogue.json b/src/locales/de/mystery-encounters/safari-zone-dialogue.json new file mode 100644 index 00000000000..2302833036b --- /dev/null +++ b/src/locales/de/mystery-encounters/safari-zone-dialogue.json @@ -0,0 +1,46 @@ +{ + "intro": "Es ist die Safari-Zone!", + "title": "Die Safari-Zone", + "description": "Es gibt alle Arten von seltenen und besonderen Pokémon, die hier gefunden werden können!\nWenn du dich entscheidest, einzutreten, hast du kannst du in den nächsten 3 Wellen versuchen, besondere Pokémon zu fangen.\nAber sei gewarnt, diese Pokémon können fliehen, bevor du sie fangen kannst!", + "query": "Willst du eintreten?", + "option": { + "1": { + "label": "Eintreten", + "tooltip": "(-) Zahle {{option1Money, money}}\n@[SUMMARY_GREEN]{(?) Safari Zone}", + "selected": "Zeit, dein Glück herauszufordern!" + }, + "2": { + "label": "Weggehen", + "tooltip": "(-) Keine Belohnung", + "selected": "Du gehst deines Weges, mit einem leichten Gefühl der Reue." + } + }, + "safari": { + "1": { + "label": "Pokéball werfen", + "tooltip": "(+) Werfe einen Pokéball", + "selected": "Du wirfst einen Pokéball!" + }, + "2": { + "label": "Köder werfen", + "tooltip": "(+) Erhöht die Fangrate\n(-) Erhöht die Fluchtchance", + "selected": "Du wirfst einen Köder!" + }, + "3": { + "label":"Matsch werfen", + "tooltip": "(+) Vermindert die Fluchtchance\n(-) Chance, die Fangrate zu verringern", + "selected": "Du wirst ein wenig Matsch!" + }, + "4": { + "label": "Fliehen", + "tooltip": "(?) Fliehe vor diesem Pokémon" + }, + "watching": "{{pokemonName}} beobachtet alles aufmerksam!", + "eating": "{{pokemonName}} frisst!", + "busy_eating": "{{pokemonName}} konzentriert sich aufs Futter!", + "angry": "{{pokemonName}} ist wütend!", + "beside_itself_angry": "{{pokemonName}} ist außer sich vor Wut!", + "remaining_count": "{{remainingCount}} Pokémon übrig!" + }, + "outro": "Das war ein spannendes Abenteuer in der Safari-Zone!" +} \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/shady-vitamin-dealer-dialogue.json b/src/locales/de/mystery-encounters/shady-vitamin-dealer-dialogue.json new file mode 100644 index 00000000000..6579d94bd2e --- /dev/null +++ b/src/locales/de/mystery-encounters/shady-vitamin-dealer-dialogue.json @@ -0,0 +1,27 @@ +{ + "intro": "Ein Mann in einem dunklen Mantel kommt auf dich zu.", + "speaker": "Zwielichtiger Verkäufer", + "intro_dialogue": ".@d{16}.@d{16}.@d{16}$Ich habe die Ware, wenn du das Geld hast.$Aber sei sicher, dass deine Pokémon es vertragen können.", + "title": "Der Nährstoff-Verkäufer", + "description": "Der Mann öffnet seinen Mantel und zeigt dir einige Pokémon-Nährstoffe. Die Preise, die er nennt, scheinen ein wirklich gutes Angebot zu sein. Fast zu gut...\nEr bietet dir zwei Möglichkeiten zur Auswahl an.", + "query": "Welches Angebot wirst du wählen?", + "invalid_selection": "Pokémon must be healthy enough.", + "option": { + "1": { + "label": "Der billige Deal", + "tooltip": "(-) Zahle {{option1Money, money}}\n(-) Nebenwirkungen?\n(+) Das gewählte Pokémon erhält 2 zufällige Nährstoffe" + }, + "2": { + "label": "Der teure Deal", + "tooltip": "(-) Zahle {{option2Money, money}}\n(+) Das gewählte Pokémon erhält 2 zufällige Nährstoffe" + }, + "3": { + "label": "Weggehen", + "tooltip": "(-) Keine Belohnung", + "selected": "Ey, hätte ich nicht gedacht, dass du ein Feigling bist." + }, + "selected": "Der Mann überreicht dir zwei Flaschen und verschwindet schnell.${{selectedPokemon}} erhält {{boost1}} und {{boost2}} Nährstoffe!" + }, + "cheap_side_effects": "Aber die Medizin hatte Nebenwirkungen!$Dein {{selectedPokemon}} nimmt etwas Schaden,\nund sein Wesen wurde zu {{newNature}} geändert!", + "no_bad_effects": "Es scheint, als hätten die Nährstoffe keine Nebenwirkungen." +} \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/slumbering-snorlax-dialogue.json b/src/locales/de/mystery-encounters/slumbering-snorlax-dialogue.json new file mode 100644 index 00000000000..097bf3acd95 --- /dev/null +++ b/src/locales/de/mystery-encounters/slumbering-snorlax-dialogue.json @@ -0,0 +1,25 @@ +{ + "intro": "Als du einen schmalen Pfad entlang gehst, siehst du eine riesige Silhouette, die deinen Weg blockiert.$Du kommst näher, um zu sehen, dass ein {{snorlaxName}} friedlich schläft.$Es scheint, als gäbe es keinen Weg daran vorbei.", + "title": "Schlafendes {{snorlaxName}}", + "description": "Du könntest es angreifen, um es zum Bewegen zu bringen, oder einfach warten, bis es aufwacht. Wer weiß, wie lange das dauern könnte...", + "query": "Was wirst du tun?", + "option": { + "1": { + "label": "Kampf beginnen", + "tooltip": "(-) Schlafendes {{snorlaxName}} greift an\n(+) Spezielle Belohnung", + "selected": "Du trittst dem Pokémon ohne Furcht entgegen." + }, + "2": { + "label":"Warte, bis es sich bewegt", + "tooltip": "(-) Warte eine lange Zeit\n(+) Dein Team wird geheilt", + "selected": ".@d{32}.@d{32}.@d{32}$Du wartest sehr lange, bis das {{snorlaxName}} endlich aufwacht. Dein Team wird schläfrig...", + "rest_result": "Nachdem ihr alle aufgewacht seid, ist das {{snorlaxName}} nirgends zu finden - aber deine Pokémon sind alle geheilt!" + }, + "3": { + "label": "Klaue seine Items", + "tooltip": "(+) {{option3PrimaryName}} setzt {{option3PrimaryMove}} ein\n(+) Spezielle Belohnung", + "disabled_tooltip": "Dein Pokémon muss bestimmte Attacken beherrschen, um diese Option zu wählen.", + "selected": "Dein {{option3PrimaryName}} setzt {{option3PrimaryMove}} ein!$@s{item_fanfare}Es stiehlt die Überreste des schlafenden {{snorlaxName}}s und ihr macht euch aus dem Staub!" + } + } +} \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/teleporting-hijinks-dialogue.json b/src/locales/de/mystery-encounters/teleporting-hijinks-dialogue.json new file mode 100644 index 00000000000..552f082c20d --- /dev/null +++ b/src/locales/de/mystery-encounters/teleporting-hijinks-dialogue.json @@ -0,0 +1,27 @@ +{ + "intro": "Es ist eine seltsame Maschine, die laut summt...", + "title": "Teleportierende Streiche", + "description": "Die Maschine hat ein Schild, auf dem steht:\n\"Geld einwerfen und in die Kapsel steigen.\"\nVielleicht kann sie dich irgendwohin transportieren...", + "query": "Was wirst du tun?", + "option": { + "1": { + "label": "Geld einwerfen", + "tooltip": "(-) Bezahle {{price, money}}\n(?) Teleportiere dich in ein neues Biom", + "selected": "Du wirfst etwas Geld ein, und die Kapsel öffnet sich.\nDu steigst ein..." + }, + "2": { + "label": "Ein Pokémon hilft", + "tooltip": "(-) {{option2PrimaryName}} hilft\n(+) {{option2PrimaryName}} erhält EXP\n(?) Teleportiere dich in ein neues Biom", + "disabled_tooltip": "Du brauchst ein Stahl- oder Elektro-Pokémon, um diese Option zu wählen.", + "selected": "Der Typ von {{option2PrimaryName}} ermöglicht es ihm, die Bezahlschranke der Maschine zu umgehen!$Die Kapsel öffnet sich, und du steigst ein..." + }, + "3": { + "label": "Maschine inspizieren", + "tooltip": "(-) Pokémon-Kampf", + "selected": "Du wirst von den blinkenden Lichtern und den seltsamen Geräuschen der Maschine angezogen...$Du bemerkst nicht einmal, wie ein wildes Pokémon sich anschleicht und dich überfällt!" + } + }, + "transport": "Die Maschine zittert heftig und macht seltsame Geräusche!$Kaum hat es begonnen, wird es wieder ruhig.", + "attacked": "Du trittst in eine völlig neue Gegend und erschreckst ein wildes Pokémon!$Das wilde Pokémon greift an!", + "boss_enraged": "Das wilde {{enemyPokemon}} ist wütend geworden!" +} \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/the-expert-pokemon-breeder-dialogue.json b/src/locales/de/mystery-encounters/the-expert-pokemon-breeder-dialogue.json new file mode 100644 index 00000000000..d2fcc91d153 --- /dev/null +++ b/src/locales/de/mystery-encounters/the-expert-pokemon-breeder-dialogue.json @@ -0,0 +1,31 @@ +{ + "intro": "Ein Trainer mit vielen Pokémon-Eiern!", + "intro_dialogue": "Hey Trainer!$Es sieht so aus, als ob einige deiner Pokémon sich ein wenig niedergeschlagen fühlen.$Warum kämpfst du nicht gegen mich, um sie aufzumuntern?", + "title": "Triff die Pokémon-Züchter-Expertin!", + "description": "Du wurdest zu einem Kampf herausgefordert, bei dem du nur @[TOOLTIP_TITLE]{ein einziges Pokémon verwenden darfst}. Es könnte schwierig werden, aber es würde sicherlich die Bindung vertiefen, die du mit dem Pokémon hast, das du wählst!$Wenn du gewinnst, wird die Züchterin dir einige @[TOOLTIP_TITLE]{Pokémon-Eier} geben!", + "query": "Wer wird für dich kämpfen?", + "cleffa_1_nickname": "Ass-Pii-Rin", + "cleffa_2_nickname": "Pi-mal-Daumen", + "cleffa_3_nickname": "Pii-cknick", + "option": { + "1": { + "label": "{{pokemon1Name}}", + "tooltip_base": "(-) Harter Kampf\n(+) Erhöhe Freundschaft mit {{pokemon1Name}}" + }, + "2": { + "label": "{{pokemon2Name}}", + "tooltip_base": "(-) Harter Kampf\n(+) Erhöhe Freundschaft mit {{pokemon2Name}}" + }, + "3": { + "label": "{{pokemon3Name}}", + "tooltip_base": "(-) Harter Kampf\n(+) Erhöhe Freundschaft mit {{pokemon3Name}}" + }, + "selected": "Lass uns beginnen!" + }, + "outro": "Schau wie glücklich dein {{chosenPokemon}} nun ist!$Hier, diese Pokémon-Eier kannst du auch haben.", + "outro_failed": "Wie enttäuschend...$Es sieht so aus, als hättest du noch einen langen Weg vor dir, um das Vertrauen deines Pokémon zu gewinnen!", + "gained_eggs": "@s{item_fanfare}Du erhählst {{numEggs}}!", + "eggs_tooltip": "\n(+) Erhalte {{eggs}}", + "numEggs_one": "{{count}} Ei der Stufe {{rarity}}", + "numEggs_other": "{{count}} Eier der Stufe {{rarity}}" +} diff --git a/src/locales/de/mystery-encounters/the-pokemon-salesman-dialogue.json b/src/locales/de/mystery-encounters/the-pokemon-salesman-dialogue.json new file mode 100644 index 00000000000..1e055fa5ed0 --- /dev/null +++ b/src/locales/de/mystery-encounters/the-pokemon-salesman-dialogue.json @@ -0,0 +1,23 @@ +{ + "intro": "Ein fröhlicher älterer Mann kommt auf dich zu.", + "speaker": "Reicher Mann", + "intro_dialogue": "Hallo! Ich habe ein Angebot, das du nicht ablehnen kannst!", + "title": "Der Pokémon-Verkäufer", + "description": "Dieses {{purchasePokemon}} ist extrem einzigartig und hat eine Fähigkeit, die normalerweise nicht bei seiner Art zu finden ist! Ich lasse dich dieses tolle {{purchasePokemon}} für gerade einmal {{price, money}} haben!\"\n\"Was sagst du dazu?\"", + "description_shiny": "Dieses {{purchasePokemon}} ist extrem einzigartig und hat eine Farbe, die normalerweise nicht bei seiner Art zu finden ist! Ich lasse dich dieses tolle {{purchasePokemon}} für gerade einmal {{price, money}} haben!\"\n\"Was sagst du dazu?\"", + "query": "Was wirst du tun?", + "option": { + "1": { + "label": "Akzeptieren", + "tooltip": "(-) Bezahlen {{price, money}}\n(+) Erhalte ein {{purchasePokemon}} mit seiner versteckten Fähigkeit", + "tooltip_shiny": "(-) Bezahlen {{price, money}}\n(+) Erhalte ein schillerndes {{purchasePokemon}}", + "selected_message": "Du bezahlst einen unverschämten Betrag und kaufst das {{purchasePokemon}}.", + "selected_dialogue": "Ausgezeichnete Wahl!$Ich sehe, dass du ein gutes Auge für Geschäfte hast.$Oh, ja...@d{64} Rückgaben werden nicht akzeptiert, hast du das verstanden?" + }, + "2": { + "label": "Ablehnen", + "tooltip": "(-) Keine Belohnung", + "selected": "Nein?@d{32} Du sagst nein?$Ich mache das nur als Gefallen für dich!" + } + } +} \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/the-strong-stuff-dialogue.json b/src/locales/de/mystery-encounters/the-strong-stuff-dialogue.json new file mode 100644 index 00000000000..0fd1a7ad64f --- /dev/null +++ b/src/locales/de/mystery-encounters/the-strong-stuff-dialogue.json @@ -0,0 +1,21 @@ +{ + "intro": "Es ist ein riesiger {{shuckleName}} und ein riesiger Vorrat an... Saft?", + "title": "Das gute Zeug", + "description": "Das {{shuckleName}} das deinen Weg blockiert, sieht unglaublich stark aus. In der Zwischenzeit strahlt der Saft daneben eine Art Kraft aus.\nDas {{shuckleName}} streckt seine Fühler in deine Richtung aus. Es scheint, als wolle es etwas tun...", + "query": "Was wirst du tun?", + "option": { + "1": { + "label": "Dem {{shuckleName}} näher kommen", + "tooltip": "(?) Etwas Schreckliches oder Wunderbares könnte passieren", + "selected": "Dir wird schwarz vor Augen...", + "selected_2": "@f{150}Als du aufwachst, ist das {{shuckleName}} verschwunden und der Saftvorrat komplett geleert.${{highBstPokemon1}} und {{highBstPokemon2}} fühlen eine schreckliche Lethargie über sich kommen!$Ihre Basiswerte wurden um {{reductionValue}} reduziert!$Deine verbleibenden Pokémon fühlen jedoch eine unglaubliche Vitalität!$Ihre Basiswerte werden um {{increaseValue}} erhöht!" + }, + "2": { + "label": "Das {{shuckleName}} bekämpfen", + "tooltip": "(-) Schwieriger Kampf\n(+) Spezielle Belohnungen", + "selected": "Das {{shuckleName}} wird wütend und trinkt etwas von seinem Saft, bevor es angreift!", + "stat_boost": "Der Saft des {{shuckleName}} erhöht seine Werte!" + } + }, + "outro": "Was ist hier gerade passiert?" +} \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/the-winstrate-challenge-dialogue.json b/src/locales/de/mystery-encounters/the-winstrate-challenge-dialogue.json new file mode 100644 index 00000000000..e3d0dddd21a --- /dev/null +++ b/src/locales/de/mystery-encounters/the-winstrate-challenge-dialogue.json @@ -0,0 +1,22 @@ +{ + "intro": "Eine Familie steht vor ihrem Haus!", + "speaker": "Die Sihgers", + "intro_dialogue": "Wir sind die Sihgers!$Wie wäre es, wenn du gegen unsere Familie in einer Reihe von Pokémon-Kämpfen antrittst?", + "title": "Die Sihgers-Herausforderung", + "description": "Die Sihgers sind eine Familie von 5 Trainern, und sie wollen kämpfen! Wenn du sie alle hintereinander besiegst, bekommst du einen grandiosen Preis. Aber kannst du die Hitze aushalten?", + "query": "Was wirst du tun?", + "option": { + "1": { + "label": "Die Herausforderung annehmen", + "tooltip": "(-) Brutaler Kampf\n(+) Spezielle Belohnung", + "selected": "Lass die Herausforderung beginnen!" + }, + "2": { + "label": "Die Herausforderung ablehnen", + "tooltip": "(+) Team wird geheilt\n(+) Erhalte ein Supersondererbonbon", + "selected": "Das ist zu schade. Dein Team sieht ziemlich mitgenommen aus, warum ruhst du dich nicht eine Weile aus?" + } + }, + "victory": "Glückwunsch, du hast unsere Herausforderung gemeistert!$Zuerst möchten wir dir diesen Gutschein geben.", + "victory_2": "Außerdem benutzt unsere Familie diese Machoschiene, um unsere Pokémon effektiver zu tranieren.$Du brauchst es vielleicht nicht, da du uns alle geschlagen hast, aber wir hoffen, dass du es trotzdem annimmst!" +} \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/training-session-dialogue.json b/src/locales/de/mystery-encounters/training-session-dialogue.json new file mode 100644 index 00000000000..f7d22ef6deb --- /dev/null +++ b/src/locales/de/mystery-encounters/training-session-dialogue.json @@ -0,0 +1,33 @@ +{ + "intro": "Du stolperst über einige Trainingsutensilien und Vorräte.", + "title": "Traningssitzung", + "description": "Diese Vorräte sehen so aus, als könnten sie verwendet werden, um ein Mitglied deines Teams zu trainieren! Es gibt ein paar Möglichkeiten, wie du dein Pokémon trainieren könntest, indem du gegen es mit dem Rest deines Teams kämpfst.", + "query": "Wie möchtest du trainieren?", + "invalid_selection": "Pokémon muss genügend KP haben.", + "option": { + "1": { + "label": "Leichtes Training", + "tooltip": "(-) Leichter Kampf\n(+) Verbessere 2 zufällige IS-Werte des Pokémon", + "finished": "{{selectedPokemon}} kommt zurück, fühlt sich erschöpft aber zufrieden!$Seine {{stat1}} und {{stat2}} IS-Werte wurden verbessert!" + }, + "2": { + "label": "Moderates Training", + "tooltip": "(-) Moderater Kampf\n(+) Ändere das Wesen des Pokémon", + "select_prompt": "Wähle ein neues Wesen aus, um dein Pokémon zu trainieren.", + "finished": "{{selectedPokemon}} kehrt zurück, fühlt sich erschöpft aber zufrieden!$Es hat nun ein neues Wesen: {{nature}}!" + }, + "3": { + "label": "Schweres Training", + "tooltip": "(-) Harter Kampf\n(+) Ändere die Fähigkeit des Pokémon", + "select_prompt": "Wähle eine neue Fähigkeit aus, um dein Pokémon zu trainieren.", + "finished": "{{selectedPokemon}} kehrt zurück, fühlt sich erschöpft aber zufrieden!$Seine Fähigkeit wurde zu {{ability}} geändert!" + }, + "4": { + "label": "Weggehen", + "tooltip": "(-) Keine Belohnung", + "selected": "Du hast keine Zeit für Training und gehst weiter." + }, + "selected": "{{selectedPokemon}} bewegt sich über die Lichtung, um dir gegenüberzutreten..." + }, + "outro": "Das war eine erfolgreiche Trainingssitzung!" +} \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/trash-to-treasure-dialogue.json b/src/locales/de/mystery-encounters/trash-to-treasure-dialogue.json new file mode 100644 index 00000000000..da744ccb697 --- /dev/null +++ b/src/locales/de/mystery-encounters/trash-to-treasure-dialogue.json @@ -0,0 +1,20 @@ +{ + "intro":"Ein riesieger Haufen Müll. Wo kommt der auf einmal her?", + "title": "Vom Müllhaufen zum Schatzhaufen", + "description": "Der Müllberg ragt über dir auf und du kannst einige wertvolle Gegenstände im Müll entdecken. Bist du sicher, dass du dich in den Dreck wälzen willst, um sie zu bekommen?", + "query": "Was willst du tun?", + "option": { + "1": { + "label": "Nach Wertsachen suchen", + "tooltip": "(-) Heilitems kosten ab jetzt das Dreifache\n(+) Erhalte tolle Items", + "selected": "Du arbeitest dich durch den Müllhaufen und wirst von Dreck überzogen.$Kein respektabler Ladenbesitzer wird dir in deinem schmutzigen Zustand etwas verkaufen!$Aber es gibt ja auch andere... weniger respektable.$Natürlich verlangen sie höhere Preise.$Aber du hast einige unglaubliche Items im Müll gefunden!" + + }, + "2": { + "label": "Genauer untersuchen", + "tooltip": "(?) Finde die Quelle des Mülls", + "selected": "Du wanderst um den Müllhaufen herum und suchst nach Hinweisen, wie dieser hier gelandet sein könnte...", + "selected_2": "Der Müll bewegt sich! Es war nicht nur Müll, es war ein Pokémon!" + } + } +} diff --git a/src/locales/de/mystery-encounters/uncommon-breed-dialogue.json b/src/locales/de/mystery-encounters/uncommon-breed-dialogue.json new file mode 100644 index 00000000000..efd5ff52e16 --- /dev/null +++ b/src/locales/de/mystery-encounters/uncommon-breed-dialogue.json @@ -0,0 +1,26 @@ +{ + "intro": "Das ist kein gewöhnliches Pokémon!", + "title": "Ungewöhnliche Züchtung", + "description": "Das {{enemyPokemon}} sieht im Vergleich zu anderen seiner Art besonders aus. @[TOOLTIP_TITLE]{Vielleicht kennt es einen besondere Attacke?} Du könntest es einfach bekämpfen und fangen, aber es gibt vielleicht auch eine Möglichkeit, es zu befreunden.", + "query": "Was wirst du tun?", + "option": { + "1": { + "label": "Kampf beginnen", + "tooltip": "(-) Schwieriger Kampf\n(+) Starkes fangbares Pokémon", + "selected": "Du stellst dich dem {{enemyPokemon}} ohne Furcht.", + "stat_boost": "Die gesteigerten Fähigkeiten des {{enemyPokemon}} erhöhen seine Werte!" + }, + "2": { + "label": "Ihm Futter geben", + "disabled_tooltip": "Du brauchst 4 Beeren, um diese Option zu wählen", + "tooltip": "(-) Gib 4 Beeren\n(+) Das {{enemyPokemon}} mag dich", + "selected": "Du wirfst die Beeren zu {{enemyPokemon}}!$Es frisst sie glücklich!$Das {{enemyPokemon}} möchte sich dir anschließen!" + }, + "3": { + "label": "Es befreunden", + "disabled_tooltip": "Dein Pokémon muss bestimmte Attacken kennen, um diese Option zu wählen", + "tooltip": "(+) {{option3PrimaryName}} setzt {{option3PrimaryMove}} ein\n(+) Das {{enemyPokemon}} mag dich", + "selected": "Dein {{option3PrimaryName}} setzt {{option3PrimaryMove}} ein, um das {{enemyPokemon}} zu bezaubern!$Das {{enemyPokemon}} möchte sich dir anschließen!" + } + } +} \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/weird-dream-dialogue.json b/src/locales/de/mystery-encounters/weird-dream-dialogue.json new file mode 100644 index 00000000000..11fe3b4078c --- /dev/null +++ b/src/locales/de/mystery-encounters/weird-dream-dialogue.json @@ -0,0 +1,22 @@ +{ + "intro": "Eine schemenhafte Frau versperrt dir den Weg. Irgendetwas an ihr ist beunruhigend...", + "speaker": "Frau", + "intro_dialogue": "Ich habe deine Zukünfte gesehen, deine Vergangenheiten...$Siehst du sie auch?", + "title": "???", + "description": "Die Worte der Frau hallen in deinem Kopf wider. Es war nicht nur eine einzelne Stimme, sondern eine unendliche Vielzahl aus allen Zeiten und Realitäten. Dir wird schwindelig, die Frage bleibt in deinem Kopf hängen...\n@[TOOLTIP_TITLE]{\"Ich habe deine Zukünfte gesehen, deine Vergangenheiten...Siehst du sie auch?\"}", + "query": "Was wirst du tun?", + "option": { + "1": { + "label": "\"Ich sehe sie\"", + "tooltip": "@[SUMMARY_GREEN]{(?) Beeinflusst deine Pokémon}", + "selected": "Ihre Hand berührt dich und alles wird schwarz.$Dann...@d{64} Du siehst alles. Jede Zeitlinie, all deine verschiedenen Ichs, Vergangenheit und Zukunft.$Alles, was dich ausmacht, alles, was du sein wirst...@d{64}", + "cutscene": "Du siehst deine Pokémon,@d{32} wie sie sich aus jeder Realität vereinen, um etwas Neues zu werden...@d{64}", + "dream_complete": "Als du erwachst, ist die Frau - war es eine Frau oder ein Geist? - verschwunden...$.@d{32}.@d{32}.@d{32}$Dein Pokémon-Team hat sich verändert... Oder ist es das gleiche Team, das du schon immer hattest?" + }, + "2": { + "label": "Schnell wegrennen", + "tooltip": "(-) Beeinflusst deine Pokémon", + "selected": "Du reißt deinen Geist aus einem betäubenden Griff und fliehst hastig.$Als du schließlich anhältst, um dich zu sammeln, überprüfst du die Pokémon in deinem Team.$Aus irgendeinem Grund hat sich das Level aller Pokémon verringert!" + } + } +} \ No newline at end of file diff --git a/src/locales/de/party-ui-handler.json b/src/locales/de/party-ui-handler.json index fb5a5207569..8c662ef2b78 100644 --- a/src/locales/de/party-ui-handler.json +++ b/src/locales/de/party-ui-handler.json @@ -15,6 +15,7 @@ "UNPAUSE_EVOLUTION": "Entwicklung fortsetzen", "REVIVE": "Wiederbeleben", "RENAME": "Umbenennen", + "SELECT": "Auswählen", "choosePokemon": "Wähle ein Pokémon.", "doWhatWithThisPokemon": "Was soll mit diesem Pokémon geschehen?", "noEnergy": "{{pokemonName}} ist nicht fit genug, um zu kämpfen!", @@ -44,4 +45,4 @@ "untilWeMeetAgain": "Bis wir uns wiedersehen, {{pokemonName}}!", "sayonara": "Sayonara, {{pokemonName}}!", "smellYaLater": "Also dann, man riecht sich! Ciao!, {{pokemonName}}!" -} \ No newline at end of file +} 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/status-effect.json b/src/locales/de/status-effect.json index c30d432fe79..bec1bf14b3f 100644 --- a/src/locales/de/status-effect.json +++ b/src/locales/de/status-effect.json @@ -1,12 +1,6 @@ { "none": { - "name": "None", - "description": "", - "obtain": "", - "obtainSource": "", - "activation": "", - "overlap": "", - "heal": "" + "name": "None" }, "poison": { "name": "Gift", 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..c722263ff55 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", @@ -161,5 +167,19 @@ "alder_iris_double": "Lauro & Lilia", "iris_alder_double": "Lilia & Lauro", "piers_marnie_double": "Nezz & Mary", - "marnie_piers_double": "Mary & Nezz" + "marnie_piers_double": "Mary & Nezz", + + "buck": "Avenaro", + "cheryl": "Raissa", + "marley": "Charlie", + "mira": "Orisa", + "riley": "Urs", + "victor": "Viktor", + "victoria": "Viktoria", + "vivi": "Sieglinde", + "vicky": "Vicky", + "vito": "Paul", + "bug_type_superfan": "Käfersammler-Superfan", + "expert_pokemon_breeder": "Pokémon-Züchter-Expertin" + } diff --git a/src/locales/de/trainer-titles.json b/src/locales/de/trainer-titles.json index 21a4b2fa7b6..ed82d6dc458 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,8 @@ "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", + "the_winstrates": "Sihgers" } 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 b31826b0244..481f69db250 100644 --- a/src/locales/en/battler-tags.json +++ b/src/locales/en/battler-tags.json @@ -73,5 +73,6 @@ "tarShotOnAdd": "{{pokemonNameWithAffix}} became weaker to fire!", "substituteOnAdd": "{{pokemonNameWithAffix}} put in a substitute!", "substituteOnHit": "The substitute took damage for {{pokemonNameWithAffix}}!", - "substituteOnRemove": "{{pokemonNameWithAffix}}'s substitute faded!" + "substituteOnRemove": "{{pokemonNameWithAffix}}'s substitute faded!", + "autotomizeOnAdd": "{{pokemonNameWithAffix}} became nimble!" } 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..6958c579dd5 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 "./mystery-encounters/mysterious-chest-dialogue.json"; +import mysteriousChallengers from "./mystery-encounters/mysterious-challengers-dialogue.json"; +import darkDeal from "./mystery-encounters/dark-deal-dialogue.json"; +import departmentStoreSale from "./mystery-encounters/department-store-sale-dialogue.json"; +import fieldTrip from "./mystery-encounters/field-trip-dialogue.json"; +import fieryFallout from "./mystery-encounters/fiery-fallout-dialogue.json"; +import fightOrFlight from "./mystery-encounters/fight-or-flight-dialogue.json"; +import safariZone from "./mystery-encounters/safari-zone-dialogue.json"; +import shadyVitaminDealer from "./mystery-encounters/shady-vitamin-dealer-dialogue.json"; +import slumberingSnorlax from "./mystery-encounters/slumbering-snorlax-dialogue.json"; +import trainingSession from "./mystery-encounters/training-session-dialogue.json"; +import theStrongStuff from "./mystery-encounters/the-strong-stuff-dialogue.json"; +import pokemonSalesman from "./mystery-encounters/the-pokemon-salesman-dialogue.json"; +import offerYouCantRefuse from "./mystery-encounters/an-offer-you-cant-refuse-dialogue.json"; +import delibirdy from "./mystery-encounters/delibirdy-dialogue.json"; +import absoluteAvarice from "./mystery-encounters/absolute-avarice-dialogue.json"; +import aTrainersTest from "./mystery-encounters/a-trainers-test-dialogue.json"; +import trashToTreasure from "./mystery-encounters/trash-to-treasure-dialogue.json"; +import berriesAbound from "./mystery-encounters/berries-abound-dialogue.json"; +import clowningAround from "./mystery-encounters/clowning-around-dialogue.json"; +import partTimer from "./mystery-encounters/part-timer-dialogue.json"; +import dancingLessons from "./mystery-encounters/dancing-lessons-dialogue.json"; +import weirdDream from "./mystery-encounters/weird-dream-dialogue.json"; +import theWinstrateChallenge from "./mystery-encounters/the-winstrate-challenge-dialogue.json"; +import teleportingHijinks from "./mystery-encounters/teleporting-hijinks-dialogue.json"; +import bugTypeSuperfan from "./mystery-encounters/bug-type-superfan-dialogue.json"; +import funAndGames from "./mystery-encounters/fun-and-games-dialogue.json"; +import uncommonBreed from "./mystery-encounters/uncommon-breed-dialogue.json"; +import globalTradeSystem from "./mystery-encounters/global-trade-system-dialogue.json"; +import expertPokemonBreeder from "./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 bc58e2878dd..93d25e506ba 100644 --- a/src/locales/en/move-trigger.json +++ b/src/locales/en/move-trigger.json @@ -66,6 +66,7 @@ "suppressAbilities": "{{pokemonName}}'s ability\nwas suppressed!", "revivalBlessing": "{{pokemonName}} was revived!", "swapArenaTags": "{{pokemonName}} swapped the battle effects affecting each side of the field!", + "chillyReception": "{{pokemonName}} is preparing to tell a chillingly bad joke!", "exposedMove": "{{pokemonName}} identified\n{{targetPokemonName}}!", "safeguard": "{{targetName}} is protected by Safeguard!", "substituteOnOverlap": "{{pokemonName}} already\nhas a substitute!", 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..553f4822f07 --- /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": "(-) Extremely 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..188c94c7994 --- /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 any 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..c60a5eb0c01 --- /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 One Pokémon's Ability", + "selected": "Your pitiful Pokémon are poised for a pathetic performance!", + "apply_ability_dialogue": "A sensational showcase!\nYour savvy suits a special 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": "(?) Affects One Pokémon's 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": "(?) Affects Your Pokémons' 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..20b01708be0 --- /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 despondent.\n\nPerhaps 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 any 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..0a16c424f5a --- /dev/null +++ b/src/locales/en/mystery-encounters/delibirdy-dialogue.json @@ -0,0 +1,29 @@ + + +{ + "intro": "A flock 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}} flock 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..84abebd4cbb --- /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 special 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 improved 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..e789771b7b1 --- /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]{({{trapPercent}}%) Something terrible}\n@[SUMMARY_GREEN]{({{commonPercent}}%) Okay Rewards}\n@[SUMMARY_GREEN]{({{ultraPercent}}%) Good Rewards}\n@[SUMMARY_GREEN]{({{roguePercent}}%) Great Rewards}\n@[SUMMARY_GREEN]{({{masterPercent}}%) 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..b96e3b5beb8 --- /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 @[TOOLTIP_TITLE]{{{numEncounters}} 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..0dc22574686 --- /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 @[TOOLTIP_TITLE]{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 @[TOOLTIP_TITLE]{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..ec557eb8cea --- /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 formidable. Meanwhile, the juice next to it emanates 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..e2963b98d61 --- /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 @[TOOLTIP_TITLE]{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 Against 5 Trainers\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..61597dc7c89 --- /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 @[TOOLTIP_TITLE]{battling and defeating 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 with Chosen Pokémon\n(+) Permanently Improve 2 Random IVs of Chosen Pokémon", + "finished": "{{selectedPokemon}} returns, feeling\nworn out but accomplished!$Its {{stat1}} and {{stat2}} IVs were improved!" + }, + "2": { + "label": "Moderate Training", + "tooltip": "(-) Moderate Battle with Chosen Pokémon\n(+) Permanently Change Chosen 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 with Chosen Pokémon\n(+) Permanently Change Chosen 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..da35082248c --- /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 Will Cost {{costMultiplier}}x\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/status-effect.json b/src/locales/en/status-effect.json index fdbacfdb9be..64468047761 100644 --- a/src/locales/en/status-effect.json +++ b/src/locales/en/status-effect.json @@ -1,12 +1,6 @@ { "none": { - "name": "None", - "description": "", - "obtain": "", - "obtainSource": "", - "activation": "", - "overlap": "", - "heal": "" + "name": "None" }, "poison": { "name": "Poison", 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/achv.json b/src/locales/es/achv.json index 14501dbdb6b..b66650ee137 100644 --- a/src/locales/es/achv.json +++ b/src/locales/es/achv.json @@ -174,5 +174,9 @@ "INVERSE_BATTLE": { "name": "Espejo ojepsE", "description": "Completa el reto de Combate Inverso.\n.osrevnI etabmoC ed oter le atelpmoC" + }, + "BREEDERS_IN_SPACE": { + "name": "¡Criapokémon en el Espacio!", + "description": "Derrota al Criapokémon Experto en el Bioma Espacial." } } diff --git a/src/locales/es/battle-info.json b/src/locales/es/battle-info.json index 9e26dfeeb6e..2d7561c40b3 100644 --- a/src/locales/es/battle-info.json +++ b/src/locales/es/battle-info.json @@ -1 +1,3 @@ -{} \ No newline at end of file +{ + "generation": "Generación {{generation}}" +} diff --git a/src/locales/es/battle.json b/src/locales/es/battle.json index c79315f297b..42c1346c118 100644 --- a/src/locales/es/battle.json +++ b/src/locales/es/battle.json @@ -1,89 +1,107 @@ { "bossAppeared": "¡{{bossName}} te corta el paso!", "trainerAppeared": "¡{{trainerName}}\nte desafía!", + "trainerAppearedDouble": "¡{{trainerName}}\nquieren luchar!", "trainerSendOut": "¡{{trainerName}} saca a\n{{pokemonName}}!", - "singleWildAppeared": "¡Un {{pokemonName}} salvaje te corta el paso!", - "multiWildAppeared": "¡Un {{pokemonName1}} y un {{pokemonName2}} salvajes\nte cortan el paso!", - "playerComeBack": "¡{{pokemonName}}, ven aquí!", + "singleWildAppeared": "¡{{pokemonName}} salvaje te corta el paso!", + "multiWildAppeared": "¡{{pokemonName1}} y {{pokemonName2}} salvajes\nte cortan el paso!", + "playerComeBack": "¡{{pokemonName}}, vuelve!", "trainerComeBack": "¡{{trainerName}} retira a {{pokemonName}} del combate!", "playerGo": "¡Adelante, {{pokemonName}}!", "trainerGo": "¡{{trainerName}} saca a {{pokemonName}}!", - "switchQuestion": "¿Quieres cambiar a\n{{pokemonName}}?", + "switchQuestion": "¿Quieres cambiar de {{pokemonName}}?", "trainerDefeated": "¡Has derrotado a\n{{trainerName}}!", - "moneyWon": "¡Has ganado\n₽{{moneyAmount}} por vencer!", - "pokemonCaught": "¡{{pokemonName}} atrapado!", - "addedAsAStarter": "{{pokemonName}} ha sido añadido\na tus iniciales!", - "partyFull": "Tu equipo esta completo.\n¿Quieres liberar un Pokémon para meter a {{pokemonName}}?", + "moneyWon": "¡Has ganado\n{{moneyAmount}}₽ por vencer!", + "moneyPickedUp": "¡Has recogido {{moneyAmount}}₽!", + "pokemonCaught": "¡{{pokemonName}} ha sido atrapado!", + "pokemonObtained": "¡Has recibido a {{pokemonName}}!", + "pokemonBrokeFree": "¡Oh, no!\n¡El Pokémon se ha escapado!", + "pokemonFled": "¡El {{pokemonName}} salvaje ha huido!", + "playerFled": "¡Huiste del {{pokemonName}}!", + "addedAsAStarter": "¡{{pokemonName}} ha sido añadido a tus iniciales!", + "partyFull": "Tu equipo está completo.\n¿Quieres liberar a un Pokémon para quedarte con {{pokemonName}}?", "pokemon": "Pokémon", - "sendOutPokemon": "¡Adelante, {{pokemonName}}!", + "sendOutPokemon": "¡Vamos, {{pokemonName}}!", "hitResultCriticalHit": "¡Un golpe crítico!", "hitResultSuperEffective": "¡Es supereficaz!", "hitResultNotVeryEffective": "No es muy eficaz…", - "hitResultNoEffect": "No afecta a {{pokemonName}}!", + "hitResultNoEffect": "No afecta a {{pokemonName}}...", "hitResultImmune": "¡No afecta a {{pokemonName}}!", - "hitResultOneHitKO": "¡KO en 1 golpe!", + "hitResultOneHitKO": "¡KO de un golpe!", "attackFailed": "¡Pero ha fallado!", "attackMissed": "¡{{pokemonNameWithAffix}}\nha evitado el ataque!", "attackHitsCount": "N.º de golpes: {{count}}.", - "rewardGain": "¡Has obtenido\n{{modifierName}}!", + "rewardGain": "¡Has conseguido\n{{modifierName}}!", "expGain": "{{pokemonName}} ha ganado\n{{exp}} puntos de experiencia.", - "levelUp": "¡{{pokemonName}} ha subido al \nNv. {{level}}!", + "levelUp": "¡{{pokemonName}} ha subido a\nNv. {{level}}!", "learnMove": "¡{{pokemonName}} ha aprendido {{moveName}}!", - "learnMovePrompt": "{{pokemonName}} quiere aprender\n{{moveName}}.", - "learnMoveLimitReached": "Pero, {{pokemonName}} ya conoce\ncuatro movimientos.", + "learnMovePrompt": "{{pokemonName}} quiere aprender {{moveName}}.", + "learnMoveLimitReached": "Pero {{pokemonName}} ya conoce\ncuatro movimientos.", "learnMoveReplaceQuestion": "¿Quieres sustituir uno de sus movimientos por {{moveName}}?", "learnMoveStopTeaching": "¿Prefieres que no aprenda\n{{moveName}}?", "learnMoveNotLearned": "{{pokemonName}} no ha aprendido {{moveName}}.", - "learnMoveForgetQuestion": "¿Qué movimiento quieres que olvide?", + "learnMoveForgetQuestion": "¿Qué movimiento debería olvidar?", "learnMoveForgetSuccess": "{{pokemonName}} ha olvidado cómo utilizar {{moveName}}.", - "countdownPoof": "@d{32}1, @d{15}2, @d{15}y@d{15}… @d{15}… @d{15}… @d{15}@s{se/pb_bounce_1}¡Puf!", - "learnMoveAnd": "Y…", + "countdownPoof": "@d{32}1, @d{15}2, @d{15}3@d{15}… @d{15}… @d{15}… @d{15}@s{se/pb_bounce_1}Y...", + "learnMoveAnd": "¡Puf!", "levelCapUp": "¡Se ha incrementado el\nnivel máximo a {{levelCap}}!", "moveNotImplemented": "{{moveName}} aún no está implementado y no se puede seleccionar.", - "moveNoPP": "¡No hay suficientes PP\npara este movimiento!", - "moveDisabled": "!No puede usar {{moveName}} porque ha sido anulado!", + "moveNoPP": "¡No quedan PP para este movimiento!", + "moveDisabled": "¡{{moveName}} está anulado!", "disableInterruptedMove": "¡Se ha anulado el movimiento {{moveName}}\nde {{pokemonNameWithAffix}}!", "noPokeballForce": "Una fuerza misteriosa\nte impide usar Poké Balls.", - "noPokeballTrainer": "¡No puedes atrapar a los\nPokémon de los demás!", - "noPokeballMulti": "¡No se pueden lanzar Poké Balls\ncuando hay más de un Pokémon!", - "noPokeballStrong": "¡Este Pokémon es demasiado fuerte para ser capturado!\nNecesitas bajarle los PS primero!", + "noPokeballTrainer": "¡No está bien capturar los\nPokémon de los demás!", + "noPokeballMulti": "¡No puedes lanzar Poké Balls\ncuando hay más de un Pokémon!", + "noPokeballStrong": "Este Pokémon es demasiado fuerte para ser capturado.\n¡Baja sus PS!", + "noPokeballMysteryEncounter": "¡No puedes capturar este Pokémon!", "noEscapeForce": "Una fuerza misteriosa\nte impide huir.", - "noEscapeTrainer": "¡No puedes huir de los\ncombates contra Entrenadores!", - "noEscapePokemon": "¡El movimiento {{moveName}} de {{pokemonName}} impide la huida!", + "noEscapeTrainer": "¡No puedes huir de los\ncombates contra entrenadores!", + "noEscapePokemon": "¡{{moveName}} de {{pokemonName}} no te deja huir!", "runAwaySuccess": "¡Escapas sin problemas!", "runAwayCannotEscape": "¡No has podido escapar!", "escapeVerbSwitch": "cambiar", "escapeVerbFlee": "huir", - "notDisabled": "¡El movimiento {{moveName}} de {{pokemonName}}\nya no está anulado!", - "turnEndHpRestore": "Los PS de {{pokemonName}} fueron restaurados.", + "notDisabled": "¡{{moveName}} de {{pokemonName}} ya no está anulado!", + "turnEndHpRestore": "{{pokemonName}} ha recuperado PS.", "hpIsFull": "¡Los PS de {{pokemonName}}\nestán al máximo!", "skipItemQuestion": "¿Estás seguro de que no quieres coger un objeto?", - "itemStackFull": "El máximo número de {{fullItemName}} ha sido alcanzado. Recibirás {{itemName}} en su lugar.", + "itemStackFull": "No tienes sitio para más {{fullItemName}}. Recibirás {{itemName}} en su lugar.", "eggHatching": "¿Y esto?", "ivScannerUseQuestion": "¿Quieres usar el Escáner de IVs en {{pokemonName}}?", "wildPokemonWithAffix": "El {{pokemonName}} salvaje", "foePokemonWithAffix": "El {{pokemonName}} enemigo", "useMove": "¡{{pokemonNameWithAffix}} usó {{moveName}}!", - "drainMessage": "¡{{pokemonName}} tuvo su\nenergía absorbida!", + "drainMessage": "¡La energía de {{pokemonName}}\nha sido absorbida!", "regainHealth": "¡{{pokemonName}} recuperó\nPS!", "stealEatBerry": "¡{{pokemonName}} robó la {{berryName}}\nde {{targetName}} y se la comió!", + "ppHealBerry": "¡{{pokemonNameWithAffix}} recuperó algunos PP de {{moveName}}\ngracias a su {{berryName}}!", + "hpHealBerry": "¡{{pokemonNameWithAffix}} recuperó sus PS gracias a su {{berryName}}!", "fainted": "¡{{pokemonNameWithAffix}} se debilitó!", "statsAnd": "y", "stats": "Las estadísticas", - "statRose_one": "¡El {{stats}} de {{pokemonNameWithAffix}} ha subido!", - "statRose_other": "¡{{stats}} de\n{{pokemonNameWithAffix}} han subido!", - "statSharplyRose_one": "¡El {{stats}} de {{pokemonNameWithAffix}} ha subido mucho!", - "statSharplyRose_other": "¡{{stats}} de\n{{pokemonNameWithAffix}} han subido mucho!", - "statRoseDrastically_one": "¡El {{stats}} de {{pokemonNameWithAffix}} ha subido muchísimo!", - "statRoseDrastically_other": "¡{{stats}} de\n{{pokemonNameWithAffix}} han subido muchísimo!", - "statWontGoAnyHigher_one": "¡El {{stats}} de {{pokemonNameWithAffix}} no puede subir más!", - "statWontGoAnyHigher_other": "¡{{stats}} de\n{{pokemonNameWithAffix}} no pueden subir más!", - "statFell_one": "¡El {{stats}} de {{pokemonNameWithAffix}} ha bajado!", - "statFell_other": "¡{{stats}} de\n{{pokemonNameWithAffix}} han bajado!", - "statHarshlyFell_one": "¡El {{stats}} de {{pokemonNameWithAffix}} ha bajado mucho!", - "statHarshlyFell_other": "¡{{stats}} de\n{{pokemonNameWithAffix}} han bajado mucho!", - "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!" + "statRose_one": "¡{{stats}} de {{pokemonNameWithAffix}} ha aumentado!", + "statRose_other": "¡{{stats}} de {{pokemonNameWithAffix}} ha aumentado!", + "statSharplyRose_one": "¡{{stats}} de {{pokemonNameWithAffix}} ha aumentado mucho!", + "statSharplyRose_other": "¡{{stats}} de {{pokemonNameWithAffix}} ha aumentado mucho!", + "statRoseDrastically_one": "¡{{stats}} de {{pokemonNameWithAffix}} ha aumentado muchísimo!", + "statRoseDrastically_other": "¡{{stats}} de {{pokemonNameWithAffix}} ha aumentado muchísimo!", + "statWontGoAnyHigher_one": "¡{{stats}} de {{pokemonNameWithAffix}} no puede aumentar más!", + "statWontGoAnyHigher_other": "¡{{stats}} de {{pokemonNameWithAffix}} no puede aumentar más!", + "statFell_one": "¡{{stats}} de {{pokemonNameWithAffix}} ha disminuido!", + "statFell_other": "¡{{stats}} de {{pokemonNameWithAffix}} ha disminuido!", + "statHarshlyFell_one": "¡{{stats}} de {{pokemonNameWithAffix}} ha disminuido mucho!", + "statHarshlyFell_other": "¡{{stats}} de {{pokemonNameWithAffix}} ha disminuido mucho!", + "statSeverelyFell_one": "¡{{stats}} de {{pokemonNameWithAffix}} ha disminuido muchísimo!", + "statSeverelyFell_other": "¡{{stats}} de {{pokemonNameWithAffix}} ha disminuido muchísimo!", + "statWontGoAnyLower_one": "¡{{stats}} de {{pokemonNameWithAffix}} no puede disminuir más!", + "statWontGoAnyLower_other": "¡{{stats}} de {{pokemonNameWithAffix}} no puede disminuir más!", + "transformedIntoType": "¡{{pokemonName}} ha cambiado a tipo {{type}}!", + "retryBattle": "¿Quieres reintentar este combate?", + "unlockedSomething": "¡Has desbloqueado {{unlockedThing}}!", + "congratulations": "¡Enhorabuena!", + "beatModeFirstTime": "¡{{speciesName}} ha completado el modo {{gameMode}} por primera vez!\n¡Has conseguido {{newModifier}}!", + "ppReduced": "¡El movimiento {{moveName}} de {{targetName}} ha perdido {{reduction}} PP!", + "mysteryEncounterAppeared": "¿Que es esto?", + "battlerTagsHealBlock": "¡{{pokemonNameWithAffix}} no puede restaurar sus PS!", + "battlerTagsHealBlockOnRemove": "¡{{pokemonNameWithAffix}} ya puede recuperar PS!" } diff --git a/src/locales/es/bgm-name.json b/src/locales/es/bgm-name.json index f0e0ab7e852..1def3575a6d 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!", @@ -145,5 +145,11 @@ "encounter_youngster": "BW - ¡Vs. chico joven!", "heal": "BW - Cura Pokémon", "menu": "PMD EoS - ¡Bienvenidos al mundo de los Pokémon!", - "title": "PMD EoS - Tema del menú principal" + "title": "PMD EoS - Tema del menú principal", + + "mystery_encounter_weird_dream": "PMM EdC Pináculo del Tiempo", + "mystery_encounter_fun_and_games": "PMM EdC Gran Bluff", + "mystery_encounter_gen_5_gts": "BN GTS", + "mystery_encounter_gen_6_gts": "XY GTS", + "mystery_encounter_delibirdy": "Firel - DeliDelivery!" } diff --git a/src/locales/es/challenges.json b/src/locales/es/challenges.json index 6a7db8c10c3..3760d95126c 100644 --- a/src/locales/es/challenges.json +++ b/src/locales/es/challenges.json @@ -1,5 +1,6 @@ { - "title": "Parámetros de Desafíos", + "title": "Parámetros de desafíos", + "illegalEvolution": "¡{{pokemon}} ha evolucionado! ¡No puede ser parte del desafío!", "singleGeneration": { "name": "Monogeneración", "desc": "Solo puedes usar Pokémon de {{gen}} generación.", @@ -14,16 +15,22 @@ "gen_8": "octava", "gen_9": "novena" }, + "freshStart": { + "name": "Nuevo Comienzo", + "desc": "Sólo puedes usar Pokémon iniciales originales, como si acabases de empezar PokéRogue.", + "value.0": "Desact.", + "value.1": "Activo" + }, "singleType": { "name": "Monotipo", "desc": "Solo puedes usar Pokémon with the {{type}} type.", "desc_default": "Solo puedes usar Pokémon del tipo elegido." }, "inverseBattle": { - "name": "Combate Inverso", + "name": "Lucha Inversa", "shortName": "Inverso", - "desc": "La efectividad de los tipos es invertida. No hay inmunidades entre tipos.\nEste reto deshabilita logros de otros retos.", - "value.0": "Desactivado", - "value.1": "Activado" + "desc": "Las relaciones entre tipos son invertidas y ningún tipo es inmune a otro.\nDesactiva el resto de desafíos.", + "value.0": "Desact.", + "value.1": "Activo" } -} \ No newline at end of file +} diff --git a/src/locales/es/common.json b/src/locales/es/common.json index 556ebb8454e..2b0af6c600c 100644 --- a/src/locales/es/common.json +++ b/src/locales/es/common.json @@ -1,7 +1,8 @@ { + "start": "Listo", "luckIndicator": "Suerte:", "shinyOnHover": "Shiny", "commonShiny": "Común", "rareShiny": "Raro", "epicShiny": "Épico" -} \ No newline at end of file +} diff --git a/src/locales/es/config.ts b/src/locales/es/config.ts index 8f75c08f3f6..28d9a2d9346 100644 --- a/src/locales/es/config.ts +++ b/src/locales/es/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 "./mystery-encounters/mysterious-chest-dialogue.json"; +import mysteriousChallengers from "./mystery-encounters/mysterious-challengers-dialogue.json"; +import darkDeal from "./mystery-encounters/dark-deal-dialogue.json"; +import departmentStoreSale from "./mystery-encounters/department-store-sale-dialogue.json"; +import fieldTrip from "./mystery-encounters/field-trip-dialogue.json"; +import fieryFallout from "./mystery-encounters/fiery-fallout-dialogue.json"; +import fightOrFlight from "./mystery-encounters/fight-or-flight-dialogue.json"; +import safariZone from "./mystery-encounters/safari-zone-dialogue.json"; +import shadyVitaminDealer from "./mystery-encounters/shady-vitamin-dealer-dialogue.json"; +import slumberingSnorlax from "./mystery-encounters/slumbering-snorlax-dialogue.json"; +import trainingSession from "./mystery-encounters/training-session-dialogue.json"; +import theStrongStuff from "./mystery-encounters/the-strong-stuff-dialogue.json"; +import pokemonSalesman from "./mystery-encounters/the-pokemon-salesman-dialogue.json"; +import offerYouCantRefuse from "./mystery-encounters/an-offer-you-cant-refuse-dialogue.json"; +import delibirdy from "./mystery-encounters/delibirdy-dialogue.json"; +import absoluteAvarice from "./mystery-encounters/absolute-avarice-dialogue.json"; +import aTrainersTest from "./mystery-encounters/a-trainers-test-dialogue.json"; +import trashToTreasure from "./mystery-encounters/trash-to-treasure-dialogue.json"; +import berriesAbound from "./mystery-encounters/berries-abound-dialogue.json"; +import clowningAround from "./mystery-encounters/clowning-around-dialogue.json"; +import partTimer from "./mystery-encounters/part-timer-dialogue.json"; +import dancingLessons from "./mystery-encounters/dancing-lessons-dialogue.json"; +import weirdDream from "./mystery-encounters/weird-dream-dialogue.json"; +import theWinstrateChallenge from "./mystery-encounters/the-winstrate-challenge-dialogue.json"; +import teleportingHijinks from "./mystery-encounters/teleporting-hijinks-dialogue.json"; +import bugTypeSuperfan from "./mystery-encounters/bug-type-superfan-dialogue.json"; +import funAndGames from "./mystery-encounters/fun-and-games-dialogue.json"; +import uncommonBreed from "./mystery-encounters/uncommon-breed-dialogue.json"; +import globalTradeSystem from "./mystery-encounters/global-trade-system-dialogue.json"; +import expertPokemonBreeder from "./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 esConfig = { ability, abilityTriggers, @@ -109,5 +151,41 @@ export const esConfig = { 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/es/dialogue.json b/src/locales/es/dialogue.json index cbd7dbf39ad..4e10b17dfd8 100644 --- a/src/locales/es/dialogue.json +++ b/src/locales/es/dialogue.json @@ -48,5 +48,115 @@ "defeat": { "1": "¡Todo lo que quiero es a esta preciosa criatura! ¡Los demás no me importáis!" } + }, + "stat_trainer_buck": { + "encounter": { + "1": "Para que luego no digas que no te he advertido: soy muy fuerte.", + "2": "¡Noto cómo tiemblan mis Pokémon dentro de sus Poké Balls!" + }, + "victory": { + "1": "¡Je, je, je!\n¡Eres una máquina!", + "2": "¡Je, je, je!\n¡Eres una máquina!" + }, + "defeat": { + "1": "¡Vaya! supongo que te quedaste sin fuerzas.", + "2": "¡Vaya! supongo que te quedaste sin fuerzas." + } + }, + "stat_trainer_cheryl": { + "encounter": { + "1": "Mis Pokémon han estado deseando una batalla.", + "2": "Debería advertirte de que mis Pokémon son un poco... hiperactivos." + }, + "victory": { + "1": "No es fácil encontrar el equilibrio entre ataque y defensa...", + "2": "No es fácil encontrar el equilibrio entre ataque y defensa..." + }, + "defeat": { + "1": "Necesitas curar a tus Pokémon?", + "2": "Necesitas curar a tus Pokémon?" + } + }, + "stat_trainer_marley": { + "encounter": { + "1": "... Vale.\n¡Voy a esforzarme al máximo!", + "2": "... Vale.\nNo... perderé... !" + }, + "victory": { + "1": "... ¡Eeeh!", + "2": "... ¡Eeeh!" + }, + "defeat": { + "1": "... Adiós.", + "2": "... Adiós." + } + }, + "stat_trainer_mira": { + "encounter": { + "1": "Serás sorprendido por Maiza!", + "2": "¡Maiza te mostrará que Mira ya no se pierde!" + }, + "victory": { + "1": "Maiza se pregunta si puede llegar muy lejos en esta tierra.", + "2": "Maiza se pregunta si puede llegar muy lejos en esta tierra." + }, + "defeat": { + "1": "¡Maiza sabía que ganaría!", + "2": "¡Maiza sabía que ganaría!" + } + }, + "stat_trainer_riley": { + "encounter": { + "1": "¡Combatir es nuestra manera de saludarnos!", + "2": "Vamos a hacer lo imposible por derrotar a tu equipo." + }, + "victory": { + "1": "A veces combatimos entre nosotros y otras veces unimos fuerzas.$Es maravilloso ser entrenador.", + "2": "A veces combatimos entre nosotros y otras veces unimos fuerzas.$Es maravilloso ser entrenador." + }, + "defeat": { + "1": "Vaya demostración pusiste.\nMejor suerte la próxima vez.", + "2": "Vaya demostración pusiste.\nMejor suerte la próxima vez." + } + }, + "winstrates_victor": { + "encounter": { + "1": "¡Qué entusiasmo! ¡Así me gusta!" + }, + "victory": { + "1": "¡Ayyy! ¡Eres más fuerte de lo que pensaba!" + } + }, + "winstrates_victoria": { + "encounter": { + "1": "Huy, huy, huy... ¡Qué joven eres!$Pero le has dado una buena tunda a mi marido...$No hay que fiarse...¡Lucha ahora contra mí!" + }, + "victory": { + "1": "¡Ay! ¡No puedo creer que seas tan fuerte!" + } + }, + "winstrates_vivi": { + "encounter": { + "1": "¿Eres más fuerte que mamá? ¡Halaaa!$¡Pero yo también soy fuerte!\n¡Ahora vas a ver!" + }, + "victory": { + "1": "Pero... ¿he perdido?\nSnif, snif... ¡Abuelitaaa!" + } + }, + "winstrates_vicky": { + "encounter": { + "1": "¿Cómo te atreves a hacer llorar a mi nieta? ¡Voy a echarle un buen rapapolvo a tu equipo Pokémon! ¡Vas a ver lo que es bueno!" + }, + "victory": { + "1": "¡Jarl! Eres fuerte...\nLos demás tenían razón..." + } + }, + "winstrates_vito": { + "encounter": { + "1": "He entrenado con toda mi familia,\ntoda enterita.$¡No voy a perder!" + }, + "victory": { + "1": "Logré superar a toda mi familia.\nNunca había perdido..." + } } } diff --git a/src/locales/es/egg.json b/src/locales/es/egg.json index 2542d577323..8ee29c582fd 100644 --- a/src/locales/es/egg.json +++ b/src/locales/es/egg.json @@ -11,6 +11,7 @@ "gachaTypeLegendary": "Mayor tasa de Legendario", "gachaTypeMove": "Mayor tasa de Movimiento Huevo Raro", "gachaTypeShiny": "Mayor tasa de Shiny", + "eventType": "Evento Misterioso", "selectMachine": "Seleccione una máquina.", "notEnoughVouchers": "¡No tienes suficientes vales!", "tooManyEggs": "¡No tienes suficiente espacio!", diff --git a/src/locales/es/game-stats-ui-handler.json b/src/locales/es/game-stats-ui-handler.json index 9e26dfeeb6e..d79a0dcd9cc 100644 --- a/src/locales/es/game-stats-ui-handler.json +++ b/src/locales/es/game-stats-ui-handler.json @@ -1 +1,42 @@ -{} \ No newline at end of file +{ + "stats": "Estadísticas", + "playTime": "Tiempo jugado", + "totalBattles": "Batallas totales", + "starters": "Iniciales", + "shinyStarters": "Shinies iniciales", + "speciesSeen": "Especies vistas", + "speciesCaught": "Especies capturadas", + "ribbonsOwned": "Cintas obtenidas", + "classicRuns": "Partidas clásicas", + "classicWins": "Victorias clásicas", + "dailyRunAttempts": "Intentos del reto diario", + "dailyRunWins": "Victorias del reto diario", + "endlessRuns": "Partidas Infinitas", + "highestWaveEndless": "Oleada récord (infinito)", + "highestMoney": "Dinero récord", + "highestDamage": "Daño máximo", + "highestHPHealed": "Máximos PS curados", + "pokemonEncountered": "Pokémon encontrados", + "pokemonDefeated": "Pokémon derrotados", + "pokemonCaught": "Pokémon capturados", + "eggsHatched": "Huevos eclosionados", + "subLegendsSeen": "Sublegendarios vistos", + "subLegendsCaught": "Sub-legs. capturados", + "subLegendsHatched": "Sub-legs. eclosionados", + "legendsSeen": "Legendarios vistos", + "legendsCaught": "Legendarios capturados", + "legendsHatched": "Legendarios eclosionados", + "mythicalsSeen": "Míticos vistos", + "mythicalsCaught": "Singulares capturados", + "mythicalsHatched": "Singulares eclosionados", + "shiniesSeen": "Shinies vistos", + "shiniesCaught": "Shinies capturados", + "shiniesHatched": "Shinies eclosionados", + "pokemonFused": "Pokémons fusionados", + "trainersDefeated": "Entrenadores derrotados", + "eggsPulled": "Huevos canjeados", + "rareEggsPulled": "Huevos raros canjeados", + "epicEggsPulled": "Huevos épicos canjeados", + "legendaryEggsPulled": "Huevos legendarios canjeados", + "manaphyEggsPulled": "Huevos Manaphy canjeados" +} diff --git a/src/locales/es/menu-ui-handler.json b/src/locales/es/menu-ui-handler.json index c906324cdbf..11bdfa7527b 100644 --- a/src/locales/es/menu-ui-handler.json +++ b/src/locales/es/menu-ui-handler.json @@ -3,26 +3,27 @@ "ACHIEVEMENTS": "Logros", "STATS": "Estadísticas", "RUN_HISTORY": "Historial de partida", - "EGG_LIST": "Lista de Huevos", - "EGG_GACHA": "Gacha de Huevos", - "MANAGE_DATA": "Gestionar Datos", + "EGG_LIST": "Lista de huevos", + "EGG_GACHA": "Lotería Oval", + "MANAGE_DATA": "Gestionar datos", "COMMUNITY": "Comunidad", - "SAVE_AND_QUIT": "Guardar y Salir", - "LOG_OUT": "Cerrar Sesión", + "SAVE_AND_QUIT": "Guardar y salir", + "LOG_OUT": "Cerrar sesión", "slot": "Ranura {{slotNumber}}", - "importSession": "Importar Sesión", + "importSession": "Importar sesión", "importSlotSelect": "Selecciona una ranura para importar.", - "exportSession": "Exportar Sesión", + "exportSession": "Exportar sesión", "exportSlotSelect": "Selecciona una ranura para exportar.", - "importRunHistory":"Importar Historial de partida", - "exportRunHistory":"Exportar Historial de partida", - "importData": "Importar Datos", - "exportData": "Exportar Datos", + "importRunHistory": "Importar historial de partida", + "exportRunHistory": "Exportar historial de partida", + "importData": "Importar datos", + "exportData": "Exportar datos", "consentPreferences": "Consentimiento de datos", "linkDiscord": "Conectar Discord", "unlinkDiscord": "Desconectar Discord", "linkGoogle": "Conectar Google", "unlinkGoogle": "Desconectar Google", "cancel": "Cancelar", - "losingProgressionWarning": "Perderás cualquier progreso desde el inicio de la batalla. ¿Continuar?" -} \ No newline at end of file + "losingProgressionWarning": "Perderás cualquier progreso desde el inicio de la batalla. ¿Continuar?", + "noEggs": "¡No hay huevos incubándose\nahora mismo!" +} 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/modifier-select-ui-handler.json b/src/locales/es/modifier-select-ui-handler.json index 7adcb885c9e..1ad07d2316b 100644 --- a/src/locales/es/modifier-select-ui-handler.json +++ b/src/locales/es/modifier-select-ui-handler.json @@ -8,5 +8,7 @@ "lockRaritiesDesc": "Bloquea las rarezas de los objetos al actualizar (afecta el costo de actualización).", "checkTeamDesc": "Revisa tu equipo o usa un objeto que cambia de forma.", "rerollCost": "{{formattedMoney}} ₽", - "itemCost": "{{formattedMoney}} ₽" -} \ No newline at end of file + "itemCost": "{{formattedMoney}} ₽", + "continueNextWaveButton": "Continuar", + "continueNextWaveDescription": "Continuar a la siguiente ronda" +} diff --git a/src/locales/es/modifier-type.json b/src/locales/es/modifier-type.json index 3ac4d85f793..472a28c77a0 100644 --- a/src/locales/es/modifier-type.json +++ b/src/locales/es/modifier-type.json @@ -68,6 +68,20 @@ "BaseStatBoosterModifierType": { "description": "Aumenta la est. {{stat}} base del portador en un 10%.\nCuanto mayores sean tus IVs, mayor será el límite de acumulación." }, + "PokemonBaseStatTotalModifierType": { + "name": "Jugo de Shuckle", + "description": "{{increaseDecrease}} todas las estadísticas base del portador en {{statValue}}. Fuiste {{blessCurse}} por el Shuckle.", + "extra": { + "increase": "Aumenta", + "decrease": "Disminuye", + "blessed": "bendecido", + "cursed": "maldecido" + } + }, + "PokemonBaseStatFlatModifierType": { + "name": "Tarta Vieja", + "description": "Aumenta las estadísticas base de {{stats}} del portador en {{statValue}}. Encontrado después de un sueño extraño." + }, "AllPokemonFullHpRestoreModifierType": { "description": "Restaura el 100% de los PS de todos los Pokémon." }, @@ -401,7 +415,13 @@ "ENEMY_FUSED_CHANCE": { "name": "Ficha fusión", "description": "Agrega un 1% de probabilidad de que un Pokémon salvaje sea una fusión." - } + }, + + "MYSTERY_ENCOUNTER_SHUCKLE_JUICE": { "name": "Jugo de Shuckle" }, + "MYSTERY_ENCOUNTER_BLACK_SLUDGE": { "name": "Lodo Negro", "description": "El hedor es tan poderoso que las tiendas solo te venderán artículos a un coste mucho más alto" }, + "MYSTERY_ENCOUNTER_MACHO_BRACE": { "name": "Brazal Firme", "description": "Derrotar a un Pokémon otorga al poseedor una pila de Brazal Firme. Cada pila aumenta ligeramente las estadísticas, con un bono extra al alcanzar el máximo de pilas." }, + "MYSTERY_ENCOUNTER_OLD_GATEAU": { "name": "Tarta Vieja", "description": "Aumenta las estadísticas de {{stats}} del portador en {{statValue}}." }, + "MYSTERY_ENCOUNTER_GOLDEN_BUG_NET": { "name": "Cazamariposas Dorado", "description": "Imbuye al dueño con suerte para encontrar Pokémon de tipo Bicho más a menudo. Tiene un peso extraño." } }, "SpeciesBoosterItem": { "LIGHT_BALL": { diff --git a/src/locales/es/move-trigger.json b/src/locales/es/move-trigger.json index 2322a49056f..b49a64ac42a 100644 --- a/src/locales/es/move-trigger.json +++ b/src/locales/es/move-trigger.json @@ -1,8 +1,37 @@ { + "hitWithRecoil": "¡{{pokemonName}} también\nse ha hecho daño!", + "cutHpPowerUpMove": "¡{{pokemonName}} sacrifica sus PS para mejorar su movimiento!", + "absorbedElectricity": "¡{{pokemonName}} está acumulando electricidad!", + "switchedStatChanges": "¡{{pokemonName}} intercambió los cambios de características con el objetivo!", "switchedTwoStatChanges": "{{pokemonName}} ha intercambiado los cambios en {{firstStat}} y {{secondStat}} con los del objetivo!", "switchedStat": "{{pokemonName}} cambia su {{stat}} por la de su objetivo!", "sharedGuard": "{{pokemonName}} suma su capacidad defensiva a la del objetivo y la reparte equitativamente!", "sharedPower": "{{pokemonName}} suma su capacidad ofensiva a la del objetivo y la reparte equitativamente!", + "goingAllOutForAttack": "¡{{pokemonName}} lo ha dado todo!", + "regainedHealth": "¡{{pokemonName}} ha recuperado PS!", + "keptGoingAndCrashed": "¡{{pokemonName}} ha fallado y se ha caído al suelo!", + "fled": "¡{{pokemonName}} ha huido!", + "cannotBeSwitchedOut": "¡{{pokemonName}} no puede dejar el combate!", + "swappedAbilitiesWithTarget": "¡{{pokemonName}} ha intercambiado su habilidad con la de su objetivo!", + "coinsScatteredEverywhere": "¡Hay monedas por todas partes!", + "attackedByItem": "¡{{pokemonName}} sufre daño por su {{itemName}}!", + "whippedUpAWhirlwind": "¡{{pokemonName}} se prepara para lanzar una borrasca!", + "flewUpHigh": "¡{{pokemonName}} voló alto!", + "tookInSunlight": "¡{{pokemonName}} ha absorbido luz solar!", + "dugAHole": "¡{{pokemonName}} se ha ocultado bajo tierra!", + "loweredItsHead": "¡{{pokemonName}} ha agachado la cabeza!", + "isGlowing": "¡Un intenso halo rodea a {{pokemonName}}!", + "bellChimed": "Ha repicado una campana.", + "foresawAnAttack": "¡{{pokemonName}} ha previsto el ataque!", + "isTighteningFocus": "¡{{pokemonName}} está reforzando su concentración!", + "hidUnderwater": "¡{{pokemonName}} se ha ocultado bajo el agua!", + "soothingAromaWaftedThroughArea": "Un aroma balsámico flota en el aire...", + "sprangUp": "¡{{pokemonName}} ha saltado muy alto!", + "choseDoomDesireAsDestiny": "¡{{pokemonName}} ha elegido Deseo Oculto para el futuro!", + "vanishedInstantly": "¡{{pokemonName}} ha desaparecido en un abrir y cerrar de ojos!", + "tookTargetIntoSky": "¡{{pokemonName}} se ha llevado a {{targetName}} por los aires!", + "becameCloakedInFreezingLight": "¡Una luz fría envuelve a {{pokemonName}}!", + "becameCloakedInFreezingAir": "¡Una ráfaga gélida envuelve a {{pokemonName}}!", "isChargingPower": "¡{{pokemonName}} está acumulando energía!", "burnedItselfOut": "¡El fuego interior de {{pokemonName}} se ha extinguido!", "startedHeatingUpBeak": "¡{{pokemonName}} empieza\na calentar su pico!", @@ -10,8 +39,33 @@ "isOverflowingWithSpacePower": "¡{{pokemonName}} rebosa\nenergía cósmica!", "usedUpAllElectricity": "¡{{pokemonName}} ha descargado toda su electricidad!", "stoleItem": "¡{{pokemonName}} robó el objeto\n{{itemName}} de {{targetName}}!", + "incineratedItem": "¡{{pokemonName}} ha incinerado la {{itemName}} de {{targetName}}!", + "knockedOffItem": "¡{{itemName}} de {{targetName}} ha caído al suelo!", + "tookMoveAttack": "¡{{pokemonName}} ha sido alcanzado por {{moveName}}!", + "cutOwnHpAndMaximizedStat": "¡{{pokemonName}} ha sacrificado algunos PS y ha aumentado su {{statName}} al máximo!", + "copiedStatChanges": "¡{{pokemonName}} ha copiado los cambios de características de\n{{targetName}}!", + "magnitudeMessage": "Magnitud: {{magnitude}}!", + "tookAimAtTarget": "¡{{pokemonName}} tiene a {{targetName}} en su punto de mira!", + "transformedIntoType": "¡{{pokemonName}} ha cambiado a tipo {{typeName}}!", + "copiedMove": "¡{{pokemonName}} ha copiado {{moveName}}!", + "sketchedMove": "¡{{pokemonName}} ha usado Esquema para copiar {{moveName}}!", + "acquiredAbility": "¡La habilidad de {{pokemonName}} ha cambiado a {{abilityName}}!", + "copiedTargetAbility": "¡{{pokemonName}} ha copiado la habilidad {{abilityName}} de {{targetName}}!", + "transformedIntoTarget": "¡{{pokemonName}} se ha transformado en {{targetName}}!", + "tryingToTakeFoeDown": "¡{{pokemonName}} intenta que el rival sufra su mismo destino!", + "addType": "¡{{pokemonName}} ahora también es de tipo {{typeName}}!", + "cannotUseMove": "¡{{pokemonName}} no puede usar {{moveName}}!", + "healHp": "¡{{pokemonName}} recuperó sus PS!", + "sacrificialFullRestore": "¡El deseo de curación se ha hecho realidad para {{pokemonName}}!", + "invertStats": "¡Se han invertido los cambios de características de {{pokemonName}}!", + "resetStats": "¡Se han anulado todos los cambios de características de\t{{pokemonName}}!", "statEliminated": "¡Los cambios en estadísticas fueron eliminados!", + "faintCountdown": "{{pokemonName}}\nse debilitará dentro de {{turnCount}} turnos.", + "copyType": "¡{{pokemonName}} ahora es del mismo tipo que\n{{targetPokemonName}}!", + "suppressAbilities": "¡Se ha anulado la habilidad de {{pokemonName}}!", "revivalBlessing": "¡{{pokemonName}} ha revivido!", + "swapArenaTags": "¡{{pokemonName}} ha intercambiado los efectos del terreno de combate!", + "exposedMove": "¡{{pokemonName}} ha identificado\n{{targetPokemonName}}!", "safeguard": "¡{{targetName}} está protegido por Velo Sagrado!", "afterYou": "¡{{pokemonName}} ha decidido aprovechar la oportunidad!" } diff --git a/src/locales/es/move.json b/src/locales/es/move.json index f4c28dd02e7..401ca5588aa 100644 --- a/src/locales/es/move.json +++ b/src/locales/es/move.json @@ -9,7 +9,7 @@ }, "doubleSlap": { "name": "Doble Bofetón", - "effect": "Abofetea de dos a cinco veces seguidas." + "effect": "El objetivo recibe bofetadas repetidamente, de ida y vuelta, de dos a cinco veces seguidas." }, "cometPunch": { "name": "Puño Cometa", @@ -53,7 +53,7 @@ }, "swordsDance": { "name": "Danza Espada", - "effect": "Baile frenético que aumenta mucho el Ataque." + "effect": "Baile frenético que aumenta mucho el ataque." }, "cut": { "name": "Corte", @@ -77,7 +77,7 @@ }, "bind": { "name": "Atadura", - "effect": "Ata y oprime de cuatro a cinco turnos." + "effect": "Usa el cuerpo o las extremidades para atar y oprimir\nal objetivo e infligirle daño de cuatro a cinco turnos." }, "slam": { "name": "Atizar", @@ -109,7 +109,7 @@ }, "sandAttack": { "name": "Ataque Arena", - "effect": "Arroja arena a la cara y baja la Precisión." + "effect": "Arroja arena a la cara y baja la precisión." }, "headbutt": { "name": "Golpe Cabeza", @@ -153,14 +153,14 @@ }, "tailWhip": { "name": "Látigo", - "effect": "Agita la cola para bajar la Defensa del equipo rival." + "effect": "Agita la cola para bajar la defensa del equipo rival." }, "poisonSting": { "name": "Picotazo Veneno", "effect": "Lanza un aguijón tóxico que puede envenenar al objetivo." }, "twineedle": { - "name": "Doble Ataque", + "name": "Doble ataque", "effect": "Pincha dos veces con dos espinas. Puede envenenar." }, "pinMissile": { @@ -169,7 +169,7 @@ }, "leer": { "name": "Malicioso", - "effect": "Intimida a los rivales para bajar su Defensa." + "effect": "Intimida a los rivales para bajar su defensa." }, "bite": { "name": "Mordisco", @@ -177,11 +177,11 @@ }, "growl": { "name": "Gruñido", - "effect": "Dulce gruñido que distrae al objetivo para que baje la guardia y reduce su Ataque." + "effect": "Dulce gruñido que distrae al objetivo para que baje la guardia y reduce su ataque." }, "roar": { "name": "Rugido", - "effect": "Se lleva al objetivo, que es cambiado por otro Pokémon. Si es un Pokémon salvaje, acaba el combate." + "effect": "El objetivo se asusta y otro Pokémon es sacado a la batalla. En batalla contra un solo pokémon salvaje, esto termina la batalla." }, "sing": { "name": "Canto", @@ -201,7 +201,7 @@ }, "acid": { "name": "Ácido", - "effect": "Rocía al objetivo con un ácido corrosivo. Puede reducir la Defensa Especial." + "effect": "Rocía al objetivo con un ácido corrosivo. Puede reducir la defensa especial." }, "ember": { "name": "Ascuas", @@ -241,11 +241,11 @@ }, "bubbleBeam": { "name": "Rayo Burbuja", - "effect": "Ráfaga de burbujas que puede reducir la Velocidad." + "effect": "Ráfaga de burbujas que puede reducir la velocidad." }, "auroraBeam": { "name": "Rayo Aurora", - "effect": "Rayo multicolor que puede reducir el Ataque." + "effect": "Rayo multicolor que puede reducir el ataque." }, "hyperBeam": { "name": "Hiperrayo", @@ -293,7 +293,7 @@ }, "growth": { "name": "Desarrollo", - "effect": "Hace que su cuerpo crezca a marchas forzadas con lo que aumenta su Ataque y Ataque Especial." + "effect": "Hace que su cuerpo crezca a marchas forzadas con lo que aumenta su ataque y ataque especial." }, "razorLeaf": { "name": "Hoja Afilada", @@ -321,7 +321,7 @@ }, "stringShot": { "name": "Disparo Demora", - "effect": "Lanza seda a los rivales y reduce mucho su Velocidad." + "effect": "Lanza seda a los rivales y reduce mucho su velocidad." }, "dragonRage": { "name": "Furia Dragón", @@ -373,7 +373,7 @@ }, "psychic": { "name": "Psíquico", - "effect": "Fuerte ataque telequinético que puede bajar la Defensa Especial del objetivo." + "effect": "Fuerte ataque telequinético que puede bajar la defensa especial del objetivo." }, "hypnosis": { "name": "Hipnosis", @@ -381,11 +381,11 @@ }, "meditate": { "name": "Meditación", - "effect": "El usuario reposa y medita para potenciar el Ataque." + "effect": "El usuario reposa y medita para potenciar el ataque." }, "agility": { "name": "Agilidad", - "effect": "Relaja el cuerpo para ganar mucha Velocidad." + "effect": "Relaja el cuerpo para ganar mucha velocidad." }, "quickAttack": { "name": "Ataque Rápido", @@ -393,7 +393,7 @@ }, "rage": { "name": "Furia", - "effect": "Al usarse, aumenta el Ataque del usuario cada vez que es golpeado." + "effect": "Al usarse, aumenta el ataque del usuario cada vez que es golpeado." }, "teleport": { "name": "Teletransporte", @@ -409,11 +409,11 @@ }, "screech": { "name": "Chirrido", - "effect": "Alarido agudo que reduce mucho la Defensa del objetivo." + "effect": "Alarido agudo que reduce mucho la defensa del objetivo." }, "doubleTeam": { "name": "Doble Equipo", - "effect": "Crea copias de sí mismo para mejorar la Evasión." + "effect": "Crea copias de sí mismo para mejorar la evasión." }, "recover": { "name": "Recuperación", @@ -421,15 +421,15 @@ }, "harden": { "name": "Fortaleza", - "effect": "Tensa la musculatura del usuario para aumentar la Defensa." + "effect": "Tensa la musculatura del usuario para aumentar la defensa." }, "minimize": { "name": "Reducción", - "effect": "El usuario mengua para aumentar mucho la Evasión." + "effect": "El usuario mengua para aumentar mucho la evasión." }, "smokescreen": { "name": "Pantalla de Humo", - "effect": "Reduce la Precisión del objetivo con una nube de humo o tinta." + "effect": "Reduce la precisión del objetivo con una nube de humo o tinta." }, "confuseRay": { "name": "Rayo Confuso", @@ -437,15 +437,15 @@ }, "withdraw": { "name": "Refugio", - "effect": "El usuario se resguarda en su coraza, por lo que le sube la Defensa." + "effect": "El usuario se resguarda en su coraza, por lo que le sube la defensa." }, "defenseCurl": { - "name": "Rizo Defensa", - "effect": "Se enrosca para ocultar sus puntos débiles y aumentar la Defensa." + "name": "Rizo defensa", + "effect": "Se enrosca para ocultar sus puntos débiles y aumentar la defensa." }, "barrier": { "name": "Barrera", - "effect": "Crea una barrera que aumenta mucho la Defensa." + "effect": "Crea una barrera que aumenta mucho la defensa." }, "lightScreen": { "name": "Pantalla de Luz", @@ -477,7 +477,7 @@ }, "selfDestruct": { "name": "Autodestrucción", - "effect": "El atacante explota y hiere a los Pokémon adyacentes. El usuario se debilita de inmediato." + "effect": "El usuario explota y hiere a los Pokémon adyacentes. El usuario se debilita de inmediato." }, "eggBomb": { "name": "Bomba Huevo", @@ -517,7 +517,7 @@ }, "skullBash": { "name": "Cabezazo", - "effect": "El usuario se prepara y sube su Defensa en el primer turno y en el segundo arremete con un cabezazo." + "effect": "El usuario se prepara y sube su defensa en el primer turno y en el segundo arremete con un cabezazo." }, "spikeCannon": { "name": "Clavo Cañón", @@ -525,15 +525,15 @@ }, "constrict": { "name": "Restricción", - "effect": "Ataca con largos tentáculos o zarcillos que pueden bajar la Velocidad." + "effect": "Ataca con largos tentáculos o zarcillos que pueden bajar la velocidad." }, "amnesia": { "name": "Amnesia", - "effect": "El usuario olvida sus preocupaciones y aumenta mucho la Defensa Especial." + "effect": "El usuario olvida sus preocupaciones y aumenta mucho la defensa especial." }, "kinesis": { "name": "Kinético", - "effect": "Dobla una cuchara para distraer al objetivo y reducir su Precisión." + "effect": "Dobla una cuchara para distraer al objetivo y reducir su precisión." }, "softBoiled": { "name": "Ovocuración", @@ -577,7 +577,7 @@ }, "bubble": { "name": "Burbuja", - "effect": "Lanza burbujas a los contrincantes y puede reducir su Velocidad." + "effect": "Lanza burbujas a los contrincantes y puede reducir su velocidad." }, "dizzyPunch": { "name": "Puño Mareo", @@ -589,7 +589,7 @@ }, "flash": { "name": "Destello", - "effect": "Luz cegadora que baja la Precisión del objetivo." + "effect": "Luz cegadora que baja la precisión del objetivo." }, "psywave": { "name": "Psicoonda", @@ -601,7 +601,7 @@ }, "acidArmor": { "name": "Armadura Ácida", - "effect": "Transforma la estructura celular para hacerse líquido y aumenta mucho la Defensa." + "effect": "Transforma la estructura celular para hacerse líquido y aumenta mucho la defensa." }, "crabhammer": { "name": "Martillazo", @@ -609,7 +609,7 @@ }, "explosion": { "name": "Explosión", - "effect": "El atacante causa una grandísima explosión y hiere a los Pokémon adyacentes. El usuario se debilita de inmediato." + "effect": "El usuario causa una grandísima explosión y hiere a los Pokémon adyacentes. El usuario se debilita de inmediato." }, "furySwipes": { "name": "Golpes Furia", @@ -633,7 +633,7 @@ }, "sharpen": { "name": "Afilar", - "effect": "El perfil del usuario se hace más afilado y su Ataque mejora." + "effect": "El perfil del usuario se hace más afilado y su ataque mejora." }, "conversion": { "name": "Conversión", @@ -657,7 +657,7 @@ }, "struggle": { "name": "Forcejeo", - "effect": "Solo se usa como último recurso al acabarse los PP. Hiere un poco al agresor." + "effect": "Solo se usa como último recurso al acabarse los PP. Hiere un poco al usuario." }, "sketch": { "name": "Esquema", @@ -697,7 +697,7 @@ }, "flail": { "name": "Azote", - "effect": "Ataque frenético. Cuantos menos PS tenga el usuario, más daño producirá." + "effect": "Ataca de forma frenética. Cuantos menos PS tenga el usuario, más daño producirá." }, "conversion2": { "name": "Conversión2", @@ -709,7 +709,7 @@ }, "cottonSpore": { "name": "Esporagodón", - "effect": "Adhiere esporas a los rivales para reducir mucho su Velocidad." + "effect": "Adhiere esporas a los rivales para reducir mucho su velocidad." }, "reversal": { "name": "Inversión", @@ -733,7 +733,7 @@ }, "scaryFace": { "name": "Cara Susto", - "effect": "Asusta al objetivo para reducir mucho su Velocidad." + "effect": "Asusta al objetivo para reducir mucho su velocidad." }, "feintAttack": { "name": "Finta", @@ -745,7 +745,7 @@ }, "bellyDrum": { "name": "Tambor", - "effect": "Reduce la mitad de los PS máximos para mejorar al máximo el Ataque." + "effect": "Reduce la mitad de los PS máximos para mejorar al máximo el ataque." }, "sludgeBomb": { "name": "Bomba Lodo", @@ -753,11 +753,11 @@ }, "mudSlap": { "name": "Bofetón Lodo", - "effect": "Echa lodo en la cara del objetivo para infligirle daño y reducir su Precisión." + "effect": "Echa lodo en la cara del objetivo para infligirle daño y reducir su precisión." }, "octazooka": { "name": "Pulpocañón", - "effect": "Dispara tinta a la cara. Puede bajar la Precisión." + "effect": "Dispara tinta a la cara. Puede bajar la precisión." }, "spikes": { "name": "Púas", @@ -781,7 +781,7 @@ }, "icyWind": { "name": "Viento Hielo", - "effect": "Ataque con aire helado que reduce la Velocidad del objetivo." + "effect": "Ataque con aire helado que reduce la velocidad del objetivo." }, "detect": { "name": "Detección", @@ -801,7 +801,7 @@ }, "sandstorm": { "name": "Tormenta Arena", - "effect": "Tormenta de arena que dura cinco turnos y hiere a todos, excepto a los de tipo Roca, Tierra y Acero, y aumenta la Defensa Especial de los de tipo Roca." + "effect": "Tormenta de arena que dura cinco turnos y hiere a todos, excepto a los de tipo Roca, Tierra y Acero, y aumenta la defensa especial de los de tipo Roca." }, "gigaDrain": { "name": "Gigadrenado", @@ -813,7 +813,7 @@ }, "charm": { "name": "Encanto", - "effect": "Engatusa al objetivo y reduce mucho su Ataque." + "effect": "Engatusa al objetivo y reduce mucho su ataque." }, "rollout": { "name": "Rodar", @@ -825,7 +825,7 @@ }, "swagger": { "name": "Contoneo", - "effect": "Provoca confusión en el objetivo, pero también sube mucho su Ataque." + "effect": "Provoca confusión en el objetivo, pero también sube mucho su ataque." }, "milkDrink": { "name": "Batido", @@ -841,7 +841,7 @@ }, "steelWing": { "name": "Ala de Acero", - "effect": "Alas macizas que golpean al objetivo y pueden subir la Defensa del usuario." + "effect": "Alas macizas que golpean al objetivo y pueden subir la defensa del usuario." }, "meanLook": { "name": "Mal de Ojo", @@ -913,19 +913,19 @@ }, "rapidSpin": { "name": "Giro Rápido", - "effect": "Ataque giratorio que puede eliminar movimientos como Atadura, Constricción y Drenadoras. También aumenta la Velocidad del usuario." + "effect": "Ataque giratorio que puede eliminar movimientos como Atadura, Constricción y Drenadoras. También aumenta la velocidad del usuario." }, "sweetScent": { "name": "Dulce Aroma", - "effect": "Un dulce aroma engatusa al objetivo, por lo que se reduce mucho su Evasión." + "effect": "Un dulce aroma engatusa al objetivo, por lo que se reduce mucho su evasión." }, "ironTail": { "name": "Cola Férrea", - "effect": "Ataca con una cola férrea y puede reducir la Defensa del objetivo." + "effect": "Ataca con una cola férrea y puede reducir la defensa del objetivo." }, "metalClaw": { "name": "Garra Metal", - "effect": "Ataque con garras de acero que puede aumentar el Ataque del usuario." + "effect": "Ataque con garras de acero que puede aumentar el ataque del usuario." }, "vitalThrow": { "name": "Llave Vital", @@ -965,7 +965,7 @@ }, "crunch": { "name": "Triturar", - "effect": "Tritura con afilados colmillos y puede reducir la Defensa del objetivo." + "effect": "Tritura con afilados colmillos y puede reducir la defensa del objetivo." }, "mirrorCoat": { "name": "Manto Espejo", @@ -985,7 +985,7 @@ }, "shadowBall": { "name": "Bola Sombra", - "effect": "Lanza una bola oscura que puede bajar la Defensa Especial del objetivo." + "effect": "Lanza una bola oscura que puede bajar la defensa especial del objetivo." }, "futureSight": { "name": "Premonición", @@ -993,11 +993,11 @@ }, "rockSmash": { "name": "Golpe Roca", - "effect": "Propina un gran puñetazo que puede reducir la Defensa del objetivo." + "effect": "Propina un gran puñetazo que puede reducir la defensa del objetivo." }, "whirlpool": { "name": "Torbellino", - "effect": "Una tromba de agua atrapa al objetivo durante cuatro o cinco turnos." + "effect": "El usuario atrapa al objetivo en un violento remolino durante cuatro o cinco turnos." }, "beatUp": { "name": "Paliza", @@ -1013,7 +1013,7 @@ }, "stockpile": { "name": "Reserva", - "effect": "Acumula energía y sube la Defensa y la Defensa Especial. Puede utilizarse hasta tres veces." + "effect": "Acumula energía y sube la defensa y la defensa especial. Puede utilizarse hasta tres veces." }, "spitUp": { "name": "Escupir", @@ -1037,7 +1037,7 @@ }, "flatter": { "name": "Camelo", - "effect": "Halaga al objetivo y lo confunde, pero también sube su Ataque Especial." + "effect": "Halaga al objetivo y lo confunde, pero también sube su ataque especial." }, "willOWisp": { "name": "Fuego Fatuo", @@ -1045,7 +1045,7 @@ }, "memento": { "name": "Legado", - "effect": "El usuario se debilita, pero baja mucho tanto el Ataque como el Ataque Especial del objetivo." + "effect": "El usuario se debilita, pero baja mucho tanto el ataque como el ataque especial del objetivo." }, "facade": { "name": "Imagen", @@ -1069,7 +1069,7 @@ }, "charge": { "name": "Carga", - "effect": "Recarga energía para potenciar el siguiente movimiento de tipo Eléctrico. También sube la Defensa Especial." + "effect": "Recarga energía para potenciar el siguiente movimiento de tipo Eléctrico. También sube la defensa especial." }, "taunt": { "name": "Mofa", @@ -1101,7 +1101,7 @@ }, "superpower": { "name": "Fuerza Bruta", - "effect": "Ataque de gran potencia, pero que reduce el Ataque y la Defensa del agresor." + "effect": "Ataque de gran potencia, pero que reduce el ataque y la defensa del agresor." }, "magicCoat": { "name": "Capa Mágica", @@ -1133,7 +1133,7 @@ }, "eruption": { "name": "Estallido", - "effect": "Furia explosiva. Cuanto menor sea el número de PS del usuario, menos potencia tendrá el movimiento." + "effect": "El usuario ataca al Pokémon rival con una furia explosiva. Cuanto menor sea el HP del usuario, menor será la potencia del movimiento." }, "skillSwap": { "name": "Intercambio", @@ -1173,19 +1173,19 @@ }, "tailGlow": { "name": "Luminicola", - "effect": "Se concentra en una ráfaga de luz que sube muchísimo el Ataque Especial." + "effect": "Se concentra en una ráfaga de luz que sube muchísimo el ataque especial." }, "lusterPurge": { "name": "Resplandor", - "effect": "Fogonazo de luz que inflige daño al objetivo y puede reducir su Defensa Especial." + "effect": "Fogonazo de luz que inflige daño al objetivo y puede reducir su defensa especial." }, "mistBall": { "name": "Bola Neblina", - "effect": "Bola de plumas neblinosas que inflige daño al objetivo y puede reducir su Ataque Especial." + "effect": "Bola de plumas neblinosas que inflige daño al objetivo y puede reducir su ataque especial." }, "featherDance": { "name": "Danza Pluma", - "effect": "Envuelve al objetivo con un manto de plumas para reducir mucho su Ataque." + "effect": "Envuelve al objetivo con un manto de plumas para reducir mucho su ataque." }, "teeterDance": { "name": "Danza Caos", @@ -1201,7 +1201,7 @@ }, "iceBall": { "name": "Bola Hielo", - "effect": "El atacante rueda contra el objetivo durante cinco turnos, cada vez con mayor fuerza." + "effect": "El usuario rueda contra el objetivo durante cinco turnos, cada vez con mayor fuerza." }, "needleArm": { "name": "Brazo Pincho", @@ -1221,7 +1221,7 @@ }, "crushClaw": { "name": "Garra Brutal", - "effect": "Hace trizas al objetivo con garras afiladas y puede reducir su Defensa." + "effect": "Hace trizas al objetivo con garras afiladas y puede reducir su defensa." }, "blastBurn": { "name": "Anillo Ígneo", @@ -1233,7 +1233,7 @@ }, "meteorMash": { "name": "Puño Meteoro", - "effect": "Puñetazo que impacta como un meteorito y puede subir el Ataque del agresor." + "effect": "Puñetazo que impacta como un meteorito y puede subir el ataque del agresor." }, "astonish": { "name": "Impresionar", @@ -1249,15 +1249,15 @@ }, "fakeTears": { "name": "Llanto Falso", - "effect": "Lágrimas de cocodrilo que bajan mucho la Defensa Especial del objetivo." + "effect": "Lágrimas de cocodrilo que bajan mucho la defensa especial del objetivo." }, "airCutter": { "name": "Aire Afilado", - "effect": "Viento cortante que azota. Suele ser un golpe crítico." + "effect": "Viento cortante que azota a los Pokémon oponentes. Suele ser un golpe crítico." }, "overheat": { "name": "Sofoco", - "effect": "Ataque en toda regla que baja mucho el Ataque Especial de quien lo usa." + "effect": "Ataque en toda regla que baja mucho el ataque especial de quien lo usa." }, "odorSleuth": { "name": "Rastreo", @@ -1265,15 +1265,15 @@ }, "rockTomb": { "name": "Tumba Rocas", - "effect": "Tira rocas que detienen al objetivo y bajan su Velocidad." + "effect": "Tira rocas que detienen al objetivo y bajan su velocidad." }, "silverWind": { "name": "Viento Plata", - "effect": "Fuerte viento con polvo de escamas. Puede subir todas las características de quien lo usa." + "effect": "El objetivo es atacado con escamas en polvo que arrastra el viento. Esto también puede aumentar todas las estadísticas del usuario." }, "metalSound": { "name": "Eco Metálico", - "effect": "Horrible chirrido metálico que reduce mucho la Defensa Especial del objetivo." + "effect": "Horrible chirrido metálico que reduce mucho la defensa especial del objetivo." }, "grassWhistle": { "name": "Silbato", @@ -1281,11 +1281,11 @@ }, "tickle": { "name": "Cosquillas", - "effect": "Hace reír al objetivo para bajar su Ataque y Defensa." + "effect": "El usuario hace cosquillas al objetivo para que se ría, lo que reduce sus estadísticas de ataque y defensa." }, "cosmicPower": { "name": "Masa Cósmica", - "effect": "Sube la Defensa y la Defensa Especial propias con energía mística." + "effect": "Sube la defensa y la defensa especial propias con energía mística." }, "waterSpout": { "name": "Salpicar", @@ -1317,7 +1317,7 @@ }, "muddyWater": { "name": "Agua Lodosa", - "effect": "Ataque con agua lodosa que puede reducir la Precisión del objetivo." + "effect": "Ataque con agua lodosa que puede reducir la precisión del objetivo." }, "bulletSeed": { "name": "Semilladora", @@ -1333,7 +1333,7 @@ }, "ironDefense": { "name": "Defensa Férrea", - "effect": "Fortalece el cuerpo como si fuera de hierro y sube mucho la Defensa." + "effect": "Fortalece el cuerpo como si fuera de hierro y sube mucho la defensa." }, "block": { "name": "Bloqueo", @@ -1341,7 +1341,7 @@ }, "howl": { "name": "Aullido", - "effect": "Aullido que sube el ánimo y aumenta el Ataque del equipo." + "effect": "Aullido que sube el ánimo y aumenta el ataque del equipo." }, "dragonClaw": { "name": "Garra Dragón", @@ -1353,7 +1353,7 @@ }, "bulkUp": { "name": "Corpulencia", - "effect": "Robustece el cuerpo para subir el Ataque y la Defensa." + "effect": "Robustece el cuerpo para subir el ataque y la defensa." }, "bounce": { "name": "Bote", @@ -1361,7 +1361,7 @@ }, "mudShot": { "name": "Disparo Lodo", - "effect": "El usuario ataca lanzando una bola de lodo al objetivo que también reduce su Velocidad." + "effect": "El usuario ataca lanzando una bola de lodo al objetivo que también reduce su velocidad." }, "poisonTail": { "name": "Cola Veneno", @@ -1369,11 +1369,11 @@ }, "covet": { "name": "Antojo", - "effect": "Se acerca con ternura al objetivo y tiene un 30% de posibilidades de robar el objeto que lleve." + "effect": "Se acerca con ternura al objetivo y tiene un 30% de posibilidades de robar un objeto que lleve." }, "voltTackle": { "name": "Placaje Eléc", - "effect": "Quien lo usa electrifica su cuerpo para luego atacar. Se hiere mucho a sí mismo, pero puede paralizar al objetivo." + "effect": "El usuario electrifica su cuerpo para luego atacar. Se hiere mucho a sí mismo, pero puede paralizar al objetivo." }, "magicalLeaf": { "name": "Hoja Mágica", @@ -1385,7 +1385,7 @@ }, "calmMind": { "name": "Paz Mental", - "effect": "Aumenta la concentración y calma el espíritu para subir el Ataque Especial y la Defensa Especial." + "effect": "Aumenta la concentración y calma el espíritu para subir el ataque especial y la defensa especial." }, "leafBlade": { "name": "Hoja Aguda", @@ -1393,7 +1393,7 @@ }, "dragonDance": { "name": "Danza Dragón", - "effect": "Danza mística que sube el Ataque y la Velocidad." + "effect": "Danza mística que sube el ataque y la velocidad." }, "rockBlast": { "name": "Pedrada", @@ -1413,7 +1413,7 @@ }, "psychoBoost": { "name": "Psicoataque", - "effect": "Ataque en toda regla que baja mucho el Ataque Especial de quien lo usa." + "effect": "Ataque en toda regla que baja mucho el ataque especial de quien lo usa." }, "roost": { "name": "Respiro", @@ -1433,7 +1433,7 @@ }, "hammerArm": { "name": "Machada", - "effect": "Un terrible puño golpea al contrincante, pero la Velocidad del usuario se ve reducida." + "effect": "El usuario golpea con un puño pesado al contrincante, pero baja la velocidad del usuario." }, "gyroBall": { "name": "Giro Bola", @@ -1461,7 +1461,7 @@ }, "tailwind": { "name": "Viento Afín", - "effect": "Crea un fuerte remolino que aumenta la Velocidad de los Pokémon de tu equipo durante cuatro turnos." + "effect": "Crea un fuerte remolino que aumenta la velocidad de los Pokémon de tu equipo durante cuatro turnos." }, "acupressure": { "name": "Acupresión", @@ -1477,7 +1477,7 @@ }, "closeCombat": { "name": "A Bocajarro", - "effect": "Lucha abiertamente contra el objetivo sin protegerse. También reduce la Defensa y la Defensa Especial del usuario." + "effect": "Lucha abiertamente contra el objetivo sin protegerse. También reduce la defensa y la defensa especial del usuario." }, "payback": { "name": "Vendetta", @@ -1489,7 +1489,7 @@ }, "embargo": { "name": "Embargo", - "effect": "Impide al objetivo usar el objeto que lleva durante cinco turnos. Su Entrenador tampoco puede usar objetos con él." + "effect": "Impide al objetivo usar objetos que lleva durante cinco turnos." }, "fling": { "name": "Lanzamiento", @@ -1513,7 +1513,7 @@ }, "powerTrick": { "name": "Truco Fuerza", - "effect": "El usuario emplea su poder mental para intercambiar su Ataque y su Defensa." + "effect": "El usuario emplea su poder mental para intercambiar su ataque y su defensa." }, "gastroAcid": { "name": "Bilis", @@ -1521,7 +1521,7 @@ }, "luckyChant": { "name": "Conjuro", - "effect": "Lanza al cielo un conjuro que protege a todo su equipo de golpes críticos." + "effect": "Lanza al cielo un conjuro que protege a todo su equipo de golpes críticos durante 5 turnos." }, "meFirst": { "name": "Yo Primero", @@ -1533,11 +1533,11 @@ }, "powerSwap": { "name": "Cambiafuerza", - "effect": "El usuario emplea su poder mental para intercambiar los cambios en el Ataque y el Ataque Especial con el objetivo." + "effect": "El usuario emplea su poder mental para intercambiar los cambios en el ataque y el ataque especial con el objetivo." }, "guardSwap": { "name": "Cambiadefensa", - "effect": "El usuario emplea su poder mental para intercambiar los cambios en la Defensa y la Defensa Especial con el objetivo." + "effect": "El usuario emplea su poder mental para intercambiar los cambios en la defensa y la defensa especial con el objetivo." }, "punishment": { "name": "Castigo", @@ -1557,7 +1557,7 @@ }, "toxicSpikes": { "name": "Púas Tóxicas", - "effect": "Lanza una trampa de púas tóxicas a los pies del objetivo que envenena a los rivales que entran en combate." + "effect": "Lanza una trampa de púas tóxicas a los pies del lado oponente cuyo envenena a los Pokémon que entran en combate." }, "heartSwap": { "name": "Cambiaalmas", @@ -1573,7 +1573,7 @@ }, "flareBlitz": { "name": "Envite Ígneo", - "effect": "El Pokémon se cubre de llamas y carga contra el objetivo, aunque él también recibe daño. Puede quemar." + "effect": "El usuario se cubre de llamas y carga contra el objetivo, aunque él también recibe daño. Puede quemar." }, "forcePalm": { "name": "Palmeo", @@ -1585,7 +1585,7 @@ }, "rockPolish": { "name": "Pulimento", - "effect": "Reduce la resistencia puliendo su cuerpo. Aumenta mucho la Velocidad." + "effect": "Reduce la resistencia puliendo su cuerpo. Aumenta mucho la velocidad." }, "poisonJab": { "name": "Puya Nociva", @@ -1617,7 +1617,7 @@ }, "bugBuzz": { "name": "Zumbido", - "effect": "El usuario crea una onda sónica dañina moviendo su cuerpo que también puede disminuir la Defensa Especial del objetivo." + "effect": "El usuario crea una onda sónica dañina moviendo su cuerpo que también puede disminuir la defensa especial del objetivo." }, "dragonPulse": { "name": "Pulso Dragón", @@ -1641,11 +1641,11 @@ }, "focusBlast": { "name": "Onda Certera", - "effect": "Agudiza la concentración mental y libera su poder. Puede reducir la Defensa Especial del objetivo." + "effect": "Agudiza la concentración mental y libera su poder. Puede reducir la defensa especial del objetivo." }, "energyBall": { "name": "Energibola", - "effect": "Aúna fuerzas de la naturaleza y libera su ataque. Puede reducir la Defensa Especial del objetivo." + "effect": "El usuario reúne fuerzas de la naturaleza y libera su ataque. Puede reducir la defensa especial del objetivo." }, "braveBird": { "name": "Pájaro Osado", @@ -1653,7 +1653,7 @@ }, "earthPower": { "name": "Tierra Viva", - "effect": "La tierra a los pies del objetivo erupciona violentamente. Puede reducir la Defensa Especial del objetivo." + "effect": "La tierra a los pies del objetivo erupciona violentamente. Puede reducir la defensa especial del objetivo." }, "switcheroo": { "name": "Trapicheo", @@ -1665,7 +1665,7 @@ }, "nastyPlot": { "name": "Maquinación", - "effect": "Estimula su cerebro pensando en cosas malas. Aumenta mucho el Ataque Especial." + "effect": "Estimula su cerebro pensando en cosas malas. Aumenta mucho el ataque especial." }, "bulletPunch": { "name": "Puño Bala", @@ -1701,7 +1701,7 @@ }, "mudBomb": { "name": "Bomba Fango", - "effect": "Ataca lanzando una compacta bola de fango. Puede bajar la Precisión del objetivo." + "effect": "Ataca lanzando una compacta bola de fango. Puede bajar la precisión del objetivo." }, "psychoCut": { "name": "Psicocorte", @@ -1713,11 +1713,11 @@ }, "mirrorShot": { "name": "Disparo Espejo", - "effect": "El usuario libera un haz de energía desde su pulido cuerpo. Puede bajar la Precisión." + "effect": "El usuario libera un haz de energía desde su pulido cuerpo. Puede bajar la precisión." }, "flashCannon": { "name": "Foco Resplandor", - "effect": "El usuario concentra toda la luz del cuerpo y la libera. Puede bajar la Defensa Especial del objetivo." + "effect": "El usuario concentra toda la luz del cuerpo y la libera. Puede bajar la defensa especial del objetivo." }, "rockClimb": { "name": "Treparrocas", @@ -1725,7 +1725,7 @@ }, "defog": { "name": "Despejar", - "effect": "Potente viento que barre los efectos de movimientos como Reflejo o Pantalla de Luz usados por el objetivo. También reduce su Evasión." + "effect": "Potente viento que barre los efectos de movimientos como Reflejo o Pantalla de Luz usados por el objetivo. También reduce su evasión." }, "trickRoom": { "name": "Espacio Raro", @@ -1733,7 +1733,7 @@ }, "dracoMeteor": { "name": "Cometa Draco", - "effect": "Hace que grandes cometas caigan del cielo sobre el objetivo. Baja mucho el Ataque Especial del que lo usa." + "effect": "Hace que grandes cometas caigan del cielo sobre el objetivo. Baja mucho el ataque especial del que lo usa." }, "discharge": { "name": "Chispazo", @@ -1745,7 +1745,7 @@ }, "leafStorm": { "name": "Lluevehojas", - "effect": "Envuelve al objetivo con una lluvia de hojas afiladas, pero reduce mucho su Ataque Especial." + "effect": "Envuelve al objetivo con una lluvia de hojas afiladas, pero reduce mucho su ataque especial." }, "powerWhip": { "name": "Latigazo", @@ -1777,7 +1777,7 @@ }, "captivate": { "name": "Seducción", - "effect": "Si el objetivo es del sexo opuesto, queda embelesado y baja mucho su Ataque Especial." + "effect": "Si el objetivo es del sexo opuesto, queda embelesado y baja mucho su ataque especial." }, "stealthRock": { "name": "Trampa Rocas", @@ -1801,7 +1801,7 @@ }, "chargeBeam": { "name": "Rayo Carga", - "effect": "Lanza un rayo eléctrico contra el objetivo. Puede subir el Ataque Especial de quien lo usa." + "effect": "Lanza un rayo eléctrico contra el objetivo. Puede subir el ataque especial de quien lo usa." }, "woodHammer": { "name": "Mazazo", @@ -1812,12 +1812,12 @@ "effect": "Ataque de una rapidez espeluznante. Este movimiento tiene prioridad alta." }, "attackOrder": { - "name": "Al Ataque", + "name": "Al ataque", "effect": "El usuario llama a sus súbditos para que ataquen al objetivo. Suele ser crítico." }, "defendOrder": { "name": "A Defender", - "effect": "El usuario llama a sus súbditos para que formen un escudo viviente. Sube la Defensa y la Defensa Especial." + "effect": "El usuario llama a sus súbditos para que formen un escudo viviente. Sube la defensa y la defensa especial." }, "healOrder": { "name": "Auxilio", @@ -1857,7 +1857,7 @@ }, "seedFlare": { "name": "Fulgor Semilla", - "effect": "Una onda de choque se libera del cuerpo. Puede bajar mucho la Defensa Especial del objetivo." + "effect": "Una onda de choque se libera del cuerpo. Puede bajar mucho la defensa especial del objetivo." }, "ominousWind": { "name": "Viento Aciago", @@ -1869,7 +1869,7 @@ }, "honeClaws": { "name": "Afilagarras", - "effect": "El usuario se afila las garras para aumentar su Ataque y su Precisión." + "effect": "El usuario se afila las garras para aumentar su ataque y su precisión." }, "wideGuard": { "name": "Vasta Guardia", @@ -1877,15 +1877,15 @@ }, "guardSplit": { "name": "Isoguardia", - "effect": "El usuario emplea sus poderes para hacer la media de su Defensa y su Defensa Especial con las del objetivo y compartirlas." + "effect": "El usuario emplea sus poderes para hacer la media de su defensa y su defensa especial con las del objetivo y compartirlas." }, "powerSplit": { "name": "Isofuerza", - "effect": "El usuario emplea sus poderes para hacer la media de su Ataque y su Ataque Especial con los del objetivo y compartirlos." + "effect": "El usuario emplea sus poderes para hacer la media de su ataque y su ataque especial con los del objetivo y compartirlos." }, "wonderRoom": { "name": "Zona Extraña", - "effect": "Crea un espacio misterioso donde se intercambian la Defensa y la Defensa Especial de todos los Pokémon durante cinco turnos." + "effect": "Crea un espacio misterioso donde se intercambian la defensa y la defensa especial de todos los Pokémon durante cinco turnos." }, "psyshock": { "name": "Psicocarga", @@ -1897,7 +1897,7 @@ }, "autotomize": { "name": "Aligerar", - "effect": "El usuario se desprende de partes prescindibles de su cuerpo para hacerse más ligero y aumentar mucho su Velocidad." + "effect": "El usuario se desprende de partes prescindibles de su cuerpo para hacerse más ligero y aumentar mucho su velocidad." }, "ragePowder": { "name": "Polvo Ira", @@ -1929,7 +1929,7 @@ }, "quiverDance": { "name": "Danza Aleteo", - "effect": "Danza mística que aumenta el Ataque Especial, la Defensa Especial y la Velocidad." + "effect": "Danza mística que aumenta el ataque especial, la defensa especial y la velocidad." }, "heavySlam": { "name": "Cuerpo Pesado", @@ -1941,31 +1941,31 @@ }, "electroBall": { "name": "Bola Voltio", - "effect": "Lanza una bola eléctrica. Cuanto mayor sea la Velocidad del usuario en comparación con la del objetivo, mayor será la potencia del movimiento." + "effect": "Lanza una bola eléctrica. Cuanto mayor sea la velocidad del usuario en comparación con la del objetivo, mayor será la potencia del movimiento." }, "soak": { "name": "Empapar", - "effect": "Potente lluvia que transforma al objetivo en un Pokémon de tipo Agua." + "effect": "El usuario dispara un torrente de agua al objetivo y cambia el tipo del objetivo a Agua." }, "flameCharge": { "name": "Nitrocarga", - "effect": "Llamas que golpean al objetivo y aumentan la Velocidad del atacante." + "effect": "Llamas que golpean al objetivo y aumentan la velocidad del atacante." }, "coil": { "name": "Enrosque", - "effect": "El usuario se concentra, lo que le permite aumentar su Ataque, Defensa y Precisión." + "effect": "El usuario se enrosca y se concentra. Esto aumenta sus estadísticas de ataque y defensa, así como su precisión." }, "lowSweep": { "name": "Puntapié", - "effect": "Ataque rápido dirigido a los pies del objetivo que le hace perder Velocidad." + "effect": "Ataque rápido dirigido a los pies del objetivo que le hace perder velocidad." }, "acidSpray": { "name": "Bomba Ácida", - "effect": "Ataca con un líquido corrosivo que reduce mucho la Defensa Especial del objetivo." + "effect": "Ataca con un líquido corrosivo que reduce mucho la defensa especial del objetivo." }, "foulPlay": { "name": "Juego Sucio", - "effect": "El usuario emplea la fuerza del objetivo para atacarlo. Cuanto mayor es el Ataque del objetivo, más daño provoca." + "effect": "El usuario emplea la fuerza del objetivo para atacarlo. Cuanto mayor es el ataque del objetivo, más daño provoca." }, "simpleBeam": { "name": "Onda Simple", @@ -2013,7 +2013,7 @@ }, "shellSmash": { "name": "Rompecoraza", - "effect": "El usuario rompe su coraza y baja su Defensa y Defensa Especial, pero aumenta mucho su Ataque, Ataque Especial y Velocidad." + "effect": "El usuario rompe su coraza y baja su defensa y defensa especial, pero aumenta mucho su ataque, ataque especial y velocidad." }, "healPulse": { "name": "Pulso Cura", @@ -2029,7 +2029,7 @@ }, "shiftGear": { "name": "Cambio de Marcha", - "effect": "Al hacer girar los engranajes, el usuario mejora su Ataque y aumenta mucho su Velocidad." + "effect": "Al hacer girar los engranajes, el usuario mejora su ataque y aumenta mucho su velocidad." }, "circleThrow": { "name": "Llave Giro", @@ -2053,7 +2053,7 @@ }, "retaliate": { "name": "Represalia", - "effect": "Venga a los amigos caídos. Si en el turno anterior han derrotado a alguno, la potencia del ataque aumentará." + "effect": "El usuario se venga de un aliado desmayado. Si un aliado se desmayó en el turno anterior, el poder de este movimiento aumenta." }, "finalGambit": { "name": "Sacrificio", @@ -2085,11 +2085,11 @@ }, "struggleBug": { "name": "Estoicismo", - "effect": "El usuario opone resistencia y ataca a los oponentes. También reduce su Ataque Especial." + "effect": "El usuario opone resistencia y ataca a los oponentes. También reduce su ataque especial." }, "bulldoze": { "name": "Terratemblor", - "effect": "Sacudida sísmica que afecta a los Pokémon adyacentes y también reduce su Velocidad." + "effect": "Sacudida sísmica que afecta a los Pokémon adyacentes y también reduce su velocidad." }, "frostBreath": { "name": "Vaho Gélido", @@ -2097,15 +2097,15 @@ }, "dragonTail": { "name": "Cola Dragón", - "effect": "Ataca al objetivo y lo obliga a cambiarse por otro Pokémon. Si es uno salvaje, acaba el combate." + "effect": "El objetivo es derribado y otro Pokémon es arrastrado hacia al combate. En combates contra un solo Pokémon salvaje termina la batalla." }, "workUp": { "name": "Avivar", - "effect": "Quien lo usa se concentra y potencia su Ataque y su Ataque Especial." + "effect": "Quien lo usa se concentra y potencia su ataque y su ataque especial." }, "electroweb": { "name": "Electrotela", - "effect": "Atrapa y ataca a los objetivos con una telaraña eléctrica. También reduce su Velocidad." + "effect": "Atrapa y ataca a los objetivos con una telaraña eléctrica. También reduce su velocidad." }, "wildCharge": { "name": "Voltio Cruel", @@ -2133,7 +2133,7 @@ }, "razorShell": { "name": "Concha Filo", - "effect": "Una afilada vieira ataca al objetivo. También puede hacer disminuir su Defensa." + "effect": "Una afilada vieira ataca al objetivo. También puede hacer disminuir su defensa." }, "heatCrash": { "name": "Golpe Calor", @@ -2141,7 +2141,7 @@ }, "leafTornado": { "name": "Ciclón de Hojas", - "effect": "Tritura con afiladas hojas y puede bajar la Precisión del objetivo." + "effect": "Tritura con afiladas hojas y puede bajar la precisión del objetivo." }, "steamroller": { "name": "Rodillo de Púas", @@ -2149,11 +2149,11 @@ }, "cottonGuard": { "name": "Rizo Algodón", - "effect": "Cubre al Pokémon con una madeja protectora. Aumenta muchísimo la Defensa." + "effect": "Cubre al Pokémon con una madeja protectora. Aumenta muchísimo la defensa." }, "nightDaze": { "name": "Pulso Noche", - "effect": "Ataca al objetivo con una onda siniestra. Puede bajar su Precisión." + "effect": "Ataca al objetivo con una onda siniestra. Puede bajar su precisión." }, "psystrike": { "name": "Onda Mental", @@ -2193,7 +2193,7 @@ }, "glaciate": { "name": "Mundo Gélido", - "effect": "Ataque con aire helado que baja la Velocidad del objetivo." + "effect": "Ataque con aire helado que baja la velocidad del objetivo." }, "boltStrike": { "name": "Ataque Fulgor", @@ -2205,7 +2205,7 @@ }, "fieryDance": { "name": "Danza Llama", - "effect": "Envuelve en llamas y daña al objetivo. Puede aumentar el Ataque Especial de quien lo usa." + "effect": "Envuelve en llamas y daña al objetivo. Puede aumentar el ataque especial de quien lo usa." }, "freezeShock": { "name": "Rayo Gélido", @@ -2217,7 +2217,7 @@ }, "snarl": { "name": "Alarido", - "effect": "Chillido desagradable que reduce el Ataque Especial del objetivo." + "effect": "Chillido desagradable que reduce el ataque especial del objetivo." }, "icicleCrash": { "name": "Chuzos", @@ -2225,7 +2225,7 @@ }, "vCreate": { "name": "V de Fuego", - "effect": "Golpea con una V de llamas al objetivo. Baja la Defensa, la Defensa Especial y la Velocidad de quien lo usa." + "effect": "Golpea con una V de llamas al objetivo. Baja la defensa, la defensa especial y la velocidad de quien lo usa." }, "fusionFlare": { "name": "Llama Fusión", @@ -2249,15 +2249,15 @@ }, "rototiller": { "name": "Fertilizante", - "effect": "Labra la tierra haciendo que sea más fácil cultivarla y consigue que aumente el Ataque y el Ataque Especial de los Pokémon de tipo Planta." + "effect": "Labra la tierra haciendo que sea más fácil cultivarla y consigue que aumente el ataque y el ataque especial de los Pokémon de tipo Planta." }, "stickyWeb": { "name": "Red Viscosa", - "effect": "Coloca una red pegajosa alrededor del objetivo que reduce la Velocidad de los rivales que entran en combate." + "effect": "Coloca una red pegajosa alrededor del objetivo que reduce la velocidad de los rivales que entran en combate." }, "fellStinger": { "name": "Aguijón Letal", - "effect": "Si se derrota al objetivo utilizando este movimiento, aumenta muchísimo el Ataque del usuario." + "effect": "Si se derrota al objetivo utilizando este movimiento, aumenta muchísimo el ataque del usuario." }, "phantomForce": { "name": "Golpe Fantasma", @@ -2269,7 +2269,7 @@ }, "nobleRoar": { "name": "Rugido de Guerra", - "effect": "Intimida a su oponente con un rugido de guerra, lo que hace que disminuyan tanto su Ataque como su Ataque Especial." + "effect": "Al emitir un rugido noble, el usuario intimida al objetivo y reduce sus estadísticas de ataque y ataque especial." }, "ionDeluge": { "name": "Cortina Plasma", @@ -2297,7 +2297,7 @@ }, "partingShot": { "name": "Última Palabra", - "effect": "El usuario se cambia por otro Pokémon de su equipo, pero antes amedrenta a su oponente y hace que disminuyan su Ataque y Ataque Especial." + "effect": "Con una amenaza de despedida, el usuario reduce ataque y ataque especial del objetivo. Luego, lo cambia por un otro Pokémon." }, "topsyTurvy": { "name": "Reversión", @@ -2305,15 +2305,15 @@ }, "drainingKiss": { "name": "Beso Drenaje", - "effect": "El usuario absorbe PS del objetivo con un beso y restaura su propia energía en una cantidad igual o superior a la mitad del daño infligido." + "effect": "El usuario roba los HP del objetivo con un beso. Los HP del usuario se recuperan con más de la mitad del daño recibido por el objetivo." }, "craftyShield": { - "name": "Truco Defensa", + "name": "Truco defensa", "effect": "Usa unos misteriosos poderes para protegerse a sí mismo y a sus aliados de movimientos de estado, pero no de otro tipo de ataques." }, "flowerShield": { "name": "Defensa Floral", - "effect": "Aumenta la Defensa de todos los Pokémon de tipo Planta que hay en el combate usando unos misteriosos poderes." + "effect": "Aumenta la defensa de todos los Pokémon de tipo Planta que hay en el combate usando unos misteriosos poderes." }, "grassyTerrain": { "name": "Campo de Hierba", @@ -2329,7 +2329,7 @@ }, "playRough": { "name": "Carantoña", - "effect": "El Pokémon que lo usa le hace cucamonas al objetivo y lo ataca. Puede disminuir el Ataque del objetivo." + "effect": "El Pokémon que lo usa le hace cucamonas al objetivo y lo ataca. Puede disminuir el ataque del objetivo." }, "fairyWind": { "name": "Viento Feérico", @@ -2337,7 +2337,7 @@ }, "moonblast": { "name": "Fuerza Lunar", - "effect": "Invoca el poder de la luna para atacar al objetivo. Puede reducir su Ataque Especial." + "effect": "Invoca el poder de la luna para atacar al objetivo. Puede reducir su ataque especial." }, "boomburst": { "name": "Estruendo", @@ -2349,19 +2349,19 @@ }, "kingsShield": { "name": "Escudo Real", - "effect": "El usuario adopta una postura defensiva y se protege de cualquier daño. Reduce el Ataque de cualquier Pokémon con el que entre en contacto." + "effect": "El usuario adopta una postura defensiva y se protege de cualquier daño. Reduce el ataque de cualquier Pokémon con el que entre en contacto." }, "playNice": { "name": "Camaradería", - "effect": "Se hace amigo de su objetivo y consigue que a este se le quiten las ganas de combatir, lo que reduce su Ataque." + "effect": "Se hace amigo de su objetivo y consigue que a este se le quiten las ganas de combatir, lo que reduce su ataque." }, "confide": { "name": "Confidencia", - "effect": "Hace que el objetivo pierda la concentración contándole un secreto y reduce su Ataque Especial." + "effect": "Hace que el objetivo pierda la concentración contándole un secreto y reduce su ataque especial." }, "diamondStorm": { "name": "Torm. Diamantes", - "effect": "Desata un devastador vendaval de diamantes para dañar a los oponentes. Puede aumentar mucho la Defensa del usuario." + "effect": "Desata un devastador vendaval de diamantes para dañar a los oponentes. Puede aumentar mucho la defensa del usuario." }, "steamEruption": { "name": "Chorro de Vapor", @@ -2373,11 +2373,11 @@ }, "waterShuriken": { "name": "Shuriken de Agua", - "effect": "Golpea al objetivo de dos a cinco veces seguidas con estrellas arrojadizas hechas de mucosidad. Este movimiento tiene prioridad alta." + "effect": "El usuario golpea el objetivo con estrellas ninja de dos a cinco veces seguidas. Este movimiento es de prioridad alta." }, "mysticalFire": { "name": "Llama Embrujada", - "effect": "El usuario lanza por la boca una singular llama a gran temperatura con la que ataca a su objetivo y reduce su Ataque Especial." + "effect": "El usuario lanza por la boca una singular llama a gran temperatura con la que ataca a su objetivo y reduce su ataque especial." }, "spikyShield": { "name": "Barrera Espinosa", @@ -2385,15 +2385,15 @@ }, "aromaticMist": { "name": "Niebla Aromática", - "effect": "Consigue aumentar la Defensa Especial de un Pokémon de su equipo con una fragancia misteriosa." + "effect": "Consigue aumentar la defensa especial de un Pokémon de su equipo con una fragancia misteriosa." }, "eerieImpulse": { "name": "Onda Anómala", - "effect": "El usuario irradia unas raras ondas que, al alcanzar al objetivo, reducen mucho su Ataque Especial." + "effect": "El usuario irradia unas raras ondas que, al alcanzar al objetivo, reducen mucho su ataque especial." }, "venomDrench": { "name": "Trampa Venenosa", - "effect": "Impregna a su objetivo con un líquido venenoso que disminuye el Ataque, el Ataque Especial y la Velocidad. Solo afecta a Pokémon ya envenenados." + "effect": "Impregna a su objetivo con un líquido venenoso que disminuye el ataque, el ataque especial y la velocidad. Solo afecta a Pokémon ya envenenados." }, "powder": { "name": "Polvo Explosivo", @@ -2401,11 +2401,11 @@ }, "geomancy": { "name": "Geocontrol", - "effect": "Concentra energía durante el primer turno, de forma que su Velocidad, Ataque Especial y Defensa Especial aumenten mucho en el segundo." + "effect": "Concentra energía durante el primer turno, de forma que su velocidad, ataque especial y defensa especial aumenten mucho en el segundo." }, "magneticFlux": { "name": "Aura Magnética", - "effect": "Manipula el campo magnético y logra aumentar la Defensa y la Defensa Especial de los Pokémon aliados que cuenten con las habilidades Más y Menos." + "effect": "Manipula el campo magnético y logra aumentar la defensa y la defensa especial de los Pokémon aliados que cuenten con las habilidades Más y Menos." }, "happyHour": { "name": "Paga Extra", @@ -2429,7 +2429,7 @@ }, "babyDollEyes": { "name": "Ojitos Tiernos", - "effect": "Lanza una mirada al objetivo con ojos acaramelados, con lo que logra que su Ataque se reduzca. Este movimiento tiene prioridad alta." + "effect": "Lanza una mirada al objetivo con ojos acaramelados, con lo que logra que su ataque se reduzca. Este movimiento tiene prioridad alta." }, "nuzzle": { "name": "Moflete Estático", @@ -2445,7 +2445,7 @@ }, "powerUpPunch": { "name": "Puño Incremento", - "effect": "Cada vez que golpea a un oponente se endurecen sus puños. Si acierta al objetivo, el Ataque del usuario aumenta." + "effect": "Cada vez que golpea a un oponente se endurecen sus puños. Si acierta al objetivo, el ataque del usuario aumenta." }, "oblivionWing": { "name": "Ala Mortífera", @@ -2477,11 +2477,11 @@ }, "dragonAscent": { "name": "Ascenso Draco", - "effect": "El usuario se precipita desde el cielo a una velocidad de vértigo para atacar al objetivo, pero hace que bajen la Defensa y la Defensa Especial del usuario." + "effect": "El usuario se precipita desde el cielo a una velocidad de vértigo para atacar al objetivo, pero hace que bajen la defensa y la defensa especial del usuario." }, "hyperspaceFury": { "name": "Cerco Dimensión", - "effect": "Ataca al objetivo con una ráfaga de golpes que pasan por alto los efectos de movimientos como Protección o Detección. Baja la Defensa del usuario." + "effect": "Ataca al objetivo con una ráfaga de golpes que pasan por alto los efectos de movimientos como Protección o Detección. Baja la defensa del usuario." }, "breakneckBlitzPhysical": { "name": "Carrera Arrolladora", @@ -2657,7 +2657,7 @@ }, "iceHammer": { "name": "Martillo Hielo", - "effect": "Un terrible puño golpea al contrincante, pero la Velocidad del usuario se ve reducida." + "effect": "Un terrible puño golpea al contrincante, pero la velocidad del usuario se ve reducida." }, "floralHealing": { "name": "Cura Floral", @@ -2669,7 +2669,7 @@ }, "strengthSap": { "name": "Absorbefuerza", - "effect": "Restaura una cantidad de PS equivalente al valor de Ataque del rival, que además verá reducida esta característica." + "effect": "Restaura una cantidad de PS equivalente al valor de ataque del rival, que además verá reducida esta característica." }, "solarBlade": { "name": "Cuchilla Solar", @@ -2685,7 +2685,7 @@ }, "toxicThread": { "name": "Hilo Venenoso", - "effect": "Ataca al objetivo con hilillos venenosos que reducen su Velocidad y lo envenenan." + "effect": "Ataca al objetivo con hilillos venenosos que reducen su velocidad y lo envenenan." }, "laserFocus": { "name": "Aguzar", @@ -2693,7 +2693,7 @@ }, "gearUp": { "name": "Piñón Auxiliar", - "effect": "Cambia de marcha y logra aumentar el Ataque y el Ataque Especial de los Pokémon aliados que cuenten con las habilidades Más y Menos." + "effect": "Cambia de marcha y logra aumentar el ataque y el ataque especial de los Pokémon aliados que cuenten con las habilidades Más y Menos." }, "throatChop": { "name": "Golpe Mordaza", @@ -2713,11 +2713,11 @@ }, "lunge": { "name": "Plancha", - "effect": "Ataca al objetivo abalanzándose sobre él con todas sus fuerzas y reduce su Ataque." + "effect": "Ataca al objetivo abalanzándose sobre él con todas sus fuerzas y reduce su ataque." }, "fireLash": { "name": "Látigo Ígneo", - "effect": "Golpea al objetivo con un látigo incandescente y reduce su Defensa." + "effect": "Golpea al objetivo con un látigo incandescente y reduce su defensa." }, "powerTrip": { "name": "Chulería", @@ -2729,7 +2729,7 @@ }, "speedSwap": { "name": "Cambiavelocidad", - "effect": "Intercambia su Velocidad por la del objetivo." + "effect": "Intercambia su velocidad por la del objetivo." }, "smartStrike": { "name": "Cuerno Certero", @@ -2749,7 +2749,7 @@ }, "tropKick": { "name": "Patada Tropical", - "effect": "Lanza una patada con la fuerza del trópico que golpea al objetivo y reduce su Ataque." + "effect": "Lanza una patada con la fuerza del trópico que golpea al objetivo y reduce su ataque." }, "instruct": { "name": "Mandato", @@ -2761,7 +2761,7 @@ }, "clangingScales": { "name": "Fragor Escamas", - "effect": "Frota todas las escamas de su cuerpo para crear un fuerte sonido con el que ataca. Cuando el ataque termina, su Defensa se ve reducida." + "effect": "Frota todas las escamas de su cuerpo para crear un fuerte sonido con el que ataca. Cuando el ataque termina, su defensa se ve reducida." }, "dragonHammer": { "name": "Martillo Dragón", @@ -2817,7 +2817,7 @@ }, "fleurCannon": { "name": "Cañón Floral", - "effect": "El usuario emite un potente rayo, pero su Ataque Especial se reduce mucho." + "effect": "El usuario emite un potente rayo, pero su ataque especial se reduce mucho." }, "psychicFangs": { "name": "Psicocolmillo", @@ -2829,7 +2829,7 @@ }, "shadowBone": { "name": "Hueso Sombrío", - "effect": "Ataca golpeando con un hueso poseído por un espíritu. Puede reducir la Defensa del objetivo." + "effect": "Ataca golpeando con un hueso poseído por un espíritu. Puede reducir la defensa del objetivo." }, "accelerock": { "name": "Roca Veloz", @@ -2837,7 +2837,7 @@ }, "liquidation": { "name": "Hidroariete", - "effect": "Ataca golpeando gracias a la fuerza del agua. También puede reducir la Defensa del objetivo." + "effect": "Ataca golpeando gracias a la fuerza del agua. También puede reducir la defensa del objetivo." }, "prismaticLaser": { "name": "Láser Prisma", @@ -2857,7 +2857,7 @@ }, "tearfulLook": { "name": "Ojos Llorosos", - "effect": "Mira al objetivo con ojos llorosos para hacerle perder su espíritu combativo y reduce su Ataque y Ataque Especial." + "effect": "Mira al objetivo con ojos llorosos para hacerle perder su espíritu combativo y reduce su ataque y ataque especial." }, "zingZap": { "name": "Electropunzada", @@ -2885,11 +2885,11 @@ }, "photonGeyser": { "name": "Géiser Fotónico", - "effect": "El usuario ataca con una gran columna de luz. Compara sus valores de Ataque y Ataque Especial para infligir daño con el más alto de los dos." + "effect": "El usuario ataca con una gran columna de luz. Compara sus valores de ataque y ataque especial para infligir daño con el más alto de los dos." }, "lightThatBurnsTheSky": { "name": "Fotodestrucción Apocalíptica", - "effect": "Necrozma escoge la característica que tenga el valor más alto entre el Ataque y el Ataque Especial para infligir daño, ignorando la habilidad del objetivo." + "effect": "Necrozma escoge la característica que tenga el valor más alto entre el ataque y el ataque especial para infligir daño, ignorando la habilidad del objetivo." }, "searingSunrazeSmash": { "name": "Embestida Solar", @@ -2913,7 +2913,7 @@ }, "zippyZap": { "name": "Pikaturbo", - "effect": "The user attacks the target with bursts of electricity at high speed. This move always goes first and raises the user's evasiveness." + "effect": "Ataque eléctrico a la velocidad del rayo. Este movimiento tiene prioridad alta y propina golpes críticos." }, "splishySplash": { "name": "Salpikasurf", @@ -2985,7 +2985,7 @@ }, "stuffCheeks": { "name": "Atiborramiento", - "effect": "El usuario ingiere la baya que lleva equipada para aumentar mucho su Defensa." + "effect": "El usuario ingiere la baya que lleva equipada para aumentar mucho su defensa." }, "noRetreat": { "name": "Bastión Final", @@ -2993,7 +2993,7 @@ }, "tarShot": { "name": "Alquitranazo", - "effect": "Cubre al objetivo de un alquitrán pegajoso que reduce su Velocidad y lo vuelve débil contra el fuego." + "effect": "Cubre al objetivo de un alquitrán pegajoso que reduce su velocidad y lo vuelve débil contra el fuego." }, "magicPowder": { "name": "Polvo Mágico", @@ -3001,7 +3001,7 @@ }, "dragonDarts": { "name": "Dracoflechas", - "effect": "El usuario ataca propulsando a ambos Dreepy. En caso de haber dos adversarios, cada Dreepy golpea a su propio objetivo por separado." + "effect": "El usuario ataca propulsando proyectiles como escamas u otros pokémon ectera. En caso de haber dos adversarios, cada proyectil golpea a su propio objetivo por separado." }, "teatime": { "name": "Hora del Té", @@ -3009,7 +3009,7 @@ }, "octolock": { "name": "Octopresa", - "effect": "Retiene al objetivo e impide que pueda huir o ser cambiado por otro, a la vez que reduce su Defensa y su Defensa Especial cada turno." + "effect": "Retiene al objetivo e impide que pueda huir o ser cambiado por otro, a la vez que reduce su defensa y su defensa especial cada turno." }, "boltBeak": { "name": "Electropico", @@ -3029,7 +3029,7 @@ }, "maxFlutterby": { "name": "Maxinsecto", - "effect": "Ataque de tipo Bicho ejecutado por un Pokémon Dinamax. Reduce el Ataque Especial del objetivo." + "effect": "Ataque de tipo Bicho ejecutado por un Pokémon Dinamax. Reduce el ataque especial del objetivo." }, "maxLightning": { "name": "Maxitormenta", @@ -3037,15 +3037,15 @@ }, "maxStrike": { "name": "Maxiataque", - "effect": "Ataque de tipo Normal ejecutado por un Pokémon Dinamax. Reduce la Velocidad del objetivo." + "effect": "Ataque de tipo Normal ejecutado por un Pokémon Dinamax. Reduce la velocidad del objetivo." }, "maxKnuckle": { "name": "Maxipuño", - "effect": "Ataque de tipo Lucha ejecutado por un Pokémon Dinamax. Aumenta el Ataque de tu bando." + "effect": "Ataque de tipo Lucha ejecutado por un Pokémon Dinamax. Aumenta el ataque de tu bando." }, "maxPhantasm": { "name": "Maxiespectro", - "effect": "Ataque de tipo Fantasma ejecutado por un Pokémon Dinamax. Reduce la Defensa del objetivo." + "effect": "Ataque de tipo Fantasma ejecutado por un Pokémon Dinamax. Reduce la defensa del objetivo." }, "maxHailstorm": { "name": "Maxihelada", @@ -3053,7 +3053,7 @@ }, "maxOoze": { "name": "Maxiácido", - "effect": "Ataque de tipo Veneno ejecutado por un Pokémon Dinamax. Aumenta el Ataque Especial de tu bando." + "effect": "Ataque de tipo Veneno ejecutado por un Pokémon Dinamax. Aumenta el ataque especial de tu bando." }, "maxGeyser": { "name": "Maxichorro", @@ -3061,7 +3061,7 @@ }, "maxAirstream": { "name": "Maxiciclón", - "effect": "Ataque de tipo Volador ejecutado por un Pokémon Dinamax. Aumenta la Velocidad de tu bando." + "effect": "Ataque de tipo Volador ejecutado por un Pokémon Dinamax. Aumenta la velocidad de tu bando." }, "maxStarfall": { "name": "Maxiestela", @@ -3069,7 +3069,7 @@ }, "maxWyrmwind": { "name": "Maxidraco", - "effect": "Ataque de tipo Dragón ejecutado por un Pokémon Dinamax. Reduce el Ataque del objetivo." + "effect": "Ataque de tipo Dragón ejecutado por un Pokémon Dinamax. Reduce el ataque del objetivo." }, "maxMindstorm": { "name": "Maxionda", @@ -3081,11 +3081,11 @@ }, "maxQuake": { "name": "Maxitemblor", - "effect": "Ataque de tipo Tierra ejecutado por un Pokémon Dinamax. Aumenta la Defensa Especial de tu bando." + "effect": "Ataque de tipo Tierra ejecutado por un Pokémon Dinamax. Aumenta la defensa especial de tu bando." }, "maxDarkness": { "name": "Maxisombra", - "effect": "Ataque de tipo Siniestro ejecutado por un Pokémon Dinamax. Reduce la Defensa Especial del objetivo." + "effect": "Ataque de tipo Siniestro ejecutado por un Pokémon Dinamax. Reduce la defensa especial del objetivo." }, "maxOvergrowth": { "name": "Maxiflora", @@ -3093,7 +3093,7 @@ }, "maxSteelspike": { "name": "Maximetal", - "effect": "Ataque de tipo Acero ejecutado por un Pokémon Dinamax. Aumenta la Defensa de tu bando." + "effect": "Ataque de tipo Acero ejecutado por un Pokémon Dinamax. Aumenta la defensa de tu bando." }, "clangorousSoul": { "name": "Estruendo Escama", @@ -3101,15 +3101,15 @@ }, "bodyPress": { "name": "Plancha Corporal", - "effect": "El usuario usa el cuerpo para lanzar su ataque e infligir un daño directamente proporcional a su Defensa." + "effect": "El usuario usa el cuerpo para lanzar su ataque e infligir un daño directamente proporcional a su defensa." }, "decorate": { "name": "Decoración", - "effect": "Aumenta mucho el Ataque y el Ataque Especial del objetivo al decorarlo." + "effect": "Aumenta mucho el ataque y el ataque especial del objetivo al decorarlo." }, "drumBeating": { "name": "Batería Asalto", - "effect": "El usuario controla un tocón mediante la percusión y al atacar reduce la Velocidad del objetivo." + "effect": "El usuario controla un tocón mediante la percusión y al atacar reduce la velocidad del objetivo." }, "snapTrap": { "name": "Cepo", @@ -3129,11 +3129,11 @@ }, "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", - "effect": "El usuario sacude violentamente su enorme cola para golpear al objetivo y reducir su Ataque a la par." + "effect": "El usuario sacude violentamente su enorme cola para golpear al objetivo y reducir su ataque a la par." }, "branchPoke": { "name": "Punzada Rama", @@ -3145,15 +3145,15 @@ }, "appleAcid": { "name": "Ácido Málico", - "effect": "Ataca al objetivo con el fluido corrosivo que desprende una manzana ácida, lo que también reduce la Defensa Especial de este." + "effect": "Ataca al objetivo con el fluido corrosivo que desprende una manzana ácida, lo que también reduce la defensa especial de este." }, "gravApple": { "name": "Fuerza G", - "effect": "El usuario ataca haciendo caer una manzana desde gran altura. Reduce la Defensa del objetivo." + "effect": "El usuario ataca haciendo caer una manzana desde gran altura. Reduce la defensa del objetivo." }, "spiritBreak": { "name": "Choque Anímico", - "effect": "El usuario ataca al objetivo con tal ímpetu que acaba minando su moral y, en consecuencia, reduce su Ataque Especial." + "effect": "El usuario ataca al objetivo con tal ímpetu que acaba minando su moral y, en consecuencia, reduce su ataque especial." }, "strangeSteam": { "name": "Cautivapor", @@ -3165,7 +3165,7 @@ }, "obstruct": { "name": "Obstrucción", - "effect": "Frena todos los ataques, pero puede fallar si se usa repetidamente. Reduce mucho la Defensa de quien ejecute un movimiento de contacto contra el usuario." + "effect": "Frena todos los ataques, pero puede fallar si se usa repetidamente. Reduce mucho la defensa de quien ejecute un movimiento de contacto contra el usuario." }, "falseSurrender": { "name": "Irreverencia", @@ -3193,11 +3193,11 @@ }, "scaleShot": { "name": "Ráfaga Escamas", - "effect": "Lanza escamas al objetivo de dos a cinco veces seguidas. Aumenta la Velocidad del usuario, pero reduce su Defensa." + "effect": "Lanza escamas al objetivo de dos a cinco veces seguidas. Aumenta la velocidad del usuario, pero reduce su defensa." }, "meteorBeam": { "name": "Rayo Meteórico", - "effect": "El usuario dedica el primer turno a aumentar su Ataque Especial acumulando energía cósmica y lanza su ofensiva contra el objetivo en el segundo." + "effect": "El usuario dedica el primer turno a aumentar su ataque especial acumulando energía cósmica y lanza su ofensiva contra el objetivo en el segundo." }, "shellSideArm": { "name": "Moluscañón", @@ -3221,7 +3221,7 @@ }, "skitterSmack": { "name": "Golpe Rastrero", - "effect": "Ataca al objetivo por la espalda de forma subrepticia y, además, reduce su Ataque Especial." + "effect": "Ataca al objetivo por la espalda de forma subrepticia y, además, reduce su ataque especial." }, "burningJealousy": { "name": "Envidia Ardiente", @@ -3241,7 +3241,7 @@ }, "coaching": { "name": "Motivación", - "effect": "El usuario imparte indicaciones precisas a sus aliados, que ven aumentados su Ataque y su Defensa." + "effect": "El usuario imparte indicaciones precisas a sus aliados, que ven aumentados su ataque y su defensa." }, "flipTurn": { "name": "Viraje", @@ -3289,7 +3289,7 @@ }, "thunderousKick": { "name": "Patada Relámpago", - "effect": "El usuario desconcierta al objetivo con movimientos centelleantes y le propina una patada que, además, reduce su Defensa." + "effect": "El usuario desconcierta al objetivo con movimientos centelleantes y le propina una patada que, además, reduce su defensa." }, "glacialLance": { "name": "Lanza Glacial", @@ -3309,11 +3309,11 @@ }, "psyshieldBash": { "name": "Asalto Barrera", - "effect": "El usuario ataca envuelto en una energía psíquica que además aumenta su Defensa." + "effect": "El usuario ataca envuelto en una energía psíquica que además aumenta su defensa." }, "powerShift": { "name": "Cambiapoder", - "effect": "Intercambia su Ataque por su Defensa." + "effect": "Intercambia su ataque por su defensa." }, "stoneAxe": { "name": "Hachazo Pétreo", @@ -3321,11 +3321,11 @@ }, "springtideStorm": { "name": "Ciclón Primavera", - "effect": "Desata una tormenta de amor y odio con la que envuelve y ataca al objetivo. También puede reducir su Ataque." + "effect": "Desata una tormenta de amor y odio con la que envuelve y ataca al objetivo. También puede reducir su ataque." }, "mysticalPower": { "name": "Poder Místico", - "effect": "Ataca desatando un misterioso poder, que también aumenta su Ataque Especial." + "effect": "Ataca desatando un misterioso poder, que también aumenta su ataque especial." }, "ragingFury": { "name": "Erupción de Ira", @@ -3345,11 +3345,11 @@ }, "victoryDance": { "name": "Danza Triunfal", - "effect": "Ejecuta una danza frenética que invoca la victoria y aumenta el Ataque, la Defensa y la Velocidad." + "effect": "Ejecuta una danza frenética que invoca la victoria y aumenta el ataque, la defensa y la velocidad." }, "headlongRush": { "name": "Arremetida", - "effect": "El usuario arremete con todas sus fuerzas, pero se reducen su Defensa y su Defensa Especial." + "effect": "El usuario arremete con todas sus fuerzas, pero se reducen su defensa y su defensa especial." }, "barbBarrage": { "name": "Mil Púas Tóxicas", @@ -3357,19 +3357,19 @@ }, "esperWing": { "name": "Ala Aural", - "effect": "Corta con unas alas imbuidas de aura. Suele asestar un golpe crítico y aumenta la Velocidad del usuario." + "effect": "Corta con unas alas imbuidas de aura. Suele asestar un golpe crítico y aumenta la velocidad del usuario." }, "bitterMalice": { "name": "Rencor Reprimido", - "effect": "Ataca al objetivo sometiéndolo a su frío rencor y reduce su Ataque." + "effect": "Ataca al objetivo sometiéndolo a su frío rencor y reduce su ataque." }, "shelter": { "name": "Retracción", - "effect": "La piel del usuario se vuelve dura como un escudo de acero, lo que aumenta mucho su Defensa." + "effect": "La piel del usuario se vuelve dura como un escudo de acero, lo que aumenta mucho su defensa." }, "tripleArrows": { "name": "Triple Flecha", - "effect": "Propina un talonazo y lanza tres flechas. Suele asestar un golpe crítico y puede reducir la Defensa del objetivo o amedrentarlo." + "effect": "Propina un talonazo y lanza tres flechas. Suele asestar un golpe crítico y puede reducir la defensa del objetivo o amedrentarlo." }, "infernalParade": { "name": "Marcha Espectral", @@ -3381,7 +3381,7 @@ }, "bleakwindStorm": { "name": "Vendaval Gélido", - "effect": "Ataca con un viento muy frío que estremece el cuerpo y la mente y que, además, puede reducir la Velocidad del objetivo." + "effect": "Ataca con un viento muy frío que estremece el cuerpo y la mente y que, además, puede reducir la velocidad del objetivo." }, "wildboltStorm": { "name": "Electormenta", @@ -3397,7 +3397,7 @@ }, "takeHeart": { "name": "Bálsamo Osado", - "effect": "El usuario se envalentona y se cura de los problemas de estado. Además, aumenta su Ataque Especial y su Defensa Especial." + "effect": "El usuario se envalentona y se cura de los problemas de estado. Además, aumenta su ataque especial y su defensa especial." }, "gMaxWildfire": { "name": "Gigallamarada", @@ -3477,7 +3477,7 @@ }, "gMaxTartness": { "name": "Gigacorrosión", - "effect": "Ataque de tipo Planta ejecutado por un Flapple Gigamax. Reduce la Evasión del objetivo." + "effect": "Ataque de tipo Planta ejecutado por un Flapple Gigamax. Reduce la evasión del objetivo." }, "gMaxSweetness": { "name": "Giganéctar", @@ -3497,7 +3497,7 @@ }, "gMaxFoamBurst": { "name": "Gigaespuma", - "effect": "Ataque de tipo Agua ejecutado por un Kingler Gigamax. Reduce mucho la Velocidad del objetivo." + "effect": "Ataque de tipo Agua ejecutado por un Kingler Gigamax. Reduce mucho la velocidad del objetivo." }, "gMaxCentiferno": { "name": "Gigacienfuegos", @@ -3533,11 +3533,11 @@ }, "teraBlast": { "name": "Teraexplosión", - "effect": "Si el usuario se ha teracristalizado, ataca con la energía de su teratipo. Compara sus valores de Ataque y Ataque Especial para infligir daño con el más alto de los dos." + "effect": "Si el usuario se ha teracristalizado, ataca con la energía de su teratipo. Compara sus valores de ataque y ataque especial para infligir daño con el más alto de los dos." }, "silkTrap": { "name": "Telatrampa", - "effect": "Tiende una trampa sedosa que protege al usuario de los ataques al tiempo que reduce la Velocidad de cualquier Pokémon con el que entre en contacto." + "effect": "Tiende una trampa sedosa que protege al usuario de los ataques al tiempo que reduce la velocidad de cualquier Pokémon con el que entre en contacto." }, "axeKick": { "name": "Patada Hacha", @@ -3549,7 +3549,7 @@ }, "luminaCrash": { "name": "Fotocolisión", - "effect": "Ataca proyectando una extraña luz que afecta a la mente. Reduce mucho la Defensa Especial del objetivo." + "effect": "Ataca proyectando una extraña luz que afecta a la mente. Reduce mucho la defensa especial del objetivo." }, "orderUp": { "name": "Oído Cocina", @@ -3561,11 +3561,11 @@ }, "spicyExtract": { "name": "Extracto Picante", - "effect": "Libera un extracto extraordinariamente picante que aumenta mucho el Ataque del objetivo, pero también reduce mucho su Defensa." + "effect": "Libera un extracto extraordinariamente picante que aumenta mucho el ataque del objetivo, pero también reduce mucho su defensa." }, "spinOut": { "name": "Quemarrueda", - "effect": "Inflige daño al objetivo ejerciendo presión sobre sus extremidades y girando violentamente sobre sí. Reduce mucho la Velocidad del usuario." + "effect": "Inflige daño al objetivo ejerciendo presión sobre sus extremidades y girando violentamente sobre sí. Reduce mucho la velocidad del usuario." }, "populationBomb": { "name": "Proliferación", @@ -3601,7 +3601,7 @@ }, "filletAway": { "name": "Deslome", - "effect": "Aumenta mucho el Ataque, el Ataque Especial y la Velocidad del usuario a costa de parte de sus PS." + "effect": "Aumenta mucho el ataque, el ataque especial y la velocidad del usuario a costa de parte de sus PS." }, "kowtowCleave": { "name": "Genufendiente", @@ -3613,11 +3613,11 @@ }, "torchSong": { "name": "Canto Ardiente", - "effect": "Expele tórridas llamaradas como si entonara una canción y abrasa al objetivo con ellas. Aumenta el Ataque Especial del usuario." + "effect": "Expele tórridas llamaradas como si entonara una canción y abrasa al objetivo con ellas. Aumenta el ataque especial del usuario." }, "aquaStep": { "name": "Danza Acuática", - "effect": "Juguetea con el objetivo mientras ejecuta una elegante y fluida danza y le inflige daño. Aumenta la Velocidad del usuario." + "effect": "Juguetea con el objetivo mientras ejecuta una elegante y fluida danza y le inflige daño. Aumenta la velocidad del usuario." }, "ragingBull": { "name": "Furia Taurina", @@ -3625,7 +3625,7 @@ }, "makeItRain": { "name": "Fiebre Dorada", - "effect": "El usuario ataca arrojando una generosa cantidad de monedas, pero su Ataque Especial se ve reducido. Al finalizar el combate, las recupera en forma de ganancias." + "effect": "El usuario ataca arrojando una generosa cantidad de monedas, pero su ataque especial se ve reducido. Al finalizar el combate, las recupera en forma de ganancias." }, "psyblade": { "name": "Psicohojas", @@ -3657,23 +3657,23 @@ }, "tidyUp": { "name": "Limpieza General", - "effect": "Efectúa una limpieza a fondo que anula los efectos de Púas, Trampa Rocas, Red Viscosa, Púas Tóxicas y Sustituto. Aumenta el Ataque y la Velocidad del usuario." + "effect": "Efectúa una limpieza a fondo que anula los efectos de Púas, Trampa Rocas, Red Viscosa, Púas Tóxicas y Sustituto. Aumenta el ataque y la velocidad del usuario." }, "snowscape": { "name": "Paisaje Nevado", - "effect": "Desata una nevada que dura cinco turnos y aumenta la Defensa de los Pokémon de tipo Hielo." + "effect": "Desata una nevada que dura cinco turnos y aumenta la defensa de los Pokémon de tipo Hielo." }, "pounce": { "name": "Brinco", - "effect": "Ataca abalanzándose sobre el objetivo y le reduce la Velocidad." + "effect": "Ataca abalanzándose sobre el objetivo y le reduce la velocidad." }, "trailblaze": { "name": "Abrecaminos", - "effect": "Ataca de pronto como si saltara desde la hierba alta. El usuario se mueve con gran agilidad y aumenta su Velocidad." + "effect": "Ataca de pronto como si saltara desde la hierba alta. El usuario se mueve con gran agilidad y aumenta su velocidad." }, "chillingWater": { "name": "Agua Fría", - "effect": "Ataca al objetivo rociándolo con un agua gélida y desalentadora que reduce su Ataque." + "effect": "Ataca al objetivo rociándolo con un agua gélida y desalentadora que reduce su ataque." }, "hyperDrill": { "name": "Hipertaladro", @@ -3689,7 +3689,7 @@ }, "armorCannon": { "name": "Cañón Armadura", - "effect": "Se deshace de su armadura y arroja las partes al objetivo cuales proyectiles ardientes. Reduce la Defensa y la Defensa Especial del usuario." + "effect": "Se deshace de su armadura y arroja las partes al objetivo cuales proyectiles ardientes. Reduce la defensa y la defensa especial del usuario." }, "bitterBlade": { "name": "Espada Lamento", @@ -3741,7 +3741,7 @@ }, "syrupBomb": { "name": "Bomba Caramelo", - "effect": "Impregna al objetivo con una explosión de su viscoso néctar y lo carameliza, lo que hace que su Velocidad se reduzca progresivamente durante tres turnos." + "effect": "Impregna al objetivo con una explosión de su viscoso néctar y lo carameliza, lo que hace que su velocidad se reduzca progresivamente durante tres turnos." }, "ivyCudgel": { "name": "Garrote Liana", @@ -3749,7 +3749,7 @@ }, "electroShot": { "name": "Electrorrayo", - "effect": "Acumula electricidad y aumenta su Ataque Especial en el primer turno y dispara una descarga de alto voltaje en el segundo. Si llueve, puede atacar en el primer turno." + "effect": "Acumula electricidad y aumenta su ataque especial en el primer turno y dispara una descarga de alto voltaje en el segundo. Si llueve, puede atacar en el primer turno." }, "teraStarstorm": { "name": "Teraclúster", diff --git a/src/locales/es/mystery-encounter-messages.json b/src/locales/es/mystery-encounter-messages.json new file mode 100644 index 00000000000..8d7d98bdd0f --- /dev/null +++ b/src/locales/es/mystery-encounter-messages.json @@ -0,0 +1,7 @@ +{ + "paid_money": "Pagaste {{amount, number}}₽.", + "receive_money": "¡Recibiste {{amount, number}}₽!", + "affects_pokedex": "Afecta los datos de la Pokédex", + "cancel_option": "Volver a la selección de opciones", + "view_party_button": "Ver equipo" +} diff --git a/src/locales/es/mystery-encounters/a-trainers-test-dialogue.json b/src/locales/es/mystery-encounters/a-trainers-test-dialogue.json new file mode 100644 index 00000000000..bc100f60454 --- /dev/null +++ b/src/locales/es/mystery-encounters/a-trainers-test-dialogue.json @@ -0,0 +1,47 @@ +{ + "intro": "Un entrenador extremadamente fuerte se te acerca...", + "buck": { + "intro_dialogue": "¡Hola, entrenador! Me llamo Bulgur.$Tengo una propuesta súper genial para un entrenador fuerte como tú.$Llevo conmigo dos Huevos Pokémon raros, pero me gustaría que alguien más cuidara uno.$Si puedes demostrarme tu fuerza como entrenador, ¡te daré el huevo más raro", + "accept": "¡Guau!¡Este combate promete!", + "decline": "Vaya, parece que tu equipo no está en condiciones óptimas.$Aquí, déjame ayudarte con eso." + }, + "cheryl": { + "intro_dialogue": "Hola, me llamo Malta.$Tengo una petición particularmente interesante para un entrenador fuerte como tú.$Llevo conmigo dos Huevos Pokémon raros, pero me gustaría que alguien más cuidara uno.$Si puedes demostrarme tu fuerza como entrenador, ¡te daré el Huevo más raro!", + "accept": "¡Espero que estés listo!", + "decline": "Entiendo, parece que tu equipo no está en las mejores condiciones en este momento.$Aquí, déjame ayudar con eso." + }, + "marley": { + "intro_dialogue": "...@d{64} Soy Sémola.$Tengo una oferta para ti...$Llevo conmigo dos Huevos Pokémon, pero me gustaría que alguien más cuidara uno.$Si eres más fuerte que yo, te daré el Huevo más raro.", + "accept": "... Ya veo.", + "decline": "... Ya veo.$Tus Pokémon parecen heridos… Déjame ayudarte." + }, + "mira": { + "intro_dialogue": "¡Hola! Soy Maiza. Maiza tiene una petición para un entrenador fuerte como tú. Maiza tiene dos Huevos Pokémon raros, pero quiere que alguien más se quede con uno. Si le demuestras a Maiza que eres fuerte,¡Maiza te dará el Huevo más raro!", + "accept": "¿Vas a luchar contra Maiza?\n¡Genial!", + "decline": "Vaya, ¿no hay batalla? ¡Está bien! Aquí, Maiza curará a tu equipo." + }, + "riley": { + "intro_dialogue": "Soy Quinoa.$Tengo una propuesta extraña para un entrenador fuerte como tú.$Llevo conmigo dos Huevos Pokémon raros, pero me gustaría darle uno a otro entrenador.$Si puedes demostrarme tu fuerza, te daré el Huevo más raro.", + "accept": "Esa mirada que tienes...\nhagamos esto..", + "decline": "Entiendo, tu equipo parece agotado.$Aquí, déjame ayudarte con eso." + }, + "title": "Una Prueba de Entrenador", + "description": "Parece que este entrenador está dispuesto a darte un Huevo sin importar tu decisión. Sin embargo, si logras derrotar a este entrenador fuerte, recibirás un Huevo mucho más raro.", + "query": "¿Qué harás?", + "option": { + "1": { + "label": "Aceptar el Desafío", + "tooltip": "(-) Batalla Ardua\n(+) Obtén un @[TOOLTIP_TITLE]{Huevo muy raro}" + }, + "2": { + "label": "Rechazar el Desafío", + "tooltip": "(+) Equipo Curado\n(+) Obtén un @[TOOLTIP_TITLE]{Huevo}" + } + }, + "eggTypes": { + "rare": "un huevo Raro", + "epic": "un huavo Épico", + "legendary": "un huevo Legendario " + }, + "outro": "{{statTrainerName}} te dio {{eggType}}!" +} diff --git a/src/locales/es/mystery-encounters/absolute-avarice-dialogue.json b/src/locales/es/mystery-encounters/absolute-avarice-dialogue.json new file mode 100644 index 00000000000..f3700c1d60b --- /dev/null +++ b/src/locales/es/mystery-encounters/absolute-avarice-dialogue.json @@ -0,0 +1,25 @@ +{ + "intro": "¡Un {{greedentName}} te embosca y roba las bayas de tu equipo!", + "title": "Avaricia Absoluta", + "description": "¡Un {{greedentName}} te ha tomado completamente por sorpresa y ahora todas tus bayas han desaparecido! El {{greedentName}} parece que está a punto de comérselas cuando se detiene para mirarte, interesado.", + "query": "¿Qué harás?", + "option": { + "1": { + "label": "Combatir", + "tooltip": "(-) Batalla Ardua\n(+) Recompensas de su Alijo de Bayas", + "selected": "El {{greedentName}} llena sus mejillas y se prepara para la batalla!", + "boss_enraged": "¡El feroz amor de {{greedentName}} por la comida lo tiene enfurecido!", + "food_stash": "¡Parece que el {{greedentName}} estaba protegiendo un enorme alijo de comida!$@s{item_fanfare}¡Cada Pokémon en tu grupo obtiene una {{foodReward}}!" + }, + "2": { + "label": "Razona con él", + "tooltip": "(+) Recupera algunas bayas perdidas", + "selected": "Tus súplicas conmueven al {{greedentName}}.$No te devuelve todas tus bayas, pero aún así te lanza algunas en tu dirección." + }, + "3": { + "label": "Déjalo quedarse con la comida", + "tooltip": "(-) Perder todas las bayas\n(?) Le gustarás al {{greedentName}}", + "selected": "El {{greedentName}} devora todas sus bayas en un instante!$Acariciando su estómago, te mira con aprecio.$Quizás podrías darle más bayas en tu aventura....$@s{level_up_fanfare}El {{greedentName}} quiere unirse a tu equipo!" + } + } +} \ No newline at end of file diff --git a/src/locales/es/mystery-encounters/an-offer-you-cant-refuse-dialogue.json b/src/locales/es/mystery-encounters/an-offer-you-cant-refuse-dialogue.json new file mode 100644 index 00000000000..07396cbc3b2 --- /dev/null +++ b/src/locales/es/mystery-encounters/an-offer-you-cant-refuse-dialogue.json @@ -0,0 +1,26 @@ +{ + "intro": "Te detiene un chico de aspecto rico.", + "speaker": "Niño Bien", + "intro_dialogue": "Buenos días a usted.$¡No puedo evitar notar que tu\n{{strongestPokemon}} se ve absolutamente divino!$¡Siempre he querido tener un Pokémon así!$¡Te pagaría generosamente,\n también te daría este viejo abalorio!", + "title": "Una oferta que no puedes rechazar", + "description": "Te están ofreciendo @[TOOLTIP_TITLE]{Amuleto Iris} y {{price, money}} por tu {{strongestPokemon}}!¡Es un trato extremadamente bueno, pero ¿realmente puedes soportar separarte de un miembro tan fuerte de tu equipo?", + "query": "¿Qué harás?", + "option": { + "1": { + "label": "Aceptar el trato", + "tooltip": "(-) Pierdes a {{strongestPokemon}}\n(+) Obtén un @[TOOLTIP_TITLE]{Amuleto Iris}\n(+) Obtén {{price, money}}", + "selected": "¡Maravilloso!@d{32} ¡Ven, John!, {{strongestPokemon}}!$¡Es hora de mostrarte a todos en el club náutico!$¡Estarán tan celosos!" + }, + "2": { + "label": "Extorsiona al niño", + "tooltip": "(+) {{option2PrimaryName}} usa {{moveOrAbility}}\n(+) Obtén {{price, money}}", + "tooltip_disabled": "Tus Pokémon necesitan tener ciertos movimientos o habilidades para elegir esto", + "selected": "¡Por Dios, nos están robando, {{liepardName}}!$¡Oirás de mis abogados por esto!" + }, + "3": { + "label": "Irse", + "tooltip": "(-) Ninguna Recompensa", + "selected": "Qué día más horrible…$Bueno, volvamos al club náutico entonces, {{liepardName}}." + } + } +} \ No newline at end of file diff --git a/src/locales/es/mystery-encounters/berries-abound-dialogue.json b/src/locales/es/mystery-encounters/berries-abound-dialogue.json new file mode 100644 index 00000000000..4b9da99fbeb --- /dev/null +++ b/src/locales/es/mystery-encounters/berries-abound-dialogue.json @@ -0,0 +1,26 @@ +{ + "intro": "¡Hay un gran arbusto de bayas cerca de ese Pokémon!", + "title": "Bayas Abundantes", + "description": "Parece que hay un Pokémon fuerte protegiendo un arbusto de bayas. Luchar es el enfoque directo, pero parece fuerte. ¿Quizás un Pokémon rápido podría agarrar algunas bayas sin ser descubierto?", + "query": "¿Qué harás?", + "berries": "¡Bayas!", + "option": { + "1": { + "label": "Enfréntate al Pokémon", + "tooltip": "(-) Batalla Difícil\n(+) Obtén bayas", + "selected": "Te acercas al\nPokémon sin miedo." + }, + "2": { + "label": "Corre hacia el arbusto", + "tooltip": "(-) {{fastestPokemon}} Utiliza su velocidad\n(+) Obtén bayas", + "selected": "¡Tu {{fastestPokemon}} corre hacia el arbusto de bayas!$¡Logra agarrar {{numBerries}} antes de que {{enemyPokemon}} pueda reaccionar!$Huyes rápidamente con tu nuevo premio.", + "selected_bad": "¡Tu {{fastestPokemon}} corre hacia el arbusto de bayas!$¡Oh no! ¡El {{enemyPokemon}} fue más rápido y bloqueó el camino!", + "boss_enraged": "¡El {{enemyPokemon}} oponente se ha enfurecido!" + }, + "3": { + "label": "Irse", + "tooltip": "(-) Ninguna Recompensa", + "selected": "Dejas al Pokémon fuerte con su premio y continúas." + } + } +} \ No newline at end of file diff --git a/src/locales/es/mystery-encounters/bug-type-superfan-dialogue.json b/src/locales/es/mystery-encounters/bug-type-superfan-dialogue.json new file mode 100644 index 00000000000..0fdc0995039 --- /dev/null +++ b/src/locales/es/mystery-encounters/bug-type-superfan-dialogue.json @@ -0,0 +1,40 @@ +{ + "intro": "¡Un entrenador inusual con todo tipo de parafernalia de bichos bloquea tu camino!", + "intro_dialogue": "¡Hola, entrenador! ¡Estoy en una misión para encontrar el Pokémon Bicho más raro que existe!$¿A ti también te encantan los Pokémon Bicho, verdad? ¡A todos les encantan los Pokémon Bicho!", + "title": "El Superfan de los Pokémon Bicho", + "speaker": "Superfan de los Pokémon Bicho", + "description": "El entrenador parlotea, sin siquiera esperar una respuesta...\n\n¡Parece que la única forma de salir de esta situación es captando la atención del entrenador!", + "query": "¿Qué harás?", + "option": { + "1": { + "label": "Proponer a luchar", + "tooltip": "(-) Batalla Desafiante\n(+) Enseña un movimiento de tipo Bicho a un Pokémon", + "selected": "¿Un desafío, eh?\n¡Mis Pokémon Bicho están más que preparados para ti!" + }, + "2": { + "label": "Muestra tus tipos Bicho", + "tooltip": "(+) ¡Recibe un objeto de regalo!", + "disabled_tooltip": "Necesitas al menos 1 Pokémon de tipo Bicho en tu equipo para seleccionar esto.", + "selected": "Le muestras al entrenador todos tus Pokémon de tipo Bicho...", + "selected_0_to_1": "¿Eh? Solo tienes {{numBugTypes}} tipo Bicho...$Supongo que estoy perdiendo el tiempo con alguien como tú...", + "selected_2_to_3": "¡Oye, tienes {{numBugTypes}}! No está mal.$Aquí, esto podría ayudarte en tu viaje para atrapar más.", + "selected_4_to_5": "¿Qué? ¿Tienes {{numBugTypes}}?\nNice!$No estás a mi nivel, pero puedo ver destellos de mí en ti.$¡Toma esto, mi joven aprendiz!", + "selected_6": "¡Vaya! {{numBugTypes}}!$¡Debes amar a los Pokémon de tipo Bicho casi tanto como yo!$Aquí, toma esto como un símbolo de nuestra camaradería." + }, + "3": { + "label": "Regala un objeto de tipo Bicho", + "tooltip": "(-) Dale al entrenador un {{requiredBugItems}}\n(+) Recibe un item de regalo", + "disabled_tooltip": "Necesitas tener un {{requiredBugItems}} para seleccionar esto.", + "select_prompt": "Selecciona un objeto para dar", + "invalid_selection": "El Pokémon no tiene ese tipe de objeto.", + "selected": "Le entregas al entrenador un{{selectedItem}}.", + "selected_dialogue": "¡Vaya! ¿Un {{selectedItem}}, para mí? ¡No eres tan malo, chico!$Como muestra de mi agradecimiento, quiero que tengas este regalo especial.$Ha pasado por toda mi familia, y ahora quiero que lo tengas tú." + } + }, + "battle_won": "¡Tu conocimiento y habilidad fueron perfectos para explotar nuestras debilidades!$A cambio de la valiosa lección, permíteme enseñarle a uno de tus Pokémon un movimiento de tipo Bicho.", + "teach_move_prompt": "Selecciona un movimiento para enseñar a un Pokémon.", + "confirm_no_teach": "¿Estás seguro de que no quieres aprender uno de estos excellentes movimientos?", + "outro": "¡Veo grandes Pokémon de tipo Bicho en tu futuro! ¡Que nuestros caminos se crucen de nuevo! ¡Bicho fuera!", + "numBugTypes_one": "{{count}} Pokémon de tipo Bicho", + "numBugTypes_other": "{{count}} Pokémon de tipo Bicho" +} diff --git a/src/locales/es/mystery-encounters/clowning-around-dialogue.json b/src/locales/es/mystery-encounters/clowning-around-dialogue.json new file mode 100644 index 00000000000..5199c8d6432 --- /dev/null +++ b/src/locales/es/mystery-encounters/clowning-around-dialogue.json @@ -0,0 +1,33 @@ +{ + "intro": "¿Es un...@d{64} payaso?", + "speaker": "Payaso", + "intro_dialogue": "¡Bufón torpe, prepárate para una batalla brillante! ¡Serás derrotado por este trovador peleador!", + "description": "Algo no esta bien en este encuentro. El payaso parece ansioso por provocarte a una batalla, ¿pero con qué fin? El {{blacephalonName}} es especialmente extraño, como si tuviera @[TOOLTIP_TITLE]{tipos y habilidades raros.}", + "query": "¿Qué harás?", + "option": { + "1": { + "label": "Enfrentarse al Payaso", + "tooltip": "(-) Batalla extraña\n(?) Afecta las habilidades de los Pokémon", + "selected": "¡Tus patéticos Pokémon están listos para una actuación patética!", + "apply_ability_dialogue": "¡Una exhibición sensacional! ¡Tu astucia se adapta a una habilidad sensacional como recompensa!", + "apply_ability_message": "¡El payaso está ofreciendo intercambiar permanentemente la habilidad de uno de tus Pokémon por {{ability}}!", + "ability_prompt": "¿Te gustaría enseñar permanentemente a un Pokémon la habilidad {{ability}}?", + "ability_gained": "¡@s{level_up_fanfare}{{chosenPokemon}} obtenió la habilidad {{ability}}!" + }, + "2": { + "label": "No involucrarse", + "tooltip": "(-) Molesta al payaso\n(?) Afecta los objetos de los Pokémon", + "selected": "¡Cobarde desdichado, niegas un exquisito duelo?\n ¡Siente mi furia!", + "selected_2": "¡El {{blacephalonName}} del payaso usa Truco! ¡Todos los objetos de tu {{switchPokemon}} fueron intercambiados al azar!", + "selected_3": "¡Tonto desconcertado, cae en mi engaño impecable!" + }, + "3": { + "label": "Devolver los insultos", + "tooltip": "(-) Molesta al payaso\n(?) Afecta los objetos de los Pokémon", + "selected": "¡Cobarde desdichado, niegas un exquisito duelo?\n ¡Siente mi furia!", + "selected_2": "¡El {{blacephalonName}} del payaso usa un movimiento extraño! ¡Todos los tipos de tu equipo fueron intercambiados al azar!", + "selected_3": "¡Tonto desconcertado, cae en mi engaño impecable!" + } + }, + "outro": "El payaso y sus secuaces\ndesaparecen en una nube de humo." +} \ No newline at end of file diff --git a/src/locales/es/mystery-encounters/dancing-lessons-dialogue.json b/src/locales/es/mystery-encounters/dancing-lessons-dialogue.json new file mode 100644 index 00000000000..c4494e3efa8 --- /dev/null +++ b/src/locales/es/mystery-encounters/dancing-lessons-dialogue.json @@ -0,0 +1,27 @@ +{ + "intro": "Un {{oricorioName}} baila tristemente solo, sin pareja.", + "title": "Clases de baile", + "description": "El {{oricorioName}} no parece agresivo, más bien parece triste.\nTal vez solo quiera alguien con quien bailar...", + "query": "¿Qué harás?", + "option": { + "1": { + "label": "Enfrentarse", + "tooltip": "(-) Batalla Ardua\n(+) Obtén el objeto Relevo", + "selected": "¡El {{oricorioName}} está angustiado e intenta defenderse!", + "boss_enraged": "¡El miedo del {{oricorioName}} aumentó sus estadísticas!" + }, + "2": { + "label": "Aprende su danza", + "tooltip": "(+) Enseña a un Pokémon Danza despertar", + "selected": "Observas atentamente al {{oricorioName}} mientras realiza su danza…$@s{level_up_fanfare}¡Tu {{selectedPokemon}} aprendió del {{oricorioName}}!" + }, + "3": { + "label": "Muéstrale una danza", + "tooltip": "(-) Enseña al {{oricorioName}} un movimiento de danza\n(+) Le gustaras al {{oricorioName}}", + "disabled_tooltip": "Tus Pokémon necesitan conocer un movimiento de danza para esto.", + "select_prompt": "Selecciona un movimiento de tipo danza para usar.", + "selected": "¡El {{oricorioName}} observa fascinado mientras\n{{selectedPokemon}} muestra {{selectedMove}}!$¡Le encanta la exhibición!$@s{level_up_fanfare}¡El {{oricorioName}} quiere unirse a tu equipo!" + } + }, + "invalid_selection": "Este Pokémon no conoce ningún movimiento de danza" +} \ No newline at end of file diff --git a/src/locales/es/mystery-encounters/dark-deal-dialogue.json b/src/locales/es/mystery-encounters/dark-deal-dialogue.json new file mode 100644 index 00000000000..19b5c5c5b78 --- /dev/null +++ b/src/locales/es/mystery-encounters/dark-deal-dialogue.json @@ -0,0 +1,24 @@ + + +{ + "intro": "Un hombre extraño con un abrigo andrajoso se interpone en tu camino...", + "speaker": "Tipo sombrío", + "intro_dialogue": "¡Oye, tú!$He estado trabajando en un nuevo dispositivo\npara sacar el poder latente de un Pokémon!$Reorganiza completamente los átomos del Pokémon\na nivel molecular en una forma mucho más poderosa.$Jeje…@d{64} Solo necesito algunos sac-@d{32}\nEh, sujetos de prueba, para demostrar que funciona.", + "title": "Pacto Oscuro", + "description": "El tipo inquietante sostiene unas Pokéballs.\n\"¡Te lo compensaré! Puedes tener estas Pokéballs fuertes como pago. ¡Todo lo que necesito es un Pokémon de tu equipo! Jeje...", + "query": "¿Qué harás?", + "option": { + "1": { + "label": "Acceptar", + "tooltip": "(+) 5 Rogue Balls\n(?) Mejora un Pokémon aleatorio", + "selected_dialogue": "Veamos, ¡Ese {{pokeName}} servirá muy bien!$Recuerda, no soy responsable\nsi algo malo sucede!@d{32} Jeje...", + "selected_message": "El hombre te entrega 5 Rogue Balls.${{pokeName}} entra dentro de la máquina...$¡Luces intermitentes y ruidos extraños\ncomienzan a salir de la máquina!$...@d{96} Algo emerge\ndel dispositivo, ¡furiosamente!" + }, + "2": { + "label": "Rechazar", + "tooltip": "(-) Ninguna Recompensa", + "selected": "¿No vas a ayudar a un pobre hombre?\n¡Bah!" + } + }, + "outro": "Después del encuentro angustioso, te recuperas y te marchas." +} \ No newline at end of file diff --git a/src/locales/es/mystery-encounters/delibirdy-dialogue.json b/src/locales/es/mystery-encounters/delibirdy-dialogue.json new file mode 100644 index 00000000000..79e0b457b01 --- /dev/null +++ b/src/locales/es/mystery-encounters/delibirdy-dialogue.json @@ -0,0 +1,29 @@ + + +{ + "intro": "¡Ha aparecido una bandada de {{delibirdName}}!", + "title": "Pajarradas", + "description": "Los {{delibirdName}} te miran expectantes, como si quisieran algo. Tal vez darles un objeto o algo de dinero los satisfaría.", + "query": "¿Qué les darás?", + "invalid_selection": "Pokémon no tienen ese tipo de objeto.", + "option": { + "1": { + "label": "Dar dinero", + "tooltip": "(-) Dar a los {{delibirdName}}s {{money, money}}\n(+) Recibe un objeto de regalo", + "selected": "Lanzas el dinero a los {{delibirdName}}s, \nque charlan emocionados entre ellos.$Luego se vuelven hacia ti y te dan un regalo con alegría." + }, + "2": { + "label": "Dar comida", + "tooltip": "(-) Dar a los {{delibirdName}}s una Baya o Semilla milagro\n(+) Recibe un objeto de regalo", + "select_prompt": "Selecciona un objeto para dar.", + "selected": "Lanzas el {{chosenItem}} a los {{delibirdName}},\nque charlan emocionados entre ellos.$Se vuelven hacia ti y te dan un regalo alegremente." + }, + "3": { + "label": "Dar objeto", + "tooltip": "(-) Dar a los {{delibirdName}}s un objeto equipado\n(+) Recibe un objeto de regalo", + "select_prompt": "Selecciona un objeto para dar.", + "selected": "Lanzas el {{chosenItem}} a los {{delibirdName}},\nque charlan emocionados entre ellos.$Se vuelven hacia ti y te dan un regalo alegremente." + } + }, + "outro": "La bandada de {{delibirdName}} se aleja felizmente a lo lejos. ¡Qué intercambio tan curioso!" +} diff --git a/src/locales/es/mystery-encounters/department-store-sale-dialogue.json b/src/locales/es/mystery-encounters/department-store-sale-dialogue.json new file mode 100644 index 00000000000..e17d57b8c3c --- /dev/null +++ b/src/locales/es/mystery-encounters/department-store-sale-dialogue.json @@ -0,0 +1,27 @@ +{ + "intro": "Es una señora con un montón de bolsas de compras.", + "speaker": "Compradora", + "intro_dialogue": "¡Hola! ¿También estás aquí por las increíbles rebajas? Hay un cupón especial que puedes canjear por un artículo gratis durante la venta. ¡Tengo uno extra. ¡Aquí tienes!", + "title": "¡Rebajas en el Centro Comercial!", + "description": "¡Hay mercancía en todas direcciones! Parece que hay 4 mostradores donde puedes canjear el cupón por varios artículos. ¡Las posibilidades son infinitas!", + "query": "¿A qué mostrador irás?", + "option": { + "1": { + "label": "Mostrador de MTs", + "tooltip": "(+) Tienda de MTs" + }, + "2": { + "label": "Mostrador de Vitaminas", + "tooltip": "(+) Tienda de vitaminas" + }, + "3": { + "label": "Mostrador de objetos de batalla", + "tooltip": "(+) Tienda de objetos X" + }, + "4": { + "label": "Mostrador de Pokéballs ", + "tooltip": "(+) Tienda de Pokéballs" + } + }, + "outro": "¡Qué chollo! Deberías comprar allí más a menudo." +} \ No newline at end of file diff --git a/src/locales/es/mystery-encounters/field-trip-dialogue.json b/src/locales/es/mystery-encounters/field-trip-dialogue.json new file mode 100644 index 00000000000..be2554aba35 --- /dev/null +++ b/src/locales/es/mystery-encounters/field-trip-dialogue.json @@ -0,0 +1,31 @@ +{ + "intro": "¡Es un profesor y alugnos estudiantes parvulitos", + "speaker": "Profesor", + "intro_dialogue": "¡Hola! ¿Podrías dedicar un minuto a mis alumnos?$Les estoy enseñando sobre los movimientos de Pokémon\ny me encantaría mostrarles una demostración.$¿Te importaría mostrarnos uno de los movimientos\nque puede usar tu Pokémon?", + "title": "Excursión", + "description": "Una profesora está solicitando una demostración de un movimiento de un Pokémon. Dependiendo del movimiento que elijas, podría tener algo útil para ti a cambio.", + "query": "¿Qué categoría de movimiento mostrarás?", + "option": { + "1": { + "label": "Un movimiento físico", + "tooltip": "(+) Recompensas de objetos para movimientos físicos" + }, + "2": { + "label": "Un movimiento especial", + "tooltip": "(+) Recompensas de objetos para movimientos especiales" + }, + "3": { + "label": "Un ataque de estado", + "tooltip": "(+) Recompensas de objetos de estado" + }, + "selected": "¡{{pokeName}} muestra una increíble exhibición de {{move}}!" + }, + "second_option_prompt": "Elige un movimiento para que tu Pokémon use.", + "incorrect": "...$¡Eso no es un movimiento {{moveCategory}}!\nLo siento, pero no puedo darte nada.$Vamos niños, encontraremos una mejor demostración en otro lugar.", + "incorrect_exp": "Parece que has aprendido una valiosa lección.$Tu Pokémon también ganó algo de experiencia.", + "correct": "¡Muchas gracias por tu amabilidad!\n¡Espero que estos objetos te sean de utilidad!", + "correct_exp": "{{pokeName}} también ganó una valiosa experiencia!", + "status": "Estado", + "physical": "Físico", + "special": "Especial" +} \ No newline at end of file diff --git a/src/locales/es/mystery-encounters/fiery-fallout-dialogue.json b/src/locales/es/mystery-encounters/fiery-fallout-dialogue.json new file mode 100644 index 00000000000..272544cba9e --- /dev/null +++ b/src/locales/es/mystery-encounters/fiery-fallout-dialogue.json @@ -0,0 +1,26 @@ +{ + "intro": "¡Te encuentras con una tormenta abrasadora de humo y ceniza!", + "title": "¡Pasión Ardiente!", + "description": "La ceniza y las brasas han reducido la visibilidad a casi cero. Parece que podría haber alguna… fuente que esté causando estas condiciones. Pero, ¿qué podría estar detrás de un fenómeno de esta magnitud?", + "query": "¿Qué harás?", + "option": { + "1": { + "label": "Encontrar la causa", + "tooltip": "(?) Encunetra la causa\n(-) Batalla Difícil", + "selected": "¡Empujas a través de la tormenta y encuentras a dos {{volcaronaName}}s en medio de una danza de apareamiento!$¡No toman bien la interrupción y atacan!" + }, + "2": { + "label": "Refugiarse", + "tooltip": "(-) Sufre los efectos del clima", + "selected": "Los efectos del clima causan un daño significativo mientras luchas por encontrar refugio. ¡Tu grupo pierde el 20% de su HP máximo!", + "target_burned": "¡Tu {{burnedPokemon}} también acabo quemándose!" + }, + "3": { + "label": "Tus tipos de fuego ayudan", + "tooltip": "(+) Paras las condiciones del clima\n(+) Obtén un Carbón", + "disabled_tooltip": "Necesitas al menos 2 Pokémon de tipo Fuego para elegir esto", + "selected": "Tus {{option3PrimaryName}} y {{option3SecondaryName}} te guían hasta donde dos {{volcaronaName}}s están en medio de una danza de apareamiento.$Afortunadamente, tus Pokémon logran calmarlos y se van sin problemas." + } + }, + "found_charcoal": "Después de que el clima se despeja,\ntu {{leadPokemon}} ve algo en el suelo. $¡@s{item_fanfare}{{leadPokemon}} obtuvo un Carbón!" +} \ No newline at end of file diff --git a/src/locales/es/mystery-encounters/fight-or-flight-dialogue.json b/src/locales/es/mystery-encounters/fight-or-flight-dialogue.json new file mode 100644 index 00000000000..33ccc09e1c6 --- /dev/null +++ b/src/locales/es/mystery-encounters/fight-or-flight-dialogue.json @@ -0,0 +1,25 @@ +{ + "intro": "¡Algo brillante está reluciendo\nen el suelo cerca de ese Pokémon!", + "title": "Lucha o huye", + "description": "Parece que hay un Pokémon fuerte protegiendo un objeto. Lucharlo es el enfoque más directo, pero parece fuerte. Quizás podrías robar el objeto, si tienes el Pokémon adecuado para el trabajo.", + "query": "¿Qué harás?", + "option": { + "1": { + "label": "Enfréntarse al Pokémon", + "tooltip": "(-) Batalla Difícil\n(+) Obtén un objeto", + "selected": "Te acercas al\nPokémon sin miedo.", + "stat_boost": "¡La fuerza latente de {{enemyPokemon}} aumentó una de sus estadísticas!" + }, + "2": { + "label": "Robar el objeto", + "disabled_tooltip": "Tus Pokémon necesitan conocer ciertos movimientos para elegir esto", + "tooltip": "(+) {{option2PrimaryName}} usa {{option2PrimaryMove}}", + "selected": ".@d{32}.@d{32}.@d{32}$Tu {{option2PrimaryName}} te ayuda y usa {{option2PrimaryMove}}!$¡Agarraste el obejto!" + }, + "3": { + "label": "Irse", + "tooltip": "(-) Ninguna recompensa", + "selected": "Dejas al Pokémon fuerte\ncon su premio y continúas." + } + } +} \ No newline at end of file diff --git a/src/locales/es/mystery-encounters/fun-and-games-dialogue.json b/src/locales/es/mystery-encounters/fun-and-games-dialogue.json new file mode 100644 index 00000000000..bbaa52996ad --- /dev/null +++ b/src/locales/es/mystery-encounters/fun-and-games-dialogue.json @@ -0,0 +1,30 @@ +{ + "intro_dialogue": "¡Pasen y vean, amigos! Prueben su suerte en el nuevo {{wobbuffetName}} Golpe-o-matic!", + "speaker": "El Feriante", + "title": "Juegos y diversión", + "description": "¡Te has encontrado con una feria ambulante con un juego de premios! Tendrás @[TOOLTIP_TITLE]{3 turnos} para dejar al {{wobbuffetName}} lo más cerca posible de @[TOOLTIP_TITLE]{1 PS} @[TOOLTIP_TITLE]{sin debilitarlo} para que pueda cargar un gran Contraataque en la máquina de golpear la campana. ¡Pero ten cuidado! Si debilitas al {{wobbuffetName}}, tendrás que pagar el coste de revivirlo.", + "query": "¿Te gustaría jugar?", + "option": { + "1": { + "label": "Jugar el juego", + "tooltip": "(-) Paga {{option1Money, money}}\n(+) Juega al {{wobbuffetName}} Golpe-o-matic", + "selected": "¡Es hora de probar tu suerte!" + }, + "2": { + "label": "Irse", + "tooltip": "(-) Ninguna Recompensa", + "selected": "Te apresuras en irte, con una ligera sensación de arrepentimiento." + } + }, + "ko": "¡Oh no! ¡El {{wobbuffetName}} se debilitó!$Pierdes el juego y\ntienes que pagar el coste de revivirlo...", + "charging_continue": "¡El Wobbuffet sigue cargando su contraataque!", + "turn_remaining_3": "¡Quedan tres turnos!", + "turn_remaining_2": "¡Quedan dos turnos!", + "turn_remaining_1": "¡Queda un turno!", + "end_game": "¡Se acabó el tiempo!$El {{wobbuffetName}} se prepara para el contraataque y@d{16}.@d{16}.@d{16}.", + "best_result": "¡El {{wobbuffetName}} golpea el botón con tanta fuerza\nque la campana se rompe!$¡Ganas el gran premio!", + "great_result": "¡El {{wobbuffetName}} golpea el botón, casi alcanzando la campana!$¡Tan cerca!\n¡Ganas el segundo premio!", + "good_result": "¡El {{wobbuffetName}} golpea el botón con suficiente fuerza para llegar a la mitad de la escala!$¡Ganas el tercer premio!", + "bad_result": "El {{wobbuffetName}} apenas toca el botón y no pasa nada…$¡Oh no!\nNo ganas nada.", + "outro": "¡Ese fue un juego muy divertido!" +} \ No newline at end of file diff --git a/src/locales/es/mystery-encounters/global-trade-system-dialogue.json b/src/locales/es/mystery-encounters/global-trade-system-dialogue.json new file mode 100644 index 00000000000..b2598007b0d --- /dev/null +++ b/src/locales/es/mystery-encounters/global-trade-system-dialogue.json @@ -0,0 +1,32 @@ +{ + "intro": "¡Es una interfaz para la Estación de Intercambio Global, la GTS!", + "title": "La GTS", + "description": "¡Ah, la GTS! Una maravilla tecnológica, puedes conectarte con cualquier persona en todo el mundo para intercambiar Pokémon con ellos. ¿Tendrás suerte en tu intercambio hoy?", + "query": "¿Qué harás?", + "option": { + "1": { + "label": "Ver ofertas de intercambio", + "tooltip": "(+) Selecciona una oferta de intercambio para uno de tus Pokémon", + "trade_options_prompt": "Selecciona un Pokémon para recibir a través del intercambio." + }, + "2": { + "label": "Intercambio Prodigioso", + "tooltip": "(+) Envía uno de tus Pokémon a la GTS y recibe un Pokémon aleatorio a cambio." + }, + "3": { + "label": "Intercambia un objeto", + "trade_options_prompt": "Selecciona un objeto para enviar.", + "invalid_selection": "Este Pokémon no tiene objetos legales para intercambiar.", + "tooltip": "(+) Envía uno de tus objetos a la GTS y recibe un nuevo objeto aleatorio." + }, + "4": { + "label": "Irse", + "tooltip": "(-) Ninguna Recompensa", + "selected": "¡No hay tiempo para intercambiar hoy!\nSigues adelante." + } + }, + "pokemon_trade_selected": "{{tradedPokemon}} será enviado a {{tradeTrainerName}}.", + "pokemon_trade_goodbye": "¡Adiós, {{tradedPokemon}}!", + "item_trade_selected": "{{chosenItem}} será enviado a {{tradeTrainerName}}.$.@d{64}.@d{64}.@d{64}\n@s{level_up_fanfare}¡Intercambio completo!$¡Has recibido un {{itemName}} de {{tradeTrainerName}}!", + "trade_received": "¡@s{evolution_fanfare}{{tradeTrainerName}} envió a {{received}}!" +} diff --git a/src/locales/es/mystery-encounters/lost-at-sea-dialogue.json b/src/locales/es/mystery-encounters/lost-at-sea-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/es/mystery-encounters/lost-at-sea-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/es/mystery-encounters/mysterious-challengers-dialogue.json b/src/locales/es/mystery-encounters/mysterious-challengers-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/es/mystery-encounters/mysterious-challengers-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/es/mystery-encounters/mysterious-chest-dialogue.json b/src/locales/es/mystery-encounters/mysterious-chest-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/es/mystery-encounters/mysterious-chest-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/es/mystery-encounters/part-timer-dialogue.json b/src/locales/es/mystery-encounters/part-timer-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/es/mystery-encounters/part-timer-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/es/mystery-encounters/safari-zone-dialogue.json b/src/locales/es/mystery-encounters/safari-zone-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/es/mystery-encounters/safari-zone-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/es/mystery-encounters/shady-vitamin-dealer-dialogue.json b/src/locales/es/mystery-encounters/shady-vitamin-dealer-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/es/mystery-encounters/shady-vitamin-dealer-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/es/mystery-encounters/slumbering-snorlax-dialogue.json b/src/locales/es/mystery-encounters/slumbering-snorlax-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/es/mystery-encounters/slumbering-snorlax-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/es/mystery-encounters/teleporting-hijinks-dialogue.json b/src/locales/es/mystery-encounters/teleporting-hijinks-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/es/mystery-encounters/teleporting-hijinks-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/es/mystery-encounters/the-expert-pokemon-breeder-dialogue.json b/src/locales/es/mystery-encounters/the-expert-pokemon-breeder-dialogue.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/src/locales/es/mystery-encounters/the-expert-pokemon-breeder-dialogue.json @@ -0,0 +1 @@ +{} diff --git a/src/locales/es/mystery-encounters/the-pokemon-salesman-dialogue.json b/src/locales/es/mystery-encounters/the-pokemon-salesman-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/es/mystery-encounters/the-pokemon-salesman-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/es/mystery-encounters/the-strong-stuff-dialogue.json b/src/locales/es/mystery-encounters/the-strong-stuff-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/es/mystery-encounters/the-strong-stuff-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/es/mystery-encounters/the-winstrate-challenge-dialogue.json b/src/locales/es/mystery-encounters/the-winstrate-challenge-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/es/mystery-encounters/the-winstrate-challenge-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/es/mystery-encounters/training-session-dialogue.json b/src/locales/es/mystery-encounters/training-session-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/es/mystery-encounters/training-session-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/es/mystery-encounters/trash-to-treasure-dialogue.json b/src/locales/es/mystery-encounters/trash-to-treasure-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/es/mystery-encounters/trash-to-treasure-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/es/mystery-encounters/uncommon-breed-dialogue.json b/src/locales/es/mystery-encounters/uncommon-breed-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/es/mystery-encounters/uncommon-breed-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/es/mystery-encounters/weird-dream-dialogue.json b/src/locales/es/mystery-encounters/weird-dream-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/es/mystery-encounters/weird-dream-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/es/party-ui-handler.json b/src/locales/es/party-ui-handler.json index 0e59aee6fd1..780ef633297 100644 --- a/src/locales/es/party-ui-handler.json +++ b/src/locales/es/party-ui-handler.json @@ -15,6 +15,7 @@ "UNPAUSE_EVOLUTION": "Reanudar evolución", "REVIVE": "Revivir", "RENAME": "Rename", + "SELECT": "Select", "choosePokemon": "Elige a un Pokémon.", "doWhatWithThisPokemon": "¿Que quieres hacer con este Pokémon?", "noEnergy": "¡A {{pokemonName}} no le\nquedan fuerzas para luchar!", 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-info-container.json b/src/locales/es/pokemon-info-container.json index 7159c7eae71..6d120d4ad40 100644 --- a/src/locales/es/pokemon-info-container.json +++ b/src/locales/es/pokemon-info-container.json @@ -2,5 +2,6 @@ "moveset": "Movimientos", "gender": "Género:", "ability": "Habilid:", - "nature": "Natur:" -} \ No newline at end of file + "nature": "Natur:", + "form": "Forma:" +} 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/starter-select-ui-handler.json b/src/locales/es/starter-select-ui-handler.json index 69cc937d4e3..9eb8efb1151 100644 --- a/src/locales/es/starter-select-ui-handler.json +++ b/src/locales/es/starter-select-ui-handler.json @@ -1,5 +1,6 @@ { "confirmStartTeam": "¿Comenzar con estos Pokémon?", + "confirmExit": "¿Quieres salir?", "invalidParty": "¡Este equipo no es válido!", "gen1": "I", "gen2": "II", @@ -11,21 +12,24 @@ "gen8": "VIII", "gen9": "IX", "growthRate": "Crecimiento:", - "ability": "Habilid:", + "ability": "Habili:", "passive": "Pasiva:", "nature": "Natur:", "eggMoves": "Mov. Huevo", "addToParty": "Añadir al Equipo", - "removeFromParty": "Excluir del Equipo", + "removeFromParty": "Quitar del Equipo", "toggleIVs": "Mostrar IVs", "manageMoves": "Cambiar movs.", "manageNature": "Cambiar natur.", + "addToFavorites": "Añadir a Favoritos", + "removeFromFavorites": "Quitar de Favoritos", "useCandies": "Usar Caramelos", "selectNature": "Elige Natur.", "selectMoveSwapOut": "Elige el movimiento que sustituir.", "selectMoveSwapWith": "Elige el movimiento que sustituirá a", "unlockPassive": "Añadir Pasiva", "reduceCost": "Reducir Coste", + "sameSpeciesEgg": "Comprar Huevo", "cycleShiny": ": Shiny", "cycleForm": ": Forma", "cycleGender": ": Género", @@ -38,4 +42,4 @@ "locked": "Bloqueado", "disabled": "No disponible", "uncaught": "No capturado" -} \ No newline at end of file +} diff --git a/src/locales/es/status-effect.json b/src/locales/es/status-effect.json index eeb8c251e8a..9673a6a495b 100644 --- a/src/locales/es/status-effect.json +++ b/src/locales/es/status-effect.json @@ -1,12 +1,6 @@ { "none": { - "name": "Ninguno", - "description": "", - "obtain": "", - "obtainSource": "", - "activation": "", - "overlap": "", - "heal": "" + "name": "Ninguno" }, "poison": { "name": "Envenenamiento", @@ -40,7 +34,7 @@ "description": "dormir", "obtain": "¡{{pokemonNameWithAffix}}\nse ha dormido!", "obtainSource": "¡{{pokemonNameWithAffix}}\nse ha dormido\npor culpa de {{sourceText}}!", - "activation": "¡{{pokemonNameWithAffix}} está/ndormido como un tronco.", + "activation": "¡{{pokemonNameWithAffix}} está\ndormido como un tronco.", "overlap": "¡{{pokemonNameWithAffix}} ya\nestá dormido!", "heal": "¡{{pokemonNameWithAffix}} se despertó!" }, @@ -62,4 +56,4 @@ "overlap": "¡{{pokemonNameWithAffix}} ya\nestá quemado!", "heal": "¡{{pokemonNameWithAffix}} ya no\nestá quemado!" } -} \ No newline at end of file +} diff --git a/src/locales/es/trainer-classes.json b/src/locales/es/trainer-classes.json index 0677193e4f8..6a85e1bf83d 100644 --- a/src/locales/es/trainer-classes.json +++ b/src/locales/es/trainer-classes.json @@ -1,7 +1,7 @@ { - "ace_trainer": "Entrenador Guay", - "ace_trainer_female": "Entrenadora Guay", - "ace_duo": "Pareja Guay", + "ace_trainer": "Entrenador guay", + "ace_trainer_female": "Entrenadora guay", + "ace_duo": "Pareja guay", "artist": "Artista", "artist_female": "Artista", "backers": "Hinchas", @@ -19,7 +19,7 @@ "breeders": "Criadores", "clerk": "Empresario", "clerk_female": "Oficinista", - "colleagues": "Colegas Oficina", + "colleagues": "Colegas oficina", "crush_kin": "Luchadores", "cyclist": "Ciclista", "cyclist_female": "Ciclista", @@ -46,13 +46,13 @@ "linebacker": "Quarterback", "maid": "Criada", "madame": "Señora", - "medical_team": "Equipo Médico", + "medical_team": "Equipo médico", "musician": "Cantautor", "hex_maniac": "Bruja", "nurse": "Enfermera", "nursery_aide": "Seño", "officer": "Policía", - "parasol_lady": "Dama Parasol", + "parasol_lady": "Dama parasol", "pilot": "Piloto", "pokéfan": "Pokéfan", "pokéfan_female": "Pokéfan", @@ -63,22 +63,26 @@ "psychic": "Médium", "psychic_female": "Mentalista", "psychics": "Pareja Médium", + "pokémon_ranger": "Pokémon Ranger", + "pokémon_ranger_female": "Pokéguarda", + "pokémon_rangers": "Pokéguardas", + "ranger": "Guarda", "restaurant_staff": "Personal Restaurante", "rich": "Bien", "rich_female": "Bien", - "rich_boy": "Niño Bien", - "rich_couple": "Pareja Bien", - "rich_kid": "Niño Bien", - "rich_kid_female": "Niña Bien", - "rich_kids": "Niños Bien", + "rich_boy": "Niño bien", + "rich_couple": "Pareja bien", + "rich_kid": "Niño bien", + "rich_kid_female": "Niña bien", + "rich_kids": "Niños bien", "roughneck": "Calvo", "sailor": "Marinero", "scientist": "Científico", "scientist_female": "Científica", "scientists": "Científicos", "smasher": "Tenista", - "snow_worker": "Operario Nieve", - "snow_worker_female": "Operaria Nieve", + "snow_worker": "Operario nieve", + "snow_worker_female": "Operaria nieve", "striker": "Delantero", "school_kid": "Colegial", "school_kid_female": "Colegial", @@ -89,14 +93,41 @@ "twins": "Gemelas", "veteran": "Veterano", "veteran_female": "Veterana", - "veteran_duo": "Dúo Veterano", + "veteran_duo": "Dúo veterano", "waiter": "Camarero", "waitress": "Camarera", "worker": "Operario", "worker_female": "Operaria", "workers": "Operarios", "youngster": "Joven", + "rocket_grunt": "Recluta Rocket", + "rocket_grunts": "Reclutas Rocket", + "rocket_grunt_female": "Recluta Rocket", + "magma_grunt": "Recluta Magma", + "magma_grunt_female": "Recluta Magma", + "magma_grunts": "Reclutas Magma", + "aqua_grunt": "Recluta Aqua", + "aqua_grunt_female": "Recluta Aqua", + "aqua_grunts": "Reclutas Aqua", + "galactic_grunt": "Recluta Galaxia", + "galactic_grunt_female": "Recluta Galaxia", + "galactic_grunts": "Reclutas Galaxia", + "plasma_grunt": "Recluta Plasma", + "plasma_grunt_female": "Recluta Plasma", + "plasma_grunts": "Reclutas Plasma", + "flare_grunt": "Recluta Flare", + "flare_grunt_female": "Recluta Flare", + "flare_grunts": "Reclutas Flare", "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", + "skull_grunt": "Recluta Skull", + "skull_grunt_female": "Recluta Skull", + "skull_grunts": "Reclutas Skull", + "macro_grunt": "Entrenador Macrocosmos", + "macro_grunt_female": "Entrenadora Macrocosmos", + "macro_grunts": "Entrenadores Macrocosmos", + "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..adef8173687 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", @@ -154,5 +167,18 @@ "alder_iris_double": "Mirto e Iris", "iris_alder_double": "Iris y Mirto", "marnie_piers_double": "Roxy y Nerio", - "piers_marnie_double": "Nerio y Roxy" + "piers_marnie_double": "Nerio y Roxy", + + "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/es/trainer-titles.json b/src/locales/es/trainer-titles.json index 5bee9fc8c51..34fef949e87 100644 --- a/src/locales/es/trainer-titles.json +++ b/src/locales/es/trainer-titles.json @@ -3,32 +3,40 @@ "elite_four_female": "Alto Mando", "gym_leader": "Líder de gimnasio", "gym_leader_female": "Líder de gimnasio", - "gym_leader_double": "Líderes de Gimnasio", + "gym_leader_double": "Líderes de gimnasio", "champion": "Campeón", "champion_female": "Campeona", "champion_double": "Campeones", "rival": "Rival", "professor": "Profesor", "frontier_brain": "As del Frente Batalla", - "rocket_boss": "Team Rocket Boss", - "magma_boss": "Team Magma Boss", - "aqua_boss": "Team Aqua Boss", - "galactic_boss": "Team Galactic Boss", - "plasma_boss": "Team Plasma Boss", - "flare_boss": "Team Flare Boss", + "rocket_boss": "Jefe del Team Rocket", + "magma_boss": "Jefe del Equipo Magma", + "aqua_boss": "Jefe del Equipo Aqua", + "galactic_boss": "Jefe del Equipo Galaxia", + "plasma_boss": "Jefe del Equipo Plasma", + "flare_boss": "Jefe del Team Flare", "aether_boss": "Presidente Æther", + "skull_boss": "Jefe del Team Skull", + "macro_boss": "Presidente de Macrocosmos", + "star_boss": "Team Star Leader", - "rocket_admin": "Team Rocket Admin", - "rocket_admin_female": "Team Rocket Admin", - "magma_admin": "Team Magma Admin", - "magma_admin_female": "Team Magma Admin", - "aqua_admin": "Team Aqua Admin", - "aqua_admin_female": "Team Aqua Admin", - "galactic_commander": "Team Galactic Commander", - "galactic_commander_female": "Team Galactic Commander", - "plasma_sage": "Team Plasma Sage", - "plasma_admin": "Team Plasma Admin", - "flare_admin": "Team Flare Admin", - "flare_admin_female": "Team Flare Admin", - "aether_admin": "Director de la Fundación Æther" + "rocket_admin": "Admin. del Team Rocket", + "rocket_admin_female": "Admin. del Team Rocket", + "magma_admin": "Admin. del Equipo Magma", + "magma_admin_female": "Admin. del Equipo Magma", + "aqua_admin": "Admin. del Equipo Aqua", + "aqua_admin_female": "Admin. del Equipo Aqua", + "galactic_commander": "Comand. del Equipo Galaxia", + "galactic_commander_female": "Comand. del Equipo Galaxia", + "plasma_sage": "Sabio del Equipo Plasma", + "plasma_admin": "Admin. del Equipo Plasma", + "flare_admin": "Admin. del Team Flare", + "flare_admin_female": "Admin. del Team Flare", + "aether_admin": "Director de la Fundación Æther", + "skull_admin": "Admin. del Team Skull", + "macro_admin": "Admin. de Macrocosmos", + "star_admin": "Team Star Squad Boss", + + "the_winstrates": "Familia Estratega" } diff --git a/src/locales/es/tutorial.json b/src/locales/es/tutorial.json index e19dbcf09ad..f03d1e216e4 100644 --- a/src/locales/es/tutorial.json +++ b/src/locales/es/tutorial.json @@ -1,5 +1,5 @@ { - "intro": "¡Bienvenido/a a PokéRogue! Este es un fangame de Pokémon centrado en el combate con elementos roguelite.\n$Este juego no está monetizado y no reclamamos ningún derecho de propiedad sobre Pokémon ni sobre ninguno de\n$los recursos con copyright utilizados.\n$El juego está en desarrollo, pero es completamente jugable.\nPara reportar bugs, por favor, hazlo en nuestra\n$comunidad de Discord.\n$Si el juego va lento, por favor, asegúrate de que tengas activada la opción 'Aceleración de gráficos' en los\n$ajustes de tu navegador.", + "intro": "¡Bienvenido/a a PokéRogue! Este es un fangame de Pokémon centrado en el combate con elementos roguelite.\n$Este juego no está monetizado y no reclamamos ningún derecho de propiedad sobre Pokémon ni sobre ninguno de\n$los recursos con copyright utilizados.\n$El juego está en desarrollo, pero es completamente jugable.\n$Por favor, reporta los errores que veas en nuestro Discord oficial.\n$Si el juego va lento, por favor, asegúrate de que tengas activada la opción 'Aceleración de gráficos' en los\n$ajustes de tu navegador.", "accessMenu": "Para acceder al menú, pulsa M o Escape cuando\ntengas el control.\n$El menú contiene los ajustes y otras funciones.", "menu": "Desde este menú podrás acceder a los ajustes.\n$Podrás cambiar la velocidad del juego, el estilo de la ventana y demás.\n$Hay más opciones, ¡así que pruébalas todas!", "starterSelect": "En esta pantalla, podrás elegir tus iniciales presionando Z\no Espacio. Estos serán tus miembros de equipo al comenzar.\n$Cada inicial tiene un valor. Tu equipo puede contener hasta 6\nmiembros mientras el valor total no pase de 10.\n$También puedes elegir su género, habilidad y forma\ndependiendo de las variantes que hayas conseguido.\n$Los IVs de los iniciales corresponderán al valor más alto de\nlos Pokémon de la misma especie que hayas obtenido.\n$¡Así que intenta conseguir muchos Pokémon de la misma\nespecie!", @@ -7,4 +7,4 @@ "statChange": "Los cambios de estadísticas se mantienen entre combates\nmientras que el Pokémon no vuelva a su Poké Ball.\n$Tus Pokémon vuelven a sus Poké Balls antes de combates contra entrenadores y de entrar a un nuevo bioma.\n$También puedes ver los cambios de estadísticas del Pokémon en campo manteniendo pulsado C o Shift.\n$También puedes ver los movimientos de un Pokémon enemigo manteniendo presionada la V.\n$Esto solo revela los movimientos que has visto usar al Pokémon en esta combate.", "selectItem": "Tras cada combate, tendrás la opción de elegir entre tres objetos aleatorios. Solo podrás escoger uno.\n$Estos objetos pueden ser consumibles, objetos equipables u objetos pasivos permanentes (hasta acabar la partida).\n$La mayoría de los efectos de objetos no consumibles se acumularán de varias maneras.\n$Algunos objetos solo aparecerán si pueden ser utilizados, como las piedras evolutivas.\n$También puedes transferir objetos equipados entre Pokémon, utilizando la opción de transferir.\n$La opción de transferir aparecerá en la parte inferior derecha una vez hayas obtenido un objeto equipable.\n$También puedes comprar objetos consumibles con dinero y su variedad irá aumentando según tu avance.\n$Asegúrate de comprar antes de escoger una recompensa, ya que se avanzará automáticamente al siguiente combate.", "eggGacha": "En esta pantalla podrás canjear tus vales por huevos\nde Pokémon.\n$Los huevos deben eclosionar y estarán más cerca de\nhacerlo tras cada combate.\n$Los huevos más raros tardarán más en eclosionar.\n$Los Pokémon que hayan salido del huevo no se\nañadirán a tu equipo, pero sí a tus iniciales.\n$Los Pokémon salidos de un huevo suelen tener mejores\nIVs que los Pokémon salvajes.\n$Algunos Pokémon solo pueden ser obtenidos de huevos.\n$Hay 3 máquinas diferentes entre las que elegir, cada\nuna con diferentes bonificaciones.\n$¡Así que escoge la que más te interese!" -} \ No newline at end of file +} 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/achv.json b/src/locales/fr/achv.json index a557a423db7..175ddfc404c 100644 --- a/src/locales/fr/achv.json +++ b/src/locales/fr/achv.json @@ -278,5 +278,9 @@ "INVERSE_BATTLE": { "name": "La teuté à verlan", "description": "Terminer un challenge en Combat Inversé.\nMineter un lenjcha en Ba-con Versin." + }, + "BREEDERS_IN_SPACE": { + "name": "Éleveurs de l'espace !", + "description": "Vaincre l'Éleveuse Experte dans le biome Espace." } } diff --git a/src/locales/fr/battle.json b/src/locales/fr/battle.json index 7b78c963187..26b46f77e9d 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,11 +48,15 @@ "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 !", "noPokeballStrong": "Le Pokémon est trop fort pour être capturé !\nVous devez d’abord l’affaiblir !", + "noPokeballMysteryEncounter": "Vous ne pouvez pas\nattraper ce Pokémon !", "noEscapeForce": "Une force mystérieuse\nempêche la fuite.", "noEscapeTrainer": "On ne s’enfuit pas d’un\ncombat de Dresseurs !", "noEscapePokemon": "{{moveName}} de {{pokemonName}}\nempêche {{escapeVerb}} !", @@ -62,6 +70,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", @@ -91,10 +100,12 @@ "statWontGoAnyLower_one": "{{stats}} de {{pokemonNameWithAffix}}\nne peut plus baisser !", "statWontGoAnyLower_other": "{{stats}}\nde {{pokemonNameWithAffix}} ne peuvent plus baisser !", "transformedIntoType": "{{pokemonName}} prend\nle type {{type}} !", - "ppReduced": "Les PP de la capacité {{moveName}}\nde {{targetName}} baissent de {{reduction}} !", "retryBattle": "Voulez-vous réessayer depuis le début du combat ?", "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}} !", + "mysteryEncounterAppeared": "Hein ?\nC’est quoi ça ?", + "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 4c5c7ea0df6..93b70490ed6 100644 --- a/src/locales/fr/battler-tags.json +++ b/src/locales/fr/battler-tags.json @@ -7,7 +7,7 @@ "nightmareDesc": "les cauchemars", "ingrainDesc": "l’enracinement", "drowsyDesc": "la somnolence", - "rechargingLapse": "Le contrecoup empêche {{pokemonNameWithAffix}}\n de bouger !", + "rechargingLapse": "Le contrecoup empêche {{pokemonNameWithAffix}}\nde bouger !", "trappedOnAdd": "{{pokemonNameWithAffix}}\nne peut plus s’échapper !", "trappedOnRemove": "{{pokemonNameWithAffix}} est libéré\nde la capacité {{moveName}} !", "flinchedLapse": "{{pokemonNameWithAffix}} a la trouille !\nIl ne peut plus attaquer !", diff --git a/src/locales/fr/bgm-name.json b/src/locales/fr/bgm-name.json index ecf0075e79d..c2a0de4d230 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)", @@ -146,5 +149,11 @@ "encounter_youngster": "NB - Regards croisés (Gamin)", "heal": "NB - Soin de Pokémon", "menu": "PDM EdC - Bienvenue dans le monde de Pokémon !", - "title": "PDM EdC - Menu Principal" + "title": "PDM EdC - Menu Principal", + + "mystery_encounter_weird_dream": "PDM EdC - Aiguille du Temps", + "mystery_encounter_fun_and_games": "PDM EdC - Maitre Grodoudou", + "mystery_encounter_gen_5_gts": "NB - GTS", + "mystery_encounter_gen_6_gts": "XY - GTS", + "mystery_encounter_delibirdy": "Firel - DeliDelivery!" } diff --git a/src/locales/fr/config.ts b/src/locales/fr/config.ts index f79374cd3b9..d4c75a0ce24 100644 --- a/src/locales/fr/config.ts +++ b/src/locales/fr/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 "./mystery-encounters/mysterious-chest-dialogue.json"; +import mysteriousChallengers from "./mystery-encounters/mysterious-challengers-dialogue.json"; +import darkDeal from "./mystery-encounters/dark-deal-dialogue.json"; +import departmentStoreSale from "./mystery-encounters/department-store-sale-dialogue.json"; +import fieldTrip from "./mystery-encounters/field-trip-dialogue.json"; +import fieryFallout from "./mystery-encounters/fiery-fallout-dialogue.json"; +import fightOrFlight from "./mystery-encounters/fight-or-flight-dialogue.json"; +import safariZone from "./mystery-encounters/safari-zone-dialogue.json"; +import shadyVitaminDealer from "./mystery-encounters/shady-vitamin-dealer-dialogue.json"; +import slumberingSnorlax from "./mystery-encounters/slumbering-snorlax-dialogue.json"; +import trainingSession from "./mystery-encounters/training-session-dialogue.json"; +import theStrongStuff from "./mystery-encounters/the-strong-stuff-dialogue.json"; +import pokemonSalesman from "./mystery-encounters/the-pokemon-salesman-dialogue.json"; +import offerYouCantRefuse from "./mystery-encounters/an-offer-you-cant-refuse-dialogue.json"; +import delibirdy from "./mystery-encounters/delibirdy-dialogue.json"; +import absoluteAvarice from "./mystery-encounters/absolute-avarice-dialogue.json"; +import aTrainersTest from "./mystery-encounters/a-trainers-test-dialogue.json"; +import trashToTreasure from "./mystery-encounters/trash-to-treasure-dialogue.json"; +import berriesAbound from "./mystery-encounters/berries-abound-dialogue.json"; +import clowningAround from "./mystery-encounters/clowning-around-dialogue.json"; +import partTimer from "./mystery-encounters/part-timer-dialogue.json"; +import dancingLessons from "./mystery-encounters/dancing-lessons-dialogue.json"; +import weirdDream from "./mystery-encounters/weird-dream-dialogue.json"; +import theWinstrateChallenge from "./mystery-encounters/the-winstrate-challenge-dialogue.json"; +import teleportingHijinks from "./mystery-encounters/teleporting-hijinks-dialogue.json"; +import bugTypeSuperfan from "./mystery-encounters/bug-type-superfan-dialogue.json"; +import funAndGames from "./mystery-encounters/fun-and-games-dialogue.json"; +import uncommonBreed from "./mystery-encounters/uncommon-breed-dialogue.json"; +import globalTradeSystem from "./mystery-encounters/global-trade-system-dialogue.json"; +import expertPokemonBreeder from "./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 frConfig = { ability, abilityTriggers, @@ -110,4 +152,40 @@ export const frConfig = { modifierSelectUiHandler, moveTriggers, 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/fr/dialogue.json b/src/locales/fr/dialogue.json index adc58de0563..3b362fc0c2c 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,138 @@ "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." + } + }, + "stat_trainer_buck": { + "encounter": { + "1": "Je te préviens… Je suis sacrément coriace ! Fais comme si tu ne t’y attendais pas !", + "2": "Je sens mes Pokémon gigoter dans leurs Poké Balls !" + }, + "victory": { + "1": "Hé, hé, hé !\nTu mets le feu !", + "2": "Hé, hé, hé !\nTu mets le feu !" + }, + "defeat": { + "1": "Oh ?\nEn panne de carburant je suppose ?", + "2": "Oh ?\nEn panne de carburant je suppose ?" + } + }, + "stat_trainer_cheryl": { + "encounter": { + "1": "Mes Pokémon meurent d’envie de se battre.", + "2": "Je dois te prévenir que mes Pokémon peuvent se montrer un peu exubérants." + }, + "victory": { + "1": "Trouver le bon équilibre en attaque et défense…\nCe n’est pas la chose la plus aisée.", + "2": "Trouver le bon équilibre en attaque et défense…\nCe n’est pas la chose la plus aisée." + }, + "defeat": { + "1": "T’as besoin que je soigne ton équipe ?", + "2": "T’as besoin que je soigne ton équipe ?" + } + }, + "stat_trainer_marley": { + "encounter": { + "1": "OK…\nJe ferai de mon mieux.", + "2": "OK…\nJe… Je peux le faire… !" + }, + "victory": { + "1": "Argh…", + "2": "Argh…" + }, + "defeat": { + "1": "Au revoir…", + "2": "Au revoir…" + } + }, + "stat_trainer_mira": { + "encounter": { + "1": "Maïté va t’éblouir !", + "2": "Je vais te prouver que je ne me mélange plus les pédales !" + }, + "victory": { + "1": "Hum… Je me demande si je suis à la hauteur de ici…", + "2": "Hum… Je me demande si je suis à la hauteur de ici…" + }, + "defeat": { + "1": "Maïté savait qu’elle t’éblouirait !", + "2": "Maïté savait qu’elle t’éblouirait !" + } + }, + "stat_trainer_riley": { + "encounter": { + "1": "Le combat, c’est une façon de se saluer pour nous !", + "2": "On va mettre le paquet pour venir à bout de ton équipe." + }, + "victory": { + "1": "Parfois, on fait équipe…\nEt parfois, on s’affronte…$La vie des Dresseurs est pleine de rebondissements.", + "2": "Parfois, on fait équipe…\nEt parfois, on s’affronte…$La vie des Dresseurs est pleine de rebondissements.." + }, + "defeat": { + "1": "Tu t’en tire bien.\nTu feras mieux la prochaine fois.", + "2": "Tu t’en tire bien.\nTu feras mieux la prochaine fois." + } + }, + "winstrates_victor": { + "encounter": { + "1": "Bon esprit !\nJ’aime ça !" + }, + "victory": { + "1": "Mince !\nTu as un meilleur niveau que je ne le pensais !" + } + }, + "winstrates_victoria": { + "encounter": { + "1": "Oh, ciel !\nCe que tu es jeune !$Mais si tu as battu mon mari,\nc’est que tu sais t’y prendre.$À nous deux, maintenant !" + }, + "victory": { + "1": "Ciel ! Cette force !\nJ’en suis toute retournée !" + } + }, + "winstrates_vivi": { + "encounter": { + "1": "Tu as un meilleur niveau que maman ?\nWaouh!$Mais moi aussi, je suis forte !\nVraiment ! Honnêtement !" + }, + "victory": { + "1": "Quoi ?\nNe me dis pas que j’ai perdu !" + } + }, + "winstrates_vicky": { + "encounter": { + "1": "Comment oses-tu faire pleurer\nmon adorable petite-fille !$Tu vas me le payer !\nTes Pokémon vont mordre la poussière !" + }, + "victory": { + "1": "Ouh! Quelle puissance…\nMa petite-fille avait raison…" + } + }, + "winstrates_vito": { + "encounter": { + "1": "On s’entraine tous ensemble,\navec les membres de ma famille !$Je ne perds contre personne !" + }, + "victory": { + "1": "J’ai toujours été le meilleur de la famille.\nJe n’avais encore jamais perdu…" + } + }, "brock": { "encounter": { "1": "Mon expertise des types Roche va te mettre au sol ! En garde !", diff --git a/src/locales/fr/egg.json b/src/locales/fr/egg.json index cbc912e9d50..8157baa4708 100644 --- a/src/locales/fr/egg.json +++ b/src/locales/fr/egg.json @@ -11,6 +11,7 @@ "gachaTypeLegendary": "Taux de Légendaires élevé", "gachaTypeMove": "Taux de Capacité Œuf Rare élevé", "gachaTypeShiny": "Taux de Chromatiques élevé", + "eventType": "Évènement Mystère", "selectMachine": "Sélectionnez une machine.", "notEnoughVouchers": "Vous n’avez pas assez de coupons !", "tooManyEggs": "Vous avez trop d’Œufs !", 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/modifier-select-ui-handler.json b/src/locales/fr/modifier-select-ui-handler.json index d1de2d222a8..727226d7890 100644 --- a/src/locales/fr/modifier-select-ui-handler.json +++ b/src/locales/fr/modifier-select-ui-handler.json @@ -8,5 +8,7 @@ "lockRaritiesDesc": "Assure la relance de proposer des objets gratuits de rareté égale ou supérieure. Affecte le cout de relance.", "checkTeamDesc": "Consulter votre équipe ou utiliser un objet\nde changement de forme.", "rerollCost": "{{formattedMoney}} ₽", - "itemCost": "{{formattedMoney}} ₽" -} \ No newline at end of file + "itemCost": "{{formattedMoney}} ₽", + "continueNextWaveButton": "Continuer", + "continueNextWaveDescription": "Continuer vers la prochaine vague" +} diff --git a/src/locales/fr/modifier-type.json b/src/locales/fr/modifier-type.json index 4f8033b50d7..a4bd7cabe3c 100644 --- a/src/locales/fr/modifier-type.json +++ b/src/locales/fr/modifier-type.json @@ -68,6 +68,20 @@ "BaseStatBoosterModifierType": { "description": "Augmente de 10% {{stat}} de base de son porteur. Plus les IV sont hauts, plus il peut en porter." }, + "PokemonBaseStatTotalModifierType": { + "name": "Jus de Caratroc", + "description": "{{increaseDecrease}} toutes les stats de son porteur de {{statValue}} cran. Caratroc vous a {{blessCurse}}.", + "extra": { + "increase": "Augmente", + "decrease": "Baisse", + "blessed": "accordé sa bénédiction", + "cursed": "jeté une malédiction" + } + }, + "PokemonBaseStatFlatModifierType": { + "name": "Vieux Gâteau", + "description": "Augmente de {{statValue}} {{stats}} de base du porteur. Trouvé après un étrange rêve." + }, "AllPokemonFullHpRestoreModifierType": { "description": "Restaure tous les PV de toute l’équipe." }, @@ -247,7 +261,13 @@ "ENEMY_ATTACK_BURN_CHANCE": { "name": "Jeton Brulure" }, "ENEMY_STATUS_EFFECT_HEAL_CHANCE": { "name": "Jeton Total Soin", "description": "Ajoute 2,5% de chances à chaque tour de se soigner d’un problème de statut." }, "ENEMY_ENDURE_CHANCE": { "name": "Jeton Ténacité" }, - "ENEMY_FUSED_CHANCE": { "name": "Jeton Fusion", "description": "Ajoute 1% de chances qu’un Pokémon sauvage soit une fusion." } + "ENEMY_FUSED_CHANCE": { "name": "Jeton Fusion", "description": "Ajoute 1% de chances qu’un Pokémon sauvage soit une fusion." }, + + "MYSTERY_ENCOUNTER_SHUCKLE_JUICE": { "name": "Jus de Caratroc" }, + "MYSTERY_ENCOUNTER_BLACK_SLUDGE": { "name": "Boue Noire", "description": "Sa puenteur est telle que la boutique ne vous vendra que des objets aux prix fortement augmentés." }, + "MYSTERY_ENCOUNTER_MACHO_BRACE": { "name": "Bracelet Macho", "description": "Battre un Pokémon donnera à son porteur un lot de Bracelet Macho. Chacun augmentant légèrement les stats, avec un bonus au max de lots." }, + "MYSTERY_ENCOUNTER_OLD_GATEAU": { "name": "Vieux Gâteau", "description": "Augmente de {{statValue}} {{stats}} de base du porteur." }, + "MYSTERY_ENCOUNTER_GOLDEN_BUG_NET": { "name": "Filet Doré", "description": "Impreigne son propriétaire d’une chance qui attire les Pokémon Insecte. Sa prise en main est étrange." } }, "SpeciesBoosterItem": { "LIGHT_BALL": { "name": "Balle Lumière", "description": "À faire tenir à Pikachu. Un orbe énigmatique qui double son Attaque et son Atq. Spé. ." }, diff --git a/src/locales/fr/move-trigger.json b/src/locales/fr/move-trigger.json index 3704bc90718..7564718e7ce 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 !", @@ -65,7 +66,10 @@ "suppressAbilities": "Le talent de {{pokemonName}}\na été rendu inactif !", "revivalBlessing": "{{pokemonName}} a repris connaissance\net est prêt à se battre de nouveau !", "swapArenaTags": "Les effets affectant chaque côté du terrain\nont été échangés par {{pokemonName}} !", + "chillyReception": "{{pokemonName}} s’apprête\nà faire un mauvais jeu de mots…", "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/mystery-encounter-messages.json b/src/locales/fr/mystery-encounter-messages.json new file mode 100644 index 00000000000..a4394d7d503 --- /dev/null +++ b/src/locales/fr/mystery-encounter-messages.json @@ -0,0 +1,7 @@ +{ + "paid_money": "Vous payez {{amount, number}} ₽.", + "receive_money": "Vous recevez {{amount, number}} ₽ !", + "affects_pokedex": "Affecte les données du Pokédex", + "cancel_option": "Retour au choix des options.", + "view_party_button": "Voir l’Équipe" +} diff --git a/src/locales/fr/mystery-encounters/a-trainers-test-dialogue.json b/src/locales/fr/mystery-encounters/a-trainers-test-dialogue.json new file mode 100644 index 00000000000..6040e0c4cbb --- /dev/null +++ b/src/locales/fr/mystery-encounters/a-trainers-test-dialogue.json @@ -0,0 +1,47 @@ +{ + "intro": "Quelqu’un à l’air très balèze s’apporche de vous…", + "buck": { + "intro_dialogue": "Oh, salut toi, je suis Cornil !$J’ai une bonne affaire à proposer\npour quelqu’un d’aussi costaud que toi !$J’ai deux Œufs de Pokémon sur moi,\net j’aimerais que quelqu’un prenne soin d’un.$Si t’arrives à me prouver que t’en est digne,\nje te donne l’Œuf le plus rare !", + "accept": "Hé, hé hé ! Tu mets le feu !", + "decline": "Hé ! Ton équipe\nn’a pas l’air au meilleur de sa forme.$Attends, laisse-moi t’aider." + }, + "cheryl": { + "intro_dialogue": "Bonjour, mon nom est Sara.$J’ai une requête un peu particulière à proposer,\nà quelqu’un de plutôt fort comme toi.$Je porte avec moi deux Œufs de Pokémon,\nmais j’aimerais que quelqu’un prenne soin d’un.$Si tu parviens à me démontrer toute ta force,\nje te donne l’Œuf le plus rare !", + "accept": "On y va ?", + "decline": "Je peux comprendre. Il semblerait que\nton équipe ne soit pas très en forme là.$Laisse-moi m’en occuper." + }, + "marley": { + "intro_dialogue": "Je…@d{64} je m’appelle Viviane.$Je peux te demander un truc ?…$J’ai deux Œufs de Pokémon sur moi,\net j’aimerais que quelqu’un prenne soin d’un.$Si t’arrives à me battre,\nje te donne le plus rare…", + "accept": "… D’accord.", + "decline": "… D’accord.$Tes Pokémon on l’air blessés…\nLaisse-moi les aider." + }, + "mira": { + "intro_dialogue": "Salut ! Moi, c’est Maïté!$J’ai quelque chose\nà demander à quelqu’un de fortiche comme toi !$J’ai deux Œufs de Pokémon sur moi,\net j’aimerais que quelqu’un prenne soin d’un d’eux !$Si t’acceptes de me prouver ta force,\nje te donnerai le plus rare !", + "accept": "Un combat, pour de vrai ?\nOuiiiii !", + "decline": "Oh, pas de combat ?\nPas grave !$Je vais quand même soigner ton équipe !" + }, + "riley": { + "intro_dialogue": "Enchanté, je m’appelle Armand.$J’ai une proposition un peu étrange\npour une personne de ton calibre.$Je porte deux Œufs de Pokémon avec moi\nmais j’aimerais en donner un à quelqu’un d’autre.$Si tu prouves en être digne,\nje te donnerai le plus rare !", + "accept": "Ce regard…\nBattons-nous.", + "decline": "Je comprends, ton équipe m’a l’air lessivée.$Laisse-moi t’aider." + }, + "title": "Prouver sa valeur", + "description": "Cette personne semble déterminée à vous donner un Œuf, qu’importe votre décision. Cependant, si vous parvenez à la battre, vous recevrez l’Œuf le plus rare.", + "query": "Que voulez-vous faire ?", + "option": { + "1": { + "label": "Accepter le défi", + "tooltip": "(-) Combat difficile\n(+) Gain d’un @[TOOLTIP_TITLE]{Œuf très rare}" + }, + "2": { + "label": "Décliner le défi", + "tooltip": "(+) Équipe soignée \n(+) Gain d’un @[TOOLTIP_TITLE]{Œuf}" + } + }, + "eggTypes": { + "rare": "Œuf Rare", + "epic": "Œuf Épique", + "legendary": "Œuf Légendaire" + }, + "outro": "{{statTrainerName}} vous donne un {{eggType}} !" +} diff --git a/src/locales/fr/mystery-encounters/absolute-avarice-dialogue.json b/src/locales/fr/mystery-encounters/absolute-avarice-dialogue.json new file mode 100644 index 00000000000..923ad780f30 --- /dev/null +++ b/src/locales/fr/mystery-encounters/absolute-avarice-dialogue.json @@ -0,0 +1,25 @@ +{ + "intro": "Un {{greedentName}} vous tend une embuscade\net vole les Baies de votre équipe !", + "title": "Rongrigus-ratus", + "description": "Un {{greedentName}} vous a pris par surprise et a volé toutes vos Baies !\n\nIl semble sur le point de les dévorer mais s’arrête et vous fixe, l’air intéressé.", + "query": "Que voulez-vous faire ?", + "option": { + "1": { + "label": "L’affronter", + "tooltip": "(-) Combat difficile\n(+) Récompenses de sa planque à Baies", + "selected": "Le {{greedentName}} gonfle ses joues\net se prépare au combat !", + "boss_enraged": "L’insatiable amour de {{greedentName}}\npour la nourriture le rend fou !", + "food_stash": "Il semblerait que {{greedentName}}\ngardait une pile de nourriture colossale !$@s{item_fanfare}Tous les Pokémon de votre équipe\nremportent une {{foodReward}} !" + }, + "2": { + "label": "Négocier", + "tooltip": "(+) Regagner quelques Baies", + "selected": "Vos arguments ont touché {{greedentName}}\nsur sa corde sensible.$Il accepte malgré tout de vous balancer\nquelques Baies et se garde le reste." + }, + "3": { + "label": "Lui laisser les Baies", + "tooltip": "(-) Baies définitivement perdues\n(?) {{greedentName}} vous apprécie", + "selected": "Le {{greedentName}} engloutit l’intégralité\ndes Baies en un éclair !$Il vous regarde avec tendresse\nen se tapotant le ventre.$Peut-être pourriez-vous lui en donner encore\nplus pendant votre périple…$@s{level_up_fanfare}Le {{greedentName}} veut rejoindre votre équipe !" + } + } +} diff --git a/src/locales/fr/mystery-encounters/an-offer-you-cant-refuse-dialogue.json b/src/locales/fr/mystery-encounters/an-offer-you-cant-refuse-dialogue.json new file mode 100644 index 00000000000..1df9288981f --- /dev/null +++ b/src/locales/fr/mystery-encounters/an-offer-you-cant-refuse-dialogue.json @@ -0,0 +1,26 @@ +{ + "intro": "Un jeune garçon aux airs très bourgeois vous arrête.", + "speaker": "Richard", + "intro_dialogue": "Bonchour-haann !$Je ne puis carrément pas ignorer que votre\n{{strongestPokemon}} m’a l’air fa-bu-leux-han !$J’ai toujours désiré posséder un tel Pokémon !$Je peux vous payer grassement,\nainsi que vous donner petite babiole-han !", + "title": "L’affaire du siècle", + "description": "Un fils à papa vous offre un @[TOOLTIP_TITLE]{Charme Chroma} et {{price, money}} en échange de votre {{strongestPokemon}} !\n\nÇa semble être une bonne affaire, mais pourriez-vous supporter de priver votre équipe d’un tel atout ?", + "query": "Que voulez-vous faire ?", + "option": { + "1": { + "label": "Accepter l’offre", + "tooltip": "(-) Vous perdez {{strongestPokemon}}\n(+) Gain d’un @[TOOLTIP_TITLE]{Charme Chroma}\n(+) Gain de {{price, money}}", + "selected": "Fa-bu-leux-han !@d{32} Par ici, {{strongestPokemon}} !$Viens que je te montre fièrement au club de yacht !$Ils vont trooop avoir le seum-haann !" + }, + "2": { + "label": "Le racketter", + "tooltip": "(+) {{option2PrimaryName}} utilise {{moveOrAbility}}\n(+) Gain de {{price, money}}", + "tooltip_disabled": "Votre Pokémon doit connaitre certaines capacités ou talents pour choisir cette option", + "selected": "Oh les aïeux-haann ! {{liepardName}}, on se fait voler !$Attends que je t’envoie mes avocats !" + }, + "3": { + "label": "Partir", + "tooltip": "(-) Aucune récompense", + "selected": "Quelle journée pourrie…$Viens {{liepardName}}.\nOn retourne au club de yacht…" + } + } +} diff --git a/src/locales/fr/mystery-encounters/berries-abound-dialogue.json b/src/locales/fr/mystery-encounters/berries-abound-dialogue.json new file mode 100644 index 00000000000..e3aedf37f33 --- /dev/null +++ b/src/locales/fr/mystery-encounters/berries-abound-dialogue.json @@ -0,0 +1,26 @@ +{ + "intro": "Il y a un gros buisson à Baies\nprès de ce Pokémon !", + "title": "Baies à gogo", + "description": "Un Pokémon a l’air de monter la garde de ce buisson à Baies. Vous pourriez aller frontalement au combat, mais il a l’air costaud. Un Pokémon rapide pourrait peut-être en attraper quelques-unes sans se faire prendre ?", + "query": "Que voulez-vous faire ?", + "berries": "Baies ", + "option": { + "1": { + "label": "L’affronter", + "tooltip": "(-) Combat difficile\n(+) Gain de Baies", + "selected": "Vous approchez\nle Pokémon sans frémir." + }, + "2": { + "label": "Foncer au buisson", + "tooltip": "(-) {{fastestPokemon}} utilise sa Vitesse\n(+) Gain de Baies", + "selected": "Votre {{fastestPokemon}} fonce vers le buisson !$Il en arrache {{numBerries}} avant que {{enemyPokemon}} ne parvienne à réagir !$Vous prenez vos jambes à votre cou avec votre butin.", + "selected_bad": "Votre {{fastestPokemon}} fonce vers le buisson !$Oh non ! Le {{enemyPokemon}} est plus rapide et bloque l’accès au buisson !", + "boss_enraged": "Le {{enemyPokemon}} ennemi s’énerve !" + }, + "3": { + "label": "Partir", + "tooltip": "(-) Aucune récompense", + "selected": "Vous renoncez à ce Pokémon avec\nson butin et continuez votre route." + } + } +} diff --git a/src/locales/fr/mystery-encounters/bug-type-superfan-dialogue.json b/src/locales/fr/mystery-encounters/bug-type-superfan-dialogue.json new file mode 100644 index 00000000000..63195e37062 --- /dev/null +++ b/src/locales/fr/mystery-encounters/bug-type-superfan-dialogue.json @@ -0,0 +1,40 @@ +{ + "intro": "Une Dresseur un peu étrange avec tout un attirail pour Insectes vous bloque la route !", + "intro_dialogue": "Hé, toi ! Je suis en mission, je dois trouver le Pokémon Inscete le plus rare du monde !$Toi aussi t’aimes les Pokémon Insecte, non ?\nTout le monde aime les Pokémon Insecte !", + "title": "Le Fan d’entomologie", + "speaker": "Fan d’entomologie", + "description": "Ce Dresseur est un vrai moulin à parole, il ne vous laisse aucune ouverture pour répondre…\n\nMais attirer son attention semble être le seul moyen possible de vous sortir de là !", + "query": "Que voulez-vous faire ?", + "option": { + "1": { + "label": "Proposer un combat", + "tooltip": "(-) Combat compliqué\n(+) Apprendre une capacité Insecte à un Pokémon", + "selected": "Un combat hein ?\nMes Insectes sont plus que prêts !" + }, + "2": { + "label": "Montrer vos types Insecte", + "tooltip": "(+) Recevez un objet en cadeau", + "disabled_tooltip": "Vous devez avoir au moins un Pokémon Insecte dans votre équipe pour choisir cette option.", + "selected": "Vous lui montrez tous les types Insecte de votre équipe…", + "selected_0_to_1": "Oh ? T’as qu’{{numBugTypes}}…$Pourquoi je gaspille ma salive à te parler…", + "selected_2_to_3": "Oh, t’as {{numBugTypes}} !\nC’est pas si mal.$Tiens, voilà qui devrait t’aider dans ta quête pour en attraper encore plus !", + "selected_4_to_5": "Quoi ? T’as {{numBugTypes}} ?\nSympa !$T’en est pas encore à mon niveau, mais je me reconnais un peu en toi !\n$Prends ça mon poulain, c’est cadeau !", + "selected_6": "Impressionnant ! {{numBugTypes}} !\n$Tu dois vraiment les aimer presque autant que moi !$Tiens, accepte ceci en signe de notre camaraderie !" + }, + "3": { + "label": "Donner un objet Insecte", + "tooltip": "(-) Lui donner l’objet {{requiredBugItems}}\n(+) Recevez un objet en cadeau", + "disabled_tooltip": "Vous avez besoin d’un objet {{requiredBugItems}} pour choisir cette option.", + "select_prompt": "Choisissez un objet à donner.", + "invalid_selection": "Ce Pokémon ne porte pas ce genre d’objet.", + "selected": "Vous remettez l’objet {{selectedItem}} au Dresseur.", + "selected_dialogue": "Sérieux ? {{selectedItem}}, comme ça en cadeau ?\nC’est très générieux !$En guise de ma reconnaissance, laisse-moi\nte faire un cadeau un peu spécial !$Il est dans ma famille depusi des générations, et ça me ferait plaisir de te l’offrir !" + } + }, + "battle_won": "Tes connaissances et tes capacités ont totalement exploité nos faiblesses !$En remerciement de cette leçon, permets-moi\nd’apprendre une capacité Insecte à un de tes Pokémon !", + "teach_move_prompt": "Sélectionnez une capacité à enseigner.", + "confirm_no_teach": "T’es sûr·e de vouloir renoncer à une des ces incroyables capacités ?", + "outro": "Je te prédis un futur plein d’incroyables Insectes !\nJ’espère qu’on se reverra !$À plus !", + "numBugTypes_one": "{{count}} type Insecte", + "numBugTypes_other": "{{count}} types Insecte" +} diff --git a/src/locales/fr/mystery-encounters/clowning-around-dialogue.json b/src/locales/fr/mystery-encounters/clowning-around-dialogue.json new file mode 100644 index 00000000000..83329153ff3 --- /dev/null +++ b/src/locales/fr/mystery-encounters/clowning-around-dialogue.json @@ -0,0 +1,34 @@ +{ + "intro": "Mais c’est…@d{64} un clown ?", + "speaker": "Clown", + "intro_dialogue": "Eh toi, tu m’as l’air clownesque !\nPrépare-toi pour un combat magistral !$Je vais te montrer ce que sont les arts de la rue !", + "title": "Bouffonneries", + "description": "Quelque chose semble louche. Ce Clown a l’air très motivé de vous provoquer en combat, mais dans quel but ?\n\nSon {{blacephalonName}} est très étrange, comme s’il possédait @[TOOLTIP_TITLE]{des types et un talent inhabituels.}", + "query": "Que voulez-vous faire ?", + "option": { + "1": { + "label": "Affronter le Clown", + "tooltip": "(-) Combat étrange\n(?) Affecte les talents des Pokémon", + "selected": "Vos Pokémon sont prêts pour cette performance pathétique !", + "apply_ability_dialogue": "Un spectacle sensationnel !\nVotre savoir-faire est en harmonie avec vos compétences !", + "apply_ability_message": "Le Clown vous propose d’Échanger définitivement le talent d’un de vos Pokémon contre {{ability}} !", + "ability_prompt": "Voulez-vous définitivement donner le talent {{ability}} à un Pokémon ?", + "ability_gained": "@s{level_up_fanfare}{{chosenPokemon}} obtient le talent {{ability}} !" + }, + "2": { + "label": "Rester de marbre", + "tooltip": "(-) Agace le Clown\n(?) Affecte les objets de vos Pokémon", + "selected": "Ça se défile lâchement d’un duel exceptionnel ?\nDans ce cas, tâte à ma colère !", + "selected_2": "Le {{blacephalonName}} du Clown utilise\nTour de Magie !$Tous les objets de {{switchPokemon}}\nsont échangés au hasard !", + "selected_3": "Sombre imbécile, tombe dans mon piège !" + }, + "3": { + "label": "Retourner les insultes", + "tooltip": "(-) Agace le Clown\n(?) Affecte les types de vos Pokémon", + "selected": "Ça se défile lâchement d’un duel exceptionnel ?\nDans ce cas, tâte à ma colère !", + "selected_2": "Le {{blacephalonName}} du Clown utilise\nune étrange capacité !$Tous les types de votre équipe\nsont échangés au hasard !", + "selected_3": "Sombre imbécile, tombe dans mon piège !" + } + }, + "outro": "Le Clown et sa bande disparaissent\ndans un nuage de fumée." +} diff --git a/src/locales/fr/mystery-encounters/dancing-lessons-dialogue.json b/src/locales/fr/mystery-encounters/dancing-lessons-dialogue.json new file mode 100644 index 00000000000..61a5049a7c7 --- /dev/null +++ b/src/locales/fr/mystery-encounters/dancing-lessons-dialogue.json @@ -0,0 +1,27 @@ +{ + "intro": "Un {{oricorioName}} danse tristement seul,\nsans partenaire pour l’accompagner.", + "title": "Leçons de danse", + "description": "Ce {{oricorioName}} ne semble pas hostile, mais a tout au plus juste l’air triste.\n\nPeut-être a-t-il juste besoin d’un ou d’une partenaire pour l’accompagner ?…", + "query": "Que voulez-vous faire ?", + "option": { + "1": { + "label": "L’affronter", + "tooltip": "(-) Combat difficile\n(+) Gain d’un Bâton", + "selected": "Le {{oricorioName}} est desemparé\net tente de se défendre !", + "boss_enraged": "La peur de {{oricorioName}} augmente beaucoup ses stats !" + }, + "2": { + "label": "Apprendre sa danse", + "tooltip": "(+) Apprendre Danse Éveil à un Pokémon", + "selected": "Vos scrutez {{oricorioName}} de près pendant sa danse…$@s{level_up_fanfare}Votre {{selectedPokemon}} apprend la capacité\nDanse Éveil de {{oricorioName}} !" + }, + "3": { + "label": "Montrer une danse", + "tooltip": "(-) Apprendre à {{oricorioName}} une capacité dansante\n(+) Le {{oricorioName}} vous apprécie", + "disabled_tooltip": "Votre Pokémon doit connaitre une capacité dansante pour choisir cette option.", + "select_prompt": "Sélectionnez une capacité dansante.", + "selected": "Le {{oricorioName}} observe avec fascination\n{{selectedPokemon}} exécuter {{selectedMove}} !$Il adore le démonstration !$@s{level_up_fanfare}Le {{oricorioName}} veut se joindre à votre équipe !" + } + }, + "invalid_selection": "Il ne connait aucune capacité dansante" +} diff --git a/src/locales/fr/mystery-encounters/dark-deal-dialogue.json b/src/locales/fr/mystery-encounters/dark-deal-dialogue.json new file mode 100644 index 00000000000..1db9266459a --- /dev/null +++ b/src/locales/fr/mystery-encounters/dark-deal-dialogue.json @@ -0,0 +1,24 @@ + + +{ + "intro": "Un homme suspect vêtu d’une blouse en lambeaux\nse tient au milieu du chemin…", + "speaker": "Savant fou", + "intro_dialogue": "Hé, toi !$Je travaille sur un dispositif qui permet d’éveiller\nla vraie puissance d’un Pokémon !$Il restructure complètement les atomes du Pokémon\npour en faire une version bien plus puissante.$Héhé…@d{64} Je n’ai besoin que de sac-@d{32}\nEuuh, sujets tests, pour prouver son fonctionnement.", + "title": "L’Expérience interdite", + "description": "Ce scientifique à l’air un peu taré tient dans ses mains quelques Poké Balls.\n« T’inquiètes pas, je gère ! Voilà quelques Poké Balls plutôt efficaces en caution, tout ce dont j’ai besoin, c’est un de tes Pokémon ! Héhé… »", + "query": "Que voulez-vous faire ?", + "option": { + "1": { + "label": "Accepter", + "tooltip": "(+) 5 Rogue Balls\n(?) Améliore un Pokémon au hasard", + "selected_dialogue": "Ah bien, ton {{pokeName}} fera parfaitement l’affaire !$Je précise que si quelque chose tourne mal,\nje ne suis pas responsable !@d{32} Héhé…", + "selected_message": "L’homme vous remet 5 Rogue Balls.$Votre {{pokeName}} saute dans l’étrange dispositif…$Le dispositif émet des lumières\nclignotantes et des bruits douteux !$…@d{96} Quelque chose sort du dispositif avec fureur !" + }, + "2": { + "label": "Refuser", + "tooltip": "(-) Aucune récompense", + "selected": "On a même plus le droit de rendre service maintenant ?\nPfff !" + } + }, + "outro": "Après cette épuisante rencontre,\nvous reprenez vos esprits et repartez." +} diff --git a/src/locales/fr/mystery-encounters/delibirdy-dialogue.json b/src/locales/fr/mystery-encounters/delibirdy-dialogue.json new file mode 100644 index 00000000000..20c878db24f --- /dev/null +++ b/src/locales/fr/mystery-encounters/delibirdy-dialogue.json @@ -0,0 +1,29 @@ + + +{ + "intro": "Une horde de {{delibirdName}} apparait !", + "title": "Cas d’oiseaux", + "description": "Les {{delibirdName}} vous scrutent avec curiosité, comme s’ils attendaient quelque chose. Peut-être que leur donner un objet ou un peu d’argent pourrait les satisfaire ?", + "query": "Que voulez-vous faire ?", + "invalid_selection": "Ce Pokémon ne porte pas ce genre d’objet.", + "option": { + "1": { + "label": "Donner de l’argent", + "tooltip": "(-) Donner {{money, money}} aux {{delibirdName}}\n(+) Recevez un objet", + "selected": "Vous lancez de l’argent aux {{delibirdName}},\nqui se lancent dans une grande délibération.$Ils reviennent ravis vers vous avec un cadeau !" + }, + "2": { + "label": "Donner de la nourriture", + "tooltip": "(-) Donner une Baie ou une Résugraine aux {{delibirdName}}\n(+) Recevez un objet", + "select_prompt": "Sélectionnez un objet à donner.", + "selected": "Vous lancez la {{chosenItem}} aux {{delibirdName}},\nqui se lancent dans une grande délibération.$Ils reviennent ravis vers vous avec un cadeau !" + }, + "3": { + "label": "Donner un objet", + "tooltip": "(-) Donner un objet tenu aux {{delibirdName}}\n(+) Recevez un objet", + "select_prompt": "Sélectionnez un objet à donner.", + "selected": "Vous lancez l’objet {{chosenItem}} aux {{delibirdName}},\nqui se lancent dans une grande délibération.$Ils reviennent ravis vers vous avec un cadeau !" + } + }, + "outro": "La horde de {{delibirdName}} repartent dans la joie.$Un bien curieux échange !" +} diff --git a/src/locales/fr/mystery-encounters/department-store-sale-dialogue.json b/src/locales/fr/mystery-encounters/department-store-sale-dialogue.json new file mode 100644 index 00000000000..fc06aa05582 --- /dev/null +++ b/src/locales/fr/mystery-encounters/department-store-sale-dialogue.json @@ -0,0 +1,27 @@ +{ + "intro": "Il y a une dame avec des tas de sacs de courses\nà ne savoir qu’en faire.", + "speaker": "Cliente", + "intro_dialogue": "Bonjour !\nToi aussi t’es là pour les incroyables promos ?$Il y a un coupon spécial que tu peux utiliser en échange\nd’un objet gratuit pendant toute la durée de la promo !$J’en ai un en trop.\nTiens, prends-le !", + "title": "Promos au Centre Commercial", + "description": "Tellement de choix que votre regard ne sait plus où se porter !\nIl y a 4 comptoirs auprès desquels vous pouvez dépenser ce coupon parmi une grande variété d’objets.", + "query": "À quel comptoir se rendre ?", + "option": { + "1": { + "label": "CT", + "tooltip": "(+) CT dans la boutique" + }, + "2": { + "label": "Accélérateurs", + "tooltip": "(+) Accélérateurs dans la boutique" + }, + "3": { + "label": "Objets de Combat", + "tooltip": "(+) Objets de boost dans la boutique" + }, + "4": { + "label": "Poké Balls", + "tooltip": "(+) Poké Balls dans la boutique" + } + }, + "outro": "Quelle affaire !\nVous devriez revenir y faire vos achats plus souvent." +} diff --git a/src/locales/fr/mystery-encounters/field-trip-dialogue.json b/src/locales/fr/mystery-encounters/field-trip-dialogue.json new file mode 100644 index 00000000000..6311976732c --- /dev/null +++ b/src/locales/fr/mystery-encounters/field-trip-dialogue.json @@ -0,0 +1,31 @@ +{ + "intro": "C’est une enseignante avec ses élèves !", + "speaker": "Enseignante", + "intro_dialogue": "Hé, bonjour !\nAurais-tu une minute à accorder à mes élèves ?$Je leur apprends ce que sont les capacités et\nj’adorerais que tu leur fasse une démonstration.$Tu serais d’accord pour nous montrer ça\navec un de tes Pokémon ?", + "title": "Sortie scolaire", + "description": "Une enseignante vous demande de faire la démonstration d’une capacité d’un de vos Pokémon.\nEn fonction de la capacité, elle pourrait vous donner quelque chose d’utile.", + "query": "Utiliser quelle catégorie de capacité ?", + "option": { + "1": { + "label": "Physique", + "tooltip": "(+) Objets de boost axés sur le Physique à la prochaine boutique" + }, + "2": { + "label": "Spéciale", + "tooltip": "(+) Objets de boost axés sur le Spécial à la prochaine boutique" + }, + "3": { + "label": "De Statut", + "tooltip": "(+) Objets de boost axés sur le Statut à la prochaine boutique" + }, + "selected": "{{pokeName}} fait une incroyable démonstration de sa capacité {{move}} !" + }, + "second_option_prompt": "Choisissez une capacité à utiliser.", + "incorrect": "…$Ce n’est pas une capacité {{moveCategory}} !\nJe suis désolée, mais je ne peux rien te donner.$Venez les enfants, on va trouver mieux ailleurs.", + "incorrect_exp": "Il semble que vous avez appris une précieuse leçon ?$Votre Pokémon a gagné un peu d’expérience.", + "correct": "Merci beaucoup de nous avoir accordé de ton temps !\nJ’espère que ces quelques objets de seront utiles !", + "correct_exp": "{{pokeName}} a aussi gagné un beaucoup d’expérience !", + "status": "de Statut", + "physical": "Physique", + "special": "Spéciale" +} diff --git a/src/locales/fr/mystery-encounters/fiery-fallout-dialogue.json b/src/locales/fr/mystery-encounters/fiery-fallout-dialogue.json new file mode 100644 index 00000000000..6477bae43d8 --- /dev/null +++ b/src/locales/fr/mystery-encounters/fiery-fallout-dialogue.json @@ -0,0 +1,26 @@ +{ + "intro": "Un vent incandescent de fumées et de cendres\nvous fonce dessus !", + "title": "Fait chaud là, non ?", + "description": "La visibilité est quasi nulle à cause des cendres et des braises tourbillonnantes. Il doit forcément y avoir une quelconque source responsable de ces conditions. Mais que pourrait causer un phénomène d’une telle ampleur ?", + "query": "Que voulez-vous faire ?", + "option": { + "1": { + "label": "Chercher la source", + "tooltip": "(?) Trouver la source\n(-) Combat difficile", + "selected": "Vous pénétrez tant bien que mal dans la tempête et y trouvez deux {{volcaronaName}} en pleine parade nuptiale !$Ils n’apprécient guère d’avoir été interrompus\net vous attaquent !" + }, + "2": { + "label": "S’accroupir", + "tooltip": "(-) Subir la météo", + "selected": "Les effets météorologiques causent d’importantes blessures alors que vous cherchez un abri !$Votre équipe perd 20% de ses PV totaux !", + "target_burned": "Votre {{burnedPokemon}} est également brulé !" + }, + "3": { + "label": "Un type Feu à l’aide", + "tooltip": "(+) Met fin à la météo\n(+) Gain d’un Charbon", + "disabled_tooltip": "Vous avez besoin d’au moins 2 Pokémon Feu pour choisir cette option", + "selected": "Vos {{option3PrimaryName}} et {{option3SecondaryName}} vous guident et tombez sur deux {{volcaronaName}} en pleine parade nuptiale !$Heureusement, vos Pokémon parviennent à les calmer, et passent leur chemin sans causer de problèmes." + } + }, + "found_charcoal": "Alors que la météo se calme, votre {{leadPokemon}} repère quelque chose au sol.$@s{item_fanfare}{{leadPokemon}} obtient\nun Charobn !" +} diff --git a/src/locales/fr/mystery-encounters/fight-or-flight-dialogue.json b/src/locales/fr/mystery-encounters/fight-or-flight-dialogue.json new file mode 100644 index 00000000000..d5c6bb7513d --- /dev/null +++ b/src/locales/fr/mystery-encounters/fight-or-flight-dialogue.json @@ -0,0 +1,25 @@ +{ + "intro": "Quelque chose d’une lueur éclatante\nbrille sur sol près de ce Pokémon !", + "title": "Voler ou s’envoler", + "description": "Un puissant Pokémon semble monter la garde d’un objet. Vous pourriez aller frontalement au combat, mais il a l’air costaud. Avec le bon Pokémon, vous pourriez peut-être voler l’objet sans vous faire prendre ?", + "query": "Que voulez-vous faire ?", + "option": { + "1": { + "label": "L’affronter", + "tooltip": "(-) Combat difficile\n(+) Nouvel objet", + "selected": "Vous approchez\nle Pokémon sans frémir.", + "stat_boost": "Les forces dormantes de {{enemyPokemon}}\naugmentent une de ses stats !" + }, + "2": { + "label": "Voler l’objet", + "disabled_tooltip": "Votre Pokémon doit connaitre certaines capacités pour choisir cette option", + "tooltip": "(+) {{option2PrimaryName}} utilise {{option2PrimaryMove}}", + "selected": ".@d{32}.@d{32}.@d{32}$Votre {{option2PrimaryName}} vous aide avec sa capacité {{option2PrimaryMove}} !$Vous dérobez l’objet !" + }, + "3": { + "label": "Partir", + "tooltip": "(-) Aucune récompense", + "selected": "Vous renoncez à ce Pokémon avec\nson butin et continuez votre route." + } + } +} diff --git a/src/locales/fr/mystery-encounters/fun-and-games-dialogue.json b/src/locales/fr/mystery-encounters/fun-and-games-dialogue.json new file mode 100644 index 00000000000..a649b5dea60 --- /dev/null +++ b/src/locales/fr/mystery-encounters/fun-and-games-dialogue.json @@ -0,0 +1,30 @@ +{ + "intro_dialogue": "Approchez mesdames et messieurs !$Tentez votre chance au tout nouveau\nQulbut-o-matic de {{wobbuffetName}} !", + "speaker": "Animateur", + "title": "Du rire et des jeux !", + "description": "Vous rencontrez un forain avec une mailloche ! Vous disposez de @[TOOLTIP_TITLE]{3 tours} pour amener {{wobbuffetName}} le plus près possible de @[TOOLTIP_TITLE]{1 PV} pour ainsi charger la Riposte la plus puissante possible, @[TOOLTIP_TITLE]{sans le mettre K.O.} .\nMais attention ! Si {{wobbuffetName}} est mis K.O., vous devrez payer pour le ranimer !", + "query": "Voulez-vous jouer ?", + "option": { + "1": { + "label": "Jouer", + "tooltip": "(-) Payer {{option1Money, money}}\n(+) Jouer au Qulbut-o-matic", + "selected": "Vous tentez votre chance !" + }, + "2": { + "label": "Partir", + "tooltip": "(-) Aucune récompense", + "selected": "Vous passez votre chemin,\navec un léger sentiment de regret." + } + }, + "ko": "Oh non ! Le {{wobbuffetName}} est K.O. !$Vous avez perdu et devez maintenant\npayer pour le ranimer…", + "charging_continue": "Le Qulbutoké charge\nsa Riposte !", + "turn_remaining_3": "Trois tours restants !", + "turn_remaining_2": "Deux tours restants !", + "turn_remaining_1": "Un tour restant !", + "end_game": "Tous vos tours sont écoulés !$Le {{wobbuffetName}} relâche\nsa Riposte et@d{16}.@d{16}.@d{16}.", + "best_result": "Le {{wobbuffetName}} éclate le bouton si fort\nque la cloche crève le plafond !$Vous remportez le premier prix !", + "great_result": "Le {{wobbuffetName}} éclate le bouton,\net touche presque la cloche !$Mince, à deux doigts !\nVous remportez le deuxième prix !", + "good_result": "Le {{wobbuffetName}} tape le bouton assez fort\npour atteindre la moitié !$Vous remportez le troisième prix !", + "bad_result": "Le {{wobbuffetName}} touche à peine le bouton et rien ne se passe…$Oh non, dommage !\nVous repartez les mains vides !", + "outro": "Ouais, ça va, c’était sympa comme jeu !" +} diff --git a/src/locales/fr/mystery-encounters/global-trade-system-dialogue.json b/src/locales/fr/mystery-encounters/global-trade-system-dialogue.json new file mode 100644 index 00000000000..6caafc3c82d --- /dev/null +++ b/src/locales/fr/mystery-encounters/global-trade-system-dialogue.json @@ -0,0 +1,32 @@ +{ + "intro": "C’est une interface d’accès à la GTS !", + "title": "La GTS", + "description": "Ah, la GTS ! Une révolution technologique permettant de connecter le monde au travers des échanges de Pokémon !\n\nLa chance va-t-elle vous sourire aujourd’hui ?", + "query": "Que voulez-vous faire ?", + "option": { + "1": { + "label": "Consulter les offres", + "tooltip": "(+) Sélectionner une offre d’échange pour un de vos Pokémon", + "trade_options_prompt": "Choisissez un Pokémon à recevoir en échange." + }, + "2": { + "label": "Échange Miracle", + "tooltip": "(+) Envoyer un de vos Pokémon à la GTS et en recevoir un au hasard" + }, + "3": { + "label": "Échanger un objet", + "trade_options_prompt": "Choisissez l’objet à envoyer.", + "invalid_selection": "Ce Pokémon n’a aucun objet légal à échanger.", + "tooltip": "(+) Envoyer un de vos objets à la GTS et en recevoir un au hasard" + }, + "4": { + "label": "Partir", + "tooltip": "(-) Aucune récompense", + "selected": "Pas le temps pour ça aujourd’hui !\nVous reprenez votre chemin." + } + }, + "pokemon_trade_selected": "{{tradedPokemon}} est envoyé\nà {{tradeTrainerName}}.", + "pokemon_trade_goodbye": "Au revoir, {{tradedPokemon}}!", + "item_trade_selected": "{{chosenItem}} est envoyé à\n{{tradeTrainerName}}.$.@d{64}.@d{64}.@d{64}\n@s{level_up_fanfare}Échangé terminé !$Vous recevez l’objet {{itemName}}\nde {{tradeTrainerName}} !", + "trade_received": "@s{evolution_fanfare}{{tradeTrainerName}} a envoyé {{received}} !" +} diff --git a/src/locales/fr/mystery-encounters/lost-at-sea-dialogue.json b/src/locales/fr/mystery-encounters/lost-at-sea-dialogue.json new file mode 100644 index 00000000000..38191a4d587 --- /dev/null +++ b/src/locales/fr/mystery-encounters/lost-at-sea-dialogue.json @@ -0,0 +1,28 @@ +{ + "intro": "Vous errez sans but en pleine mer\net n’arrivez à rien.", + "title": "Un cap pas clair", + "description": "La mer n’est pas très clémente dans cette zone et vous vous sentez faiblir à petit feu.\nÇa commence à sentir le roussi.\nEst-il au moins possible de se sortir de cette situation ?", + "query": "Que voulez-vous faire ?", + "option": { + "1": { + "label": "{{option1PrimaryName}} à l’aide", + "label_disabled": "{{option1RequiredMove}} impossible", + "tooltip": "(+) {{option1PrimaryName}} vous sauve\n(+) {{option1PrimaryName}} gagne un peu d’Exp", + "tooltip_disabled": "Vous n’avez aucun Pokémon avec {{option1RequiredMove}}", + "selected": "{{option1PrimaryName}} nage en tête et vous guide.${{option1PrimaryName}} semble ressorti plus fort de cette épreuve difficile !" + }, + "2": { + "label": "{{option2PrimaryName}} à l’aide", + "label_disabled": "{{option2RequiredMove}} impossible", + "tooltip": "(+) {{option2PrimaryName}} vous sauve\n(+) {{option2PrimaryName}} gagne un peu d’Exp", + "tooltip_disabled": "Vous n’avez aucun Pokémon avec {{option2RequiredMove}}", + "selected": "{{option2PrimaryName}} vole au dessus de votre embarcation et vous guide.${{option2PrimaryName}} semble ressorti plus fort de cette épreuve difficile !" + }, + "3": { + "label": "Naviguer au jugé", + "tooltip": "(-) Votre équipe perd {{damagePercentage}}% de ses PV totaux", + "selected": "Vous vous laissez porter sans but par les vagues, quand soudainement vous remarquez un lieu familier.$Vous et vos Pokémon êtes complètement rincés\nde fatigue par cette épreuve." + } + }, + "outro": "Vous retrouvez un cap clair." +} diff --git a/src/locales/fr/mystery-encounters/mysterious-challengers-dialogue.json b/src/locales/fr/mystery-encounters/mysterious-challengers-dialogue.json new file mode 100644 index 00000000000..9e83b5e4f3f --- /dev/null +++ b/src/locales/fr/mystery-encounters/mysterious-challengers-dialogue.json @@ -0,0 +1,22 @@ +{ + "intro": "De mystérieux adversaires apparaissent !", + "title": "De mystérieux adversaires", + "description": "Si vous défaites un de ces adversaires, vous pourriez peut-être recevoir une récompense si vous les avez impressionnés.\nCertains ont cependant l’air bien coriaces. Acceptez-vous le défi ?", + "query": "Qui voulez-vous affronter ?", + "option": { + "1": { + "label": "Adversaire lucide", + "tooltip": "(-) Combat normal\n(+) Objets de capacités" + }, + "2": { + "label": "Adversaire difficile", + "tooltip": "(-) Combat difficile\n(+) Bonnes récompenses" + }, + "3": { + "label": "Adversaire puissant", + "tooltip": "(-) Combat brutal\n(+) Super récompenses" + }, + "selected": "Votre adversaire s’avance…" + }, + "outro": "Vous avez battu ce mystérieux adversaire !" +} diff --git a/src/locales/fr/mystery-encounters/mysterious-chest-dialogue.json b/src/locales/fr/mystery-encounters/mysterious-chest-dialogue.json new file mode 100644 index 00000000000..4e2fd868bcf --- /dev/null +++ b/src/locales/fr/mystery-encounters/mysterious-chest-dialogue.json @@ -0,0 +1,23 @@ +{ + "intro": "Vous tombez sur…@d{32} un coffre ?", + "title": "Un étrange coffre", + "description": "Un magnifique coffre orné est posé en travers du chemin.\nIl doit forcément contenir quelque chose… pas vrai ?", + "query": "Voulez-vous ouvrir le coffre ?", + "option": { + "1": { + "label": "Ouvrir", + "tooltip": "@[SUMMARY_BLUE]{({{trapPercent}}%) Un truc terrible}\n@[SUMMARY_GREEN]{({{commonPercent}}%) Petites récompenses }\n@[SUMMARY_GREEN]{({{ultraPercent}}%) Bonnes récompenses}\n@[SUMMARY_GREEN]{({{roguePercent}}%) Super récompenses}\n@[SUMMARY_GREEN]{({{masterPercent}}%) Incroyables récompenses}", + "selected": "Vous décidez d’ouvrir le coffre et y trouvez…", + "normal": "De simples outils et objets banaux.", + "good": "Quelques bons outils et objets.", + "great": "Un outil et un objet plutôt sympas !", + "amazing": "Wow ! Quel objet incroyable !", + "bad": "Oh non !@d{32}\nCe coffre s’avère être possédé par un {{gimmighoulName}} !$Votre {{pokeName}} s’interpose\nmais est mis K.O. !" + }, + "2": { + "label": "Trop risqué, partir", + "tooltip": "(-) Aucun récompense", + "selected": "Vous tracez votre chemin,\navec un léger sentiment de regret." + } + } +} diff --git a/src/locales/fr/mystery-encounters/part-timer-dialogue.json b/src/locales/fr/mystery-encounters/part-timer-dialogue.json new file mode 100644 index 00000000000..88bb68df03e --- /dev/null +++ b/src/locales/fr/mystery-encounters/part-timer-dialogue.json @@ -0,0 +1,31 @@ +{ + "intro": "Un Ouvrière croulant sous le travail\nvous interpelle.", + "speaker": "Ouvrière", + "intro_dialogue": "T’as l’air d’avoir pas mal de Pokémon compétents\navec toi !$Si t’acceptes de nous aider pour quelques tâches,\non peut te payer !", + "title": "L’intérimaire", + "description": "La liste de tâches à effectuer a l’air incroyablement longue.\nEn fonction des compétences du Pokémon choisi à une tâche donnée, vous gagnerez plus ou moins d’argent.", + "query": "Quel poste vous intéresse le plus ?", + "invalid_selection": "Le Pokémon doit être en bonne santé.", + "option": { + "1": { + "label": "Livreur", + "tooltip": "(-) Votre Pokémon utilise sa Vitesse\n(+) Payé avec un @[MONEY]{salaire}", + "selected": "Votre {{selectedPokemon}} passe tout un shift\nà livrer des clients." + }, + "2": { + "label": "Manutentionnaire", + "tooltip": "(-) Votre Pokémon utilise sa Force et son Endurance\n(+) Payé avec un @[MONEY]{salaire}", + "selected": "Votre {{selectedPokemon}} passe tout un shift\nà porter des charges à travers le hangar." + }, + "3": { + "label": "Assistant vendeur", + "tooltip": "(-) Votre {{option3PrimaryName}} utilise {{option3PrimaryMove}}\n(+) Payé avec un @[MONEY]{salaire}", + "disabled_tooltip": "Votre Pokémon doit connaitre certaines capacités pour choisir ce poste", + "selected": "Votre {{option3PrimaryName}} passe sa journée à utiliser {{option3PrimaryMove}} pour attirer des clients !" + } + }, + "job_complete_good": "Merci pour votre aide !\nVotre {{selectedPokemon}} nous a été d’un grand renfort !$Voici votre salaire.", + "job_complete_bad": "Votre {{selectedPokemon}} nous a plutôt bien aidé !$Voici votre salaire.", + "pokemon_tired": "Votre {{selectedPokemon}} est épuisé !\nLes PP de toutes ses capacités baissent de 2 !", + "outro": "Reviens nous aider à l’occasion !" +} diff --git a/src/locales/fr/mystery-encounters/safari-zone-dialogue.json b/src/locales/fr/mystery-encounters/safari-zone-dialogue.json new file mode 100644 index 00000000000..24834d128c0 --- /dev/null +++ b/src/locales/fr/mystery-encounters/safari-zone-dialogue.json @@ -0,0 +1,46 @@ +{ + "intro": "C’est un Parc Safari !", + "title": "Le Parc Safari", + "description": "Toutes sortes de Pokémon rares et particuliers peuvent y être rencontrés !\nSi vous décidez d’y entrer, vous aurez une limite de 3 Pokémon sauvages à rencontrer que vous pourrez essayer de capturer.\n\nMais attention, ces Pokémon peuvent prendre la fuite avant que vous n’ayez pu les capturer !", + "query": "Voulez-vous y entrer ?", + "option": { + "1": { + "label": "Entrer", + "tooltip": "(-) Payer {{option1Money, money}}\n@[SUMMARY_GREEN]{(?) Parc Safari}", + "selected": "Vous tentez votre chance !" + }, + "2": { + "label": "Partir", + "tooltip": "(-) Aucune récompense", + "selected": "Vous tracez votre chemin,\navec un léger sentiment de regret." + } + }, + "safari": { + "1": { + "label": "Lancer une Poké Ball", + "tooltip": "(+) Lancer une Poké Ball", + "selected": "Vous lancez une Poké Ball !" + }, + "2": { + "label": "Lancer un appât", + "tooltip": "(+) Augmente le taux de capture\n(-) Augmente le risque de fuite", + "selected": "Vous lancez quelques appâts !" + }, + "3": { + "label": "Lancer de la boue", + "tooltip": "(+) Diminue le risque de fuite\n(-) Diminue le taux de capture", + "selected": "Vous lancez un peu de boue !" + }, + "4": { + "label": "Fuite", + "tooltip": "(?) Fuir ce Pokémon" + }, + "watching": "{{pokemonName}} vous observe\nattentivement.", + "eating": "{{pokemonName}} est en train\nde manger.", + "busy_eating": "{{pokemonName}} se concentre\nsur sa nourriture.", + "angry": "{{pokemonName}} se fâche !", + "beside_itself_angry": "{{pokemonName}} laisse\nexploser sa colère !", + "remaining_count": "Il reste {{remainingCount}} Pokémon !" + }, + "outro": "Cette excursion était fun !" +} diff --git a/src/locales/fr/mystery-encounters/shady-vitamin-dealer-dialogue.json b/src/locales/fr/mystery-encounters/shady-vitamin-dealer-dialogue.json new file mode 100644 index 00000000000..95628cdfb84 --- /dev/null +++ b/src/locales/fr/mystery-encounters/shady-vitamin-dealer-dialogue.json @@ -0,0 +1,27 @@ +{ + "intro": "Un homme en manteau noir vous aborde.", + "speaker": "Dealer", + "intro_dialogue": ".@d{16}.@d{16}.@d{16}$J’ai de la bonne came pour toi, mais seulement\nsi t’as les thunes.$Assure-toi quand même que tes Pokémon\npuissent encaisser.", + "title": "Le Dealer d’accélérateurs", + "description": "L’homme ouvre son manteau et vous laisse apercevoir des accélérateurs pour Pokémon. Le tarif qu’il annonce semble être une bonne affaire. Peut-être même un peu trop belle…\nIl vous laisse le choix entre deux offres.", + "query": "Laquelle choisissez-vous ?", + "invalid_selection": "Le Pokémon doit être en bonne santé.", + "option": { + "1": { + "label": "Offre douteuse", + "tooltip": "(-) Payer {{option1Money, money}}\n(-) Effets secondaires ?\n(+) Le Pokémon choisi gagne 2 accélérateurs au hasard" + }, + "2": { + "label": "Offre honnête", + "tooltip": "(-) Payer {{option2Money, money}}\n(+) Le Pokémon choisi gagne 2 accélérateurs au hasard" + }, + "3": { + "label": "Partir", + "tooltip": "(-) Aucune récompense", + "selected": "Eh bien, j’aurais pas cru que\ntu ferais preuve d’une telle lâcheté." + }, + "selected": "L’homme vous remet deux Accélérateurs et s’évapore.${{selectedPokemon}} reçoit 1 {{boost1}} et 1 {{boost2}} !" + }, + "cheap_side_effects": "Ah !\nDes effets secondaires se manifestent !$Votre {{selectedPokemon}} prend des dégâts,\net sa nature se change en {{newNature}} !", + "no_bad_effects": "Tout semble bien se passer, aucun effet indésirable à signaler !" +} diff --git a/src/locales/fr/mystery-encounters/slumbering-snorlax-dialogue.json b/src/locales/fr/mystery-encounters/slumbering-snorlax-dialogue.json new file mode 100644 index 00000000000..b436f373dfc --- /dev/null +++ b/src/locales/fr/mystery-encounters/slumbering-snorlax-dialogue.json @@ -0,0 +1,25 @@ +{ + "intro": "Alors que vous vous aventurez dans un passage étroit, une silhouette imposante bloque le passage.$En vous approchant, vous apercevez un {{snorlaxName}}\nendormi paisiblement.$Vous regardez autour de vous, mais n’apercevez aucun chemin alternatif.", + "title": "{{snorlaxName}} au Bois dormant", + "description": "Vous pourriez soit tenter de le faire bouger en l’attaquant, soit attendre son réveil.\nMais impossible de savoir combien de temps cette dernière option prendrait…", + "query": "Que voulez-vous faire ?", + "option": { + "1": { + "label": "L’affronter", + "tooltip": "(-) Affronter le {{snorlaxName}} endormi\n(+) Récompense spéciale", + "selected": "Vous approchez\nle Pokémon sans frémir." + }, + "2": { + "label": "Attendre qu’il bouge", + "tooltip": "(-) Attendre longtemps\n(+) Équipe soignée", + "selected": ".@d{32}.@d{32}.@d{32}$Vous attendez un long moment et les bâillements de {{snorlaxName}} endorment votre équipe…", + "rest_result": "À votre réveil le {{snorlaxName}} a disparu,\net toute votre équipe est soignée !" + }, + "3": { + "label": "Voler son objet", + "tooltip": "(+) {{option3PrimaryName}} utilise {{option3PrimaryMove}}\n(+) Récompense spéciale", + "disabled_tooltip": "Votre Pokémon doit connaitre certaines capacités pour choisir cette option", + "selected": "Votre {{option3PrimaryName}} utilise\n{{option3PrimaryMove}} !$@s{item_fanfare}Vole les Restes du {{snorlaxName}} encore endormi et ressentez de la culpabilité pour ce méfait !" + } + } +} diff --git a/src/locales/fr/mystery-encounters/teleporting-hijinks-dialogue.json b/src/locales/fr/mystery-encounters/teleporting-hijinks-dialogue.json new file mode 100644 index 00000000000..7a819f0bb37 --- /dev/null +++ b/src/locales/fr/mystery-encounters/teleporting-hijinks-dialogue.json @@ -0,0 +1,27 @@ +{ + "intro": "Cette machine un peu étrange émet un lourd ronronnement…", + "title": "Le télé-torpeur", + "description": "Vous pouvez lire sur la machine :\n « Notice d’utilisation : Insérez de l’argent et pénétrez dans la capsule. »\n\nPeut-être qu’elle vous emmènera quelque part…", + "query": "Que voulez-vous faire ?", + "option": { + "1": { + "label": "Insérer de l’argent", + "tooltip": "(-) Payer {{price, money}}\n(?) Téléportation vers un nouveau biome", + "selected": "Vous insérez un peu d’argent et la capsule s’ouvre.\nVous y pénétrez…" + }, + "2": { + "label": "Un Pokémon à l’aide", + "tooltip": "(-) {{option2PrimaryName}} vous aide\n(+) Gain d’Exp pour {{option2PrimaryName}}\n(?) Téléportation vers un nouveau biome", + "disabled_tooltip": "Vous avez besoin d’un Pokémon de type Électrik ou Acier pour choisir cette option", + "selected": "Le type de {{option2PrimaryName}} vous permet de frauder la machine !$La capsule s’ouvre et y pénétrez…" + }, + "3": { + "label": "Inspecter la machine", + "tooltip": "(-) Combat Pokémon", + "selected": "Tous ces bruits et lumières clignotantes\némis par la machine titillent vos sens…$Mais vous manquez d’apercevoir un Pokémon\nsauvage et qui tend une embuscade !" + } + }, + "transport": "Elle vibre violemment, émettant\ntoutes sortes de bruits !$À peine la machine lancée, elle redevient\nsubitement très silencieuse…", + "attacked": "Vous en ressortez en un lieu complètement nouveau, effrayant au passage un Pokémon sauvage !$Le Pokémon sauvage vous attaque !", + "boss_enraged": "Le {{enemyPokemon}} ennemi s’énerve !" +} diff --git a/src/locales/fr/mystery-encounters/the-expert-pokemon-breeder-dialogue.json b/src/locales/fr/mystery-encounters/the-expert-pokemon-breeder-dialogue.json new file mode 100644 index 00000000000..ebd450730d6 --- /dev/null +++ b/src/locales/fr/mystery-encounters/the-expert-pokemon-breeder-dialogue.json @@ -0,0 +1,31 @@ +{ + "intro": "C’est une Dresseuse avec une quantité astronomique d’Œufs !", + "intro_dialogue": "Hé, toi !$Certains de tes Pokémon m’ont l’air\nun peu à plat.$Ça te dirait un p’tit combat pour\nleur redonner un peu de peps ?", + "title": "L’experte en élevage", + "description": "Une Éleveuse vous propose un combat où @[TOOLTIP_TITLE]{vous ne pouvez utiliser qu’un seul Pokémon}. Ça s’annonce un peu compliqué, mais devrait certainement approfondir vos liens avec le Pokémon choisi !\nL’Éleveuse devrait vous faire don de quelques @[TOOLTIP_TITLE]{Œufs} si vous gagnez !", + "query": "Avec qui voulez-vous combattre ?", + "cleffa_1_nickname": "As", + "cleffa_2_nickname": "Méloïssime", + "cleffa_3_nickname": "{{speciesName}} le Grand", + "option": { + "1": { + "label": "{{pokemon1Name}}", + "tooltip_base": "(-) Combat difficile\n(+) Augmente le bonheur de {{pokemon1Name}}" + }, + "2": { + "label": "{{pokemon2Name}}", + "tooltip_base": "(-) Combat difficile\n(+) Augmente le bonheur de {{pokemon2Name}}" + }, + "3": { + "label": "{{pokemon3Name}}", + "tooltip_base": "(-) Combat difficile\n(+) Augmente le bonheur de {{pokemon3Name}}" + }, + "selected": "D’accord, faisons ça !" + }, + "outro": "Ton {{chosenPokemon}} et toi avez\nl’air très heureux !$Tiens, prends ça aussi.", + "outro_failed": "Voilà qui est bien décevant…$T’as encore visiblement bien du chemin à faire\npour acquérir la confiance de tes Pokémon !", + "gained_eggs": "@s{item_fanfare}Vous recevez\n{{numEggs}} !", + "eggs_tooltip": "\n(+) Recevez {{eggs}}", + "numEggs_one": "{{count}} Œuf {{rarity}}", + "numEggs_other": "{{count}} Œufs {{rarity}}s" +} diff --git a/src/locales/fr/mystery-encounters/the-pokemon-salesman-dialogue.json b/src/locales/fr/mystery-encounters/the-pokemon-salesman-dialogue.json new file mode 100644 index 00000000000..401678490fe --- /dev/null +++ b/src/locales/fr/mystery-encounters/the-pokemon-salesman-dialogue.json @@ -0,0 +1,23 @@ +{ + "intro": "Un homme âgé a l’air malicieux vous aborde.", + "speaker": "Gentleman", + "intro_dialogue": "Salutations !\nJ’ai une offre à proposer rien qu’à VOUS !", + "title": "Le vendeur de Pokémon", + "description": "« Cet incroyable et unique {{purchasePokemon}} possède un talent jamais vu dans son espèce ! Ce superbe {{purchasePokemon}} est à vous pour le prix imbattable de {{price, money}} ! »\n\n« Qu’en dites-vous ? »", + "description_shiny": "« Cet incroyable et unique {{purchasePokemon}} possède une pigmentation unique jamais vue dans son espèce ! Ce superbe {{purchasePokemon}} est à vous pour le prix imbattable de {{price, money}} ! »\n\n« Qu’en dites-vous ? »", + "query": "Que voulez-vous faire ?", + "option": { + "1": { + "label": "Accepter", + "tooltip": "(-) Payer {{price, money}}\n(+) {{purchasePokemon}} obtenu avec son talent caché", + "tooltip_shiny": "(-) Payer {{price, money}}\n(+) {{purchasePokemon}} chromatique obtenu", + "selected_message": "Vous déboursez une somme absolument honteuse\npour acheter ce {{purchasePokemon}}.", + "selected_dialogue": "Excellent choix !\nVous avez pour sûr un sens aiguisé des affaires.$Oh, ah euh…@d{64}\nNi repris, ni échangé par contre hein, compris ?" + }, + "2": { + "label": "Refuser", + "tooltip": "(-) Aucune récompense", + "selected": "Non ?@d{32} Vous déclinez ?$C’était pourtant offre en or que je vous faisais là !" + } + } +} diff --git a/src/locales/fr/mystery-encounters/the-strong-stuff-dialogue.json b/src/locales/fr/mystery-encounters/the-strong-stuff-dialogue.json new file mode 100644 index 00000000000..563442e4f1f --- /dev/null +++ b/src/locales/fr/mystery-encounters/the-strong-stuff-dialogue.json @@ -0,0 +1,21 @@ +{ + "intro": "C’est un {{shuckleName}} absolument énorme et\nce qui semble être un énorme bol de… jus ?", + "title": "Un breuvage qui arrache", + "description": "Ce {{shuckleName}} qui bloque la route semble incroyablement puissant.\nLe jus qui l’accompagne semble émaner une étrange forme de puissance.\n\nIl tend une patte dans votre direction, l’air de vouloir quelque chose…", + "query": "Que voulez-vous faire ?", + "option": { + "1": { + "label": "Approcher {{shuckleName}}", + "tooltip": "(?) Quelque chose de terrible ou d’incroyable peut se produire", + "selected": "Vous perdez subitement connaissance.", + "selected_2": "@f{150}À votre réveil, le {{shuckleName}} est parti et\nle bol de jus est complètement vide.${{highBstPokemon1}} et {{highBstPokemon2}}\nsont victimes d’une forte léthargie !$Leurs stats de base sont baissées de {{reductionValue}} !$Mais par contre, vos autres Pokémon sont envahis\nd’une vigueur encore jamais vue !$Leurs stats de base sont augmentées de {{increaseValue}} !" + }, + "2": { + "label": "Affronter {{shuckleName}}", + "tooltip": "(-) Combat difficile\n(+) Récompense spéciale", + "selected": "Le {{shuckleName}} s’énerve, boit un peu de jus et attaque !", + "stat_boost": "Le jus de {{shuckleName}} augmente ses stats !" + } + }, + "outro": "Quelle étrange tournure des évènements…" +} diff --git a/src/locales/fr/mystery-encounters/the-winstrate-challenge-dialogue.json b/src/locales/fr/mystery-encounters/the-winstrate-challenge-dialogue.json new file mode 100644 index 00000000000..144d6a21cde --- /dev/null +++ b/src/locales/fr/mystery-encounters/the-winstrate-challenge-dialogue.json @@ -0,0 +1,22 @@ +{ + "intro": "C’est une famille dans la cour de leur maison !", + "speaker": "La Famille Stratège", + "intro_dialogue": "Nous sommes les Stratège !$Ça te dirait d’affronter la famille dans une série\nde combats ?", + "title": "Le défi de la Famille Stratège", + "description": "Les Stratège sont une famille de 5 Dresseurs, et ne demandent qu’à se battre ! Si vous parvenez à tous les battre à la suite, ils vous remettront une grosse récompense. Vous sentez-vous d’encaisser ce défi ?", + "query": "Que voulez-vous faire ?", + "option": { + "1": { + "label": "Accepter le défi", + "tooltip": "(-) Combat brutal\n(+) Récompense d’un objet spécial", + "selected": "Que le défi commence !" + }, + "2": { + "label": "Décliner le défi", + "tooltip": "(+) Équipe soignée\n(+) Gain d’un Hyper Bonbon", + "selected": "C’est bien dommage. Hé, ton équipe a quand même l’air bien au bout, ça te dirait de rester te reposer un peu ?" + } + }, + "victory": "Félicitations pour avoir relevé notre défi !$Tout d’abord, nous aimerions t’offrir ce Coupon.", + "victory_2": "Mais aussi, notre famille utilise ce Bracelet Macho\npour entrainer plus efficacement nos Pokémon.$Il ne te sera peut-être d’aucune utilité vu que tu nous as tous battus, mais on serait ravis que tu l’accepte !" +} diff --git a/src/locales/fr/mystery-encounters/training-session-dialogue.json b/src/locales/fr/mystery-encounters/training-session-dialogue.json new file mode 100644 index 00000000000..33dcabf5c21 --- /dev/null +++ b/src/locales/fr/mystery-encounters/training-session-dialogue.json @@ -0,0 +1,33 @@ +{ + "intro": "Vous tombez sur du matériel d’entrainement.", + "title": "Session d’entrainement", + "description": "Ce matériel semble pouvoir être utilisé pour entrainer un membre de votre équipe ! Il existe plusieurs moyens avec lesquels vous pourriez entrainer un Pokémon, comme en le faisant combattre le reste de votre équipe.", + "query": "Quel entrainement choisir ?", + "invalid_selection": "Le Pokémon doit être en bonne santé.", + "option": { + "1": { + "label": "Léger", + "tooltip": "(-) Combat léger\n(+) Augmente au hasard 2 IV du Pokémon", + "finished": "{{selectedPokemon}} revient vers vous,\nl’air fatigué mais fier de lui !$Ses IV en {{stat1}} et\nen {{stat2}} augmentent !" + }, + "2": { + "label": "Modéré", + "tooltip": "(-) Combat modéré\n(+) Modifie la nature du Pokémon", + "select_prompt": "Sélectionnez la nature pour laquelle\nvotre Pokémon doit s’entrainer.", + "finished": "{{selectedPokemon}} revient vers vous,\nl’air fatigué mais fier de lui !$Il a beaucoup changé et\nest devenu {{nature}} !" + }, + "3": { + "label": "Intense", + "tooltip": "(-) Combat intense\n(+) Modifie le talent du Pokémon", + "select_prompt": "Sélectionnez le talent pour lequel\nvotre Pokémon doit s’entrainer.", + "finished": "{{selectedPokemon}} revient vers vous,\nl’air fatigué mais fier de lui !$Il a beaucoup changé possède\ndesormais le talent {{ability}} !" + }, + "4": { + "label": "Partir", + "tooltip": "(-) Aucune récompense", + "selected": "Oh la flemme, pas le temps de faire du sport.\nAllons ailleurs." + }, + "selected": "{{selectedPokemon}} va de l’autre côté\ndu terrain pour vous faire face…" + }, + "outro": "Cet entrainement a vraiment été très stimulant !" +} diff --git a/src/locales/fr/mystery-encounters/trash-to-treasure-dialogue.json b/src/locales/fr/mystery-encounters/trash-to-treasure-dialogue.json new file mode 100644 index 00000000000..fb39a1d12a1 --- /dev/null +++ b/src/locales/fr/mystery-encounters/trash-to-treasure-dialogue.json @@ -0,0 +1,19 @@ +{ + "intro": "C’est un énorme tas d’ordures !\nComment il est arrivé là ?", + "title": "20 000 lieues sous la mer-", + "description": "Un énorme tas d’ordures se dresse devant vos yeux, dans lequel vous arrivez à entrevoir quelques objets de valeur.\nPar contre, il faut que vous acceptiez l’idée de vous couvrir de saletés pour allez les récupérer…", + "query": "Que voulez-vous faire ?", + "option": { + "1": { + "label": "Le fouiller", + "tooltip": "(-) Prix de la boutique triplés\n(+) Gain d’objets exceptionnels", + "selected": "Vous barbotez dans le tas d’ordures et\nvous vous couvrez de crasse.$Vu votre état, la prochaine boutique va pour sûr\nfortement gonfler ses prix pour vous forcer à fuir !$Mais ça valait le coup, car ce que vous avez trouvé\ndans les ordures est incroyable !" + }, + "2": { + "label": "Enquêter sur le tas", + "tooltip": "(?) Trouver la source des ordures", + "selected": "Vous vous baladez autour du tas, à la recherche du moindre indice sur la raison de sa présence ici…", + "selected_2": "Oh !\nLes ordures se mettent à bouger !$Le tas est en réalité un Pokémon !\nIl vous attaque !" + } + } +} diff --git a/src/locales/fr/mystery-encounters/uncommon-breed-dialogue.json b/src/locales/fr/mystery-encounters/uncommon-breed-dialogue.json new file mode 100644 index 00000000000..d5f5e7e336f --- /dev/null +++ b/src/locales/fr/mystery-encounters/uncommon-breed-dialogue.json @@ -0,0 +1,26 @@ +{ + "intro": "C’est un Pokémon tout ce qu’il y a de plus banal !", + "title": "Une forme peu commune", + "description": "Ce {{enemyPokemon}} a l’air spécial par rapport au reste de son espèce. @[TOOLTIP_TITLE]{Peut-être connait-il une capacité particulière ?} Vous pourriez décider de l’affronter sur-le-champ, mais il y a peut-être moyen d’en faire un nouvel ami.", + "query": "Que voulez-vous faire ?", + "option": { + "1": { + "label": "L’affronter", + "tooltip": "(-) Combat compliqué\n(+) Capturer un adversaire puissant", + "selected": "Vous approchez\nle Pokémon sans frémir.", + "stat_boost": "Les caractéristiques particulières\nde ce {{enemyPokemon}} augmentent ses stats !" + }, + "2": { + "label": "Le nourrir", + "disabled_tooltip": "Vous avez besoin de 4 Baies pour choisir cette option", + "tooltip": "(-) Donner 4 Baies\n(+) Le {{enemyPokemon}} vous apprécie", + "selected": "Vous lancer quelques Baies\nau {{enemyPokemon}} !$Il les engloutit avec joie !$Le {{enemyPokemon}} veut se joindre\nà votre équipe !" + }, + "3": { + "label": "Devenir amis", + "disabled_tooltip": "Votre Pokémon doit connaitre certaines capacités pour choisir cette option", + "tooltip": "(+) {{option3PrimaryName}} utilise {{option3PrimaryMove}}\n(+) Le {{enemyPokemon}} vous apprécie", + "selected": "Votre {{option3PrimaryName}} utilise {{option3PrimaryMove}} pour charmer le {{enemyPokemon}} !$The {{enemyPokemon}} veut se joindre\nà votre équipe !" + } + } +} diff --git a/src/locales/fr/mystery-encounters/weird-dream-dialogue.json b/src/locales/fr/mystery-encounters/weird-dream-dialogue.json new file mode 100644 index 00000000000..7cbcc8af30e --- /dev/null +++ b/src/locales/fr/mystery-encounters/weird-dream-dialogue.json @@ -0,0 +1,22 @@ +{ + "intro": "Une femme aux airs sombres vous coupe la route.\nElle dégage comme une aura troublante…", + "speaker": "Femme mystérieuse", + "intro_dialogue": "J’ai tout vu…\nTes futurs, tes passés…$Les perçois-tu aussi, mon enfant ?", + "title": "???", + "description": "Les paroles de cette mystérieuse femme raisonnent dans votre tête. Ce n’est pas qu’une simple voix isolée, mais une infinité, d’une infinité d’espace-temps et de réalités.\nVous commencez ressentir des vertiges à mesure que cette question s’empare de votre esprit…\n\n@[TOOLTIP_TITLE]{« J’ai tout vu… Tes futurs, tes passés… Les perçois-tu aussi, mon enfant ? »}", + "query": "Que voulez-vous faire ?", + "option": { + "1": { + "label": "« Je les vois. »", + "tooltip": "@[SUMMARY_GREEN]{(?) Affecte vos Pokémon}", + "selected": "Sa main s’approche de vous et vous touche.\nTout devient subitement sombre.$Puis enfin…@d{64} La lumière.\nChaque espace-temps. Chacune de vos incarnations.$Tout ce qui a composé, compose\net composera votre être…@d{64}", + "cutscene": "Vous percevez vos Pokémon,@d{32} convergeant de chaque\nréalité pour former quelque chose de nouveau…@d{64}", + "dream_complete": "Vous vous réveillez, mais la femme a disparu…\nOu bien n’était-elle qu’une hallucination ?$.@d{32}.@d{32}.@d{32}$Les Pokémon de votre équipe ont changé…$Mais alors, comment peuvent-ils quand même\ntoujours vous sembler si familiers ?" + }, + "2": { + "label": "Partir en courant", + "tooltip": "(-) Affecte vos Pokémon", + "selected": "Vous parvenez à vous extraire de son emprise\net prenez vos jambes à votre cou.$Vous vous remettez de vos émotions et\nvérifiez si votre équipe va bien.$Mais pour une raison mystérieuse,\nils ont tous perdu des niveaux !" + } + } +} diff --git a/src/locales/fr/party-ui-handler.json b/src/locales/fr/party-ui-handler.json index 6adba2c8309..045e3e4fe86 100644 --- a/src/locales/fr/party-ui-handler.json +++ b/src/locales/fr/party-ui-handler.json @@ -13,8 +13,10 @@ "ALL": "Tout", "PASS_BATON": "Relais", "UNPAUSE_EVOLUTION": "Réactiver Évolution", + "PAUSE_EVOLUTION": "Empêcher Évolution", "REVIVE": "Ranimer", "RENAME": "Renommer", + "SELECT": "Sélectionner", "choosePokemon": "Sélectionnez un Pokémon.", "doWhatWithThisPokemon": "Que faire avec ce Pokémon ?", "noEnergy": "{{pokemonName}} n’a plus l’énergie\nde se battre !", @@ -23,6 +25,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 +47,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/status-effect.json b/src/locales/fr/status-effect.json index bfb7121f522..13bcf59ef00 100644 --- a/src/locales/fr/status-effect.json +++ b/src/locales/fr/status-effect.json @@ -1,12 +1,6 @@ { "none": { - "name": "Aucun", - "description": "", - "obtain": "", - "obtainSource": "", - "activation": "", - "overlap": "", - "heal": "" + "name": "Aucun" }, "poison": { "name": "Empoisonnement", 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..ff9e0d47721 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", @@ -160,5 +166,18 @@ "alder_iris_double": "Goyah & Iris", "iris_alder_double": "Iris & Goyah", "marnie_piers_double": "Rosemary & Peterson", - "piers_marnie_double": "Peterson & Rosemary" + "piers_marnie_double": "Peterson & Rosemary", + + "buck": "Cornil", + "cheryl": "Sara", + "marley": "Viviane", + "mira": "Maïté", + "riley": "Armand", + "victor": "Atrée", + "victoria": "Électre", + "vivi": "Andromaque", + "vicky": "Clytemnestre", + "vito": "Adalbert", + "bug_type_superfan": "Fan d’entomologie", + "expert_pokemon_breeder": "Éleveuse Experte" } diff --git a/src/locales/fr/trainer-titles.json b/src/locales/fr/trainer-titles.json index 6d966bbd9ec..afcb755d034 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,8 @@ "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", + + "the_winstrates": "Famille Stratège" } diff --git a/src/locales/fr/tutorial.json b/src/locales/fr/tutorial.json index 7936987457f..3236bdafea2 100644 --- a/src/locales/fr/tutorial.json +++ b/src/locales/fr/tutorial.json @@ -6,5 +6,5 @@ "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\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. 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 !" + "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/achv.json b/src/locales/it/achv.json index d1607f6c548..c4a7d3dab7b 100644 --- a/src/locales/it/achv.json +++ b/src/locales/it/achv.json @@ -260,5 +260,13 @@ "FRESH_START": { "name": "Buona la prima!", "description": "Completa la modalità sfida 'Un nuovo inizio'." + }, + "BREEDERS_IN_SPACE": { + "name": "Breeders in Space!", + "description": "Beat the Expert Pokémon Breeder in the Space Biome." + }, + "BREEDERS_IN_SPACE": { + "name": "Breeders in Space!", + "description": "Beat the Expert Pokémon Breeder in the Space Biome." } } diff --git a/src/locales/it/battle.json b/src/locales/it/battle.json index 9b187756025..e11399dff6c 100644 --- a/src/locales/it/battle.json +++ b/src/locales/it/battle.json @@ -14,6 +14,10 @@ "moneyWon": "Hai vinto {{moneyAmount}}₽", "moneyPickedUp": "Hai raccolto ₽{{moneyAmount}}!", "pokemonCaught": "Preso! {{pokemonName}} è stato catturato!", + "pokemonObtained": "Hai ricevuto {{pokemonName}}!", + "pokemonBrokeFree": "Oh no!\nIl Pokémon è uscito dalla Poké Ball!", + "pokemonFled": "{pokemonName}} selvatico è fuggito!", + "playerFled": "Sei fuggito/a da {{pokemonName}}!", "addedAsAStarter": "{{pokemonName}} è stato\naggiunto agli starter!", "partyFull": "La tua squadra è al completo.\nVuoi liberare un Pokémon per far spazio a {{pokemonName}}?", "pokemon": "Pokémon", @@ -49,6 +53,7 @@ "noPokeballTrainer": "Non puoi catturare\nPokémon di altri allenatori!", "noPokeballMulti": "Puoi lanciare una Poké Ball\nsolo quando rimane un singolo Pokémon!", "noPokeballStrong": "Il Pokémon avversario è troppo forte per essere catturato!\nDevi prima indebolirlo.", + "noPokeballMysteryEncounter": "Non ti è possibile\ncatturare questo Pokémon!", "noEscapeForce": "Una forza misteriosa\nimpedisce la fuga.", "noEscapeTrainer": "Non puoi sottrarti\nalla lotta con un'allenatore!", "noEscapePokemon": "{{moveName}} di {{pokemonName}}\npreviene la {{escapeVerb}}!", @@ -95,5 +100,6 @@ "unlockedSomething": "{{unlockedThing}}\nè stato/a sbloccato/a.", "congratulations": "Congratulazioni!", "beatModeFirstTime": "{{speciesName}} ha completato la modalità {{gameMode}} per la prima volta!\nHai ricevuto {{newModifier}}!", - "ppReduced": "I PP della mossa {{moveName}} di\n{{targetName}} sono stati ridotti di {{reduction}}!" + "ppReduced": "I PP della mossa {{moveName}} di\n{{targetName}} sono stati ridotti di {{reduction}}!", + "mysteryEncounterAppeared": "Che succede?" } diff --git a/src/locales/it/bgm-name.json b/src/locales/it/bgm-name.json index 9e26dfeeb6e..5d943512d4b 100644 --- a/src/locales/it/bgm-name.json +++ b/src/locales/it/bgm-name.json @@ -1 +1,7 @@ -{} \ No newline at end of file +{ + "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/it/config.ts b/src/locales/it/config.ts index a8cd1e4e0bd..129c0f67466 100644 --- a/src/locales/it/config.ts +++ b/src/locales/it/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 "./mystery-encounters/mysterious-chest-dialogue.json"; +import mysteriousChallengers from "./mystery-encounters/mysterious-challengers-dialogue.json"; +import darkDeal from "./mystery-encounters/dark-deal-dialogue.json"; +import departmentStoreSale from "./mystery-encounters/department-store-sale-dialogue.json"; +import fieldTrip from "./mystery-encounters/field-trip-dialogue.json"; +import fieryFallout from "./mystery-encounters/fiery-fallout-dialogue.json"; +import fightOrFlight from "./mystery-encounters/fight-or-flight-dialogue.json"; +import safariZone from "./mystery-encounters/safari-zone-dialogue.json"; +import shadyVitaminDealer from "./mystery-encounters/shady-vitamin-dealer-dialogue.json"; +import slumberingSnorlax from "./mystery-encounters/slumbering-snorlax-dialogue.json"; +import trainingSession from "./mystery-encounters/training-session-dialogue.json"; +import theStrongStuff from "./mystery-encounters/the-strong-stuff-dialogue.json"; +import pokemonSalesman from "./mystery-encounters/the-pokemon-salesman-dialogue.json"; +import offerYouCantRefuse from "./mystery-encounters/an-offer-you-cant-refuse-dialogue.json"; +import delibirdy from "./mystery-encounters/delibirdy-dialogue.json"; +import absoluteAvarice from "./mystery-encounters/absolute-avarice-dialogue.json"; +import aTrainersTest from "./mystery-encounters/a-trainers-test-dialogue.json"; +import trashToTreasure from "./mystery-encounters/trash-to-treasure-dialogue.json"; +import berriesAbound from "./mystery-encounters/berries-abound-dialogue.json"; +import clowningAround from "./mystery-encounters/clowning-around-dialogue.json"; +import partTimer from "./mystery-encounters/part-timer-dialogue.json"; +import dancingLessons from "./mystery-encounters/dancing-lessons-dialogue.json"; +import weirdDream from "./mystery-encounters/weird-dream-dialogue.json"; +import theWinstrateChallenge from "./mystery-encounters/the-winstrate-challenge-dialogue.json"; +import teleportingHijinks from "./mystery-encounters/teleporting-hijinks-dialogue.json"; +import bugTypeSuperfan from "./mystery-encounters/bug-type-superfan-dialogue.json"; +import funAndGames from "./mystery-encounters/fun-and-games-dialogue.json"; +import uncommonBreed from "./mystery-encounters/uncommon-breed-dialogue.json"; +import globalTradeSystem from "./mystery-encounters/global-trade-system-dialogue.json"; +import expertPokemonBreeder from "./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 itConfig = { ability, abilityTriggers, @@ -110,4 +152,40 @@ export const itConfig = { modifierSelectUiHandler, moveTriggers, 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/it/dialogue.json b/src/locales/it/dialogue.json index 9e26dfeeb6e..dd82247493b 100644 --- a/src/locales/it/dialogue.json +++ b/src/locales/it/dialogue.json @@ -1 +1,112 @@ -{} \ No newline at end of file +{ + "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..." + } + } +} diff --git a/src/locales/it/egg.json b/src/locales/it/egg.json index 9f3baddf975..49f00ca5939 100644 --- a/src/locales/it/egg.json +++ b/src/locales/it/egg.json @@ -11,6 +11,7 @@ "gachaTypeLegendary": "Tasso dei leggendari aumentato", "gachaTypeMove": "Tasso delle mosse rare da uova aumentato", "gachaTypeShiny": "Tasso degli shiny aumentato", + "eventType": "Evento Misterioso", "selectMachine": "Seleziona un macchinario.", "notEnoughVouchers": "Non hai abbastanza biglietti!", "tooManyEggs": "Hai troppe uova!", @@ -23,4 +24,4 @@ "moveUPGacha": "Mossa +", "shinyUPGacha": "Shiny +", "legendaryUPGacha": "+" -} \ No newline at end of file +} diff --git a/src/locales/it/modifier-select-ui-handler.json b/src/locales/it/modifier-select-ui-handler.json index f07d8dd2680..62770999817 100644 --- a/src/locales/it/modifier-select-ui-handler.json +++ b/src/locales/it/modifier-select-ui-handler.json @@ -8,5 +8,7 @@ "lockRaritiesDesc": "Blocca le rarità al reroll (influisce sui costi).", "checkTeamDesc": "Controlla la squadra Pokémon.", "rerollCost": "{{formattedMoney}}₽", - "itemCost": "{{formattedMoney}}₽" -} \ No newline at end of file + "itemCost": "{{formattedMoney}}₽", + "continueNextWaveButton": "Continua", + "continueNextWaveDescription": "Continua alla onda successiva" +} diff --git a/src/locales/it/modifier-type.json b/src/locales/it/modifier-type.json index f06755bdfa0..9edf6e5a53a 100644 --- a/src/locales/it/modifier-type.json +++ b/src/locales/it/modifier-type.json @@ -68,6 +68,20 @@ "BaseStatBoosterModifierType": { "description": "Aumenta l'/la {{stat}} di base del possessore del 10%." }, + "PokemonBaseStatTotalModifierType": { + "name": "Succo Shuckle", + "description": "{{increaseDecrease}} tutte le statistiche del possessore di {{statValue}}. Shuckle ti ha {{blessCurse}}.", + "extra": { + "increase": "Aumenta", + "decrease": "Diminuisce", + "blessed": "benedetto/a", + "cursed": "maledetto/a" + } + }, + "PokemonBaseStatFlatModifierType": { + "name": "Dolce Gateau", + "description": "Aumenta le statistiche {{stats}} del possessore di {{statValue}}. Trovato dopo uno strano sogno." + }, "AllPokemonFullHpRestoreModifierType": { "description": "Restituisce il 100% dei PS a tutti i Pokémon." }, @@ -401,7 +415,13 @@ "ENEMY_FUSED_CHANCE": { "name": "Gettone della fusione", "description": "Aggiunge l'1% di possibilità che un Pokémon selvatico sia una fusione." - } + }, + + "MYSTERY_ENCOUNTER_SHUCKLE_JUICE": { "name": "Succo Shuckle" }, + "MYSTERY_ENCOUNTER_BLACK_SLUDGE": { "name": "Fangopece", "description": "L'odore è talmente sgradevole che i prezzi dei negozi aumentano drasticamente." }, + "MYSTERY_ENCOUNTER_MACHO_BRACE": { "name": "Crescicappa", "description": "Sconfiggere un Pokémon aumenta di 1 il punteggio Crescicappa. Maggiore il punteggio, maggiore l'aumento alle statistiche, con un ulteriore bonus al massimo." }, + "MYSTERY_ENCOUNTER_OLD_GATEAU": { "name": "Dolce Gateau", "description": "Aumenta le statistiche {{stats}} del possessore di {{statValue}}." }, + "MYSTERY_ENCOUNTER_GOLDEN_BUG_NET": { "name": "Retino Dorato", "description": "Infonde fortuna nel possessore affinché trovi più Pokémon coleottero. Ha uno strano peso." } }, "SpeciesBoosterItem": { "LIGHT_BALL": { diff --git a/src/locales/it/move-trigger.json b/src/locales/it/move-trigger.json index c8fb390e53f..fba671a6813 100644 --- a/src/locales/it/move-trigger.json +++ b/src/locales/it/move-trigger.json @@ -66,6 +66,7 @@ "revivalBlessing": "{{pokemonName}} torna in forze!", "swapArenaTags": "{{pokemonName}} ha invertito gli effetti attivi\nnelle due metà del campo!", "exposedMove": "{{pokemonName}} ha identificato\n{{targetPokemonName}}!", + "chillyReception": "{{pokemonName}} sta per fare una battuta!", "safeguard": "Salvaguardia protegge {{targetName}}!", "afterYou": "{{pokemonName}} approfitta della cortesia!" } 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/mystery-encounter-messages.json b/src/locales/it/mystery-encounter-messages.json new file mode 100644 index 00000000000..4f8b528d18b --- /dev/null +++ b/src/locales/it/mystery-encounter-messages.json @@ -0,0 +1,7 @@ +{ + "paid_money": "Hai pagato {{amount, number}}₽.", + "receive_money": "Hai ricevuto {{amount, number}}₽!", + "affects_pokedex": "Influisce sul Pokédex", + "cancel_option": "Torna alla scelta dell'incontro.", + "view_party_button": "Info squadra" +} diff --git a/src/locales/it/mystery-encounters/a-trainers-test-dialogue.json b/src/locales/it/mystery-encounters/a-trainers-test-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/it/mystery-encounters/a-trainers-test-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/it/mystery-encounters/absolute-avarice-dialogue.json b/src/locales/it/mystery-encounters/absolute-avarice-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/it/mystery-encounters/absolute-avarice-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/it/mystery-encounters/an-offer-you-cant-refuse-dialogue.json b/src/locales/it/mystery-encounters/an-offer-you-cant-refuse-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/it/mystery-encounters/an-offer-you-cant-refuse-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/it/mystery-encounters/berries-abound-dialogue.json b/src/locales/it/mystery-encounters/berries-abound-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/it/mystery-encounters/berries-abound-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/it/mystery-encounters/bug-type-superfan-dialogue.json b/src/locales/it/mystery-encounters/bug-type-superfan-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/it/mystery-encounters/bug-type-superfan-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/it/mystery-encounters/clowning-around-dialogue.json b/src/locales/it/mystery-encounters/clowning-around-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/it/mystery-encounters/clowning-around-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/it/mystery-encounters/dancing-lessons-dialogue.json b/src/locales/it/mystery-encounters/dancing-lessons-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/it/mystery-encounters/dancing-lessons-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/it/mystery-encounters/dark-deal-dialogue.json b/src/locales/it/mystery-encounters/dark-deal-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/it/mystery-encounters/dark-deal-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/it/mystery-encounters/delibirdy-dialogue.json b/src/locales/it/mystery-encounters/delibirdy-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/it/mystery-encounters/delibirdy-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/it/mystery-encounters/department-store-sale-dialogue.json b/src/locales/it/mystery-encounters/department-store-sale-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/it/mystery-encounters/department-store-sale-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/it/mystery-encounters/field-trip-dialogue.json b/src/locales/it/mystery-encounters/field-trip-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/it/mystery-encounters/field-trip-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/it/mystery-encounters/fiery-fallout-dialogue.json b/src/locales/it/mystery-encounters/fiery-fallout-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/it/mystery-encounters/fiery-fallout-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/it/mystery-encounters/fight-or-flight-dialogue.json b/src/locales/it/mystery-encounters/fight-or-flight-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/it/mystery-encounters/fight-or-flight-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/it/mystery-encounters/fun-and-games-dialogue.json b/src/locales/it/mystery-encounters/fun-and-games-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/it/mystery-encounters/fun-and-games-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/it/mystery-encounters/global-trade-system-dialogue.json b/src/locales/it/mystery-encounters/global-trade-system-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/it/mystery-encounters/global-trade-system-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/it/mystery-encounters/lost-at-sea-dialogue.json b/src/locales/it/mystery-encounters/lost-at-sea-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/it/mystery-encounters/lost-at-sea-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/it/mystery-encounters/mysterious-challengers-dialogue.json b/src/locales/it/mystery-encounters/mysterious-challengers-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/it/mystery-encounters/mysterious-challengers-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/it/mystery-encounters/mysterious-chest-dialogue.json b/src/locales/it/mystery-encounters/mysterious-chest-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/it/mystery-encounters/mysterious-chest-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/it/mystery-encounters/part-timer-dialogue.json b/src/locales/it/mystery-encounters/part-timer-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/it/mystery-encounters/part-timer-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/it/mystery-encounters/safari-zone-dialogue.json b/src/locales/it/mystery-encounters/safari-zone-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/it/mystery-encounters/safari-zone-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/it/mystery-encounters/shady-vitamin-dealer-dialogue.json b/src/locales/it/mystery-encounters/shady-vitamin-dealer-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/it/mystery-encounters/shady-vitamin-dealer-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/it/mystery-encounters/slumbering-snorlax-dialogue.json b/src/locales/it/mystery-encounters/slumbering-snorlax-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/it/mystery-encounters/slumbering-snorlax-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/it/mystery-encounters/teleporting-hijinks-dialogue.json b/src/locales/it/mystery-encounters/teleporting-hijinks-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/it/mystery-encounters/teleporting-hijinks-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/it/mystery-encounters/the-expert-pokemon-breeder-dialogue.json b/src/locales/it/mystery-encounters/the-expert-pokemon-breeder-dialogue.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/src/locales/it/mystery-encounters/the-expert-pokemon-breeder-dialogue.json @@ -0,0 +1 @@ +{} diff --git a/src/locales/it/mystery-encounters/the-pokemon-salesman-dialogue.json b/src/locales/it/mystery-encounters/the-pokemon-salesman-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/it/mystery-encounters/the-pokemon-salesman-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/it/mystery-encounters/the-strong-stuff-dialogue.json b/src/locales/it/mystery-encounters/the-strong-stuff-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/it/mystery-encounters/the-strong-stuff-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/it/mystery-encounters/the-winstrate-challenge-dialogue.json b/src/locales/it/mystery-encounters/the-winstrate-challenge-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/it/mystery-encounters/the-winstrate-challenge-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/it/mystery-encounters/training-session-dialogue.json b/src/locales/it/mystery-encounters/training-session-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/it/mystery-encounters/training-session-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/it/mystery-encounters/trash-to-treasure-dialogue.json b/src/locales/it/mystery-encounters/trash-to-treasure-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/it/mystery-encounters/trash-to-treasure-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/it/mystery-encounters/uncommon-breed-dialogue.json b/src/locales/it/mystery-encounters/uncommon-breed-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/it/mystery-encounters/uncommon-breed-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/it/mystery-encounters/weird-dream-dialogue.json b/src/locales/it/mystery-encounters/weird-dream-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/it/mystery-encounters/weird-dream-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/it/party-ui-handler.json b/src/locales/it/party-ui-handler.json index 95466779727..7bcb5ccf7a0 100644 --- a/src/locales/it/party-ui-handler.json +++ b/src/locales/it/party-ui-handler.json @@ -15,6 +15,7 @@ "UNPAUSE_EVOLUTION": "Consenti evoluzione", "REVIVE": "Revitalizza", "RENAME": "Rinomina", + "SELECT": "Seleziona", "choosePokemon": "Scegli un Pokémon.", "doWhatWithThisPokemon": "Hai selezionato questo Pokémon.", "noEnergy": "{{pokemonName}} non ha più energie\nper lottare!", 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/status-effect.json b/src/locales/it/status-effect.json index 6270bbb10a5..bbf96f2ec82 100644 --- a/src/locales/it/status-effect.json +++ b/src/locales/it/status-effect.json @@ -1,11 +1,5 @@ { "none": { - "name": "None", - "description": "", - "obtain": "", - "obtainSource": "", - "activation": "", - "overlap": "", - "heal": "" + "name": "None" } } \ No newline at end of file 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..b29fd2e9f6f 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", @@ -156,5 +166,19 @@ "alder_iris_double": "Nardo & Iris", "iris_alder_double": "Iris & Nardo", "marnie_piers_double": "Mary & Ginepro", - "piers_marnie_double": "Ginepro & Mary" + "piers_marnie_double": "Ginepro & Mary", + + "buck": "Chicco", + "cheryl": "Demetra", + "marley": "Risetta", + "mira": "Matilde", + "riley": "Fabiolo", + "victor": "Vincenzo", + "victoria": "Vittoria", + "vivi": "Viviana", + "vicky": "Vicky", + "vito": "Enrico", + + "bug_type_superfan": "Fan n.1 dei tipi Coleottero", + "expert_pokemon_breeder": "Expert Pokémon Breeder" } diff --git a/src/locales/it/trainer-titles.json b/src/locales/it/trainer-titles.json index eff152795cd..c2408a071ad 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,9 @@ "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", + + "the_winstrates": "Famiglia Vinci'" } 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/achv.json b/src/locales/ja/achv.json index fd5e4b9e6c4..ad85e8aeb05 100644 --- a/src/locales/ja/achv.json +++ b/src/locales/ja/achv.json @@ -265,5 +265,9 @@ "INVERSE_BATTLE": { "name": "カガミよミガカ", "description": "反転バトルチャレンジを クリアする\nるすアリク をジンレャチルトバ転反" + }, + "BREEDERS_IN_SPACE": { + "name": "宇宙のブリーダー!", + "description": "宇宙バイオームで エキスパートブリーダーを 倒す" } } diff --git a/src/locales/ja/battle.json b/src/locales/ja/battle.json index 1fe24068cdf..7402c6155b4 100644 --- a/src/locales/ja/battle.json +++ b/src/locales/ja/battle.json @@ -96,5 +96,6 @@ "unlockedSomething": "{{unlockedThing}}\nを アンロックした!", "congratulations": "おめでとうございます!!", "beatModeFirstTime": "初めて {{speciesName}}が {{gameMode}}モードを クリアした!\n{{newModifier}}を 手に入れた!", - "ppReduced": "{{targetName}}の {{moveName}}を {{reduction}}削った!" + "ppReduced": "{{targetName}}の {{moveName}}を {{reduction}}削った!", + "mysteryEncounterAppeared": "あれ?" } diff --git a/src/locales/ja/bgm-name.json b/src/locales/ja/bgm-name.json index fc3d4c0fdd2..fbb86deaa0d 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 視線!バックパッカー", @@ -146,5 +149,11 @@ "encounter_youngster": "BW 視線!たんぱんこぞう", "heal": "BW 回復", "menu": "ポケダン空 ようこそ! ポケモンたちのせかいへ!", - "title": "ポケダン空 トップメニュー" + "title": "ポケダン空 トップメニュー", + + "mystery_encounter_weird_dream": "ポケダン空 じげんのとう", + "mystery_encounter_fun_and_games": "ポケダン空 おやかたプクリン", + "mystery_encounter_gen_5_gts": "BW GTS", + "mystery_encounter_gen_6_gts": "XY GTS", + "mystery_encounter_delibirdy": "Firel - DeliDelivery! (デーリーデーリー!)" } diff --git a/src/locales/ja/config.ts b/src/locales/ja/config.ts index f8afd6eb167..f806a792d39 100644 --- a/src/locales/ja/config.ts +++ b/src/locales/ja/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 "./mystery-encounters/mysterious-chest-dialogue.json"; +import mysteriousChallengers from "./mystery-encounters/mysterious-challengers-dialogue.json"; +import darkDeal from "./mystery-encounters/dark-deal-dialogue.json"; +import departmentStoreSale from "./mystery-encounters/department-store-sale-dialogue.json"; +import fieldTrip from "./mystery-encounters/field-trip-dialogue.json"; +import fieryFallout from "./mystery-encounters/fiery-fallout-dialogue.json"; +import fightOrFlight from "./mystery-encounters/fight-or-flight-dialogue.json"; +import safariZone from "./mystery-encounters/safari-zone-dialogue.json"; +import shadyVitaminDealer from "./mystery-encounters/shady-vitamin-dealer-dialogue.json"; +import slumberingSnorlax from "./mystery-encounters/slumbering-snorlax-dialogue.json"; +import trainingSession from "./mystery-encounters/training-session-dialogue.json"; +import theStrongStuff from "./mystery-encounters/the-strong-stuff-dialogue.json"; +import pokemonSalesman from "./mystery-encounters/the-pokemon-salesman-dialogue.json"; +import offerYouCantRefuse from "./mystery-encounters/an-offer-you-cant-refuse-dialogue.json"; +import delibirdy from "./mystery-encounters/delibirdy-dialogue.json"; +import absoluteAvarice from "./mystery-encounters/absolute-avarice-dialogue.json"; +import aTrainersTest from "./mystery-encounters/a-trainers-test-dialogue.json"; +import trashToTreasure from "./mystery-encounters/trash-to-treasure-dialogue.json"; +import berriesAbound from "./mystery-encounters/berries-abound-dialogue.json"; +import clowningAround from "./mystery-encounters/clowning-around-dialogue.json"; +import partTimer from "./mystery-encounters/part-timer-dialogue.json"; +import dancingLessons from "./mystery-encounters/dancing-lessons-dialogue.json"; +import weirdDream from "./mystery-encounters/weird-dream-dialogue.json"; +import theWinstrateChallenge from "./mystery-encounters/the-winstrate-challenge-dialogue.json"; +import teleportingHijinks from "./mystery-encounters/teleporting-hijinks-dialogue.json"; +import bugTypeSuperfan from "./mystery-encounters/bug-type-superfan-dialogue.json"; +import funAndGames from "./mystery-encounters/fun-and-games-dialogue.json"; +import uncommonBreed from "./mystery-encounters/uncommon-breed-dialogue.json"; +import globalTradeSystem from "./mystery-encounters/global-trade-system-dialogue.json"; +import expertPokemonBreeder from "./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 jaConfig = { ability, abilityTriggers, @@ -110,4 +152,40 @@ export const jaConfig = { modifierSelectUiHandler, moveTriggers, 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/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..9f47d9c31ef 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,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": "……言っとくけど オレ 強いからな! 驚けよ!", + "2": "モンスターボールの 中で オレの ポケモン達 震えてる!\nこれ 武者震いって やつだな!" + }, + "victory": { + "1": "イヒヒ!\nあっちーな  おまえ!!", + "2": "イヒヒ!\nあっちーな  おまえ!!" + }, + "defeat": { + "1": "おっと! 元気切れか", + "2": "おっと! 元気切れか" + } + }, + "stat_trainer_cheryl": { + "encounter": { + "1": "あたしの ポケモンたち 戦いたくて ウズウズしてるよ", + "2": "あたしの ポケモン達 結構 ヤンチャですわよ" + }, + "victory": { + "1": "攻めること 守ること その バランスは 大変ですね…", + "2": "攻めること 守ること その バランスは 大変ですね…" + }, + "defeat": { + "1": "早く ポケモンを 回復してくださいね", + "2": "早く ポケモンを 回復してくださいね?" + } + }, + "stat_trainer_marley": { + "encounter": { + "1": "……わかった\nできるだけ がんばるから", + "2": "……わかった\nあたし…… 負けないから……!" + }, + "victory": { + "1": "……もう", + "2": "……もう" + }, + "defeat": { + "1": "…… サヨナラ.", + "2": "…… サヨナラ" + } + }, + "stat_trainer_mira": { + "encounter": { + "1": "ミル ビックリしちゃいますよ!", + "2": "もう 迷ったりしない ってところ\nトレーナーさんに 見せるからね!" + }, + "victory": { + "1": "これだと この地方で\nミル 活躍 できないかな…", + "2": "これだと この地方で\nミル 活躍 できないかな…" + }, + "defeat": { + "1": "やった! ミル 最強!", + "2": "やった! ミル 最強!" + } + }, + "stat_trainer_riley": { + "encounter": { + "1": "ポケモン勝負 こそが 私達の 挨拶さ!", + "2": "全力を出して 君の ポケモンを 倒して見せるよ" + }, + "victory": { + "1": "戦ったり 組んでみたり……$トレーナー同士って いいよね", + "2": "戦ったり 組んでみたり……$トレーナー同士って いいよね" + }, + "defeat": { + "1": "よく 頑張ってた…\n次が あるよ", + "2": "よく 頑張ってた…\n次が あるよ" + } + }, + "winstrates_victor": { + "encounter": { + "1": "いい 度胸だ! 気に入ったぞ!" + }, + "victory": { + "1": "たはーっ! 思っていたより 強いんだね きみは!" + } + }, + "winstrates_victoria": { + "encounter": { + "1": "あれま! 意外に 若いのね!$うちの 主人に 勝ってしまうとは なんと 凄腕の トレーナー なの!$じゃあ こんどは 私と 勝負よ!" + }, + "victory": { + "1": "んまーっ! あなたって なんて 強いのかしら!" + } + }, + "winstrates_vivi": { + "encounter": { + "1": "ママより 強いなんて すごーい!$でも あたし だって 強いんだから!\nほんと なんだってば!" + }, + "victory": { + "1": "悔しい……\n……ぐっすん! おばあちゃーん!!" + } + }, + "winstrates_vicky": { + "encounter": { + "1": "こらーっ! わしの かわいい 孫に 何すんじゃ!$こうなったら わしが おまえさんの\nポケモンを こらしめちゃるから 覚悟せぇ!" + }, + "victory": { + "1": "フガッ! 強いのう……\n孫の いうことは 本当 じゃった" + } + }, + "winstrates_vito": { + "encounter": { + "1": "家族 全員で\nポケモンの 修行を してたんだ!$誰にも 負けないぜ!" + }, + "victory": { + "1": "家族の 誰よりも つよかった おれ……\n今まで 誰にも 負けなかった おれ……" + } + }, "brock": { "encounter": { "1": "My expertise on Rock-type Pokémon will take you down! Come on!", @@ -2718,7 +2921,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 +2937,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 +2956,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 +2975,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 +2986,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 +3013,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/egg.json b/src/locales/ja/egg.json index 91b1442c56c..9ed31bfc04b 100644 --- a/src/locales/ja/egg.json +++ b/src/locales/ja/egg.json @@ -11,6 +11,7 @@ "gachaTypeLegendary": "伝説確率アップ", "gachaTypeMove": "レアなタマゴ技確率アップ", "gachaTypeShiny": "色違い確率アップ", + "eventType": "謎イベント", "selectMachine": "ガチャマシンを選択", "notEnoughVouchers": "タマゴクーポンが足りません!", "tooManyEggs": "タマゴが一杯です!", 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/modifier-select-ui-handler.json b/src/locales/ja/modifier-select-ui-handler.json index d7428c8e373..d5cd199063b 100644 --- a/src/locales/ja/modifier-select-ui-handler.json +++ b/src/locales/ja/modifier-select-ui-handler.json @@ -8,5 +8,7 @@ "lockRaritiesDesc": "選択肢を 変更するときの レア度を 固定する\n(選択肢変更の価格は変わる)", "checkTeamDesc": "手持ちポケモンの 状態を 確認する\nフォルムチェンジアイテムを 有効・無効にする", "rerollCost": "{{formattedMoney}}円", - "itemCost": "{{formattedMoney}}円" + "itemCost": "{{formattedMoney}}円", + "continueNextWaveButton": "続く", + "continueNextWaveDescription": "次の ラウンドに 続く" } diff --git a/src/locales/ja/modifier-type.json b/src/locales/ja/modifier-type.json index a2e62bd941e..b217ab054b7 100644 --- a/src/locales/ja/modifier-type.json +++ b/src/locales/ja/modifier-type.json @@ -68,6 +68,20 @@ "BaseStatBoosterModifierType": { "description": "ポケモンの 基本の{{stat}}を 10% あげる。\n個体値が 高けば高いほど 持てる限界が 上がる" }, + "PokemonBaseStatTotalModifierType": { + "name": "ツボツボジュース", + "description": "持たせると 基本能力が {{statValue}} {{increaseDecrease}}。ツボツボに {{blessCurse}}。", + "extra": { + "increase": "上がる", + "decrease": "下がる", + "blessed": "恵まれた", + "cursed": "呪われた" + } + }, + "PokemonBaseStatFlatModifierType": { + "name": "もりのヨウカン", + "description": "持たせると 基本能力が {{statValue}} 上がる。 謎の夢で 見つけた お菓子。" + }, "AllPokemonFullHpRestoreModifierType": { "description": "手持ちポケモン 全員の HPを すべて 回復する" }, @@ -247,7 +261,13 @@ "ENEMY_ATTACK_BURN_CHANCE": { "name": "やけどトークン" }, "ENEMY_STATUS_EFFECT_HEAL_CHANCE": { "name": "なんでもなおしトークン", "description": "毎ターン 状態異常を 治せる 2.5%の可能性を 加える" }, "ENEMY_ENDURE_CHANCE": { "name": "こらえるトークン" }, - "ENEMY_FUSED_CHANCE": { "name": "がったいトークン", "description": "野生ポケモンは 吸収合体している 1%の可能性を 加える" } + "ENEMY_FUSED_CHANCE": { "name": "がったいトークン", "description": "野生ポケモンは 吸収合体している 1%の可能性を 加える" }, + + "MYSTERY_ENCOUNTER_SHUCKLE_JUICE": { "name": "ツボツボジュース" }, + "MYSTERY_ENCOUNTER_BLACK_SLUDGE": { "name": "くろいヘドロ", "description": "ムゴい 悪臭を放つから ショップが アイテムの 価格を ぐんと 上げる" }, + "MYSTERY_ENCOUNTER_MACHO_BRACE": { "name": "きょうせいギプス", "description": "持たせると 相手を倒すとき もう1つの ギプスを もらう。 ギプス当たり 能力が 少し 上がる。 最多で さらに 上がる" }, + "MYSTERY_ENCOUNTER_OLD_GATEAU": { "name": "もりのヨウカン", "description": "持たせると 基本能力が {{statValue}} 上がる" }, + "MYSTERY_ENCOUNTER_GOLDEN_BUG_NET": { "name": "きんのむしとりあみ", "description": "むしタイプの ポケモンが 見つけやすく なる運に 注ぎ込まれた 妙に重い 虫取り網" } }, "SpeciesBoosterItem": { "LIGHT_BALL": { "name": "でんきだま", "description": "ピカチュウに 持たせると 攻撃と 特攻が あがる 不思議な玉" }, diff --git a/src/locales/ja/move-trigger.json b/src/locales/ja/move-trigger.json index fbefe883836..afede7edfb3 100644 --- a/src/locales/ja/move-trigger.json +++ b/src/locales/ja/move-trigger.json @@ -64,6 +64,8 @@ "copyType": "{{pokemonName}}は {{targetPokemonName}}と\n同じタイプに なった!", "suppressAbilities": "{{pokemonName}}の 特性が 効かなくなった!", "revivalBlessing": "{{pokemonName}}は\n復活して 戦えるようになった!", + "swapArenaTags": "{{pokemonName}}は\nお互いの 場の効果を 入れ替えた!", + "chillyReception": "{{pokemonName}}は\n寒い ギャグを かました!", "swapArenaTags": "{{pokemonName}}は\nお互いの 場の 効果を 入れ替えた!", "exposedMove": "{{pokemonName}}は {{targetPokemonName}}の\n正体を 見破った!", "afterYou": "{{pokemonName}}は\nお言葉に 甘えることにした!" 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/mystery-encounter-messages.json b/src/locales/ja/mystery-encounter-messages.json new file mode 100644 index 00000000000..f67a8764d49 --- /dev/null +++ b/src/locales/ja/mystery-encounter-messages.json @@ -0,0 +1,7 @@ +{ + "paid_money": "{{amount, number}}円 払った", + "receive_money": "{{amount, number}}円 もらった!", + "affects_pokedex": "ポケモン図鑑の データに 影響する", + "cancel_option": "遭遇の 選択肢に 戻る", + "view_party_button": "手持ちを確認" +} diff --git a/src/locales/ja/mystery-encounters/a-trainers-test-dialogue.json b/src/locales/ja/mystery-encounters/a-trainers-test-dialogue.json new file mode 100644 index 00000000000..33818d27ab3 --- /dev/null +++ b/src/locales/ja/mystery-encounters/a-trainers-test-dialogue.json @@ -0,0 +1,47 @@ +{ + "intro": "非常に 強いトレーナーが 近寄る……", + "buck": { + "intro_dialogue": "おっ, トレーナーさん!\nオレ、バクだ!$あなたみたいな 強いトレーナーに\n最高の 頼みが あるのさ!$レアな タマゴが 2つ 持っているけど、\n誰か他の トレーナーに 1つ 世話をすれば いいなあ!$おまえの 強さが 証明できれば\n一番レアな タマゴを あげるからさ!", + "accept": "いひひ……! きた きた!\nさあさあ! 勝負しようぜ!", + "decline": "ざーんねん! おまえの 手持ちポケモンも\n絶好調に 見えないな……$ほら、治して あげるさ!" + }, + "cheryl": { + "intro_dialogue": "はじめまして、\nあたしの 名前は モミです。$かなり強い トレーナーですね?\nお願いが あるの!$今は レアな タマゴを 運んでいます。\nでも、1つ 世話をできる トレーナーが 探がすの$トレーナーさん 強さを 証明できれば\n最もレアな 方を あげますよ!", + "accept": "覚悟して!", + "decline": "あっ 分かります。 あなたの 手持ちポケモンは\nちょっと 元気は なさそうですね……$ねえ、 治してあげましょう!" + }, + "marley": { + "intro_dialogue": "……@d{64}あたし マイ$……お願いが あるんだけど いい?$……タマゴを 2つ 持ってる けど\n1つ あげたいの$……バトルで あたしより 強いなら\n一番レアな タマゴ あげるの", + "accept": "……分かった できるだけ 頑張るから", + "decline": "……分かった$……ポケモン 傷つくの ダメ だから 回復してあげる" + }, + "mira": { + "intro_dialogue": "あたし ミル!$強い トレーナーだね?\nお願いです!$ミルはね、レアなタマゴ 2つ あるのに\n1つ あげたいの!$勝負で 強いのを ミルに 見せたら\n一番レアな たまご あげるよ!", + "accept": "ミルと 戦う?\nやった!", + "decline": "ちょっと がっかりです……\nでも 大丈夫!$あなたの ポケモン 元気に してあげるね!" + }, + "riley": { + "intro_dialogue": "やあ、 わたしは ゲン というんだ$キミのような 強いトレーナーに\nもの好きな 申し入れが あるよ$レアなタマゴが 2つ もっているが、\n1つ もらえないか?$勝負で 強さを 証明できれば\n最もレアな 方を あげるよ", + "accept": "その顔…… 戦う 準備は できたかな?", + "decline": "分かる、 手持ちポケモンが 結構 傷ついたようだ。$私が 回復 してあげるよ" + }, + "title": "強さの証明", + "description": "このトレーナーは 選択にかかわらず タマゴを 1つ あげる ようです。 しかし、 非常に強い トレーナーを 倒せば 最もレアな タマゴを もらいます。", + "query": "どうしますか?", + "option": { + "1": { + "label": "勝負を受け取る", + "tooltip": "(-) 勝負がきつい\n(+) @[TOOLTIP_TITLE]{Very Rare Egg}をもらう" + }, + "2": { + "label": "勝負を断る", + "tooltip": "(+) 手持ち 全回復\n(+) @[TOOLTIP_TITLE]{Egg}をもらう" + } + }, + "eggTypes": { + "rare": "レアなタマゴ", + "epic": "超レアなタマゴ", + "legendary": "伝説のタマゴ" + }, + "outro": "{{statTrainerName}}から {{eggType}}を もらった!" +} diff --git a/src/locales/ja/mystery-encounters/absolute-avarice-dialogue.json b/src/locales/ja/mystery-encounters/absolute-avarice-dialogue.json new file mode 100644 index 00000000000..d2aba5dfce9 --- /dev/null +++ b/src/locales/ja/mystery-encounters/absolute-avarice-dialogue.json @@ -0,0 +1,25 @@ +{ + "intro": "待ち伏せの {{greedentName}}が\n手持ちポケモンの きのみを 全部 奪った!", + "title": "よくよく欲張り", + "description": "{{greedentName}}の 不意打ちで 持っていた きのみを 全部 失った!\n\n{{greedentName}}は 食べ始めそうですが、 突然に 止まって 気になる 顔で あなとを 見つめる。", + "query": "どうしますか", + "option": { + "1": { + "label": "勝負だ", + "tooltip": "(-) 勝負がきつい\n(+) きのみの山積みからご褒美", + "selected": "{{greedentName}}は 頬張って おそいかかってくる!", + "boss_enraged": "{{greedentName}}の 食べ物への 情熱で ぐっと キレた!", + "food_stash": "{{greedentName}}は 巨大な 食べ物の\n山積みを 守っていたようだ!$@s{item_fanfare}手持ちポケモンは 各々 {{foodReward}}を もらう!" + }, + "2": { + "label": "言い聞かせてみる", + "tooltip": "(+) きのみを数個取り戻す", + "selected": "あなたの 言い聞かせてる姿は {{greedentName}}の 胸に ちょっと こたえたようです。$きのみを 全部 戻さないが、\n数個を あなたの 方面へ 投げてあげる。" + }, + "3": { + "label": "全部食べさせて", + "tooltip": "(-) きのみを全部失う\n(?) {{greedentName}}は好きになる", + "selected": "あっという間に {{greedentName}}は\nきのみの 山積みを むさぼり食う!$お腹を ポンポンしながら\nありがたさうな 表情で 見つめる。$もしかして、 冒険中に きのみを\nもっと 食べさせて あげようかなぁ……$@s{level_up_fanfare}{{greedentName}}は 手持ちに 入りたい!" + } + } +} diff --git a/src/locales/ja/mystery-encounters/an-offer-you-cant-refuse-dialogue.json b/src/locales/ja/mystery-encounters/an-offer-you-cant-refuse-dialogue.json new file mode 100644 index 00000000000..d85ff4e0572 --- /dev/null +++ b/src/locales/ja/mystery-encounters/an-offer-you-cant-refuse-dialogue.json @@ -0,0 +1,26 @@ +{ + "intro": "お金持ちっぽいな 男の子が 道を さえぎる……", + "speaker": "お坊っちゃま", + "intro_dialogue": "こんにちは!$まるで 素敵滅法な {{strongestPokemon}}を 飼っている ことに 気づかず にはいられません!$ずーっと そのような ポケモンを 飼いたかったんです!$ね、 気前よく 支払って、\nサービスで この古くて 安かった石を あげて いかがですか?", + "title": "断れない申し出", + "description": "お坊っちゃまは {{strongestPokemon}}の代わりに @[TOOLTIP_TITLE]{Shiny Charm}と {{price, money}}を 申し出ています。\n交換として 結構 すごいですが、 本当に こんな 強い仲間と 別れるのを 耐えられますか?", + "query": "どうしますか", + "option": { + "1": { + "label": "申し出を受け取る", + "tooltip": "(-) {{strongestPokemon}}を失う\n(+) @[TOOLTIP_TITLE]{Shiny Charm}をもらう\n(+) {{price, money}}をもらう", + "selected": "素晴らしい!@d{32} {{strongestPokemon}} こっち こっち!$ヨットクラブの みんなに おまえを 自慢するぞ!$きっと ヤキモチを 焼くね…… ふへへへ" + }, + "2": { + "label": "ガキを恐喝", + "tooltip": "(+) {{option2PrimaryName}}は{{moveOrAbility}}を使う\n(+) {{price, money}}を貰う", + "tooltip_disabled": "選ぶには 手持ちポケモンが 特定な 技や 特性が 必要", + "selected": "いやあ! {{liepardName}} 泥棒だ!$お父さんの 弁護士は キサマを 連絡するぞ!!" + }, + "3": { + "label": "立ち去る", + "tooltip": "(-) ご褒美なし", + "selected": "フン 今日は 最低だ……$まあね ヨットクラブへ 戻ろう、 {{liepardName}}" + } + } +} diff --git a/src/locales/ja/mystery-encounters/berries-abound-dialogue.json b/src/locales/ja/mystery-encounters/berries-abound-dialogue.json new file mode 100644 index 00000000000..a11e0cb3360 --- /dev/null +++ b/src/locales/ja/mystery-encounters/berries-abound-dialogue.json @@ -0,0 +1,26 @@ +{ + "intro": "あのポケモンの 近くに\n巨大な きのみの木が あります!", + "title": "溢れるきのみ", + "description": "強いポケモンは きのみの木を 守っている ようです。一番 当たり前な 対処法は バトルですが、 結構 強力そう です。 早いポケモンが いれば 捕まえられずに 数個 取れる かもしれません……", + "query": "どうしますか?", + "berries": "きのみ", + "option": { + "1": { + "label": "バトルだ", + "tooltip": "(-) 勝負がきつい\n(+) きのみをもらう", + "selected": "怖がらないで ポケモンに 近寄っていく……" + }, + "2": { + "label": "木に突っ走る!", + "tooltip": "(-) {{fastestPokemon}}は素早さを使う\n(+) きのみをもらう", + "selected": "{{fastestPokemon}}は きのみの木へ 突っ走っていきます!${{enemyPokemon}}は 受け止められる 前に {{numBerries}}を 取れます!$新たに得た ご褒美で 早く 逃げます。", + "selected_bad": "{{fastestPokemon}}は きのみの木へ 突っ走って行きます!$ダメ! {{enemyPokemon}}の方は 早くて 受け止められました!", + "boss_enraged": "相手の {{enemyPokemon}}は 怒り出した!" + }, + "3": { + "label": "置いていく", + "tooltip": "(-) ご褒美なし", + "selected": "強いポケモンを そのままで 置いて 進んでいきます。" + } + } +} diff --git a/src/locales/ja/mystery-encounters/bug-type-superfan-dialogue.json b/src/locales/ja/mystery-encounters/bug-type-superfan-dialogue.json new file mode 100644 index 00000000000..c11a1191e4c --- /dev/null +++ b/src/locales/ja/mystery-encounters/bug-type-superfan-dialogue.json @@ -0,0 +1,40 @@ +{ + "intro": "色々な むし式の アクセを 着ている\n奇抜な トレーナーは 道を さえぎる!", + "intro_dialogue": "むしむし トレーナーむ氏! わたむしの 目的は この世界で 一番珍しい むしポケモンを 見つけること でチュウ!$トレーナーむ氏も むしが 大好き でチョウ?\nみんなと同じ でチュウね!", + "title": "虫好きも好き好き", + "speaker": "むしマニア", + "description": "返事も 待たずに むしマニアは 言い続けています……\n\nこの状況から 出るには 注目を 引くしかない ようです!", + "query": "どうしますか?", + "option": { + "1": { + "label": "バトルに挑む", + "tooltip": "(-) 相手が手強い\n(+) ポケモンにむしタイプ技を教える", + "selected": "わたむしを 挑むッシか?\n仲間の むしたちは もう 覚悟していまチュウ!" + }, + "2": { + "label": "むしポケモンを見せる", + "tooltip": "(+) プレゼントをもらう", + "disabled_tooltip": "選ぶには 最小限 1匹の むしタイプの 手持ちポケモンが 必要", + "selected": "むしマニアに 手持ちの むしポケモンを 見せる……", + "selected_0_to_1": "何チュウこと?? {{numBugTypes}}しか いないの でチョウ……$よクモ 時間を むダニ させます……", + "selected_2_to_3": "ほほう! {{numBugTypes}}が いるんでチュウね!\n良く 出来ムシた!$ね、 これが あれば 冒険チュウに もっともっと 捕まえられまチュウ!", + "selected_4_to_5": "ほほう!  {{numBugTypes}}が いるんでチュウね!\nチョウナイス!$わたむしの 段階には まだ 追いつきません けど、\nトレーナーむ氏の 中に チョーっと 自分の姿が 見えますね!\n$こちらをどうぞ! 我が虫弟子よ!", + "selected_6": "チョチョチョーっと 待って! {{numBugTypes}}!!\n$わたむしと 同じくらいに むしが 大好きでちょう?!$この友情の印を 受け取って ください!" + }, + "3": { + "label": "むしアイテムをあげる", + "tooltip": "(-) {{requiredBugItems}}をあげる\n(+) プレゼントをもらう", + "disabled_tooltip": "選ぶには {{requiredBugItems}}を 持つことが 必要", + "select_prompt": "あげたい アイテムを 選んで ください", + "invalid_selection": "このポケモンが そのような アイテムを 持っていない!", + "selected": "むしマニアに {{selectedItem}}を あげます。", + "selected_dialogue": "チョーっと待って! その{{selectedItem}}、 くれますか?\nアリがとう ございムシ!$このアリがたさの印、 受け取ってください!$古くから 先祖の 形見 でチュウよ… トレーナーむ氏が 預かってくれます 蚊?" + } + }, + "battle_won": "トレーナーむ氏の 知識と 腕前は 弱点を\nつけ込むには チョウど良かった でチュウ!$いい勉強に なったから トレーナーむ氏の\nポケモン 1匹に むしタイプの 技を 教えまチョウ!", + "teach_move_prompt": "教えたい 技を 選んでください", + "confirm_no_teach": "本当に すごい技を 教えませんか?", + "outro": "チョウ強い むしポケモンが\nトレーナーむ氏の 未来に 予知しまチュウよ!$また ハエる まで バイ ビー!", + "numBugTypes_one": "{{count}}匹の むしポケモン", + "numBugTypes_other": "{{count}}匹の むしポケモン" +} diff --git a/src/locales/ja/mystery-encounters/clowning-around-dialogue.json b/src/locales/ja/mystery-encounters/clowning-around-dialogue.json new file mode 100644 index 00000000000..6b429da5083 --- /dev/null +++ b/src/locales/ja/mystery-encounters/clowning-around-dialogue.json @@ -0,0 +1,34 @@ +{ + "intro": "……@d{64}クラウン?", + "speaker": "クラウン", + "intro_dialogue": "バカバカしい ボンクラ、 抜本的な バトルで ぶっ壊す!\n微笑で ボコボコ Baby!", + "title": "くらくらクラウンの変え方", + "description": "この遭遇は 変な 気がします。\nクラウンは バトルを 引き起こりたがります が、\n一体 どうして ですか?\n\nあの{{blacephalonName}}は 特に おかしい…… @[TOOLTIP_TITLE]{変わったタイプと特性}が ある みたいです。", + "query": "どうします?", + "option": { + "1": { + "label": "クラウンと勝負", + "tooltip": "(-) 勝負はおかしい\n(?) 手持ちポケモンの特性は変化", + "selected": "パッとしない ポケモンを ぽかぽか パンチしちゃうんだ ぴょん!", + "apply_ability_dialogue": "とても たくましく 倒した!\n倒すのが 得意な トレーナーと一緒に 特性を 取り替えたい!", + "apply_ability_message": "クラウンは スキルスワップで 手持ちポケモン 1匹の\n特性を {{ability}}と 取り替えることを 申し出ています!", + "ability_prompt": "手持ちポケモンの 特性を\n恒久的に {{ability}}に 変化しますか?", + "ability_gained": "@s{level_up_fanfare}{{chosenPokemon}}は {{ability}}の 特性を 身についた!" + }, + "2": { + "label": "怒らない", + "tooltip": "(-) クラウンは怒る\n(?) 手持ちポケモンのアイテムは変化", + "selected": "しつこい! シンプルな 勝負を スルー?!\n己は 俺の 怒りを 起こした!", + "selected_2": "クラウンの {{blacephalonName}}は トリックを 使った!\n{{switchPokemon}}の 持ったアイテムは 全て デタラメに 交換された!", + "selected_3": "豪華に ごまかした 下衆…… 元気でね!" + }, + "3": { + "label": "悪口を返す", + "tooltip": "(-) クラウンは怒る\n(?) 手持ちポケモンのタイプは変化", + "selected": "しつこい! シンプルな 勝負を スルー?!\n己は 俺の 怒りを 起こした!", + "selected_2": "クラウンの {{blacephalonName}}は 変な技を 使った!\n手持ちポケモン 全員の タイプは デタラメに 交換された!", + "selected_3": "豪華に ごまかした 下衆…… 元気でね!" + } + }, + "outro": "クラウンたちは パッと\n煙に なっちゃいます。" +} diff --git a/src/locales/ja/mystery-encounters/dancing-lessons-dialogue.json b/src/locales/ja/mystery-encounters/dancing-lessons-dialogue.json new file mode 100644 index 00000000000..2bade712ec4 --- /dev/null +++ b/src/locales/ja/mystery-encounters/dancing-lessons-dialogue.json @@ -0,0 +1,27 @@ +{ + "intro": "相手が いなくて 寂しそうな {{oricorioName}}は 悲しく 踊ってます。", + "title": "フリフリ振り付け", + "description": "{{oricorioName}}は 攻撃的 じゃなさそう…… むしろ、 悲しい 顔を しています。\n\n誰かと 踊りたいだけ かもしれません……", + "query": "どうしますか?", + "option": { + "1": { + "label": "バトルする", + "tooltip": "(-) 勝負はきつい\n(+) バトンをもらう", + "selected": "{{oricorioName}}は 取り乱して 防御の 体勢に 入る!", + "boss_enraged": "{{oricorioName}}は 恐れで 能力を 上げた!" + }, + "2": { + "label": "踊りを覚える", + "tooltip": "(+) ポケモンにめざめるダンスを教える", + "selected": "{{oricorioName}}の 踊りを よく 調べます……$@s{level_up_fanfare}{{selectedPokemon}}が {{oricorioName}}から 技を 覚えました!" + }, + "3": { + "label": "踊りを教える", + "tooltip": "(-) {{oricorioName}}に踊りの技を教える\n(+) {{oricorioName}}は好きになる", + "disabled_tooltip": "選ぶには 踊りの技がある 手持ちポケモンが 必要", + "select_prompt": "踊りの技を 選んでください", + "selected": "{{oricorioName}}は {{selectedPokemon}}が {{selectedMove}}を\n演舞する 姿を じっと 見とれています!!$演舞に 全く 好きになった ようです!$@s{level_up_fanfare}{{oricorioName}}は 手持ちに 入りたい!" + } + }, + "invalid_selection": "このポケモンは 踊りの技を 覚えていません!" +} diff --git a/src/locales/ja/mystery-encounters/dark-deal-dialogue.json b/src/locales/ja/mystery-encounters/dark-deal-dialogue.json new file mode 100644 index 00000000000..a797e599f19 --- /dev/null +++ b/src/locales/ja/mystery-encounters/dark-deal-dialogue.json @@ -0,0 +1,24 @@ + + +{ + "intro": "ぼろぼろの コートを 着ている\n謎の男が 道を 立ち塞いでいます……", + "speaker": "怪しい男", + "intro_dialogue": "おい キミ!$俺、潜在能力を 引き出す 装置を 作り出した!$ポケモンの 原子を 分子レベルで\n完全に 再結合し 断然強力な 姿に 変えるぞ$ウエッヘへ…@d{32} 機能が 効いてるのを 確認するには ぎせッ@d{32}…えーと、 被験者が 必要 だが… ", + "title": "裏取引", + "description": "怪しいヤツは ボールを 数個 差し上げます。\n「礼は弾むぞ! 支払いとして 高級なボールを あげて、 代わりに 手持ちポケモンが 1匹だけ 要るな…… ウエッヘヘ」", + "query": "どうしますか?", + "option": { + "1": { + "label": "受け入れる", + "tooltip": "(+) 5個のローグボール\n(?) 無作為なポケモンを強化", + "selected_dialogue": "じゃあ、 その{{pokeName}}で いい だろう!$ただし、 何か 悪いことが 起こっても 俺は 責任を 負わないって 覚えとけ!@d{32} ウエッヘヘ...", + "selected_message": "怪しい男から 5個の ローグボールを もらいます。${{pokeName}}は 謎の 機械に 入っていく...$機械は ピカッと光って 変な音を し出します!$...@d{96} 何かが 機械から 飛んできて 暴れ出す! " + }, + "2": { + "label": "断る", + "tooltip": "(-) ご褒美なし", + "selected": "助けが 要る人を 無視?\nケッ!" + } + }, + "outro": "苛む 遭遇の 後で、\n冷静にしてみて 立ち去ります。" +} \ No newline at end of file diff --git a/src/locales/ja/mystery-encounters/delibirdy-dialogue.json b/src/locales/ja/mystery-encounters/delibirdy-dialogue.json new file mode 100644 index 00000000000..530830a6409 --- /dev/null +++ b/src/locales/ja/mystery-encounters/delibirdy-dialogue.json @@ -0,0 +1,29 @@ + + +{ + "intro": "{{delibirdName}}の 群れは 現れてきた!", + "title": "デリバリーか デリシャスか", + "description": "何かを ほしがる 表情で {{delibirdName}}は 期待で 見つめる。 アイテムや お金で 満足できる かもしれません…", + "query": "何をあげますか?", + "invalid_selection": "このポケモンは そのような アイテムが 持っていない!", + "option": { + "1": { + "label": "お金をあげる", + "tooltip": "(-) {{delibirdName}}に{{money, money}}あげる\n(+) プレセントをもらう", + "selected": "お金を {{delibirdName}}に 投げます。\n興奮で ペチャペチャと 話し合います。$話しが 終わってから ありがたく プレセントを くれます!" + }, + "2": { + "label": "食べ物をあげる", + "tooltip": "(-) {{delibirdName}}に きのみ や ふっかつのタネ をあげる\n(+) プレセントをもらう", + "select_prompt": "あげたい アイテムを 選んでください", + "selected": "{{chosenItem}}を {{delibirdName}}に 投げます。\n興奮で ペチャペチャと 話し合います。$話しが 終わってから ありがたく プレセントを くれます!" + }, + "3": { + "label": "アイテムをあげる", + "tooltip": "(-) {{delibirdName}}に持ち物をあげる\n(+) プレセントをもらう", + "select_prompt": "あげたい アイテムを 選んでください", + "selected": "{{chosenItem}}を {{delibirdName}}に 投げます。\n興奮で ペチャペチャと 話し合います。$話しが 終わってから ありがたく プレセントを くれます!" + } + }, + "outro": "{{delibirdName}}の 群れが 嬉しそうに ちょこちょこ歩きで 帰ります。$何と 不思議な 遭遇!" +} diff --git a/src/locales/ja/mystery-encounters/department-store-sale-dialogue.json b/src/locales/ja/mystery-encounters/department-store-sale-dialogue.json new file mode 100644 index 00000000000..246672e4907 --- /dev/null +++ b/src/locales/ja/mystery-encounters/department-store-sale-dialogue.json @@ -0,0 +1,27 @@ +{ + "intro": "買い物袋が いっぱいな おねえさん だ。", + "speaker": "お客様", + "intro_dialogue": "こんにちは! すごーい セールの ために\nここに 来ましたね?$セール中に 特別な クーポンで\nアイテムを サービスとして 1つ もらえます!$もう 1つの クーポンが ありますから どうぞ!", + "title": "デパートで大特価セール", + "description": "各方面で 商品 いっぱい! クーポンで アイテムを もらえる 売り場が 4つ ようです。\n可能性は無限大!", + "query": "どの売り場に行きますか?", + "option": { + "1": { + "label": "技マシーンの売り場", + "tooltip": "(+) 技マシーンショップ" + }, + "2": { + "label": "栄養士の売り場", + "tooltip": "(+) えいようドリンクショップ" + }, + "3": { + "label": "戦闘良品の売り場", + "tooltip": "(+) 戦闘用ショップ" + }, + "4": { + "label": "ボールの売り場", + "tooltip": "(+) モンスターボールショップ" + } + }, + "outro": "何と お買い得! ちょくちょく 買いに行った 方が いいね" +} diff --git a/src/locales/ja/mystery-encounters/field-trip-dialogue.json b/src/locales/ja/mystery-encounters/field-trip-dialogue.json new file mode 100644 index 00000000000..a4613206579 --- /dev/null +++ b/src/locales/ja/mystery-encounters/field-trip-dialogue.json @@ -0,0 +1,31 @@ +{ + "intro": "先生と 生徒です!", + "speaker": "先生", + "intro_dialogue": "あら、 こんにちは! 少しも 生徒のために\nお時間を させていただきますか?$ポケモンの 技に付いて 教えています から\n実演を 見た方が 良いです。$手持ちポケモンの 技を 一つ 見させても 良いですか?", + "title": "修学旅行", + "description": "先生は ポケモンの 技の 実演を 頼んでいます。 技の 分類によって お礼に 便利な アイテムを くれるかもしれません。", + "query": "どの分類の 技を 見せますか?", + "option": { + "1": { + "label": "物理技", + "tooltip": "(+) 物理アイテム" + }, + "2": { + "label": "特殊技", + "tooltip": "(+) 特殊アイテム" + }, + "3": { + "label": "変化技", + "tooltip": "(+) 変化アイテム" + }, + "selected": "{{pokeName}}は {{move}}を 堂々と 実演します!" + }, + "second_option_prompt": "使いたい 技を 選んでください", + "incorrect": "……$あれは {{moveCategory}}では ありませんね!\n申し訳ありませんが、 何も あげられません。$さあ みんな、\nましな 実演を 探しに 行きましょう。", + "incorrect_exp": "……とにかく いい勉強に なりました?$ポケモンも 経験値を 得ました。", + "correct": "見せてくれて ありがとう ございます!\nこのアイテムは 冒険中に 役に立つと 良いです!", + "correct_exp": "{{pokeName}}も 貴重な 経験値を 得ました!", + "status": "変化技", + "physical": "物理技", + "special": "特殊技" +} diff --git a/src/locales/ja/mystery-encounters/fiery-fallout-dialogue.json b/src/locales/ja/mystery-encounters/fiery-fallout-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/ja/mystery-encounters/fiery-fallout-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/ja/mystery-encounters/fight-or-flight-dialogue.json b/src/locales/ja/mystery-encounters/fight-or-flight-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/ja/mystery-encounters/fight-or-flight-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/ja/mystery-encounters/fun-and-games-dialogue.json b/src/locales/ja/mystery-encounters/fun-and-games-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/ja/mystery-encounters/fun-and-games-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/ja/mystery-encounters/global-trade-system-dialogue.json b/src/locales/ja/mystery-encounters/global-trade-system-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/ja/mystery-encounters/global-trade-system-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/ja/mystery-encounters/lost-at-sea-dialogue.json b/src/locales/ja/mystery-encounters/lost-at-sea-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/ja/mystery-encounters/lost-at-sea-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/ja/mystery-encounters/mysterious-challengers-dialogue.json b/src/locales/ja/mystery-encounters/mysterious-challengers-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/ja/mystery-encounters/mysterious-challengers-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/ja/mystery-encounters/mysterious-chest-dialogue.json b/src/locales/ja/mystery-encounters/mysterious-chest-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/ja/mystery-encounters/mysterious-chest-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/ja/mystery-encounters/part-timer-dialogue.json b/src/locales/ja/mystery-encounters/part-timer-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/ja/mystery-encounters/part-timer-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/ja/mystery-encounters/safari-zone-dialogue.json b/src/locales/ja/mystery-encounters/safari-zone-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/ja/mystery-encounters/safari-zone-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/ja/mystery-encounters/shady-vitamin-dealer-dialogue.json b/src/locales/ja/mystery-encounters/shady-vitamin-dealer-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/ja/mystery-encounters/shady-vitamin-dealer-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/ja/mystery-encounters/slumbering-snorlax-dialogue.json b/src/locales/ja/mystery-encounters/slumbering-snorlax-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/ja/mystery-encounters/slumbering-snorlax-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/ja/mystery-encounters/teleporting-hijinks-dialogue.json b/src/locales/ja/mystery-encounters/teleporting-hijinks-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/ja/mystery-encounters/teleporting-hijinks-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/ja/mystery-encounters/the-expert-pokemon-breeder-dialogue.json b/src/locales/ja/mystery-encounters/the-expert-pokemon-breeder-dialogue.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/src/locales/ja/mystery-encounters/the-expert-pokemon-breeder-dialogue.json @@ -0,0 +1 @@ +{} diff --git a/src/locales/ja/mystery-encounters/the-pokemon-salesman-dialogue.json b/src/locales/ja/mystery-encounters/the-pokemon-salesman-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/ja/mystery-encounters/the-pokemon-salesman-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/ja/mystery-encounters/the-strong-stuff-dialogue.json b/src/locales/ja/mystery-encounters/the-strong-stuff-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/ja/mystery-encounters/the-strong-stuff-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/ja/mystery-encounters/the-winstrate-challenge-dialogue.json b/src/locales/ja/mystery-encounters/the-winstrate-challenge-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/ja/mystery-encounters/the-winstrate-challenge-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/ja/mystery-encounters/training-session-dialogue.json b/src/locales/ja/mystery-encounters/training-session-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/ja/mystery-encounters/training-session-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/ja/mystery-encounters/trash-to-treasure-dialogue.json b/src/locales/ja/mystery-encounters/trash-to-treasure-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/ja/mystery-encounters/trash-to-treasure-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/ja/mystery-encounters/uncommon-breed-dialogue.json b/src/locales/ja/mystery-encounters/uncommon-breed-dialogue.json new file mode 100644 index 00000000000..4e4e4038897 --- /dev/null +++ b/src/locales/ja/mystery-encounters/uncommon-breed-dialogue.json @@ -0,0 +1,26 @@ +{ + "intro": "普段の ポケモン じゃなそうです……!", + "title": "珍しい類型", + "description": "他の {{enemyPokemon}}と 比べて 特別そう です。@[TOOLTIP_TITLE]{特別な 技を 覚えている かな?} 普通に バトルで 捕まえてみることが できます が、 仲良くする 方法も ある かもしれません。", + "query": "どうしますか", + "option": { + "1": { + "label": "バトルする", + "tooltip": "(-) 勝負が厳しい\n(+) 相手が強くて捕まえられる", + "selected": "恐れないで {{enemyPokemon}}に 近寄ります!", + "stat_boost": "{{enemyPokemon}}の 強めた実力で 能力を上げた!" + }, + "2": { + "label": "食べ物をあげる", + "disabled_tooltip": "選ぶには 4個の きのみが 必要", + "tooltip": "(-) きのみを4個あげる\n(+) {{enemyPokemon}}が好きになる", + "selected": "{{enemyPokemon}}に きのみを なげます!$嬉しそうに 食べます!${{enemyPokemon}}は 手持ちに 入りたいです!" + }, + "3": { + "label": "仲良くする", + "disabled_tooltip": "選ぶには 手持ちポケモンが 特定な 技が 必要", + "tooltip": "(+) {{option3PrimaryName}}は{{option3PrimaryMove}}を使う\n(+) {{enemyPokemon}}は好きになる", + "selected": "{{enemyPokemon}}を 魅了するには {{option3PrimaryName}}は {{option3PrimaryMove}}を 使います!${{enemyPokemon}}が 手持ちに 入りたいです!" + } + } +} diff --git a/src/locales/ja/mystery-encounters/weird-dream-dialogue.json b/src/locales/ja/mystery-encounters/weird-dream-dialogue.json new file mode 100644 index 00000000000..02311e19119 --- /dev/null +++ b/src/locales/ja/mystery-encounters/weird-dream-dialogue.json @@ -0,0 +1,22 @@ +{ + "intro": "影で 隠れている 謎の 女の人が 道を さえぎる。\n何か 不安に なる……", + "speaker": "女の人", + "intro_dialogue": "あなたの 未来たちも 過去たちも 全て 見えます。$我が子よ、 見えますか?", + "title": "???", + "description": "女の人の 言葉は 頭に 響いています。 一人の声じゃなくて、 全ての 異世界と 時間軸から 数多な 声々でした。 クラッと めまいがして、 あの質問が 頭で めぐります……\n\n@[TOOLTIP_TITLE]{「あなたの 未来たちも 過去たちも 全て 見えます。 我が子よ、 見えますか?」}", + "query": "どうしますか?", + "option": { + "1": { + "label": "「見えます。」", + "tooltip": "@[SUMMARY_GREEN]{(?) 手持ちポケモンに影響}", + "selected": "女の人は 手を 伸ばすと\n全部は 真っ黒に なります。$そして……@d{64} 何事も 見えます。\n時間軸たち。 自分たち。\n過去たち。 未来たち。 $自分を 作り出した 何事も\nいつか 成る 何事も……@d{64}", + "cutscene": "手持ちポケモンも 見えます……@d{32} 各現実 から\n集中してきて 混じって 新しい 生存たちに なります……@d{64}", + "dream_complete": "目が 覚めると、 女の人が (女か? 幽霊か?) 消えました……$.@d{32}.@d{32}.@d{32}$手持ちポケモンが 変わった……\nもしくは 前と 同じ 手持ち かな……?" + }, + "2": { + "label": "さっさと逃げる", + "tooltip": "(-) 手持ちポケモンに影響", + "selected": "ジーンと握った 意識を 取り戻して さっさと 逃げます。$一旦止まって 気を取り戻すと 手持ちポケモンを 見ます。$なんとなく 全員の レベルが 減っている ようです!" + } + } +} 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/status-effect.json b/src/locales/ja/status-effect.json index 8dafbbdcba7..14e8932a400 100644 --- a/src/locales/ja/status-effect.json +++ b/src/locales/ja/status-effect.json @@ -1,12 +1,6 @@ { "none": { - "name": "なし", - "description": "", - "obtain": "", - "obtainSource": "", - "activation": "", - "overlap": "", - "heal": "" + "name": "なし" }, "poison": { "name": "どく", 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..2782043b578 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": "レッドとグリーン", @@ -160,5 +166,18 @@ "alder_iris_double": "アデクとアイリス", "iris_alder_double": "アイリスとアデク", "marnie_piers_double": "マリィとネズ", - "piers_marnie_double": "ネズとマリィ" + "piers_marnie_double": "ネズとマリィ", + + "buck": "バク", + "cheryl": "モミ", + "marley": "マイ", + "mira": "ミル", + "riley": "ゲン", + "victor": "ハルヒコ", + "victoria": "ヤスエ", + "vivi": "アキ", + "vicky": "ミツヨ", + "vito": "リョウヘイ", + "bug_type_superfan": "むしマニア", + "expert_pokemon_breeder": "ブリーダー名人" } diff --git a/src/locales/ja/trainer-titles.json b/src/locales/ja/trainer-titles.json index b3829c701e5..094de5fe101 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,8 @@ "flare_admin_female": "フレア団幹部", "aether_admin": "エーテル支部長", "skull_admin": "スカル団幹部", - "macro_admin": "マクロコスモス" + "macro_admin": "マクロコスモス", + "star_admin": "スター団 組ボス", + + "the_winstrates": "カチヌキさんち" } 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/achv.json b/src/locales/ko/achv.json index 9364c1c55b6..e1bf57a8ed1 100644 --- a/src/locales/ko/achv.json +++ b/src/locales/ko/achv.json @@ -264,5 +264,9 @@ "INVERSE_BATTLE": { "name": "상성 전문가(였던 것)", "description": "거꾸로 배틀 챌린지 모드 클리어." + }, + "BREEDERS_IN_SPACE": { + "name": "충격!우주에 브리더 진짜 계심ㄷㄷ", + "description": "우주 바이옴에서 포켓몬 전문 브리더에게 승리." } } diff --git a/src/locales/ko/battle.json b/src/locales/ko/battle.json index 154ca04fd49..987f816bbf6 100644 --- a/src/locales/ko/battle.json +++ b/src/locales/ko/battle.json @@ -14,6 +14,10 @@ "moneyWon": "상금으로\n₽{{moneyAmount}}을 손에 넣었다!", "moneyPickedUp": "₽{{moneyAmount}}을 주웠다!", "pokemonCaught": "신난다-!\n{{pokemonName}}[[를]] 잡았다!", + "pokemonObtained": "{{pokemonName}}[[를]] 손에 넣었다!", + "pokemonBrokeFree": "안 돼!\n포켓몬이 볼에서 나와 버렸다!", + "pokemonFled": "야생 {{pokemonName}}[[는]]\n도망쳤다!", + "playerFled": "{{pokemonName}}[[로]]부터 도망쳤다!", "addedAsAStarter": "{{pokemonName}}[[가]]\n스타팅 포켓몬에 추가되었다!", "partyFull": "지닌 포켓몬이 가득 찼습니다. {{pokemonName}}[[를]]\n대신해 포켓몬을 놓아주시겠습니까?", "pokemon": "포켓몬", @@ -52,6 +56,7 @@ "noPokeballTrainer": "다른 트레이너의 포켓몬은 잡을 수 없다!", "noPokeballMulti": "안돼! 2마리 있어서\n목표를 정할 수가 없어…!", "noPokeballStrong": "너무 강해서 잡을 수가 없다!\n먼저 약화시켜야 한다!", + "noPokeballMysteryEncounter": "이 포켓몬은 잡을 수 없습니다!", "noEscapeForce": "본 적 없는 힘이\n도망칠 수 없게 한다.", "noEscapeTrainer": "안돼! 승부 도중에\n상대에게 등을 보일 순 없어!", "noEscapePokemon": "{{pokemonName}}의 {{moveName}}때문에\n{{escapeVerb}} 수 없다!", @@ -99,5 +104,6 @@ "unlockedSomething": "{{unlockedThing}}[[가]]\n해금되었다.", "congratulations": "축하합니다!", "beatModeFirstTime": "{{speciesName}}[[가]] {{gameMode}} 모드를 처음으로 클리어했다!\n{{newModifier}}[[를]] 손에 넣었다!", - "ppReduced": "{{targetName}}의\n{{moveName}}[[를]] {{reduction}} 깎았다!" + "ppReduced": "{{targetName}}의\n{{moveName}}[[를]] {{reduction}} 깎았다!", + "mysteryEncounterAppeared": "어라?" } diff --git a/src/locales/ko/bgm-name.json b/src/locales/ko/bgm-name.json index 5295e2d8708..1ae9e37d189 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 눈이 마주치면 승부! (등산가)", @@ -145,5 +148,11 @@ "encounter_youngster": "BW 눈이 마주치면 승부! (반바지 꼬마)", "heal": "BW 포켓몬 센터", "menu": "불가사의 던전 하늘의 탐험대 포켓몬 세계에 온 것을 환영한다!", - "title": "불가사의 던전 하늘의 탐험대 메뉴 테마" + "title": "불가사의 던전 하늘의 탐험대 메뉴 테마", + + "mystery_encounter_weird_dream": "불가사의 던전 하늘의 탐험대 시한의 탑 최상부", + "mystery_encounter_fun_and_games": "불가사의 던전 하늘의 탐험대 푸크린 길드", + "mystery_encounter_gen_5_gts": "BW GTS", + "mystery_encounter_gen_6_gts": "XY GTS", + "mystery_encounter_delibirdy": "Firel - DeliDelivery!" } diff --git a/src/locales/ko/config.ts b/src/locales/ko/config.ts index 978cdc83002..049fb016d33 100644 --- a/src/locales/ko/config.ts +++ b/src/locales/ko/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 "./mystery-encounters/mysterious-chest-dialogue.json"; +import mysteriousChallengers from "./mystery-encounters/mysterious-challengers-dialogue.json"; +import darkDeal from "./mystery-encounters/dark-deal-dialogue.json"; +import departmentStoreSale from "./mystery-encounters/department-store-sale-dialogue.json"; +import fieldTrip from "./mystery-encounters/field-trip-dialogue.json"; +import fieryFallout from "./mystery-encounters/fiery-fallout-dialogue.json"; +import fightOrFlight from "./mystery-encounters/fight-or-flight-dialogue.json"; +import safariZone from "./mystery-encounters/safari-zone-dialogue.json"; +import shadyVitaminDealer from "./mystery-encounters/shady-vitamin-dealer-dialogue.json"; +import slumberingSnorlax from "./mystery-encounters/slumbering-snorlax-dialogue.json"; +import trainingSession from "./mystery-encounters/training-session-dialogue.json"; +import theStrongStuff from "./mystery-encounters/the-strong-stuff-dialogue.json"; +import pokemonSalesman from "./mystery-encounters/the-pokemon-salesman-dialogue.json"; +import offerYouCantRefuse from "./mystery-encounters/an-offer-you-cant-refuse-dialogue.json"; +import delibirdy from "./mystery-encounters/delibirdy-dialogue.json"; +import absoluteAvarice from "./mystery-encounters/absolute-avarice-dialogue.json"; +import aTrainersTest from "./mystery-encounters/a-trainers-test-dialogue.json"; +import trashToTreasure from "./mystery-encounters/trash-to-treasure-dialogue.json"; +import berriesAbound from "./mystery-encounters/berries-abound-dialogue.json"; +import clowningAround from "./mystery-encounters/clowning-around-dialogue.json"; +import partTimer from "./mystery-encounters/part-timer-dialogue.json"; +import dancingLessons from "./mystery-encounters/dancing-lessons-dialogue.json"; +import weirdDream from "./mystery-encounters/weird-dream-dialogue.json"; +import theWinstrateChallenge from "./mystery-encounters/the-winstrate-challenge-dialogue.json"; +import teleportingHijinks from "./mystery-encounters/teleporting-hijinks-dialogue.json"; +import bugTypeSuperfan from "./mystery-encounters/bug-type-superfan-dialogue.json"; +import funAndGames from "./mystery-encounters/fun-and-games-dialogue.json"; +import uncommonBreed from "./mystery-encounters/uncommon-breed-dialogue.json"; +import globalTradeSystem from "./mystery-encounters/global-trade-system-dialogue.json"; +import expertPokemonBreeder from "./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 koConfig = { ability, abilityTriggers, @@ -110,4 +152,40 @@ export const koConfig = { modifierSelectUiHandler, moveTriggers, 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/ko/dialogue.json b/src/locales/ko/dialogue.json index 13fcd64a8d3..3b998c8e218 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,138 @@ "1": "너희가 보기에는 내가 끔찍한 짓을 벌이고 있는 것처럼 보이겠지? 조금도 이해가 가지 않을 거야.\n$하지만 난 가라르지방의 영원한 번영을 위해서 무한한 에너지를 가져다줘야 해." } }, + "star_boss_penny_1": { + "encounter": { + "1": "내가 바로 스타단의 진 보스, 카시오페아… \n$…진 보스의 힘 앞에 무릎 꿇게 만들어 주겠어!!" + }, + "victory": { + "1": "… … …" + }, + "defeat": { + "1": "후후…" + } + }, + "star_boss_penny_2": { + "encounter": { + "1": "승부인 이상, 봐주지 않아! 그것이 스타단의 규칙이니까! \n$브이브이 파워로 우주의 먼지로 만들어 주겠어!!" + }, + "victory": { + "1": "이걸로 진짜 끝이구나…" + }, + "defeat": { + "1": "군더더기가 없는 실력이네. 보스들이 당한 걸 생각해 보면 말이야." + } + }, + "stat_trainer_buck": { + "encounter": { + "1": "…말해두겠지만 나 강하다고!\n놀라기나 해라!", + "2": "몬스터볼 안에서 내 포켓몬들이 떨고 있다!\n이게 흥분해서 떨린다는 것이구나!" + }, + "victory": { + "1": "이히히!\n너 열정이 뜨거운걸!!", + "2": "이히히!\n너 열정이 뜨거운걸!!" + }, + "defeat": { + "1": "우왓!\n연료가 다 떨어졌나 본데?", + "2": "우왓!\n연료가 다 떨어졌나 본데?" + } + }, + "stat_trainer_cheryl": { + "encounter": { + "1": "내 포켓몬들\n싸우고 싶어서 근질근질하고 있었어.", + "2": "내 포켓몬들\n꽤나 개구쟁이라니까." + }, + "victory": { + "1": "공격하는 것과 방어하는 것\n그 밸런스 유지가 큰일이네.", + "2": "공격하는 것과 방어하는 것\n그 밸런스 유지가 큰일이네." + }, + "defeat": { + "1": "네 포켓몬들 다친 것 같은데?", + "2": "네 포켓몬들 다친 것 같은데?" + } + }, + "stat_trainer_marley": { + "encounter": { + "1": "…좋아.\n최선을 다할게.", + "2": "…좋아.\n난…지지 않아…!" + }, + "victory": { + "1": "…으윽.", + "2": "…으윽." + }, + "defeat": { + "1": "…잘 가.", + "2": "…잘 가." + } + }, + "stat_trainer_mira": { + "encounter": { + "1": "너 깜짝 놀랄 거야!", + "2": "더는 헤매거나 하지 않는다는 것을 너에게 보여줄게!" + }, + "victory": { + "1": "이거라면 이곳에서\n미루가 활약할 수 없을까.", + "2": "이거라면 이곳에서\n미루가 활약할 수 없을까." + }, + "defeat": { + "1": "미루가 이길 줄 알았어!", + "2": "미루가 이길 줄 알았어!" + } + }, + "stat_trainer_riley": { + "encounter": { + "1": "포켓몬 승부야말로\n우리들의 인사 방식이지!", + "2": "전력을 다해서\n네 포켓몬을 쓰러뜨리겠다." + }, + "victory": { + "1": "싸우거나 편을 만들거나…\n트레이너들은 좋겠어.", + "2": "싸우거나 편을 만들거나…\n트레이너들은 좋겠어." + }, + "defeat": { + "1": "꽤 인상적인 실력이었어.\n다음엔 더 잘해봐.", + "2": "꽤 인상적인 실력이었어.\n다음엔 더 잘해봐." + } + }, + "winstrates_victor": { + "encounter": { + "1": "멋진 배짱이군! 마음에 들었다!" + }, + "victory": { + "1": "아-앗! 생각보다 더 강하구나!" + } + }, + "winstrates_victoria": { + "encounter": { + "1": "어머머! 생각보다 어리군요?!$우리 남편을 이기다니\n정말 굉장한 실력의 트레이너인가 보죠?$자 그럼 이번엔 저와 승부해요!" + }, + "victory": { + "1": "에구머니나!\n어쩜 그렇게 강하신가요?!" + } + }, + "winstrates_vivi": { + "encounter": { + "1": "엄마보다 강하다니 굉장하다!$하지만 나도 꽤 강하다고!\n진짜라니까?!" + }, + "victory": { + "1": "너무 분해…\n…훌쩍!$할머니이~이!!" + } + }, + "winstrates_vicky": { + "encounter": { + "1": "요 녀석~!\n우리 귀여운 손녀에게 무슨 짓을 한 게냐!$이렇게 된 이상 내가 직접\n네 포켓몬을 혼내줄 테니 각오해라잇!" + }, + "victory": { + "1": "에구! 정말 강하구나!\n우리 손녀 말이 진짜였구나." + } + }, + "winstrates_vito": { + "encounter": { + "1": "가족들끼리 포켓몬 수행을 했었어!\n누구에게도 지지 않는다고!" + }, + "victory": { + "1": "난 우리 가족 중에서도 제일 강했어.\n한 번도 져본 적이 없었는데…" + } + }, "brock": { "encounter": { "1": "내 전문인 바위 타입 포켓몬으로 널 쓰러뜨려줄게! 덤벼!", @@ -2446,7 +2649,7 @@ }, "iono": { "encounter": { - "1": "자~ 오늘의 각오는~ 모야모야~?\n$...\n$그럼, 이제 시작해 볼까! \n$도전자님의 실력은 과연 과연~!?" + "1": "자~ 오늘의 각오는~ 모야모야~?\n$…\n$그럼, 이제 시작해 볼까! \n$도전자님의 실력은 과연 과연~!?" }, "victory": { "1": "너의 반짝임은 1000만볼트!" @@ -2566,7 +2769,7 @@ "1": "장하구나! 실로 견줄 자가 천하에 없도다!" }, "defeat": { - "1": "나의 마음에 상쾌한 바람이 지나갔다...\n$정말 대단한 노력이다!" + "1": "나의 마음에 상쾌한 바람이 지나갔다…\n$정말 대단한 노력이다!" } }, "kieran": { @@ -2574,7 +2777,7 @@ "1": "난 노력을 통해 강해지고 또 강해지지!\n$난 지지 않아." }, "victory": { - "1": "믿을 수 없어...\n$정말 재밌고 가슴 뛰는 배틀이었어!" + "1": "믿을 수 없어…\n$정말 재밌고 가슴 뛰는 배틀이었어!" }, "defeat": { "1": "세상에 마상에! 정말 멋진 배틀이었어!\n$네가 더 열심히 훈련할 시간이야." diff --git a/src/locales/ko/egg.json b/src/locales/ko/egg.json index 96985be8cfe..b8550357dfa 100644 --- a/src/locales/ko/egg.json +++ b/src/locales/ko/egg.json @@ -11,6 +11,7 @@ "gachaTypeLegendary": "레전더리 확률 업", "gachaTypeMove": "희귀 알 기술 확률 업", "gachaTypeShiny": "색이 다른 포켓몬 확률 업", + "eventType": "미스터리 이벤트", "selectMachine": "사용할 뽑기 기계를 골라주세요.", "notEnoughVouchers": "바우처가 충분하지 않습니다!", "tooManyEggs": "알을 너무 많이 갖고 있습니다!", @@ -23,4 +24,4 @@ "moveUPGacha": "알 기술 UP!", "shinyUPGacha": "색이 다른 포켓몬\nUP!", "legendaryUPGacha": "UP!" -} \ No newline at end of file +} diff --git a/src/locales/ko/modifier-select-ui-handler.json b/src/locales/ko/modifier-select-ui-handler.json index 04b47542e14..ec9096622c5 100644 --- a/src/locales/ko/modifier-select-ui-handler.json +++ b/src/locales/ko/modifier-select-ui-handler.json @@ -8,5 +8,7 @@ "lockRaritiesDesc": "갱신되는 아이템의 희귀도가 고정됩니다(갱신 비용 증가).", "checkTeamDesc": "파티를 확인하거나 폼 변경 아이템을 사용합니다.", "rerollCost": "₽{{formattedMoney}}", - "itemCost": "₽{{formattedMoney}}" -} \ No newline at end of file + "itemCost": "₽{{formattedMoney}}", + "continueNextWaveButton": "진행하기", + "continueNextWaveDescription": "다음 웨이브로 진행합니다." +} diff --git a/src/locales/ko/modifier-type.json b/src/locales/ko/modifier-type.json index e957fd0d58a..f04dff7b1af 100644 --- a/src/locales/ko/modifier-type.json +++ b/src/locales/ko/modifier-type.json @@ -66,7 +66,21 @@ "description": "자신의 모든 포켓몬의 레벨이 {{levels}}만큼 상승한다." }, "BaseStatBoosterModifierType": { - "description": "지니게 하면 {{stat}} 종족값을 10% 올려준다. 개체값이 높을수록 더 많이 누적시킬 수 있다." + "description": "지니게 하면 {{stat}} 종족치를 10% 올려준다. 개체값이 높을수록 더 많이 누적시킬 수 있다." + }, + "PokemonBaseStatTotalModifierType": { + "name": "단단지주스", + "description": "모든 종족치를 {{statValue}}만큼 {{increaseDecrease}}. 단단지의 {{blessCurse}}[[를]] 받았다.", + "extra": { + "increase": "올려준다", + "decrease": "떨어뜨린다", + "blessed": "축복", + "cursed": "저주" + } + }, + "PokemonBaseStatFlatModifierType": { + "name": "숲의양갱", + "description": "{{stats}} 종족치를 {{statValue}}만큼 올려준다. 이상한 꿈을 꾸고 얻었다." }, "AllPokemonFullHpRestoreModifierType": { "description": "자신의 포켓몬의 HP를 모두 회복한다." @@ -345,7 +359,7 @@ "description": "이 도구를 지닌 포켓몬은 턴이 끝나는 시점에 상태이상에 걸리지 않았다면 화상 상태가 된다." }, "BATON": { - "name": "바톤", + "name": "배턴", "description": "포켓몬을 교체할 때 효과를 넘겨줄 수 있으며, 함정의 영향을 받지 않게 함" }, "SHINY_CHARM": { @@ -401,7 +415,13 @@ "ENEMY_FUSED_CHANCE": { "name": "합체 토큰", "description": "야생 포켓몬이 합체되어 등장할 확률이 1% 추가된다." - } + }, + + "MYSTERY_ENCOUNTER_SHUCKLE_JUICE": { "name": "단단지주스" }, + "MYSTERY_ENCOUNTER_BLACK_SLUDGE": { "name": "검은진흙", "description": "악취가 너무 심한 나머지 상점 물건 가격이 크게 비싸진다." }, + "MYSTERY_ENCOUNTER_MACHO_BRACE": { "name": "교정깁스", "description": "포켓몬을 쓰러뜨리면 교정깁스 스택을 얻는다. 스택이 올라갈수록 능력치가 약간 올라가고, 최대 스택에서 추가 보너스를 얻는다." }, + "MYSTERY_ENCOUNTER_OLD_GATEAU": { "name": "숲의양갱", "description": "{{stats}} 종족치를 {{statValue}}만큼 올려준다." }, + "MYSTERY_ENCOUNTER_GOLDEN_BUG_NET": { "name": "황금잠자리채", "description": "벌레 타입 포켓몬을 더 자주 만날 수 있는 행운을 부여한다. 묘한 무게감이 든다." } }, "SpeciesBoosterItem": { "LIGHT_BALL": { diff --git a/src/locales/ko/move-trigger.json b/src/locales/ko/move-trigger.json index a8a6c0cf86f..12a126baf9d 100644 --- a/src/locales/ko/move-trigger.json +++ b/src/locales/ko/move-trigger.json @@ -66,6 +66,7 @@ "suppressAbilities": "{{pokemonName}}의\n특성이 효과를 발휘하지 못하게 되었다!", "revivalBlessing": "{{pokemonName}}[[는]]\n정신을 차려 싸울 수 있게 되었다!", "swapArenaTags": "{{pokemonName}}[[는]]\n서로의 필드 효과를 교체했다!", + "chillyReception": "{{pokemonName}}[[는]] 썰렁한 개그를 선보였다!", "exposedMove": "{{pokemonName}}[[는]]\n{{targetPokemonName}}의 정체를 꿰뚫어 보았다!", "safeguard": "{{targetName}}[[는]] 신비의 베일이 지켜 주고 있다!", "afterYou": "{{pokemonName}}[[는]]\n배려를 받아들이기로 했다!" 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/mystery-encounter-messages.json b/src/locales/ko/mystery-encounter-messages.json new file mode 100644 index 00000000000..30a9b8a2b18 --- /dev/null +++ b/src/locales/ko/mystery-encounter-messages.json @@ -0,0 +1,7 @@ +{ + "paid_money": "₽{{amount, number}}[[를]] 지불했다.", + "receive_money": "₽{{amount, number}}[[를]] 받았다!", + "affects_pokedex": "도감에 추가 가능", + "cancel_option": "조우 선택지로 돌아가기", + "view_party_button": "파티 확인" +} diff --git a/src/locales/ko/mystery-encounters/a-trainers-test-dialogue.json b/src/locales/ko/mystery-encounters/a-trainers-test-dialogue.json new file mode 100644 index 00000000000..9b36aa0ff62 --- /dev/null +++ b/src/locales/ko/mystery-encounters/a-trainers-test-dialogue.json @@ -0,0 +1,47 @@ +{ + "intro": "무척 강해보이는 트레이너가 다가왔다…", + "buck": { + "intro_dialogue": "여-, 트레이너!\n나는 맥이라고해.$너처럼 강한 트레이너를 만난 김에\n엄청난 제안을 하나 할게!$희귀한 알을 두 개 가지고 있는데,\n하나를 누군가 맡아줬으면 하거든.$네가 나를 이긴다면,\n더 희귀한 알을 너한테 맡기도록 할게!", + "accept": "좋아, 불타오르는걸!", + "decline": "쳇, 아무래도 네 포켓몬들 컨디션이 별로인 거 같구나.$자, 내가 좀 도와줄게." + }, + "cheryl": { + "intro_dialogue": "안녕, 내 이름은 모미야.$너처럼 강한 트레이너가\n관심 있을 만한 제안이 있어.$희귀한 알을 두 개 가지고 있는데,\n하나를 누가 맡아줬으면 해.$네가 승부에서 이긴다면,\n더 희귀한 알을 너한테 맡기도록 할게!", + "accept": "준비 됐겠지!", + "decline": "알았어.\n네 포켓몬들 좀 지쳐 보이네.$자, 내가 좀 도와줄게." + }, + "marley": { + "intro_dialogue": "…@d{64}난 미정이야.$제안이 있어…$알 두 개가 있는데,\n하나를 다른 사람이 맡아줬으면 해.$네가 나보다 강하다면,\n더 희귀한 알을 줄게.", + "accept": "…알겠어.", + "decline": "…알겠어.$네 포켓몬 좀 다쳤구나…\n내가 도와줄게." + }, + "mira": { + "intro_dialogue": "안녕!\n나는 미루!$너처럼 강한 트레이너에게\n미루가 제안이 있어!$미루가 지금 희귀한 알 두개가 있거든\n하나를 다른 사람이 맡아줬으면 좋겠어!$미루에게 네가 더 강하다는걸 보여주면,\n미루가 더 희귀한 알을 줄게!", + "accept": "승부하는 거지?\n야호!", + "decline": "어라, 승부 안 하는 거야?\n괜찮아!$미루가 네 포켓몬들을 치료해 줄게!" + }, + "riley": { + "intro_dialogue": "나는 현이.$너처럼 강한 트레이너에게\n별난 제안을 하나 할게.$지금 두 개의 희귀한 알이 있는데,\n하나를 다른 트레이너에게 맡기고 싶어.$네가 승부에서 이긴다면,\n더 희귀한 알을 줄게!", + "accept": "그 얼굴…\n승부다.", + "decline": "알겠어, 네 포켓몬들 지쳐 보이는구나.$자, 내가 도와줄게." + }, + "title": "선배 트레이너의 테스트", + "description": "이 트레이너는 승부를 받아들이든 거절하든 알을 줄 것 같습니다.\n하지만 이 강한 트레이너를 쓰러트린다면 훨씬 더 희귀한 알을 받을 수 있을 겁니다.", + "query": "어떻게 하시겠습니까?", + "option": { + "1": { + "label": "승부한다", + "tooltip": "(-) 힘든 전투\n(+) @[TOOLTIP_TITLE]{매우 희귀한 알} 획득" + }, + "2": { + "label": "승부를 거절한다", + "tooltip": "(+) 파티 전체 회복\n(+) @[TOOLTIP_TITLE]{알} 획득" + } + }, + "eggTypes": { + "rare": "레어 알", + "epic": "에픽 알", + "legendary": "레전더리 알" + }, + "outro": "{{statTrainerName}}[[가]] {{eggType}}[[를]] 줬다!" +} diff --git a/src/locales/ko/mystery-encounters/absolute-avarice-dialogue.json b/src/locales/ko/mystery-encounters/absolute-avarice-dialogue.json new file mode 100644 index 00000000000..5e039af8f08 --- /dev/null +++ b/src/locales/ko/mystery-encounters/absolute-avarice-dialogue.json @@ -0,0 +1,25 @@ +{ + "intro": "갑자기 나타난 {{greedentName}}[[가]]\n나무열매를 훔쳐갔다!", + "title": "끝없는 욕심", + "description": "당신이 방심한 틈에 {{greedentName}}[[가]] 나무열매를 모두 훔쳐 가버렸습니다!\n\n{{greedentName}}[[는]] 나무열매를 먹으려다가,\n잠시 멈춰서 당신을 재밌다는 듯 바라보고 있습니다.", + "query": "어떻게 하시겠습니까?", + "option": { + "1": { + "label": "싸운다", + "tooltip": "(-) 어려운 배틀\n(+) 비축한 나무열매 획득", + "selected": "{{greedentName}}[[는]] 볼을 빵빵하게 채우고 덤벼들었다!", + "boss_enraged": "{{greedentName}}[[는]] 식탐으로 가득 차 있다!", + "food_stash": "{{greedentName}}[[는]] 많은 음식을 가지고 있는 것 같다!$@s{item_fanfare}모든 포켓몬이 각각 {{foodReward}}[[를]] 얻었다!" + }, + "2": { + "label": "설득한다", + "tooltip": "(+) 나무열매 일부를 돌려받음", + "selected": "{{greedentName}}의 마음이 움직인 것 같다.$훔쳐간 나무열매 일부를 이쪽으로 던졌다." + }, + "3": { + "label": "먹게 둔다", + "tooltip": "(-) 모든 나무열매를 잃음\n(?) {{greedentName}}[[가]] 매우 좋아함", + "selected": "{{greedentName}}[[가]] 눈 깜짝할 새에\n모든 나무열매를 먹어치웠다!$흡족하게 이쪽을 바라보면서\n빵빵해진 배를 두드렸다.$아마 앞으로 식비가 만만치 않을 것 같다…$@s{level_up_fanfare}{{greedentName}}[[는]] 동료가 되고 싶어하는 것 같다!" + } + } +} \ No newline at end of file diff --git a/src/locales/ko/mystery-encounters/an-offer-you-cant-refuse-dialogue.json b/src/locales/ko/mystery-encounters/an-offer-you-cant-refuse-dialogue.json new file mode 100644 index 00000000000..2eefd164182 --- /dev/null +++ b/src/locales/ko/mystery-encounters/an-offer-you-cant-refuse-dialogue.json @@ -0,0 +1,26 @@ +{ + "intro": "금수저 도련님에게 붙들렸다!", + "speaker": "금수저 도련님", + "intro_dialogue": "안녕하세요.$당신의 {{strongestPokemon}}[[가]] 정말 멋져서 그냥 지나칠 수가 없네요!$항상 저런 애완 포켓몬을 갖고 싶었거든요!$제가 오래된 장신구랑\n돈도 함께 섭섭지 않게 챙겨주도록 하죠!", + "title": "거절할 수 없는 제안", + "description": "{{strongestPokemon}}[[를]] 넘기는 대신\n@[TOOLTIP_TITLE]{빛나는부적}과 {{price, money}}을 준다고 합니다!\n\n매우 좋은 조건이지만,\n이렇게 든든한 동료를 보내줘야 할까요?", + "query": "어떻게 하시겠습니까?", + "option": { + "1": { + "label": "제안을 받아들인다", + "tooltip": "(-) {{strongestPokemon}}[[를]] 보내줌\n(+) @[TOOLTIP_TITLE]{빛나는부적}[[를]] 획득\n(+) {{price, money}}[[를]] 획득", + "selected": "좋아요!@d{32} 이리와, {{strongestPokemon}}!$요트 클럽에 가서 다른 사람들에게 자랑해야겠어!$다들 엄청 부러워하겠는걸!" + }, + "2": { + "label": "꼬마 녀석을 혼내준다", + "tooltip": "(+) {{option2PrimaryName}}의 {{moveOrAbility}} 사용\n(+) {{price, money}} 획득", + "tooltip_disabled": "이 선택지를 선택하려면 포켓몬이 특정 기술이나 능력을 가지고 있어야 한다.", + "selected": "말도 안 돼!\n{{liepardName}}, 우리 지금 삥 뜯긴 거야?!$너…!\n곧 우리 변호사 연락을 받게 될 거다!" + }, + "3": { + "label": "떠난다", + "tooltip": "(-) 보상 없음", + "selected": "재수 없는 날이네요....$뭐, 어때. 요트 클럽이나 가자, {{liepardName}}." + } + } +} diff --git a/src/locales/ko/mystery-encounters/berries-abound-dialogue.json b/src/locales/ko/mystery-encounters/berries-abound-dialogue.json new file mode 100644 index 00000000000..090d18911af --- /dev/null +++ b/src/locales/ko/mystery-encounters/berries-abound-dialogue.json @@ -0,0 +1,26 @@ +{ + "intro": "포켓몬 근처에 열매가\n달린 거대한 나무가 있다!", + "title": "나무열매 풍년", + "description": "나무열매 덤불을 지키고 있는 강력한 포켓몬이 있는 것 같습니다.\n그냥 싸워 볼 수도 있겠지만, 조금 강해 보입니다.\n혹시 빠른 포켓몬이 있다면 잡히지 않고도 열매를 가져올 수 있지 않을까요?", + "query": "어떻게 하시겠습니까?", + "berries": "나무열매", + "option": { + "1": { + "label": "싸운다", + "tooltip": "(-) 어려운 배틀\n(+) 나무열매 획득", + "selected": "용감하게 포켓몬에게 다가갔다." + }, + "2": { + "label": "덤불을 향해 뛴다", + "tooltip": "(-) {{fastestPokemon}}의 스피드를 활용\n(+) 나무열매 획득", + "selected": "{{fastestPokemon}}[[가]] 나무열매 덤불을 향해 달려간다!${{enemyPokemon}}[[가]] 반응하기 전에 나무열매 {{numBerries}}개를 낚아챘다!$빼앗은 열매를 들고 재빨리 도망쳤다.", + "selected_bad": "{{fastestPokemon}}[[가]] 나무열매 덤불을 향해 달려간다!$이런! {{enemyPokemon}}[[가]] 더 빨라서 가로막혔다!", + "boss_enraged": "상대 {{enemyPokemon}}[[는]] 분노했다!" + }, + "3": { + "label": "떠난다", + "tooltip": "(-) 보상 없음", + "selected": "강한 포켓몬과 열매를 남겨둔 채 계속 나아갔다." + } + } +} \ No newline at end of file diff --git a/src/locales/ko/mystery-encounters/bug-type-superfan-dialogue.json b/src/locales/ko/mystery-encounters/bug-type-superfan-dialogue.json new file mode 100644 index 00000000000..537de55b97b --- /dev/null +++ b/src/locales/ko/mystery-encounters/bug-type-superfan-dialogue.json @@ -0,0 +1,40 @@ +{ + "intro": "온갖 종류의 곤충채집 장비를 가진\n특이한 트레이너가 길을 막아섰다!", + "intro_dialogue": "거기, 트레이너!\n난 세상에서 가장 희귀한 벌레 포켓몬을 찾고 있어!$너도 벌레 타입 포켓몬 좋아하지?\n모두 벌레 타입 포켓몬을 좋아하니깐!", + "title": "벌레 타입 포켓몬 매니아", + "speaker": "벌레 타입 매니아", + "description": "이 트레이너는 대답할 틈도 없이 계속 떠들어대고 있습니다…\n\n이 상황에서 벗어나려면, 트레이너의 주의를 돌려야 할 것 같습니다!", + "query": "어떻게 하시겠습니까?", + "option": { + "1": { + "label": "승부한다", + "tooltip": "(-) 포켓몬 배틀 도전\n(+) 벌레 타입 기술을 가르침", + "selected": "나한테 도전하겠다고?\n내 벌레 포켓몬들은 이미 준비됐어!" + }, + "2": { + "label": "벌레 포켓몬을 보여준다", + "tooltip": "(+) 아이템 획득", + "disabled_tooltip": "벌레 타입 포켓몬이 없다.", + "selected": "트레이너에게 가지고 있는 모든 벌레 포켓몬을 보여주었다...", + "selected_0_to_1": "응? 너 {{numBugTypes}}밖에 안 갖고 있구나...$너 같은 일반인한테 떠들었다니, 쓸데없이 입만 아팠네…", + "selected_2_to_3": "어라, 너 {{numBugTypes}} 갖고 있구나!\n나쁘지 않아.$내가 더 많이 잡는 데 도움이 될만한 걸 줄게!", + "selected_4_to_5": "뭐야? 너 {{numBugTypes}}마리나 갖고 있네?\n좋아!$아직 수준엔 못 미치지만, 너한테 내 옛날 모습이 보여!\n$이걸 받거라, 내 제자여…!", + "selected_6": "우와!\n{{numBugTypes}}! 모두 벌레 타입!$너도 나만큼 벌레 타입을 사랑하는 거지?$이걸 가져가!\n동지의 증표로 나눠줄게!" + }, + "3": { + "label": "벌레 아이템을 준다", + "tooltip": "(-) 트레이너에게 {{requiredBugItems}}[[를]] 건네줌\n(+) 아이템 획득", + "disabled_tooltip": "{{requiredBugItems}}[[를]] 가지고 있어야 한다.", + "select_prompt": "건네줄 아이템을 선택하세요.", + "invalid_selection": "포켓몬이 해당하는 아이템을 갖고 있지 않다.", + "selected": "트레이너에게 {{selectedItem}}[[를]] 건넸다.", + "selected_dialogue": "우와! {{selectedItem}}, 나한테 주는 거야?\n너 꽤 괜찮은 꼬마구나!$고마우니깐,\n나도 너한테 특별한 선물을 줄게!$가보로 전해져 내려온 귀한 물건이지만\n너한테 주고 싶어!" + } + }, + "battle_won": "너의 지식과 기술들이 우리 약점을 완벽하게 파고들었어.$귀중한 경험을 했으니,\n나도 벌레 타입 기술을 하나 가르쳐 줄게!", + "teach_move_prompt": "기술을 가르칠 포켓몬을 선택하세요.", + "confirm_no_teach": "너, 정말로 이 대단한 기술들이 필요없는 거야?", + "outro": "나중에 멋진 벌레 포켓몬을 만나길 바라며!\n다시 만날 때까지!$사사삭!", + "numBugTypes_one": "벌레 타입 {{count}}마리", + "numBugTypes_other": "벌레 타입 {{count}}마리" +} diff --git a/src/locales/ko/mystery-encounters/clowning-around-dialogue.json b/src/locales/ko/mystery-encounters/clowning-around-dialogue.json new file mode 100644 index 00000000000..f1d8fed6fbf --- /dev/null +++ b/src/locales/ko/mystery-encounters/clowning-around-dialogue.json @@ -0,0 +1,34 @@ +{ + "intro": "어라...@d{64}어릿광대인 걸까?", + "speaker": "어릿광대", + "intro_dialogue": "거기 있는 어리버리한 바보씨, 화려한 배틀을 맞이할 각오를 하시길!\n이 싸움꾼 광대가 당신을 쓰러트리겠습니다!", + "title": "어릿광대의 장난", + "description": "이번 상대는 뭔가 이상합니다.\n어릿광대가 당신을 전투에 끌어들이고 싶어하는 것 같은데, 무슨 목적일까요?\n\n{{blacephalonName}}[[은]] 더욱 수상합니다.\n마치 @[TOOLTIP_TITLE]{이상한 타입과 능력}을 가진 것 같습니다.", + "query": "어떻게 하시겠습니까?", + "option": { + "1": { + "label": "어릿광대와 싸운다", + "tooltip": "(-) 이상한 배틀\n(?) 포켓몬의 특성 조정", + "selected": "당신의 불쌍한 포켓몬들이 한심한 공연을 할 준비가 된 것 같군요!", + "apply_ability_dialogue": "센세이셔널한 쇼였습니다!\n당신의 멋진 기지에는 이 센세이셔널한 보상이 잘 어울릴 것 같군요!", + "apply_ability_message": "어릿광대가 포켓몬 한 마리의 특성을 영구적으로 {{ability}}로 교환해 주겠다고 제안했다!", + "ability_prompt": "포켓몬의 특성을 영구적으로 {{ability}} 특성으로 바꾸시겠습니까?", + "ability_gained": "@s{level_up_fanfare}{{chosenPokemon}}[[는]] {{ability}} 특성을 얻었다!" + }, + "2": { + "label": "도발에 응하지 않는다", + "tooltip": "(-) 광대가 화를 냄\n(?) 포켓몬의 아이템 조정", + "selected": "한심한 겁쟁이로군요? 즐거운 배틀을 거부하다니!\n이러면 한 방 먹이고 싶어지잖습니까!", + "selected_2": "광대의 {{blacephalonName}}[[가]] 무언가 사용한 것 같다!\n{{switchPokemon}}의 모든 아이템이 랜덤하게 재조정된다!", + "selected_3": "당황한 바보같은 표정! 완벽한 저의 한 수를 즐겨주시길!" + }, + "3": { + "label": "조롱을 되돌려준다", + "tooltip": "(-) 광대가 화를 냄\n(?) 포켓몬의 타입 조정", + "selected": "한심한 겁쟁이로군요? 즐거운 배틀을 거부하다니!\n이러면 한 방 먹이고 싶어지잖습니까!", + "selected_2": "광대의 {{blacephalonName}}[[가]] 무언가 사용한 것 같다!\n멤버 포켓몬들의 타입이 랜덤하게 재조정된다!", + "selected_3": "당황한 바보같은 표정! 완벽한 저의 한 수를 즐겨주시길!" + } + }, + "outro": "어릿광대는 동료 포켓몬과 함께 연기 속으로 사라져 버렸다." +} \ No newline at end of file diff --git a/src/locales/ko/mystery-encounters/dancing-lessons-dialogue.json b/src/locales/ko/mystery-encounters/dancing-lessons-dialogue.json new file mode 100644 index 00000000000..b96417274ff --- /dev/null +++ b/src/locales/ko/mystery-encounters/dancing-lessons-dialogue.json @@ -0,0 +1,27 @@ +{ + "intro": "{{oricorioName}}[[가]] 외로이 혼자 춤을 추고 있다.\n주변에 파트너도 없는 것 같다!", + "title": "댄스 댄스 레볼루션", + "description": "{{oricorioName}}[[는]] 공격적이진 않아 보입니다.\n오히려 슬퍼 보이기까지 하네요.\n\n그저 함께 춤출 누군가를\n원하고 있는지도 모릅니다….", + "query": "어떻게 하시겠습니까?", + "option": { + "1": { + "label": "싸운다", + "tooltip": "(-) 어려운 배틀\n(+) 배턴 획득", + "selected": "{{oricorioName}}[[는]] 혼란스러워하며 방어 태세를 갖췄다!", + "boss_enraged": "{{oricorioName}}[[는]] 겁에 질려 능력치가 상승했다!" + }, + "2": { + "label": "춤을 배운다", + "tooltip": "(+) 잠재댄스 기술을 배움", + "selected": "{{oricorioName}}가 춤을 추는 모습을 주의 깊게 지켜보았다….$@s{level_up_fanfare}{{selectedPokemon}}[[는]] {{oricorioName}}에게서 춤을 배웠다!" + }, + "3": { + "label": "춤을 보여준다", + "tooltip": "(-) {{oricorioName}}에게 춤을 가르침\n(+) {{oricorioName}}[[가]] 매우 기뻐함", + "disabled_tooltip": "포켓몬이 춤 기술을 알고 있어야 한다.", + "select_prompt": "사용할 춤 기술을 선택한다.", + "selected": "{{oricorioName}}[[가]] {{selectedPokemon}}[[를]] 선보이는 모습을\n흥미진진하게 지켜본다!$마음에 들어하는 것 같다!$@s{level_up_fanfare}{{oricorioName}}[[가]] 동료가 되고 싶어 하는 것 같다!" + } + }, + "invalid_selection": "이 포켓몬은 춤 기술을 모른다!" +} \ No newline at end of file diff --git a/src/locales/ko/mystery-encounters/dark-deal-dialogue.json b/src/locales/ko/mystery-encounters/dark-deal-dialogue.json new file mode 100644 index 00000000000..c570bc8d5b4 --- /dev/null +++ b/src/locales/ko/mystery-encounters/dark-deal-dialogue.json @@ -0,0 +1,24 @@ + + +{ + "intro": "너덜너덜한 외투를 입은\n이상한 남자가 길을 막았다….", + "speaker": "수상한 남자", + "intro_dialogue": "거기, 너!$난 포켓몬의 숨겨진 힘을 끌어낼 수 있는\n새로운 장치를 만드는 중이거든!$포켓몬을 분자 수준에서 재결합해서\n훨씬 더 강력한 형태로 만드는 거야.$히히…@d{64} 이걸 증명하려면 약간의 희ㅅ-\n음…@d{32} 실험 대상이 필요해.", + "title": "수상한 거래", + "description": "수상한 남자가\n몬스터볼을 여러 개 들고 있습니다.\n\"내가 보장할게! 네 포켓몬을 한 마리만 줘!\n대신 아주 좋은 몬스터볼을 줄 테니까. 히히…\"", + "query": "어떻게 하시겠습니까?", + "option": { + "1": { + "label": "승낙한다", + "tooltip": "(+) 5 로그볼\n(?) 포켓몬 1마리 랜덤 강화", + "selected_dialogue": "어디 보자, {{pokeName}}[[가]] 좋겠군!$명심해, 나쁜 일이 생겨도\n책임 못 지니까!@d{32} 히히…", + "selected_message": "남자는 로그볼 5개를 건넸다.${{pokeName}}[[는]] 이상한 기계에 들어갔다…$표시등이 깜빡이고 이상한 소리가\n기계에서 들려오기 시작했다!$…@d{96} 기계에서 뭔가가\n맹렬한 기세로 뛰쳐나왔다!" + }, + "2": { + "label": "거절한다", + "tooltip": "(-) 보상 없음", + "selected": "불쌍한 사람을 그냥 지나칠거야?\n하!" + } + }, + "outro": "꺼림직한 만남을 뒤로 하고\n침착하게 자리를 떠났다." +} \ No newline at end of file diff --git a/src/locales/ko/mystery-encounters/delibirdy-dialogue.json b/src/locales/ko/mystery-encounters/delibirdy-dialogue.json new file mode 100644 index 00000000000..121c812b1db --- /dev/null +++ b/src/locales/ko/mystery-encounters/delibirdy-dialogue.json @@ -0,0 +1,29 @@ + + +{ + "intro": "{{delibirdName}} 무리가 나타났다!", + "title": "딜리버-디", + "description": "{{delibirdName}}들의 눈이 초롱초롱합니다.\n뭔가 바라는 게 있는 것 같습니다.\n혹시 아이템이나 돈을 원하는 걸까요?", + "query": "무언가 주시겠습니까?", + "invalid_selection": "포켓몬이 그런 종류의 아이템이 없다.", + "option": { + "1": { + "label": "돈을 준다", + "tooltip": "(-) {{delibirdName}}들에게 {{money, money}} 선물\n(+) 선물 아이템 획득", + "selected": "돈을 건네니,\n{{delibirdName}}들은 자기들끼리 신나서 떠들어댔다.$그리고 당신에게 돌아와 행복하게 선물을 건넨다!" + }, + "2": { + "label": "음식을 준다", + "tooltip": "(-) {{delibirdName}}들에게 나무열매나 기적의씨를 선물\n(+) 선물 아이템 획득", + "select_prompt": "건네줄 아이템을 선택하세요.", + "selected": "{{chosenItem}}[[를]] 건네니,\n{{delibirdName}}들은 자기들끼리 신나서 떠들어댔다.$그리고 당신에게 돌아와 행복하게 선물을 건넨다!" + }, + "3": { + "label": "아이템을 준다", + "tooltip": "(-) {{delibirdName}}들에게 지니고 있는 아이템을 선물\n(+) 선물 아이템 획득", + "select_prompt": "건네줄 아이템을 선택하세요.", + "selected": "{{chosenItem}}[[를]] 건네니,\n{{delibirdName}}들은 자기들끼리 신나서 떠들어댔다.$그리고 당신에게 돌아와 행복하게 선물을 건넨다!" + } + }, + "outro": "{{delibirdName}} 무리가 행복하게 뒤뚱거리며 저 멀리 날아간다…$정말 별난 선물교환이었다!" +} \ No newline at end of file diff --git a/src/locales/ko/mystery-encounters/department-store-sale-dialogue.json b/src/locales/ko/mystery-encounters/department-store-sale-dialogue.json new file mode 100644 index 00000000000..96de06d8413 --- /dev/null +++ b/src/locales/ko/mystery-encounters/department-store-sale-dialogue.json @@ -0,0 +1,27 @@ +{ + "intro": "쇼핑백을 잔뜩 가진 여자가 있다!", + "speaker": "백화점 손님", + "intro_dialogue": "안녕!\n너도 대박 세일을 노리고 온 거지?$세일 기간 동안 무료로 아이템을 교환할 수 있는 특별한 쿠폰이 있어!$남는 쿠폰을 나눠줄게!", + "title": "백화점 세일", + "description": "사방에 상품이 가득합니다!\n쿠폰을 쓸 수 있는 교환 코너가 4개나 있는 것 같습니다.\n선택지가 너무 많네요!", + "query": "어디로 가시겠습니까?", + "option": { + "1": { + "label": "기술머신 코너", + "tooltip": "(+) 기술머신 상점" + }, + "2": { + "label": "영양제 코너", + "tooltip": "(+) 영양제 상점" + }, + "3": { + "label": "랭크업 도구 코너", + "tooltip": "(+) 랭크업 도구 상점" + }, + "4": { + "label": "몬스터볼 코너", + "tooltip": "(+) 몬스터볼 상점" + } + }, + "outro": "대박이다!\n앞으로 자주 쇼핑하러 와야겠다." +} \ No newline at end of file diff --git a/src/locales/ko/mystery-encounters/field-trip-dialogue.json b/src/locales/ko/mystery-encounters/field-trip-dialogue.json new file mode 100644 index 00000000000..4f7f3797ac9 --- /dev/null +++ b/src/locales/ko/mystery-encounters/field-trip-dialogue.json @@ -0,0 +1,31 @@ +{ + "intro": "선생님과 어린 학생들이다!", + "speaker": "선생님", + "intro_dialogue": "안녕하세요!\n저희 학생들에게 잠시 시간 좀 내주실 수 있을까요?$포켓몬의 기술에 대해서 가르치고 있는데\n실제 기술을 보게 되면 좋아할 것 같아서요.$괜찮으시면 저희에게\n포켓몬이 배우고 있는 기술을 보여주실래요?", + "title": "현장 학습", + "description": "학교 선생님이 포켓몬의 기술 시연을 부탁했습니다.\n어떤 기술을 선택했는지에 따라 선생님이 뭔가 쓸 만한 걸 주실 수도 있을 것 같네요.", + "query": "어떤 형태의 기술을 보여주겠습니까?", + "option": { + "1": { + "label": "물리 기술", + "tooltip": "(+) 물리 아이템 보상" + }, + "2": { + "label": "특수 기술", + "tooltip": "(+) 특수 아이템 보상" + }, + "3": { + "label": "상태 기술", + "tooltip": "(+) 상태 아이템 보상" + }, + "selected": "{{pokeName}}[[는]] 멋지게 {{move}}[[를]] 보여줬다!" + }, + "second_option_prompt": "포켓몬이 사용할 기술을 골라주세요.", + "incorrect": "…$이건 {{moveCategory}} 기술이 아닌데요!\n죄송하지만, 저희는 이만 가볼게요.$얘들아 이리 오렴.\n다른 곳에서는 제대로 된 기술 시연을 볼 수 있을 거야.", + "incorrect_exp": "좋은 경험(?)을 한 것 같다.$포켓몬도 약간의 경험치를 얻었다.", + "correct": "멋진 시연 감사합니다!\n이 아이템이 도움이 되시길 바랄게요!", + "correct_exp": "{{pokeName}}도 좋은 경험을 쌓았다!", + "status": "상태", + "physical": "물리", + "special": "특수" +} \ No newline at end of file diff --git a/src/locales/ko/mystery-encounters/fiery-fallout-dialogue.json b/src/locales/ko/mystery-encounters/fiery-fallout-dialogue.json new file mode 100644 index 00000000000..9883e038dca --- /dev/null +++ b/src/locales/ko/mystery-encounters/fiery-fallout-dialogue.json @@ -0,0 +1,26 @@ +{ + "intro": "매캐한 연기와 화산재 폭풍을 맞닥뜨렸다!", + "title": "불타는 폴아웃", + "description": "소용돌이치는 화산재와 불씨로 인해 앞이 거의 보이질 않습니다.\n이런 일에는 무언가… 원인이 있는 것 같습니다.\n그렇지만 이 정도로 큰 규모라면 뒤에 도대체 뭐가 있는 걸까요?", + "query": "어떻게 하시겠습니까?", + "option": { + "1": { + "label": "원인을 찾는다", + "tooltip": "(?) 원인을 밝힘\n(-) 어려운 배틀", + "selected": "화산재 폭풍을 뚫고 나아가자,\n구애의 춤을 추고 있는 {{volcaronaName}} 두 마리를 발견했다!$방해받고 싶지 않았던 두 포켓몬이 싸움을 걸어왔다!" + }, + "2": { + "label": "낮은 자세로 이동한다", + "tooltip": "(-) 날씨의 피해를 받음", + "selected": "셸터를 향해 고군분투하는 동안\n재와 불씨로 인해 심각한 피해를 입었다!$모든 포켓몬이 HP의 20% 피해를 받았다!", + "target_burned": "{{burnedPokemon}}도 화상을 입고 말았다!" + }, + "3": { + "label": "불꽃 포켓몬들의 도움", + "tooltip": "(+) 상황 해결\n(+) 목탄 획득", + "disabled_tooltip": "2마리 이상의 불꽃 타입 포켓몬이 필요하다.", + "selected": "{{option3PrimaryName}}[[과]] {{option3SecondaryName}}의 도움으로,\n구애의 춤을 추고 있는 {{volcaronaName}} 두 마리를 발견했다!$다행히 포켓몬들이 그 두 마리를 진정시키고,\n문제없이 신혼여행을 떠나보냈다." + } + }, + "found_charcoal": "날씨가 개자,\n{{leadPokemon}}[[는]] 바닥에서 무언가 발견했다.$@s{item_fanfare}{{leadPokemon}}[[는]] 목탄을 얻었다!" +} \ No newline at end of file diff --git a/src/locales/ko/mystery-encounters/fight-or-flight-dialogue.json b/src/locales/ko/mystery-encounters/fight-or-flight-dialogue.json new file mode 100644 index 00000000000..52fe92f2b63 --- /dev/null +++ b/src/locales/ko/mystery-encounters/fight-or-flight-dialogue.json @@ -0,0 +1,26 @@ +{ + "intro": "빛나는 무언가가 포켓몬 근처에서 반짝반짝 빛나고 있다!", + "title": "도망치거나 맞서 싸우거나", + "description": "강한 포켓몬이 아이템을 지키고 있는 것 같습니다.\n그냥 싸워볼 수도 있지만, 조금 강해보이네요.\n적합한 기술을 가진 포켓몬이 있다면,\n아이템을 빼앗을 수도 있을 것 같습니다.", + "query": "어떻게 하시겠습니까?", + "option": { + "1": { + "label": "포켓몬과 싸운다", + "tooltip": "(-) 어려운 배틀\n(+) 새 아이템", + "selected": "용감하게 포켓몬에게 다가갔다.", + "stat_boost": "{{enemyPokemon}}의 잠재력이 능력치 중 하나를 강화했다!" + }, + "2": { + "label": "아이템을 훔친다", + "disabled_tooltip": "이 선택지를 선택하려면 포켓몬이 특정 기술을 가지고 있어야 합니다.", + "tooltip": "(+) {{option2PrimaryName}}의 {{option2PrimaryMove}} 사용", + "selected": ".@d{32}.@d{32}.@d{32}${{option2PrimaryName}}[[가]] {{option2PrimaryMove}}[[를]] 사용했다!$아이템을 훔쳤다!" + }, + "3": { + "label": "떠난다", + "tooltip": "(-) 보상 없음", + "selected": "강한 포켓몬과 전리품을 남겨둔 채\n당신은 계속 나아간다." + + } + } +} \ No newline at end of file diff --git a/src/locales/ko/mystery-encounters/fun-and-games-dialogue.json b/src/locales/ko/mystery-encounters/fun-and-games-dialogue.json new file mode 100644 index 00000000000..77b8c134399 --- /dev/null +++ b/src/locales/ko/mystery-encounters/fun-and-games-dialogue.json @@ -0,0 +1,30 @@ +{ + "intro_dialogue": "자, 오세요 여러분!\n새로 나온 {{wobbuffetName}} 해머 게임에 도전해 보세요!", + "speaker": "진행자", + "title": "재밌는 놀이!", + "description": "상품이 걸린 게임이 진행 중입니다!\n@[TOOLTIP_TITLE]{3턴} 안에 {{wobbuffetName}}[[를]] @[TOOLTIP_TITLE]{기절시키지 않고 HP를 1}에 가깝게 만들어서, {{wobbuffetName}}이 해머기계에 최고로 강력한 카운터를 날리도록 해야합니다.\n하지만 조심하세요! 만약 {{wobbuffetName}}[[가]] 기절해버리면, 치료비를 내야 할테니까요!", + "query": "게임에 참가해 볼까요?", + "option": { + "1": { + "label": "게임에 참가한다", + "tooltip": "(-) {{option1Money, money}} 지불\n(+) {{wobbuffetName}} 해머 게임 플레이", + "selected": "시작합니다!" + }, + "2": { + "label": "떠난다", + "tooltip": "(-) 보상 없음", + "selected": "약간의 아쉬움을 안고, 서둘러 길을 떠났다." + } + }, + "ko": "안 돼! {{wobbuffetName}}[[가]] 기절해버렸다!$게임에 지고 {{wobbuffetName}}의 병원비까지 내야할 판이다…", + "charging_continue": "마자용은 카운터 한 방을 충전하고 있다!", + "turn_remaining_3": "세 턴 남았다!", + "turn_remaining_2": "두 턴 남았다!", + "turn_remaining_1": "남은 턴은 단 한 번!", + "end_game": "거기까지!${{wobbuffetName}}[[가]] 한껏 충전한 카운터를 날릴 준비를 한다@d{16}.@d{16}.@d{16}.", + "best_result": "{{wobbuffetName}}[[가]] 기계를 때려 부술 기세로 내리쳤다!!!$기계 맨 위에 달려 있던 벨이\n아예 박살나 날아가 버렸다!$최고 1등상을 얻었다!", + "great_result": "{{wobbuffetName}}[[가]] 버튼을 박살낼 듯 내려쳤다!!$벨에 닿을락 말락 하는 곳까지 올라갔다!\n아깝다!!$2등상을 받았다!", + "good_result": "{{wobbuffetName}}[[가]] 버튼을 내리쳤다!$거의 중간까지 올라갔다!$3등상을 받았다!", + "bad_result": "{{wobbuffetName}}[[는]] 힘차게 휘둘러 봤지만\n버튼을 살짝 건드렸을 뿐 아무일도 일어나지 않았다…$안 돼!\n본전도 못 건졌다!", + "outro": "재밌는 미니게임이었다!" +} \ No newline at end of file diff --git a/src/locales/ko/mystery-encounters/global-trade-system-dialogue.json b/src/locales/ko/mystery-encounters/global-trade-system-dialogue.json new file mode 100644 index 00000000000..a4e8c3a1dd1 --- /dev/null +++ b/src/locales/ko/mystery-encounters/global-trade-system-dialogue.json @@ -0,0 +1,32 @@ +{ + "intro": "GTS 단말기가 있다!", + "title": "GTS", + "description": "오, GTS! 세상 참 좋아졌네요.\n이렇게 전세계 사람들과 연결해서 포켓몬을 교환 할 수 있으니 말이에요!\n오늘은 어떤 흥미로운 교환이 이루어질까요?", + "query": "어떻게 하시겠습니까?", + "option": { + "1": { + "label": "교환 제안을 확인한다", + "tooltip": "(+) 포켓몬 1마리와 제안을 골라 교환", + "trade_options_prompt": "교환을 통해 받을 포켓몬을 선택하세요." + }, + "2": { + "label": "미라클 교환을 한다", + "tooltip": "(+) 포켓몬 중 하나를 GTS에 보내고 랜덤한 포켓몬을 받는다" + }, + "3": { + "label": "아이템을 교환한다", + "trade_options_prompt": "교환할 아이템을 선택하세요.", + "invalid_selection": "이 포켓몬은 교환할 아이템이 없습니다.", + "tooltip": "(+) 아이템 하나를 GTS에 보내고 랜덤한 아이템을 받는다" + }, + "4": { + "label": "떠난다", + "tooltip": "(-) 보상 없음", + "selected": "교환할 시간이 없다!\n서둘러 떠났다." + } + }, + "pokemon_trade_selected": "{{tradedPokemon}}[[를]] {{tradeTrainerName}}에게 보냅니다.", + "pokemon_trade_goodbye": "바이바이 {{tradedPokemon}}!", + "item_trade_selected": "{{chosenItem}}[[를]] {{tradeTrainerName}}에게 보냅니다.$.@d{64}.@d{64}.@d{64}\n@s{level_up_fanfare}전송 완료!${{tradeTrainerName}}[[로]]부터 {{itemName}}[[가]] 전송됐다!", + "trade_received": "@s{evolution_fanfare}{{tradeTrainerName}}[[로]]부터 {{received}}[[가]] 전송됐다!${{received}}[[를]] 귀여워해 줘!" +} \ No newline at end of file diff --git a/src/locales/ko/mystery-encounters/lost-at-sea-dialogue.json b/src/locales/ko/mystery-encounters/lost-at-sea-dialogue.json new file mode 100644 index 00000000000..deeb69f0d7a --- /dev/null +++ b/src/locales/ko/mystery-encounters/lost-at-sea-dialogue.json @@ -0,0 +1,28 @@ +{ + "intro": "정처 없이 바다를 헤메었지만,\n당신은 아무런 성과도 거두지 못했습니다.", + "title": "바다 위의 표류", + "description": "이 지역의 바다는 꽤 사나운 편입니다.\n게다가 당신의 기력은 점점 떨어지고 있습니다.\n난감한 상황입니다.\n이 상황을 벗어날 수 있는 방법이 있을까요?", + "query": "어떻게 하시겠습니까?", + "option": { + "1": { + "label": "{{option1PrimaryName}}의 도움", + "label_disabled": "{{option1RequiredMove}} 사용 불가", + "tooltip": "(+) {{option1PrimaryName}}[[가]] 당신을 구함\n(+) {{option1PrimaryName}} 경험치 획득", + "tooltip_disabled": "{{option1RequiredMove}}[[를]] 사용할 수 있는 포켓몬이 없다.", + "selected": "{{option1PrimaryName}}[[가]] 앞장서 헤엄치며, 돌아가는 길을 안내했다.${{option1PrimaryName}} 역시 이번 기회로 더 강해진 것 같다!" + }, + "2": { + "label": "{{option2PrimaryName}}의 도움", + "label_disabled": "{{option2RequiredMove}} 사용 불가", + "tooltip": "(+) {{option2PrimaryName}}[[가]] 당신을 구함\n(+) {{option2PrimaryName}} 경험치 획득", + "tooltip_disabled": "{{option2RequiredMove}}[[를]] 사용할 수 있는 포켓몬이 없다.", + "selected": "{{option2PrimaryName}}[[가]] 보트 앞에서 날아가며, 돌아가는 길을 안내했다.${{option2PrimaryName}} 역시 이번 기회로 더 강해진 것 같다!" + }, + "3": { + "label": "정처 없이 방황한다", + "tooltip": "(-) 모든 포켓몬 HP {{damagePercentage}}% 감소", + "selected": "마침내 기억하는 곳에 다다를 때까지\n정처 없이 배를 타고 떠돌아다닌다.$길고 긴 표류에 포켓몬과 당신 모두 지쳐버렸다." + } + }, + "outro": "다시 여정으로 돌아왔다." +} \ No newline at end of file diff --git a/src/locales/ko/mystery-encounters/mysterious-challengers-dialogue.json b/src/locales/ko/mystery-encounters/mysterious-challengers-dialogue.json new file mode 100644 index 00000000000..9ccfe54d808 --- /dev/null +++ b/src/locales/ko/mystery-encounters/mysterious-challengers-dialogue.json @@ -0,0 +1,22 @@ +{ + "intro": "수수께끼의 도전자들이 나타났다!", + "title": "수수께끼의 도전자", + "description": "만약 도전자에게 승리한다면, 당신의 배틀에 감명해 보상을 받을 수 있을 것 같습니다.\n그렇지만 어떤 도전자들은 꽤 강해보이기도 합니다.\n도전을 받아들일까요?", + "query": "누구와 배틀하시겠습니까?", + "option": { + "1": { + "label": "신중해 보이는 도전자", + "tooltip": "(-) 일반 배틀\n(+) 기술 아이템 보상" + }, + "2": { + "label": "강해 보이는 도전자", + "tooltip": "(-) 어려운 배틀\n(+) 좋은 보상" + }, + "3": { + "label": "가장 강한 도전자", + "tooltip": "(-) 악랄한 배틀\n(+) 훌륭한 보상" + }, + "selected": "선택한 트레이너가 앞으로 걸어 나온다…" + }, + "outro": "수수께끼의 도전자를 물리쳤다!" +} \ No newline at end of file diff --git a/src/locales/ko/mystery-encounters/mysterious-chest-dialogue.json b/src/locales/ko/mystery-encounters/mysterious-chest-dialogue.json new file mode 100644 index 00000000000..3b97f52e625 --- /dev/null +++ b/src/locales/ko/mystery-encounters/mysterious-chest-dialogue.json @@ -0,0 +1,23 @@ +{ + "intro": "이건…@d{32} 뭐지?", + "title": "수수께끼 상자", + "description": "아름답게 장식된 상자가 바닥에 놓여 있습니다. 보통 이럴 땐, 안에 뭔가 좋은 게 들었다고 생각하는 것이 인지상정…\n그렇지 않을까요?", + "query": "열어보시겠습니까?", + "option": { + "1": { + "label": "열어본다", + "tooltip": "@[SUMMARY_BLUE]{({{trapPercent}}%) 끔찍한 무언가}\n@[SUMMARY_GREEN]{({{commonPercent}}%) 평범한 보상}\n@[SUMMARY_GREEN]{({{ultraPercent}}%) 좋은 보상}\n@[SUMMARY_GREEN]{({{roguePercent}}%) 훌륭한 보상}\n@[SUMMARY_GREEN]{({{masterPercent}}%) 대단한 보상}", + "selected": "당신이 상자안에서 찾은 것은…", + "normal": "평범한 도구와 아이템이다.", + "good": "꽤 좋은 도구와 아이템이다.", + "great": "훌륭한 도구와 아이템이다!", + "amazing": "앗! 대단한 아이템!", + "bad": "으악!@d{32}\n상자는 사실 변장한 {{gimmighoulName}}이었다!${{pokeName}}[[가]] 앞으로 뛰어들어 막았지만\n기절해버렸다!" + }, + "2": { + "label": "위험할 수 있으니 떠난다", + "tooltip": "(-) 보상 없음", + "selected": "아쉬움을 뒤로 하고,\n당신은 서둘러 떠났다." + } + } +} diff --git a/src/locales/ko/mystery-encounters/part-timer-dialogue.json b/src/locales/ko/mystery-encounters/part-timer-dialogue.json new file mode 100644 index 00000000000..52dceaa5d35 --- /dev/null +++ b/src/locales/ko/mystery-encounters/part-timer-dialogue.json @@ -0,0 +1,31 @@ +{ + "intro": "바빠보이는 작업 팀장이 나를 불러세웠다.", + "speaker": "작업 팀장", + "intro_dialogue": "거기 당신!\n쓸만한 포켓몬들을 가지고 있는거 같네요.$혹시 시간 있으면 알바 안 할래요?", + "title": "아르바이트", + "description": "해야 할 일이 산더미같이 쌓여 있네요.\n포켓몬이 일을 얼마나 잘하는지에 따라\n일당을 더 많이 받을 수도, 더 적게 받을 수도 있을 것 같습니다.", + "query": "어떤 아르바이트를 해볼까요?", + "invalid_selection": "포켓몬이 건강하지 않습니다.", + "option": { + "1": { + "label": "배달하기", + "tooltip": "(-) 포켓몬의 스피드를 활용\n(+) @[MONEY]{소지금} 획득", + "selected": "{{selectedPokemon}}[[는]] 배달 아르바이트를 시작했다." + }, + "2": { + "label": "창고 정리", + "tooltip": "(-) 포켓몬의 힘과 튼튼함을 활용\n(+) @[MONEY]{소지금} 획득", + "selected": "{{selectedPokemon}}[[는]] 창고 정리를 시작했다." + }, + "3": { + "label": "바람잡이", + "tooltip": "(-) {{option3PrimaryName}}의 {{option3PrimaryMove}}를 활용\n(+) @[MONEY]{소지금} 획득", + "disabled_tooltip": "포켓몬이 해당 작업에 필요한 기술이 없습니다.", + "selected": "{{option3PrimaryName}}[[는]] {{option3PrimaryMove}}[[로]]\n하루 종일 손님들을 끌어모았다!" + } + }, + "job_complete_good": "도와줘서 고마워요!\n오늘은 거의 {{selectedPokemon}} 혼자서 일을 다한 것 같네요!$여기 일당이에요!", + "job_complete_bad": "{{selectedPokemon}}[[가]] 뭐… 조금은 도움이 된 거 같네요!$여기 일당이에요.", + "pokemon_tired": "{{selectedPokemon}}[[가]] 지친 것 같다!\n모든 기술의 PP가 2로 감소했다!", + "outro": "다음에 또 도와주러 와요!" +} \ No newline at end of file diff --git a/src/locales/ko/mystery-encounters/safari-zone-dialogue.json b/src/locales/ko/mystery-encounters/safari-zone-dialogue.json new file mode 100644 index 00000000000..4cdad3dd1ed --- /dev/null +++ b/src/locales/ko/mystery-encounters/safari-zone-dialogue.json @@ -0,0 +1,46 @@ +{ + "intro": "사파리존이다!", + "title": "사파리존", + "description": "여기에는 오만 가지 희귀하고 특별한 포켓몬들이 살고 있습니다! 특별한 포켓몬을 최대 3마리까지 만나고 잡을 기회가 있답니다.\n\n하지만 조심하세요! 잡기도 전에 포켓몬이 도망칠 수도 있으니까요!", + "query": "들어가시겠습니까?", + "option": { + "1": { + "label": "들어간다", + "tooltip": "(-) {{option1Money, money}} 지불\n@[SUMMARY_GREEN]{(?) 사파리존 입장}", + "selected": "행운을 시험할 시간입니다!" + }, + "2": { + "label": "떠난다", + "tooltip": "(-) 보상 없음", + "selected": "약간의 아쉬움을 남기고, 서둘러 길을 떠났다" + } + }, + "safari": { + "1": { + "label": "몬스터볼을 던진다", + "tooltip": "(+) 몬스터볼을 던짐", + "selected": "몬스터볼을 던졌다!" + }, + "2": { + "label": "먹이를 던진다", + "tooltip": "(+) 포획률 상승\n(-) 도망칠 확률 상승", + "selected": "먹이를 던졌다!" + }, + "3": { + "label": "진흙을 던진다", + "tooltip": "(+) 도망칠 확률 감소\n(-) 포획률 감소", + "selected": "진흙을 던졌다!" + }, + "4": { + "label": "도망친다", + "tooltip": "(?) 이 포켓몬에게서 도망친다" + }, + "watching": "{{pokemonName}}[[는]] 조심스럽게 바라보고 있다!", + "eating": "{{pokemonName}}[[가]] 먹이를 먹고 있다!", + "busy_eating": "{{pokemonName}}[[가]] 먹이를 먹기 바쁘다!", + "angry": "{{pokemonName}}[[가]] 화가 난 것 같다!", + "beside_itself_angry": "{{pokemonName}}[[가]] 머리 끝까지 화가 난 것 같다!", + "remaining_count": "포켓몬이 {{remainingCount}}마리 남았다!" + }, + "outro": "재미있는 사파리 체험이었다!" +} \ No newline at end of file diff --git a/src/locales/ko/mystery-encounters/shady-vitamin-dealer-dialogue.json b/src/locales/ko/mystery-encounters/shady-vitamin-dealer-dialogue.json new file mode 100644 index 00000000000..3866570b1db --- /dev/null +++ b/src/locales/ko/mystery-encounters/shady-vitamin-dealer-dialogue.json @@ -0,0 +1,27 @@ +{ + "intro": "검은 코트를 입은 남자가 다가왔다.", + "speaker": "수상한 상인", + "intro_dialogue": ".@d{16}.@d{16}.@d{16}$아-주 좋은 물건이 있는데, 관심 있어?$물론, 네 포켓몬이 이걸 감당할 수 있을지는 모르지만.", + "title": "영양제(?) 딜러", + "description": "남자가 코트를 살짝 열어 보여주자,\n코트 안쪽에 포켓몬 영양제들이 빼곡히 들어있는 것이 보입니다.\n그가 부른 가격은 아주 매력적입니다. 좋아도 너무 좋군요…\n그는 두가지 선택지를 제시했습니다.", + "query": "거래를 받아들일까요?", + "invalid_selection": "포켓몬이 충분히 건강하지 않습니다.", + "option": { + "1": { + "label": "저렴한 거래", + "tooltip": "(-) {{option1Money, money}} 지불\n(-) 부작용?\n(+) 선택한 포켓몬이 2가지 무작위 영양제 효과를 얻음" + }, + "2": { + "label": "비싼 거래", + "tooltip": "(-) {{option2Money, money}} 지불\n(+) 선택한 포켓몬이 2가지 무작위 영양제 효과를 얻음" + }, + "3": { + "label": "떠난다", + "tooltip": "(-) 보상 없음", + "selected": "흥, 너 같은 겁쟁이한테는 안 팔아." + }, + "selected": "남자는 영양제 두 병을 건넨 뒤,\n빠르게 사라졌다.${{selectedPokemon}}[[는]] {{boost1}}[[과]] {{boost2}} 효과를 얻었다!" + }, + "cheap_side_effects": "그런데 약이 부작용이 있는 것 같다!${{selectedPokemon}}[[가]] 약간 고통스러워한다.\n성격이 {{newNature}}[[로]] 변해버렸다!", + "no_bad_effects": "딱히 부작용은 없는 것 같다!" +} \ No newline at end of file diff --git a/src/locales/ko/mystery-encounters/slumbering-snorlax-dialogue.json b/src/locales/ko/mystery-encounters/slumbering-snorlax-dialogue.json new file mode 100644 index 00000000000..afac8be35e2 --- /dev/null +++ b/src/locales/ko/mystery-encounters/slumbering-snorlax-dialogue.json @@ -0,0 +1,25 @@ +{ + "intro": "좁은 길을 걷다보니 거대한 실루엣이\n길을 막고 있는 것을 발견했다.$가까이 다가가 보니 평화롭게 자고있는 {{snorlaxName}}였다.\n피할 수 있는 방법은 없는 것 같다….", + "title": "잠자는 {{snorlaxName}}", + "description": "자고있는 {{snorlaxName}}[[를]] 지금 당장 공격해서 자리를 비키게 할 수도, 그냥 알아서 잠에서 깨어날 때까지 기다릴 수도 있습니다.\n얼마나 기다려야 할지는 알 수 없겠지만요….", + "query": "어떻게 하시겠습니까?", + "option": { + "1": { + "label": "싸운다", + "tooltip": "(-) 잠자는 {{snorlaxName}}[[과]] 배틀\n(+) 특별한 보상", + "selected": "용감하게 포켓몬에게 다가갔다." + }, + "2": { + "label": "움직일 때까지 기다린다", + "tooltip": "(-) 긴 시간을 기다림 \n(+) 멤버 HP회복", + "selected": ".@d{32}.@d{32}.@d{32}$한참을 기다리다보니, {{snorlaxName}}의 하품이 모두를 졸리게 만든다….", + "rest_result": "모두가 잠에서 깨어났을 무렵, {{snorlaxName}}[[는]] 어디에도 없었다.\n하지만 모든 포켓몬이 회복되었다!" + }, + "3": { + "label": "아이템을 훔친다", + "tooltip": "(+) {{option3PrimaryName}}[[가]] {{option3PrimaryMove}}[[를]] 사용\n(+) 특별한 보상", + "disabled_tooltip": "포켓몬이 특정 기술을 알고 있어야 한다.", + "selected": "{{option3PrimaryName}}[[는]] {{option3PrimaryMove}}[[를]] 사용했다!$@s{item_fanfare}자고 있는 {{snorlaxName}}[[로]]부터\n남은 음식을 훔쳐서 살금살금 도망쳤다!" + } + } +} \ No newline at end of file diff --git a/src/locales/ko/mystery-encounters/teleporting-hijinks-dialogue.json b/src/locales/ko/mystery-encounters/teleporting-hijinks-dialogue.json new file mode 100644 index 00000000000..de87ab83858 --- /dev/null +++ b/src/locales/ko/mystery-encounters/teleporting-hijinks-dialogue.json @@ -0,0 +1,27 @@ +{ + "intro": "시끄럽게 윙윙거리고 있는\n이상한 기계를 발견했다….", + "title": "텔레포트 대소동", + "description": "기계에는 다음과 같이 설명이 쓰여 있습니다.\n\"사용하려면 돈을 넣고 캡슐에 들어가세요.\"\n\n기계를 작동시키면 어딘가로 이동할 수 있을 듯하다……", + "query": "어떻게 하시겠습니까?", + "option": { + "1": { + "label": "돈을 넣는다", + "tooltip": "(-) {{price, money}} 지불\n(?) 다른 바이옴으로 이동", + "selected": "돈을 넣자 캡슐이 열렸다.\n당신은 안으로 들어갔다……." + }, + "2": { + "label": "포켓몬의 도움", + "tooltip": "(-) {{option2PrimaryName}}[[가]] 도와줌\n(+) {{option2PrimaryName}}[[가]] 경험치를 획득\n(?) 다른 바이옴으로 이동", + "disabled_tooltip": "강철 또는 전기 타입 포켓몬이 있어야 한다.", + "selected": "{{option2PrimaryName}} 덕분에\n결제 시스템을 해제할 수 있었다!$캡슐이 열리자\n당신은 안으로 들어갔다…." + }, + "3": { + "label": "기계를 조사한다", + "tooltip": "(-) 포켓몬 배틀", + "selected": "기계에서 나오는 깜빡이는 불빛과\n기묘한 소리에 몰두했다…….$정신이 팔려서 야생 포켓몬이\n다가오는 것도 눈치채지 못했다!" + } + }, + "transport": "별안간 기계가 심하게 흔들리면서\n온갖 이상한 소리가 나기 시작했다!$얼마 안 가서 기계는 다시 조용해졌다.", + "attacked": "새로운 지역에 발을 내딛자마자\n놀란 야생 포켓몬과 마주쳤다!$야생 포켓몬이 달려들었다!", + "boss_enraged": "상대 {{enemyPokemon}}[[는]] 화가 난 것 같다!" +} \ No newline at end of file diff --git a/src/locales/ko/mystery-encounters/the-expert-pokemon-breeder-dialogue.json b/src/locales/ko/mystery-encounters/the-expert-pokemon-breeder-dialogue.json new file mode 100644 index 00000000000..e226741c60a --- /dev/null +++ b/src/locales/ko/mystery-encounters/the-expert-pokemon-breeder-dialogue.json @@ -0,0 +1,32 @@ +{ + "intro": "포켓몬 알을 잔뜩 가진 트레이너가 나타났다!", + "intro_dialogue": "안녕하세요, 트레이너!$당신의 몇몇 파트너 포켓몬이\n조금 기운 없어 보이네요.$저와 배틀을 해서\n기운이 나게끔 해주는 건 어떨까요?", + "title": "전문 브리더", + "description": "브리더를 상대로 @[TOOLTIP_TITLE]{포켓몬 1마리만} 사용해서 승부해야 합니다. 조금 힘들겠지만, 선택한 포켓몬과 유대감이 더욱 깊어질 기회입니다!\n게다가 승리하면 @[TOOLTIP_TITLE]{포켓몬 알}까지 준다고 합니다!", + + "query": "어떤 포켓몬으로 싸우겠습니까?", + "cleffa_1_nickname": "에이스", + "cleffa_2_nickname": "픽시짱", + "cleffa_3_nickname": "{{speciesName}} 대왕", + "option": { + "1": { + "label": "{{pokemon1Name}}", + "tooltip_base": "(-) 힘든 배틀\n(+) {{pokemon1Name}} 친밀도 상승" + }, + "2": { + "label": "{{pokemon2Name}}", + "tooltip_base": "(-) 힘든 배틀\n(+) {{pokemon2Name}} 친밀도 상승" + }, + "3": { + "label": "{{pokemon3Name}}", + "tooltip_base": "(-) 힘든 배틀\n(+) {{pokemon3Name}} 친밀도 상승" + }, + "selected": "그럼 갑니다!" + }, + "outro": "{{chosenPokemon}}[[가]] 정말 행복해 보이네요!$여기, 이것도 드릴게요.", + "outro_failed": "실망이네요….$포켓몬의 신뢰를 얻으려면 아직 멀었어요!", + "gained_eggs": "@s{item_fanfare}{{numEggs}}[[를]] 받았습니다!", + "eggs_tooltip": "\n(+) {{eggs}} 획득", + "numEggs_one": "{{rarity}}알 {{count}}개", + "numEggs_other": "{{rarity}}알 {{count}}개" +} diff --git a/src/locales/ko/mystery-encounters/the-pokemon-salesman-dialogue.json b/src/locales/ko/mystery-encounters/the-pokemon-salesman-dialogue.json new file mode 100644 index 00000000000..f5b35cb33cb --- /dev/null +++ b/src/locales/ko/mystery-encounters/the-pokemon-salesman-dialogue.json @@ -0,0 +1,23 @@ +{ + "intro": "쾌활한 노신사가 다가왔다.", + "speaker": "신사", + "intro_dialogue": "거기, 안녕하신가!\n자네에게 좋은 거래를 제안하지!", + "title": "포켓몬 상인", + "description": "\"이 {{purchasePokemon}}[[는]] 다른 평범한 {{purchasePokemon}}에서는 찾을 수 없는 매우 희귀한 특성을 갖고 있다네!\n내가 이 멋진 {{purchasePokemon}}[[를]] 자네에게 {{price, money}}에 주고 싶은데!\"\n\n\"어떻게 생각하는가?\"", + "description_shiny": "\"이 {{purchasePokemon}}[[는]] 다른 평범한 {{purchasePokemon}}에서는 볼 수 없는 매우 희귀한 색깔이라네!\n내가 이 멋진 {{purchasePokemon}}[[를]] 자네에게 {{price, money}}에 주고 싶은데!\"\n\n\"어떻게 생각하는가?\"", + "query": "어떻게 하시겠습니까?", + "option": { + "1": { + "label": "승낙한다", + "tooltip": "(-) {{price, money}} 지불\n(+) 숨겨진 특성을 가진 {{purchasePokemon}}[[를]] 얻음", + "tooltip_shiny": "(-) {{price, money}} 지불\n(+) 특별한 색을 가진 {{purchasePokemon}}[[를]] 얻음", + "selected_message": "터무니없는 금액을 내고\n{{purchasePokemon}}[[를]] 데려왔다.", + "selected_dialogue": "탁월한 선택이로군!$자네는 비즈니스에 안목이 있구먼.$아차차…@d{64} 반품은 안 된다네. 알고 있겠지?" + }, + "2": { + "label": "거절한다", + "tooltip": "(-) 보상 없음", + "selected": "아니?@d{32} 거절하겠다고?$요즘 젊은이들은\n호의를 베풀어도 모른다니까!" + } + } +} \ No newline at end of file diff --git a/src/locales/ko/mystery-encounters/the-strong-stuff-dialogue.json b/src/locales/ko/mystery-encounters/the-strong-stuff-dialogue.json new file mode 100644 index 00000000000..1563041e5bd --- /dev/null +++ b/src/locales/ko/mystery-encounters/the-strong-stuff-dialogue.json @@ -0,0 +1,21 @@ +{ + "intro": "커다란 {{shuckleName}}를 만났다!\n옆에 한가득 차 있는 저건 주스…?", + "title": "강력한 물질", + "description": "당신을 막아선 {{shuckleName}}[[는]] 엄청나게 강해보입니다. 게다가 그 옆에 있는 주스에서는 무언가 강한 기운이 느껴지는 것 같습니다.\n\n{{shuckleName}}가 당신 쪽으로 다리를 내밉니다.\n무언가 하려는 모양입니다…", + "query": "어떻게 하시겠습니까?", + "option": { + "1": { + "label": "{{shuckleName}}에게 다가간다", + "tooltip": "(?) 끔찍하거나 놀라운 일", + "selected": "당신은 기절해버렸다……….", + "selected_2": "@f{150}정신을 차리고 나니, {{shuckleName}}[[는]] 사라지고\n주스도 완전히 비었다.${{highBstPokemon1}}[[과]] {{highBstPokemon2}}[[는]]\n굉장히 무기력해졌다!$두 포켓몬의 종족치가 각각 {{reductionValue}}만큼 감소했다!$하지만 다른 포켓몬들은 놀라울만큼 활력이 넘친다!\n다른 포켓몬들의 종족치가 각각 {{increaseValue}}만큼 증가했다!" + }, + "2": { + "label": "{{shuckleName}}[[과]] 싸운다", + "tooltip": "(-) 어려운 배틀\n(+) 특별한 보상", + "selected": "화가 난 {{shuckleName}}[[가]] 주스를 마시고 공격해 왔다!", + "stat_boost": "주스를 마신 {{shuckleName}}의 능력치가 올라갔다!" + } + }, + "outro": "기묘한 일을 겪은 것 같다…." +} \ No newline at end of file diff --git a/src/locales/ko/mystery-encounters/the-winstrate-challenge-dialogue.json b/src/locales/ko/mystery-encounters/the-winstrate-challenge-dialogue.json new file mode 100644 index 00000000000..b7b96926f57 --- /dev/null +++ b/src/locales/ko/mystery-encounters/the-winstrate-challenge-dialogue.json @@ -0,0 +1,22 @@ +{ + "intro": "한 가족이 집 앞에 모두 나와있다!", + "speaker": "연승가족", + "intro_dialogue": "우리는 연승가족!$잠깐 어떤가?\n지금 여기서 우리 가족 4명과 포켓몬 승부를 겨뤄보지 않겠나?", + "title": "연승가족", + "description": "모두 5명인 연승가족이 승부를 바라고 있습니다!\n가족 모두를 연달아 쓰러뜨릴 수 있다면 어마어마한 보상을 줄 겁니다.\n하지만 이걸 감당할 수 있을까요?", + "query": "어떻게 하시겠습니까?", + "option": { + "1": { + "label": "승부한다", + "tooltip": "(-) 치열한 승부\n(+) 특별한 아이템 획득", + "selected": "승부를 시작하지!" + }, + "2": { + "label": "승부를 거절한다", + "tooltip": "(+) 파티 전체 회복\n(+) 더이상한사탕 획득", + "selected": "아쉽군. 그런데 자네 포켓몬들 좀 지쳐보이는구만,\n조금 쉬고 가지 않을텐가?" + } + }, + "victory": "우리의 시련을 모두 극복하다니 축하하네!$먼저 이 바우처를 주겠네.", + "victory_2": "우리 가족은 이 교정깁스로 포켓몬을 더 효과적으로 훈련하고 있다네.$우리 전부를 이긴 자네에겐 의미 없을 수도 있겠지만,\n그래도 이걸 받아주게!" +} \ No newline at end of file diff --git a/src/locales/ko/mystery-encounters/training-session-dialogue.json b/src/locales/ko/mystery-encounters/training-session-dialogue.json new file mode 100644 index 00000000000..fc9d6721c40 --- /dev/null +++ b/src/locales/ko/mystery-encounters/training-session-dialogue.json @@ -0,0 +1,33 @@ +{ + "intro": "훈련에 쓸 만한 물건들을 발견했다.", + "title": "트레이닝 시간", + "description": "발견한 기구들은 포켓몬들을 훈련시키는 데 쓸 수 있을 것 같습니다!\n파티의 다른 포켓몬들과 싸우면서 원하는 방식으로 훈련해 보면 어떨까요?", + "query": "어떻게 훈련할까요?", + "invalid_selection": "포켓몬이 힘들어 보인다.", + "option": { + "1": { + "label": "가벼운 트레이닝", + "tooltip": "(-) 쉬운 배틀 \n(+) 선택 포켓몬 무작위 개체치 2가지 향상", + "finished": "훈련을 마친 {{selectedPokemon}}[[는]] 조금 지쳤지만 뿌듯해 보인다!${{selectedPokemon}}의 {{stat1}}[[과]] {{stat2}}[[가]] 상승했다!" + }, + "2": { + "label": "일반적인 트레이닝", + "tooltip": "(-) 평범한 배틀\n(+) 선택 포켓몬 성격 변경", + "select_prompt": "포켓몬과 훈련할\n새로운 성격을 선택하세요.", + "finished": "훈련을 마친 {{selectedPokemon}}[[는]] 조금 지쳤지만 뿌듯해 보인다!${{selectedPokemon}}의 성격이 {{nature}}[[로]] 바뀌었다!" + }, + "3": { + "label": "고강도 트레이닝", + "tooltip": "(-) 힘든 배틀\n(+) 선택 포켓몬 특성 변경", + "select_prompt": "포켓몬과 훈련할\n새로운 특성을 선택을 선택하세요.", + "finished": "훈련을 마친 {{selectedPokemon}}[[는]] 조금 지쳤지만 뿌듯해 보인다!${{selectedPokemon}}의 특성이 {{ability}}[[로]] 바뀌었다!" + }, + "4": { + "label": "떠난다", + "tooltip": "(-) 보상 없음", + "selected": "훈련할 시간은 없다.\n바로 길을 떠났다." + }, + "selected": "{{selectedPokemon}}[[가]] 공터를 가로질러\n이쪽을 보고 마주섰다…" + }, + "outro": "성공적인 트레이닝 시간이었다!" +} \ No newline at end of file diff --git a/src/locales/ko/mystery-encounters/trash-to-treasure-dialogue.json b/src/locales/ko/mystery-encounters/trash-to-treasure-dialogue.json new file mode 100644 index 00000000000..ac285692d23 --- /dev/null +++ b/src/locales/ko/mystery-encounters/trash-to-treasure-dialogue.json @@ -0,0 +1,19 @@ +{ + "intro": "엄청난 쓰레기 더미다!\n도대체 어디서 온 걸까?", + "title": "쓰레기 속 보물", + "description": "쓰레기 더미가 눈앞에 펼쳐져 있습니다.\n그 속에서 가치 있는 물건을 찾을 수도 있겠죠.\n하지만 그 물건들을 얻기 위해\n정말로 오물을 뒤집어쓰고 싶으신가요?", + "query": "무엇을 하시겠습니까?", + "option": { + "1": { + "label": "쓸만한 것을 발굴한다", + "tooltip": "(-) 회복 아이템 가격 3배 상승\n(+) 끝내주는 아이템들을 획득", + "selected": "쓰레기 더미를 헤집고 다니며 오물을 뒤집어썼다.$지금 이 상태라면 아무리 친절한 점원이라도\n물건을 제 가격에 안 팔 것 같다!$앞으로는 상점에서 파는 회복약을 훨씬 비싸게 사야 한다.$하지만 쓰레기에서 엄청난 물건들을 발견했다!" + }, + "2": { + "label": "주변을 조사한다", + "tooltip": "(?) 쓰레기의 근원을 찾는다", + "selected": "쓰레기 더미 주변을 거닐며,\n이게 여기 어떻게 생겨난 건지 단서를 찾아본다…", + "selected_2": "갑자기 쓰레기 더미가 움직였다!\n쓰레기가 아니라 포켓몬이었다!" + } + } +} \ No newline at end of file diff --git a/src/locales/ko/mystery-encounters/uncommon-breed-dialogue.json b/src/locales/ko/mystery-encounters/uncommon-breed-dialogue.json new file mode 100644 index 00000000000..aeec128b313 --- /dev/null +++ b/src/locales/ko/mystery-encounters/uncommon-breed-dialogue.json @@ -0,0 +1,26 @@ +{ + "intro": "저건 평범한 포켓몬이 아닌 것 같다!", + "title": "흔하지 않은 포켓몬", + "description": "저 {{enemyPokemon}}[[는]] 다른 {{enemyPokemon}}[[과]]는 어딘가 달라보입니다. @[TOOLTIP_TITLE]{아마 특별한 기술을 알고 있을지도?}\n평범하게 배틀로 잡을 수도 있지만, 친구가 되는 방법도 있을 것 같습니다.", + "query": "어떻게 하시겠습니까?", + "option": { + "1": { + "label": "포켓몬과 싸운다", + "tooltip": "(-) 까다로운 배틀\n(+) 포획 가능한 강한 적", + "selected": "{{enemyPokemon}}에게 용감하게 맞섰다.", + "stat_boost": "{{enemyPokemon}}[[가]] 집중해서 능력치가 상승했다!" + }, + "2": { + "label": "음식을 준다", + "disabled_tooltip": "선택지를 고르려면 나무열매가 4개 필요하다.", + "tooltip": "(-) 나무열매 4개를 줌\n(+) {{enemyPokemon}}[[가]] 좋아함", + "selected": "{{enemyPokemon}}에게 나무열매를 주었다!$행복한 듯 맛있게 먹기 시작했다!${{enemyPokemon}}[[는]] 동료가 되고 싶어하는 것 같다!" + }, + "3": { + "label": "친구가 되기", + "disabled_tooltip": "파티의 포켓몬이 특정한 기술을 알고 있어야한다.", + "tooltip": "(+) {{option3PrimaryName}}[[가]] {{option3PrimaryMove}}[[를]] 사용\n(+) {{enemyPokemon}}[[가]] 좋아함", + "selected": "{{option3PrimaryName}}[[가]] {{option3PrimaryMove}}[[를]] 사용해\n{{enemyPokemon}}를 매료시켰다!${{enemyPokemon}}[[는]] 동료가 되고 싶어하는 것 같다!" + } + } +} \ No newline at end of file diff --git a/src/locales/ko/mystery-encounters/weird-dream-dialogue.json b/src/locales/ko/mystery-encounters/weird-dream-dialogue.json new file mode 100644 index 00000000000..303c459a1fb --- /dev/null +++ b/src/locales/ko/mystery-encounters/weird-dream-dialogue.json @@ -0,0 +1,22 @@ +{ + "intro": "어떤 여자의 실루엣이 앞을 막아섰다.\n그녀에게서 뭔가 불안한 느낌이 든다…….", + "speaker": "여자", + "intro_dialogue": "너의 미래와 과거를 봤어….$저기…\n너도 그게 보이는 걸까?", + "title": "???", + "description": "여자의 말이 머릿속에서 메아리처럼 울려 퍼집니다. 이건 단순히 한 사람의 목소리가 아닙니다. 모든 시간선과 현실에서 온 수많은 사람들의 목소리입니다.\n당신은 어지러움을 느끼기 시작하고, 한 질문이 머릿속을 맴돌기 시작합니다…\n\n@[TOOLTIP_TITLE]{\"너의 미래와 과거를 봤어….저기… 너도 그게 보이는 걸까?\"}", + "query": "어떻게 하시겠습니까?", + "option": { + "1": { + "label": "고개를 끄덕인다", + "tooltip": "@[SUMMARY_GREEN]{(?) 파티의 포켓몬에 영향}", + "selected": "그녀는 당신에게 손을 뻗었고,\n이내 모든 것이 깜깜해졌다.$그리고…@d{64} 당신은 모든 것이 보인다.\n모든 시간선, 또 다른 당신의 모습,\n 과거와 미래.$당신을 만들어온 모든 것,\n당신이 될 모든 것…@d{64}", + "cutscene": "당신의 포켓몬들이 보인다.@d{32} 모든 현실이\n합쳐져 새로운 무언가가 되고 있다…@d{64}", + "dream_complete": "잠에서 깨어나자, 여자는 사라진 것 같다.\n사람이 아니라 유령이었을지도…$.@d{32}.@d{32}.@d{32}?$파티의 포켓몬이 바뀌었다…?$…아니, 원래부터 이 상태였는지도 모른다." + }, + "2": { + "label": "빠르게 떠난다", + "tooltip": "(-) 파티의 포켓몬에 영향", + "selected": "당신을 무감각하게 만들던 손아귀에서 벗어나,\n정신을 차리고 서둘러 자리를 떠났다.$간신히 정신을 추스르고 멈춰 서서\n파티의 포켓몬들을 확인했다.$이유는 알 수 없지만\n파티의 모든 포켓몬 레벨이 약간 줄어들었다!" + } + } +} \ No newline at end of file diff --git a/src/locales/ko/party-ui-handler.json b/src/locales/ko/party-ui-handler.json index 468f33bf960..35cf379556f 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정말 놓아주겠습니까?", @@ -44,4 +47,4 @@ "untilWeMeetAgain": "다시 만날 때까지, {{pokemonName}}!", "sayonara": "사요나라, {{pokemonName}}!", "smellYaLater": "또 보자, {{pokemonName}}!" -} \ No newline at end of file +} 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/status-effect.json b/src/locales/ko/status-effect.json index d65243a8a24..489e67caaac 100644 --- a/src/locales/ko/status-effect.json +++ b/src/locales/ko/status-effect.json @@ -1,12 +1,6 @@ { "none": { - "name": "없음", - "description": "", - "obtain": "", - "obtainSource": "", - "activation": "", - "overlap": "", - "heal": "" + "name": "없음" }, "poison": { "name": "독", 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..b9d9a365b46 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": "레드 & 그린", @@ -160,5 +166,18 @@ "alder_iris_double": "노간주 & 아이리스", "iris_alder_double": "아이리스 & 노간주", "marnie_piers_double": "마리 & 두송", - "piers_marnie_double": "두송 & 마리" + "piers_marnie_double": "두송 & 마리", + + "buck": "맥", + "cheryl": "모미", + "marley": "미정", + "mira": "미루", + "riley": "현이", + "victor": "상우", + "victoria": "수혜", + "vivi": "추희", + "vicky": "영아", + "vito": "철준", + "bug_type_superfan": "벌레타입 마니아", + "expert_pokemon_breeder": "포켓몬 전문 브리더" } diff --git a/src/locales/ko/trainer-titles.json b/src/locales/ko/trainer-titles.json index 7cff2207817..20b2a8e0f65 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,8 @@ "flare_admin_female": "플레어단 간부", "aether_admin": "에테르재단 지부장", "skull_admin": "스컬단 간부", - "macro_admin": "매크로코스모스 간부" + "macro_admin": "매크로코스모스 간부", + "star_admin": "스타단 군단 보스", + + "the_winstrates": "연승가족" } diff --git a/src/locales/pt_BR/ability-trigger.json b/src/locales/pt_BR/ability-trigger.json index cd47fd8e3dc..60e86f3723c 100644 --- a/src/locales/pt_BR/ability-trigger.json +++ b/src/locales/pt_BR/ability-trigger.json @@ -60,4 +60,4 @@ "postSummonTabletsOfRuin": "Tablets of Ruin de {{pokemonNameWithAffix}} reduziu o {{statName}}\nde todos os Pokémon em volta!", "postSummonBeadsOfRuin": "Beads of Ruin de {{pokemonNameWithAffix}} reduziu a {{statName}}\nde todos os Pokémon em volta!", "preventBerryUse": "{{pokemonNameWithAffix}} está nervoso\ndemais para comer frutas!" -} \ No newline at end of file +} diff --git a/src/locales/pt_BR/ability.json b/src/locales/pt_BR/ability.json index c4180ff01dd..2c70f82f53f 100644 --- a/src/locales/pt_BR/ability.json +++ b/src/locales/pt_BR/ability.json @@ -421,7 +421,7 @@ }, "aftermath": { "name": "Aftermath", - "description": "Caso o Pokémon seja derrotado em decorrência de um movimento de contato, o atacante recebe dano." + "description": "Causa dano ao atacante se ele entrar em contato com o Pokémon com um golpe final." }, "anticipation": { "name": "Anticipation", @@ -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/achv.json b/src/locales/pt_BR/achv.json index 93e982b60ea..05727deeb18 100644 --- a/src/locales/pt_BR/achv.json +++ b/src/locales/pt_BR/achv.json @@ -268,5 +268,9 @@ "INVERSE_BATTLE": { "name": "A torre da derrotA", "description": "Complete o desafio da Batalha Inversa.\n.asrevnI ahlataB ad oifased o etelpmoC" + }, + "BREEDERS_IN_SPACE": { + "name": "Criadores no Espaço!", + "description": "Derrote o Criador de Pokémon Experiente no Bioma Espaço." } } diff --git a/src/locales/pt_BR/arena-tag.json b/src/locales/pt_BR/arena-tag.json index 7ab1ecea721..3a1476dcef6 100644 --- a/src/locales/pt_BR/arena-tag.json +++ b/src/locales/pt_BR/arena-tag.json @@ -54,4 +54,4 @@ "safeguardOnRemove": "O campo não está mais protegido por Safeguard!", "safeguardOnRemovePlayer": "Sua equipe não está mais protegido por Safeguard!", "safeguardOnRemoveEnemy": "A equipe adversária não está mais protegido por Safeguard!" -} \ No newline at end of file +} diff --git a/src/locales/pt_BR/battle-info.json b/src/locales/pt_BR/battle-info.json index 0fd211c5c85..8a43839efb1 100644 --- a/src/locales/pt_BR/battle-info.json +++ b/src/locales/pt_BR/battle-info.json @@ -1,3 +1,3 @@ { "generation": "Geração {{generation}}" -} \ No newline at end of file +} diff --git a/src/locales/pt_BR/battle-message-ui-handler.json b/src/locales/pt_BR/battle-message-ui-handler.json index ee7062bccba..d82fd665821 100644 --- a/src/locales/pt_BR/battle-message-ui-handler.json +++ b/src/locales/pt_BR/battle-message-ui-handler.json @@ -5,4 +5,4 @@ "ivPrettyGood": "Bom", "ivDecent": "Regular", "ivNoGood": "Ruim" -} \ No newline at end of file +} diff --git a/src/locales/pt_BR/battle-scene.json b/src/locales/pt_BR/battle-scene.json index a0288475d69..de2232b4edf 100644 --- a/src/locales/pt_BR/battle-scene.json +++ b/src/locales/pt_BR/battle-scene.json @@ -1,3 +1,3 @@ { "moneyOwned": "₽{{formattedMoney}}" -} \ No newline at end of file +} diff --git a/src/locales/pt_BR/battle.json b/src/locales/pt_BR/battle.json index 08eeb99e0cd..72fcc018980 100644 --- a/src/locales/pt_BR/battle.json +++ b/src/locales/pt_BR/battle.json @@ -14,6 +14,10 @@ "moneyWon": "Você ganhou\n₽{{moneyAmount}} por vencer!", "moneyPickedUp": "Você pegou ₽{{moneyAmount}} do chão!", "pokemonCaught": "{{pokemonName}} foi capturado!", + "pokemonObtained": "Você recebeu um {{pokemonName}}!", + "pokemonBrokeFree": "Não!\nO Pokémon escapou!", + "pokemonFled": "{{pokemonName}} selvagem fugiu!", + "playerFled": "Você fugiu de {{pokemonName}}!", "addedAsAStarter": "{{pokemonName}} foi adicionado\naos seus iniciais!", "partyFull": "Sua equipe está cheia.\nSolte um Pokémon para ter espaço para {{pokemonName}}?", "pokemon": "Pokémon", @@ -49,6 +53,7 @@ "noPokeballTrainer": "Não se pode capturar\nPokémon dos outros!", "noPokeballMulti": "Não se pode lançar Poké Bolas\nquando há mais de um Pokémon!", "noPokeballStrong": "Este Pokémon é forte demais para ser capturado!\nÉ preciso enfraquecê-lo primeiro!", + "noPokeballMysteryEncounter": "Você não pode capturar\nesse Pokémon!", "noEscapeForce": "Uma força misteriosa\nte impede de fugir.", "noEscapeTrainer": "Não se pode fugir de\nbatalhas contra treinadores!", "noEscapePokemon": "O movimento {{moveName}} de {{pokemonName}} te impede de fugir!", @@ -96,5 +101,8 @@ "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}}!", + "mysteryEncounterAppeared": "O que é isso?", + "battlerTagsHealBlock": "{{pokemonNameWithAffix}} não pode restaurar seus PS!", + "battlerTagsHealBlockOnRemove": "{{pokemonNameWithAffix}} pode restaurar seus PS novamente!" } diff --git a/src/locales/pt_BR/berry.json b/src/locales/pt_BR/berry.json index 0ca5d3de286..7cd5e5c14dd 100644 --- a/src/locales/pt_BR/berry.json +++ b/src/locales/pt_BR/berry.json @@ -43,4 +43,4 @@ "name": "Fruta Leppa", "effect": "Restaura 10 PP de um movimento se seus PP acabarem" } -} \ No newline at end of file +} diff --git a/src/locales/pt_BR/bgm-name.json b/src/locales/pt_BR/bgm-name.json index 86d8ce3e790..4b95c0a1865 100644 --- a/src/locales/pt_BR/bgm-name.json +++ b/src/locales/pt_BR/bgm-name.json @@ -83,17 +83,20 @@ "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", "battle_plasma_boss": "B2W2 Batalha do Ghetsis", "battle_flare_boss": "XY Batalha do Lysandre", - "battle_aether_boss": "SM Batalha da Lusamine", + "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)", @@ -146,5 +149,10 @@ "encounter_youngster": "BW Encontro com Treinador (Jovem)", "heal": "BW Centro Pokémon", "menu": "PMD EoS Bem-vindo ao Mundo dos Pokémon!", - "title": "PMD EoS Menu Principal" + "title": "PMD EoS Menu Principal", + + "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" } diff --git a/src/locales/pt_BR/biome.json b/src/locales/pt_BR/biome.json index d10f22eb487..6235ebc4c3f 100644 --- a/src/locales/pt_BR/biome.json +++ b/src/locales/pt_BR/biome.json @@ -35,4 +35,4 @@ "ISLAND": "Ilha", "LABORATORY": "Laboratório", "END": "???" -} \ No newline at end of file +} diff --git a/src/locales/pt_BR/challenges.json b/src/locales/pt_BR/challenges.json index 9dc613651a6..b8249882033 100644 --- a/src/locales/pt_BR/challenges.json +++ b/src/locales/pt_BR/challenges.json @@ -34,4 +34,4 @@ "value.0": "Desligado", "value.1": "Ligado" } -} \ No newline at end of file +} diff --git a/src/locales/pt_BR/command-ui-handler.json b/src/locales/pt_BR/command-ui-handler.json index fcd8e7026b4..1e541e60b9a 100644 --- a/src/locales/pt_BR/command-ui-handler.json +++ b/src/locales/pt_BR/command-ui-handler.json @@ -4,4 +4,4 @@ "pokemon": "Pokémon", "run": "Fugir", "actionMessage": "O que {{pokemonName}}\ndeve fazer?" -} \ No newline at end of file +} diff --git a/src/locales/pt_BR/common.json b/src/locales/pt_BR/common.json index a5ec4381f26..9c04c3ec811 100644 --- a/src/locales/pt_BR/common.json +++ b/src/locales/pt_BR/common.json @@ -5,4 +5,4 @@ "commonShiny": "Comum", "rareShiny": "Raro", "epicShiny": "Épico" -} \ No newline at end of file +} diff --git a/src/locales/pt_BR/config.ts b/src/locales/pt_BR/config.ts index 9e8377149c1..fc4ae9000b4 100644 --- a/src/locales/pt_BR/config.ts +++ b/src/locales/pt_BR/config.ts @@ -1,59 +1,101 @@ -import common from "./common.json"; -import settings from "./settings.json"; -import ability from "./ability.json"; import abilityTriggers from "./ability-trigger.json"; +import ability from "./ability.json"; +import achv from "./achv.json"; import arenaFlyout from "./arena-flyout.json"; import arenaTag from "./arena-tag.json"; -import achv from "./achv.json"; -import battle from "./battle.json"; -import battleScene from "./battle-scene.json"; import battleInfo from "./battle-info.json"; import battleMessageUiHandler from "./battle-message-ui-handler.json"; +import battleScene from "./battle-scene.json"; +import battle from "./battle.json"; import battlerTags from "./battler-tags.json"; import berry from "./berry.json"; import bgmName from "./bgm-name.json"; import biome from "./biome.json"; import challenges from "./challenges.json"; import commandUiHandler from "./command-ui-handler.json"; -import dialogue from "./dialogue.json"; +import common from "./common.json"; +import doubleBattleDialogue from "./dialogue-double-battle.json"; import battleSpecDialogue from "./dialogue-final-boss.json"; import miscDialogue from "./dialogue-misc.json"; -import doubleBattleDialogue from "./dialogue-double-battle.json"; +import dialogue from "./dialogue.json"; import egg from "./egg.json"; import fightUiHandler from "./fight-ui-handler.json"; import filterBar from "./filter-bar.json"; import gameMode from "./game-mode.json"; import gameStatsUiHandler from "./game-stats-ui-handler.json"; import growth from "./growth.json"; -import menu from "./menu.json"; import menuUiHandler from "./menu-ui-handler.json"; -import modifier from "./modifier.json"; +import menu from "./menu.json"; +import modifierSelectUiHandler from "./modifier-select-ui-handler.json"; import modifierType from "./modifier-type.json"; +import modifier from "./modifier.json"; +import moveTriggers from "./move-trigger.json"; import move from "./move.json"; +import mysteryEncounterMessages from "./mystery-encounter-messages.json"; +import aTrainersTest from "./mystery-encounters/a-trainers-test-dialogue.json"; +import absoluteAvarice from "./mystery-encounters/absolute-avarice-dialogue.json"; +import offerYouCantRefuse from "./mystery-encounters/an-offer-you-cant-refuse-dialogue.json"; +import berriesAbound from "./mystery-encounters/berries-abound-dialogue.json"; +import bugTypeSuperfan from "./mystery-encounters/bug-type-superfan-dialogue.json"; +import clowningAround from "./mystery-encounters/clowning-around-dialogue.json"; +import dancingLessons from "./mystery-encounters/dancing-lessons-dialogue.json"; +import darkDeal from "./mystery-encounters/dark-deal-dialogue.json"; +import delibirdy from "./mystery-encounters/delibirdy-dialogue.json"; +import departmentStoreSale from "./mystery-encounters/department-store-sale-dialogue.json"; +import fieldTrip from "./mystery-encounters/field-trip-dialogue.json"; +import fieryFallout from "./mystery-encounters/fiery-fallout-dialogue.json"; +import fightOrFlight from "./mystery-encounters/fight-or-flight-dialogue.json"; +import funAndGames from "./mystery-encounters/fun-and-games-dialogue.json"; +import globalTradeSystem from "./mystery-encounters/global-trade-system-dialogue.json"; +import lostAtSea from "./mystery-encounters/lost-at-sea-dialogue.json"; +import mysteriousChallengers from "./mystery-encounters/mysterious-challengers-dialogue.json"; +import mysteriousChest from "./mystery-encounters/mysterious-chest-dialogue.json"; +import partTimer from "./mystery-encounters/part-timer-dialogue.json"; +import safariZone from "./mystery-encounters/safari-zone-dialogue.json"; +import shadyVitaminDealer from "./mystery-encounters/shady-vitamin-dealer-dialogue.json"; +import slumberingSnorlax from "./mystery-encounters/slumbering-snorlax-dialogue.json"; +import teleportingHijinks from "./mystery-encounters/teleporting-hijinks-dialogue.json"; +import pokemonSalesman from "./mystery-encounters/the-pokemon-salesman-dialogue.json"; +import theStrongStuff from "./mystery-encounters/the-strong-stuff-dialogue.json"; +import theWinstrateChallenge from "./mystery-encounters/the-winstrate-challenge-dialogue.json"; +import trainingSession from "./mystery-encounters/training-session-dialogue.json"; +import trashToTreasure from "./mystery-encounters/trash-to-treasure-dialogue.json"; +import uncommonBreed from "./mystery-encounters/uncommon-breed-dialogue.json"; +import weirdDream from "./mystery-encounters/weird-dream-dialogue.json"; +import expertPokemonBreeder from "./mystery-encounters/the-expert-pokemon-breeder-dialogue.json"; import nature from "./nature.json"; import partyUiHandler from "./party-ui-handler.json"; import pokeball from "./pokeball.json"; -import pokemon from "./pokemon.json"; -import pokemonForm from "./pokemon-form.json"; import battlePokemonForm from "./pokemon-form-battle.json"; -import pokemonInfo from "./pokemon-info.json"; +import pokemonForm from "./pokemon-form.json"; import pokemonInfoContainer from "./pokemon-info-container.json"; +import pokemonInfo from "./pokemon-info.json"; import pokemonSummary from "./pokemon-summary.json"; +import pokemon from "./pokemon.json"; +import runHistory from "./run-history.json"; import saveSlotSelectUiHandler from "./save-slot-select-ui-handler.json"; +import settings from "./settings.json"; import splashMessages from "./splash-messages.json"; import starterSelectUiHandler from "./starter-select-ui-handler.json"; import statusEffect from "./status-effect.json"; -import trainerTitles from "./trainer-titles.json"; +import terrain from "./terrain.json"; import trainerClasses from "./trainer-classes.json"; import trainerNames from "./trainer-names.json"; +import trainerTitles from "./trainer-titles.json"; import tutorial from "./tutorial.json"; import voucher from "./voucher.json"; import weather from "./weather.json"; -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"; +/** + * 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 ptBrConfig = { ability, abilityTriggers, @@ -110,4 +152,40 @@ export const ptBrConfig = { 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/pt_BR/dialogue-double-battle.json b/src/locales/pt_BR/dialogue-double-battle.json index 4d1c7d90c9b..e796a6ea2a2 100644 --- a/src/locales/pt_BR/dialogue-double-battle.json +++ b/src/locales/pt_BR/dialogue-double-battle.json @@ -80,4 +80,4 @@ "1": "Piers: Agora esse foi um ótimo show!\n$Marnie: Irmão..." } } -} \ No newline at end of file +} diff --git a/src/locales/pt_BR/dialogue-final-boss.json b/src/locales/pt_BR/dialogue-final-boss.json index 7009f00db5b..c1e492a41ef 100644 --- a/src/locales/pt_BR/dialogue-final-boss.json +++ b/src/locales/pt_BR/dialogue-final-boss.json @@ -3,4 +3,4 @@ "encounter_female": "Parece que a hora finalmente chegou novamente.\nVocê sabe por que veio aqui, não sabe?\n$Você foi atraída para cá, porque já esteve aqui antes.\nInúmeras vezes.\n$Embora talvez isso possa ser contado.\nPara ser preciso, este é de fato o seu {{cycleCount}}º ciclo.\n$A cada ciclo, sua mente retorna ao seu estado anterior.\nMesmo assim, de alguma forma, vestígios de seus antigos \"eus\" permanecem.\n$Até agora, você ainda não conseguiu, mas sinto uma presença diferente em você desta vez.\n\n$Você é a única aqui, embora pareça haver... outro.\n$Você finalmente vai se mostrar um desafio formidável para mim?\nO desafio que anseio há milênios?\n$Vamos começar.", "firstStageWin": "Entendo. A presença que senti era realmente real.\nParece que não preciso mais me segurar.\n$Não me decepcione.", "secondStageWin": "…Magnífico." -} \ No newline at end of file +} diff --git a/src/locales/pt_BR/dialogue.json b/src/locales/pt_BR/dialogue.json index 2f39442ee5a..e3f9d61738f 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,141 @@ "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ê." + } + }, + "stat_trainer_buck": { + "encounter": { + "1": "...Estou te falando agora. Eu sou muito durão. Finge surpresa!", + "2": "Posso sentir meus Pokémon tremendo dentro de suas Pokébolas!" + }, + "victory": { + "1": "Hehehe!\nVocê é uma máquina!", + "2": "Hehehe!\nVocê é uma máquina!" + }, + "defeat": { + "1": "Uau! Acho que vocês estão sem gás.", + "2": "Uau! Acho que vocês estão sem gás." + } + }, + "stat_trainer_cheryl": { + "encounter": { + "1": "Meus Pokémon estavam ansiosos por uma batalha.", + "2": "Devo avisá-lo, meus Pokémon podem ser bastante agitados.", + "2_female": "Devo avisá-la, meus Pokémon podem ser bastante agitados." + }, + "victory": { + "1": "Acertar o equilíbrio certo entre ataque e defesa... Não é fácil de fazer.", + "2": "Acertar o equilíbrio certo entre ataque e defesa... Não é fácil de fazer." + }, + "defeat": { + "1": "Seus Pokémon precisam de cura?", + "2": "Seus Pokémon precisam de cura?" + } + }, + "stat_trainer_marley": { + "encounter": { + "1": "... Tá.\nVou dar o meu melhor.", + "2": "... Tá.\nEu... não vou perder...!" + }, + "victory": { + "1": "... Awww.", + "2": "... Awww." + }, + "defeat": { + "1": "... Adeus.", + "2": "... Adeus." + } + }, + "stat_trainer_mira": { + "encounter": { + "1": "Você ficará surpreso com a Mira!", + "1_female": "Você ficará surpresa com a Mira!", + "2": "Mira vai te mostrar que ela não se perde mais!" + }, + "victory": { + "1": "Mira se pergunta se conseguirá ir longe nessa terra.", + "2": "Mira se pergunta se conseguirá ir longe nessa terra." + }, + "defeat": { + "1": "Mira sabia que venceria!", + "2": "Mira sabia que venceria!" + } + }, + "stat_trainer_riley": { + "encounter": { + "1": "Lutar é nossa forma de saudação!", + "2": "Estamos fazendo de tudo para derrubar seus Pokémon." + }, + "victory": { + "1": "Às vezes, lutamos e, às vezes, nos unimos...$É ótimo como Treinadores podem interagir.", + "2": "Às vezes, lutamos e, às vezes, nos unimos...$É ótimo como Treinadores podem interagir." + }, + "defeat": { + "1": "Você fez uma bela exibição.\nMelhor sorte na próxima vez.", + "2": "Você fez uma bela exibição.\nMelhor sorte na próxima vez." + } + }, + "winstrates_victor": { + "encounter": { + "1": "Esse é o espírito! Gosto de você!" + }, + "victory": { + "1": "A-ha! Você é mais forte do que eu pensei!" + } + }, + "winstrates_victoria": { + "encounter": { + "1": "Meu Deus! Você não é jovem?$Deve ser um grande Treinador para derrotar meu marido.$Acho que agora é a minha vez de lutar!", + "1_female": "Meu Deus! Você não é jovem?$Deve ser uma grande Treinadora para derrotar meu marido.$Acho que agora é a minha vez de lutar!" + }, + "victory": { + "1": "Uwah! Quão forte você é?!" + } + }, + "winstrates_vivi": { + "encounter": { + "1": "Você é mais forte que a Mamãe? Uau!$Mas eu também sou forte!\nDe verdade! Falando sério!" + }, + "victory": { + "1": "Ahn? Eu perdi mesmo?\nSnif... Vovóóó!" + } + }, + "winstrates_vicky": { + "encounter": { + "1": "Como ousa fazer minha preciosa\nneta chorar!$Vejo que preciso lhe dar uma lição.\nPrepare-se para sentir o ferrão da derrota!" + }, + "victory": { + "1": "Uau! Tão forte!\nMinha neta não estava mentindo." + } + }, + "winstrates_vito": { + "encounter": { + "1": "Treinei com toda a minha família,\ncada um de nós!$Não vou perder para para ninguém!" + }, + "victory": { + "1": "Eu era melhor do que todos em minha família.\nEu nunca perdi antes..." + } + }, "brock": { "encounter": { "1": "Minha especialidade em Pokémon do tipo Pedra vai te derrubar! Vamos lá!", diff --git a/src/locales/pt_BR/egg.json b/src/locales/pt_BR/egg.json index a14217858b5..cbe31d27ed4 100644 --- a/src/locales/pt_BR/egg.json +++ b/src/locales/pt_BR/egg.json @@ -11,6 +11,7 @@ "gachaTypeLegendary": "Chance de Lendário Aumentada", "gachaTypeMove": "Chance de Movimento de Ovo Raro Aumentada", "gachaTypeShiny": "Chance de Shiny Aumentada", + "eventType": "Evento Misterioso", "selectMachine": "Escolha uma máquina.", "notEnoughVouchers": "Você não tem vouchers suficientes!", "tooManyEggs": "Você já tem muitos ovos!", @@ -23,4 +24,4 @@ "moveUPGacha": "Movimento\nde Ovo Bônus!", "shinyUPGacha": "Shiny Bônus!", "legendaryUPGacha": "Bônus!" -} \ No newline at end of file +} diff --git a/src/locales/pt_BR/filter-bar.json b/src/locales/pt_BR/filter-bar.json index 05898796d9f..45b17c33077 100644 --- a/src/locales/pt_BR/filter-bar.json +++ b/src/locales/pt_BR/filter-bar.json @@ -35,4 +35,4 @@ "sortByCandies": "# Doces", "sortByIVs": "IVs", "sortByName": "Nome" -} \ No newline at end of file +} diff --git a/src/locales/pt_BR/game-mode.json b/src/locales/pt_BR/game-mode.json index 9aa0f286959..674552bd923 100644 --- a/src/locales/pt_BR/game-mode.json +++ b/src/locales/pt_BR/game-mode.json @@ -5,4 +5,4 @@ "dailyRun": "Desafio Diário", "unknown": "Desconhecido", "challenge": "Desafio" -} \ No newline at end of file +} diff --git a/src/locales/pt_BR/game-stats-ui-handler.json b/src/locales/pt_BR/game-stats-ui-handler.json index eb36f484cc3..b473aa09c2f 100644 --- a/src/locales/pt_BR/game-stats-ui-handler.json +++ b/src/locales/pt_BR/game-stats-ui-handler.json @@ -39,4 +39,4 @@ "epicEggsPulled": "Ovos Épicos Ganhos", "legendaryEggsPulled": "Ovos Lendários Ganhos", "manaphyEggsPulled": "Ovos de Manaphy Ganhos" -} \ No newline at end of file +} diff --git a/src/locales/pt_BR/growth.json b/src/locales/pt_BR/growth.json index d9b11dc23c3..272856a006c 100644 --- a/src/locales/pt_BR/growth.json +++ b/src/locales/pt_BR/growth.json @@ -5,4 +5,4 @@ "Medium_Slow": "Meio Lento", "Slow": "Lento", "Fluctuating": "Muito Lento" -} \ No newline at end of file +} diff --git a/src/locales/pt_BR/menu.json b/src/locales/pt_BR/menu.json index 415796f91ed..97acc63400f 100644 --- a/src/locales/pt_BR/menu.json +++ b/src/locales/pt_BR/menu.json @@ -52,4 +52,4 @@ "rename": "Renomear", "nickname": "Apelido", "errorServerDown": "Opa! Não foi possível conectar-se ao servidor.\n\nVocê pode deixar essa janela aberta,\npois o jogo irá se reconectar automaticamente." -} \ No newline at end of file +} diff --git a/src/locales/pt_BR/modifier-select-ui-handler.json b/src/locales/pt_BR/modifier-select-ui-handler.json index 4aef7f145ae..e741eaa5f21 100644 --- a/src/locales/pt_BR/modifier-select-ui-handler.json +++ b/src/locales/pt_BR/modifier-select-ui-handler.json @@ -8,5 +8,7 @@ "lockRaritiesDesc": "Trava a raridade dos itens na atualização (afeta o custo da atualização).", "checkTeamDesc": "Cheque seu time ou use um item de mudança de forma.", "rerollCost": "₽{{formattedMoney}}", - "itemCost": "₽{{formattedMoney}}" -} \ No newline at end of file + "itemCost": "₽{{formattedMoney}}", + "continueNextWaveButton": "Continuar", + "continueNextWaveDescription": "Continuar para a próxima onda" +} diff --git a/src/locales/pt_BR/modifier-type.json b/src/locales/pt_BR/modifier-type.json index f20a4ef3d0f..ae7a2cba93e 100644 --- a/src/locales/pt_BR/modifier-type.json +++ b/src/locales/pt_BR/modifier-type.json @@ -68,6 +68,20 @@ "BaseStatBoosterModifierType": { "description": "Aumenta o atributo base de {{stat}} em 10%. Quanto maior os IVs, maior o limite de aumento." }, + "PokemonBaseStatTotalModifierType": { + "name": "Suco Shuckle", + "description": "{{increaseDecrease}} todos os atributos base de quem o segurar por {{statValue}}. Você foi {{blessCurse}} por Shuckle.", + "extra": { + "increase": "Aumenta", + "decrease": "Diminui", + "blessed": "abençoado", + "cursed": "amaldiçoado" + } + }, + "PokemonBaseStatFlatModifierType": { + "name": "Doce Antigo", + "description": "Aumenta os atributos base de {{stats}} de quem o segurar por {{statValue}}. Encontrado depois de um sonho estranho." + }, "AllPokemonFullHpRestoreModifierType": { "description": "Restaura totalmente os PS de todos os Pokémon." }, @@ -252,11 +266,11 @@ "name": "Lentes de Mira", "description": "Estas lentes facilitam o foco em pontos fracos. Aumenta a chance de acerto crítico de quem a segurar." }, - "DIRE_HIT": { - "name": "Direto", - "extra": { - "raises": "Chance de Acerto Crítico" - } + "DIRE_HIT": { + "name": "Direto", + "extra": { + "raises": "Chance de Acerto Crítico" + } }, "LEEK": { "name": "Alho-poró", @@ -401,6 +415,24 @@ "ENEMY_FUSED_CHANCE": { "name": "Token de Fusão", "description": "Adiciona uma chance de 1% de que um Pokémon selvagem seja uma fusão." + }, + + "MYSTERY_ENCOUNTER_SHUCKLE_JUICE": { "name": "Suco Shuckle" }, + "MYSTERY_ENCOUNTER_BLACK_SLUDGE": { + "name": "Lodo Escuro", + "description": "O fedor é tão forte que as lojas só venderão itens com um grande aumento de custo." + }, + "MYSTERY_ENCOUNTER_MACHO_BRACE": { + "name": "Pulseira Macho", + "description": "Derrotar um Pokémon concede a quem segura uma pilha de Pulseira Macho. Cada pilha aumenta ligeiramente os atributos, com um bônus extra no máximo de pilhas." + }, + "MYSTERY_ENCOUNTER_OLD_GATEAU": { + "name": "Doce Antigo", + "description_pt": "Aumenta os atributos de {{stats}} de quem o segurar por {{statValue}}." + }, + "MYSTERY_ENCOUNTER_GOLDEN_BUG_NET": { + "name": "Rede de Insetos Dourada", + "description": "Concede ao dono sorte para encontrar Pokémon do tipo Inseto com mais frequência. Tem um peso estranho." } }, "SpeciesBoosterItem": { diff --git a/src/locales/pt_BR/modifier.json b/src/locales/pt_BR/modifier.json index 38622de579e..6d1a1222f73 100644 --- a/src/locales/pt_BR/modifier.json +++ b/src/locales/pt_BR/modifier.json @@ -9,4 +9,4 @@ "contactHeldItemTransferApply": "{{itemName}} de {{pokemonNameWithAffix}} foi pego(a)\npela {{typeName}} de {{pokemonName}}!", "enemyTurnHealApply": "{{pokemonNameWithAffix}}\nrestaurou um pouco de seus PS!", "bypassSpeedChanceApply": "{{pokemonName}} se move mais rápido que o normal graças à sua {{itemName}}!" -} \ No newline at end of file +} diff --git a/src/locales/pt_BR/move-trigger.json b/src/locales/pt_BR/move-trigger.json index 4549f83cdf1..307364e1b55 100644 --- a/src/locales/pt_BR/move-trigger.json +++ b/src/locales/pt_BR/move-trigger.json @@ -4,11 +4,11 @@ "absorbedElectricity": "{{pokemonName}} absorveu eletricidade!", "switchedStatChanges": "{{pokemonName}} trocou as mudanças de atributo com o alvo!", "goingAllOutForAttack": "{{pokemonName}} está arriscando tudo nesse ataque!", - "regainedHealth": "{{pokemonName}} recuperou/nsaúde!", - "keptGoingAndCrashed": "{{pokemonName}} errou o alvo/ne se arrebentou!", + "regainedHealth": "{{pokemonName}} recuperou\nsaúde!", + "keptGoingAndCrashed": "{{pokemonName}} errou o alvo\ne se arrebentou!", "fled": "{{pokemonName}} fugiu!", "cannotBeSwitchedOut": "{{pokemonName}} não pode ser trocado!", - "swappedAbilitiesWithTarget": "{{pokemonName}} trocou/nde habilidades com o alvo!", + "swappedAbilitiesWithTarget": "{{pokemonName}} trocou\nde habilidades com o alvo!", "coinsScatteredEverywhere": "Moedas foram espalhadas por toda parte!", "attackedByItem": "{{pokemonName}} está prestes a ser atacado por {{itemName}}!", "whippedUpAWhirlwind": "{{pokemonName}} criou\num redemoinho de vento!", @@ -18,23 +18,23 @@ "loweredItsHead": "{{pokemonName}} abaixou sua cabeça!", "isGlowing": "{{pokemonName}} ficou envolto em uma luz forte!", "bellChimed": "Um sino tocou!", - "foresawAnAttack": "{{pokemonName}} previu/num ataque!", + "foresawAnAttack": "{{pokemonName}} previu\num ataque!", "isTighteningFocus": "{{pokemonName}} está\naumentando seu foco!", - "hidUnderwater": "{{pokemonName}} se escondeu/nembaixo d'água!", + "hidUnderwater": "{{pokemonName}} se escondeu\nembaixo d'água!", "soothingAromaWaftedThroughArea": "Um aroma suave se espalhou pelo ambiente!", "sprangUp": "{{pokemonName}} se levantou!", "choseDoomDesireAsDestiny": "{{pokemonName}} escolheu\no Desejo da Perdição como seu destino!", "vanishedInstantly": "{{pokemonName}} desapareceu\nde repente!", "tookTargetIntoSky": "{{pokemonName}} levou {{targetName}}\npara o céu!", - "becameCloakedInFreezingLight": "{{pokemonName}} ficou envolto/nem uma luz congelante!", - "becameCloakedInFreezingAir": "{{pokemonName}} ficou envolto/nem ar congelante!", + "becameCloakedInFreezingLight": "{{pokemonName}} ficou envolto\nem uma luz congelante!", + "becameCloakedInFreezingAir": "{{pokemonName}} ficou envolto\nem ar congelante!", "isChargingPower": "{{pokemonName}} está absorvendo energia!", "burnedItselfOut": "{{pokemonName}} apagou seu próprio fogo!", "startedHeatingUpBeak": "{{pokemonName}} começou\na esquentar seu bico!", "setUpShellTrap": "{{pokemonName}} armou uma armadilha de carapaça!", "isOverflowingWithSpacePower": "{{pokemonName}} está sobrecarregado\ncom energia espacial!", "usedUpAllElectricity": "{{pokemonName}} usou toda a sua eletricidade!", - "stoleItem": "{{pokemonName}} roubou/no(a) {{itemName}} de {{targetName}}!", + "stoleItem": "{{pokemonName}} roubou\no(a) {{itemName}} de {{targetName}}!", "incineratedItem": "{{pokemonName}} incinerou\na {{itemName}} de {{targetName}}!", "knockedOffItem": "{{pokemonName}} derrubou\no(a) {{itemName}} de {{targetName}}!", "tookMoveAttack": "{{pokemonName}} pegou\no movimento {{moveName}}!", @@ -61,6 +61,7 @@ "suppressAbilities": "A habilidade de {{pokemonName}}\nfoi suprimida!", "revivalBlessing": "{{pokemonName}} foi reanimado!", "swapArenaTags": "{{pokemonName}} trocou os efeitos de batalha que afetam cada lado do campo!", + "chillyReception": "{{pokemonName}} está prestes a contar uma piada gelada!", "exposedMove": "{{pokemonName}} identificou\n{{targetPokemonName}}!", "safeguard": "{{targetName}} está protegido por Safeguard!", "afterYou": "{{pokemonName}} aceitou a gentil oferta!" 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/mystery-encounter-messages.json b/src/locales/pt_BR/mystery-encounter-messages.json new file mode 100644 index 00000000000..2c8b3738949 --- /dev/null +++ b/src/locales/pt_BR/mystery-encounter-messages.json @@ -0,0 +1,7 @@ +{ + "paid_money": "Você pagou ₽{{amount, number}}.", + "receive_money": "Você recebeu ₽{{amount, number}}!", + "affects_pokedex": "Afeta Dados da Pokédex", + "cancel_option": "Voltar para a seleção de opções de encontro.", + "view_party_button": "Ver Equipe" +} diff --git a/src/locales/pt_BR/mystery-encounters/a-trainers-test-dialogue.json b/src/locales/pt_BR/mystery-encounters/a-trainers-test-dialogue.json new file mode 100644 index 00000000000..f80e7771cc1 --- /dev/null +++ b/src/locales/pt_BR/mystery-encounters/a-trainers-test-dialogue.json @@ -0,0 +1,47 @@ +{ + "intro": "Um treinador extremamente forte se aproxima de você...", + "buck": { + "intro_dialogue": "Ei, treinador! Meu nome é Buck. Tenho uma proposta super incrível para um treinador tão forte quanto você! Estou carregando dois Ovos de Pokémon raros comigo, mas gostaria que alguém cuidasse de um deles. Se você conseguir provar sua força como treinador para mim, te darei o ovo mais raro!", + "accept": "Uhul, estou ficando empolgado!", + "decline": "Droga, parece que seu time\nnão está em plena forma. Aqui, deixe-me ajudar com isso." + }, + "cheryl": { + "intro_dialogue": "Olá, meu nome é Cheryl. Tenho um pedido particularmente interessante para um treinador forte como você. Estou carregando dois Ovos de Pokémon raros comigo, mas gostaria que alguém cuidasse de um deles. Se você conseguir provar sua força como treinador para mim, te darei o Ovo mais raro!", + "accept": "Espero que esteja preparado!", + "decline": "Eu entendo, parece que seu time não\nestá nas melhores condições no momento. Aqui, deixe-me ajudar com isso." + }, + "marley": { + "intro_dialogue": "...@d{64} Eu sou a Marley.$Tenho uma oferta para você...$Estou carregando dois Ovos de Pokémon comigo,\nmas gostaria que alguém cuidasse de um deles.$Se você for mais forte que eu,\nte darei o Ovo mais raro.", + "accept": "... Entendo.", + "decline": "... Entendo.$Seus Pokémon parecem machucados...\nDeixe-me ajudar." + }, + "mira": { + "intro_dialogue": "Oi! Eu sou a Mira!$A Mira tem um pedido\npara um treinador forte como você!$A Mira tem dois Ovos de Pokémon raros,\nmas a Mira quer que alguém cuide de um!$Se você mostrar à Mira que é forte,\na Mira te dará o Ovo mais raro!", + "accept": "Você vai lutar contra a Mira?\nYay!", + "decline": "Aww, sem batalha?\nTudo bem!$Aqui, a Mira vai curar seu time!" + }, + "riley": { + "intro_dialogue": "Eu sou o Riley.$Tenho uma proposta estranha\npara um treinador forte como você.$Estou carregando dois Ovos de Pokémon raros comigo,\nmas gostaria de dar um a outro treinador.$Se você puder provar sua força para mim,\neu lhe darei o Ovo mais raro!", + "accept": "Esse olhar que você tem...\nVamos fazer isso.", + "decline": "Entendo, seu time parece exausto.$Deixe-me ajudá-lo com isso." + }, + "title": "O Teste de um Treinador", + "description": "Parece que este treinador está disposto a lhe dar um Ovo independentemente da sua decisão. No entanto, se você conseguir derrotar este treinador forte, receberá um Ovo muito mais raro.", + "query": "O que você vai fazer?", + "option": { + "1": { + "label": "Aceitar o Desafio", + "tooltip": "(-) Batalha Difícil\n(+) Ganhe um @[TOOLTIP_TITLE]{Ovo Muito Raro}" + }, + "2": { + "label": "Recusar o Desafio", + "tooltip": "(+) Curar Todo o Time\n(+) Ganhe um @[TOOLTIP_TITLE]{Ovo}" + } + }, + "eggTypes": { + "rare": "um Ovo Raro", + "epic": "um Ovo Épico", + "legendary": "um Ovo Lendário" + }, + "outro": "{{statTrainerName}} lhe deu {{eggType}}!" +} diff --git a/src/locales/pt_BR/mystery-encounters/absolute-avarice-dialogue.json b/src/locales/pt_BR/mystery-encounters/absolute-avarice-dialogue.json new file mode 100644 index 00000000000..85123330f4b --- /dev/null +++ b/src/locales/pt_BR/mystery-encounters/absolute-avarice-dialogue.json @@ -0,0 +1,25 @@ +{ + "intro": "Um {{greedentName}} te embosca\ne rouba as Frutas do seu time!", + "title": "Ganância Absoluta", + "description": "Um {{greedentName}} te pegou completamente desprevenido e agora todas as suas Frutas se foram!\n\nA {{greedentName}} parece prestes a comê-las quando para para te olhar, interessado.", + "query": "O que você vai fazer?", + "option": { + "1": { + "label": "Batalhar", + "tooltip": "(-) Batalha Difícil\n(+) Recompensas do Tesouro de Frutas", + "selected": "Um {{greedentName}} enche suas bochechas\ne se prepara para a batalha!", + "boss_enraged": "O amor feroz de {{greedentName}} por comida o deixou enfurecido!", + "food_stash": "Parece que o {{greedentName}} estava guardando um enorme estoque de comida!$@s{item_fanfare}Cada Pokémon do seu time ganha um {{foodReward}}!" + }, + "2": { + "label": "Resolver com Ele", + "tooltip": "(+) Recuperar Algumas Frutas Perdidas", + "selected": "Seu apelo toca o coração de {{greedentName}}.$Ele não devolve todas as suas Frutas, mas joga algumas na sua direção." + }, + "3": { + "label": "Deixá-lo Ficar com a Comida", + "tooltip": "(-) Perder Todas as Frutas\n(?) O {{greedentName}} Vai Gostar de Você", + "selected": "O {{greedentName}} devora todo o\nestoque de Frutas num piscar de olhos!$Bate na barriga satisfeito\ne olha para você agradecido.$Talvez você possa alimentá-lo\ncom mais Frutas em sua aventura...$@s{level_up_fanfare}O {{greedentName}} quer se juntar ao seu time!" + } + } +} diff --git a/src/locales/pt_BR/mystery-encounters/an-offer-you-cant-refuse-dialogue.json b/src/locales/pt_BR/mystery-encounters/an-offer-you-cant-refuse-dialogue.json new file mode 100644 index 00000000000..059664b9c07 --- /dev/null +++ b/src/locales/pt_BR/mystery-encounters/an-offer-you-cant-refuse-dialogue.json @@ -0,0 +1,26 @@ +{ + "intro": "Um garoto de aparência rica te para.", + "speaker": "Garoto Rico", + "intro_dialogue": "Bom dia para você.$Não pude deixar de notar que seu\n{{strongestPokemon}} parece absolutamente divino!$Sempre quis ter um Pokémon assim!$Eu te pagaria generosamente,\ne também te daria este velho amuleto!", + "title": "Uma Oferta Irrecusável", + "description": "Você está sendo oferecido um @[TOOLTIP_TITLE]{Amuleto Brilhante} e {{price, money}} pelo seu {{strongestPokemon}}!\n\nÉ um negócio extremamente bom, mas será que você pode realmente se desfazer de um membro tão forte do seu time?", + "query": "O que você vai fazer?", + "option": { + "1": { + "label": "Aceitar o Acordo", + "tooltip": "(-) Perder {{strongestPokemon}}\n(+) Ganhar um @[TOOLTIP_TITLE]{Amuleto Brilhante}\n(+) Ganhar {{price, money}}", + "selected": "Maravilhoso!@d{32} Venha comigo, {{strongestPokemon}}!$É hora de mostrá-lo para todos no clube de iates!$Eles ficarão com tanta inveja!" + }, + "2": { + "label": "Extorquir o Garoto", + "tooltip": "(+) {{option2PrimaryName}} usa {{moveOrAbility}}\n(+) Ganhar {{price, money}}", + "tooltip_disabled": "Seus Pokémon precisam ter certos golpes ou habilidades para escolher isso", + "selected": "Minha nossa, estamos sendo roubados, {{liepardName}}!$Você vai ouvir falar dos meus advogados por isso!" + }, + "3": { + "label": "Sair", + "tooltip": "(-) Sem Recompensas", + "selected": "Que dia horrível...$Bem, vamos voltar ao clube de iates então, {{liepardName}}." + } + } +} diff --git a/src/locales/pt_BR/mystery-encounters/berries-abound-dialogue.json b/src/locales/pt_BR/mystery-encounters/berries-abound-dialogue.json new file mode 100644 index 00000000000..5a3a30d0bd3 --- /dev/null +++ b/src/locales/pt_BR/mystery-encounters/berries-abound-dialogue.json @@ -0,0 +1,26 @@ +{ + "intro": "Há um enorme arbusto de frutas\nperto daquele Pokémon!", + "title": "Frutas em Abundância", + "description": "Parece que há um Pokémon forte guardando um arbusto de frutas. Batalhar é a abordagem mais direta, mas ele parece ser forte. Talvez um Pokémon rápido consiga pegar algumas frutas sem ser pego?", + "query": "O que você vai fazer?", + "berries": "Frutas!", + "option": { + "1": { + "label": "Batalhar com o Pokémon", + "tooltip": "(-) Batalha Difícil\n(+) Ganhar Frutas", + "selected": "Você se aproxima do\nPokémon sem medo." + }, + "2": { + "label": "Correr para o Arbusto", + "tooltip": "(-) {{fastestPokemon}} Usa sua Velocidade\n(+) Ganhar Frutas", + "selected": "Seu {{fastestPokemon}} corre em direção ao arbusto de frutas!$Ele consegue pegar {{numBerries}} antes que o {{enemyPokemon}} possa reagir!$Você rapidamente recua com seu novo prêmio.", + "selected_bad": "Seu {{fastestPokemon}} corre em direção ao arbusto de frutas!$Oh não! O {{enemyPokemon}} foi mais rápido e bloqueou o caminho!", + "boss_enraged": "O Pokémon oponente, {{enemyPokemon}}, ficou furioso!" + }, + "3": { + "label": "Sair", + "tooltip": "(-) Sem Recompensas", + "selected": "Você deixa o Pokémon forte\ncom seu prêmio e segue em frente." + } + } +} diff --git a/src/locales/pt_BR/mystery-encounters/bug-type-superfan-dialogue.json b/src/locales/pt_BR/mystery-encounters/bug-type-superfan-dialogue.json new file mode 100644 index 00000000000..62244eee918 --- /dev/null +++ b/src/locales/pt_BR/mystery-encounters/bug-type-superfan-dialogue.json @@ -0,0 +1,40 @@ +{ + "intro": "Um treinador incomum com todo tipo de parafernália de Pokémon Inseto bloqueia seu caminho!", + "intro_dialogue": "Ei, treinador! Estou em uma missão para encontrar o Pokémon Inseto mais raro que existe!$Você deve amar Pokémon Inseto também, certo?\nTodo mundo ama Pokémon Inseto!", + "title": "O Superfã dos Pokémon Inseto", + "speaker": "Superfã de Pokémon Inseto", + "description": "O treinador tagarela, nem sequer esperando uma resposta...\n\nParece que a única maneira de sair dessa situação é chamar a atenção do treinador!", + "query": "O que você vai fazer?", + "option": { + "1": { + "label": "Oferecer uma Batalha", + "tooltip": "(-) Batalha Desafiadora\n(+) Ensinar um Movimento de Tipo Inseto a um Pokémon", + "selected": "Um desafio, hein?\nMeus insetos estão mais do que prontos para você!" + }, + "2": { + "label": "Mostrar Seus Tipos Inseto", + "tooltip": "(+) Receber um Item de Presente", + "disabled_tooltip": "Você precisa de pelo menos 1 Pokémon do Tipo Inseto no seu time para selecionar isso.", + "selected": "Você mostra todos os seus Pokémon do Tipo Inseto ao treinador...", + "selected_0_to_1": "Hã? Você só tem {{numBugTypes}}...$Acho que estou perdendo meu tempo com alguém como você...", + "selected_2_to_3": "Ei, você tem {{numBugTypes}}!\nNada mal.$Aqui, isso pode te ajudar na sua jornada para capturar mais!", + "selected_4_to_5": "O quê? Você tem {{numBugTypes}}?\nLegal!$Você ainda não está no meu nível, mas consigo ver uma sombra de mim mesmo em você!\n$Leve isso, meu jovem aprendiz!", + "selected_6": "Uau! {{numBugTypes}}!\n$Você deve amar Pokémon do Tipo Inseto quase tanto quanto eu!$Aqui, leve isso como um símbolo de nossa camaradagem!" + }, + "3": { + "label": "Presentear com um Item de Inseto", + "tooltip": "(-) Dar ao treinador um {{requiredBugItems}}\n(+) Receber um Item de Presente", + "disabled_tooltip": "Você precisa ter um {{requiredBugItems}} para selecionar isso.", + "select_prompt": "Selecione um item para dar.", + "invalid_selection": "Esse Pokémon não possui esse tipo de item.", + "selected": "Você entrega ao treinador um {{selectedItem}}.", + "selected_dialogue": "Uau! Um {{selectedItem}}, para mim?\nVocê não é tão ruim, garoto!$Como símbolo do meu apreço,\nquero que você receba este presente especial!$Ele foi passado por toda a minha família, e agora eu quero que você o tenha!" + } + }, + "battle_won": "Seu conhecimento e habilidade foram perfeitos para explorar nossas fraquezas!$Em troca pela valiosa lição,\ndeixe-me ensinar a um dos seus Pokémon um Movimento de Tipo Inseto!", + "teach_move_prompt": "Selecione um movimento para ensinar a um Pokémon.", + "confirm_no_teach": "Tem certeza de que não quer aprender um desses ótimos movimentos?", + "outro": "Vejo grandes Pokémon Inseto no seu futuro!\nQue nossos caminhos se cruzem novamente!$Saia zunindo!", + "numBugTypes_one": "{{count}} Tipo Inseto", + "numBugTypes_other": "{{count}} Tipos Inseto" +} diff --git a/src/locales/pt_BR/mystery-encounters/clowning-around-dialogue.json b/src/locales/pt_BR/mystery-encounters/clowning-around-dialogue.json new file mode 100644 index 00000000000..83bf85aa0f7 --- /dev/null +++ b/src/locales/pt_BR/mystery-encounters/clowning-around-dialogue.json @@ -0,0 +1,34 @@ +{ + "intro": "É...@d{64} um palhaço?", + "speaker": "Palhaço", + "intro_dialogue": "Pateta trapalhão, prepare-se para uma batalha brilhante!\nVocê será derrotado por este artista de rua brigão!", + "title": "Brincando de Palhaço", + "description": "Algo está estranho nesse encontro. O palhaço parece ansioso para provocá-lo a entrar em uma batalha, mas com que objetivo?\n\nO {{blacephalonName}} é especialmente estranho, como se tivesse @[TOOLTIP_TITLE]{tipos e habilidade esquisitos.}", + "query": "O que você vai fazer?", + "option": { + "1": { + "label": "Batalhar com o Palhaço", + "tooltip": "(-) Batalha Estranha\n(?) Afeta Habilidades dos Pokémon", + "selected": "Seus Pokémon patéticos estão preparados para uma performance patética!", + "apply_ability_dialogue": "Um espetáculo sensacional!\nSeu conhecimento garante uma habilidade sensacional como recompensa!", + "apply_ability_message": "O palhaço está oferecendo para trocar permanentemente a habilidade de um dos seus Pokémon por {{ability}}!", + "ability_prompt": "Você gostaria de ensinar permanentemente a habilidade {{ability}} a um Pokémon?", + "ability_gained": "@s{level_up_fanfare}{{chosenPokemon}} adquiriu a habilidade {{ability}}!" + }, + "2": { + "label": "Não Se Provocar", + "tooltip": "(-) Irrita o Palhaço\n(?) Afeta os Itens dos Pokémon", + "selected": "Covarde dissimulado, você nega um duelo delicioso?\nSinta minha fúria!", + "selected_2": "O {{blacephalonName}} do palhaço usa Trick!\nTodos os itens de {{switchPokemon}} foram trocados aleatoriamente!", + "selected_3": "Tolo frustrado, caia na minha enganação impecável!" + }, + "3": { + "label": "Retrucar os Insultos", + "tooltip": "(-) Irrita o Palhaço\n(?) Afeta os Tipos dos Pokémon", + "selected": "Covarde dissimulado, você nega um duelo delicioso?\nSinta minha fúria!", + "selected_2": "O {{blacephalonName}} do palhaço usa um movimento estranho!\nTodos os tipos da sua equipe foram trocados aleatoriamente!", + "selected_3": "Tolo frustrado, caia na minha enganação impecável!" + } + }, + "outro": "O palhaço e seus companheiros\ndesaparecem em uma nuvem de fumaça." +} diff --git a/src/locales/pt_BR/mystery-encounters/dancing-lessons-dialogue.json b/src/locales/pt_BR/mystery-encounters/dancing-lessons-dialogue.json new file mode 100644 index 00000000000..5769de91ea5 --- /dev/null +++ b/src/locales/pt_BR/mystery-encounters/dancing-lessons-dialogue.json @@ -0,0 +1,27 @@ +{ + "intro": "Um {{oricorioName}} dança tristemente sozinho, sem um parceiro.", + "title": "Aulas de Dança", + "description": "O {{oricorioName}} não parece agressivo, na verdade parece triste.\n\nTalvez ele só queira alguém para dançar junto...", + "query": "O que você vai fazer?", + "option": { + "1": { + "label": "Batalhar", + "tooltip": "(-) Batalha Difícil\n(+) Ganhar um Bastão", + "selected": "O {{oricorioName}} está angustiado e se move para se defender!", + "boss_enraged": "O medo do {{oricorioName}} aumentou suas atributos!" + }, + "2": { + "label": "Aprender Sua Dança", + "tooltip": "(+) Ensinar Revelation Dance a um Pokémon", + "selected": "Você observa o {{oricorioName}} de perto enquanto ele realiza sua dança...$@s{level_up_fanfare}Seu {{selectedPokemon}} aprendeu com o {{oricorioName}}!" + }, + "3": { + "label": "Mostrar uma Dança", + "tooltip": "(-) Ensinar um Movimento de Dança ao {{oricorioName}}\n(+) O {{oricorioName}} Vai Gostar de Você", + "disabled_tooltip": "Seus Pokémon precisam conhecer um movimento de Dança para isso.", + "select_prompt": "Selecione um movimento de tipo Dança para usar.", + "selected": "O {{oricorioName}} observa fascinado enquanto\n{{selectedPokemon}} exibe {{selectedMove}}!$Ele adora a apresentação!$@s{level_up_fanfare}O {{oricorioName}} quer se juntar ao seu time!" + } + }, + "invalid_selection": "Este Pokémon não conhece um movimento de Dança" +} diff --git a/src/locales/pt_BR/mystery-encounters/dark-deal-dialogue.json b/src/locales/pt_BR/mystery-encounters/dark-deal-dialogue.json new file mode 100644 index 00000000000..5f0800d15d2 --- /dev/null +++ b/src/locales/pt_BR/mystery-encounters/dark-deal-dialogue.json @@ -0,0 +1,22 @@ +{ + "intro": "Um homem estranho com um casaco esfarrapado\nbloqueia seu caminho...", + "speaker": "Cara Sombrio", + "intro_dialogue": "Ei, você!$Tenho trabalhado em um novo dispositivo\npara trazer à tona o poder latente de um Pokémon!$Ele reorganiza completamente os átomos do Pokémon\nem nível molecular, tornando-o muito mais poderoso.$Hehe...@d{64} Só preciso de alguns volunt-@d{32}\nErr, sujeitos de teste, para provar que funciona.", + "title": "Acordo Sombrio", + "description": "O sujeito perturbador levanta algumas Pokébolas.\n\"Vou fazer valer a pena! Você pode ficar com estas Pokébolas fortes como pagamento, só preciso de um Pokémon da sua equipe! Hehe...\"", + "query": "O que você vai fazer?", + "option": { + "1": { + "label": "Aceitar", + "tooltip": "(+) 5 Bolas Desonestas\n(?) Melhorar um Pokémon Aleatório", + "selected_dialogue": "Vamos ver, aquele {{pokeName}} serve muito bem!$Lembre-se, não sou responsável\nse algo de ruim acontecer!@d{32} Hehe...", + "selected_message": "O homem te entrega 5 Bolas Desonestas.${{pokeName}} pula na máquina estranha...$Luzes piscando e sons estranhos\ncomeçam a sair da máquina!$...@d{96} Algo emerge\ndo dispositivo, enfurecido!" + }, + "2": { + "label": "Recusar", + "tooltip": "(-) Sem Recompensas", + "selected": "Não vai ajudar um pobre coitado?\nBah!" + } + }, + "outro": "Após o encontro aterrorizante,\nvocê se recompõe e parte." +} diff --git a/src/locales/pt_BR/mystery-encounters/delibirdy-dialogue.json b/src/locales/pt_BR/mystery-encounters/delibirdy-dialogue.json new file mode 100644 index 00000000000..a1ac79681d0 --- /dev/null +++ b/src/locales/pt_BR/mystery-encounters/delibirdy-dialogue.json @@ -0,0 +1,27 @@ +{ + "intro": "Um bando de {{delibirdName}} apareceu!", + "title": "Delibir-dy", + "description": "Os {{delibirdName}} estão te olhando com expectativa, como se quisessem algo. Talvez dar a eles um item ou um pouco de dinheiro os satisfaça?", + "query": "O que você vai dar a eles?", + "invalid_selection": "O Pokémon não tem esse tipo de item.", + "option": { + "1": { + "label": "Dar Dinheiro", + "tooltip": "(-) Dar aos {{delibirdName}} {{money, money}}\n(+) Receber um Item de Presente", + "selected": "Você joga o dinheiro para os {{delibirdName}},\nque conversam entre si animadamente.$Eles se viram para você e alegremente te dão um presente!" + }, + "2": { + "label": "Dar Comida", + "tooltip": "(-) Dar aos {{delibirdName}} uma Fruta ou Semente Reanimadora\n(+) Receber um Item de Presente", + "select_prompt": "Selecione um item para dar.", + "selected": "Você joga a {{chosenItem}} para os {{delibirdName}},\nque conversam entre si animadamente.$Eles se viram para você e alegremente te dão um presente!" + }, + "3": { + "label": "Dar um Item", + "tooltip": "(-) Dar aos {{delibirdName}} um Item Segurado\n(+) Receber um Item de Presente", + "select_prompt": "Selecione um item para dar.", + "selected": "Você joga o(a) {{chosenItem}} para os {{delibirdName}},\nque conversam entre si animadamente.$Eles se viram para você e alegremente te dão um presente!" + } + }, + "outro": "O bando de {{delibirdName}} sai alegremente andando para longe.$Que troca curiosa!" +} diff --git a/src/locales/pt_BR/mystery-encounters/department-store-sale-dialogue.json b/src/locales/pt_BR/mystery-encounters/department-store-sale-dialogue.json new file mode 100644 index 00000000000..c7a85c811aa --- /dev/null +++ b/src/locales/pt_BR/mystery-encounters/department-store-sale-dialogue.json @@ -0,0 +1,27 @@ +{ + "intro": "É uma senhora com um monte de sacolas de compras.", + "speaker": "Compradora", + "intro_dialogue": "Olá! Você também está aqui\npelas vendas incríveis?$Há um cupom especial que você pode\nresgatar por um item grátis durante a venda!$Eu tenho um extra. Aqui está!", + "title": "Venda na Loja de Departamentos", + "description": "Há mercadorias em todas as direções! Parece que há 4 balcões onde você pode resgatar o cupom por vários itens. As possibilidades são infinitas!", + "query": "Em qual balcão você vai?", + "option": { + "1": { + "label": "Balcão de TMs", + "tooltip": "(+) Loja de TMs" + }, + "2": { + "label": "Balcão de Vitaminas", + "tooltip": "(+) Loja de Vitaminas" + }, + "3": { + "label": "Balcão de Itens de Batalha", + "tooltip": "(+) Loja de Itens X" + }, + "4": { + "label": "Balcão de Pokébolas", + "tooltip": "(+) Loja de Pokébolas" + } + }, + "outro": "Que barganha! Você deveria comprar lá mais vezes." +} diff --git a/src/locales/pt_BR/mystery-encounters/field-trip-dialogue.json b/src/locales/pt_BR/mystery-encounters/field-trip-dialogue.json new file mode 100644 index 00000000000..4c3dd45fe3f --- /dev/null +++ b/src/locales/pt_BR/mystery-encounters/field-trip-dialogue.json @@ -0,0 +1,31 @@ +{ + "intro": "É uma professora e algumas crianças da escola!", + "speaker": "Professora", + "intro_dialogue": "Olá! Você poderia\nnos dar um minutinho para meus alunos?$Estou ensinando a eles sobre movimentos de Pokémon\ne adoraria mostrar uma demonstração.$Você se importaria de nos mostrar um\ndos movimentos que seu Pokémon pode usar?", + "title": "Excursão", + "description": "Uma professora está pedindo uma demonstração de movimentos de um Pokémon. Dependendo do movimento que você escolher, ela pode ter algo útil para te dar em troca.", + "query": "Qual categoria de movimento você vai mostrar?", + "option": { + "1": { + "label": "Um Movimento Físico", + "tooltip": "(+) Recompensas de Itens Físicos" + }, + "2": { + "label": "Um Movimento Especial", + "tooltip": "(+) Recompensas de Itens Especiais" + }, + "3": { + "label": "Um Movimento de Status", + "tooltip": "(+) Recompensas de Itens de Status" + }, + "selected": "{{pokeName}} faz uma exibição incrível de {{move}}!" + }, + "second_option_prompt": "Escolha um movimento para seu Pokémon usar.", + "incorrect": "...$Esse não é um movimento de {{moveCategory}}!\nSinto muito, mas não posso te dar nada.$Vamos, crianças, encontraremos\numa demonstração melhor em outro lugar.", + "incorrect_exp": "Parece que você aprendeu uma lição valiosa?$Seu Pokémon também ganhou um pouco de experiência.", + "correct": "Muito obrigada pela sua gentileza!\nEspero que esses itens sejam úteis para você!", + "correct_exp": "{{pokeName}} também ganhou uma valiosa experiência!", + "status": "Status", + "physical": "Físico", + "special": "Especial" +} diff --git a/src/locales/pt_BR/mystery-encounters/fiery-fallout-dialogue.json b/src/locales/pt_BR/mystery-encounters/fiery-fallout-dialogue.json new file mode 100644 index 00000000000..24c601e8e1e --- /dev/null +++ b/src/locales/pt_BR/mystery-encounters/fiery-fallout-dialogue.json @@ -0,0 +1,26 @@ +{ + "intro": "Você encontra uma tempestade abrasadora de fumaça e cinzas!", + "title": "Rescaldo Ardente", + "description": "As cinzas e brasas rodopiantes reduziram a visibilidade a quase zero. Parece que pode haver alguma... fonte causando essas condições. Mas o que poderia estar por trás de um fenômeno dessa magnitude?", + "query": "O que você vai fazer?", + "option": { + "1": { + "label": "Encontrar a Fonte", + "tooltip": "(?) Descobrir a fonte\n(-) Batalha Difícil", + "selected": "Você avança pela tempestade e encontra dois {{volcaronaName}}s no meio de uma dança de acasalamento!$Eles não gostam da interrupção e atacam!" + }, + "2": { + "label": "Abaixar-se e Esperar", + "tooltip": "(-) Sofrer os efeitos do clima", + "selected": "Os efeitos climáticos causam um dano significativo\nenquanto você luta para encontrar abrigo!$Seu grupo perde 20% de PS Máximo!", + "target_burned": "Seu {{burnedPokemon}} também foi queimado!" + }, + "3": { + "label": "Seus Tipos Fogo Ajudam", + "tooltip": "(+) Encerrar as condições\n(+) Ganhar um Carvão", + "disabled_tooltip": "Você precisa de pelo menos 2 Pokémon do Tipo Fogo para escolher esta opção", + "selected": "Seu {{option3PrimaryName}} e {{option3SecondaryName}} guiam você até onde dois {{volcaronaName}}s estão no meio de uma dança de acasalamento!$Felizmente, seus Pokémon conseguem acalmá-los,\ne eles partem sem problemas." + } + }, + "found_charcoal": "Após o clima limpar,\nseu {{leadPokemon}} vê algo no chão.$@s{item_fanfare}{{leadPokemon}} encontrou um Carvão!" +} diff --git a/src/locales/pt_BR/mystery-encounters/fight-or-flight-dialogue.json b/src/locales/pt_BR/mystery-encounters/fight-or-flight-dialogue.json new file mode 100644 index 00000000000..f90868e6045 --- /dev/null +++ b/src/locales/pt_BR/mystery-encounters/fight-or-flight-dialogue.json @@ -0,0 +1,25 @@ +{ + "intro": "Algo brilhante está cintilando\nno chão perto daquele Pokémon!", + "title": "Lutar ou Fugir", + "description": "Parece que há um Pokémon forte guardando um item. Batalhar é a abordagem mais direta, mas ele parece ser forte. Talvez você possa roubar o item, se tiver o Pokémon certo para o trabalho.", + "query": "O que você vai fazer?", + "option": { + "1": { + "label": "Batalhar com o Pokémon", + "tooltip": "(-) Batalha Difícil\n(+) Novo Item", + "selected": "Você se aproxima do\nPokémon sem medo.", + "stat_boost": "A força latente do {{enemyPokemon}} aumentou um de seus atributos!" + }, + "2": { + "label": "Roubar o Item", + "disabled_tooltip": "Seus Pokémon precisam conhecer certos movimentos para escolher isso", + "tooltip": "(+) {{option2PrimaryName}} usa {{option2PrimaryMove}}", + "selected": ".@d{32}.@d{32}.@d{32}$Seu {{option2PrimaryName}} te ajuda e usa {{option2PrimaryMove}}!$Você conseguiu pegar o item!" + }, + "3": { + "label": "Sair", + "tooltip": "(-) Sem Recompensas", + "selected": "Você deixa o Pokémon forte\ncom seu prêmio e segue em frente." + } + } +} diff --git a/src/locales/pt_BR/mystery-encounters/fun-and-games-dialogue.json b/src/locales/pt_BR/mystery-encounters/fun-and-games-dialogue.json new file mode 100644 index 00000000000..d3fd98f5d7f --- /dev/null +++ b/src/locales/pt_BR/mystery-encounters/fun-and-games-dialogue.json @@ -0,0 +1,30 @@ +{ + "intro_dialogue": "Venham, venham, pessoal! Tentem a sorte\nno novíssimo {{wobbuffetName}} Bate-o-mático!", + "speaker": "Animador", + "title": "Diversão e Jogos!", + "description": "Você encontrou um show itinerante com um jogo de prêmios! Você terá @[TOOLTIP_TITLE]{3 turnos} para deixar o {{wobbuffetName}} o mais próximo possível de @[TOOLTIP_TITLE]{1 PS} @[TOOLTIP_TITLE]{sem nocauteá-lo} para que ele possa carregar um grande Counter na máquina de tocar o sino.\nMas cuidado! Se você nocautear o {{wobbuffetName}}, terá que pagar o custo de revivê-lo!", + "query": "Gostaria de jogar?", + "option": { + "1": { + "label": "Jogar", + "tooltip": "(-) Pagar {{option1Money, money}}\n(+) Jogar {{wobbuffetName}} Bate-o-mático", + "selected": "Hora de testar sua sorte!" + }, + "2": { + "label": "Sair", + "tooltip": "(-) Sem Recompensas", + "selected": "Você segue seu caminho apressadamente,\ncom uma leve sensação de arrependimento." + } + }, + "ko": "Oh não! O {{wobbuffetName}} desmaiou!$Você perdeu o jogo e\nterá que pagar o custo da reanimação...", + "charging_continue": "O Wobbuffet continua carregando seu contra-ataque!", + "turn_remaining_3": "Restam três turnos!", + "turn_remaining_2": "Restam dois turnos!", + "turn_remaining_1": "Resta um turno!", + "end_game": "O tempo acabou!$O {{wobbuffetName}} se prepara para o contra-ataque e@d{16}.@d{16}.@d{16}.", + "best_result": "O {{wobbuffetName}} bate o botão com tanta força\nque o sino se quebra no topo!$Você ganhou o grande prêmio!", + "great_result": "O {{wobbuffetName}} bate o botão, quase acertando o sino!$Tão perto!\nVocê ganhou o prêmio de segundo nível!", + "good_result": "O {{wobbuffetName}} bate o botão com força suficiente para chegar no meio da escala!$Você ganhou o prêmio de terceiro nível!", + "bad_result": "O {{wobbuffetName}} mal toca o botão e nada acontece...$Oh não!\nVocê não ganhou nada!", + "outro": "Esse foi um joguinho divertido!" +} diff --git a/src/locales/pt_BR/mystery-encounters/global-trade-system-dialogue.json b/src/locales/pt_BR/mystery-encounters/global-trade-system-dialogue.json new file mode 100644 index 00000000000..d9866d64c7f --- /dev/null +++ b/src/locales/pt_BR/mystery-encounters/global-trade-system-dialogue.json @@ -0,0 +1,32 @@ +{ + "intro": "É uma interface para o Sistema de Troca Global!", + "title": "O GTS", + "description": "Ah, o GTS! Uma maravilha tecnológica, você pode se conectar com qualquer pessoa ao redor do mundo para trocar Pokémon com ela! Será que a sorte vai sorrir para sua troca hoje?", + "query": "O que você vai fazer?", + "option": { + "1": { + "label": "Ver Ofertas de Troca", + "tooltip": "(+) Selecionar uma oferta de troca para um dos seus Pokémon", + "trade_options_prompt": "Selecione um Pokémon para receber através da troca." + }, + "2": { + "label": "Troca Surpresa", + "tooltip": "(+) Envie um dos seus Pokémon para o GTS e receba um Pokémon aleatório em troca" + }, + "3": { + "label": "Trocar um Item", + "trade_options_prompt": "Selecione um item para enviar.", + "invalid_selection": "Este Pokémon não possui itens válidos para troca.", + "tooltip": "(+) Envie um dos seus Itens para o GTS e receba um novo Item aleatório" + }, + "4": { + "label": "Sair", + "tooltip": "(-) Sem Recompensas", + "selected": "Sem tempo para trocas hoje!\nVocê segue em frente." + } + }, + "pokemon_trade_selected": "{{tradedPokemon}} será enviado para {{tradeTrainerName}}.", + "pokemon_trade_goodbye": "Adeus, {{tradedPokemon}}!", + "item_trade_selected": "{{chosenItem}} será enviado para {{tradeTrainerName}}.$.@d{64}.@d{64}.@d{64}\n@s{level_up_fanfare}Troca concluída!$Você recebeu um {{itemName}} de {{tradeTrainerName}}!", + "trade_received": "@s{evolution_fanfare}{{tradeTrainerName}} enviou um {{received}}!" +} diff --git a/src/locales/pt_BR/mystery-encounters/lost-at-sea-dialogue.json b/src/locales/pt_BR/mystery-encounters/lost-at-sea-dialogue.json new file mode 100644 index 00000000000..86502fc06cd --- /dev/null +++ b/src/locales/pt_BR/mystery-encounters/lost-at-sea-dialogue.json @@ -0,0 +1,28 @@ +{ + "intro": "Vagando sem rumo pelo mar, você efetivamente não chegou a lugar nenhum.", + "title": "Perdido no Mar", + "description": "O mar está turbulento nesta área, e você está ficando sem energia.\nIsso é ruim. Haverá uma saída para essa situação?", + "query": "O que você vai fazer?", + "option": { + "1": { + "label": "{{option1PrimaryName}} Pode Ajudar", + "label_disabled": "Não pode {{option1RequiredMove}}", + "tooltip": "(+) {{option1PrimaryName}} te salva\n(+) {{option1PrimaryName}} ganha um pouco de EXP", + "tooltip_disabled": "Você não tem um Pokémon que possa {{option1RequiredMove}}", + "selected": "{{option1PrimaryName}} nada à frente, guiando você de volta ao caminho.${{option1PrimaryName}} parece ter ficado mais forte nesse momento de necessidade!" + }, + "2": { + "label": "{{option2PrimaryName}} Pode Ajudar", + "label_disabled": "Não pode {{option2RequiredMove}}", + "tooltip": "(+) {{option2PrimaryName}} te salva\n(+) {{option2PrimaryName}} ganha um pouco de EXP", + "tooltip_disabled": "Você não tem um Pokémon que possa {{option2RequiredMove}}", + "selected": "{{option2PrimaryName}} voa à frente do seu barco, guiando você de volta ao caminho.${{option2PrimaryName}} parece ter ficado mais forte nesse momento de necessidade!" + }, + "3": { + "label": "Vaguear Sem Rumo", + "tooltip": "(-) Cada um dos seus Pokémon perde {{damagePercentage}}% de seus PS totais", + "selected": "Você flutua no barco, navegando sem direção até finalmente avistar um ponto de referência que você se lembra.$Você e seus Pokémon estão exaustos de toda a provação." + } + }, + "outro": "Você está de volta ao caminho." +} diff --git a/src/locales/pt_BR/mystery-encounters/mysterious-challengers-dialogue.json b/src/locales/pt_BR/mystery-encounters/mysterious-challengers-dialogue.json new file mode 100644 index 00000000000..3d6a6744f2e --- /dev/null +++ b/src/locales/pt_BR/mystery-encounters/mysterious-challengers-dialogue.json @@ -0,0 +1,22 @@ +{ + "intro": "Desafiantes misteriosos apareceram!", + "title": "Desafiantes Misteriosos", + "description": "Se você derrotar um desafiante, pode impressioná-los o suficiente para receber uma recompensa. Mas alguns parecem fortes, você está à altura do desafio?", + "query": "Quem você vai enfrentar?", + "option": { + "1": { + "label": "Um Oponente Astuto e Atento", + "tooltip": "(-) Batalha Padrão\n(+) Recompensas de Itens de Movimento" + }, + "2": { + "label": "Um Oponente Forte", + "tooltip": "(-) Batalha Difícil\n(+) Boas Recompensas" + }, + "3": { + "label": "O Oponente Mais Poderoso", + "tooltip": "(-) Batalha Brutal\n(+) Grandes Recompensas" + }, + "selected": "O treinador dá um passo à frente..." + }, + "outro": "O desafiante misterioso foi derrotado!" +} diff --git a/src/locales/pt_BR/mystery-encounters/mysterious-chest-dialogue.json b/src/locales/pt_BR/mystery-encounters/mysterious-chest-dialogue.json new file mode 100644 index 00000000000..b2541f2c82a --- /dev/null +++ b/src/locales/pt_BR/mystery-encounters/mysterious-chest-dialogue.json @@ -0,0 +1,23 @@ +{ + "intro": "Você encontrou...@d{32} um baú?", + "title": "O Baú Misterioso", + "description": "Um baú lindamente decorado está no chão. Deve haver algo bom dentro... certo?", + "query": "Você vai abri-lo?", + "option": { + "1": { + "label": "Abrir", + "tooltip": "@[SUMMARY_BLUE]{({{trapPercent}}%) Algo terrível}\n@[SUMMARY_GREEN]{({{commonPercent}}%) Recompensas razoáveis}\n@[SUMMARY_GREEN]{({{ultraPercent}}%) Boas Recompensas}\n@[SUMMARY_GREEN]{({{roguePercent}}%) Ótimas Recompensas}\n@[SUMMARY_GREEN]{({{masterPercent}}%) Recompensas Incríveis}", + "selected": "Você abre o baú e encontra...", + "normal": "Apenas algumas ferramentas e itens normais.", + "good": "Algumas ferramentas e itens bem legais.", + "great": "Algumas ferramentas e itens ótimos!", + "amazing": "Uau! Um item incrível!", + "bad": "Oh não!@d{32}\nO baú era na verdade um {{gimmighoulName}} disfarçado!$Seu {{pokeName}} pula na sua frente,\nmas é nocauteado no processo!" + }, + "2": { + "label": "Muito Arriscado, Sair", + "tooltip": "(-) Sem Recompensas", + "selected": "Você segue seu caminho apressadamente,\ncom uma leve sensação de arrependimento." + } + } +} diff --git a/src/locales/pt_BR/mystery-encounters/part-timer-dialogue.json b/src/locales/pt_BR/mystery-encounters/part-timer-dialogue.json new file mode 100644 index 00000000000..3893aa41d2d --- /dev/null +++ b/src/locales/pt_BR/mystery-encounters/part-timer-dialogue.json @@ -0,0 +1,31 @@ +{ + "intro": "Um trabalhador ocupado te chama.", + "speaker": "Trabalhador", + "intro_dialogue": "Você parece ser alguém com muitos Pokémon capazes!$Nós podemos te pagar se você puder nos ajudar com algum trabalho de meio período!", + "title": "Trabalho Temporário", + "description": "Parece que há muitas tarefas que precisam ser feitas. Dependendo de quão bem o seu Pokémon se adapta a uma tarefa, ele pode ganhar mais ou menos dinheiro.", + "query": "Qual trabalho você vai escolher?", + "invalid_selection": "O Pokémon precisa estar saudável o suficiente.", + "option": { + "1": { + "label": "Fazer Entregas", + "tooltip": "(-) Seu Pokémon usa sua Velocidade\n(+) Ganhe @[MONEY]{Dinheiro}", + "selected": "Seu {{selectedPokemon}} faz um turno entregando pedidos para os clientes." + }, + "2": { + "label": "Trabalho no Armazém", + "tooltip": "(-) Seu Pokémon usa sua Força e Resistência\n(+) Ganhe @[MONEY]{Dinheiro}", + "selected": "Seu {{selectedPokemon}} faz um turno movendo itens no armazém." + }, + "3": { + "label": "Assistente de Vendas", + "tooltip": "(-) Seu {{option3PrimaryName}} usa {{option3PrimaryMove}}\n(+) Ganhe @[MONEY]{Dinheiro}", + "disabled_tooltip": "Seu Pokémon precisa conhecer certos movimentos para este trabalho", + "selected": "Seu {{option3PrimaryName}} passa o dia usando {{option3PrimaryMove}} para atrair clientes para o negócio!" + } + }, + "job_complete_good": "Obrigado pela ajuda!\nSeu {{selectedPokemon}} foi incrivelmente útil!$Aqui está seu pagamento pelo dia.", + "job_complete_bad": "Seu {{selectedPokemon}} nos ajudou um pouco!$Aqui está seu pagamento pelo dia.", + "pokemon_tired": "Seu {{selectedPokemon}} está exausto!\nO PP de todos os seus movimentos foi reduzido para 2!", + "outro": "Volte e nos ajude novamente algum dia!" +} diff --git a/src/locales/pt_BR/mystery-encounters/safari-zone-dialogue.json b/src/locales/pt_BR/mystery-encounters/safari-zone-dialogue.json new file mode 100644 index 00000000000..a43538e41f5 --- /dev/null +++ b/src/locales/pt_BR/mystery-encounters/safari-zone-dialogue.json @@ -0,0 +1,46 @@ +{ + "intro": "É uma zona de safári!", + "title": "A Zona de Safári", + "description": "Há todos os tipos de Pokémon raros e especiais que podem ser encontrados aqui!\nSe você escolher entrar, terá um limite de tempo de 3 encontros selvagens onde poderá tentar capturar esses Pokémon especiais.\n\nCuidado, no entanto. Esses Pokémon podem fugir antes que você consiga capturá-los!", + "query": "Você gostaria de entrar?", + "option": { + "1": { + "label": "Entrar", + "tooltip": "(-) Pagar {{option1Money, money}}\n@[SUMMARY_GREEN]{(?) Zona de Safári}", + "selected": "Hora de testar sua sorte!" + }, + "2": { + "label": "Sair", + "tooltip": "(-) Sem Recompensas", + "selected": "Você segue seu caminho apressadamente,\ncom uma leve sensação de arrependimento." + } + }, + "safari": { + "1": { + "label": "Jogar uma Pokébola", + "tooltip": "(+) Jogar uma Pokébola", + "selected": "Você joga uma Pokébola!" + }, + "2": { + "label": "Jogar Isca", + "tooltip": "(+) Aumenta a Taxa de Captura\n(-) Chance de Aumentar a Taxa de Fuga", + "selected": "Você joga um pouco de isca!" + }, + "3": { + "label": "Jogar Lama", + "tooltip": "(+) Diminui a Taxa de Fuga\n(-) Chance de Diminuir a Taxa de Captura", + "selected": "Você joga um pouco de lama!" + }, + "4": { + "label": "Fugir", + "tooltip": "(?) Fugir deste Pokémon" + }, + "watching": "{{pokemonName}} está observando cuidadosamente!", + "eating": "{{pokemonName}} está comendo!", + "busy_eating": "{{pokemonName}} está ocupado comendo!", + "angry": "{{pokemonName}} está com raiva!", + "beside_itself_angry": "{{pokemonName}} está fora de si de tanta raiva!", + "remaining_count": "{{remainingCount}} Pokémon restantes!" + }, + "outro": "Essa foi uma pequena excursão divertida!" +} diff --git a/src/locales/pt_BR/mystery-encounters/shady-vitamin-dealer-dialogue.json b/src/locales/pt_BR/mystery-encounters/shady-vitamin-dealer-dialogue.json new file mode 100644 index 00000000000..af58e99924c --- /dev/null +++ b/src/locales/pt_BR/mystery-encounters/shady-vitamin-dealer-dialogue.json @@ -0,0 +1,27 @@ +{ + "intro": "Um homem com um casaco escuro se aproxima de você.", + "speaker": "Vendedor Suspeito", + "intro_dialogue": ".@d{16}.@d{16}.@d{16}$Eu tenho os produtos, se você tiver o dinheiro.$Mas certifique-se de que seu Pokémon pode lidar com isso.", + "title": "O Vendedor de Vitaminas", + "description": "O homem abre o casaco para revelar algumas vitaminas de Pokémon. Os preços que ele menciona parecem uma boa oferta. Quase boa demais...\nEle oferece dois pacotes de ofertas para você escolher.", + "query": "Qual oferta você vai escolher?", + "invalid_selection": "O Pokémon precisa estar saudável o suficiente.", + "option": { + "1": { + "label": "A Oferta Barata", + "tooltip": "(-) Pagar {{option1Money, money}}\n(-) Efeitos colaterais?\n(+) Pokémon Escolhido Ganha 2 Vitaminas Aleatórias" + }, + "2": { + "label": "A Oferta Cara", + "tooltip": "(-) Pagar {{option2Money, money}}\n(+) Pokémon Escolhido Ganha 2 Vitaminas Aleatórias" + }, + "3": { + "label": "Sair", + "tooltip": "(-) Sem Recompensas", + "selected": "Heh, não imaginei que você fosse um covarde." + }, + "selected": "O homem te entrega dois frascos e desaparece rapidamente.${{selectedPokemon}} ganhou impulsos de {{boost1}} e {{boost2}}!" + }, + "cheap_side_effects": "Mas o remédio teve alguns efeitos colaterais!$Seu {{selectedPokemon}} sofreu alguns danos,\ne sua Natureza mudou para {{newNature}}!", + "no_bad_effects": "Parece que não houve efeitos colaterais do remédio!" +} diff --git a/src/locales/pt_BR/mystery-encounters/slumbering-snorlax-dialogue.json b/src/locales/pt_BR/mystery-encounters/slumbering-snorlax-dialogue.json new file mode 100644 index 00000000000..a53a60dd21a --- /dev/null +++ b/src/locales/pt_BR/mystery-encounters/slumbering-snorlax-dialogue.json @@ -0,0 +1,25 @@ +{ + "intro": "Enquanto você caminha por um caminho estreito, vê uma silhueta imponente bloqueando seu caminho.$Você se aproxima e vê um {{snorlaxName}} dormindo pacificamente.\nParece que não há como contorná-lo.", + "title": "{{snorlaxName}} Adormecido", + "description": "Você pode atacá-lo para tentar fazê-lo se mover, ou simplesmente esperar que ele acorde. Quem sabe quanto tempo isso pode levar, no entanto...", + "query": "O que você vai fazer?", + "option": { + "1": { + "label": "Batalhar com Ele", + "tooltip": "(-) Lutar com o {{snorlaxName}} Adormecido\n(+) Recompensa Especial", + "selected": "Você se aproxima do\nPokémon sem medo." + }, + "2": { + "label": "Esperar que Ele se Mova", + "tooltip": "(-) Esperar por Muito Tempo\n(+) Recuperar o Time", + "selected": ".@d{32}.@d{32}.@d{32}$Você espera por um tempo, mas os bocejos do {{snorlaxName}} deixam seu time sonolento...", + "rest_result": "Quando todos vocês acordam, o {{snorlaxName}} não está em lugar algum -\nmas seus Pokémon estão todos curados!" + }, + "3": { + "label": "Roubar Seu Item", + "tooltip": "(+) {{option3PrimaryName}} usa {{option3PrimaryMove}}\n(+) Recompensa Especial", + "disabled_tooltip": "Seus Pokémon precisam conhecer certos movimentos para escolher isso", + "selected": "Seu {{option3PrimaryName}} usa {{option3PrimaryMove}}!$@s{item_fanfare}Ele rouba Leftovers do {{snorlaxName}} adormecido\ne você foge como um bandido!" + } + } +} diff --git a/src/locales/pt_BR/mystery-encounters/teleporting-hijinks-dialogue.json b/src/locales/pt_BR/mystery-encounters/teleporting-hijinks-dialogue.json new file mode 100644 index 00000000000..e7b6dfa6831 --- /dev/null +++ b/src/locales/pt_BR/mystery-encounters/teleporting-hijinks-dialogue.json @@ -0,0 +1,27 @@ +{ + "intro": "É uma máquina estranha, zumbindo ruidosamente...", + "title": "Travessuras de Teletransporte", + "description": "A máquina tem uma placa que diz:\n \"Para usar, insira dinheiro e entre na cápsula.\"\n\nTalvez ela possa te transportar para algum lugar...", + "query": "O que você vai fazer?", + "option": { + "1": { + "label": "Colocar Dinheiro", + "tooltip": "(-) Pagar {{price, money}}\n(?) Teletransportar para Novo Bioma", + "selected": "Você insere algum dinheiro, e a cápsula se abre.\nVocê entra..." + }, + "2": { + "label": "Um Pokémon Ajuda", + "tooltip": "(-) {{option2PrimaryName}} Ajuda\n(+) {{option2PrimaryName}} ganha EXP\n(?) Teletransportar para Novo Bioma", + "disabled_tooltip": "Você precisa de um Pokémon do Tipo Aço ou Elétrico para escolher isso", + "selected": "O Tipo de {{option2PrimaryName}} permite que ele contorne a barreira de pagamento da máquina!$A cápsula se abre, e você entra..." + }, + "3": { + "label": "Inspecionar a Máquina", + "tooltip": "(-) Batalha de Pokémon", + "selected": "Você é atraído pelas luzes piscantes\ne pelos sons estranhos vindos da máquina...$Você nem percebe enquanto um Pokémon selvagem\nse aproxima e te embosca!" + } + }, + "transport": "A máquina treme violentamente,\nemitindo todo tipo de som estranho!$Logo após começar, ela se silencia mais uma vez.", + "attacked": "Você sai em uma área completamente nova, assustando um Pokémon selvagem!$O Pokémon selvagem ataca!", + "boss_enraged": "O {{enemyPokemon}} oponente ficou furioso!" +} diff --git a/src/locales/pt_BR/mystery-encounters/the-expert-pokemon-breeder-dialogue.json b/src/locales/pt_BR/mystery-encounters/the-expert-pokemon-breeder-dialogue.json new file mode 100644 index 00000000000..d84c6c07273 --- /dev/null +++ b/src/locales/pt_BR/mystery-encounters/the-expert-pokemon-breeder-dialogue.json @@ -0,0 +1,31 @@ +{ + "intro": "É um treinador carregando muitos Ovos de Pokémon!", + "intro_dialogue": "Ei, treinador!$Parece que alguns dos seus\nPokémon parceiros estão um pouco desanimados.$Por que não batalha comigo para animá-los?", + "title": "O Criador de Pokémon Experiente", + "description": "Você foi desafiado para uma batalha onde @[TOOLTIP_TITLE]{você só pode usar um único Pokémon}. Pode ser difícil, mas certamente fortaleceria o vínculo que você tem com o Pokémon que escolher!\nO criador também te dará alguns @[TOOLTIP_TITLE]{Ovos de Pokémon} se você vencer!", + "query": "Com quem você vai batalhar?", + "cleffa_1_nickname": "Ás", + "cleffa_2_nickname": "Clefablest", + "cleffa_3_nickname": "{{speciesName}} o Grande", + "option": { + "1": { + "label": "{{pokemon1Name}}", + "tooltip_base": "(-) Batalha Difícil\n(+) Aumentar Amizade com {{pokemon1Name}}" + }, + "2": { + "label": "{{pokemon2Name}}", + "tooltip_base": "(-) Batalha Difícil\n(+) Aumentar Amizade com {{pokemon2Name}}" + }, + "3": { + "label": "{{pokemon3Name}}", + "tooltip_base": "(-) Batalha Difícil\n(+) Aumentar Amizade com {{pokemon3Name}}" + }, + "selected": "Vamos lá!" + }, + "outro": "Veja como seu {{chosenPokemon}} está feliz agora!$Aqui, você também pode ficar com isso.", + "outro_failed": "Que decepção...$Parece que você ainda tem um longo caminho\na percorrer para ganhar a confiança dos seus Pokémon!", + "gained_eggs": "@s{item_fanfare}Você recebeu {{numEggs}}!", + "eggs_tooltip": "\n(+) Ganhe {{eggs}}", + "numEggs_one": "{{count}} Ovo {{rarity}}", + "numEggs_other": "{{count}} Ovos {{rarity}}" +} diff --git a/src/locales/pt_BR/mystery-encounters/the-pokemon-salesman-dialogue.json b/src/locales/pt_BR/mystery-encounters/the-pokemon-salesman-dialogue.json new file mode 100644 index 00000000000..71467a78cf9 --- /dev/null +++ b/src/locales/pt_BR/mystery-encounters/the-pokemon-salesman-dialogue.json @@ -0,0 +1,23 @@ +{ + "intro": "Um idoso animado se aproxima de você.", + "speaker": "Cavalheiro", + "intro_dialogue": "Olá! Tenho um negócio especialmente para VOCÊ!", + "title": "O Vendedor de Pokémon", + "description": "\"Este {{purchasePokemon}} é extremamente único e possui uma habilidade que normalmente não é encontrada em sua espécie! Vou deixar você levar este ótimo {{purchasePokemon}} por apenas {{price, money}}!\"\n\n\"O que me diz?\"", + "description_shiny": "\"Este {{purchasePokemon}} é extremamente único e possui uma pigmentação que normalmente não é encontrada em sua espécie! Vou deixar você levar este ótimo {{purchasePokemon}} por apenas {{price, money}}!\"\n\n\"O que me diz?\"", + "query": "O que você vai fazer?", + "option": { + "1": { + "label": "Aceitar", + "tooltip": "(-) Pagar {{price, money}}\n(+) Ganhar um {{purchasePokemon}} com sua Habilidade Oculta", + "tooltip_shiny": "(-) Pagar {{price, money}}\n(+) Ganhar um {{purchasePokemon}} brilhante", + "selected_message": "Você pagou uma quantia exorbitante e comprou o {{purchasePokemon}}.", + "selected_dialogue": "Excelente escolha!$Posso ver que você tem um olho afiado para negócios.$Ah, sim...@d{64} Devoluções não são aceitas, entendeu?" + }, + "2": { + "label": "Recusar", + "tooltip": "(-) Sem Recompensas", + "selected": "Não?@d{32} Você disse não?$Estou fazendo isso como um favor para você!" + } + } +} diff --git a/src/locales/pt_BR/mystery-encounters/the-strong-stuff-dialogue.json b/src/locales/pt_BR/mystery-encounters/the-strong-stuff-dialogue.json new file mode 100644 index 00000000000..0d479cdc629 --- /dev/null +++ b/src/locales/pt_BR/mystery-encounters/the-strong-stuff-dialogue.json @@ -0,0 +1,21 @@ +{ + "intro": "É um enorme {{shuckleName}} e o que parece ser\na grande reserva de... suco?", + "title": "O Suco Forte", + "description": "O {{shuckleName}} que bloqueia seu caminho parece incrivelmente forte. Enquanto isso, o suco ao lado dele está emanando algum tipo de poder.\n\nO {{shuckleName}} estende seus tentáculos na sua direção. Parece que ele quer fazer algo...", + "query": "O que você vai fazer?", + "option": { + "1": { + "label": "Aproximar-se do {{shuckleName}}", + "tooltip": "(?) Algo terrível ou incrível pode acontecer", + "selected": "Você desmaia.", + "selected_2": "@f{150}Quando você desperta, o {{shuckleName}} se foi\ne a reserva de suco está completamente drenada.${{highBstPokemon1}} e {{highBstPokemon2}}\nsentem uma terrível letargia!$Os atributos base deles foram reduzidas em {{reductionValue}}!$Seus outros Pokémon, no entanto, sentem um vigor incrível!\nOs atributos base deles aumentaram em {{increaseValue}}!" + }, + "2": { + "label": "Batalhar com o {{shuckleName}}", + "tooltip": "(-) Batalha Difícil\n(+) Recompensas Especiais", + "selected": "Enfurecido, o {{shuckleName}} bebe um pouco do seu suco e ataca!", + "stat_boost": "O suco do {{shuckleName}} aumenta suas estatísticas!" + } + }, + "outro": "Que reviravolta bizarra de eventos." +} diff --git a/src/locales/pt_BR/mystery-encounters/the-winstrate-challenge-dialogue.json b/src/locales/pt_BR/mystery-encounters/the-winstrate-challenge-dialogue.json new file mode 100644 index 00000000000..54a486be533 --- /dev/null +++ b/src/locales/pt_BR/mystery-encounters/the-winstrate-challenge-dialogue.json @@ -0,0 +1,22 @@ +{ + "intro": "É uma família parada do lado de fora da casa deles!", + "speaker": "Os Winstrates", + "intro_dialogue": "Nós somos os Winstrates!$Que tal enfrentar nossa família em uma série de batalhas Pokémon?", + "title": "O Desafio Winstrate", + "description": "Os Winstrates são uma família de 5 treinadores, e eles querem batalhar! Se você derrotar todos eles em sequência, receberá um grande prêmio. Mas será que você aguenta o desafio?", + "query": "O que você vai fazer?", + "option": { + "1": { + "label": "Aceitar o Desafio", + "tooltip": "(-) Batalha Brutal\n(+) Recompensa Especial de Item", + "selected": "Que comece o desafio!" + }, + "2": { + "label": "Recusar o Desafio", + "tooltip": "(+) Curar Todo o Time\n(+) Ganhar um Doce Raro", + "selected": "Que pena. Diga, seu time parece exausto, por que não fica um pouco e descansa?" + } + }, + "victory": "Parabéns por vencer nosso desafio!$Primeiro de tudo, queremos te dar este Voucher.", + "victory_2": "Além disso, nossa família usa este Bracelete Macho para fortalecer\nnossos Pokémon mais efetivamente durante o treinamento.$Você pode não precisar, já que derrotou todos nós, mas esperamos que aceite assim mesmo!" +} diff --git a/src/locales/pt_BR/mystery-encounters/training-session-dialogue.json b/src/locales/pt_BR/mystery-encounters/training-session-dialogue.json new file mode 100644 index 00000000000..6bb41058e01 --- /dev/null +++ b/src/locales/pt_BR/mystery-encounters/training-session-dialogue.json @@ -0,0 +1,33 @@ +{ + "intro": "Você encontrou algumas\nferramentas e suprimentos de treinamento.", + "title": "Sessão de Treinamento", + "description": "Esses suprimentos parecem que poderiam ser usados para treinar um membro do seu time! Há algumas maneiras de treinar seu Pokémon, batalhando contra ele com o resto do seu time.", + "query": "Como você deve treinar?", + "invalid_selection": "O Pokémon precisa estar saudável o suficiente.", + "option": { + "1": { + "label": "Treinamento Leve", + "tooltip": "(-) Batalha Leve\n(+) Melhorar 2 IVs Aleatórios do Pokémon", + "finished": "{{selectedPokemon}} retorna, sentindo-se\ncansado, mas realizado!$Seus IVs de {{stat1}} e {{stat2}} foram melhorados!" + }, + "2": { + "label": "Treinamento Moderado", + "tooltip": "(-) Batalha Moderada\n(+) Mudar a Natureza do Pokémon", + "select_prompt": "Selecione uma nova natureza\npara treinar seu Pokémon.", + "finished": "{{selectedPokemon}} retorna, sentindo-se\ncansado, mas realizado!$Sua natureza foi alterada para {{nature}}!" + }, + "3": { + "label": "Treinamento Intenso", + "tooltip": "(-) Batalha Rigorosa\n(+) Mudar a Habilidade do Pokémon", + "select_prompt": "Selecione uma nova habilidade\npara treinar seu Pokémon.", + "finished": "{{selectedPokemon}} retorna, sentindo-se\ncansado, mas realizado!$Sua habilidade foi alterada para {{ability}}!" + }, + "4": { + "label": "Sair", + "tooltip": "(-) Sem Recompensas", + "selected": "Você não tem tempo para treinar.\nHora de seguir em frente." + }, + "selected": "{{selectedPokemon}} atravessa\na clareira para te enfrentar..." + }, + "outro": "Essa foi uma sessão de treinamento bem-sucedida!" +} diff --git a/src/locales/pt_BR/mystery-encounters/trash-to-treasure-dialogue.json b/src/locales/pt_BR/mystery-encounters/trash-to-treasure-dialogue.json new file mode 100644 index 00000000000..0af71ddf6ff --- /dev/null +++ b/src/locales/pt_BR/mystery-encounters/trash-to-treasure-dialogue.json @@ -0,0 +1,19 @@ +{ + "intro": "É uma pilha enorme de lixo!\nDe onde isso veio?", + "title": "Do Lixo ao Luxo", + "description": "A pilha de lixo se ergue sobre você, e você pode ver alguns itens de valor enterrados no meio dos detritos. Você tem certeza de que quer se cobrir de sujeira para pegá-los?", + "query": "O que você vai fazer?", + "option": { + "1": { + "label": "Cavar em Busca de Valiosos", + "tooltip": "(-) Perder Itens de Cura nas Lojas\n(+) Ganhar Itens Incríveis", + "selected": "Você vasculha a pilha de lixo, ficando preso na sujeira.$Não há como os lojistas respeitáveis\nvenderem algo para você nesse estado imundo!$Você terá que se virar sem itens de cura nas lojas.$No entanto, você encontrou alguns itens incríveis no lixo!" + }, + "2": { + "label": "Investigar Mais", + "tooltip": "(?) Encontrar a Fonte do Lixo", + "selected": "Você vagueia ao redor da pilha, procurando qualquer indicação de como isso apareceu aqui...", + "selected_2": "De repente, o lixo se move! Não era apenas lixo, era um Pokémon!" + } + } +} diff --git a/src/locales/pt_BR/mystery-encounters/uncommon-breed-dialogue.json b/src/locales/pt_BR/mystery-encounters/uncommon-breed-dialogue.json new file mode 100644 index 00000000000..7d9b07ae99d --- /dev/null +++ b/src/locales/pt_BR/mystery-encounters/uncommon-breed-dialogue.json @@ -0,0 +1,26 @@ +{ + "intro": "Esse não é um Pokémon comum!", + "title": "Raça Incomum", + "description": "O {{enemyPokemon}} parece especial comparado a outros de sua espécie. @[TOOLTIP_TITLE]{Talvez ele conheça um movimento especial?} Você poderia batalhar e capturá-lo diretamente, mas também pode haver uma maneira de fazer amizade com ele.", + "query": "O que você vai fazer?", + "option": { + "1": { + "label": "Batalhar com o Pokémon", + "tooltip": "(-) Batalha Difícil\n(+) Oponente Forte Capturável", + "selected": "Você se aproxima do\n{{enemyPokemon}} sem medo.", + "stat_boost": "As habilidades elevadas do {{enemyPokemon}} aumentam seus atributos!" + }, + "2": { + "label": "Dar Comida", + "disabled_tooltip": "Você precisa de 4 itens de fruta para escolher isso", + "tooltip": "(-) Dar 4 Frutas\n(+) O {{enemyPokemon}} Gosta de Você", + "selected": "Você joga as frutas para o {{enemyPokemon}}!$Ele as come feliz!$O {{enemyPokemon}} quer se juntar ao seu time!" + }, + "3": { + "label": "Fazer Amizade", + "disabled_tooltip": "Seus Pokémon precisam conhecer certos movimentos para escolher isso", + "tooltip": "(+) {{option3PrimaryName}} usa {{option3PrimaryMove}}\n(+) O {{enemyPokemon}} Gosta de Você", + "selected": "Seu {{option3PrimaryName}} usa {{option3PrimaryMove}} para encantar o {{enemyPokemon}}!$O {{enemyPokemon}} quer se juntar ao seu time!" + } + } +} diff --git a/src/locales/pt_BR/mystery-encounters/weird-dream-dialogue.json b/src/locales/pt_BR/mystery-encounters/weird-dream-dialogue.json new file mode 100644 index 00000000000..1806e1fd04f --- /dev/null +++ b/src/locales/pt_BR/mystery-encounters/weird-dream-dialogue.json @@ -0,0 +1,22 @@ +{ + "intro": "Uma mulher sombria bloqueia seu caminho.\nAlgo nela é perturbador...", + "speaker": "Mulher", + "intro_dialogue": "Eu vi seu futuro, seu passado...$Criança, você os vê também?", + "title": "???", + "description": "As palavras da mulher ecoam na sua cabeça. Não era apenas uma única voz, mas uma vasta multidão, de todas as linhas do tempo e realidades. Você começa a sentir tontura, a pergunta permanece em sua mente...\n\n@[TOOLTIP_TITLE]{\"Eu vi seu futuro, seu passado... Criança, você os vê também?\"}", + "query": "O que você vai fazer?", + "option": { + "1": { + "label": "\"Eu os Vejo\"", + "tooltip": "@[SUMMARY_GREEN]{(?) Afeta seus Pokémon}", + "selected": "A mão dela se estende para te tocar,\ne tudo fica escuro.$Então...@d{64} Você vê tudo.\nTodas as linhas do tempo, todos os seus eus diferentes,\n passado e futuro.$Tudo o que te formou,\ntudo o que você se tornará...@d{64}", + "cutscene": "Você vê seus Pokémon,@d{32} convergindo de\ntodas as realidades para se tornarem algo novo...@d{64}", + "dream_complete": "Quando você desperta, a mulher - era uma mulher ou um fantasma? - se foi...$.@d{32}.@d{32}.@d{32}$Sua equipe de Pokémon mudou...\nOu é a mesma equipe que você sempre teve?" + }, + "2": { + "label": "Sair Rapidamente", + "tooltip": "(-) Afeta seus Pokémon", + "selected": "Você arranca sua mente de um aperto entorpecente e sai apressadamente.$Quando finalmente para para se recompor, você verifica os Pokémon em sua equipe.$Por algum motivo, todos os níveis deles diminuíram!" + } + } +} diff --git a/src/locales/pt_BR/nature.json b/src/locales/pt_BR/nature.json index 5678c74061c..11be27d6f53 100644 --- a/src/locales/pt_BR/nature.json +++ b/src/locales/pt_BR/nature.json @@ -24,4 +24,4 @@ "Sassy": "Atrevida", "Careful": "Cuidadosa", "Quirky": "Peculiar" -} \ No newline at end of file +} diff --git a/src/locales/pt_BR/party-ui-handler.json b/src/locales/pt_BR/party-ui-handler.json index 435bc916ac6..ef09e09fb2b 100644 --- a/src/locales/pt_BR/party-ui-handler.json +++ b/src/locales/pt_BR/party-ui-handler.json @@ -15,6 +15,7 @@ "UNPAUSE_EVOLUTION": "Ativar Evolução", "REVIVE": "Reanimar", "RENAME": "Renomear", + "SELECT": "Select", "choosePokemon": "Escolha um Pokémon.", "doWhatWithThisPokemon": "O que você deseja fazer?", "noEnergy": "{{pokemonName}} não pode\nmais batalhar!", @@ -44,4 +45,4 @@ "untilWeMeetAgain": "Até nos encontrarmos novamente, {{pokemonName}}!", "sayonara": "Sayonara, {{pokemonName}}!", "smellYaLater": "Te vejo depois, {{pokemonName}}!" -} \ No newline at end of file +} diff --git a/src/locales/pt_BR/pokeball.json b/src/locales/pt_BR/pokeball.json index 3a059654bb3..5f1dff974ec 100644 --- a/src/locales/pt_BR/pokeball.json +++ b/src/locales/pt_BR/pokeball.json @@ -5,4 +5,4 @@ "rogueBall": "Bola Rogue", "masterBall": "Bola Mestra", "luxuryBall": "Bola Luxo" -} \ No newline at end of file +} diff --git a/src/locales/pt_BR/pokemon-form-battle.json b/src/locales/pt_BR/pokemon-form-battle.json index 6ea7947fb66..50fbeeef29e 100644 --- a/src/locales/pt_BR/pokemon-form-battle.json +++ b/src/locales/pt_BR/pokemon-form-battle.json @@ -11,4 +11,4 @@ "revertChange": "{{pokemonName}} voltou\npara sua forma original!", "formChange": "{{preName}} mudou de forma!", "disguiseChange": "O seu disfarce serviu-lhe de isca!" -} \ No newline at end of file +} 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-info-container.json b/src/locales/pt_BR/pokemon-info-container.json index dcf1fc4e0b5..3ba3312a401 100644 --- a/src/locales/pt_BR/pokemon-info-container.json +++ b/src/locales/pt_BR/pokemon-info-container.json @@ -4,4 +4,4 @@ "ability": "Habilidade:", "nature": "Natureza:", "form": "Forma:" -} \ No newline at end of file +} 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/pokemon.json b/src/locales/pt_BR/pokemon.json index f780d2accbd..f50b89c4f70 100644 --- a/src/locales/pt_BR/pokemon.json +++ b/src/locales/pt_BR/pokemon.json @@ -1081,4 +1081,4 @@ "paldea_tauros": "Tauros", "paldea_wooper": "Wooper", "bloodmoon_ursaluna": "Ursaluna" -} \ No newline at end of file +} diff --git a/src/locales/pt_BR/run-history.json b/src/locales/pt_BR/run-history.json index 74cc2c35d28..a615b598bcf 100644 --- a/src/locales/pt_BR/run-history.json +++ b/src/locales/pt_BR/run-history.json @@ -34,4 +34,4 @@ "hallofFameText_female": "Bem-vinda ao Hall da Fama!", "viewHallOfFame": "Veja o Hall da Fama!", "viewEndingSplash": "Veja a arte final!" -} \ No newline at end of file +} diff --git a/src/locales/pt_BR/save-slot-select-ui-handler.json b/src/locales/pt_BR/save-slot-select-ui-handler.json index 31fe28de691..60ea7b073e9 100644 --- a/src/locales/pt_BR/save-slot-select-ui-handler.json +++ b/src/locales/pt_BR/save-slot-select-ui-handler.json @@ -4,4 +4,4 @@ "wave": "Onda", "lv": "Nv", "empty": "Vazio" -} \ No newline at end of file +} diff --git a/src/locales/pt_BR/settings.json b/src/locales/pt_BR/settings.json index 74f3918bed8..6c4eae23a82 100644 --- a/src/locales/pt_BR/settings.json +++ b/src/locales/pt_BR/settings.json @@ -104,4 +104,4 @@ "reroll": "Atualizar", "shop": "Loja", "checkTeam": "Checar Time" -} +} \ No newline at end of file diff --git a/src/locales/pt_BR/splash-messages.json b/src/locales/pt_BR/splash-messages.json index 55c0b1b9e74..174dc5e4092 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!", @@ -32,5 +32,5 @@ "alsoTryRadicalRed": "Também Jogue Radical Red!", "eeveeExpo": "Eevee Expo!", "ynoproject": "YNOproject!", - "breedersInSpace": "Criadores Pokémon no Espaço!" -} \ No newline at end of file + "breedersInSpace": "Criadores de Pokémon no Espaço!" +} diff --git a/src/locales/pt_BR/starter-select-ui-handler.json b/src/locales/pt_BR/starter-select-ui-handler.json index 1d83e43f12c..8de6c8bcae4 100644 --- a/src/locales/pt_BR/starter-select-ui-handler.json +++ b/src/locales/pt_BR/starter-select-ui-handler.json @@ -42,4 +42,4 @@ "locked": "Bloqueada", "disabled": "Desativada", "uncaught": "Não capturado" -} \ No newline at end of file +} diff --git a/src/locales/pt_BR/status-effect.json b/src/locales/pt_BR/status-effect.json index 5a851a0bdeb..f3684128043 100644 --- a/src/locales/pt_BR/status-effect.json +++ b/src/locales/pt_BR/status-effect.json @@ -1,12 +1,6 @@ { "none": { - "name": "Nenhum", - "description": "", - "obtain": "", - "obtainSource": "", - "activation": "", - "overlap": "", - "heal": "" + "name": "Nenhum" }, "poison": { "name": "Envenenamento", @@ -62,4 +56,4 @@ "overlap": "{{pokemonNameWithAffix}} já\nestá queimado!", "heal": "{{pokemonNameWithAffix}} foi\ncurado de sua queimadura!" } -} \ No newline at end of file +} diff --git a/src/locales/pt_BR/terrain.json b/src/locales/pt_BR/terrain.json index 73df2b441ac..a672c58ca82 100644 --- a/src/locales/pt_BR/terrain.json +++ b/src/locales/pt_BR/terrain.json @@ -13,4 +13,4 @@ "psychicStartMessage": "O campo de batalha ficou esquisito!", "psychicClearMessage": "A esquisitice sumiu do campo de batalha!", "defaultBlockMessage": "{{pokemonNameWithAffix}} está protegido pelo Terreno {{terrainName}}!" -} \ No newline at end of file +} 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..deffc346e03 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", @@ -158,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": "Superfã de Insetos", + "expert_pokemon_breeder": "Expert Pokémon Breeder" } diff --git a/src/locales/pt_BR/trainer-titles.json b/src/locales/pt_BR/trainer-titles.json index 701c2e143fa..0bd03901c2b 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,8 @@ "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", + + "the_winstrates": "Os Winstrates'" } diff --git a/src/locales/pt_BR/tutorial.json b/src/locales/pt_BR/tutorial.json index 92ea0dd080f..09199a9e31c 100644 --- a/src/locales/pt_BR/tutorial.json +++ b/src/locales/pt_BR/tutorial.json @@ -1,7 +1,7 @@ { "intro": "Bem-vindo ao PokéRogue!\n$Este é um fangame Pokémon focado em batalhas com elementos roguelite.\n$Este jogo não é monetizado e não reivindicamos propriedade do Pokémon nem dos ativos protegidos$por direitos autorais usados.\n$O jogo é um trabalho em andamento,\nmas totalmente jogável.\n$Para relatórios de bugs, use a comunidade do Discord.\n$Se o jogo rodar lentamente, certifique-se de que\na 'Aceleração de Hardware' esteja ativada$nas configurações do seu navegador.", "accessMenu": "Para acessar o menu, pressione M ou Esc.\n$O menu contém configurações e diversas funções.", - "menu": "A partir deste menu, você pode\\nacessar as configurações.\n$A partir das configurações, você\npode alterar a velocidade do jogo,\n$o estilo da janela e outras opções.\n$Há também vários outros recursos aqui.\nCertifique-se de verificar todos eles!", + "menu": "A partir deste menu, você pode\nacessar as configurações.\n$A partir das configurações, você\npode alterar a velocidade do jogo,\n$o estilo da janela e outras opções.\n$Há também vários outros recursos aqui.\nCertifique-se de verificar todos eles!", "starterSelect": "Nesta tela, você pode selecionar seus iniciais\npressionando Z ou a barra de espaço.\n$Esses serão os primeiros membros da sua equipe.\n$Cada inicial tem um custo. Sua equipe pode ter até 6 membros,\ndesde que desde que o custo total não exceda 10.\n$Você pode escolher o gênero, a habilidade\ne até a forma do seu inicial.\n$Essas opções dependem das variantes dessa\nespécie que você já capturou ou chocou.\n$Os IVs de cada inicial são os melhores de todos os Pokémon\ndaquela espécie que você já capturou ou chocou.\n$Sempre capture vários Pokémon de todas as espécies!", "pokerus": "Todo dia, 3 Pokémon iniciais ficam com uma borda roxa.\n$Caso veja um inicial que você possui com uma dessa, tente\nadicioná-lo a sua equipe. Lembre-se de olhar seu sumário!", "statChange": "As mudanças de atributos se mantém após a batalha desde que o Pokémon não seja trocado.\n$Seus Pokémon voltam a suas Poké Bolas antes de batalhas contra treinadores e de entrar em um novo bioma.\n$Para ver as mudanças de atributos dos Pokémon em campo, mantena C ou Shift pressionado durante a batalha.", diff --git a/src/locales/pt_BR/voucher.json b/src/locales/pt_BR/voucher.json index c33dfa20e0f..7430fecaece 100644 --- a/src/locales/pt_BR/voucher.json +++ b/src/locales/pt_BR/voucher.json @@ -6,4 +6,4 @@ "eggVoucherGold": "Voucher de Ovo Dourado", "locked": "Bloqueado", "defeatTrainer": "Derrote {{trainerName}}" -} \ No newline at end of file +} diff --git a/src/locales/pt_BR/weather.json b/src/locales/pt_BR/weather.json index 54b929da0b9..eb3b3a74005 100644 --- a/src/locales/pt_BR/weather.json +++ b/src/locales/pt_BR/weather.json @@ -29,4 +29,4 @@ "strongWindsLapseMessage": "Os ventos fortes continuam.", "strongWindsEffectMessage": "A corrente de ar misteriosa enfraqueceu o ataque!", "strongWindsClearMessage": "Os ventos fortes diminuíram." -} \ No newline at end of file +} 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/achv.json b/src/locales/zh_CN/achv.json index b93345d876b..f5e77dac140 100644 --- a/src/locales/zh_CN/achv.json +++ b/src/locales/zh_CN/achv.json @@ -272,5 +272,9 @@ "INVERSE_BATTLE": { "name": "镜子子镜", "description": "完成逆转之战挑战\n战挑战之转逆成完" + }, + "BREEDERS_IN_SPACE": { + "name": "Breeders in Space!", + "description": "Beat the Expert Pokémon Breeder in the Space Biome." } } diff --git a/src/locales/zh_CN/battle.json b/src/locales/zh_CN/battle.json index ccf0e560805..355c253676d 100644 --- a/src/locales/zh_CN/battle.json +++ b/src/locales/zh_CN/battle.json @@ -15,6 +15,10 @@ "moneyPickedUp": "捡到了₽{{moneyAmount}}!", "pokemonCaught": "{{pokemonName}}被抓住了!", "addedAsAStarter": "增加了{{pokemonName}}作为\n一个新的基础宝可梦!", + "pokemonObtained": "你获得了{{pokemonName}}!", + "pokemonBrokeFree": "噢不!\n宝可梦挣脱了!", + "pokemonFled": "野生的{{pokemonName}}逃走了!", + "playerFled": "你从{{pokemonName}}那里逃走了!", "partyFull": "你的队伍已满员。是否放生其他宝可梦\n为{{pokemonName}}腾出空间?", "pokemon": "宝可梦", "sendOutPokemon": "上吧!\n{{pokemonName}}!", @@ -49,6 +53,7 @@ "noPokeballTrainer": "你不能捕捉其他训练家的宝可梦!", "noPokeballMulti": "只能在剩下一只宝可梦时才能扔出精灵球!", "noPokeballStrong": "目标宝可梦太强了,无法捕捉!\n你需要先削弱它!", + "noPokeballMysteryEncounter": "你无法\n捕捉这只宝可梦!", "noEscapeForce": "一股无形的力量阻止你逃跑。", "noEscapeTrainer": "你不能从与训练家的战斗中逃跑!", "noEscapePokemon": "{{pokemonName}}的{{moveName}}\n阻止了你{{escapeVerb}}!", @@ -87,5 +92,6 @@ "retryBattle": "你要从对战开始时重试么?", "unlockedSomething": "{{unlockedThing}}\n已解锁。", "congratulations": "恭喜!", - "beatModeFirstTime": "{{speciesName}}首次击败了{{gameMode}}!\n你获得了{{newModifier}}!" + "beatModeFirstTime": "{{speciesName}}首次击败了{{gameMode}}!\n你获得了{{newModifier}}!", + "mysteryEncounterAppeared": "这是什么?" } diff --git a/src/locales/zh_CN/bgm-name.json b/src/locales/zh_CN/bgm-name.json index 065347e3bb6..f59a637d7d8 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": "黑白 「视线!背包客」", @@ -144,5 +147,11 @@ "encounter_youngster": "黑白 「视线!短裤小子」", "heal": "黑白「宝可梦回复」", "menu": "空之探险队「欢迎来到宝可梦的世界」", - "title": "空之探险队「主题曲」" + "title": "空之探险队「主题曲」", + + "mystery_encounter_weird_dream": "空之探险队「时限之塔」", + "mystery_encounter_fun_and_games": "空之探险队「会长胖可丁」", + "mystery_encounter_gen_5_gts": "黑白「GTS」", + "mystery_encounter_gen_6_gts": "XY「GTS」", + "mystery_encounter_delibirdy": "Firel - 信使鸟快递!" } diff --git a/src/locales/zh_CN/config.ts b/src/locales/zh_CN/config.ts index 44a190d8c11..15a69eb481a 100644 --- a/src/locales/zh_CN/config.ts +++ b/src/locales/zh_CN/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 "./mystery-encounters/mysterious-chest-dialogue.json"; +import mysteriousChallengers from "./mystery-encounters/mysterious-challengers-dialogue.json"; +import darkDeal from "./mystery-encounters/dark-deal-dialogue.json"; +import departmentStoreSale from "./mystery-encounters/department-store-sale-dialogue.json"; +import fieldTrip from "./mystery-encounters/field-trip-dialogue.json"; +import fieryFallout from "./mystery-encounters/fiery-fallout-dialogue.json"; +import fightOrFlight from "./mystery-encounters/fight-or-flight-dialogue.json"; +import safariZone from "./mystery-encounters/safari-zone-dialogue.json"; +import shadyVitaminDealer from "./mystery-encounters/shady-vitamin-dealer-dialogue.json"; +import slumberingSnorlax from "./mystery-encounters/slumbering-snorlax-dialogue.json"; +import trainingSession from "./mystery-encounters/training-session-dialogue.json"; +import theStrongStuff from "./mystery-encounters/the-strong-stuff-dialogue.json"; +import pokemonSalesman from "./mystery-encounters/the-pokemon-salesman-dialogue.json"; +import offerYouCantRefuse from "./mystery-encounters/an-offer-you-cant-refuse-dialogue.json"; +import delibirdy from "./mystery-encounters/delibirdy-dialogue.json"; +import absoluteAvarice from "./mystery-encounters/absolute-avarice-dialogue.json"; +import aTrainersTest from "./mystery-encounters/a-trainers-test-dialogue.json"; +import trashToTreasure from "./mystery-encounters/trash-to-treasure-dialogue.json"; +import berriesAbound from "./mystery-encounters/berries-abound-dialogue.json"; +import clowningAround from "./mystery-encounters/clowning-around-dialogue.json"; +import partTimer from "./mystery-encounters/part-timer-dialogue.json"; +import dancingLessons from "./mystery-encounters/dancing-lessons-dialogue.json"; +import weirdDream from "./mystery-encounters/weird-dream-dialogue.json"; +import theWinstrateChallenge from "./mystery-encounters/the-winstrate-challenge-dialogue.json"; +import teleportingHijinks from "./mystery-encounters/teleporting-hijinks-dialogue.json"; +import bugTypeSuperfan from "./mystery-encounters/bug-type-superfan-dialogue.json"; +import funAndGames from "./mystery-encounters/fun-and-games-dialogue.json"; +import uncommonBreed from "./mystery-encounters/uncommon-breed-dialogue.json"; +import globalTradeSystem from "./mystery-encounters/global-trade-system-dialogue.json"; +import expertPokemonBreeder from "./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 zhCnConfig = { ability, abilityTriggers, @@ -110,4 +152,40 @@ export const zhCnConfig = { modifierSelectUiHandler, moveTriggers, 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/zh_CN/dialogue.json b/src/locales/zh_CN/dialogue.json index dd0fa3fb3cc..3e258de22f6 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$就算输了,我也不会改变主意的……" @@ -933,6 +1026,116 @@ "1": "我承认我做的事情非常渗人,我也不指望你能理解。\n$但我必须为伽勒尔地区提供无限的能源,确保永久的繁荣。" } }, + "stat_trainer_buck": { + "encounter": { + "1": "我现在告诉你!我强的不可思议,做好准备!", + "2": "我能感受到宝可梦在球中激动地颤抖!" + }, + "victory": { + "1": "呵哈哈!\n真火热啊,你!", + "2": "呵哈哈!\n真火热啊,你!" + }, + "defeat": { + "1": "哇哦!这下没气儿了吧,我猜。", + "2": "哇哦!这下没气儿了吧,我猜。" + } + }, + "stat_trainer_cheryl": { + "encounter": { + "1": "我的宝可梦渴望着战斗。", + "2": "我应该警告你,我的神奇宝贝可能有点活泼。" + }, + "victory": { + "1": "在进攻和防守之间找到平衡……这并不容易。", + "2": "在进攻和防守之间找到平衡……这并不容易。" + }, + "defeat": { + "1": "你的宝可梦需要治疗吗?", + "2": "你的宝可梦需要治疗吗?" + } + }, + "stat_trainer_marley": { + "encounter": { + "1": "……好的。\n我会尽力的。", + "2": "……好的。\n我不会输的。" + }, + "victory": { + "1": "……噢", + "2": "……噢" + }, + "defeat": { + "1": "……再见。", + "2": "……再见。" + } + }, + "stat_trainer_mira": { + "encounter": { + "1": "你会被麦儿吓到的哦!", + "2": "麦儿不会再输了!" + }, + "victory": { + "1": "麦儿在想……她的生涯还能不能走远。", + "2": "麦儿在想……她的生涯还能不能走远。" + }, + "defeat": { + "1": "麦儿就知道自己会赢!", + "2": "麦儿就知道自己会赢!" + } + }, + "stat_trainer_riley": { + "encounter": { + "1": "战斗是我们打招呼的方式!", + "2": "我们会尽一切努力来打败你的宝可梦。" + }, + "victory": { + "1": "有时我们会战斗,有时我们会联手...$训练师之间能交流真好。", + "2": "有时我们会战斗,有时我们会联手...$训练师之间能交流真好。" + }, + "defeat": { + "1": "你表现的相当出色\n祝你下次好运。", + "2": "你表现的相当出色\n祝你下次好运。" + } + }, + "winstrates_victor": { + "encounter": { + "1": "就是这股势头!我看好你!" + }, + "victory": { + "1": "啊哈!你比我想象的还要强!" + } + }, + "winstrates_victoria": { + "encounter": { + "1": "天哪!这么年轻?$不过,能打败我的丈夫,你至少称得上是训练师。$现在轮到我上场了!" + }, + "victory": { + "1": "呃啊!你这得多强啊!" + } + }, + "winstrates_vivi": { + "encounter": { + "1": "你比妈妈还厉害?$但是,我也很厉害哦!\n真的!真的哦!" + }, + "victory": { + "1": "啊?我真的输了吗?\n呜呜呜……奶奶!" + } + }, + "winstrates_vicky": { + "encounter": { + "1": "你怎么敢让我的宝贝孙女哭!$看来我得给你一个教训。\n准备好吃点苦头吧!" + }, + "victory": { + "1": "呜啊!这么强!\n我的孙女没在骗人。" + } + }, + "winstrates_vito": { + "encounter": { + "1": "我和我的每位家人都一起训练!$我不会输给任何人了!" + }, + "victory": { + "1": "我比家里的每个人都优秀。\n我从来没有输过……" + } + }, "brock": { "encounter": { "1": "我对岩石属性宝可梦的专精会击败你!来吧!", diff --git a/src/locales/zh_CN/egg.json b/src/locales/zh_CN/egg.json index 5a299368873..f2b95bd40cb 100644 --- a/src/locales/zh_CN/egg.json +++ b/src/locales/zh_CN/egg.json @@ -11,6 +11,7 @@ "gachaTypeLegendary": "传说概率上升", "gachaTypeMove": "稀有概率上升", "gachaTypeShiny": "闪光概率上升", + "eventType": "Mystery Event", "selectMachine": "选择一个机器。", "notEnoughVouchers": "你没有足够的兑换券!", "tooManyEggs": "你的蛋太多啦!", @@ -23,4 +24,4 @@ "moveUPGacha": "蛋招式UP!", "shinyUPGacha": "闪光UP!", "legendaryUPGacha": "UP!" -} \ No newline at end of file +} diff --git a/src/locales/zh_CN/modifier-select-ui-handler.json b/src/locales/zh_CN/modifier-select-ui-handler.json index b3475737dfd..24b6e205f10 100644 --- a/src/locales/zh_CN/modifier-select-ui-handler.json +++ b/src/locales/zh_CN/modifier-select-ui-handler.json @@ -8,5 +8,7 @@ "lockRaritiesDesc": "在刷新时锁定道具稀有度(影响刷新费用)", "checkTeamDesc": "检查队伍或使用形态改变道具", "rerollCost": "₽{{formattedMoney}}", - "itemCost": "₽{{formattedMoney}}" -} \ No newline at end of file + "itemCost": "₽{{formattedMoney}}", + "continueNextWaveButton": "Continue", + "continueNextWaveDescription": "Continue to the next wave" +} diff --git a/src/locales/zh_CN/modifier-type.json b/src/locales/zh_CN/modifier-type.json index 981f26a1603..61c36078369 100644 --- a/src/locales/zh_CN/modifier-type.json +++ b/src/locales/zh_CN/modifier-type.json @@ -68,6 +68,20 @@ "BaseStatBoosterModifierType": { "description": "增加10%持有者的{{stat}},\n个体值越高堆叠上限越高。" }, + "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": "所有宝可梦完全回复HP。" }, @@ -401,7 +415,13 @@ "ENEMY_FUSED_CHANCE": { "name": "融合硬币", "description": "增加1%野生融合宝可梦出现概率。" - } + }, + + "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": { diff --git a/src/locales/zh_CN/move-trigger.json b/src/locales/zh_CN/move-trigger.json index 436f1805c4e..60de3591915 100644 --- a/src/locales/zh_CN/move-trigger.json +++ b/src/locales/zh_CN/move-trigger.json @@ -65,6 +65,7 @@ "suppressAbilities": "{{pokemonName}}的特性\n变得无效了!", "revivalBlessing": "{{pokemonName}}复活了!", "swapArenaTags": "{{pokemonName}}\n交换了双方的场地效果!", + "chillyReception": "{{pokemonName}}\n说出了冷笑话!", "exposedMove": "{{pokemonName}}识破了\n{{targetPokemonName}}的原型!", "safeguard": "{{targetName}}\n正受到神秘之幕的保护!", "afterYou": "{{pokemonName}}\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/mystery-encounter-messages.json b/src/locales/zh_CN/mystery-encounter-messages.json new file mode 100644 index 00000000000..2d6a435adb9 --- /dev/null +++ b/src/locales/zh_CN/mystery-encounter-messages.json @@ -0,0 +1,7 @@ +{ + "paid_money": "你支付了 ₽{{amount, number}}。", + "receive_money": "你获得了 ₽{{amount, number}}!", + "affects_pokedex": "影响图鉴数据", + "cancel_option": "回到事件选项", + "view_party_button": "查看队伍" +} diff --git a/src/locales/zh_CN/mystery-encounters/a-trainers-test-dialogue.json b/src/locales/zh_CN/mystery-encounters/a-trainers-test-dialogue.json new file mode 100644 index 00000000000..c99ccba5f48 --- /dev/null +++ b/src/locales/zh_CN/mystery-encounters/a-trainers-test-dialogue.json @@ -0,0 +1,47 @@ +{ + "intro": "一个十分强大的训练家向你走来…", + "buck": { + "intro_dialogue": "哟,训练师!我叫麦可。$我有一个非常棒的提议,\n适合像你这样强大的训练师!$我随身带着两颗稀有的宝可梦蛋,\n但我想让别人来照顾一颗。$如果你能向我证明你作为训练师的实力,\n我会给你更稀有的蛋!", + "accept": "芜!!我燃起来了!", + "decline": "害,看起来你的\n队伍状态不佳。$来,让我来帮忙。" + }, + "cheryl": { + "intro_dialogue": "你好,我是芽米。$我有一个特别有趣的请求,\n因为看起来你是一位强大的训练师。$我随身带着两颗稀有的宝可梦蛋,\n但我想让别人照顾一颗。$如果你能向我证明你作为训练师的实力,\n我会给你更稀有的蛋!", + "accept": "我希望你准备好了!", + "decline": "我明白,看起来你的团队\n目前状态不佳。$来,让我来帮忙。" + }, + "marley": { + "intro_dialogue": "……@d{64} 我是米依。$我有一个提议给你...$我随身带着两颗宝可梦蛋,\n但我想让别人照顾一颗。$如果你比我强,\n我会给你更稀有的蛋。", + "accept": "……我明白了。", + "decline": "... 我明白了。$你的宝可梦看起来伤得很重...\n让我来帮忙。" + }, + "mira": { + "intro_dialogue": "嗨!我是麦儿!$麦儿有一个请求\n需要像你一样强大的训练师!$麦儿有两颗稀有的宝可梦蛋,\n但麦儿希望别人拿走一颗!$如果你向麦儿展示你的实力,\n麦儿会给你更稀有的蛋!", + "accept": "你会和麦儿对战吗?\n耶!", + "decline": "噢……不战斗?\n没关系!$来,麦尔会治疗你的宝可梦!" + }, + "riley": { + "intro_dialogue": "我是亚玄。$我有一个有点怪的提议,\n毕竟面前是像你这样的强大训练师。$我随身带着两颗稀有的宝可梦蛋,\n但我想把其中一颗送给另一位训练师。$如果你能向我证明你的实力,\n我会给你更稀有的蛋!", + "accept": "你那表情……\n我们开始吧。", + "decline": "我明白,毕竟你的队伍看起来很疲惫。$来,让我来帮你。" + }, + "title": "训练家试炼", + "description": "不管你选择什么,此训练家都会给你一个蛋。\n然而,如果你能击败他,就会获得一个更稀有的蛋。", + "query": "你要怎么做?", + "option": { + "1": { + "label": "接受挑战", + "tooltip": "(-)艰难的战斗\n(+)获得一个@[TOOLTIP_TITLE]{非常稀有的蛋}" + }, + "2": { + "label": "拒绝挑战", + "tooltip": "(+)治疗全队\n(+)获得一个@[TOOLTIP_TITLE]{蛋}" + } + }, + "eggTypes": { + "rare": "稀有的蛋", + "epic": "史诗的蛋", + "legendary": "传说的蛋" + }, + "outro": "{{statTrainerName}}给了你{{eggType}}!" +} diff --git a/src/locales/zh_CN/mystery-encounters/absolute-avarice-dialogue.json b/src/locales/zh_CN/mystery-encounters/absolute-avarice-dialogue.json new file mode 100644 index 00000000000..6637d3ff7ed --- /dev/null +++ b/src/locales/zh_CN/mystery-encounters/absolute-avarice-dialogue.json @@ -0,0 +1,25 @@ +{ + "intro": "一个{{greedentName}}埋伏了你\\n并且偷走了你队伍携带的树果!", + "title": "贪得无厌", + "description": "{{greedentName}}让你措手不及,现在你所有的树果都被夺走了!\n\n{{greedentName}}正要吃掉它们时,突然停下来饶有兴趣地看着你。", + "query": "你要怎么做?", + "option": { + "1": { + "label": "必须战斗", + "tooltip": "(-)艰难的战斗\n(+)树果宝藏的奖励", + "selected": "{{greedentName}}塞满脸颊\n并准备战斗!", + "boss_enraged": "{{greedentName}}对食物的热衷让它非常愤怒!", + "food_stash": "{{greedentName}}似乎守护着一大堆食物!$@s{item_fanfare}你队伍中的每个神奇宝贝都会获得{{foodReward}}!" + }, + "2": { + "label": "讲点道理", + "tooltip": "(+)拿回一些失去的树果", + "selected": "你的恳求引起了{{greedentName}}的共鸣。$它不会把你所有的果子都归还给你,但仍然会扔给你一些。" + }, + "3": { + "label": "让它拿吧", + "tooltip": "(-)失去所有树果\n(?){{greedentName}}会很感激你", + "selected": "{{greedentName}}一瞬间就吃掉了整个树果堆!$它拍拍肚子,\n感激地看着你。$也许你可以在冒险中喂它\n更多浆果……$@s{level_up_fanfare}{{greedentName}}想加入你的队伍!" + } + } +} diff --git a/src/locales/zh_CN/mystery-encounters/an-offer-you-cant-refuse-dialogue.json b/src/locales/zh_CN/mystery-encounters/an-offer-you-cant-refuse-dialogue.json new file mode 100644 index 00000000000..d920e4e52d1 --- /dev/null +++ b/src/locales/zh_CN/mystery-encounters/an-offer-you-cant-refuse-dialogue.json @@ -0,0 +1,26 @@ +{ + "intro": "一名看上去就很有钱的小男孩拦住了你。", + "speaker": "富家子弟", + "intro_dialogue": "向你问好。$你的{{strongestPokemon}}看起来举世无双,我挪不开我的眼睛了!\n$我一直也想要一只那样的宝可梦!$你的报酬不会少的,\n这个老家伙也可以送给你!", + "title": "难以拒绝的买卖", + "description": "对方愿意为了你的{{strongestPokemon}}出一个@[TOOLTIP_TITLE]{闪光护符}和{{price, money}}。\n\n这交易非常值得,但是你真的舍得放弃如此强大的伙伴吗?", + "query": "你要怎么做?", + "option": { + "1": { + "label": "接受交易", + "tooltip": "(-)失去{{strongestPokemon}}\n(+)获得一个@[TOOLTIP_TITLE]{闪光护符}\n(+)获得{{price, money}}", + "selected": "太棒了!@d{32}过来吧,{{strongestPokemon}}!$现在是时候向游艇俱乐部的那帮小子炫耀一番了!$他们肯定得酸爆了!" + }, + "2": { + "label": "敲诈他", + "tooltip": "(+){{option2PrimaryName}}使用{{moveOrAbility}}\n(+)获得{{price, money}}", + "tooltip_disabled": "你的宝可梦需要具备某些特性或招式才能选择此项", + "selected": "我的天,你抢劫我。{{liepardName}},走了!$我要找律师告你!" + }, + "3": { + "label": "离开", + "tooltip": "(-)无奖励", + "selected": "真够跌份儿的……$啊,好吧。那我们回游艇吧,{{liepardName}}。" + } + } +} diff --git a/src/locales/zh_CN/mystery-encounters/berries-abound-dialogue.json b/src/locales/zh_CN/mystery-encounters/berries-abound-dialogue.json new file mode 100644 index 00000000000..9e2240e2aea --- /dev/null +++ b/src/locales/zh_CN/mystery-encounters/berries-abound-dialogue.json @@ -0,0 +1,26 @@ +{ + "intro": "宝可梦旁边\n有着巨大的树果丛!", + "title": "硕“果”累累", + "description": "似乎有强大的宝可梦守护着树果丛。\n最简单的方法是战斗,但它的实力不容小觑。\n也许一只速度很快的宝可梦可以乘机顺走一些树果?", + "query": "你要怎么做?", + "berries": "树果!!", + "option": { + "1": { + "label": "与它战斗", + "tooltip": "(-)艰难的战斗\n(+)获得树果", + "selected": "你无所畏惧地\n走向了宝可梦们。" + }, + "2": { + "label": "冲向果丛", + "tooltip": "(-){{fastestPokemon}}试试速度\n(+)获得树果", + "selected": "你的{{fastestPokemon}}冲向了果丛!$它在{{enemyPokemon}}做出反应之前成功抓住了{{numBerries}}!$你带着你的战利品迅速撤退。", + "selected_bad": "你的{{fastestPokemon}}向浆果丛奔去!$哦不!{{enemyPokemon}}速度更快,挡住了它的去路!", + "boss_enraged": "对方的{{enemyPokemon}}变得非常愤怒!" + }, + "3": { + "label": "离开", + "tooltip": "(-)无奖励", + "selected": "你让那个宝可梦留着属于它的果子\n继续上路了。" + } + } +} diff --git a/src/locales/zh_CN/mystery-encounters/bug-type-superfan-dialogue.json b/src/locales/zh_CN/mystery-encounters/bug-type-superfan-dialogue.json new file mode 100644 index 00000000000..c63c74dc248 --- /dev/null +++ b/src/locales/zh_CN/mystery-encounters/bug-type-superfan-dialogue.json @@ -0,0 +1,40 @@ +{ + "intro": "一个特殊的训练家带着各种捕虫设备挡住了你的去路!", + "intro_dialogue": "嘿,训练家!我正在寻找最稀有的虫系宝可梦!\n你一定也很喜欢虫系宝可梦吧?\n大家都喜欢虫系宝可梦!", + "title": "超级虫系粉丝", + "speaker": "超级虫系粉丝", + "description": "训练家喋喋不休,根本不听你回应…\n\n似乎唯一的办法就是引起他的注意!", + "query": "你要怎么做?", + "option": { + "1": { + "label": "请求对战", + "tooltip": "(-)挑战对战\n(+)习得虫系招式", + "selected": "对战,呃?\n我的虫系宝可梦早就准备好了。" + }, + "2": { + "label": "展示虫宝可梦", + "tooltip": "(+)获得一件道具", + "disabled_tooltip": "你需要至少携带一只虫系宝可梦", + "selected": "你向训练师展示了你所有的虫系宝可梦", + "selected_0_to_1": "哈?你才有{{numBugTypes}}。$你对我来说真是浪费时间。", + "selected_2_to_3": "嘿,你有{{numBugTypes}}!\n不错。$来,这能帮你多抓点虫虫。", + "selected_4_to_5": "啥?你有{{numBugTypes}}!!\n牛啊!$你还没有达到我的水平,\n但我可以从你身上看到自己的影子!$拿着,我的小徒弟!", + "selected_6": "哇哦!{{numBugTypes}}种虫虫!\n$您一定和我一样喜欢虫属性!$来,把这个当作我们友谊的象征吧!" + }, + "3": { + "label": "赠送虫系道具", + "tooltip": "(-)给予训练师{{requiredBugItems}}\n(+)得到一件礼物", + "disabled_tooltip": "你需要一件{{requiredBugItems}}才能选择", + "select_prompt": "选择一件道具", + "invalid_selection": "宝可梦未持有该类道具", + "selected": "你递交了{{selectedItem}}.", + "selected_dialogue": "哇!一件{{selectedItem}},给我的?\n你可以啊小子!$为了表达我的谢意,\n我希望你能拥有这份特别的礼物!$这可是我的传家宝啊,现在我希望你能拥有它!" + } + }, + "battle_won": "你利用知识和技巧完美地痛击了我们的弱点!\n作为这一课的学费,请让我教你一个虫系招式!", + "teach_move_prompt": "选择要教给宝可梦的招式", + "confirm_no_teach": "你确定你不需要学习任何招式么?", + "outro": "我看到你的未来将伴随伟大的虫系宝可梦!\n愿我们再次相遇!虫虫出动!", + "numBugTypes_one": "{{count}}种虫系", + "numBugTypes_other": "{{count}}种虫系" +} diff --git a/src/locales/zh_CN/mystery-encounters/clowning-around-dialogue.json b/src/locales/zh_CN/mystery-encounters/clowning-around-dialogue.json new file mode 100644 index 00000000000..5eb739e3b18 --- /dev/null +++ b/src/locales/zh_CN/mystery-encounters/clowning-around-dialogue.json @@ -0,0 +1,34 @@ +{ + "intro": "是个…@d{64}小丑?", + "speaker": "小丑", + "intro_dialogue": "小丑踩高跷,战斗要来到!\n吵吵闹闹,将你打倒!", + "title": "小丑闹剧", + "description": "有点不对劲,这小丑想要挑衅你,\n目的是什么呢?\n尤其是那{{blacephalonName}}看起来非常奇怪,就好像它有着@[TOOLTIP_TITLE]{与众不同的属性和特性。}", + "query": "你要怎么做?", + "option": { + "1": { + "label": "与小丑战斗", + "tooltip": "(-)奇妙的战斗\n(?)影响宝可梦特性", + "selected": "你可怜的宝可梦正准备进行可悲的表演!", + "apply_ability_dialogue": "精彩的演出!\n出色的指挥与夺目的招式相得益彰!", + "apply_ability_message": "小丑将永久地将你的一只宝可梦的特性交换为{{ability}}!", + "ability_prompt": "你想永久地教会一只宝可梦{{ability}}吗?", + "ability_gained": "@s{level_up_fanfare}{{chosenPokemon}}获得了{{ability}}特性!" + }, + "2": { + "label": "保持冷静", + "tooltip": "(-)让小丑失望\n(?)影响宝可梦道具", + "selected": "小懦夫,竟敢拒绝我。\n尝尝我的厉害!", + "selected_2": "小丑的{{blacephalonName}}使用了戏法!\n你的{{switchPokemon}}的所有道具都被随机变换了!", + "selected_3": "傻了吧,我的把戏毫无破绽!" + }, + "3": { + "label": "骂回去", + "tooltip": "(-)让小丑失望\n(?)影响宝可梦属性", + "selected": "小懦夫,竟敢拒绝我。\n尝尝我的厉害!", + "selected_2": "小丑的{{blacephalonName}}使用了一个奇异的招式!\n你队伍的所有属性都被随机变换了!", + "selected_3": "傻了吧,我的把戏毫无破绽!" + } + }, + "outro": "小丑和他的帮手\n在一阵烟雾中消失了。" +} diff --git a/src/locales/zh_CN/mystery-encounters/dancing-lessons-dialogue.json b/src/locales/zh_CN/mystery-encounters/dancing-lessons-dialogue.json new file mode 100644 index 00000000000..e1818ce06c1 --- /dev/null +++ b/src/locales/zh_CN/mystery-encounters/dancing-lessons-dialogue.json @@ -0,0 +1,27 @@ +{ + "intro": "一位{{oricorioName}}正孤独起舞,没有舞伴。", + "title": "舞蹈课", + "description": "{{oricorioName}}并不咄咄逼人,反而有些哀伤。\n\n也许他只想找个人一起跳舞…", + "query": "你要怎么做?", + "option": { + "1": { + "label": "战斗", + "tooltip": "(-)艰难的战斗\n(+)获得接力棒", + "selected": "{{oricorioName}}变得急躁,想要采取行动进行自卫!", + "boss_enraged": "{{oricorioName}}的恐惧使它的能力上升了!" + }, + "2": { + "label": "学习它的舞蹈", + "tooltip": "(+)教宝可梦觉醒之舞", + "selected": "您紧盯着{{oricorioName}}的舞蹈表演……$@s{level_up_fanfare}你的{{selectedPokemon}}从{{oricorioName}}那里学到了!" + }, + "3": { + "label": "表演一支舞蹈", + "tooltip": "(-)教{{oricorioName}}一支舞蹈\n(+){{oricorioName}}会喜欢你", + "disabled_tooltip": "你的宝可梦需要习得一种舞蹈动作。", + "select_prompt": "选择一个舞蹈招式", + "selected": "{{oricorioName}}着迷地观看着\n{{selectedPokemon}}展示{{selectedMove}}!$它喜欢这场表演!$@s{level_up_fanfare}{{oricorioName}}想要加入你的队伍!" + } + }, + "invalid_selection": "该宝可梦无法学会跳舞招式" +} diff --git a/src/locales/zh_CN/mystery-encounters/dark-deal-dialogue.json b/src/locales/zh_CN/mystery-encounters/dark-deal-dialogue.json new file mode 100644 index 00000000000..82863084de7 --- /dev/null +++ b/src/locales/zh_CN/mystery-encounters/dark-deal-dialogue.json @@ -0,0 +1,24 @@ + + +{ + "intro": "一个穿着破旧大衣的陌生人\\n挡在你的路上…", + "speaker": "可疑的家伙", + "intro_dialogue": "嘿,你!我一直在研究一种新装置,\n能让宝可梦从原子级重组,变成更强大的形态!\n嘿嘿…@d{64}我只需要一点祭…@d{32}\n我是说,试验品,来证明它的效果。", + "title": "黑暗交易", + "description": "这个诡异的家伙举起一些精灵球。\n我会让你感到物超所值的!\n我只要你队伍里的一只宝可梦,这些强力的精灵球就是你的了,嘿嘿…", + "query": "你要怎么做?", + "option": { + "1": { + "label": "同意", + "tooltip": "(+)5个肉鸽球\n(?)强化随机宝可梦", + "selected_dialogue": "我看看,那只{{pokeName}}挺不错的!$记住,要是啥搞砸了\n我可不负责!@d{32}嘿嘿……", + "selected_message": "男人把五个肉鸽球教给你${{pokeName}}跳进了奇怪的机器$在机器的内部\n光芒四溢,雷声作响!$……@d{96}一个诡异的声音\n从机器中浮现,发出低吼。" + }, + "2": { + "label": "拒绝", + "tooltip": "(-)无奖励", + "selected": "都不帮哥们儿个忙吗?\n害!" + } + }, + "outro": "令人不适的遭遇结束后\n你收拾了一下,继续上路了。" +} diff --git a/src/locales/zh_CN/mystery-encounters/delibirdy-dialogue.json b/src/locales/zh_CN/mystery-encounters/delibirdy-dialogue.json new file mode 100644 index 00000000000..82b114b401b --- /dev/null +++ b/src/locales/zh_CN/mystery-encounters/delibirdy-dialogue.json @@ -0,0 +1,29 @@ + + +{ + "intro": "一伙{{delibirdName}}出现了!", + "title": "信使鸟快递", + "description": "{{delibirdName}}满怀期待地看着你,它们好像想要什么东西。\\n也许给它们一件道具或一些钱能让它们满意?", + "query": "你要给它们什么?", + "invalid_selection": "宝可梦身上并没有那种道具。", + "option": { + "1": { + "label": "给钱", + "tooltip": "(-)给予{{delibirdName}}们{{money, money}}\n(+)获得一件礼物", + "selected": "你向{{delibirdName}}们扔了点钱,\n它们兴奋地互相嚷嚷着。$信使鸟们转向你并高兴地给你一份礼物!" + }, + "2": { + "label": "给吃的", + "tooltip": "(-)给予{{delibirdName}}们树果或复活之种\n(+)获得一件礼物", + "select_prompt": "选择要给予的道具", + "selected": "你向{{delibirdName}}们扔了点{{chosenItem}},\n它们兴奋地互相嚷嚷着。$信使鸟们转向你并高兴地给你一份礼物!" + }, + "3": { + "label": "给道具", + "tooltip": "(-)给予{{delibirdName}}一件道具\n(+)获得一件礼物", + "select_prompt": "选择要给予的道具", + "selected": "你向{{delibirdName}}们扔了点{{chosenItem}},\n它们兴奋地互相嚷嚷着。$信使鸟们转向你并高兴地给你一份礼物!" + } + }, + "outro": "{{delibirdName}}们兴高采烈地摇摆着,走向远方。$多么有趣的小交易!" +} diff --git a/src/locales/zh_CN/mystery-encounters/department-store-sale-dialogue.json b/src/locales/zh_CN/mystery-encounters/department-store-sale-dialogue.json new file mode 100644 index 00000000000..09c23762ea9 --- /dev/null +++ b/src/locales/zh_CN/mystery-encounters/department-store-sale-dialogue.json @@ -0,0 +1,27 @@ +{ + "intro": "一位提着一大堆购物袋的女士。", + "speaker": "大促销", + "intro_dialogue": "你好!你也是来参加促销活动的吗?\n在促销期间,可以领取一张特别优惠券,用于购买免费商品!\n我这有一张多余的优惠券。送你了!", + "title": "百货公司促销", + "description": "到处都是商品!\n好像有4个柜台可以让你用优惠券兑换商品。\n无尽可能!", + "query": "你要去哪里?", + "option": { + "1": { + "label": "技能机柜台", + "tooltip": "(+)技能机商店" + }, + "2": { + "label": "努力药柜台", + "tooltip": "(+)努力药商店" + }, + "3": { + "label": "强化道具柜台", + "tooltip": "(+)强化道具商店" + }, + "4": { + "label": "精灵球柜台", + "tooltip": "(+)精灵球商店" + } + }, + "outro": "真划算!早该去去了。" +} diff --git a/src/locales/zh_CN/mystery-encounters/field-trip-dialogue.json b/src/locales/zh_CN/mystery-encounters/field-trip-dialogue.json new file mode 100644 index 00000000000..5458c4f18bc --- /dev/null +++ b/src/locales/zh_CN/mystery-encounters/field-trip-dialogue.json @@ -0,0 +1,31 @@ +{ + "intro": "是一位老师和几个学生!", + "speaker": "人民教师", + "intro_dialogue": "你好!你能为我的学生们抽出一点时间吗?\n我正在教他们宝可梦的招式!\n最好能有个示范来展示一下。$你介意向我们展示一下\n你的宝可梦所使用的招式吗?", + "title": "外出实践", + "description": "一位老师请求进行宝可梦招式演示。\n根据你选择的招式,她可能会给你\n一些有用的东西作为报酬。", + "query": "你要展示哪种类型的招式?", + "option": { + "1": { + "label": "物理招式", + "tooltip": "(+)物理类奖励" + }, + "2": { + "label": "特殊招式", + "tooltip": "(+)特殊类奖励" + }, + "3": { + "label": "变化招式", + "tooltip": "(+)变化类奖励" + }, + "selected": "{{pokeName}}完美地展示了{{move}}!" + }, + "second_option_prompt": "选择一个宝可梦的招式。", + "incorrect": "...$那好像不是{{moveCategory}}招式!\n对不起,我不打算给你任何东西了。$来吧孩子们,我们去别的地方\n找找更好的示范。", + "incorrect_exp": "看来你学到了宝贵的一课?$你的宝可梦也获得了一些经验。", + "correct": "感谢您的好心!\n这些东西希望对你有用!", + "correct_exp": "{{pokeName}}同样也获得了一些经验!", + "status": "变化类", + "physical": "物理类", + "special": "特殊类" +} diff --git a/src/locales/zh_CN/mystery-encounters/fiery-fallout-dialogue.json b/src/locales/zh_CN/mystery-encounters/fiery-fallout-dialogue.json new file mode 100644 index 00000000000..7113ecab2a1 --- /dev/null +++ b/src/locales/zh_CN/mystery-encounters/fiery-fallout-dialogue.json @@ -0,0 +1,26 @@ +{ + "intro": "你遭遇了一场烟灰风暴!", + "title": "炽热余波", + "description": "呼啸的火山灰和余烬使能见度几乎为零。\n看来可能有什么…东西造成了这种状况。\n但到底会是什么呢?", + "query": "你要怎么做?", + "option": { + "1": { + "label": "找到源头", + "tooltip": "(?)寻找源头\n(-)困难的战斗", + "selected": "你冲破风暴,发现两只{{volcaronaName}}正在相互求偶!$它们很不情愿被打扰,并发起了攻击!" + }, + "2": { + "label": "原地趴下", + "tooltip": "(-)忍受天气的影响", + "selected": "当您努力寻找庇护所时,天气影响会造成严重伤害!$您的队伍受到最大生命值20%的伤害!", + "target_burned": "你的{{burnedPokemon}}还被灼伤了!" + }, + "3": { + "label": "让火系宝可梦帮忙", + "tooltip": "(+)解决困境\n(+)获得木炭", + "disabled_tooltip": "你至少需要两只火系宝可梦来选择", + "selected": "你的{{option3PrimaryName}}和{{option3SecondaryName}}引导你发现了两只{{volcaronaName}}正在求偶起舞的地方!$幸运的是,你的宝可梦能够让它们平静下来,\n然后它们就平安离开了。" + } + }, + "found_charcoal": "天气转晴后,你的{{leadPokemon}}沾上了什么东西。\\n@s{item_fanfare}{{leadPokemon}}获得了煤炭!" +} diff --git a/src/locales/zh_CN/mystery-encounters/fight-or-flight-dialogue.json b/src/locales/zh_CN/mystery-encounters/fight-or-flight-dialogue.json new file mode 100644 index 00000000000..e60a73e885f --- /dev/null +++ b/src/locales/zh_CN/mystery-encounters/fight-or-flight-dialogue.json @@ -0,0 +1,25 @@ +{ + "intro": "宝可梦附近的地上\n有闪闪发光的东西!", + "title": "战逃反应", + "description": "似乎有强大的宝可梦守护着这一道具。\n战斗是最直接的方法,但是它的实力似乎不容小觑。\n如果你有合适的宝可梦,似乎也可以直接偷走它。", + "query": "你要怎么做?", + "option": { + "1": { + "label": "战斗", + "tooltip": "(-)困难的战斗\n(+)新道具", + "selected": "你无所畏惧的走向宝可梦。", + "stat_boost": "{{enemyPokemon}}的潜在实力提升了它的一项能力!" + }, + "2": { + "label": "窃取物品", + "disabled_tooltip": "你的宝可梦需要习得特定的招式来选择。", + "tooltip": "(+){{option2PrimaryName}}使用{{option2PrimaryMove}}", + "selected": ".@d{32}.@d{32}.@d{32}$你的{{option2PrimaryName}}愿意帮忙,并使用了{{option2PrimaryMove}}!$你偷走了道具!" + }, + "3": { + "label": "离开", + "tooltip": "(-)无奖励", + "selected": "你决定把宝贝留给这只宝可梦\n并继续上路了。" + } + } +} diff --git a/src/locales/zh_CN/mystery-encounters/fun-and-games-dialogue.json b/src/locales/zh_CN/mystery-encounters/fun-and-games-dialogue.json new file mode 100644 index 00000000000..d732a2444bd --- /dev/null +++ b/src/locales/zh_CN/mystery-encounters/fun-and-games-dialogue.json @@ -0,0 +1,30 @@ +{ + "intro_dialogue": "来看看吧,大伙!\n在这全新的{{wobbuffetName}}捶捶乐上试试运气!\n", + "speaker": "艺人", + "title": "快乐游戏!", + "description": "你遇到了一个有奖游戏活动!\n你有 @[TOOLTIP_TITLE]{3 回合}的时间来在\n@[TOOLTIP_TITLE]{不打倒}{{wobbuffetName}}的情况下将它尽可能的打到@[TOOLTIP_TITLE]{1血}\n这样,它就能通过反作用力在敲钟器上打出超高数字!\n但要小心!如果你不小心打倒了{{wobbuffetName}},你就得付医疗费!", + "query": "要试试么?", + "option": { + "1": { + "label": "游玩游戏", + "tooltip": "(-)支付{{option1Money, money}}\n(+)玩{{wobbuffetName}}重拳出击!", + "selected": "该试试你的运气了" + }, + "2": { + "label": "离开", + "tooltip": "(-)无奖励", + "selected": "你匆匆上路,心里却有一丝遗憾。" + } + }, + "ko": "哦不!{{wobbuffetName}}被打倒了!\n你不仅输了,还要赔医疗费…", + "charging_continue": "果然翁正在准备反击!", + "turn_remaining_3": "还有3个回合!", + "turn_remaining_2": "还有2个回合!", + "turn_remaining_1": "还有1个回合!", + "end_game": "时间到!{{wobbuffetName}}使出了双倍奉还!\n然后…@d{16}…@d{16}…@d{16}", + "best_result": "{{wobbuffetName}}猛击了按钮,打穿了进度条!\n你赢得了大奖!", + "great_result": "{{wobbuffetName}}猛击了按钮,就快碰到到钟了!\n就差一点!你获得了二等奖!", + "good_result": "{{wobbuffetName}}猛击了按钮,填满了一半进度条!\n你获得了三等奖!", + "bad_result": "{{wobbuffetName}}软绵绵的打向按钮,几乎没什么动静…\n你什么奖都没有!", + "outro": "真是有趣的小游戏!" +} diff --git a/src/locales/zh_CN/mystery-encounters/global-trade-system-dialogue.json b/src/locales/zh_CN/mystery-encounters/global-trade-system-dialogue.json new file mode 100644 index 00000000000..edc83ed5f08 --- /dev/null +++ b/src/locales/zh_CN/mystery-encounters/global-trade-system-dialogue.json @@ -0,0 +1,32 @@ +{ + "intro": "是GTS系统的交互面板!", + "title": "全球交换系统(GTS)", + "description": "啊,GTS!科学的奇迹,\\n你可以与世界各地的任何人联系,与他们交换宝可梦!\\n今天你的交换会有好运吗?", + "query": "你要怎么做?", + "option": { + "1": { + "label": "查找交换请求", + "tooltip": "(+)为一只宝可梦选择交换", + "trade_options_prompt": "选择一只要通过交换接收的神奇宝贝。" + }, + "2": { + "label": "奇迹交换", + "tooltip": "(+)将你的一只宝可梦送至GTS,并获得一只随机宝可梦作为回报" + }, + "3": { + "label": "道具交换", + "trade_options_prompt": "选择要发送的道具", + "invalid_selection": "这只宝可梦没有合法道具可以被交换。", + "tooltip": "(+)将你的一件道具发送到GTS并获得一件随机新道具" + }, + "4": { + "label": "离开", + "tooltip": "(-)无奖励", + "selected": "今日不宜交换!\n你上路了。" + } + }, + "pokemon_trade_selected": "{{tradedPokemon}}将发送给{{tradeTrainerName}}。", + "pokemon_trade_goodbye": "再见,{{tradedPokemon}}!", + "item_trade_selected": "{{chosenItem}}将发送给{{tradeTrainerName}}.$.@d{64}.@d{64}.@d{64}\\n@s{level_up_fanfare}交易成功!$你从{{tradeTrainerName}}那得到了{{itemName}}!", + "trade_received": "@s{evolution_fanfare}{{tradeTrainerName}}已发送{{received}}!" +} diff --git a/src/locales/zh_CN/mystery-encounters/lost-at-sea-dialogue.json b/src/locales/zh_CN/mystery-encounters/lost-at-sea-dialogue.json new file mode 100644 index 00000000000..9d769e99d83 --- /dev/null +++ b/src/locales/zh_CN/mystery-encounters/lost-at-sea-dialogue.json @@ -0,0 +1,28 @@ +{ + "intro": "在大海中漫无目的地游荡,无处可循。", + "title": "迷失大海", + "description": "这片海域波涛汹涌,你的体力即将耗尽。\\n太糟了,有什么办法么?", + "query": "你要怎么做?", + "option": { + "1": { + "label": "{{option1PrimaryName}}可以帮忙", + "label_disabled": "无法{{option1RequiredMove}}", + "tooltip": "(+){{option1PrimaryName}}帮助你\n(+){{option1PrimaryName}}获得经验值", + "tooltip_disabled": "你没有宝可梦能够{{option1RequiredMove}}", + "selected": "{{option1PrimaryName}}游在前面,引导你回到正轨。${{option1PrimaryName}}似乎也在这次需要它的时候变得更强大了!" + }, + "2": { + "label": "{{option2PrimaryName}}可以帮忙", + "label_disabled": "无法{{option2RequiredMove}}", + "tooltip": "(+){{option2PrimaryName}}帮助你\n(+){{option2PrimaryName}}获得经验值", + "tooltip_disabled": "你没有宝可梦能够{{option2RequiredMove}}", + "selected": "{{option2PrimaryName}}飞在你的船前面,引导你回到正轨。在这次需要它的时候,${{option2PrimaryName}}似乎也变得更强大了!" + }, + "3": { + "label": "漫无目的地漂流", + "tooltip": "(-)每一只宝可梦损失{{damagePercentage}}%的HP", + "selected": "你在船上漂浮,漫无目的地航行,直到最后发现了一个你记得的地标。$你和你的宝可梦因整个经历而疲惫不堪。" + } + }, + "outro": "一切又回到正轨了。" +} diff --git a/src/locales/zh_CN/mystery-encounters/mysterious-challengers-dialogue.json b/src/locales/zh_CN/mystery-encounters/mysterious-challengers-dialogue.json new file mode 100644 index 00000000000..574c577127c --- /dev/null +++ b/src/locales/zh_CN/mystery-encounters/mysterious-challengers-dialogue.json @@ -0,0 +1,22 @@ +{ + "intro": "神秘的挑战者出现了!", + "title": "神秘挑战者", + "description": "如果你击败了挑战者,你给他们留下的强烈印象可能会换来奖励。\\n但他们看起来很强,要接受挑战么?", + "query": "你要接受谁的挑战?", + "option": { + "1": { + "label": "谨慎的对手", + "tooltip": "(-)普通的战斗\n(+)道具奖励" + }, + "2": { + "label": "自信的对手", + "tooltip": "(-)困难的战斗\n(+)更好的奖励" + }, + "3": { + "label": "威严的对手", + "tooltip": "(-)艰苦的战斗\n(+)丰厚的奖励" + }, + "selected": "训练师走上前来……" + }, + "outro": "神秘挑战者被击败了!" +} diff --git a/src/locales/zh_CN/mystery-encounters/mysterious-chest-dialogue.json b/src/locales/zh_CN/mystery-encounters/mysterious-chest-dialogue.json new file mode 100644 index 00000000000..2ccba9215ae --- /dev/null +++ b/src/locales/zh_CN/mystery-encounters/mysterious-chest-dialogue.json @@ -0,0 +1,23 @@ +{ + "intro": "你发现了一个…@d{32}宝箱?", + "title": "神秘宝箱", + "description": "一个装饰精美的箱子立在地上。里面一定有好东西…吧?", + "query": "要打开么?", + "option": { + "1": { + "label": "打开", + "tooltip": "@[SUMMARY_BLUE]{(35%) 某种东西}\n@[SUMMARY_GREEN]{({{trapPercent}}%) 一般奖励}\n@[SUMMARY_GREEN]{({{commonPercent}}%) 精美奖励}\n@[SUMMARY_GREEN]{({{roguePercent}}%) 珍稀奖励}\n@[SUMMARY_GREEN]{({{masterPercent}}%) 超凡奖励}", + "selected": "你打开箱子发现......", + "normal": "只是一些普通的工具和物品。", + "good": "一些不错的工具和物品!", + "great": "一些很稀有的工具和物品!!", + "amazing": "哇!金色传说!", + "bad": "哦不!@d{32}\n这个箱子实际上是一个伪装的{{gimmighoul Name}}!$你的{{pokeName}}跳到了你面前\n但为了保护你被打倒了!" + }, + "2": { + "label": "有诈,走了", + "tooltip": "(-)无奖励", + "selected": "你匆匆上路,心里却有一丝遗憾。" + } + } +} diff --git a/src/locales/zh_CN/mystery-encounters/part-timer-dialogue.json b/src/locales/zh_CN/mystery-encounters/part-timer-dialogue.json new file mode 100644 index 00000000000..437a34ecd1a --- /dev/null +++ b/src/locales/zh_CN/mystery-encounters/part-timer-dialogue.json @@ -0,0 +1,31 @@ +{ + "intro": "一个忙碌的工人向你招手。", + "speaker": "工人", + "intro_dialogue": "你看起来有不少能干的宝可梦!\n如果你愿意帮忙干点活,我们会付钱的!", + "title": "兼职", + "description": "似乎有不少活要干。根据你宝可梦对任务的适应度,\n你能获得或多或少的报酬。", + "query": "你要干什么活?", + "invalid_selection": "宝可梦必须足够健康", + "option": { + "1": { + "label": "外卖", + "tooltip": "(-)宝可梦使用速度\n(+)赚取@[MONEY]{Money}", + "selected": "您的{{selectedPokemon}}轮班为客户配送订单。" + }, + "2": { + "label": "仓管", + "tooltip": "(-)宝可梦使用力量和耐力\n(+)赚取@[MONEY]{Money}", + "selected": "你的{{selectedPokemon}}轮班工作,在仓库内搬运物品。" + }, + "3": { + "label": "销售", + "tooltip": "(-)你的{{option3PrimaryName}}使用{{option3PrimaryMove}}\n(+)赚取@[MONEY]{Money}", + "disabled_tooltip": "你的宝可梦需要习得适合这份工作的招式", + "selected": "你的{{option3PrimaryName}}花费一天时间使用{{option3PrimaryMove}}来吸引客户!" + } + }, + "job_complete_good": "谢谢你的帮助!\n你的{{selectedPokemon}}帮了大忙了!这是你的日结。", + "job_complete_bad": "你的{{selectedPokemon}}也帮了不少忙!这是你的日结。", + "pokemon_tired": "你的{{selectedPokemon}}疲惫不堪!\n它所有招式的PP值只剩2点!", + "outro": "下次还来帮忙啊!" +} diff --git a/src/locales/zh_CN/mystery-encounters/safari-zone-dialogue.json b/src/locales/zh_CN/mystery-encounters/safari-zone-dialogue.json new file mode 100644 index 00000000000..b8a17aaccf4 --- /dev/null +++ b/src/locales/zh_CN/mystery-encounters/safari-zone-dialogue.json @@ -0,0 +1,46 @@ +{ + "intro": "到狩猎地带了!", + "title": "狩猎地带", + "description": "这里有各种稀有和特殊的宝可梦!\n如果选择进入,你将有3次机会遇到野生宝可梦,供你捕获。\n但注意了,它们可能会在你捕捉到之前逃走!", + "query": "要进去试试吗?", + "option": { + "1": { + "label": "进入", + "tooltip": "(-)支付{{option1Money, money}}\n@[SUMMARY_GREEN]{(?)狩猎地带}", + "selected": "该试试运气了!" + }, + "2": { + "label": "离开", + "tooltip": "(-)无奖励", + "selected": "你匆匆上路,心里却有一丝遗憾。" + } + }, + "safari": { + "1": { + "label": "投掷精灵球", + "tooltip": "(+)投掷一颗精灵球", + "selected": "你扔出了精灵球!" + }, + "2": { + "label": "投掷诱饵", + "tooltip": "(+)增加捕获率\n(-)有概率增加逃跑率", + "selected": "你扔了些诱饵!" + }, + "3": { + "label": "投掷泥巴", + "tooltip": "(+)降低逃跑率\n(-)有概率降低逃跑率", + "selected": "你扔了些泥巴!" + }, + "4": { + "label": "逃跑", + "tooltip": "(?)逃离宝可梦" + }, + "watching": "{{pokemonName}}正在凝视你!", + "eating": "{{pokemonName}}正在进食!", + "busy_eating": "{{pokemonName}}忙着进食!", + "angry": "{{pokemonName}}发怒了!", + "beside_itself_angry": "{{pokemonName}}愤怒至极!", + "remaining_count": "还有{{remainingCount}}只宝可梦!" + }, + "outro": "真是有趣的经历!" +} diff --git a/src/locales/zh_CN/mystery-encounters/shady-vitamin-dealer-dialogue.json b/src/locales/zh_CN/mystery-encounters/shady-vitamin-dealer-dialogue.json new file mode 100644 index 00000000000..570035d8670 --- /dev/null +++ b/src/locales/zh_CN/mystery-encounters/shady-vitamin-dealer-dialogue.json @@ -0,0 +1,27 @@ +{ + "intro": "一个穿着深色大衣的人向你走来。", + "speaker": "可疑的销售", + "intro_dialogue": ".@d{16}.@d{16}.@d{16}\n我有货,要来点么?\n当然,你的宝可梦得顶得住。", + "title": "营养饮料商人", + "description": "男子打开外套,露出一些宝可梦营养饮料。\n他报出的数字非常划算。有点太划算了…\n他提供两种套餐供你选择。", + "query": "你要选哪一种?", + "invalid_selection": "宝可梦必须足够健康", + "option": { + "1": { + "label": "经济套餐", + "tooltip": "(-)支付{{option1Money, money}}\n(-)副作用?\n(+)选择的宝可梦获得两份随机努力药" + }, + "2": { + "label": "高级套餐", + "tooltip": "(-)支付{{option2Money, money}}\n(+)选择的宝可梦获得两份随机努力药" + }, + "3": { + "label": "离开", + "tooltip": "(-)无奖励", + "selected": "呵呵,没想到你竟然是个懦夫。" + }, + "selected": "那人递给你两瓶东西后\n然后很快就消失了。${{selected Pokemon}}获得了{{boost}}和{{boost 2}}提升!" + }, + "cheap_side_effects": "但是药物有一些副作用!$你的{{selectedPokemon}}受到一些伤害,\n并且它的性格变为{{newNature}}!", + "no_bad_effects": "看来药物完全没有副作用!" +} diff --git a/src/locales/zh_CN/mystery-encounters/slumbering-snorlax-dialogue.json b/src/locales/zh_CN/mystery-encounters/slumbering-snorlax-dialogue.json new file mode 100644 index 00000000000..a9dda26cd2d --- /dev/null +++ b/src/locales/zh_CN/mystery-encounters/slumbering-snorlax-dialogue.json @@ -0,0 +1,25 @@ +{ + "intro": "当你走在一条狭窄的小路上时,\n一个高耸的身影挡住了你的去路。\n当你走近时,你看到了一个{{snorlaxName}}正在酣睡。\n似乎没有办法绕过它。", + "title": "沉睡的{{snorlaxName}}", + "description": "你可以攻击它来让它醒来,要么就等它睡醒。\n谁知道要等多久呢?", + "query": "你要怎么做?", + "option": { + "1": { + "label": "抡醒", + "tooltip": "(-)与沉睡的{{snorlaxName}}战斗\n(+)特别的奖励", + "selected": "你无所畏惧地\n走向了宝可梦们。" + }, + "2": { + "label": "干等", + "tooltip": "(-)漫长的等待\n(+)回复队伍", + "selected": ".@d{32}.@d{32}.@d{32}$您等待了好一段时间,但是\n{{snorlaxName}}的哈欠让你的队伍昏昏欲睡…………", + "rest_result": "当你们醒来时,{{snorlax Name}}已无处可寻。\n但你所有的宝可梦都痊愈了!" + }, + "3": { + "label": "偷摸", + "tooltip": "(+){{option3PrimaryName}}使用{{option3PrimaryMove}}\n(+)特别的奖励", + "disabled_tooltip": "宝可梦需要习得特别的招式来选择", + "selected": "你的{{option3PrimaryName}}使用{{option3PrimaryMove}}!$@s{item_fanfare}它从熟睡的\n{{snorlaxName}}身上偷走了吃剩的东西,简直像个怪盗!" + } + } +} diff --git a/src/locales/zh_CN/mystery-encounters/teleporting-hijinks-dialogue.json b/src/locales/zh_CN/mystery-encounters/teleporting-hijinks-dialogue.json new file mode 100644 index 00000000000..5b4fbf614d8 --- /dev/null +++ b/src/locales/zh_CN/mystery-encounters/teleporting-hijinks-dialogue.json @@ -0,0 +1,27 @@ +{ + "intro": "一台奇怪的机器,发出呜呜的噪音…", + "title": "瞬移把戏", + "description": "机器上有一块牌子,上面写着:\n“使用时,先投币,然后走入胶囊。”\n\n也许它可以将你传送至某处…", + "query": "你要怎么做?", + "option": { + "1": { + "label": "投币", + "tooltip": "(-)支付{{price, money}}\n(?)传送至随机地图", + "selected": "你投入一些钱,胶囊就打开了。\n你走了进去……" + }, + "2": { + "label": "宝可梦帮忙", + "tooltip": "(-){{option2PrimaryName}}帮忙\n(+){{option2PrimaryName}}获得经验\n(?)传送至随机地图", + "disabled_tooltip": "你需要一只钢系或电系宝可梦来选择此项", + "selected": "{{option2PrimaryName}}的类型允许你绕过机器的付费装置!$胶囊打开,你走进去……" + }, + "3": { + "label": "检查机器", + "tooltip": "(-)战斗", + "selected": "你被闪烁的灯光和机器发出的奇怪声音吸引住了……$你甚至没有注意到一只野生的\n宝可梦偷偷接近并偷袭了你!" + } + }, + "transport": "机器剧烈震动,发出各种奇怪的噪音!\n刚启动没多久,就又一次安静了下来。", + "attacked": "你走出来,到了一个全新的区域,惊动了一只野生宝可梦。\n野生宝可梦发动了攻击!", + "boss_enraged": "敌对的{{enemyPokemon}}被激怒了!" +} diff --git a/src/locales/zh_CN/mystery-encounters/the-expert-pokemon-breeder-dialogue.json b/src/locales/zh_CN/mystery-encounters/the-expert-pokemon-breeder-dialogue.json new file mode 100644 index 00000000000..4ab42a61264 --- /dev/null +++ b/src/locales/zh_CN/mystery-encounters/the-expert-pokemon-breeder-dialogue.json @@ -0,0 +1,33 @@ +{ + "intro": "这是一位携带大量神奇宝贝蛋的训练师!", + "intro_dialogue": "嘿,训练师!$看起来你的一些\n宝可梦感觉有点沮丧。$为什么不和我打一场来让它们兴奋一下呢?", + "title": "专业饲养员", + "description": "你被发起了一场@[TOOLTIP_TITLE]{只能使用一只宝可梦}的挑战。\n这可能很难,但它肯定会\n加深你与所选神奇宝贝之间的联系!\n如果你赢了,饲养员还会给你一些@[TOOLTIP_TITLE]{蛋}!", + "query": "Who will you battle with?", + "cleffa_1_nickname": "王牌", + "cleffa_2_nickname": "皮氪稀", + "cleffa_3_nickname": "帝王{{speciesName}}", + "option": { + "1": { + "label": "{{pokemon1Name}}", + "tooltip_base": "(-)艰难的战斗\n(+)获得与{{pokemon1Name}}的友谊" + }, + "2": { + "label": "{{pokemon2Name}}", + "tooltip_base": "(-)艰难的战斗\n(+)获得与{{pokemon2Name}}的友谊" + }, + "3": { + "label": "{{pokemon3Name}}", + "tooltip_base": "(-)艰难的战斗\n(+)获得与{{pokemon3Name}}的友谊" + }, + "selected": "我们开始吧!" + }, + "outro": "看看你的 {{chosenPokemon}}现在有多开心!$给,这些你也拿着。", + "outro_failed": "太可惜了……$看起来要让你的宝可梦信任你\n你的路还长着呢。", + + "gained_eggs": "@s{item_fanfare}你获得了{{numEggs}}!", + "eggs_tooltip": "\n(+)获得{{eggs}}", + "numEggs_one": "{{count}}个{{rarity}}蛋", + "numEggs_other": "{{count}}个{{rarity}}蛋" + +} diff --git a/src/locales/zh_CN/mystery-encounters/the-pokemon-salesman-dialogue.json b/src/locales/zh_CN/mystery-encounters/the-pokemon-salesman-dialogue.json new file mode 100644 index 00000000000..1f8c91cf4ca --- /dev/null +++ b/src/locales/zh_CN/mystery-encounters/the-pokemon-salesman-dialogue.json @@ -0,0 +1,23 @@ +{ + "intro": "一位精神抖擞的老人向你走来。", + "speaker": "绅士", + "intro_dialogue": "你好啊!我有一份专门为你准备的好买卖!", + "title": "宝可梦销售员", + "description": "“这个{{purchasePokemon}}非常独特,拥有该物种通常不具备的特性!只需{{price, money}}就让你拥有这个与众不同的{{purchasePokemon}}!”\n\n“你怎么说?”", + "description_shiny": "“这个{{purchasePokemon}}非常独特,拥有该物种非常不寻常的颜色!只需{{price, money}}就让你拥有这个与众不同的{{purchasePokemon}}!”\n\n“你怎么说?”", + "query": "你要怎么做?", + "option": { + "1": { + "label": "接受", + "tooltip": "(-)支付{{price, money}}\n(+)获得{{purchasePokemon}}和它的隐藏特性", + "tooltip_shiny": "(-)支付{{price, money}}\n(+)获得一只闪光的{{purchasePokemon}}", + "selected_message": "你花了一大笔钱买了{{purchasePokemon}}.", + "selected_dialogue": "非常好的选择!$我看得出来你对商业有敏锐的眼光。$哦,对了……@d{64}不接受退货,你懂吗?" + }, + "2": { + "label": "拒绝", + "tooltip": "(-)无奖励", + "selected": "不?@d{32} 你说不要?$又不会让你吃亏!" + } + } +} diff --git a/src/locales/zh_CN/mystery-encounters/the-strong-stuff-dialogue.json b/src/locales/zh_CN/mystery-encounters/the-strong-stuff-dialogue.json new file mode 100644 index 00000000000..170ce3193df --- /dev/null +++ b/src/locales/zh_CN/mystery-encounters/the-strong-stuff-dialogue.json @@ -0,0 +1,21 @@ +{ + "intro": "眼前是一个巨大的{{shuckleName}}而看起来\n有一大壶……果汁?", + "title": "满满当当", + "description": "挡住你去路的{{shuckleName}}看起来异常强大。与此同时,它旁边的果汁散发着某种力量。\n\n{{shuckleName}}向你伸出了触角。它似乎想做些什么……", + "query": "你要怎么做?", + "option": { + "1": { + "label": "接近{{shuckleName}}", + "tooltip": "(?)可能会发生一些可怕或惊奇的事情", + "selected": "你眼前一黑", + "selected_2": "@f{150}当你醒来时,{{shuckleName}}不见了。\n并且壶里的果汁完全耗尽。${{highBstPokemon1}}和{{highBstPokemon2}}\n觉得一阵可怕的无力感袭来!$他们的基础属性减少了{{reductionValue}}!$然而,你剩下的神奇宝贝却感到精力充沛!\n他们的基础属性增加了{{increaseValue}}!" + }, + "2": { + "label": "与{{shuckleName}}战斗", + "tooltip": "(-)艰难的战斗\n(+)特别的奖励", + "selected": "{{shuckleName}}被激怒了,它喝了一些果汁并发起了攻击!", + "stat_boost": "{{shuckleName}}的果汁提升了它的能力!" + } + }, + "outro": "好诡异的一回啊。" +} diff --git a/src/locales/zh_CN/mystery-encounters/the-winstrate-challenge-dialogue.json b/src/locales/zh_CN/mystery-encounters/the-winstrate-challenge-dialogue.json new file mode 100644 index 00000000000..ecd7d336136 --- /dev/null +++ b/src/locales/zh_CN/mystery-encounters/the-winstrate-challenge-dialogue.json @@ -0,0 +1,22 @@ +{ + "intro": "房子外站着一家人!", + "speaker": "连胜家族", + "intro_dialogue": "我们是连胜家族!$你觉得和我们一家人来一场宝可梦车轮战怎么样?", + "title": "连胜家族的挑战", + "description": "连胜家族由 5 名训练师组成,他们想要战斗!\n如果你连续击败他们所有人,他们会给你一份大奖。但你能承受压力吗?", + "query": "你要怎么做?", + "option": { + "1": { + "label": "接受挑战", + "tooltip": "(-)残酷的战斗\n(+)特别的奖励", + "selected": "挑战开始!" + }, + "2": { + "label": "拒绝挑战", + "tooltip": "(+)队伍痊愈\n(+)获得一个超神奇糖果", + "selected": "太可惜了。话说,你们队伍看上去很疲惫,为什么不留下来休息一会儿呢?" + } + }, + "victory": "恭喜您赢得了我们的挑战!\n$事不宜迟,希望您收下这张扭蛋券。", + "victory_2": "此外,我们家也使用这款强制锻炼器来在训练中\n更有效地强化我们的宝可梦。\n鉴于你能打败我们所有人,你可能用不着,\n但我们希望你无论如何都能收下它!" +} diff --git a/src/locales/zh_CN/mystery-encounters/training-session-dialogue.json b/src/locales/zh_CN/mystery-encounters/training-session-dialogue.json new file mode 100644 index 00000000000..8d0b1dea64d --- /dev/null +++ b/src/locales/zh_CN/mystery-encounters/training-session-dialogue.json @@ -0,0 +1,33 @@ +{ + "intro": "你遇到了一些\n训练工具和用品。", + "title": "集训", + "description": "这些物品似乎可以用来训练你的队伍!\n有数种方式可以进行训练:让你队伍中的其他宝可梦与其进行对战。", + "query": "要怎么训练?", + "invalid_selection": "宝可梦必须足够健康", + "option": { + "1": { + "label": "轻量训练", + "tooltip": "(-)轻松战斗\n(+)提升随机2只宝可梦的个体", + "finished": "{{selectedPokemon}}回来了,感觉\n虽然疲惫不堪,但很有成就!$它的{{stat1}}和{{stat2}}的个体得到了改善!" + }, + "2": { + "label": "适度训练", + "tooltip": "(-)适中的战斗\n(+)改变宝可梦的性格", + "select_prompt": "选择一种想要的性格\n来训练你的神奇宝贝。", + "finished": "{{selectedPokemon}}回来了,感觉\n虽然疲惫不堪,但很有成就!$它的性格变成了{{nature}}!" + }, + "3": { + "label": "重磅训练", + "tooltip": "(-)艰苦的战斗\n(+)改变宝可梦的特性", + "select_prompt": "选择一种想要的特性\n来训练你的神奇宝贝。", + "finished": "{{selectedPokemon}}回来了,感觉\n虽然疲惫不堪,但很有成就!$它的特性变成了{{nature}}!" + }, + "4": { + "label": "离开", + "tooltip": "(-)无奖励", + "selected": "你无暇训练\n该动身了。" + }, + "selected": "{{selected Pokemon}}穿过空地来到你面前……" + }, + "outro": "这次训练成果不错!" +} diff --git a/src/locales/zh_CN/mystery-encounters/trash-to-treasure-dialogue.json b/src/locales/zh_CN/mystery-encounters/trash-to-treasure-dialogue.json new file mode 100644 index 00000000000..69e719064e9 --- /dev/null +++ b/src/locales/zh_CN/mystery-encounters/trash-to-treasure-dialogue.json @@ -0,0 +1,19 @@ +{ + "intro": "一大堆垃圾!\n这是从哪儿来的?", + "title": "变废为宝", + "description": "垃圾堆在你眼前晃来晃去,你似乎在垃圾中发现一些有价值的物品。\n不过,你确定要弄得脏兮兮地去拿它们吗?", + "query": "你要怎么做?", + "option": { + "1": { + "label": "挖掘宝贝", + "tooltip": "(-)商店的治疗用品消失\n(+)获得精美礼品", + "selected": "你穿过垃圾堆,没入污秽之中。$在你这样肮脏的状态下,任何有尊严的店主\n都不会卖给你任何东西!$你只能将就将就,不再购买治疗物品。$然而,你在垃圾中还是发现了一些不可思议的宝贝!" + }, + "2": { + "label": "探究来源", + "tooltip": "(?)找到垃圾的来源", + "selected": "你在这堆东西周围徘徊,寻找表明这东西可能出现在这里的任何迹象……", + "selected_2": "突然,垃圾动了!那不是垃圾,是一只宝可梦!" + } + } +} diff --git a/src/locales/zh_CN/mystery-encounters/uncommon-breed-dialogue.json b/src/locales/zh_CN/mystery-encounters/uncommon-breed-dialogue.json new file mode 100644 index 00000000000..1e321a6e614 --- /dev/null +++ b/src/locales/zh_CN/mystery-encounters/uncommon-breed-dialogue.json @@ -0,0 +1,26 @@ +{ + "intro": "这可不是一只普通的宝可梦!", + "title": "罕见物种", + "description": "{{enemyPokemon}}看起来与其他同种宝可梦大相径庭。\n@[TOOLTIP_TITLE]{也许它会特殊招式?}\n你可以直接进行战斗并尝试捕获它,但可能也有办法和它交朋友。", + "query": "你要怎么做?", + "option": { + "1": { + "label": "与它战斗", + "tooltip": "(-)蹊跷的战斗\n(+)可捕获强敌", + "selected": "你毫无畏惧地接近\n{{enemyPokemon}}。", + "stat_boost": "{{enemyPokemon}}的不凡实力让它的能力提高!" + }, + "2": { + "label": "给他吃的", + "disabled_tooltip": "你需要4个树果来选择此项", + "tooltip": "(-)给予4个树果\n(+){{enemyPokemon}}会喜欢你", + "selected": "你把树果扔向{{enemyPokemon}}!$它高兴地把树果吃干抹净!${{enemyPokemon}}想加入你的队伍!" + }, + "3": { + "label": "交朋友", + "disabled_tooltip": "你的宝可梦需要习得某些招式才能选择此项", + "tooltip": "(+){{option3PrimaryName}}使用{{option3PrimaryMove}}\n(+){{enemyPokemon}}会喜欢你", + "selected": "你的{{option3PrimaryName}}使用了{{option3PrimaryMove}}来吸引{{enemyPokemon}}!${{enemyPokemon}}想加入你的队伍!" + } + } +} diff --git a/src/locales/zh_CN/mystery-encounters/weird-dream-dialogue.json b/src/locales/zh_CN/mystery-encounters/weird-dream-dialogue.json new file mode 100644 index 00000000000..0bff2cdbb4c --- /dev/null +++ b/src/locales/zh_CN/mystery-encounters/weird-dream-dialogue.json @@ -0,0 +1,22 @@ +{ + "intro": "一个阴暗的女人挡住了你的路。\n她身上的气氛让人不安……", + "speaker": "女人", + "intro_dialogue": "我看到了你的未来,你的过去……$孩子,你也看到了吗?", + "title": "???", + "description": "女人的话语在你的脑海里回荡。\n那不是同一个人的声音,\n而是来自所有时间和空间的众多声音。\n你开始感到头晕目眩,这个问题萦绕在你的脑海中……\n\n@[TOOLTIP_TITLE]{\"我看到了你的未来,你的过去……$孩子,你也看到了吗?\"}", + "query": "你要怎么做?", + "option": { + "1": { + "label": "\"我看见了\"", + "tooltip": "@[SUMMARY_GREEN]{(?)影响宝可梦}", + "selected": "她伸出手去触摸你,\n然后一切都变黑了。$然后……@d{64}你看到了一切。\n每一条时间线,你所有不同的自我,\n过去和未来。$一切造就了你的事物,\n一切你将变成的事物……@d{64}", + "cutscene": "你会看到你的宝可梦@d{32} 从与不同的现实融合,转变成全新的存在……@d{64}", + "dream_complete": "当你醒来时,那个女人……女人还是幽灵?\n……不见了……$.@d{32}.@d{32}.@d{32}$你的宝可梦已经改变了……\n又或者它是你某一时刻的某支队伍?" + }, + "2": { + "label": "匆匆离开", + "tooltip": "(-)影响宝可梦", + "selected": "你从麻木的控制中挣脱出来,匆匆离开。$当你终于停下来收拾好自己时,你连忙检查队伍中的宝可梦。$不知为何,他们的等级都下降了!" + } + } +} diff --git a/src/locales/zh_CN/party-ui-handler.json b/src/locales/zh_CN/party-ui-handler.json index 8dff1ffb75c..4ddea7cd789 100644 --- a/src/locales/zh_CN/party-ui-handler.json +++ b/src/locales/zh_CN/party-ui-handler.json @@ -15,6 +15,7 @@ "UNPAUSE_EVOLUTION": "解除进化暂停", "REVIVE": "复活", "RENAME": "起名", + "SELECT": "Select", "choosePokemon": "选择一只宝可梦。", "doWhatWithThisPokemon": "要对宝可梦做什么?", "noEnergy": "{{pokemonName}}没有力气战斗了!", 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/status-effect.json b/src/locales/zh_CN/status-effect.json index f7fb9092107..c9d7073021c 100644 --- a/src/locales/zh_CN/status-effect.json +++ b/src/locales/zh_CN/status-effect.json @@ -1,12 +1,6 @@ { "none": { - "name": "无", - "description": "", - "obtain": "", - "obtainSource": "", - "activation": "", - "overlap": "", - "heal": "" + "name": "无" }, "poison": { "name": "中毒", 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..dea307ef0bf 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": "赤红 & 青绿", @@ -160,5 +166,18 @@ "alder_iris_double": "阿戴克 & 艾莉丝", "iris_alder_double": "艾莉丝 & 阿戴克", "marnie_piers_double": "玛俐 & 聂梓", - "piers_marnie_double": "聂梓 & 玛俐" + "piers_marnie_double": "聂梓 & 玛俐", + + "buck": "麦可", + "cheryl": "芽米", + "marley": "米依", + "mira": "麦儿", + "riley": "亚玄", + "victor": "晴彦", + "victoria": "安江", + "vivi": "小秋", + "vicky": "光代", + "vito": "良平", + "bug_type_superfan": "超级虫系粉丝", + "expert_pokemon_breeder": "专业宝可梦饲养员" } diff --git a/src/locales/zh_CN/trainer-titles.json b/src/locales/zh_CN/trainer-titles.json index 07654ec8fc3..8ac8acf0a3a 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,8 @@ "flare_admin_female": "闪焰队干部", "aether_admin": "以太基金会干部", "skull_admin": "骷髅队干部", - "macro_admin": "马洛科蒙干部" + "macro_admin": "马洛科蒙干部", + "star_admin": "Team Star Squad Boss", + + "the_winstrates": "连胜家族" } 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/achv.json b/src/locales/zh_TW/achv.json index 804ec0c5089..5c3b137f90f 100644 --- a/src/locales/zh_TW/achv.json +++ b/src/locales/zh_TW/achv.json @@ -264,5 +264,9 @@ "INVERSE_BATTLE": { "name": "鏡子子鏡", "description": "完成逆轉之戰挑戰\n戰挑戰之轉逆成完" + }, + "BREEDERS_IN_SPACE": { + "name": "Breeders in Space!", + "description": "Beat the Expert Pokémon Breeder in the Space Biome." } } diff --git a/src/locales/zh_TW/battle.json b/src/locales/zh_TW/battle.json index 66da1b7cace..3eb9d523b03 100644 --- a/src/locales/zh_TW/battle.json +++ b/src/locales/zh_TW/battle.json @@ -21,6 +21,10 @@ "moneyWon": "你贏得了\n₽{{moneyAmount}}!", "moneyPickedUp": "撿到了₽{{moneyAmount}}!", "pokemonCaught": "{{pokemonName}}被抓住了!", + "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}}作爲\n一個新的基礎寶可夢!", "partyFull": "你的隊伍已滿員。是否放生其他寶可夢\n爲{{pokemonName}}騰出空間?", "pokemon": "寶可夢", @@ -56,6 +60,7 @@ "noPokeballTrainer": "你不能捕捉其他訓練家的寶可夢!", "noPokeballMulti": "只能在剩下一只寶可夢時才能扔出精靈球!", "noPokeballStrong": "目標寶可夢太強了,無法捕捉!\n你需要先削弱它!", + "noPokeballMysteryEncounter": "You aren't able to\ncatch this Pokémon!", "noEscapeForce": "一股無形的力量阻止你逃跑。", "noEscapeTrainer": "你不能從與訓練家的戰鬥中逃跑!", "noEscapePokemon": "{{pokemonName}}的{{moveName}}\n阻止了你{{escapeVerb}}!", @@ -94,5 +99,6 @@ "retryBattle": "你要從對戰開始時重試麽?", "unlockedSomething": "{{unlockedThing}}\n已解鎖。", "congratulations": "恭喜!", - "beatModeFirstTime": "{{speciesName}}首次擊敗了{{gameMode}}!\n你獲得了{{newModifier}}!" + "beatModeFirstTime": "{{speciesName}}首次擊敗了{{gameMode}}!\n你獲得了{{newModifier}}!", + "mysteryEncounterAppeared": "What's this?" } diff --git a/src/locales/zh_TW/bgm-name.json b/src/locales/zh_TW/bgm-name.json index e8546750977..082e4743ffc 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": "黑白 「視線!精英訓練師」", @@ -137,5 +140,11 @@ "heal": "黑白「寶可夢回複」", "menu": "空之探險隊「歡迎來到寶可夢的世界」", - "title": "空之探險隊「主題曲」" + "title": "空之探險隊「主題曲」", + + "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/zh_TW/config.ts b/src/locales/zh_TW/config.ts index cf505d683a5..d1388d336cc 100644 --- a/src/locales/zh_TW/config.ts +++ b/src/locales/zh_TW/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 "./mystery-encounters/mysterious-chest-dialogue.json"; +import mysteriousChallengers from "./mystery-encounters/mysterious-challengers-dialogue.json"; +import darkDeal from "./mystery-encounters/dark-deal-dialogue.json"; +import departmentStoreSale from "./mystery-encounters/department-store-sale-dialogue.json"; +import fieldTrip from "./mystery-encounters/field-trip-dialogue.json"; +import fieryFallout from "./mystery-encounters/fiery-fallout-dialogue.json"; +import fightOrFlight from "./mystery-encounters/fight-or-flight-dialogue.json"; +import safariZone from "./mystery-encounters/safari-zone-dialogue.json"; +import shadyVitaminDealer from "./mystery-encounters/shady-vitamin-dealer-dialogue.json"; +import slumberingSnorlax from "./mystery-encounters/slumbering-snorlax-dialogue.json"; +import trainingSession from "./mystery-encounters/training-session-dialogue.json"; +import theStrongStuff from "./mystery-encounters/the-strong-stuff-dialogue.json"; +import pokemonSalesman from "./mystery-encounters/the-pokemon-salesman-dialogue.json"; +import offerYouCantRefuse from "./mystery-encounters/an-offer-you-cant-refuse-dialogue.json"; +import delibirdy from "./mystery-encounters/delibirdy-dialogue.json"; +import absoluteAvarice from "./mystery-encounters/absolute-avarice-dialogue.json"; +import aTrainersTest from "./mystery-encounters/a-trainers-test-dialogue.json"; +import trashToTreasure from "./mystery-encounters/trash-to-treasure-dialogue.json"; +import berriesAbound from "./mystery-encounters/berries-abound-dialogue.json"; +import clowningAround from "./mystery-encounters/clowning-around-dialogue.json"; +import partTimer from "./mystery-encounters/part-timer-dialogue.json"; +import dancingLessons from "./mystery-encounters/dancing-lessons-dialogue.json"; +import weirdDream from "./mystery-encounters/weird-dream-dialogue.json"; +import theWinstrateChallenge from "./mystery-encounters/the-winstrate-challenge-dialogue.json"; +import teleportingHijinks from "./mystery-encounters/teleporting-hijinks-dialogue.json"; +import bugTypeSuperfan from "./mystery-encounters/bug-type-superfan-dialogue.json"; +import funAndGames from "./mystery-encounters/fun-and-games-dialogue.json"; +import uncommonBreed from "./mystery-encounters/uncommon-breed-dialogue.json"; +import globalTradeSystem from "./mystery-encounters/global-trade-system-dialogue.json"; +import expertPokemonBreeder from "./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 zhTwConfig = { ability, abilityTriggers, @@ -110,4 +152,40 @@ export const zhTwConfig = { modifierSelectUiHandler, moveTriggers, 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/zh_TW/dialogue.json b/src/locales/zh_TW/dialogue.json index 99427ac5f14..43cfad8b610 100644 --- a/src/locales/zh_TW/dialogue.json +++ b/src/locales/zh_TW/dialogue.json @@ -787,6 +787,116 @@ "1": "沒有遠見的蠢貨會繼續玷汙這個美麗的世界。" } }, + "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": "我對岩石屬性寶可夢的專精會擊敗你!來吧!", diff --git a/src/locales/zh_TW/egg.json b/src/locales/zh_TW/egg.json index 0178848a0d5..6667fccb826 100644 --- a/src/locales/zh_TW/egg.json +++ b/src/locales/zh_TW/egg.json @@ -11,6 +11,7 @@ "gachaTypeLegendary": "傳說概率上升", "gachaTypeMove": "稀有概率上升", "gachaTypeShiny": "閃光概率上升", + "eventType": "Mystery Event", "selectMachine": "選擇一個機器。", "notEnoughVouchers": "你沒有足夠的兌換券!", "tooManyEggs": "你的蛋太多啦!", diff --git a/src/locales/zh_TW/modifier-select-ui-handler.json b/src/locales/zh_TW/modifier-select-ui-handler.json index 84ebbbfef6a..d47ea265900 100644 --- a/src/locales/zh_TW/modifier-select-ui-handler.json +++ b/src/locales/zh_TW/modifier-select-ui-handler.json @@ -8,5 +8,7 @@ "lockRaritiesDesc": "在刷新時鎖定道具稀有度(影響刷新費用)", "checkTeamDesc": "檢查隊伍或使用形態改變道具", "rerollCost": "₽{{formattedMoney}}", - "itemCost": "₽{{formattedMoney}}" + "itemCost": "₽{{formattedMoney}}", + "continueNextWaveButton": "Continue", + "continueNextWaveDescription": "Continue to the next wave" } diff --git a/src/locales/zh_TW/modifier-type.json b/src/locales/zh_TW/modifier-type.json index 6ff593ddba7..f305c0e2d62 100644 --- a/src/locales/zh_TW/modifier-type.json +++ b/src/locales/zh_TW/modifier-type.json @@ -68,6 +68,20 @@ "BaseStatBoosterModifierType": { "description": "增加持有者的{{stat}}10%,個體值越高堆疊\n上限越高。" }, + "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": "所有寶可夢完全恢復HP。" }, @@ -401,7 +415,13 @@ "ENEMY_FUSED_CHANCE": { "name": "融合硬幣", "description": "增加1%野生融合寶可夢出現概率。" - } + }, + + "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": { diff --git a/src/locales/zh_TW/move-trigger.json b/src/locales/zh_TW/move-trigger.json index db88f6df57f..2cd33a3a416 100644 --- a/src/locales/zh_TW/move-trigger.json +++ b/src/locales/zh_TW/move-trigger.json @@ -65,6 +65,7 @@ "suppressAbilities": "{{pokemonName}}的特性\n變得無效了!", "revivalBlessing": "{{pokemonName}}復活了!", "swapArenaTags": "{{pokemonName}}\n交換了雙方的場地效果!", + "chillyReception": "{{pokemonName}}\n說了冷笑話!", "exposedMove": "{{pokemonName}}識破了\n{{targetPokemonName}}的原形!", "safeguard": "{{targetName}}\n正受到神秘之幕的保護!", "afterYou": "{{pokemonName}}\n接受了對手的好意!" 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/mystery-encounter-messages.json b/src/locales/zh_TW/mystery-encounter-messages.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/src/locales/zh_TW/mystery-encounter-messages.json @@ -0,0 +1 @@ +{} diff --git a/src/locales/zh_TW/mystery-encounters/a-trainers-test-dialogue.json b/src/locales/zh_TW/mystery-encounters/a-trainers-test-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/zh_TW/mystery-encounters/a-trainers-test-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/zh_TW/mystery-encounters/absolute-avarice-dialogue.json b/src/locales/zh_TW/mystery-encounters/absolute-avarice-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/zh_TW/mystery-encounters/absolute-avarice-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/zh_TW/mystery-encounters/an-offer-you-cant-refuse-dialogue.json b/src/locales/zh_TW/mystery-encounters/an-offer-you-cant-refuse-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/zh_TW/mystery-encounters/an-offer-you-cant-refuse-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/zh_TW/mystery-encounters/berries-abound-dialogue.json b/src/locales/zh_TW/mystery-encounters/berries-abound-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/zh_TW/mystery-encounters/berries-abound-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/zh_TW/mystery-encounters/bug-type-superfan-dialogue.json b/src/locales/zh_TW/mystery-encounters/bug-type-superfan-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/zh_TW/mystery-encounters/bug-type-superfan-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/zh_TW/mystery-encounters/clowning-around-dialogue.json b/src/locales/zh_TW/mystery-encounters/clowning-around-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/zh_TW/mystery-encounters/clowning-around-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/zh_TW/mystery-encounters/dancing-lessons-dialogue.json b/src/locales/zh_TW/mystery-encounters/dancing-lessons-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/zh_TW/mystery-encounters/dancing-lessons-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/zh_TW/mystery-encounters/dark-deal-dialogue.json b/src/locales/zh_TW/mystery-encounters/dark-deal-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/zh_TW/mystery-encounters/dark-deal-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/zh_TW/mystery-encounters/delibirdy-dialogue.json b/src/locales/zh_TW/mystery-encounters/delibirdy-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/zh_TW/mystery-encounters/delibirdy-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/zh_TW/mystery-encounters/department-store-sale-dialogue.json b/src/locales/zh_TW/mystery-encounters/department-store-sale-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/zh_TW/mystery-encounters/department-store-sale-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/zh_TW/mystery-encounters/field-trip-dialogue.json b/src/locales/zh_TW/mystery-encounters/field-trip-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/zh_TW/mystery-encounters/field-trip-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/zh_TW/mystery-encounters/fiery-fallout-dialogue.json b/src/locales/zh_TW/mystery-encounters/fiery-fallout-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/zh_TW/mystery-encounters/fiery-fallout-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/zh_TW/mystery-encounters/fight-or-flight-dialogue.json b/src/locales/zh_TW/mystery-encounters/fight-or-flight-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/zh_TW/mystery-encounters/fight-or-flight-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/zh_TW/mystery-encounters/fun-and-games-dialogue.json b/src/locales/zh_TW/mystery-encounters/fun-and-games-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/zh_TW/mystery-encounters/fun-and-games-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/zh_TW/mystery-encounters/global-trade-system-dialogue.json b/src/locales/zh_TW/mystery-encounters/global-trade-system-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/zh_TW/mystery-encounters/global-trade-system-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/zh_TW/mystery-encounters/lost-at-sea-dialogue.json b/src/locales/zh_TW/mystery-encounters/lost-at-sea-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/zh_TW/mystery-encounters/lost-at-sea-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/zh_TW/mystery-encounters/mysterious-challengers-dialogue.json b/src/locales/zh_TW/mystery-encounters/mysterious-challengers-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/zh_TW/mystery-encounters/mysterious-challengers-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/zh_TW/mystery-encounters/mysterious-chest-dialogue.json b/src/locales/zh_TW/mystery-encounters/mysterious-chest-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/zh_TW/mystery-encounters/mysterious-chest-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/zh_TW/mystery-encounters/part-timer-dialogue.json b/src/locales/zh_TW/mystery-encounters/part-timer-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/zh_TW/mystery-encounters/part-timer-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/zh_TW/mystery-encounters/safari-zone-dialogue.json b/src/locales/zh_TW/mystery-encounters/safari-zone-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/zh_TW/mystery-encounters/safari-zone-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/zh_TW/mystery-encounters/shady-vitamin-dealer-dialogue.json b/src/locales/zh_TW/mystery-encounters/shady-vitamin-dealer-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/zh_TW/mystery-encounters/shady-vitamin-dealer-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/zh_TW/mystery-encounters/slumbering-snorlax-dialogue.json b/src/locales/zh_TW/mystery-encounters/slumbering-snorlax-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/zh_TW/mystery-encounters/slumbering-snorlax-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/zh_TW/mystery-encounters/teleporting-hijinks-dialogue.json b/src/locales/zh_TW/mystery-encounters/teleporting-hijinks-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/zh_TW/mystery-encounters/teleporting-hijinks-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/zh_TW/mystery-encounters/the-expert-pokemon-breeder-dialogue.json b/src/locales/zh_TW/mystery-encounters/the-expert-pokemon-breeder-dialogue.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/src/locales/zh_TW/mystery-encounters/the-expert-pokemon-breeder-dialogue.json @@ -0,0 +1 @@ +{} diff --git a/src/locales/zh_TW/mystery-encounters/the-pokemon-salesman-dialogue.json b/src/locales/zh_TW/mystery-encounters/the-pokemon-salesman-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/zh_TW/mystery-encounters/the-pokemon-salesman-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/zh_TW/mystery-encounters/the-strong-stuff-dialogue.json b/src/locales/zh_TW/mystery-encounters/the-strong-stuff-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/zh_TW/mystery-encounters/the-strong-stuff-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/zh_TW/mystery-encounters/the-winstrate-challenge-dialogue.json b/src/locales/zh_TW/mystery-encounters/the-winstrate-challenge-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/zh_TW/mystery-encounters/the-winstrate-challenge-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/zh_TW/mystery-encounters/training-session-dialogue.json b/src/locales/zh_TW/mystery-encounters/training-session-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/zh_TW/mystery-encounters/training-session-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/zh_TW/mystery-encounters/trash-to-treasure-dialogue.json b/src/locales/zh_TW/mystery-encounters/trash-to-treasure-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/zh_TW/mystery-encounters/trash-to-treasure-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/zh_TW/mystery-encounters/uncommon-breed-dialogue.json b/src/locales/zh_TW/mystery-encounters/uncommon-breed-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/zh_TW/mystery-encounters/uncommon-breed-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/zh_TW/mystery-encounters/weird-dream-dialogue.json b/src/locales/zh_TW/mystery-encounters/weird-dream-dialogue.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/src/locales/zh_TW/mystery-encounters/weird-dream-dialogue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/locales/zh_TW/party-ui-handler.json b/src/locales/zh_TW/party-ui-handler.json index ef82d1a3ddc..e2abed5914d 100644 --- a/src/locales/zh_TW/party-ui-handler.json +++ b/src/locales/zh_TW/party-ui-handler.json @@ -15,6 +15,7 @@ "UNPAUSE_EVOLUTION": "解除進化暫停", "REVIVE": "複活", "RENAME": "起名", + "SELECT": "Select", "choosePokemon": "選擇一只寶可夢。", "doWhatWithThisPokemon": "要對寶可夢做什麽?", "noEnergy": "{{pokemonName}}沒有力氣戰鬥了!", 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/status-effect.json b/src/locales/zh_TW/status-effect.json index 5f634a0bddf..29908593eef 100644 --- a/src/locales/zh_TW/status-effect.json +++ b/src/locales/zh_TW/status-effect.json @@ -1,12 +1,6 @@ { "none": { - "name": "無", - "description": "", - "obtain": "", - "obtainSource": "", - "activation": "", - "overlap": "", - "heal": "" + "name": "無" }, "poison": { "name": "中毒", 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..266927e7b09 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": "赤紅 & 青綠", @@ -160,5 +166,18 @@ "alder_iris_double": "阿戴克 & 艾莉絲", "iris_alder_double": "艾莉絲 & 阿戴克", "marnie_piers_double": "瑪俐 & 聶梓", - "piers_marnie_double": "聶梓 & 瑪俐" + "piers_marnie_double": "聶梓 & 瑪俐", + + "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/zh_TW/trainer-titles.json b/src/locales/zh_TW/trainer-titles.json index 80b2807e7b5..4b95c5ea184 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,11 @@ "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", + + "the_winstrates": "The Winstrates'" } diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index fdfe60332f5..6503aaafbc5 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,27 @@ 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 number); + } + return new PokemonBaseStatTotalModifierType(Utils.randSeedInt(20)); + }), + MYSTERY_ENCOUNTER_OLD_GATEAU: () => new ModifierTypeGenerator((party: Pokemon[], pregenArgs?: any[]) => { + if (pregenArgs) { + return new PokemonBaseStatFlatModifierType(pregenArgs[0] as number, pregenArgs[1] as Stat[]); + } + return new PokemonBaseStatFlatModifierType(Utils.randSeedInt(20), [Stat.HP, Stat.ATK, Stat.DEF]); + }), + MYSTERY_ENCOUNTER_BLACK_SLUDGE: () => new ModifierTypeGenerator((party: Pokemon[], pregenArgs?: any[]) => { + if (pregenArgs) { + return new ModifierType("modifierType:ModifierType.MYSTERY_ENCOUNTER_BLACK_SLUDGE", "black_sludge", (type, _args) => new Modifiers.HealShopCostModifier(type, pregenArgs[0] as number)); + } + return new ModifierType("modifierType:ModifierType.MYSTERY_ENCOUNTER_BLACK_SLUDGE", "black_sludge", (type, _args) => new Modifiers.HealShopCostModifier(type, 2.5)); + }), + 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,7 +1631,7 @@ 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), @@ -1598,7 +1688,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); @@ -1621,7 +1711,7 @@ const modifierPool: ModifierPool = { }), [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), @@ -1691,7 +1781,7 @@ 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), @@ -1974,29 +2064,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} @@ -2122,7 +2290,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; @@ -2148,7 +2325,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; @@ -2181,7 +2358,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..6c998105c1d 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,59 @@ export class LockModifierTiersModifier extends PersistentModifier { } } +/** + * Black Sludge item + */ +export class HealShopCostModifier extends PersistentModifier { + public readonly shopMultiplier: number; + + constructor(type: ModifierType, shopMultiplier: number, stackCount?: integer) { + super(type, stackCount); + + this.shopMultiplier = shopMultiplier; + } + + match(modifier: Modifier): boolean { + return modifier instanceof HealShopCostModifier; + } + + clone(): HealShopCostModifier { + return new HealShopCostModifier(this.type, this.shopMultiplier, this.stackCount); + } + + apply(args: any[]): boolean { + (args[0] as Utils.IntegerHolder).value *= this.shopMultiplier; + + 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(): BoostBugSpawnModifier { + 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 +2695,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 +2740,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 +2766,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/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 66299064bb2..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, false, () => { + 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/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..25b4398025f 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,51 @@ export class EncounterPhase extends BattlePhase { const battle = this.scene.currentBattle; + // Generate and Init Mystery Encounter + if (battle.battleType === BattleType.MYSTERY_ENCOUNTER && !battle.mysteryEncounter) { + this.scene.executeWithSeedOffset(() => { + const currentSessionEncounterType = battle.mysteryEncounterType; + battle.mysteryEncounter = this.scene.getMysteryEncounter(currentSessionEncounterType); + }, battle.waveIndex << 4); + } + 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 +123,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) { @@ -103,7 +147,8 @@ export class EncounterPhase extends BattlePhase { loadEnemyAssets.push(enemyPokemon.loadAssets()); - console.log(getPokemonNameWithAffix(enemyPokemon), enemyPokemon.species.speciesId, enemyPokemon.stats); + console.log(`Pokemon: ${getPokemonNameWithAffix(enemyPokemon)}`, `Species ID: ${enemyPokemon.species.speciesId}`, `Stats: ${enemyPokemon.stats}`, `Ability: ${enemyPokemon.getAbility().name}`, `Passive Ability: ${enemyPokemon.getPassiveAbility().name}`); + return true; }); if (this.scene.getParty().filter(p => p.isShiny()).length === 6) { @@ -112,6 +157,22 @@ 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?.introVisuals) { + loadEnemyAssets.push(battle.mysteryEncounter.introVisuals.loadAssets().then(() => battle.mysteryEncounter!.introVisuals!.initSprite())); + } + if (battle.mysteryEncounter?.loadAssets && 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 +188,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 +209,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 +253,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 +264,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 +363,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 +432,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 +518,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 2b63dcdd14b..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); @@ -140,6 +137,9 @@ export class FaintPhase extends PokemonPhase { ease: "Sine.easeIn", onComplete: () => { 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/form-change-phase.ts b/src/phases/form-change-phase.ts index 33c1f8e8cef..1f18457146d 100644 --- a/src/phases/form-change-phase.ts +++ b/src/phases/form-change-phase.ts @@ -9,6 +9,7 @@ import PartyUiHandler from "../ui/party-ui-handler"; import { getPokemonNameWithAffix } from "../messages"; import { EndEvolutionPhase } from "./end-evolution-phase"; import { EvolutionPhase } from "./evolution-phase"; +import { BattlerTagType } from "#app/enums/battler-tag-type"; export class FormChangePhase extends EvolutionPhase { private formChange: SpeciesFormChange; @@ -157,6 +158,7 @@ export class FormChangePhase extends EvolutionPhase { } end(): void { + this.pokemon.findAndRemoveTags(t => t.tagType === BattlerTagType.AUTOTOMIZED); if (this.modal) { this.scene.ui.revertMode().then(() => { if (this.scene.ui.getMode() === Mode.PARTY) { 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-effect-phase.ts b/src/phases/move-effect-phase.ts index e3c8216b65a..263a576c4f0 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -1,6 +1,6 @@ import BattleScene from "#app/battle-scene"; import { BattlerIndex } from "#app/battle"; -import { applyPreAttackAbAttrs, AddSecondStrikeAbAttr, IgnoreMoveEffectsAbAttr, applyPostDefendAbAttrs, PostDefendAbAttr, applyPostAttackAbAttrs, PostAttackAbAttr, MaxMultiHitAbAttr, AlwaysHitAbAttr } from "#app/data/ability"; +import { applyPreAttackAbAttrs, AddSecondStrikeAbAttr, IgnoreMoveEffectsAbAttr, applyPostDefendAbAttrs, PostDefendAbAttr, applyPostAttackAbAttrs, PostAttackAbAttr, MaxMultiHitAbAttr, AlwaysHitAbAttr, TypeImmunityAbAttr } from "#app/data/ability"; import { ArenaTagSide, ConditionalProtectTag } from "#app/data/arena-tag"; import { MoveAnim } from "#app/data/battle-anims"; import { BattlerTagLapseType, DamageProtectedTag, ProtectedTag, SemiInvulnerableTag, SubstituteTag } from "#app/data/battler-tags"; @@ -97,12 +97,17 @@ export class MoveEffectPhase extends PokemonPhase { */ const targetHitChecks = Object.fromEntries(targets.map(p => [p.getBattlerIndex(), this.hitCheck(p)])); const hasActiveTargets = targets.some(t => t.isActive(true)); + + /** Check if the target is immune via ability to the attacking move */ + const isImmune = targets[0].hasAbilityWithAttr(TypeImmunityAbAttr) && (targets[0].getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move)); + /** * If no targets are left for the move to hit (FAIL), or the invoked move is single-target * (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) && !isImmune)) { this.stopMultiHit(); if (hasActiveTargets) { this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: this.getTarget()? getPokemonNameWithAffix(this.getTarget()!) : "" })); @@ -119,16 +124,37 @@ 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, move.hitsSubstitute(user, this.getTarget()!), () => { + 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) { + + /** The {@linkcode ArenaTagSide} to which the target belongs */ + const targetSide = target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; + /** Has the invoked move been cancelled by conditional protection (e.g Quick Guard)? */ + const hasConditionalProtectApplied = new Utils.BooleanHolder(false); + /** Does the applied conditional protection bypass Protect-ignoring effects? */ + const bypassIgnoreProtect = new Utils.BooleanHolder(false); + /** If the move is not targeting a Pokemon on the user's side, try to apply conditional protection effects */ + if (!this.move.getMove().isAllyTarget()) { + this.scene.arena.applyTagsForSide(ConditionalProtectTag, targetSide, hasConditionalProtectApplied, user, target, move.id, bypassIgnoreProtect); + } + + /** Is the target protected by Protect, etc. or a relevant conditional protection effect? */ + const isProtected = (bypassIgnoreProtect.value || !this.move.getMove().checkFlag(MoveFlags.IGNORE_PROTECT, user, target)) + && (hasConditionalProtectApplied.value || (!target.findTags(t => t instanceof DamageProtectedTag).length && target.findTags(t => t instanceof ProtectedTag).find(t => target.lapseTag(t.tagType))) + || (this.move.getMove().category !== MoveCategory.STATUS && target.findTags(t => t instanceof DamageProtectedTag).find(t => target.lapseTag(t.tagType)))); + + /** Is the pokemon immune due to an ablility? */ + const isImmune = target.hasAbilityWithAttr(TypeImmunityAbAttr) && (target.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move)); + /** * 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()]) { + if (!isImmune && !isProtected && !targetHitChecks[target.getBattlerIndex()]) { this.stopMultiHit(target); this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: getPokemonNameWithAffix(target) })); if (moveHistoryEntry.result === MoveResult.PENDING) { @@ -139,22 +165,6 @@ export class MoveEffectPhase extends PokemonPhase { continue; } - /** The {@linkcode ArenaTagSide} to which the target belongs */ - const targetSide = target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; - /** Has the invoked move been cancelled by conditional protection (e.g Quick Guard)? */ - const hasConditionalProtectApplied = new Utils.BooleanHolder(false); - /** Does the applied conditional protection bypass Protect-ignoring effects? */ - const bypassIgnoreProtect = new Utils.BooleanHolder(false); - // If the move is not targeting a Pokemon on the user's side, try to apply conditional protection effects - if (!this.move.getMove().isAllyTarget()) { - this.scene.arena.applyTagsForSide(ConditionalProtectTag, targetSide, hasConditionalProtectApplied, user, target, move.id, bypassIgnoreProtect); - } - - /** Is the target protected by Protect, etc. or a relevant conditional protection effect? */ - const isProtected = (bypassIgnoreProtect.value || !this.move.getMove().checkFlag(MoveFlags.IGNORE_PROTECT, user, target)) - && (hasConditionalProtectApplied.value || (!target.findTags(t => t instanceof DamageProtectedTag).length && target.findTags(t => t instanceof ProtectedTag).find(t => target.lapseTag(t.tagType))) - || (this.move.getMove().category !== MoveCategory.STATUS && target.findTags(t => t instanceof DamageProtectedTag).find(t => target.lapseTag(t.tagType)))); - /** Does this phase represent the invoked move's first strike? */ const firstHit = (user.turnData.hitsLeft === user.turnData.hitCount); diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index e63096360dd..0a75c32bac3 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -74,7 +74,7 @@ export class MovePhase extends BattlePhase { if (!this.followUp) { if (this.move.getMove().checkFlag(MoveFlags.IGNORE_ABILITIES, this.pokemon, null)) { - this.scene.arena.setIgnoreAbilities(); + this.scene.arena.setIgnoreAbilities(true, this.pokemon.getBattlerIndex()); } } else { this.pokemon.turnData.hitsLeft = 0; // TODO: is `0` correct? 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 93bf4cd41d5..bf38c432394 100644 --- a/src/phases/obtain-status-effect-phase.ts +++ b/src/phases/obtain-status-effect-phase.ts @@ -9,26 +9,26 @@ import { PokemonPhase } from "./pokemon-phase"; import { PostTurnStatusEffectPhase } from "./post-turn-status-effect-phase"; export class ObtainStatusEffectPhase extends PokemonPhase { - private statusEffect: StatusEffect | undefined; - private cureTurn: integer | null; - private sourceText: string | null; - private sourcePokemon: Pokemon | null; + private statusEffect?: StatusEffect | undefined; + private cureTurn?: integer | null; + private sourceText?: string | null; + private sourcePokemon?: Pokemon | null; - constructor(scene: BattleScene, battlerIndex: BattlerIndex, statusEffect?: StatusEffect, cureTurn?: integer | null, sourceText?: string, sourcePokemon?: Pokemon) { + constructor(scene: BattleScene, battlerIndex: BattlerIndex, statusEffect?: StatusEffect, cureTurn?: integer | null, sourceText?: string | null, sourcePokemon?: Pokemon | null) { super(scene, battlerIndex); this.statusEffect = statusEffect; - this.cureTurn = cureTurn!; // TODO: is this bang correct? - this.sourceText = sourceText!; // TODO: is this bang correct? - this.sourcePokemon = sourcePokemon!; // For tracking which Pokemon caused the status effect // TODO: is this bang correct? + this.cureTurn = cureTurn; + this.sourceText = sourceText; + this.sourcePokemon = sourcePokemon; // For tracking which Pokemon caused the status effect } start() { const pokemon = this.getPokemon(); - if (!pokemon?.status) { - if (pokemon?.trySetStatus(this.statusEffect, false, this.sourcePokemon)) { + if (pokemon && !pokemon.status) { + if (pokemon.trySetStatus(this.statusEffect, false, this.sourcePokemon)) { if (this.cureTurn) { - pokemon.status!.cureTurn = this.cureTurn; // TODO: is this bang correct? + pokemon.status!.cureTurn = this.cureTurn; // TODO: is this bang correct? } pokemon.updateInfo(true); new CommonBattleAnim(CommonAnim.POISON + (this.statusEffect! - 1), pokemon).play(this.scene, false, () => { @@ -40,8 +40,8 @@ export class ObtainStatusEffectPhase extends PokemonPhase { }); return; } - } else if (pokemon.status.effect === this.statusEffect) { - this.scene.queueMessage(getStatusEffectOverlapText(this.statusEffect, getPokemonNameWithAffix(pokemon))); + } else if (pokemon.status?.effect === this.statusEffect) { + this.scene.queueMessage(getStatusEffectOverlapText(this.statusEffect ?? StatusEffect.NONE, getPokemonNameWithAffix(pokemon))); } this.end(); } 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-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/quiet-form-change-phase.ts b/src/phases/quiet-form-change-phase.ts index dde500e156a..c28cc28b592 100644 --- a/src/phases/quiet-form-change-phase.ts +++ b/src/phases/quiet-form-change-phase.ts @@ -3,6 +3,7 @@ import { SemiInvulnerableTag } from "#app/data/battler-tags"; import { SpeciesFormChange, getSpeciesFormChangeMessage } from "#app/data/pokemon-forms"; import { getTypeRgb } from "#app/data/type"; import { BattleSpec } from "#app/enums/battle-spec"; +import { BattlerTagType } from "#app/enums/battler-tag-type"; import Pokemon, { EnemyPokemon } from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; import { BattlePhase } from "./battle-phase"; @@ -113,6 +114,7 @@ export class QuietFormChangePhase extends BattlePhase { } end(): void { + this.pokemon.findAndRemoveTags(t => t.tagType === BattlerTagType.AUTOTOMIZED); if (this.pokemon.scene?.currentBattle.battleSpec === BattleSpec.FINAL_BOSS && this.pokemon instanceof EnemyPokemon) { this.scene.playBgm(); this.scene.unshiftPhase(new PokemonHealPhase(this.scene, this.pokemon.getBattlerIndex(), this.pokemon.getMaxHp(), null, false, false, false, true)); diff --git a/src/phases/select-modifier-phase.ts b/src/phases/select-modifier-phase.ts index e14638c5dd2..ba55340bd8d 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/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..5068fed6b77 --- /dev/null +++ b/src/test/abilities/arena_trap.test.ts @@ -0,0 +1,58 @@ +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; + + 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()); + }); + + 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); + }); +}); 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/dry_skin.test.ts b/src/test/abilities/dry_skin.test.ts index 1af8831f25b..a97914660bc 100644 --- a/src/test/abilities/dry_skin.test.ts +++ b/src/test/abilities/dry_skin.test.ts @@ -141,4 +141,18 @@ describe("Abilities - Dry Skin", () => { expect(healthGainedFromWaterShuriken).toBe(healthGainedFromWaterGun); }); + + it("opposing water moves still heal regardless of accuracy check", async () => { + await game.classicMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.WATER_GUN); + enemy.hp = enemy.hp - 1; + await game.phaseInterceptor.to("MoveEffectPhase"); + + await game.move.forceMiss(); + await game.phaseInterceptor.to("BerryPhase", false); + expect(enemy.hp).toBe(enemy.getMaxHp()); + }); }); diff --git a/src/test/abilities/flash_fire.test.ts b/src/test/abilities/flash_fire.test.ts index c3cf31496ea..9c78de99575 100644 --- a/src/test/abilities/flash_fire.test.ts +++ b/src/test/abilities/flash_fire.test.ts @@ -38,7 +38,7 @@ describe("Abilities - Flash Fire", () => { it("immune to Fire-type moves", async () => { game.override.enemyMoveset([Moves.EMBER]).moveset(Moves.SPLASH); - await game.startBattle([Species.BLISSEY]); + await game.classicMode.startBattle([Species.BLISSEY]); const blissey = game.scene.getPlayerPokemon()!; @@ -49,7 +49,7 @@ describe("Abilities - Flash Fire", () => { it("not activate if the Pokémon is protected from the Fire-type move", async () => { game.override.enemyMoveset([Moves.EMBER]).moveset([Moves.PROTECT]); - await game.startBattle([Species.BLISSEY]); + await game.classicMode.startBattle([Species.BLISSEY]); const blissey = game.scene.getPlayerPokemon()!; @@ -60,7 +60,7 @@ describe("Abilities - Flash Fire", () => { it("activated by Will-O-Wisp", async () => { game.override.enemyMoveset([Moves.WILL_O_WISP]).moveset(Moves.SPLASH); - await game.startBattle([Species.BLISSEY]); + await game.classicMode.startBattle([Species.BLISSEY]); const blissey = game.scene.getPlayerPokemon()!; @@ -76,7 +76,7 @@ describe("Abilities - Flash Fire", () => { it("activated after being frozen", async () => { game.override.enemyMoveset([Moves.EMBER]).moveset(Moves.SPLASH); game.override.statusEffect(StatusEffect.FREEZE); - await game.startBattle([Species.BLISSEY]); + await game.classicMode.startBattle([Species.BLISSEY]); const blissey = game.scene.getPlayerPokemon()!; @@ -88,7 +88,7 @@ describe("Abilities - Flash Fire", () => { it("not passing with baton pass", async () => { game.override.enemyMoveset([Moves.EMBER]).moveset([Moves.BATON_PASS]); - await game.startBattle([Species.BLISSEY, Species.CHANSEY]); + await game.classicMode.startBattle([Species.BLISSEY, Species.CHANSEY]); // ensure use baton pass after enemy moved game.move.select(Moves.BATON_PASS); @@ -105,7 +105,7 @@ describe("Abilities - Flash Fire", () => { it("boosts Fire-type move when the ability is activated", async () => { game.override.enemyMoveset([Moves.FIRE_PLEDGE]).moveset([Moves.EMBER, Moves.SPLASH]); game.override.enemyAbility(Abilities.FLASH_FIRE).ability(Abilities.NONE); - await game.startBattle([Species.BLISSEY]); + await game.classicMode.startBattle([Species.BLISSEY]); const blissey = game.scene.getPlayerPokemon()!; const initialHP = 1000; blissey.hp = initialHP; @@ -126,4 +126,33 @@ describe("Abilities - Flash Fire", () => { expect(flashFireDmg).toBeGreaterThan(originalDmg); }, 20000); + + it("still activates regardless of accuracy check", async () => { + game.override.moveset(Moves.FIRE_PLEDGE).enemyMoveset(Moves.EMBER); + game.override.enemyAbility(Abilities.NONE).ability(Abilities.FLASH_FIRE); + game.override.enemySpecies(Species.BLISSEY); + await game.classicMode.startBattle([Species.RATTATA]); + + const blissey = game.scene.getEnemyPokemon()!; + const initialHP = 1000; + blissey.hp = initialHP; + + // first turn + game.move.select(Moves.FIRE_PLEDGE); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); + await game.phaseInterceptor.to("MoveEffectPhase"); + await game.move.forceMiss(); + await game.phaseInterceptor.to(TurnEndPhase); + const originalDmg = initialHP - blissey.hp; + + expect(blissey.hp > 0); + blissey.hp = initialHP; + + // second turn + game.move.select(Moves.FIRE_PLEDGE); + await game.phaseInterceptor.to(TurnEndPhase); + const flashFireDmg = initialHP - blissey.hp; + + expect(flashFireDmg).toBeGreaterThan(originalDmg); + }, 20000); }); 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..4f7d3d83b51 --- /dev/null +++ b/src/test/abilities/illuminate.test.ts @@ -0,0 +1,58 @@ +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; + + 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); + }); + + 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); + }); +}); 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..b0b454dd560 --- /dev/null +++ b/src/test/abilities/no_guard.test.ts @@ -0,0 +1,67 @@ +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; + + 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); + }); + + 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); + }); +}); 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/sap_sipper.test.ts b/src/test/abilities/sap_sipper.test.ts index 5e8cac74c95..b73bc3d9e27 100644 --- a/src/test/abilities/sap_sipper.test.ts +++ b/src/test/abilities/sap_sipper.test.ts @@ -165,4 +165,22 @@ describe("Abilities - Sap Sipper", () => { expect(initialEnemyHp - enemyPokemon.hp).toBe(0); expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1); }); + + it("still activates regardless of accuracy check", async () => { + game.override.moveset(Moves.LEAF_BLADE); + game.override.enemyMoveset(Moves.SPLASH); + game.override.enemySpecies(Species.MAGIKARP); + game.override.enemyAbility(Abilities.SAP_SIPPER); + + await game.classicMode.startBattle(); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.LEAF_BLADE); + await game.phaseInterceptor.to("MoveEffectPhase"); + + await game.move.forceMiss(); + await game.phaseInterceptor.to("BerryPhase", false); + expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1); + }); }); 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/synchronize.test.ts b/src/test/abilities/synchronize.test.ts new file mode 100644 index 00000000000..6e0aa46763f --- /dev/null +++ b/src/test/abilities/synchronize.test.ts @@ -0,0 +1,109 @@ +import { StatusEffect } from "#app/data/status-effect"; +import GameManager from "#app/test/utils/gameManager"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Abilities - Synchronize", () => { + 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") + .startingLevel(100) + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.SYNCHRONIZE) + .moveset([Moves.SPLASH, Moves.THUNDER_WAVE, Moves.SPORE, Moves.PSYCHO_SHIFT]) + .ability(Abilities.NO_GUARD); + }, 20000); + + it("does not trigger when no status is applied by opponent Pokemon", async () => { + await game.classicMode.startBattle([Species.FEEBAS]); + + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getParty()[0].status).toBeUndefined(); + expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase"); + }, 20000); + + it("sets the status of the source pokemon to Paralysis when paralyzed by it", async () => { + await game.classicMode.startBattle([Species.FEEBAS]); + + game.move.select(Moves.THUNDER_WAVE); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getParty()[0].status?.effect).toBe(StatusEffect.PARALYSIS); + expect(game.scene.getEnemyParty()[0].status?.effect).toBe(StatusEffect.PARALYSIS); + expect(game.phaseInterceptor.log).toContain("ShowAbilityPhase"); + }, 20000); + + it("does not trigger on Sleep", async () => { + await game.classicMode.startBattle(); + + game.move.select(Moves.SPORE); + + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getParty()[0].status?.effect).toBeUndefined(); + expect(game.scene.getEnemyParty()[0].status?.effect).toBe(StatusEffect.SLEEP); + expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase"); + }, 20000); + + it("does not trigger when Pokemon is statused by Toxic Spikes", async () => { + game.override + .ability(Abilities.SYNCHRONIZE) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Array(4).fill(Moves.TOXIC_SPIKES)); + + await game.classicMode.startBattle([Species.FEEBAS, Species.MILOTIC]); + + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + + game.doSwitchPokemon(1); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getParty()[0].status?.effect).toBe(StatusEffect.POISON); + expect(game.scene.getEnemyParty()[0].status?.effect).toBeUndefined(); + expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase"); + }, 20000); + + it("shows ability even if it fails to set the status of the opponent Pokemon", async () => { + await game.classicMode.startBattle([Species.PIKACHU]); + + game.move.select(Moves.THUNDER_WAVE); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getParty()[0].status?.effect).toBeUndefined(); + expect(game.scene.getEnemyParty()[0].status?.effect).toBe(StatusEffect.PARALYSIS); + expect(game.phaseInterceptor.log).toContain("ShowAbilityPhase"); + }, 20000); + + it("should activate with Psycho Shift after the move clears the status", async () => { + game.override.statusEffect(StatusEffect.PARALYSIS); + await game.classicMode.startBattle(); + + game.move.select(Moves.PSYCHO_SHIFT); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getParty()[0].status?.effect).toBe(StatusEffect.PARALYSIS); // keeping old gen < V impl for now since it's buggy otherwise + expect(game.scene.getEnemyParty()[0].status?.effect).toBe(StatusEffect.PARALYSIS); + expect(game.phaseInterceptor.log).toContain("ShowAbilityPhase"); + }, 20000); +}); 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 813880c7326..0f285abd98f 100644 --- a/src/test/abilities/unseen_fist.test.ts +++ b/src/test/abilities/unseen_fist.test.ts @@ -8,7 +8,7 @@ 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; @@ -37,13 +37,11 @@ describe("Abilities - Unseen Fist", () => { it( "should cause a contact move to ignore Protect", () => testUnseenFistHitResult(game, Moves.QUICK_ATTACK, Moves.PROTECT, true), - TIMEOUT ); it( "should not cause a non-contact move to ignore Protect", () => testUnseenFistHitResult(game, Moves.ABSORB, Moves.PROTECT, false), - TIMEOUT ); it( @@ -51,19 +49,17 @@ describe("Abilities - Unseen Fist", () => { () => { game.override.passiveAbility(Abilities.LONG_REACH); testUnseenFistHitResult(game, Moves.QUICK_ATTACK, Moves.PROTECT, false); - }, TIMEOUT + } ); it( "should cause a contact move to ignore Wide Guard", () => testUnseenFistHitResult(game, Moves.BREAKING_SWIPE, Moves.WIDE_GUARD, true), - TIMEOUT ); it( "should not cause a non-contact move to ignore Wide Guard", () => testUnseenFistHitResult(game, Moves.BULLDOZE, Moves.WIDE_GUARD, false), - TIMEOUT ); it( @@ -83,7 +79,7 @@ describe("Abilities - Unseen Fist", () => { expect(enemyPokemon.getTag(BattlerTagType.SUBSTITUTE)).toBeUndefined(); expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); - }, TIMEOUT + } ); }); diff --git a/src/test/abilities/volt_absorb.test.ts b/src/test/abilities/volt_absorb.test.ts index 7f3e160c7d0..9bd5de7df57 100644 --- a/src/test/abilities/volt_absorb.test.ts +++ b/src/test/abilities/volt_absorb.test.ts @@ -7,6 +7,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 { BattlerIndex } from "#app/battle"; // See also: TypeImmunityAbAttr describe("Abilities - Volt Absorb", () => { @@ -39,7 +40,7 @@ describe("Abilities - Volt Absorb", () => { game.override.enemySpecies(Species.DUSKULL); game.override.enemyAbility(Abilities.BALL_FETCH); - await game.startBattle(); + await game.classicMode.startBattle(); const playerPokemon = game.scene.getPlayerPokemon()!; @@ -51,4 +52,23 @@ describe("Abilities - Volt Absorb", () => { expect(playerPokemon.getTag(BattlerTagType.CHARGED)).toBeDefined(); expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase"); }); + it("should activate regardless of accuracy checks", async () => { + game.override.moveset(Moves.THUNDERBOLT); + game.override.enemyMoveset(Moves.SPLASH); + game.override.enemySpecies(Species.MAGIKARP); + game.override.enemyAbility(Abilities.VOLT_ABSORB); + + await game.classicMode.startBattle(); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.THUNDERBOLT); + enemyPokemon.hp = enemyPokemon.hp - 1; + 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()); + }); }); 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 index 1ce81850c13..0802b549823 100644 --- a/src/test/battlerTags/substitute.test.ts +++ b/src/test/battlerTags/substitute.test.ts @@ -1,8 +1,7 @@ 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, SubstituteTag, TrappedTag } from "#app/data/battler-tags"; -import { BattlerTagType } from "#app/enums/battler-tag-type"; +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"; @@ -11,8 +10,6 @@ import { MoveEffectPhase } from "#app/phases/move-effect-phase"; vi.mock("#app/battle-scene.js"); -const TIMEOUT = 5 * 1000; // 5 sec timeout - describe("BattlerTag - SubstituteTag", () => { let mockPokemon: Pokemon; @@ -25,7 +22,7 @@ describe("BattlerTag - SubstituteTag", () => { 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 TrappedTag(BattlerTagType.TRAPPED, BattlerTagLapseType.CUSTOM, 0, Moves.NONE, 1); + const trapTag = new BindTag(5, 0); expect(tagFilter(trapTag)).toBeTruthy(); return true; }) as Pokemon["findAndRemoveTags"] @@ -46,7 +43,7 @@ describe("BattlerTag - SubstituteTag", () => { subject.onAdd(mockPokemon); expect(subject.hp).toBe(25); - }, TIMEOUT + } ); it( @@ -68,7 +65,7 @@ describe("BattlerTag - SubstituteTag", () => { expect(subject.sourceInFocus).toBeFalsy(); expect(mockPokemon.scene.triggerPokemonBattleAnim).toHaveBeenCalledTimes(1); expect(mockPokemon.scene.queueMessage).toHaveBeenCalledTimes(1); - }, TIMEOUT + } ); it( @@ -80,7 +77,7 @@ describe("BattlerTag - SubstituteTag", () => { subject.onAdd(mockPokemon); expect(mockPokemon.findAndRemoveTags).toHaveBeenCalledTimes(1); - }, TIMEOUT + } ); }); @@ -115,7 +112,7 @@ describe("BattlerTag - SubstituteTag", () => { expect(mockPokemon.scene.triggerPokemonBattleAnim).toHaveBeenCalledTimes(1); expect(mockPokemon.scene.queueMessage).toHaveBeenCalledTimes(1); - }, TIMEOUT + } ); }); @@ -151,7 +148,7 @@ describe("BattlerTag - SubstituteTag", () => { expect(subject.sourceInFocus).toBeTruthy(); expect(mockPokemon.scene.triggerPokemonBattleAnim).toHaveBeenCalledTimes(1); expect(mockPokemon.scene.queueMessage).not.toHaveBeenCalled(); - }, TIMEOUT + } ); it( @@ -173,7 +170,7 @@ describe("BattlerTag - SubstituteTag", () => { expect(subject.sourceInFocus).toBeFalsy(); expect(mockPokemon.scene.triggerPokemonBattleAnim).toHaveBeenCalledTimes(1); expect(mockPokemon.scene.queueMessage).not.toHaveBeenCalled(); - }, TIMEOUT + } ); /** TODO: Figure out how to mock a MoveEffectPhase correctly for this test */ @@ -201,7 +198,7 @@ describe("BattlerTag - SubstituteTag", () => { expect(mockPokemon.scene.triggerPokemonBattleAnim).not.toHaveBeenCalled(); expect(mockPokemon.scene.queueMessage).toHaveBeenCalledTimes(1); - }, TIMEOUT + } ); it( @@ -213,7 +210,7 @@ describe("BattlerTag - SubstituteTag", () => { vi.spyOn(mockPokemon.scene, "queueMessage").mockReturnValue(); expect(subject.lapse(mockPokemon, BattlerTagLapseType.CUSTOM)).toBeFalsy(); - }, TIMEOUT + } ); it( 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/localization/status-effect.test.ts b/src/test/localization/status-effect.test.ts index 9dcab5aeb5f..72b4dbd3099 100644 --- a/src/test/localization/status-effect.test.ts +++ b/src/test/localization/status-effect.test.ts @@ -18,44 +18,45 @@ describe("status-effect", () => { mockI18next(); const text = getStatusEffectObtainText(statusEffect, pokemonName); - expect(text).toBe("statusEffect:none.obtain"); + console.log("text:", text); + expect(text).toBe(""); const emptySourceText = getStatusEffectObtainText(statusEffect, pokemonName, ""); - expect(emptySourceText).toBe("statusEffect:none.obtain"); + expect(emptySourceText).toBe(""); }); it("should return the source-obtain text", () => { mockI18next(); const text = getStatusEffectObtainText(statusEffect, pokemonName, sourceText); - expect(text).toBe("statusEffect:none.obtainSource"); + expect(text).toBe(""); const emptySourceText = getStatusEffectObtainText(statusEffect, pokemonName, ""); - expect(emptySourceText).not.toBe("statusEffect:none.obtainSource"); + expect(emptySourceText).toBe(""); }); it("should return the activation text", () => { mockI18next(); const text = getStatusEffectActivationText(statusEffect, pokemonName); - expect(text).toBe("statusEffect:none.activation"); + expect(text).toBe(""); }); it("should return the overlap text", () => { mockI18next(); const text = getStatusEffectOverlapText(statusEffect, pokemonName); - expect(text).toBe("statusEffect:none.overlap"); + expect(text).toBe(""); }); it("should return the heal text", () => { mockI18next(); const text = getStatusEffectHealText(statusEffect, pokemonName); - expect(text).toBe("statusEffect:none.heal"); + expect(text).toBe(""); }); it("should return the descriptor", () => { mockI18next(); const text = getStatusEffectDescriptor(statusEffect); - expect(text).toBe("statusEffect:none.description"); + expect(text).toBe(""); }); }); 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/autotomize.test.ts b/src/test/moves/autotomize.test.ts new file mode 100644 index 00000000000..329b92b38fe --- /dev/null +++ b/src/test/moves/autotomize.test.ts @@ -0,0 +1,98 @@ +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("Moves - Autotomize", () => { + 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.AUTOTOMIZE, Moves.KINGS_SHIELD, Moves.FALSE_SWIPE]) + .battleType("single") + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("Autotomize should reduce weight", async () => { + const baseDracozoltWeight = 190; + const oneAutotomizeDracozoltWeight = 90; + const twoAutotomizeDracozoltWeight = 0.1; + const threeAutotomizeDracozoltWeight = 0.1; + + await game.classicMode.startBattle([Species.DRACOZOLT]); + const playerPokemon = game.scene.getPlayerPokemon()!; + expect(playerPokemon.getWeight()).toBe(baseDracozoltWeight); + game.move.select(Moves.AUTOTOMIZE); + await game.toNextTurn(); + expect(playerPokemon.getWeight()).toBe(oneAutotomizeDracozoltWeight); + + game.move.select(Moves.AUTOTOMIZE); + await game.toNextTurn(); + expect(playerPokemon.getWeight()).toBe(twoAutotomizeDracozoltWeight); + + + game.move.select(Moves.AUTOTOMIZE); + await game.toNextTurn(); + expect(playerPokemon.getWeight()).toBe(threeAutotomizeDracozoltWeight); + }, TIMEOUT); + + it("Changing forms should revert weight", async () => { + const baseAegislashWeight = 53; + const autotomizeAegislashWeight = 0.1; + + await game.classicMode.startBattle([Species.AEGISLASH]); + const playerPokemon = game.scene.getPlayerPokemon()!; + + expect(playerPokemon.getWeight()).toBe(baseAegislashWeight); + + game.move.select(Moves.AUTOTOMIZE); + await game.toNextTurn(); + expect(playerPokemon.getWeight()).toBe(autotomizeAegislashWeight); + + // Transform to sword form + game.move.select(Moves.FALSE_SWIPE); + await game.toNextTurn(); + expect(playerPokemon.getWeight()).toBe(baseAegislashWeight); + + game.move.select(Moves.AUTOTOMIZE); + await game.toNextTurn(); + expect(playerPokemon.getWeight()).toBe(autotomizeAegislashWeight); + + // Transform to shield form + game.move.select(Moves.KINGS_SHIELD); + await game.toNextTurn(); + expect(playerPokemon.getWeight()).toBe(baseAegislashWeight); + + game.move.select(Moves.AUTOTOMIZE); + await game.toNextTurn(); + expect(playerPokemon.getWeight()).toBe(autotomizeAegislashWeight); + }, TIMEOUT); + + it("Autotomize should interact with light metal correctly", async () => { + const baseLightGroudonWeight = 475; + const autotomizeLightGroudonWeight = 425; + game.override.ability(Abilities.LIGHT_METAL); + await game.classicMode.startBattle([Species.GROUDON]); + const playerPokemon = game.scene.getPlayerPokemon()!; + expect(playerPokemon.getWeight()).toBe(baseLightGroudonWeight); + game.move.select(Moves.AUTOTOMIZE); + await game.toNextTurn(); + expect(playerPokemon.getWeight()).toBe(autotomizeLightGroudonWeight); + }, 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/chilly_reception.test.ts b/src/test/moves/chilly_reception.test.ts new file mode 100644 index 00000000000..1b5b5cecb4a --- /dev/null +++ b/src/test/moves/chilly_reception.test.ts @@ -0,0 +1,69 @@ +import { Abilities } from "#app/enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { WeatherType } from "#enums/weather-type"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Moves - Chilly Reception", () => { + 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.CHILLY_RECEPTION, Moves.SNOWSCAPE]) + .enemyMoveset(Array(4).fill(Moves.SPLASH)) + .enemyAbility(Abilities.NONE) + .ability(Abilities.NONE); + + }); + + it("should still change the weather if user can't switch out", async () => { + await game.classicMode.startBattle([Species.SLOWKING]); + + game.move.select(Moves.CHILLY_RECEPTION); + + await game.phaseInterceptor.to("BerryPhase", false); + expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW); + }); + + it("should switch out even if it's snowing", async () => { + await game.classicMode.startBattle([Species.SLOWKING, Species.MEOWTH]); + // first turn set up snow with snowscape, try chilly reception on second turn + game.move.select(Moves.SNOWSCAPE); + await game.phaseInterceptor.to("BerryPhase", false); + expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW); + + await game.phaseInterceptor.to("TurnInitPhase", false); + game.move.select(Moves.CHILLY_RECEPTION); + game.doSelectPartyPokemon(1); + + await game.phaseInterceptor.to("BerryPhase", false); + expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW); + expect(game.scene.getPlayerField()[0].species.speciesId).toBe(Species.MEOWTH); + }); + + it("happy case - switch out and weather changes", async () => { + + await game.classicMode.startBattle([Species.SLOWKING, Species.MEOWTH]); + + game.move.select(Moves.CHILLY_RECEPTION); + game.doSelectPartyPokemon(1); + + await game.phaseInterceptor.to("BerryPhase", false); + expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW); + expect(game.scene.getPlayerField()[0].species.speciesId).toBe(Species.MEOWTH); + }); +}); 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..dd7193dc97f 100644 --- a/src/test/moves/dragon_tail.test.ts +++ b/src/test/moves/dragon_tail.test.ts @@ -1,16 +1,11 @@ import { BattlerIndex } from "#app/battle"; import { allMoves } from "#app/data/move"; -import { BattleEndPhase } from "#app/phases/battle-end-phase"; -import { BerryPhase } from "#app/phases/berry-phase"; -import { TurnEndPhase } from "#app/phases/turn-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, expect, test, vi } from "vitest"; -import GameManager from "../utils/gameManager"; - -const TIMEOUT = 20 * 1000; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; describe("Moves - Dragon Tail", () => { let phaserGame: Phaser.Game; @@ -29,7 +24,7 @@ describe("Moves - Dragon Tail", () => { beforeEach(() => { game = new GameManager(phaserGame); game.override.battleType("single") - .moveset([Moves.DRAGON_TAIL, Moves.SPLASH]) + .moveset([Moves.DRAGON_TAIL, Moves.SPLASH, Moves.FLAMETHROWER]) .enemySpecies(Species.WAILORD) .enemyMoveset(Moves.SPLASH) .startingLevel(5) @@ -38,109 +33,110 @@ describe("Moves - Dragon Tail", () => { vi.spyOn(allMoves[Moves.DRAGON_TAIL], "accuracy", "get").mockReturnValue(100); }); - test( - "Single battle should cause opponent to flee, and not crash", - async () => { - await game.startBattle([Species.DRATINI]); + it("should cause opponent to flee, and not crash", async () => { + await game.classicMode.startBattle([Species.DRATINI]); - const enemyPokemon = game.scene.getEnemyPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; - game.move.select(Moves.DRAGON_TAIL); + game.move.select(Moves.DRAGON_TAIL); - await game.phaseInterceptor.to(BerryPhase); + await game.phaseInterceptor.to("BerryPhase"); - const isVisible = enemyPokemon.visible; - const hasFled = enemyPokemon.wildFlee; - expect(!isVisible && hasFled).toBe(true); + const isVisible = enemyPokemon.visible; + 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 - ); + // simply want to test that the game makes it this far without crashing + await game.phaseInterceptor.to("BattleEndPhase"); + }); - test( - "Single battle should cause opponent to flee, display ability, and not crash", - async () => { - game.override.enemyAbility(Abilities.ROUGH_SKIN); - await game.startBattle([Species.DRATINI]); + it("should cause opponent to flee, display ability, and not crash", async () => { + game.override.enemyAbility(Abilities.ROUGH_SKIN); + await game.classicMode.startBattle([Species.DRATINI]); - const leadPokemon = game.scene.getPlayerPokemon()!; - const enemyPokemon = game.scene.getEnemyPokemon()!; + const leadPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; - game.move.select(Moves.DRAGON_TAIL); + game.move.select(Moves.DRAGON_TAIL); - await game.phaseInterceptor.to(BerryPhase); + await game.phaseInterceptor.to("BerryPhase"); - const isVisible = enemyPokemon.visible; - const hasFled = enemyPokemon.wildFlee; - expect(!isVisible && hasFled).toBe(true); - expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp()); - }, TIMEOUT - ); + const isVisible = enemyPokemon.visible; + const hasFled = enemyPokemon.switchOutStatus; + expect(!isVisible && hasFled).toBe(true); + expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp()); + }); - test( - "Double battles should proceed without crashing", - async () => { - game.override.battleType("double").enemyMoveset(Moves.SPLASH); - game.override.moveset([Moves.DRAGON_TAIL, Moves.SPLASH, Moves.FLAMETHROWER]) - .enemyAbility(Abilities.ROUGH_SKIN); - await game.startBattle([Species.DRATINI, Species.DRATINI, Species.WAILORD, Species.WAILORD]); + it("should proceed without crashing in a double battle", async () => { + game.override + .battleType("double").enemyMoveset(Moves.SPLASH) + .enemyAbility(Abilities.ROUGH_SKIN); + await game.classicMode.startBattle([Species.DRATINI, Species.DRATINI, Species.WAILORD, Species.WAILORD]); - const leadPokemon = game.scene.getParty()[0]!; + const leadPokemon = game.scene.getParty()[0]!; - const enemyLeadPokemon = game.scene.getEnemyParty()[0]!; - const enemySecPokemon = game.scene.getEnemyParty()[1]!; + const enemyLeadPokemon = game.scene.getEnemyParty()[0]!; + const enemySecPokemon = game.scene.getEnemyParty()[1]!; - game.move.select(Moves.DRAGON_TAIL, 0, BattlerIndex.ENEMY); - game.move.select(Moves.SPLASH, 1); + game.move.select(Moves.DRAGON_TAIL, 0, BattlerIndex.ENEMY); + game.move.select(Moves.SPLASH, 1); - await game.phaseInterceptor.to(TurnEndPhase); + await game.phaseInterceptor.to("TurnEndPhase"); - const isVisibleLead = enemyLeadPokemon.visible; - const hasFledLead = enemyLeadPokemon.wildFlee; - const isVisibleSec = enemySecPokemon.visible; - const hasFledSec = enemySecPokemon.wildFlee; - expect(!isVisibleLead && hasFledLead && isVisibleSec && !hasFledSec).toBe(true); - expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp()); + const isVisibleLead = enemyLeadPokemon.visible; + const hasFledLead = enemyLeadPokemon.switchOutStatus; + const isVisibleSec = enemySecPokemon.visible; + const hasFledSec = enemySecPokemon.switchOutStatus; + expect(!isVisibleLead && hasFledLead && isVisibleSec && !hasFledSec).toBe(true); + expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp()); - // second turn - game.move.select(Moves.FLAMETHROWER, 0, BattlerIndex.ENEMY_2); - game.move.select(Moves.SPLASH, 1); + // second turn + game.move.select(Moves.FLAMETHROWER, 0, BattlerIndex.ENEMY_2); + game.move.select(Moves.SPLASH, 1); - await game.phaseInterceptor.to(BerryPhase); - expect(enemySecPokemon.hp).toBeLessThan(enemySecPokemon.getMaxHp()); - }, TIMEOUT - ); + await game.phaseInterceptor.to("BerryPhase"); + expect(enemySecPokemon.hp).toBeLessThan(enemySecPokemon.getMaxHp()); + }); - test( - "Flee move redirection works", - async () => { - game.override.battleType("double").enemyMoveset(Moves.SPLASH); - game.override.moveset([Moves.DRAGON_TAIL, Moves.SPLASH, Moves.FLAMETHROWER]); - game.override.enemyAbility(Abilities.ROUGH_SKIN); - await game.startBattle([Species.DRATINI, Species.DRATINI, Species.WAILORD, Species.WAILORD]); + it("should redirect targets upon opponent flee", async () => { + game.override + .battleType("double") + .enemyMoveset(Moves.SPLASH) + .enemyAbility(Abilities.ROUGH_SKIN); + await game.classicMode.startBattle([Species.DRATINI, Species.DRATINI, Species.WAILORD, Species.WAILORD]); - const leadPokemon = game.scene.getParty()[0]!; - const secPokemon = game.scene.getParty()[1]!; + const leadPokemon = game.scene.getParty()[0]!; + const secPokemon = game.scene.getParty()[1]!; - const enemyLeadPokemon = game.scene.getEnemyParty()[0]!; - const enemySecPokemon = game.scene.getEnemyParty()[1]!; + const enemyLeadPokemon = game.scene.getEnemyParty()[0]!; + const enemySecPokemon = game.scene.getEnemyParty()[1]!; - game.move.select(Moves.DRAGON_TAIL, 0, BattlerIndex.ENEMY); - // target the same pokemon, second move should be redirected after first flees - game.move.select(Moves.DRAGON_TAIL, 1, BattlerIndex.ENEMY); + game.move.select(Moves.DRAGON_TAIL, 0, BattlerIndex.ENEMY); + // target the same pokemon, second move should be redirected after first flees + game.move.select(Moves.DRAGON_TAIL, 1, BattlerIndex.ENEMY); - await game.phaseInterceptor.to(BerryPhase); + await game.phaseInterceptor.to("BerryPhase"); - const isVisibleLead = enemyLeadPokemon.visible; - const hasFledLead = enemyLeadPokemon.wildFlee; - const isVisibleSec = enemySecPokemon.visible; - const hasFledSec = enemySecPokemon.wildFlee; - 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 - ); + const isVisibleLead = enemyLeadPokemon.visible; + const hasFledLead = enemyLeadPokemon.switchOutStatus; + const isVisibleSec = enemySecPokemon.visible; + 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()); + }); + + it("doesn't switch out if the target has suction cups", async () => { + game.override.enemyAbility(Abilities.SUCTION_CUPS); + await game.classicMode.startBattle([Species.REGIELEKI]); + + const enemy = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.DRAGON_TAIL); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(enemy.isFullHp()).toBe(false); + }); }); 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 0c24f69c32c..99769b32899 100644 --- a/src/test/moves/guard_swap.test.ts +++ b/src/test/moves/guard_swap.test.ts @@ -11,8 +11,6 @@ import { MoveEndPhase } from "#app/phases/move-end-phase"; describe("Moves - Guard Swap", () => { let phaserGame: Phaser.Game; let game: GameManager; - const TIMEOUT = 20 * 1000; - beforeAll(() => { phaserGame = new Phaser.Game({ type: Phaser.HEADLESS, @@ -65,5 +63,5 @@ describe("Moves - Guard Swap", () => { expect(enemy.getStatStage(s)).toBe(1); } } - }, TIMEOUT); + }); }); 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..14662ac3fce --- /dev/null +++ b/src/test/moves/heal_block.test.ts @@ -0,0 +1,150 @@ +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"; + +// 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()); + }); + + 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); + }); + + 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); + }); + + 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); + }); + + 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); + }); + + 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); + }); + + 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); + }); +}); diff --git a/src/test/moves/heart_swap.test.ts b/src/test/moves/heart_swap.test.ts index f658641d46f..a128549c459 100644 --- a/src/test/moves/heart_swap.test.ts +++ b/src/test/moves/heart_swap.test.ts @@ -11,8 +11,6 @@ import { MoveEndPhase } from "#app/phases/move-end-phase"; describe("Moves - Heart Swap", () => { let phaserGame: Phaser.Game; let game: GameManager; - const TIMEOUT = 20 * 1000; - beforeAll(() => { phaserGame = new Phaser.Game({ type: Phaser.HEADLESS, @@ -60,5 +58,5 @@ describe("Moves - Heart Swap", () => { expect(enemy.getStatStage(s)).toBe(0); expect(player.getStatStage(s)).toBe(1); } - }, TIMEOUT); + }); }); 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 92cd786c050..e9a4b569c92 100644 --- a/src/test/moves/power_swap.test.ts +++ b/src/test/moves/power_swap.test.ts @@ -11,8 +11,6 @@ import { MoveEndPhase } from "#app/phases/move-end-phase"; describe("Moves - Power Swap", () => { let phaserGame: Phaser.Game; let game: GameManager; - const TIMEOUT = 20 * 1000; - beforeAll(() => { phaserGame = new Phaser.Game({ type: Phaser.HEADLESS, @@ -65,5 +63,5 @@ describe("Moves - Power Swap", () => { expect(enemy.getStatStage(s)).toBe(1); } } - }, TIMEOUT); + }); }); 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 index 373d88f0434..67fc557a318 100644 --- a/src/test/moves/relic_song.test.ts +++ b/src/test/moves/relic_song.test.ts @@ -10,8 +10,6 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; describe("Moves - Relic Song", () => { 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 - Relic Song", () => { await game.phaseInterceptor.to("BerryPhase"); expect(meloetta.formIndex).toBe(0); - }, TIMEOUT); + }); it("doesn't swap Meloetta's form during a mono-type challenge", async () => { game.challengeMode.addChallenge(Challenges.SINGLE_TYPE, Type.PSYCHIC + 1, 0); @@ -62,7 +60,7 @@ describe("Moves - Relic Song", () => { await game.toNextTurn(); expect(meloetta.formIndex).toBe(0); - }, TIMEOUT); + }); it("doesn't swap Meloetta's form during biome change (arena reset)", async () => { game.override @@ -77,5 +75,5 @@ describe("Moves - Relic Song", () => { await game.toNextWave(); expect(meloetta.formIndex).toBe(1); - }, TIMEOUT); + }); }); 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..643313f1eae --- /dev/null +++ b/src/test/moves/shell_side_arm.test.ts @@ -0,0 +1,78 @@ +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, expect, it, vi } from "vitest"; + +describe("Moves - Shell Side Arm", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + const shellSideArm = allMoves[Moves.SHELL_SIDE_ARM]; + const shellSideArmAttr = shellSideArm.getAttrs(ShellSideArmCategoryAttr)[0]; + + 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.RAMPARDOS]); + + 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.XURKITREE]); + + 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]); + + 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 index 3976247d489..6c18579e7f6 100644 --- a/src/test/moves/substitute.test.ts +++ b/src/test/moves/substitute.test.ts @@ -16,8 +16,6 @@ import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -const TIMEOUT = 20 * 1000; // 20 sec timeout - describe("Moves - Substitute", () => { let phaserGame: Phaser.Game; let game: GameManager; @@ -57,7 +55,7 @@ describe("Moves - Substitute", () => { await game.phaseInterceptor.to("MoveEndPhase", false); expect(leadPokemon.hp).toBe(Math.ceil(leadPokemon.getMaxHp() * 3/4)); - }, TIMEOUT + } ); it( @@ -81,7 +79,7 @@ describe("Moves - Substitute", () => { expect(leadPokemon.hp).toBe(postSubHp); expect(leadPokemon.getTag(BattlerTagType.SUBSTITUTE)).toBeDefined(); - }, TIMEOUT + } ); it( @@ -107,7 +105,7 @@ describe("Moves - Substitute", () => { expect(leadPokemon.hp).toBe(postSubHp); expect(leadPokemon.getTag(BattlerTagType.SUBSTITUTE)).toBeUndefined(); - }, TIMEOUT + } ); it( @@ -148,7 +146,7 @@ describe("Moves - Substitute", () => { expect(leadPokemon.getTag(BattlerTagType.SUBSTITUTE)).toBeDefined(); expect(leadPokemon.hp).toBeLessThan(postSubHp); - }, TIMEOUT + } ); it( @@ -172,7 +170,7 @@ describe("Moves - Substitute", () => { expect(leadPokemon.getTag(BattlerTagType.SUBSTITUTE)).toBeDefined(); expect(leadPokemon.hp).toBeLessThan(postSubHp); - }, TIMEOUT + } ); it( @@ -192,7 +190,7 @@ describe("Moves - Substitute", () => { await game.phaseInterceptor.to("MoveEndPhase", false); expect(leadPokemon.getStatStage(Stat.ATK)).toBe(2); - }, TIMEOUT + } ); it( @@ -213,7 +211,7 @@ describe("Moves - Substitute", () => { await game.phaseInterceptor.to("BerryPhase", false); expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp()); - }, TIMEOUT + } ); it( @@ -233,7 +231,7 @@ describe("Moves - Substitute", () => { await game.phaseInterceptor.to("BerryPhase", false); expect(leadPokemon.getTag(TrappedTag)).toBeUndefined(); - }, TIMEOUT + } ); it( @@ -253,7 +251,7 @@ describe("Moves - Substitute", () => { await game.phaseInterceptor.to("BerryPhase", false); expect(leadPokemon.getStatStage(Stat.DEF)).toBe(0); - }, TIMEOUT + } ); it( @@ -272,7 +270,7 @@ describe("Moves - Substitute", () => { await game.phaseInterceptor.to("BerryPhase", false); expect(leadPokemon.status?.effect).not.toBe(StatusEffect.PARALYSIS); - }, TIMEOUT + } ); it( @@ -293,7 +291,7 @@ describe("Moves - Substitute", () => { await game.phaseInterceptor.to("BerryPhase", false); expect(leadPokemon.getHeldItems().length).toBe(1); - }, TIMEOUT + } ); it( @@ -314,7 +312,7 @@ describe("Moves - Substitute", () => { await game.phaseInterceptor.to("MoveEndPhase", false); expect(enemyPokemon.getHeldItems().length).toBe(enemyNumItems); - }, TIMEOUT + } ); it( @@ -339,7 +337,7 @@ describe("Moves - Substitute", () => { expect(leadPokemon.getHeldItems().length).toBe(1); expect(enemyPokemon.hp).toBe(enemyPostAttackHp); - }, TIMEOUT + } ); it( @@ -358,7 +356,7 @@ describe("Moves - Substitute", () => { await game.phaseInterceptor.to("BerryPhase", false); expect(leadPokemon.getStatStage(Stat.ATK)).toBe(2); - }, TIMEOUT + } ); it( @@ -404,7 +402,7 @@ describe("Moves - Substitute", () => { const subTag = switchedPokemon.getTag(SubstituteTag)!; expect(subTag).toBeDefined(); expect(subTag.hp).toBe(Math.floor(leadPokemon.getMaxHp() * 1/4)); - }, TIMEOUT + } ); it( @@ -422,7 +420,7 @@ describe("Moves - Substitute", () => { await game.phaseInterceptor.to("BerryPhase", false); expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); - }, TIMEOUT + } ); it( @@ -447,7 +445,7 @@ describe("Moves - Substitute", () => { expect(playerPokemon.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp()); - }, TIMEOUT + } ); it( @@ -467,7 +465,7 @@ describe("Moves - Substitute", () => { await game.phaseInterceptor.to("BerryPhase", false); expect(playerPokemon.getLastXMoves()[0].result).toBe(MoveResult.FAIL); - }, TIMEOUT + } ); it( @@ -488,7 +486,7 @@ describe("Moves - Substitute", () => { await game.phaseInterceptor.to("MoveEndPhase"); expect(enemyPokemon.status?.effect).not.toBe(StatusEffect.BURN); - }, TIMEOUT + } ); it( 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/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..9fb2504c02b --- /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..b73d2a42cc2 --- /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.CRUNCH]); + + 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..88704746a3c --- /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("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..d71b982bfc7 --- /dev/null +++ b/src/test/mystery-encounter/encounters/berries-abound-encounter.test.ts @@ -0,0 +1,255 @@ +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"; +import { Abilities } from "#enums/abilities"; + +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) + .mysteryEncounterTier(MysteryEncounterTier.COMMON) + .startingWave(defaultWave) + .startingBiome(defaultBiome) + .disableTrainerWaves() + .startingModifier([]) + .startingHeldItems([]) + .enemyAbility(Abilities.BALL_FETCH) + .enemyPassiveAbility(Abilities.BALL_FETCH); + + 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 below wave 50", async () => { + game.override.startingWave(41); + 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([0, 1, 0, 1, 1, 0, 0]); + expect(encounterTextSpy).toHaveBeenCalledWith(expect.any(BattleScene), `${namespace}.option.2.selected_bad`); + }); + + it("should start battle if fastest pokemon is slower than boss above wave 50", async () => { + game.override.startingWave(57); + 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..dfa793fe7b5 --- /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, HitHealModifier, LevelIncrementBoosterModifier, MoneyMultiplierModifier, 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 an Amulet Coin", async () => { + scene.money = 200000; + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + await runMysteryEncounterToEnd(game, 1); + + const itemModifier = scene.findModifier(m => m instanceof MoneyMultiplierModifier) as MoneyMultiplierModifier; + + expect(itemModifier).toBeDefined(); + expect(itemModifier?.stackCount).toBe(1); + }); + + it("Should give the player a Shell Bell if they have max stacks of Amulet Coins", async () => { + scene.money = 200000; + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + + // Max Amulet Coins + scene.modifiers = []; + const amuletCoin = generateModifierType(scene, modifierTypes.AMULET_COIN)!.newModifier() as MoneyMultiplierModifier; + amuletCoin.stackCount = 5; + await scene.addModifier(amuletCoin, true, false, false, true); + await scene.updateModifiers(true); + + await runMysteryEncounterToEnd(game, 1); + + const amuletCoinAfter = scene.findModifier(m => m instanceof MoneyMultiplierModifier); + const shellBellAfter = scene.findModifier(m => m instanceof HitHealModifier); + + expect(amuletCoinAfter).toBeDefined(); + expect(amuletCoinAfter?.stackCount).toBe(5); + 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..f91e1a5a13c --- /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(5); + 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..f3723983b14 --- /dev/null +++ b/src/test/mystery-encounter/encounters/teleporting-hijinks-encounter.test.ts @@ -0,0 +1,307 @@ +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() + .enemyAbility(Abilities.BALL_FETCH) + .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, [Species.PIKACHU]); + await runMysteryEncounterToEnd(game, 2, 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..6930195b6cb --- /dev/null +++ b/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts @@ -0,0 +1,228 @@ +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"; +import { Abilities } from "#app/enums/abilities"; + +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) + .startingWave(defaultWave) + .startingBiome(defaultBiome) + .disableTrainerWaves() + .enemyAbility(Abilities.BALL_FETCH) + .enemyPassiveAbility(Abilities.BALL_FETCH); + + 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.COMMON); + 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..3943063d7c0 --- /dev/null +++ b/src/test/mystery-encounter/encounters/uncommon-breed-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 { 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"; +import { Abilities } from "#enums/abilities"; + +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) + .mysteryEncounterTier(MysteryEncounterTier.COMMON) + .startingWave(defaultWave) + .startingBiome(defaultBiome) + .disableTrainerWaves() + .enemyAbility(Abilities.BALL_FETCH) + .enemyPassiveAbility(Abilities.BALL_FETCH); + + 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 below wave 50", 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); + }); + + it.skip("should start a fight against the boss above wave 50", async () => { + game.override.startingWave(57); + 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..7c4523dd9ef 100644 --- a/src/test/reload.test.ts +++ b/src/test/reload.test.ts @@ -38,16 +38,15 @@ describe("Reload", () => { it("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..f48fe3ef228 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/challenges-select-ui-handler.ts b/src/ui/challenges-select-ui-handler.ts index e08736d2b70..924186de789 100644 --- a/src/ui/challenges-select-ui-handler.ts +++ b/src/ui/challenges-select-ui-handler.ts @@ -28,7 +28,7 @@ export default class GameChallengesUiHandler extends UiHandler { private descriptionText: BBCodeText; - private challengeLabels: Array<{ label: Phaser.GameObjects.Text, value: Phaser.GameObjects.Text }>; + private challengeLabels: Array<{ label: Phaser.GameObjects.Text, value: Phaser.GameObjects.Text, leftArrow: Phaser.GameObjects.Image, rightArrow: Phaser.GameObjects.Image }>; private monoTypeValue: Phaser.GameObjects.Sprite; private cursorObj: Phaser.GameObjects.NineSlice | null; @@ -40,6 +40,11 @@ export default class GameChallengesUiHandler extends UiHandler { private optionsWidth: number; + private widestTextBox: number; + + private readonly leftArrowGap: number = 90; // distance from the label to the left arrow + private readonly arrowSpacing: number = 3; // distance between the arrows and the value area + constructor(scene: BattleScene, mode: Mode | null = null) { super(scene, mode); } @@ -47,6 +52,8 @@ export default class GameChallengesUiHandler extends UiHandler { setup() { const ui = this.getUi(); + this.widestTextBox = 0; + this.challengesContainer = this.scene.add.container(1, -(this.scene.game.canvas.height / 6) + 1); this.challengesContainer.setName("challenges"); @@ -135,6 +142,20 @@ export default class GameChallengesUiHandler extends UiHandler { this.valuesContainer.add(label); + const leftArrow = this.scene.add.image(0, 0, "cursor_reverse"); + leftArrow.setName(`challenge-left-arrow-${i}`); + leftArrow.setOrigin(0, 0); + leftArrow.setVisible(false); + leftArrow.setScale(0.75); + this.valuesContainer.add(leftArrow); + + const rightArrow = this.scene.add.image(0, 0, "cursor"); + rightArrow.setName(`challenge-right-arrow-${i}`); + rightArrow.setOrigin(0, 0); + rightArrow.setScale(0.75); + rightArrow.setVisible(false); + this.valuesContainer.add(rightArrow); + const value = addTextObject(this.scene, 0, 28 + i * 16, "", TextStyle.SETTINGS_LABEL); value.setName(`challenge-value-text-${i}`); value.setPositionRelative(label, 100, 0); @@ -142,7 +163,9 @@ export default class GameChallengesUiHandler extends UiHandler { this.challengeLabels[i] = { label: label, - value: value + value: value, + leftArrow: leftArrow, + rightArrow: rightArrow }; } @@ -187,10 +210,26 @@ export default class GameChallengesUiHandler extends UiHandler { */ initLabels(): void { this.setDescription(this.scene.gameMode.challenges[0].getDescription()); + this.widestTextBox = 0; for (let i = 0; i < 9; i++) { if (i < this.scene.gameMode.challenges.length) { this.challengeLabels[i].label.setVisible(true); this.challengeLabels[i].value.setVisible(true); + this.challengeLabels[i].leftArrow.setVisible(true); + this.challengeLabels[i].rightArrow.setVisible(true); + + const tempText = addTextObject(this.scene, 0, 0, "", TextStyle.SETTINGS_LABEL); // this is added here to get the widest text object for this language, which will be used for the arrow placement + + for (let j = 0; j <= this.scene.gameMode.challenges[i].maxValue; j++) { // this goes through each challenge's value to find out what the max width will be + if (this.scene.gameMode.challenges[i].id !== Challenges.SINGLE_TYPE) { + tempText.setText(this.scene.gameMode.challenges[i].getValue(j)); + if (tempText.displayWidth > this.widestTextBox) { + this.widestTextBox = tempText.displayWidth; + } + } + } + + tempText.destroy(); } } } @@ -203,16 +242,33 @@ export default class GameChallengesUiHandler extends UiHandler { let monoTypeVisible = false; for (let i = 0; i < Math.min(9, this.scene.gameMode.challenges.length); i++) { const challenge = this.scene.gameMode.challenges[this.scrollCursor + i]; - this.challengeLabels[i].label.setText(challenge.getName()); + const challengeLabel = this.challengeLabels[i]; + challengeLabel.label.setText(challenge.getName()); + challengeLabel.leftArrow.setPositionRelative(challengeLabel.label, this.leftArrowGap, 4.5); + challengeLabel.leftArrow.setVisible(challenge.value !== 0); + challengeLabel.rightArrow.setPositionRelative(challengeLabel.leftArrow, Math.max(this.monoTypeValue.width, this.widestTextBox) + challengeLabel.leftArrow.displayWidth + 2 * this.arrowSpacing, 0); + challengeLabel.rightArrow.setVisible(challenge.value !== challenge.maxValue); + + // this check looks to make sure that the arrows and value textbox don't take up too much space that they'll clip the right edge of the options background + if (challengeLabel.rightArrow.x + challengeLabel.rightArrow.width + this.optionsBg.rightWidth + this.arrowSpacing > this.optionsWidth) { + // if we go out of bounds of the box, set the x position as far right as we can without going past the box, with this.arrowSpacing to allow a small gap between the arrow and border + challengeLabel.rightArrow.setX(this.optionsWidth - this.arrowSpacing - this.optionsBg.rightWidth); + } + + // this line of code gets the center point between the left and right arrows from their left side (Arrow.x gives middle point), taking into account the width of the arrows + const xLocation = Math.round((challengeLabel.leftArrow.x + challengeLabel.rightArrow.x + challengeLabel.leftArrow.displayWidth) / 2); if (challenge.id === Challenges.SINGLE_TYPE) { - this.monoTypeValue.setPositionRelative(this.challengeLabels[i].label, 113, 8); + this.monoTypeValue.setX(xLocation); + this.monoTypeValue.setY(challengeLabel.label.y + 8); this.monoTypeValue.setFrame(challenge.getValue()); this.monoTypeValue.setVisible(true); - this.challengeLabels[i].value.setVisible(false); + challengeLabel.value.setVisible(false); monoTypeVisible = true; } else { - this.challengeLabels[i].value.setText(challenge.getValue()); - this.challengeLabels[i].value.setVisible(true); + challengeLabel.value.setText(challenge.getValue()); + challengeLabel.value.setX(xLocation); + challengeLabel.value.setOrigin(0.5, 0); + challengeLabel.value.setVisible(true); } } if (!monoTypeVisible) { @@ -244,6 +300,7 @@ export default class GameChallengesUiHandler extends UiHandler { super.show(args); this.startCursor.setVisible(false); + this.updateChallengeArrows(false); this.challengesContainer.setVisible(true); // Should always be false at the start this.hasSelectedChallenge = this.scene.gameMode.challenges.some(c => c.value !== 0); @@ -259,6 +316,21 @@ export default class GameChallengesUiHandler extends UiHandler { return true; } + /* This code updates the challenge starter arrows to be tinted/not tinted when the start button is selected to show they can't be changed + */ + updateChallengeArrows(tinted: boolean) { + for (let i = 0; i < Math.min(9, this.scene.gameMode.challenges.length); i++) { + const challengeLabel = this.challengeLabels[i]; + if (tinted) { + challengeLabel.leftArrow.setTint(0x808080); + challengeLabel.rightArrow.setTint(0x808080); + } else { + challengeLabel.leftArrow.clearTint(); + challengeLabel.rightArrow.clearTint(); + } + } + } + /** * Processes input from a specified button. * This method handles navigation through a UI menu, including movement through menu items @@ -280,6 +352,7 @@ export default class GameChallengesUiHandler extends UiHandler { // If the user presses cancel when the start cursor has been activated, the game deactivates the start cursor and allows typical challenge selection behavior this.startCursor.setVisible(false); this.cursorObj?.setVisible(true); + this.updateChallengeArrows(this.startCursor.visible); } else { this.scene.clearPhaseQueue(); this.scene.pushPhase(new TitlePhase(this.scene)); @@ -294,6 +367,7 @@ export default class GameChallengesUiHandler extends UiHandler { } else { this.startCursor.setVisible(true); this.cursorObj?.setVisible(false); + this.updateChallengeArrows(this.startCursor.visible); } success = true; } else { 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 0ccafdaf0ea..7e9e2de5a19 100644 --- a/src/ui/menu-ui-handler.ts +++ b/src/ui/menu-ui-handler.ts @@ -158,6 +158,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 @@ -489,6 +492,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..edff5c25cda --- /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 270163a3d3e..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) || (pokemon.isFusion() && pokemon.fusionSpecies && pokemonEvolutions.hasOwnProperty(pokemon.fusionSpecies.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..b4e4ad1130d 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); @@ -292,25 +288,29 @@ export default class RunInfoUiHandler extends UiHandler { private parseTrainerDefeat(enemyContainer: Phaser.GameObjects.Container) { // Creating the trainer sprite and adding it to enemyContainer const tObj = this.runInfo.trainer.toTrainer(this.scene); - const tObjSpriteKey = tObj.config.getSpriteKey(this.runInfo.trainer.variant === TrainerVariant.FEMALE, false); - const tObjSprite = this.scene.add.sprite(0, 5, tObjSpriteKey); - if (this.runInfo.trainer.variant === TrainerVariant.DOUBLE) { - const doubleContainer = this.scene.add.container(5, 8); - tObjSprite.setPosition(-3, -3); - const tObjPartnerSpriteKey = tObj.config.getSpriteKey(true, true); - const tObjPartnerSprite = this.scene.add.sprite(5, -3, tObjPartnerSpriteKey); - // Double Trainers have smaller sprites than Single Trainers - tObjPartnerSprite.setScale(0.20); - tObjSprite.setScale(0.20); - doubleContainer.add(tObjSprite); - doubleContainer.add(tObjPartnerSprite); - doubleContainer.setPosition(12, 38); - enemyContainer.add(doubleContainer); - } else { - tObjSprite.setScale(0.35, 0.35); - tObjSprite.setPosition(12, 28); - enemyContainer.add(tObjSprite); - } + + // Loads trainer assets on demand, as they are not loaded by default in the scene + tObj.config.loadAssets(this.scene, this.runInfo.trainer.variant).then(() => { + const tObjSpriteKey = tObj.config.getSpriteKey(this.runInfo.trainer.variant === TrainerVariant.FEMALE, false); + const tObjSprite = this.scene.add.sprite(0, 5, tObjSpriteKey); + if (this.runInfo.trainer.variant === TrainerVariant.DOUBLE) { + const doubleContainer = this.scene.add.container(5, 8); + tObjSprite.setPosition(-3, -3); + const tObjPartnerSpriteKey = tObj.config.getSpriteKey(true, true); + const tObjPartnerSprite = this.scene.add.sprite(5, -3, tObjPartnerSpriteKey); + // Double Trainers have smaller sprites than Single Trainers + tObjPartnerSprite.setScale(0.20); + tObjSprite.setScale(0.20); + doubleContainer.add(tObjSprite); + doubleContainer.add(tObjPartnerSprite); + doubleContainer.setPosition(12, 38); + enemyContainer.add(doubleContainer); + } else { + tObjSprite.setScale(0.35, 0.35); + tObjSprite.setPosition(12, 28); + enemyContainer.add(tObjSprite); + } + }); // Determining which Terastallize Modifier belongs to which Pokemon // Creates a dictionary {PokemonId: TeraShardType} @@ -381,10 +381,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 +399,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 +863,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 +877,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 +888,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 ac6af5cbb04..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)); @@ -2533,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]; @@ -2569,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/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 decda017d94..2d9c3fca65b 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"; @@ -583,10 +584,18 @@ export function capitalizeString(str: string, sep: string, lowerFirstChar: boole * Returns if an object is null or undefined * @param object */ -export function isNullOrUndefined(object: any): boolean { +export function isNullOrUndefined(object: any): object is undefined | null { 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: {