updating branch
2
.github/workflows/eslint.yml
vendored
@ -11,6 +11,8 @@ on:
|
||||
branches:
|
||||
- main # Trigger on pull request events targeting the main branch
|
||||
- beta # Trigger on pull request events targeting the beta branch
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
|
||||
jobs:
|
||||
run-linters: # Define a job named "run-linters"
|
||||
|
2
.github/workflows/github-pages.yml
vendored
@ -8,6 +8,8 @@ on:
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
|
||||
jobs:
|
||||
pages:
|
||||
|
30
.github/workflows/test-shard-template.yml
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
name: Test Template
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
project:
|
||||
required: true
|
||||
type: string
|
||||
shard:
|
||||
required: true
|
||||
type: number
|
||||
totalShards:
|
||||
required: true
|
||||
type: number
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Shard ${{ inputs.shard }} of ${{ inputs.totalShards }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Install Node.js dependencies
|
||||
run: npm ci
|
||||
- name: Run tests
|
||||
run: npx vitest --project ${{ inputs.project }} --shard=${{ inputs.shard }}/${{ inputs.totalShards }} ${{ !runner.debug && '--silent' || '' }}
|
44
.github/workflows/tests.yml
vendored
@ -11,23 +11,37 @@ on:
|
||||
branches:
|
||||
- main # Trigger on pull request events targeting the main branch
|
||||
- beta # Trigger on pull request events targeting the beta branch
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
|
||||
jobs:
|
||||
run-tests: # Define a job named "run-tests"
|
||||
name: Run tests # Human-readable name for the job
|
||||
runs-on: ubuntu-latest # Specify the latest Ubuntu runner for the job
|
||||
|
||||
pre-test:
|
||||
name: Run Pre-test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out Git repository # Step to check out the repository
|
||||
uses: actions/checkout@v4 # Use the checkout action version 4
|
||||
|
||||
- name: Set up Node.js # Step to set up Node.js environment
|
||||
uses: actions/setup-node@v4 # Use the setup-node action version 4
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
node-version: 20 # Specify Node.js version 20
|
||||
path: tests-action
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Install Node.js dependencies
|
||||
working-directory: tests-action
|
||||
run: npm ci
|
||||
- name: Run Pre-test
|
||||
working-directory: tests-action
|
||||
run: npx vitest run --project pre ${{ !runner.debug && '--silent' || '' }}
|
||||
|
||||
- name: Install Node.js dependencies # Step to install Node.js dependencies
|
||||
run: npm ci # Use 'npm ci' to install dependencies
|
||||
|
||||
- name: tests # Step to run tests
|
||||
run: npm run test:silent
|
||||
run-tests:
|
||||
name: Run Tests
|
||||
needs: [pre-test]
|
||||
strategy:
|
||||
matrix:
|
||||
shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
||||
uses: ./.github/workflows/test-shard-template.yml
|
||||
with:
|
||||
project: main
|
||||
shard: ${{ matrix.shard }}
|
||||
totalShards: 10
|
@ -4,7 +4,8 @@ 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" or "ability".
|
||||
* @param {string} type - The type of test to create. Either "move", "ability",
|
||||
* or "item".
|
||||
* @param {string} fileName - The name of the file to create.
|
||||
* @example npm run create-test move tackle
|
||||
*/
|
||||
@ -19,51 +20,58 @@ const type = args[0]; // "move" or "ability"
|
||||
let fileName = args[1]; // The file name
|
||||
|
||||
if (!type || !fileName) {
|
||||
console.error('Please provide both a type ("move" or "ability") and a file name.');
|
||||
process.exit(1);
|
||||
console.error('Please provide a type ("move", "ability", or "item") and a file name.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Convert fileName from to snake_case if camelCase is given
|
||||
fileName = fileName.replace(/([a-z])([A-Z])/g, '$1_$2').toLowerCase();
|
||||
// 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
|
||||
|
||||
// Format the description for the test case
|
||||
const formattedName = fileName
|
||||
.replace(/_/g, ' ')
|
||||
.replace(/\b\w/g, char => char.toUpperCase());
|
||||
.replace(/_/g, ' ')
|
||||
.replace(/\b\w/g, char => char.toUpperCase());
|
||||
|
||||
// Determine the directory based on the type
|
||||
let dir;
|
||||
let description;
|
||||
if (type === 'move') {
|
||||
dir = path.join(__dirname, 'src', 'test', 'moves');
|
||||
description = `Moves - ${formattedName}`;
|
||||
dir = path.join(__dirname, 'src', 'test', 'moves');
|
||||
description = `Moves - ${formattedName}`;
|
||||
} else if (type === 'ability') {
|
||||
dir = path.join(__dirname, 'src', 'test', 'abilities');
|
||||
description = `Abilities - ${formattedName}`;
|
||||
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" or "ability".');
|
||||
process.exit(1);
|
||||
console.error('Invalid type. Please use "move", "ability", or "item".');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Ensure the directory exists
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
// Create the file with the given name
|
||||
const filePath = path.join(dir, `${fileName}.test.ts`);
|
||||
|
||||
if (fs.existsSync(filePath)) {
|
||||
console.error(`File "${fileName}.test.ts" already exists.`);
|
||||
process.exit(1);
|
||||
console.error(`File "${fileName}.test.ts" already exists.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Define the content template
|
||||
const content = `import { Abilities } from "#enums/abilities";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
import GameManager from "#test/utils/gameManager";
|
||||
import { SPLASH_ONLY } from "#test/utils/testUtils";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, it } from "vitest";
|
||||
import { afterEach, beforeAll, beforeEach, describe, it, expect } from "vitest";
|
||||
|
||||
describe("${description}", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
@ -83,14 +91,15 @@ describe("${description}", () => {
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.moveset([Moves.SPLASH])
|
||||
.battleType("single")
|
||||
.enemyAbility(Abilities.BALL_FETCH)
|
||||
.enemyMoveset(SPLASH_ONLY);
|
||||
.enemyMoveset(Moves.SPLASH);
|
||||
});
|
||||
|
||||
it("test case", async () => {
|
||||
// await game.classicMode.startBattle();
|
||||
// game.move.select();
|
||||
// await game.classicMode.startBattle([Species.MAGIKARP]);
|
||||
// game.move.select(Moves.SPLASH);
|
||||
}, TIMEOUT);
|
||||
});
|
||||
`;
|
||||
@ -98,4 +107,4 @@ describe("${description}", () => {
|
||||
// Write the template content to the file
|
||||
fs.writeFileSync(filePath, content, 'utf8');
|
||||
|
||||
console.log(`File created at: ${filePath}`);
|
||||
console.log(`File created at: ${filePath}`);
|
||||
|
@ -191,15 +191,15 @@ Now that the enemy Pokémon with the best matchup score is on the field (assumin
|
||||
|
||||
We then need to apply a 2x multiplier for the move's type effectiveness and a 1.5x multiplier since STAB applies. After applying these multipliers, the final score for this move is **75**.
|
||||
|
||||
- **Swords Dance**: As a non-attacking move, this move's benefit score is derived entirely from the sum of its attributes' benefit scores. Swords Dance's `StatChangeAttr` has a user benefit score of 0 and a target benefit score that, in this case, simplifies to
|
||||
- **Swords Dance**: As a non-attacking move, this move's benefit score is derived entirely from the sum of its attributes' benefit scores. Swords Dance's `StatStageChangeAttr` has a user benefit score of 0 and a target benefit score that, in this case, simplifies to
|
||||
|
||||
$\text{TBS}=4\times \text{levels} + (-2\times \text{sign(levels)})$
|
||||
|
||||
where `levels` is the number of stat stages added by the attribute (in this case, +2). The final score for this move is **6** (Note: because this move is self-targeted, we don't flip the sign of TBS when computing the target score).
|
||||
|
||||
- **Crush Claw**: This move is a 75-power Normal-type physical attack with a 50 percent chance to lower the target's Defense by one stage. The additional effect is implemented by the same `StatChangeAttr` as Swords Dance, so we can use the same formulas from before to compute the total TBS and base target score.
|
||||
- **Crush Claw**: This move is a 75-power Normal-type physical attack with a 50 percent chance to lower the target's Defense by one stage. The additional effect is implemented by the same `StatStageChangeAttr` as Swords Dance, so we can use the same formulas from before to compute the total TBS and base target score.
|
||||
|
||||
$\text{TBS}=\text{getTargetBenefitScore(StatChangeAttr)}-\text{attackScore}$
|
||||
$\text{TBS}=\text{getTargetBenefitScore(StatStageChangeAttr)}-\text{attackScore}$
|
||||
|
||||
$\text{TBS}=(-4 + 2)-(-2\times 2 + \lfloor \frac{75}{5} \rfloor)=-2-11=-13$
|
||||
|
||||
@ -221,4 +221,4 @@ When implementing a new move attribute, it's important to override `MoveAttr`'s
|
||||
- A move's **user benefit score (UBS)** incentivizes (or discourages) the move's usage in general. A positive UBS gives the move more incentive to be used, while a negative UBS gives the move less incentive.
|
||||
- A move's **target benefit score (TBS)** incentivizes (or discourages) the move's usage on a specific target. A positive TBS indicates the move is better used on the user or its allies, while a negative TBS indicates the move is better used on enemies.
|
||||
- **The total benefit score (UBS + TBS) of a move should never be 0.** The move selection algorithm assumes the move's benefit score is unimplemented if the total score is 0 and penalizes the move's usage as a result. With status moves especially, it's important to have some form of implementation among the move's attributes to avoid this scenario.
|
||||
- **Score functions that use formulas should include comments.** If your attribute requires complex logic or formulas to calculate benefit scores, please add comments to explain how the logic works and its intended effect on the enemy's decision making.
|
||||
- **Score functions that use formulas should include comments.** If your attribute requires complex logic or formulas to calculate benefit scores, please add comments to explain how the logic works and its intended effect on the enemy's decision making.
|
||||
|
@ -1,7 +1,7 @@
|
||||
import tseslint from '@typescript-eslint/eslint-plugin';
|
||||
import stylisticTs from '@stylistic/eslint-plugin-ts'
|
||||
import parser from '@typescript-eslint/parser';
|
||||
// import imports from 'eslint-plugin-import'; // Disabled due to not being compatible with eslint v9
|
||||
import importX from 'eslint-plugin-import-x';
|
||||
|
||||
export default [
|
||||
{
|
||||
@ -11,7 +11,7 @@ export default [
|
||||
parser: parser
|
||||
},
|
||||
plugins: {
|
||||
// imports: imports.configs.recommended // Disabled due to not being compatible with eslint v9
|
||||
"import-x": importX,
|
||||
'@stylistic/ts': stylisticTs,
|
||||
'@typescript-eslint': tseslint
|
||||
},
|
||||
@ -39,7 +39,8 @@ export default [
|
||||
}],
|
||||
"space-before-blocks": ["error", "always"], // Enforces a space before blocks
|
||||
"keyword-spacing": ["error", { "before": true, "after": true }], // Enforces spacing before and after keywords
|
||||
"comma-spacing": ["error", { "before": false, "after": true }] // Enforces spacing after comma
|
||||
"comma-spacing": ["error", { "before": false, "after": true }], // Enforces spacing after comma
|
||||
"import-x/extensions": ["error", "never", { "json": "always" }], // Enforces no extension for imports unless json
|
||||
}
|
||||
}
|
||||
]
|
||||
|
13
lefthook.yml
@ -2,6 +2,15 @@ pre-commit:
|
||||
parallel: true
|
||||
commands:
|
||||
eslint:
|
||||
glob: '*.{js,jsx,ts,tsx}'
|
||||
glob: "*.{js,jsx,ts,tsx}"
|
||||
run: npx eslint --fix {staged_files}
|
||||
stage_fixed: true
|
||||
stage_fixed: true
|
||||
skip:
|
||||
- merge
|
||||
- rebase
|
||||
|
||||
pre-push:
|
||||
commands:
|
||||
eslint:
|
||||
glob: "*.{js,ts,jsx,tsx}"
|
||||
run: npx eslint --fix {push_files}
|
200
package-lock.json
generated
@ -28,6 +28,7 @@
|
||||
"@vitest/coverage-istanbul": "^2.0.4",
|
||||
"dependency-cruiser": "^16.3.10",
|
||||
"eslint": "^9.7.0",
|
||||
"eslint-plugin-import-x": "^4.2.1",
|
||||
"jsdom": "^24.0.0",
|
||||
"lefthook": "^1.6.12",
|
||||
"phaser3spectorjs": "^0.0.8",
|
||||
@ -2505,6 +2506,19 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/doctrine": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
|
||||
"integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"esutils": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eastasianwidth": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
@ -2687,6 +2701,155 @@
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-import-resolver-node": {
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz",
|
||||
"integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^3.2.7",
|
||||
"is-core-module": "^2.13.0",
|
||||
"resolve": "^1.22.4"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-import-resolver-node/node_modules/debug": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
|
||||
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-import-x": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-import-x/-/eslint-plugin-import-x-4.2.1.tgz",
|
||||
"integrity": "sha512-WWi2GedccIJa0zXxx3WDnTgouGQTtdYK1nhXMwywbqqAgB0Ov+p1pYBsWh3VaB0bvBOwLse6OfVII7jZD9xo5Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/utils": "^8.1.0",
|
||||
"debug": "^4.3.4",
|
||||
"doctrine": "^3.0.0",
|
||||
"eslint-import-resolver-node": "^0.3.9",
|
||||
"get-tsconfig": "^4.7.3",
|
||||
"is-glob": "^4.0.3",
|
||||
"minimatch": "^9.0.3",
|
||||
"semver": "^7.6.3",
|
||||
"stable-hash": "^0.0.4",
|
||||
"tslib": "^2.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-import-x/node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.5.0.tgz",
|
||||
"integrity": "sha512-06JOQ9Qgj33yvBEx6tpC8ecP9o860rsR22hWMEd12WcTRrfaFgHr2RB/CA/B+7BMhHkXT4chg2MyboGdFGawYg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.5.0",
|
||||
"@typescript-eslint/visitor-keys": "8.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-import-x/node_modules/@typescript-eslint/types": {
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.5.0.tgz",
|
||||
"integrity": "sha512-qjkormnQS5wF9pjSi6q60bKUHH44j2APxfh9TQRXK8wbYVeDYYdYJGIROL87LGZZ2gz3Rbmjc736qyL8deVtdw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-import-x/node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.5.0.tgz",
|
||||
"integrity": "sha512-vEG2Sf9P8BPQ+d0pxdfndw3xIXaoSjliG0/Ejk7UggByZPKXmJmw3GW5jV2gHNQNawBUyfahoSiCFVov0Ruf7Q==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.5.0",
|
||||
"@typescript-eslint/visitor-keys": "8.5.0",
|
||||
"debug": "^4.3.4",
|
||||
"fast-glob": "^3.3.2",
|
||||
"is-glob": "^4.0.3",
|
||||
"minimatch": "^9.0.4",
|
||||
"semver": "^7.6.0",
|
||||
"ts-api-utils": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-import-x/node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.5.0.tgz",
|
||||
"integrity": "sha512-6yyGYVL0e+VzGYp60wvkBHiqDWOpT63pdMV2CVG4LVDd5uR6q1qQN/7LafBZtAtNIn/mqXjsSeS5ggv/P0iECw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.4.0",
|
||||
"@typescript-eslint/scope-manager": "8.5.0",
|
||||
"@typescript-eslint/types": "8.5.0",
|
||||
"@typescript-eslint/typescript-estree": "8.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-import-x/node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.5.0.tgz",
|
||||
"integrity": "sha512-yTPqMnbAZJNy2Xq2XU8AdtOW9tJIr+UQb64aXB9f3B1498Zx9JorVgFJcZpEc9UBuCCrdzKID2RGAMkYcDtZOw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.5.0",
|
||||
"eslint-visitor-keys": "^3.4.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-scope": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz",
|
||||
@ -3143,6 +3306,19 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/get-tsconfig": {
|
||||
"version": "4.8.0",
|
||||
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.0.tgz",
|
||||
"integrity": "sha512-Pgba6TExTZ0FJAn1qkJAjIeKoDJ3CsI2ChuLohJnZl/tTU8MVrq3b+2t5UOPfRa4RMsorClBjJALkJUMjG1PAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"resolve-pkg-maps": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/glob-parent": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||
@ -4854,6 +5030,16 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-pkg-maps": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/reusify": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
|
||||
@ -5069,6 +5255,13 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stable-hash": {
|
||||
"version": "0.0.4",
|
||||
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.4.tgz",
|
||||
"integrity": "sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/stackback": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
|
||||
@ -5460,6 +5653,13 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
|
||||
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==",
|
||||
"dev": true,
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
|
@ -32,6 +32,7 @@
|
||||
"@vitest/coverage-istanbul": "^2.0.4",
|
||||
"dependency-cruiser": "^16.3.10",
|
||||
"eslint": "^9.7.0",
|
||||
"eslint-plugin-import-x": "^4.2.1",
|
||||
"jsdom": "^24.0.0",
|
||||
"lefthook": "^1.6.12",
|
||||
"phaser3spectorjs": "^0.0.8",
|
||||
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 1.3 KiB |
22
public/images/pokemon/variant/465.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"1": {
|
||||
"529cc5": "8153c7",
|
||||
"d65a94": "5ad662",
|
||||
"3a73ad": "6b3aad",
|
||||
"bd216b": "21bd69",
|
||||
"5a193a": "195a2a",
|
||||
"193a63": "391963",
|
||||
"295a84": "472984"
|
||||
},
|
||||
"2": {
|
||||
"529cc5": "ffedb6",
|
||||
"d65a94": "e67d2f",
|
||||
"3a73ad": "ebc582",
|
||||
"bd216b": "b35131",
|
||||
"31313a": "3d1519",
|
||||
"5a193a": "752e2e",
|
||||
"193a63": "705040",
|
||||
"295a84": "ad875a",
|
||||
"4a4a52": "57211a"
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 43 KiB |
@ -1691,8 +1691,8 @@
|
||||
],
|
||||
"465": [
|
||||
0,
|
||||
2,
|
||||
2
|
||||
1,
|
||||
1
|
||||
],
|
||||
"466": [
|
||||
1,
|
||||
@ -3980,6 +3980,11 @@
|
||||
1,
|
||||
1
|
||||
],
|
||||
"465": [
|
||||
0,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"592": [
|
||||
1,
|
||||
1,
|
||||
@ -5690,7 +5695,7 @@
|
||||
"465": [
|
||||
0,
|
||||
1,
|
||||
2
|
||||
1
|
||||
],
|
||||
"466": [
|
||||
2,
|
||||
@ -8008,6 +8013,11 @@
|
||||
1,
|
||||
1
|
||||
],
|
||||
"465": [
|
||||
0,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"592": [
|
||||
1,
|
||||
1,
|
||||
|
@ -8,5 +8,14 @@
|
||||
"bd216b": "21bd69",
|
||||
"31313a": "31313a",
|
||||
"d65a94": "5ad662"
|
||||
},
|
||||
"2": {
|
||||
"5a193a": "752e2e",
|
||||
"31313a": "3d1519",
|
||||
"d65a94": "e67d2f",
|
||||
"3a73ad": "ebc582",
|
||||
"295a84": "ad875a",
|
||||
"bd216b": "b35131",
|
||||
"193a63": "705040"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 34 KiB |
21
public/images/pokemon/variant/back/female/465.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"1": {
|
||||
"193a63": "391963",
|
||||
"295a84": "472984",
|
||||
"3a73ad": "6b3aad",
|
||||
"000000": "000000",
|
||||
"5a193a": "195a2a",
|
||||
"bd216b": "21bd69",
|
||||
"31313a": "31313a",
|
||||
"d65a94": "5ad662"
|
||||
},
|
||||
"2": {
|
||||
"5a193a": "752e2e",
|
||||
"31313a": "3d1519",
|
||||
"d65a94": "e67d2f",
|
||||
"3a73ad": "ebc582",
|
||||
"295a84": "ad875a",
|
||||
"bd216b": "b35131",
|
||||
"193a63": "705040"
|
||||
}
|
||||
}
|
22
public/images/pokemon/variant/female/465.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"1": {
|
||||
"529cc5": "8153c7",
|
||||
"d65a94": "5ad662",
|
||||
"3a73ad": "6b3aad",
|
||||
"bd216b": "21bd69",
|
||||
"5a193a": "195a2a",
|
||||
"193a63": "391963",
|
||||
"295a84": "472984"
|
||||
},
|
||||
"2": {
|
||||
"529cc5": "ffedb6",
|
||||
"d65a94": "e67d2f",
|
||||
"3a73ad": "ebc582",
|
||||
"bd216b": "b35131",
|
||||
"31313a": "3d1519",
|
||||
"5a193a": "752e2e",
|
||||
"193a63": "705040",
|
||||
"295a84": "ad875a",
|
||||
"4a4a52": "57211a"
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 6.9 KiB |
BIN
public/images/ui/egg_summary_bg.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
public/images/ui/egg_summary_bg_blank.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
public/images/ui/icon_egg_move.png
Normal file
After Width: | Height: | Size: 179 B |
BIN
public/images/ui/legacy/egg_summary_bg.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
public/images/ui/legacy/icon_egg_move.png
Normal file
After Width: | Height: | Size: 179 B |
BIN
public/images/ui/legacy/settings_icon.png
Normal file
After Width: | Height: | Size: 261 B |
Before Width: | Height: | Size: 799 B |
Before Width: | Height: | Size: 807 B |
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 799 B |
Before Width: | Height: | Size: 800 B |
Before Width: | Height: | Size: 799 B |
BIN
public/images/ui/settings_icon.png
Normal file
After Width: | Height: | Size: 261 B |
@ -1,3 +1,3 @@
|
||||
import BattleScene from "#app/battle-scene.js";
|
||||
import BattleScene from "#app/battle-scene";
|
||||
|
||||
export type ConditionFn = (scene: BattleScene, args?: any[]) => boolean;
|
||||
|
2
src/@types/i18next.d.ts
vendored
@ -1,4 +1,4 @@
|
||||
import { type enConfig } from "#app/locales/en/config.js";
|
||||
import { type enConfig } from "#app/locales/en/config";
|
||||
import { TOptions } from "i18next";
|
||||
|
||||
//TODO: this needs to be type properly in the future
|
||||
|
@ -63,7 +63,7 @@ import { Moves } from "#enums/moves";
|
||||
import { PlayerGender } from "#enums/player-gender";
|
||||
import { Species } from "#enums/species";
|
||||
import { UiTheme } from "#enums/ui-theme";
|
||||
import { TimedEventManager } from "#app/timed-event-manager.js";
|
||||
import { TimedEventManager } from "#app/timed-event-manager";
|
||||
import i18next from "i18next";
|
||||
import { TrainerType } from "#enums/trainer-type";
|
||||
import { battleSpecDialogue } from "./data/dialogue";
|
||||
@ -130,7 +130,7 @@ export default class BattleScene extends SceneBase {
|
||||
public gameSpeed: integer = 1;
|
||||
public damageNumbersMode: integer = 0;
|
||||
public reroll: boolean = false;
|
||||
public shopCursorTarget: number = ShopCursorTarget.CHECK_TEAM;
|
||||
public shopCursorTarget: number = ShopCursorTarget.REWARDS;
|
||||
public showMovesetFlyout: boolean = true;
|
||||
public showArenaFlyout: boolean = true;
|
||||
public showTimeOfDayWidget: boolean = true;
|
||||
@ -855,7 +855,7 @@ export default class BattleScene extends SceneBase {
|
||||
overrideModifiers(this, false);
|
||||
overrideHeldItems(this, pokemon, false);
|
||||
if (boss && !dataSource) {
|
||||
const secondaryIvs = Utils.getIvsFromId(Utils.randSeedInt(4294967295));
|
||||
const secondaryIvs = Utils.getIvsFromId(Utils.randSeedInt(4294967296));
|
||||
|
||||
for (let s = 0; s < pokemon.ivs.length; s++) {
|
||||
pokemon.ivs[s] = Math.round(Phaser.Math.Linear(Math.min(pokemon.ivs[s], secondaryIvs[s]), Math.max(pokemon.ivs[s], secondaryIvs[s]), 0.75));
|
||||
@ -961,6 +961,16 @@ export default class BattleScene extends SceneBase {
|
||||
this.offsetGym = this.gameMode.isClassic && this.getGeneratedOffsetGym();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random number using the current battle's seed
|
||||
*
|
||||
* This calls {@linkcode Battle.randSeedInt}(`scene`, {@linkcode range}, {@linkcode min}) in `src/battle.ts`
|
||||
* which calls {@linkcode Utils.randSeedInt randSeedInt}({@linkcode range}, {@linkcode min}) in `src/utils.ts`
|
||||
*
|
||||
* @param range How large of a range of random numbers to choose from. If {@linkcode range} <= 1, returns {@linkcode min}
|
||||
* @param min The minimum integer to pick, default `0`
|
||||
* @returns A random integer between {@linkcode min} and ({@linkcode min} + {@linkcode range} - 1)
|
||||
*/
|
||||
randBattleSeedInt(range: integer, min: integer = 0): integer {
|
||||
return this.currentBattle?.randSeedInt(this, range, min);
|
||||
}
|
||||
@ -974,6 +984,7 @@ export default class BattleScene extends SceneBase {
|
||||
|
||||
this.setSeed(Overrides.SEED_OVERRIDE || Utils.randomString(24));
|
||||
console.log("Seed:", this.seed);
|
||||
this.resetSeed(); // Properly resets RNG after saving and quitting a session
|
||||
|
||||
this.disableMenu = false;
|
||||
|
||||
@ -1111,7 +1122,8 @@ export default class BattleScene extends SceneBase {
|
||||
doubleTrainer = false;
|
||||
}
|
||||
}
|
||||
newTrainer = trainerData !== undefined ? trainerData.toTrainer(this) : new Trainer(this, trainerType, doubleTrainer ? TrainerVariant.DOUBLE : Utils.randSeedInt(2) ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT);
|
||||
const variant = doubleTrainer ? TrainerVariant.DOUBLE : (Utils.randSeedInt(2) ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT);
|
||||
newTrainer = trainerData !== undefined ? trainerData.toTrainer(this) : new Trainer(this, trainerType, variant);
|
||||
this.field.add(newTrainer);
|
||||
}
|
||||
}
|
||||
@ -2181,8 +2193,14 @@ export default class BattleScene extends SceneBase {
|
||||
return true;
|
||||
}
|
||||
|
||||
findPhase(phaseFilter: (phase: Phase) => boolean): Phase | undefined {
|
||||
return this.phaseQueue.find(phaseFilter);
|
||||
/**
|
||||
* Find a specific {@linkcode Phase} in the phase queue.
|
||||
*
|
||||
* @param phaseFilter filter function to use to find the wanted phase
|
||||
* @returns the found phase or undefined if none found
|
||||
*/
|
||||
findPhase<P extends Phase = Phase>(phaseFilter: (phase: P) => boolean): P | undefined {
|
||||
return this.phaseQueue.find(phaseFilter) as P;
|
||||
}
|
||||
|
||||
tryReplacePhase(phaseFilter: (phase: Phase) => boolean, phase: Phase): boolean {
|
||||
@ -2619,7 +2637,7 @@ export default class BattleScene extends SceneBase {
|
||||
if (mods.length < 1) {
|
||||
return mods;
|
||||
}
|
||||
const rand = Math.floor(Utils.randSeedInt(mods.length));
|
||||
const rand = Utils.randSeedInt(mods.length);
|
||||
return [mods[rand], ...shuffleModifiers(mods.filter((_, i) => i !== rand))];
|
||||
};
|
||||
modifiers = shuffleModifiers(modifiers);
|
||||
@ -2741,6 +2759,35 @@ export default class BattleScene extends SceneBase {
|
||||
(window as any).gameInfo = gameInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function retrieves the sprite and audio keys for active Pokemon.
|
||||
* Active Pokemon include both enemy and player Pokemon of the current wave.
|
||||
* Note: Questions on garbage collection go to @frutescens
|
||||
* @returns a string array of active sprite and audio keys that should not be deleted
|
||||
*/
|
||||
getActiveKeys(): string[] {
|
||||
const keys: string[] = [];
|
||||
const playerParty = this.getParty();
|
||||
playerParty.forEach(p => {
|
||||
keys.push("pkmn__" + p.species.getSpriteId(p.gender === Gender.FEMALE, p.species.formIndex, p.shiny, p.variant));
|
||||
keys.push("pkmn__" + p.species.getSpriteId(p.gender === Gender.FEMALE, p.species.formIndex, p.shiny, p.variant, true));
|
||||
keys.push("cry/" + p.species.getCryKey(p.species.formIndex));
|
||||
if (p.fusionSpecies && p.getSpeciesForm() !== p.getFusionSpeciesForm()) {
|
||||
keys.push("cry/"+p.getFusionSpeciesForm().getCryKey(p.fusionSpecies.formIndex));
|
||||
}
|
||||
});
|
||||
// enemyParty has to be operated on separately from playerParty because playerPokemon =/= enemyPokemon
|
||||
const enemyParty = this.getEnemyParty();
|
||||
enemyParty.forEach(p => {
|
||||
keys.push(p.species.getSpriteKey(p.gender === Gender.FEMALE, p.species.formIndex, p.shiny, p.variant));
|
||||
keys.push("cry/" + p.species.getCryKey(p.species.formIndex));
|
||||
if (p.fusionSpecies && p.getSpeciesForm() !== p.getFusionSpeciesForm()) {
|
||||
keys.push("cry/"+p.getFusionSpeciesForm().getCryKey(p.fusionSpecies.formIndex));
|
||||
}
|
||||
});
|
||||
return keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialized the 2nd phase of the final boss (e.g. form-change for Eternatus)
|
||||
* @param pokemon The (enemy) pokemon
|
||||
|
@ -6,7 +6,7 @@ import Trainer, { TrainerVariant } from "./field/trainer";
|
||||
import { GameMode } from "./game-mode";
|
||||
import { MoneyMultiplierModifier, PokemonHeldItemModifier } from "./modifier/modifier";
|
||||
import { PokeballType } from "./data/pokeball";
|
||||
import {trainerConfigs} from "#app/data/trainer-config";
|
||||
import { trainerConfigs } from "#app/data/trainer-config";
|
||||
import { ArenaTagType } from "#enums/arena-tag-type";
|
||||
import { BattleSpec } from "#enums/battle-spec";
|
||||
import { Moves } from "#enums/moves";
|
||||
@ -31,7 +31,7 @@ export enum BattlerIndex {
|
||||
|
||||
export interface TurnCommand {
|
||||
command: Command;
|
||||
cursor?: integer;
|
||||
cursor?: number;
|
||||
move?: QueuedMove;
|
||||
targets?: BattlerIndex[];
|
||||
skip?: boolean;
|
||||
@ -39,38 +39,40 @@ export interface TurnCommand {
|
||||
}
|
||||
|
||||
interface TurnCommands {
|
||||
[key: integer]: TurnCommand | null
|
||||
[key: number]: TurnCommand | null
|
||||
}
|
||||
|
||||
export default class Battle {
|
||||
protected gameMode: GameMode;
|
||||
public waveIndex: integer;
|
||||
public waveIndex: number;
|
||||
public battleType: BattleType;
|
||||
public battleSpec: BattleSpec;
|
||||
public trainer: Trainer | null;
|
||||
public enemyLevels: integer[] | undefined;
|
||||
public enemyParty: EnemyPokemon[];
|
||||
public seenEnemyPartyMemberIds: Set<integer>;
|
||||
public enemyLevels: number[] | undefined;
|
||||
public enemyParty: EnemyPokemon[] = [];
|
||||
public seenEnemyPartyMemberIds: Set<number> = new Set<number>();
|
||||
public double: boolean;
|
||||
public started: boolean;
|
||||
public enemySwitchCounter: integer;
|
||||
public turn: integer;
|
||||
public started: boolean = false;
|
||||
public enemySwitchCounter: number = 0;
|
||||
public turn: number = 0;
|
||||
public turnCommands: TurnCommands;
|
||||
public playerParticipantIds: Set<integer>;
|
||||
public battleScore: integer;
|
||||
public postBattleLoot: PokemonHeldItemModifier[];
|
||||
public escapeAttempts: integer;
|
||||
public playerParticipantIds: Set<number> = new Set<number>();
|
||||
public battleScore: number = 0;
|
||||
public postBattleLoot: PokemonHeldItemModifier[] = [];
|
||||
public escapeAttempts: number = 0;
|
||||
public lastMove: Moves;
|
||||
public battleSeed: string;
|
||||
private battleSeedState: string | null;
|
||||
public moneyScattered: number;
|
||||
public lastUsedPokeball: PokeballType | null;
|
||||
public playerFaints: number; // The amount of times pokemon on the players side have fainted
|
||||
public enemyFaints: number; // The amount of times pokemon on the enemies side have fainted
|
||||
public battleSeed: string = Utils.randomString(16, true);
|
||||
private battleSeedState: string | null = null;
|
||||
public moneyScattered: number = 0;
|
||||
public lastUsedPokeball: PokeballType | null = null;
|
||||
/** The number of times a Pokemon on the player's side has fainted this battle */
|
||||
public playerFaints: number = 0;
|
||||
/** The number of times a Pokemon on the enemy's side has fainted this battle */
|
||||
public enemyFaints: number = 0;
|
||||
|
||||
private rngCounter: integer = 0;
|
||||
private rngCounter: number = 0;
|
||||
|
||||
constructor(gameMode: GameMode, waveIndex: integer, battleType: BattleType, trainer?: Trainer, double?: boolean) {
|
||||
constructor(gameMode: GameMode, waveIndex: number, battleType: BattleType, trainer?: Trainer, double?: boolean) {
|
||||
this.gameMode = gameMode;
|
||||
this.waveIndex = waveIndex;
|
||||
this.battleType = battleType;
|
||||
@ -79,22 +81,7 @@ export default class Battle {
|
||||
this.enemyLevels = battleType !== BattleType.TRAINER
|
||||
? new Array(double ? 2 : 1).fill(null).map(() => this.getLevelForWave())
|
||||
: trainer?.getPartyLevels(this.waveIndex);
|
||||
this.enemyParty = [];
|
||||
this.seenEnemyPartyMemberIds = new Set<integer>();
|
||||
this.double = !!double;
|
||||
this.enemySwitchCounter = 0;
|
||||
this.turn = 0;
|
||||
this.playerParticipantIds = new Set<integer>();
|
||||
this.battleScore = 0;
|
||||
this.postBattleLoot = [];
|
||||
this.escapeAttempts = 0;
|
||||
this.started = false;
|
||||
this.battleSeed = Utils.randomString(16, true);
|
||||
this.battleSeedState = null;
|
||||
this.moneyScattered = 0;
|
||||
this.lastUsedPokeball = null;
|
||||
this.playerFaints = 0;
|
||||
this.enemyFaints = 0;
|
||||
this.double = double ?? false;
|
||||
}
|
||||
|
||||
private initBattleSpec(): void {
|
||||
@ -105,7 +92,7 @@ export default class Battle {
|
||||
this.battleSpec = spec;
|
||||
}
|
||||
|
||||
private getLevelForWave(): integer {
|
||||
private getLevelForWave(): number {
|
||||
const levelWaveIndex = this.gameMode.getWaveForDifficulty(this.waveIndex);
|
||||
const baseLevel = 1 + levelWaveIndex / 2 + Math.pow(levelWaveIndex / 25, 2);
|
||||
const bossMultiplier = 1.2;
|
||||
@ -138,7 +125,7 @@ export default class Battle {
|
||||
return rand / value;
|
||||
}
|
||||
|
||||
getBattlerCount(): integer {
|
||||
getBattlerCount(): number {
|
||||
return this.double ? 2 : 1;
|
||||
}
|
||||
|
||||
@ -367,7 +354,13 @@ export default class Battle {
|
||||
return null;
|
||||
}
|
||||
|
||||
randSeedInt(scene: BattleScene, range: integer, min: integer = 0): integer {
|
||||
/**
|
||||
* Generates a random number using the current battle's seed. Calls {@linkcode Utils.randSeedInt}
|
||||
* @param range How large of a range of random numbers to choose from. If {@linkcode range} <= 1, returns {@linkcode min}
|
||||
* @param min The minimum integer to pick, default `0`
|
||||
* @returns A random integer between {@linkcode min} and ({@linkcode min} + {@linkcode range} - 1)
|
||||
*/
|
||||
randSeedInt(scene: BattleScene, range: number, min: number = 0): number {
|
||||
if (range <= 1) {
|
||||
return min;
|
||||
}
|
||||
@ -392,7 +385,7 @@ export default class Battle {
|
||||
}
|
||||
|
||||
export class FixedBattle extends Battle {
|
||||
constructor(scene: BattleScene, waveIndex: integer, config: FixedBattleConfig) {
|
||||
constructor(scene: BattleScene, waveIndex: number, config: FixedBattleConfig) {
|
||||
super(scene.gameMode, waveIndex, config.battleType, config.battleType === BattleType.TRAINER ? config.getTrainer(scene) : undefined, config.double);
|
||||
if (config.getEnemyParty) {
|
||||
this.enemyParty = config.getEnemyParty(scene);
|
||||
@ -408,7 +401,7 @@ export class FixedBattleConfig {
|
||||
public double: boolean;
|
||||
public getTrainer: GetTrainerFunc;
|
||||
public getEnemyParty: GetEnemyPartyFunc;
|
||||
public seedOffsetWaveIndex: integer;
|
||||
public seedOffsetWaveIndex: number;
|
||||
|
||||
setBattleType(battleType: BattleType): FixedBattleConfig {
|
||||
this.battleType = battleType;
|
||||
@ -430,7 +423,7 @@ export class FixedBattleConfig {
|
||||
return this;
|
||||
}
|
||||
|
||||
setSeedOffsetWave(seedOffsetWaveIndex: integer): FixedBattleConfig {
|
||||
setSeedOffsetWave(seedOffsetWaveIndex: number): FixedBattleConfig {
|
||||
this.seedOffsetWaveIndex = seedOffsetWaveIndex;
|
||||
return this;
|
||||
}
|
||||
@ -476,7 +469,7 @@ function getRandomTrainerFunc(trainerPool: (TrainerType | TrainerType[])[], rand
|
||||
}
|
||||
|
||||
export interface FixedBattleConfigs {
|
||||
[key: integer]: FixedBattleConfig
|
||||
[key: number]: FixedBattleConfig
|
||||
}
|
||||
/**
|
||||
* Youngster/Lass on 5
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {SettingGamepad} from "#app/system/settings/settings-gamepad.js";
|
||||
import {SettingGamepad} from "#app/system/settings/settings-gamepad";
|
||||
import {Button} from "#enums/buttons";
|
||||
|
||||
/**
|
||||
|
1
src/constants.ts
Normal file
@ -0,0 +1 @@
|
||||
export const PLAYER_PARTY_MAX_SIZE = 6;
|
682
src/data/ability.ts
Normal file → Executable file
@ -7,17 +7,17 @@ import Pokemon, { HitResult, PokemonMove } from "../field/pokemon";
|
||||
import { StatusEffect } from "./status-effect";
|
||||
import { BattlerIndex } from "../battle";
|
||||
import { BlockNonDirectDamageAbAttr, ChangeMovePriorityAbAttr, ProtectStatAbAttr, applyAbAttrs } from "./ability";
|
||||
import { BattleStat } from "./battle-stat";
|
||||
import { Stat } from "#enums/stat";
|
||||
import { CommonAnim, CommonBattleAnim } from "./battle-anims";
|
||||
import i18next from "i18next";
|
||||
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 { MoveEffectPhase } from "#app/phases/move-effect-phase.js";
|
||||
import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase.js";
|
||||
import { ShowAbilityPhase } from "#app/phases/show-ability-phase.js";
|
||||
import { StatChangePhase } from "#app/phases/stat-change-phase.js";
|
||||
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
|
||||
import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase";
|
||||
import { ShowAbilityPhase } from "#app/phases/show-ability-phase";
|
||||
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
|
||||
|
||||
export enum ArenaTagSide {
|
||||
BOTH,
|
||||
@ -786,8 +786,8 @@ class StickyWebTag extends ArenaTrapTag {
|
||||
applyAbAttrs(ProtectStatAbAttr, pokemon, cancelled);
|
||||
if (!cancelled.value) {
|
||||
pokemon.scene.queueMessage(i18next.t("arenaTag:stickyWebActivateTrap", { pokemonName: pokemon.getNameToRender() }));
|
||||
const statLevels = new Utils.NumberHolder(-1);
|
||||
pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), false, [BattleStat.SPD], statLevels.value));
|
||||
const stages = new Utils.NumberHolder(-1);
|
||||
pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), false, [ Stat.SPD ], stages.value));
|
||||
}
|
||||
}
|
||||
|
||||
@ -875,7 +875,7 @@ class TailwindTag extends ArenaTag {
|
||||
// Raise attack by one stage if party member has WIND_RIDER ability
|
||||
if (pokemon.hasAbility(Abilities.WIND_RIDER)) {
|
||||
pokemon.scene.unshiftPhase(new ShowAbilityPhase(pokemon.scene, pokemon.getBattlerIndex()));
|
||||
pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [BattleStat.ATK], 1, true));
|
||||
pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ Stat.ATK ], 1, true));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,71 +0,0 @@
|
||||
import i18next, { ParseKeys } from "i18next";
|
||||
|
||||
export enum BattleStat {
|
||||
ATK,
|
||||
DEF,
|
||||
SPATK,
|
||||
SPDEF,
|
||||
SPD,
|
||||
ACC,
|
||||
EVA,
|
||||
RAND,
|
||||
HP
|
||||
}
|
||||
|
||||
export function getBattleStatName(stat: BattleStat) {
|
||||
switch (stat) {
|
||||
case BattleStat.ATK:
|
||||
return i18next.t("pokemonInfo:Stat.ATK");
|
||||
case BattleStat.DEF:
|
||||
return i18next.t("pokemonInfo:Stat.DEF");
|
||||
case BattleStat.SPATK:
|
||||
return i18next.t("pokemonInfo:Stat.SPATK");
|
||||
case BattleStat.SPDEF:
|
||||
return i18next.t("pokemonInfo:Stat.SPDEF");
|
||||
case BattleStat.SPD:
|
||||
return i18next.t("pokemonInfo:Stat.SPD");
|
||||
case BattleStat.ACC:
|
||||
return i18next.t("pokemonInfo:Stat.ACC");
|
||||
case BattleStat.EVA:
|
||||
return i18next.t("pokemonInfo:Stat.EVA");
|
||||
case BattleStat.HP:
|
||||
return i18next.t("pokemonInfo:Stat.HPStat");
|
||||
default:
|
||||
return "???";
|
||||
}
|
||||
}
|
||||
|
||||
export function getBattleStatLevelChangeDescription(pokemonNameWithAffix: string, stats: string, levels: integer, up: boolean, count: number = 1) {
|
||||
const stringKey = (() => {
|
||||
if (up) {
|
||||
switch (levels) {
|
||||
case 1:
|
||||
return "battle:statRose";
|
||||
case 2:
|
||||
return "battle:statSharplyRose";
|
||||
case 3:
|
||||
case 4:
|
||||
case 5:
|
||||
case 6:
|
||||
return "battle:statRoseDrastically";
|
||||
default:
|
||||
return "battle:statWontGoAnyHigher";
|
||||
}
|
||||
} else {
|
||||
switch (levels) {
|
||||
case 1:
|
||||
return "battle:statFell";
|
||||
case 2:
|
||||
return "battle:statHarshlyFell";
|
||||
case 3:
|
||||
case 4:
|
||||
case 5:
|
||||
case 6:
|
||||
return "battle:statSeverelyFell";
|
||||
default:
|
||||
return "battle:statWontGoAnyLower";
|
||||
}
|
||||
}
|
||||
})();
|
||||
return i18next.t(stringKey as ParseKeys, { pokemonNameWithAffix, stats, count });
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
import { ChargeAnim, CommonAnim, CommonBattleAnim, MoveChargeAnim } from "./battle-anims";
|
||||
import { getPokemonNameWithAffix } from "../messages";
|
||||
import Pokemon, { MoveResult, HitResult } from "../field/pokemon";
|
||||
import { Stat, getStatName } from "./pokemon-stat";
|
||||
import { StatusEffect } from "./status-effect";
|
||||
import * as Utils from "../utils";
|
||||
import { ChargeAttr, MoveFlags, allMoves } from "./move";
|
||||
@ -9,20 +8,20 @@ import { Type } from "./type";
|
||||
import { BlockNonDirectDamageAbAttr, FlinchEffectAbAttr, ReverseDrainAbAttr, applyAbAttrs } from "./ability";
|
||||
import { TerrainType } from "./terrain";
|
||||
import { WeatherType } from "./weather";
|
||||
import { BattleStat } from "./battle-stat";
|
||||
import { allAbilities } from "./ability";
|
||||
import { SpeciesFormChangeManualTrigger } from "./pokemon-forms";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
import i18next from "#app/plugins/i18n.js";
|
||||
import { CommonAnimPhase } from "#app/phases/common-anim-phase.js";
|
||||
import { MoveEffectPhase } from "#app/phases/move-effect-phase.js";
|
||||
import { MovePhase } from "#app/phases/move-phase.js";
|
||||
import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase.js";
|
||||
import { ShowAbilityPhase } from "#app/phases/show-ability-phase.js";
|
||||
import { StatChangePhase, StatChangeCallback } from "#app/phases/stat-change-phase.js";
|
||||
import i18next from "#app/plugins/i18n";
|
||||
import { Stat, type BattleStat, type EffectiveStat, EFFECTIVE_STATS, getStatKey } from "#app/enums/stat";
|
||||
import { CommonAnimPhase } from "#app/phases/common-anim-phase";
|
||||
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
|
||||
import { MovePhase } from "#app/phases/move-phase";
|
||||
import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase";
|
||||
import { ShowAbilityPhase } from "#app/phases/show-ability-phase";
|
||||
import { StatStageChangePhase, StatStageChangeCallback } from "#app/phases/stat-stage-change-phase";
|
||||
|
||||
export enum BattlerTagLapseType {
|
||||
FAINT,
|
||||
@ -40,13 +39,15 @@ export class BattlerTag {
|
||||
public turnCount: number;
|
||||
public sourceMove: Moves;
|
||||
public sourceId?: number;
|
||||
public isBatonPassable: boolean;
|
||||
|
||||
constructor(tagType: BattlerTagType, lapseType: BattlerTagLapseType | BattlerTagLapseType[], turnCount: number, sourceMove?: Moves, sourceId?: number) {
|
||||
constructor(tagType: BattlerTagType, lapseType: BattlerTagLapseType | BattlerTagLapseType[], turnCount: number, sourceMove?: Moves, sourceId?: number, isBatonPassable: boolean = false) {
|
||||
this.tagType = tagType;
|
||||
this.lapseTypes = Array.isArray(lapseType) ? lapseType : [ lapseType ];
|
||||
this.turnCount = turnCount;
|
||||
this.sourceMove = sourceMove!; // TODO: is this bang correct?
|
||||
this.sourceId = sourceId;
|
||||
this.isBatonPassable = isBatonPassable;
|
||||
}
|
||||
|
||||
canAdd(pokemon: Pokemon): boolean {
|
||||
@ -97,6 +98,245 @@ export interface TerrainBattlerTag {
|
||||
terrainTypes: TerrainType[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for tags that restrict the usage of moves. This effect is generally referred to as "disabling" a move
|
||||
* in-game. This is not to be confused with {@linkcode Moves.DISABLE}.
|
||||
*
|
||||
* Descendants can override {@linkcode isMoveRestricted} to restrict moves that
|
||||
* match a condition. A restricted move gets cancelled before it is used. Players and enemies should not be allowed
|
||||
* to select restricted moves.
|
||||
*/
|
||||
export abstract class MoveRestrictionBattlerTag extends BattlerTag {
|
||||
constructor(tagType: BattlerTagType, lapseType: BattlerTagLapseType | BattlerTagLapseType[], turnCount: integer, sourceMove?: Moves, sourceId?: integer) {
|
||||
super(tagType, lapseType, turnCount, sourceMove, sourceId);
|
||||
}
|
||||
|
||||
/** @override */
|
||||
override lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
||||
if (lapseType === BattlerTagLapseType.PRE_MOVE) {
|
||||
// Cancel the affected pokemon's selected move
|
||||
const phase = pokemon.scene.getCurrentPhase() as MovePhase;
|
||||
const move = phase.move;
|
||||
|
||||
if (this.isMoveRestricted(move.moveId)) {
|
||||
if (this.interruptedText(pokemon, move.moveId)) {
|
||||
pokemon.scene.queueMessage(this.interruptedText(pokemon, move.moveId));
|
||||
}
|
||||
phase.cancel();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.lapse(pokemon, lapseType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether this tag is restricting a move.
|
||||
*
|
||||
* @param {Moves} move {@linkcode Moves} ID to check restriction for.
|
||||
* @returns {boolean} `true` if the move is restricted by this tag, otherwise `false`.
|
||||
*/
|
||||
abstract isMoveRestricted(move: Moves): boolean;
|
||||
|
||||
/**
|
||||
* Gets the text to display when the player attempts to select a move that is restricted by this tag.
|
||||
*
|
||||
* @param {Pokemon} pokemon {@linkcode Pokemon} for which the player is attempting to select the restricted move
|
||||
* @param {Moves} move {@linkcode Moves} ID of the move that is having its selection denied
|
||||
* @returns {string} text to display when the player attempts to select the restricted move
|
||||
*/
|
||||
abstract selectionDeniedText(pokemon: Pokemon, move: Moves): string;
|
||||
|
||||
/**
|
||||
* Gets the text to display when a move's execution is prevented as a result of the restriction.
|
||||
* Because restriction effects also prevent selection of the move, this situation can only arise if a
|
||||
* pokemon first selects a move, then gets outsped by a pokemon using a move that restricts the selected move.
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
interruptedText(pokemon: Pokemon, move: Moves): string {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tag representing the "Throat Chop" effect. Pokemon with this tag cannot use sound-based moves.
|
||||
* @see {@link https://bulbapedia.bulbagarden.net/wiki/Throat_Chop_(move) | Throat Chop}
|
||||
* @extends MoveRestrictionBattlerTag
|
||||
*/
|
||||
export class ThroatChoppedTag extends MoveRestrictionBattlerTag {
|
||||
constructor() {
|
||||
super(BattlerTagType.THROAT_CHOPPED, [ BattlerTagLapseType.TURN_END, BattlerTagLapseType.PRE_MOVE ], 2, Moves.THROAT_CHOP);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a {@linkcode Moves | move} is restricted by Throat Chop.
|
||||
* @override
|
||||
* @param {Moves} move the {@linkcode Moves | move} to check for sound-based restriction
|
||||
* @returns true if the move is sound-based
|
||||
*/
|
||||
override isMoveRestricted(move: Moves): boolean {
|
||||
return allMoves[move].hasFlag(MoveFlags.SOUND_BASED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a message when the player attempts to select a move that is restricted by Throat Chop.
|
||||
* @override
|
||||
* @param {Pokemon} pokemon the {@linkcode Pokemon} that is attempting to select the restricted move
|
||||
* @param {Moves} move the {@linkcode Moves | move} that is being restricted
|
||||
* @returns the message to display when the player attempts to select the restricted move
|
||||
*/
|
||||
override selectionDeniedText(pokemon: Pokemon, move: Moves): string {
|
||||
return i18next.t("battle:moveCannotBeSelected", { moveName: allMoves[move].name });
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a message when a move is interrupted by Throat Chop.
|
||||
* @override
|
||||
* @param {Pokemon} pokemon the interrupted {@linkcode Pokemon}
|
||||
* @param {Moves} move the {@linkcode Moves | move} that was interrupted
|
||||
* @returns the message to display when the move is interrupted
|
||||
*/
|
||||
override interruptedText(pokemon: Pokemon, move: Moves): string {
|
||||
return i18next.t("battle:throatChopInterruptedMove", { pokemonName: getPokemonNameWithAffix(pokemon) });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tag representing the "disabling" effect performed by {@linkcode Moves.DISABLE} and {@linkcode Abilities.CURSED_BODY}.
|
||||
* When the tag is added, the last-used move of the tag holder is set as the disabled move.
|
||||
*/
|
||||
export class DisabledTag extends MoveRestrictionBattlerTag {
|
||||
/** The move being disabled. Gets set when {@linkcode onAdd} is called for this tag. */
|
||||
private moveId: Moves = Moves.NONE;
|
||||
|
||||
constructor(sourceId: number) {
|
||||
super(BattlerTagType.DISABLED, [ BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.TURN_END ], 4, Moves.DISABLE, sourceId);
|
||||
}
|
||||
|
||||
/** @override */
|
||||
override isMoveRestricted(move: Moves): boolean {
|
||||
return move === this.moveId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*
|
||||
* Ensures that move history exists on `pokemon` and has a valid move. If so, sets the {@linkcode moveId} and shows a message.
|
||||
* Otherwise the move ID will not get assigned and this tag will get removed next turn.
|
||||
*/
|
||||
override onAdd(pokemon: Pokemon): void {
|
||||
super.onAdd(pokemon);
|
||||
|
||||
const move = pokemon.getLastXMoves()
|
||||
.find(m => m.move !== Moves.NONE && m.move !== Moves.STRUGGLE && !m.virtual);
|
||||
if (move === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.moveId = move.move;
|
||||
|
||||
pokemon.scene.queueMessage(i18next.t("battlerTags:disabledOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), moveName: allMoves[this.moveId].name }));
|
||||
}
|
||||
|
||||
/** @override */
|
||||
override onRemove(pokemon: Pokemon): void {
|
||||
super.onRemove(pokemon);
|
||||
|
||||
pokemon.scene.queueMessage(i18next.t("battlerTags:disabledLapse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), moveName: allMoves[this.moveId].name }));
|
||||
}
|
||||
|
||||
/** @override */
|
||||
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 */
|
||||
override loadTag(source: BattlerTag | any): void {
|
||||
super.loadTag(source);
|
||||
this.moveId = source.moveId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tag used by Gorilla Tactics to restrict the user to using only one move.
|
||||
* @extends MoveRestrictionBattlerTag
|
||||
*/
|
||||
export class GorillaTacticsTag extends MoveRestrictionBattlerTag {
|
||||
private moveId = Moves.NONE;
|
||||
|
||||
constructor() {
|
||||
super(BattlerTagType.GORILLA_TACTICS, BattlerTagLapseType.CUSTOM, 0);
|
||||
}
|
||||
|
||||
/** @override */
|
||||
override isMoveRestricted(move: Moves): boolean {
|
||||
return move !== this.moveId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @param {Pokemon} pokemon the {@linkcode Pokemon} to check if the tag can be added
|
||||
* @returns `true` if the pokemon has a valid move and no existing {@linkcode GorillaTacticsTag}; `false` otherwise
|
||||
*/
|
||||
override canAdd(pokemon: Pokemon): boolean {
|
||||
return (this.getLastValidMove(pokemon) !== undefined) && !pokemon.getTag(GorillaTacticsTag);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that move history exists on {@linkcode Pokemon} and has a valid move.
|
||||
* If so, sets the {@linkcode moveId} and increases the user's Attack by 50%.
|
||||
* @override
|
||||
* @param {Pokemon} pokemon the {@linkcode Pokemon} to add the tag to
|
||||
*/
|
||||
override onAdd(pokemon: Pokemon): void {
|
||||
const lastValidMove = this.getLastValidMove(pokemon);
|
||||
|
||||
if (!lastValidMove) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.moveId = lastValidMove;
|
||||
pokemon.setStat(Stat.ATK, pokemon.getStat(Stat.ATK, false) * 1.5, false);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @override
|
||||
* @param {Pokemon} pokemon n/a
|
||||
* @param {Moves} move {@linkcode Moves} ID of the move being denied
|
||||
* @returns {string} text to display when the move is denied
|
||||
*/
|
||||
override selectionDeniedText(pokemon: Pokemon, move: Moves): string {
|
||||
return i18next.t("battle:canOnlyUseMove", { moveName: allMoves[this.moveId].name, pokemonName: getPokemonNameWithAffix(pokemon) });
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the last valid move from the pokemon's move history.
|
||||
* @param {Pokemon} pokemon {@linkcode Pokemon} to get the last valid move from
|
||||
* @returns {Moves | undefined} the last valid move from the pokemon's move history
|
||||
*/
|
||||
getLastValidMove(pokemon: Pokemon): Moves | undefined {
|
||||
const move = pokemon.getLastXMoves()
|
||||
.find(m => m.move !== Moves.NONE && m.move !== Moves.STRUGGLE && !m.virtual);
|
||||
|
||||
return move?.move;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* BattlerTag that represents the "recharge" effects of moves like Hyper Beam.
|
||||
*/
|
||||
@ -207,7 +447,7 @@ export class ShellTrapTag extends BattlerTag {
|
||||
|
||||
export class TrappedTag extends BattlerTag {
|
||||
constructor(tagType: BattlerTagType, lapseType: BattlerTagLapseType, turnCount: number, sourceMove: Moves, sourceId: number) {
|
||||
super(tagType, lapseType, turnCount, sourceMove, sourceId);
|
||||
super(tagType, lapseType, turnCount, sourceMove, sourceId, true);
|
||||
}
|
||||
|
||||
canAdd(pokemon: Pokemon): boolean {
|
||||
@ -327,7 +567,7 @@ export class InterruptedTag extends BattlerTag {
|
||||
*/
|
||||
export class ConfusedTag extends BattlerTag {
|
||||
constructor(turnCount: number, sourceMove: Moves) {
|
||||
super(BattlerTagType.CONFUSED, BattlerTagLapseType.MOVE, turnCount, sourceMove);
|
||||
super(BattlerTagType.CONFUSED, BattlerTagLapseType.MOVE, turnCount, sourceMove, undefined, true);
|
||||
}
|
||||
|
||||
canAdd(pokemon: Pokemon): boolean {
|
||||
@ -362,9 +602,9 @@ export class ConfusedTag extends BattlerTag {
|
||||
|
||||
// 1/3 chance of hitting self with a 40 base power move
|
||||
if (pokemon.randSeedInt(3) === 0) {
|
||||
const atk = pokemon.getBattleStat(Stat.ATK);
|
||||
const def = pokemon.getBattleStat(Stat.DEF);
|
||||
const damage = Utils.toDmgValue(((((2 * pokemon.level / 5 + 2) * 40 * atk / def) / 50) + 2) * (pokemon.randSeedInt(15, 85) / 100));
|
||||
const atk = pokemon.getEffectiveStat(Stat.ATK);
|
||||
const def = pokemon.getEffectiveStat(Stat.DEF);
|
||||
const damage = Utils.toDmgValue(((((2 * pokemon.level / 5 + 2) * 40 * atk / def) / 50) + 2) * (pokemon.randSeedIntRange(85, 100) / 100));
|
||||
pokemon.scene.queueMessage(i18next.t("battlerTags:confusedLapseHurtItself"));
|
||||
pokemon.damageAndUpdate(damage);
|
||||
pokemon.battleData.hitCount++;
|
||||
@ -387,7 +627,7 @@ export class ConfusedTag extends BattlerTag {
|
||||
*/
|
||||
export class DestinyBondTag extends BattlerTag {
|
||||
constructor(sourceMove: Moves, sourceId: number) {
|
||||
super(BattlerTagType.DESTINY_BOND, BattlerTagLapseType.PRE_MOVE, 1, sourceMove, sourceId);
|
||||
super(BattlerTagType.DESTINY_BOND, BattlerTagLapseType.PRE_MOVE, 1, sourceMove, sourceId, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -506,7 +746,7 @@ export class SeedTag extends BattlerTag {
|
||||
private sourceIndex: number;
|
||||
|
||||
constructor(sourceId: number) {
|
||||
super(BattlerTagType.SEEDED, BattlerTagLapseType.TURN_END, 1, Moves.LEECH_SEED, sourceId);
|
||||
super(BattlerTagType.SEEDED, BattlerTagLapseType.TURN_END, 1, Moves.LEECH_SEED, sourceId, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -767,7 +1007,7 @@ export class OctolockTag extends TrappedTag {
|
||||
const shouldLapse = lapseType !== BattlerTagLapseType.CUSTOM || super.lapse(pokemon, lapseType);
|
||||
|
||||
if (shouldLapse) {
|
||||
pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), false, [BattleStat.DEF, BattleStat.SPDEF], -1));
|
||||
pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), false, [ Stat.DEF, Stat.SPDEF ], -1));
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -777,7 +1017,7 @@ export class OctolockTag extends TrappedTag {
|
||||
|
||||
export class AquaRingTag extends BattlerTag {
|
||||
constructor() {
|
||||
super(BattlerTagType.AQUA_RING, BattlerTagLapseType.TURN_END, 1, Moves.AQUA_RING, undefined);
|
||||
super(BattlerTagType.AQUA_RING, BattlerTagLapseType.TURN_END, 1, Moves.AQUA_RING, undefined, true);
|
||||
}
|
||||
|
||||
onAdd(pokemon: Pokemon): void {
|
||||
@ -809,7 +1049,7 @@ export class AquaRingTag extends BattlerTag {
|
||||
/** Tag used to allow moves that interact with {@link Moves.MINIMIZE} to function */
|
||||
export class MinimizeTag extends BattlerTag {
|
||||
constructor() {
|
||||
super(BattlerTagType.MINIMIZED, BattlerTagLapseType.TURN_END, 1, Moves.MINIMIZE, undefined);
|
||||
super(BattlerTagType.MINIMIZED, BattlerTagLapseType.TURN_END, 1, Moves.MINIMIZE);
|
||||
}
|
||||
|
||||
canAdd(pokemon: Pokemon): boolean {
|
||||
@ -1093,7 +1333,7 @@ export class ContactDamageProtectedTag extends ProtectedTag {
|
||||
}
|
||||
}
|
||||
|
||||
export class ContactStatChangeProtectedTag extends ProtectedTag {
|
||||
export class ContactStatStageChangeProtectedTag extends ProtectedTag {
|
||||
private stat: BattleStat;
|
||||
private levels: number;
|
||||
|
||||
@ -1110,7 +1350,7 @@ export class ContactStatChangeProtectedTag extends ProtectedTag {
|
||||
*/
|
||||
loadTag(source: BattlerTag | any): void {
|
||||
super.loadTag(source);
|
||||
this.stat = source.stat as BattleStat;
|
||||
this.stat = source.stat;
|
||||
this.levels = source.levels;
|
||||
}
|
||||
|
||||
@ -1121,7 +1361,7 @@ export class ContactStatChangeProtectedTag extends ProtectedTag {
|
||||
const effectPhase = pokemon.scene.getCurrentPhase();
|
||||
if (effectPhase instanceof MoveEffectPhase && effectPhase.move.getMove().hasFlag(MoveFlags.MAKES_CONTACT)) {
|
||||
const attacker = effectPhase.getPokemon();
|
||||
pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, attacker.getBattlerIndex(), true, [ this.stat ], this.levels));
|
||||
pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, attacker.getBattlerIndex(), true, [ this.stat ], this.levels));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1207,7 +1447,7 @@ export class SturdyTag extends BattlerTag {
|
||||
|
||||
export class PerishSongTag extends BattlerTag {
|
||||
constructor(turnCount: number) {
|
||||
super(BattlerTagType.PERISH_SONG, BattlerTagLapseType.TURN_END, turnCount, Moves.PERISH_SONG);
|
||||
super(BattlerTagType.PERISH_SONG, BattlerTagLapseType.TURN_END, turnCount, Moves.PERISH_SONG, undefined, true);
|
||||
}
|
||||
|
||||
canAdd(pokemon: Pokemon): boolean {
|
||||
@ -1263,7 +1503,7 @@ export class AbilityBattlerTag extends BattlerTag {
|
||||
public ability: Abilities;
|
||||
|
||||
constructor(tagType: BattlerTagType, ability: Abilities, lapseType: BattlerTagLapseType, turnCount: number) {
|
||||
super(tagType, lapseType, turnCount, undefined);
|
||||
super(tagType, lapseType, turnCount);
|
||||
|
||||
this.ability = ability;
|
||||
}
|
||||
@ -1348,11 +1588,10 @@ export class HighestStatBoostTag extends AbilityBattlerTag {
|
||||
onAdd(pokemon: Pokemon): void {
|
||||
super.onAdd(pokemon);
|
||||
|
||||
const stats = [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ];
|
||||
let highestStat: Stat;
|
||||
stats.map(s => pokemon.getBattleStat(s)).reduce((highestValue: number, value: number, i: number) => {
|
||||
let highestStat: EffectiveStat;
|
||||
EFFECTIVE_STATS.map(s => pokemon.getEffectiveStat(s)).reduce((highestValue: number, value: number, i: number) => {
|
||||
if (value > highestValue) {
|
||||
highestStat = stats[i];
|
||||
highestStat = EFFECTIVE_STATS[i];
|
||||
return value;
|
||||
}
|
||||
return highestValue;
|
||||
@ -1370,7 +1609,7 @@ export class HighestStatBoostTag extends AbilityBattlerTag {
|
||||
break;
|
||||
}
|
||||
|
||||
pokemon.scene.queueMessage(i18next.t("battlerTags:highestStatBoostOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), statName: getStatName(highestStat) }), null, false, null, true);
|
||||
pokemon.scene.queueMessage(i18next.t("battlerTags:highestStatBoostOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), statName: i18next.t(getStatKey(highestStat)) }), null, false, null, true);
|
||||
}
|
||||
|
||||
onRemove(pokemon: Pokemon): void {
|
||||
@ -1440,7 +1679,7 @@ export class TypeImmuneTag extends BattlerTag {
|
||||
public immuneType: Type;
|
||||
|
||||
constructor(tagType: BattlerTagType, sourceMove: Moves, immuneType: Type, length: number = 1) {
|
||||
super(tagType, BattlerTagLapseType.TURN_END, length, sourceMove);
|
||||
super(tagType, BattlerTagLapseType.TURN_END, length, sourceMove, undefined, true);
|
||||
|
||||
this.immuneType = immuneType;
|
||||
}
|
||||
@ -1504,7 +1743,7 @@ export class TypeBoostTag extends BattlerTag {
|
||||
|
||||
export class CritBoostTag extends BattlerTag {
|
||||
constructor(tagType: BattlerTagType, sourceMove: Moves) {
|
||||
super(tagType, BattlerTagLapseType.TURN_END, 1, sourceMove);
|
||||
super(tagType, BattlerTagLapseType.TURN_END, 1, sourceMove, undefined, true);
|
||||
}
|
||||
|
||||
onAdd(pokemon: Pokemon): void {
|
||||
@ -1596,7 +1835,7 @@ export class CursedTag extends BattlerTag {
|
||||
private sourceIndex: number;
|
||||
|
||||
constructor(sourceId: number) {
|
||||
super(BattlerTagType.CURSED, BattlerTagLapseType.TURN_END, 1, Moves.CURSE, sourceId);
|
||||
super(BattlerTagType.CURSED, BattlerTagLapseType.TURN_END, 1, Moves.CURSE, sourceId, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1788,25 +2027,25 @@ export class IceFaceBlockDamageTag extends FormBlockDamageTag {
|
||||
*/
|
||||
export class StockpilingTag extends BattlerTag {
|
||||
public stockpiledCount: number = 0;
|
||||
public statChangeCounts: { [BattleStat.DEF]: number; [BattleStat.SPDEF]: number } = {
|
||||
[BattleStat.DEF]: 0,
|
||||
[BattleStat.SPDEF]: 0
|
||||
public statChangeCounts: { [Stat.DEF]: number; [Stat.SPDEF]: number } = {
|
||||
[Stat.DEF]: 0,
|
||||
[Stat.SPDEF]: 0
|
||||
};
|
||||
|
||||
constructor(sourceMove: Moves = Moves.NONE) {
|
||||
super(BattlerTagType.STOCKPILING, BattlerTagLapseType.CUSTOM, 1, sourceMove);
|
||||
}
|
||||
|
||||
private onStatsChanged: StatChangeCallback = (_, statsChanged, statChanges) => {
|
||||
const defChange = statChanges[statsChanged.indexOf(BattleStat.DEF)] ?? 0;
|
||||
const spDefChange = statChanges[statsChanged.indexOf(BattleStat.SPDEF)] ?? 0;
|
||||
private onStatStagesChanged: StatStageChangeCallback = (_, statsChanged, statChanges) => {
|
||||
const defChange = statChanges[statsChanged.indexOf(Stat.DEF)] ?? 0;
|
||||
const spDefChange = statChanges[statsChanged.indexOf(Stat.SPDEF)] ?? 0;
|
||||
|
||||
if (defChange) {
|
||||
this.statChangeCounts[BattleStat.DEF]++;
|
||||
this.statChangeCounts[Stat.DEF]++;
|
||||
}
|
||||
|
||||
if (spDefChange) {
|
||||
this.statChangeCounts[BattleStat.SPDEF]++;
|
||||
this.statChangeCounts[Stat.SPDEF]++;
|
||||
}
|
||||
};
|
||||
|
||||
@ -1814,8 +2053,8 @@ export class StockpilingTag extends BattlerTag {
|
||||
super.loadTag(source);
|
||||
this.stockpiledCount = source.stockpiledCount || 0;
|
||||
this.statChangeCounts = {
|
||||
[ BattleStat.DEF ]: source.statChangeCounts?.[ BattleStat.DEF ] ?? 0,
|
||||
[ BattleStat.SPDEF ]: source.statChangeCounts?.[ BattleStat.SPDEF ] ?? 0,
|
||||
[ Stat.DEF ]: source.statChangeCounts?.[ Stat.DEF ] ?? 0,
|
||||
[ Stat.SPDEF ]: source.statChangeCounts?.[ Stat.SPDEF ] ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
@ -1835,9 +2074,9 @@ export class StockpilingTag extends BattlerTag {
|
||||
}));
|
||||
|
||||
// Attempt to increase DEF and SPDEF by one stage, keeping track of successful changes.
|
||||
pokemon.scene.unshiftPhase(new StatChangePhase(
|
||||
pokemon.scene.unshiftPhase(new StatStageChangePhase(
|
||||
pokemon.scene, pokemon.getBattlerIndex(), true,
|
||||
[BattleStat.SPDEF, BattleStat.DEF], 1, true, false, true, this.onStatsChanged
|
||||
[Stat.SPDEF, Stat.DEF], 1, true, false, true, this.onStatStagesChanged
|
||||
));
|
||||
}
|
||||
}
|
||||
@ -1851,15 +2090,15 @@ export class StockpilingTag extends BattlerTag {
|
||||
* one stage for each stack which had successfully changed that particular stat during onAdd.
|
||||
*/
|
||||
onRemove(pokemon: Pokemon): void {
|
||||
const defChange = this.statChangeCounts[BattleStat.DEF];
|
||||
const spDefChange = this.statChangeCounts[BattleStat.SPDEF];
|
||||
const defChange = this.statChangeCounts[Stat.DEF];
|
||||
const spDefChange = this.statChangeCounts[Stat.SPDEF];
|
||||
|
||||
if (defChange) {
|
||||
pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [BattleStat.DEF], -defChange, true, false, true));
|
||||
pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ Stat.DEF ], -defChange, true, false, true));
|
||||
}
|
||||
|
||||
if (spDefChange) {
|
||||
pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [BattleStat.SPDEF], -spDefChange, true, false, true));
|
||||
pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ Stat.SPDEF ], -spDefChange, true, false, true));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1937,7 +2176,38 @@ export class ExposedTag extends BattlerTag {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tag that doubles the type effectiveness of Fire-type moves.
|
||||
* @extends BattlerTag
|
||||
*/
|
||||
export class TarShotTag extends BattlerTag {
|
||||
constructor() {
|
||||
super(BattlerTagType.TAR_SHOT, BattlerTagLapseType.CUSTOM, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the Pokemon is terastallized, the tag cannot be added.
|
||||
* @param {Pokemon} pokemon the {@linkcode Pokemon} to which the tag is added
|
||||
* @returns whether the tag is applied
|
||||
*/
|
||||
override canAdd(pokemon: Pokemon): boolean {
|
||||
return !pokemon.isTerastallized();
|
||||
}
|
||||
|
||||
override onAdd(pokemon: Pokemon): void {
|
||||
pokemon.scene.queueMessage(i18next.t("battlerTags:tarShotOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a {@linkcode BattlerTag} based on the provided tag type, turn count, source move, and source ID.
|
||||
*
|
||||
* @param {BattlerTagType} tagType the type of the {@linkcode BattlerTagType}.
|
||||
* @param turnCount the turn count.
|
||||
* @param {Moves} sourceMove the source {@linkcode Moves}.
|
||||
* @param sourceId the source ID.
|
||||
* @returns {BattlerTag} the corresponding {@linkcode BattlerTag} object.
|
||||
*/
|
||||
export function getBattlerTag(tagType: BattlerTagType, turnCount: number, sourceMove: Moves, sourceId: number): BattlerTag {
|
||||
switch (tagType) {
|
||||
case BattlerTagType.RECHARGING:
|
||||
@ -2001,11 +2271,11 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source
|
||||
case BattlerTagType.SPIKY_SHIELD:
|
||||
return new ContactDamageProtectedTag(sourceMove, 8);
|
||||
case BattlerTagType.KINGS_SHIELD:
|
||||
return new ContactStatChangeProtectedTag(sourceMove, tagType, BattleStat.ATK, -1);
|
||||
return new ContactStatStageChangeProtectedTag(sourceMove, tagType, Stat.ATK, -1);
|
||||
case BattlerTagType.OBSTRUCT:
|
||||
return new ContactStatChangeProtectedTag(sourceMove, tagType, BattleStat.DEF, -2);
|
||||
return new ContactStatStageChangeProtectedTag(sourceMove, tagType, Stat.DEF, -2);
|
||||
case BattlerTagType.SILK_TRAP:
|
||||
return new ContactStatChangeProtectedTag(sourceMove, tagType, BattleStat.SPD, -1);
|
||||
return new ContactStatStageChangeProtectedTag(sourceMove, tagType, Stat.SPD, -1);
|
||||
case BattlerTagType.BANEFUL_BUNKER:
|
||||
return new ContactPoisonProtectedTag(sourceMove);
|
||||
case BattlerTagType.BURNING_BULWARK:
|
||||
@ -2071,6 +2341,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source
|
||||
return new StockpilingTag(sourceMove);
|
||||
case BattlerTagType.OCTOLOCK:
|
||||
return new OctolockTag(sourceId);
|
||||
case BattlerTagType.DISABLED:
|
||||
return new DisabledTag(sourceId);
|
||||
case BattlerTagType.IGNORE_GHOST:
|
||||
return new ExposedTag(tagType, sourceMove, Type.GHOST, [Type.NORMAL, Type.FIGHTING]);
|
||||
case BattlerTagType.IGNORE_DARK:
|
||||
@ -2078,6 +2350,12 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source
|
||||
case BattlerTagType.GULP_MISSILE_ARROKUDA:
|
||||
case BattlerTagType.GULP_MISSILE_PIKACHU:
|
||||
return new GulpMissileTag(tagType, sourceMove);
|
||||
case BattlerTagType.TAR_SHOT:
|
||||
return new TarShotTag();
|
||||
case BattlerTagType.THROAT_CHOPPED:
|
||||
return new ThroatChoppedTag();
|
||||
case BattlerTagType.GORILLA_TACTICS:
|
||||
return new GorillaTacticsTag();
|
||||
case BattlerTagType.NONE:
|
||||
default:
|
||||
return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId);
|
||||
|
@ -1,14 +1,14 @@
|
||||
import { getPokemonNameWithAffix } from "../messages";
|
||||
import Pokemon, { HitResult } from "../field/pokemon";
|
||||
import { BattleStat } from "./battle-stat";
|
||||
import { getStatusEffectHealText } from "./status-effect";
|
||||
import * as Utils from "../utils";
|
||||
import { DoubleBerryEffectAbAttr, ReduceBerryUseThresholdAbAttr, applyAbAttrs } from "./ability";
|
||||
import i18next from "i18next";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { BerryType } from "#enums/berry-type";
|
||||
import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase.js";
|
||||
import { StatChangePhase } from "#app/phases/stat-change-phase.js";
|
||||
import { Stat, type BattleStat } from "#app/enums/stat";
|
||||
import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase";
|
||||
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
|
||||
|
||||
export function getBerryName(berryType: BerryType): string {
|
||||
return i18next.t(`berry:${BerryType[berryType]}.name`);
|
||||
@ -35,9 +35,10 @@ export function getBerryPredicate(berryType: BerryType): BerryPredicate {
|
||||
case BerryType.SALAC:
|
||||
return (pokemon: Pokemon) => {
|
||||
const threshold = new Utils.NumberHolder(0.25);
|
||||
const battleStat = (berryType - BerryType.LIECHI) as BattleStat;
|
||||
// Offset BerryType such that LIECHI -> Stat.ATK = 1, GANLON -> Stat.DEF = 2, so on and so forth
|
||||
const stat: BattleStat = berryType - BerryType.ENIGMA;
|
||||
applyAbAttrs(ReduceBerryUseThresholdAbAttr, pokemon, null, false, threshold);
|
||||
return pokemon.getHpRatio() < threshold.value && pokemon.summonData.battleStats[battleStat] < 6;
|
||||
return pokemon.getHpRatio() < threshold.value && pokemon.getStatStage(stat) < 6;
|
||||
};
|
||||
case BerryType.LANSAT:
|
||||
return (pokemon: Pokemon) => {
|
||||
@ -95,10 +96,11 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc {
|
||||
if (pokemon.battleData) {
|
||||
pokemon.battleData.berriesEaten.push(berryType);
|
||||
}
|
||||
const battleStat = (berryType - BerryType.LIECHI) as BattleStat;
|
||||
const statLevels = new Utils.NumberHolder(1);
|
||||
applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, false, statLevels);
|
||||
pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ battleStat ], statLevels.value));
|
||||
// Offset BerryType such that LIECHI -> Stat.ATK = 1, GANLON -> Stat.DEF = 2, so on and so forth
|
||||
const stat: BattleStat = berryType - BerryType.ENIGMA;
|
||||
const statStages = new Utils.NumberHolder(1);
|
||||
applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, false, statStages);
|
||||
pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ stat ], statStages.value));
|
||||
};
|
||||
case BerryType.LANSAT:
|
||||
return (pokemon: Pokemon) => {
|
||||
@ -112,9 +114,10 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc {
|
||||
if (pokemon.battleData) {
|
||||
pokemon.battleData.berriesEaten.push(berryType);
|
||||
}
|
||||
const statLevels = new Utils.NumberHolder(2);
|
||||
applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, false, statLevels);
|
||||
pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ BattleStat.RAND ], statLevels.value));
|
||||
const randStat = Utils.randSeedInt(Stat.SPD, Stat.ATK);
|
||||
const stages = new Utils.NumberHolder(2);
|
||||
applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, false, stages);
|
||||
pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ randStat ], stages.value));
|
||||
};
|
||||
case BerryType.LEPPA:
|
||||
return (pokemon: Pokemon) => {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Type } from "./type";
|
||||
import * as Utils from "../utils";
|
||||
import {pokemonEvolutions, SpeciesFormEvolution} from "./pokemon-evolutions";
|
||||
import { pokemonEvolutions, SpeciesFormEvolution } from "./pokemon-evolutions";
|
||||
import i18next from "i18next";
|
||||
import { Biome } from "#enums/biome";
|
||||
import { Species } from "#enums/species";
|
||||
@ -46,7 +46,7 @@ export const biomeLinks: BiomeLinks = {
|
||||
[Biome.SEABED]: [ Biome.CAVE, [ Biome.VOLCANO, 3 ] ],
|
||||
[Biome.MOUNTAIN]: [ Biome.VOLCANO, [ Biome.WASTELAND, 2 ], [ Biome.SPACE, 3 ] ],
|
||||
[Biome.BADLANDS]: [ Biome.DESERT, Biome.MOUNTAIN ],
|
||||
[Biome.CAVE]: [ Biome.BADLANDS, Biome.LAKE [ Biome.LABORATORY, 2 ] ],
|
||||
[Biome.CAVE]: [ Biome.BADLANDS, Biome.LAKE, [ Biome.LABORATORY, 2 ] ],
|
||||
[Biome.DESERT]: [ Biome.RUINS, [ Biome.CONSTRUCTION_SITE, 2 ] ],
|
||||
[Biome.ICE_CAVE]: Biome.SNOWY_FOREST,
|
||||
[Biome.MEADOW]: [ Biome.PLAINS, Biome.FAIRY_CAVE ],
|
||||
|
@ -1,18 +1,18 @@
|
||||
import * as Utils from "../utils";
|
||||
import i18next from "i18next";
|
||||
import { defaultStarterSpecies, DexAttrProps, GameData } from "#app/system/game-data.js";
|
||||
import { defaultStarterSpecies, DexAttrProps, GameData } from "#app/system/game-data";
|
||||
import PokemonSpecies, { getPokemonSpecies, getPokemonSpeciesForm, speciesStarters } from "./pokemon-species";
|
||||
import Pokemon, { PokemonMove } from "#app/field/pokemon.js";
|
||||
import { BattleType, FixedBattleConfig } from "#app/battle.js";
|
||||
import Trainer, { TrainerVariant } from "#app/field/trainer.js";
|
||||
import { GameMode } from "#app/game-mode.js";
|
||||
import Pokemon, { PokemonMove } from "#app/field/pokemon";
|
||||
import { BattleType, FixedBattleConfig } from "#app/battle";
|
||||
import Trainer, { TrainerVariant } from "#app/field/trainer";
|
||||
import { GameMode } from "#app/game-mode";
|
||||
import { Type } from "./type";
|
||||
import { Challenges } from "#enums/challenges";
|
||||
import { Species } from "#enums/species";
|
||||
import { TrainerType } from "#enums/trainer-type";
|
||||
import { Nature } from "./nature";
|
||||
import { Moves } from "#app/enums/moves.js";
|
||||
import { TypeColor, TypeShadow } from "#app/enums/color.js";
|
||||
import { Moves } from "#app/enums/moves";
|
||||
import { TypeColor, TypeShadow } from "#app/enums/color";
|
||||
import { pokemonEvolutions } from "./pokemon-evolutions";
|
||||
import { pokemonFormChanges } from "./pokemon-forms";
|
||||
|
||||
|
98
src/data/egg-hatch-data.ts
Normal file
@ -0,0 +1,98 @@
|
||||
import BattleScene from "#app/battle-scene";
|
||||
import { PlayerPokemon } from "#app/field/pokemon";
|
||||
import { DexEntry, StarterDataEntry } from "#app/system/game-data";
|
||||
|
||||
/**
|
||||
* Stores data associated with a specific egg and the hatched pokemon
|
||||
* Allows hatch info to be stored at hatch then retrieved for display during egg summary
|
||||
*/
|
||||
export class EggHatchData {
|
||||
/** the pokemon that hatched from the file (including shiny, IVs, ability) */
|
||||
public pokemon: PlayerPokemon;
|
||||
/** index of the egg move from the hatched pokemon (not stored in PlayerPokemon) */
|
||||
public eggMoveIndex: number;
|
||||
/** boolean indicating if the egg move for the hatch is new */
|
||||
public eggMoveUnlocked: boolean;
|
||||
/** stored copy of the hatched pokemon's dex entry before it was updated due to hatch */
|
||||
public dexEntryBeforeUpdate: DexEntry;
|
||||
/** stored copy of the hatched pokemon's starter entry before it was updated due to hatch */
|
||||
public starterDataEntryBeforeUpdate: StarterDataEntry;
|
||||
/** reference to the battle scene to get gamedata and update dex */
|
||||
private scene: BattleScene;
|
||||
|
||||
constructor(scene: BattleScene, pokemon: PlayerPokemon, eggMoveIndex: number) {
|
||||
this.scene = scene;
|
||||
this.pokemon = pokemon;
|
||||
this.eggMoveIndex = eggMoveIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the boolean for if the egg move for the hatch is a new unlock
|
||||
* @param unlocked True if the EM is new
|
||||
*/
|
||||
setEggMoveUnlocked(unlocked: boolean) {
|
||||
this.eggMoveUnlocked = unlocked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores a copy of the current DexEntry of the pokemon and StarterDataEntry of its starter
|
||||
* Used before updating the dex, so comparing the pokemon to these entries will show the new attributes
|
||||
*/
|
||||
setDex() {
|
||||
const currDexEntry = this.scene.gameData.dexData[this.pokemon.species.speciesId];
|
||||
const currStarterDataEntry = this.scene.gameData.starterData[this.pokemon.species.getRootSpeciesId()];
|
||||
this.dexEntryBeforeUpdate = {
|
||||
seenAttr: currDexEntry.seenAttr,
|
||||
caughtAttr: currDexEntry.caughtAttr,
|
||||
natureAttr: currDexEntry.natureAttr,
|
||||
seenCount: currDexEntry.seenCount,
|
||||
caughtCount: currDexEntry.caughtCount,
|
||||
hatchedCount: currDexEntry.hatchedCount,
|
||||
ivs: [...currDexEntry.ivs]
|
||||
};
|
||||
this.starterDataEntryBeforeUpdate = {
|
||||
moveset: currStarterDataEntry.moveset,
|
||||
eggMoves: currStarterDataEntry.eggMoves,
|
||||
candyCount: currStarterDataEntry.candyCount,
|
||||
friendship: currStarterDataEntry.friendship,
|
||||
abilityAttr: currStarterDataEntry.abilityAttr,
|
||||
passiveAttr: currStarterDataEntry.passiveAttr,
|
||||
valueReduction: currStarterDataEntry.valueReduction,
|
||||
classicWinCount: currStarterDataEntry.classicWinCount
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the dex entry before update
|
||||
* @returns Dex Entry corresponding to this pokemon before the pokemon was added / updated to dex
|
||||
*/
|
||||
getDex(): DexEntry {
|
||||
return this.dexEntryBeforeUpdate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the starter dex entry before update
|
||||
* @returns Starter Dex Entry corresponding to this pokemon before the pokemon was added / updated to dex
|
||||
*/
|
||||
getStarterEntry(): StarterDataEntry {
|
||||
return this.starterDataEntryBeforeUpdate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the pokedex data corresponding with the new hatch's pokemon data
|
||||
* Also sets whether the egg move is a new unlock or not
|
||||
* @param showMessage boolean to show messages for the new catches and egg moves (false by default)
|
||||
* @returns
|
||||
*/
|
||||
updatePokemon(showMessage : boolean = false) {
|
||||
return new Promise<void>(resolve => {
|
||||
this.scene.gameData.setPokemonCaught(this.pokemon, true, true, showMessage).then(() => {
|
||||
this.scene.gameData.updateSpeciesDexIvs(this.pokemon.species.speciesId, this.pokemon.ivs);
|
||||
this.scene.gameData.setEggMoveUnlocked(this.pokemon.species, this.eggMoveIndex, showMessage).then((value) => {
|
||||
this.setEggMoveUnlocked(value);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
154
src/data/egg.ts
@ -8,14 +8,14 @@ import { PlayerPokemon } from "#app/field/pokemon";
|
||||
import i18next from "i18next";
|
||||
import { EggTier } from "#enums/egg-type";
|
||||
import { Species } from "#enums/species";
|
||||
import { EggSourceType } from "#app/enums/egg-source-types.js";
|
||||
import { EggSourceType } from "#app/enums/egg-source-types";
|
||||
|
||||
export const EGG_SEED = 1073741824;
|
||||
|
||||
// Rates for specific random properties in 1/x
|
||||
const DEFAULT_SHINY_RATE = 128;
|
||||
const GACHA_SHINY_UP_SHINY_RATE = 64;
|
||||
const SAME_SPECIES_EGG_SHINY_RATE = 24;
|
||||
const SAME_SPECIES_EGG_SHINY_RATE = 12;
|
||||
const SAME_SPECIES_EGG_HA_RATE = 8;
|
||||
const MANAPHY_EGG_MANAPHY_RATE = 8;
|
||||
const GACHA_EGG_HA_RATE = 192;
|
||||
@ -139,46 +139,57 @@ export class Egg {
|
||||
////
|
||||
|
||||
constructor(eggOptions?: IEggOptions) {
|
||||
//if (eggOptions.tier && eggOptions.species) throw Error("Error egg can't have species and tier as option. only choose one of them.")
|
||||
const generateEggProperties = (eggOptions?: IEggOptions) => {
|
||||
//if (eggOptions.tier && eggOptions.species) throw Error("Error egg can't have species and tier as option. only choose one of them.")
|
||||
|
||||
this._sourceType = eggOptions?.sourceType!; // TODO: is this bang correct?
|
||||
// Ensure _sourceType is defined before invoking rollEggTier(), as it is referenced
|
||||
this._tier = eggOptions?.tier ?? (Overrides.EGG_TIER_OVERRIDE ?? this.rollEggTier());
|
||||
// If egg was pulled, check if egg pity needs to override the egg tier
|
||||
if (eggOptions?.pulled) {
|
||||
// Needs this._tier and this._sourceType to work
|
||||
this.checkForPityTierOverrides(eggOptions.scene!); // TODO: is this bang correct?
|
||||
}
|
||||
this._sourceType = eggOptions?.sourceType!; // TODO: is this bang correct?
|
||||
// Ensure _sourceType is defined before invoking rollEggTier(), as it is referenced
|
||||
this._tier = eggOptions?.tier ?? (Overrides.EGG_TIER_OVERRIDE ?? this.rollEggTier());
|
||||
// If egg was pulled, check if egg pity needs to override the egg tier
|
||||
if (eggOptions?.pulled) {
|
||||
// Needs this._tier and this._sourceType to work
|
||||
this.checkForPityTierOverrides(eggOptions.scene!); // TODO: is this bang correct?
|
||||
}
|
||||
|
||||
this._id = eggOptions?.id ?? Utils.randInt(EGG_SEED, EGG_SEED * this._tier);
|
||||
this._id = eggOptions?.id ?? Utils.randInt(EGG_SEED, EGG_SEED * this._tier);
|
||||
|
||||
this._sourceType = eggOptions?.sourceType ?? undefined;
|
||||
this._hatchWaves = eggOptions?.hatchWaves ?? this.getEggTierDefaultHatchWaves();
|
||||
this._timestamp = eggOptions?.timestamp ?? new Date().getTime();
|
||||
this._sourceType = eggOptions?.sourceType ?? undefined;
|
||||
this._hatchWaves = eggOptions?.hatchWaves ?? this.getEggTierDefaultHatchWaves();
|
||||
this._timestamp = eggOptions?.timestamp ?? new Date().getTime();
|
||||
|
||||
// First roll shiny and variant so we can filter if species with an variant exist
|
||||
this._isShiny = eggOptions?.isShiny ?? (Overrides.EGG_SHINY_OVERRIDE || this.rollShiny());
|
||||
this._variantTier = eggOptions?.variantTier ?? (Overrides.EGG_VARIANT_OVERRIDE ?? this.rollVariant());
|
||||
this._species = eggOptions?.species ?? this.rollSpecies(eggOptions!.scene!)!; // TODO: Are those bangs correct?
|
||||
// First roll shiny and variant so we can filter if species with an variant exist
|
||||
this._isShiny = eggOptions?.isShiny ?? (Overrides.EGG_SHINY_OVERRIDE || this.rollShiny());
|
||||
this._variantTier = eggOptions?.variantTier ?? (Overrides.EGG_VARIANT_OVERRIDE ?? this.rollVariant());
|
||||
this._species = eggOptions?.species ?? this.rollSpecies(eggOptions!.scene!)!; // TODO: Are those bangs correct?
|
||||
|
||||
this._overrideHiddenAbility = eggOptions?.overrideHiddenAbility ?? false;
|
||||
this._overrideHiddenAbility = eggOptions?.overrideHiddenAbility ?? false;
|
||||
|
||||
// Override egg tier and hatchwaves if species was given
|
||||
if (eggOptions?.species) {
|
||||
this._tier = this.getEggTierFromSpeciesStarterValue();
|
||||
this._hatchWaves = eggOptions.hatchWaves ?? this.getEggTierDefaultHatchWaves();
|
||||
}
|
||||
// If species has no variant, set variantTier to common. This needs to
|
||||
// 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;
|
||||
}
|
||||
// Needs this._tier so it needs to be generated afer the tier override if bought from same species
|
||||
this._eggMoveIndex = eggOptions?.eggMoveIndex ?? this.rollEggMoveIndex();
|
||||
if (eggOptions?.pulled) {
|
||||
this.increasePullStatistic(eggOptions.scene!); // TODO: is this bang correct?
|
||||
this.addEggToGameData(eggOptions.scene!); // TODO: is this bang correct?
|
||||
// Override egg tier and hatchwaves if species was given
|
||||
if (eggOptions?.species) {
|
||||
this._tier = this.getEggTierFromSpeciesStarterValue();
|
||||
this._hatchWaves = eggOptions.hatchWaves ?? this.getEggTierDefaultHatchWaves();
|
||||
}
|
||||
// If species has no variant, set variantTier to common. This needs to
|
||||
// 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;
|
||||
}
|
||||
// Needs this._tier so it needs to be generated afer the tier override if bought from same species
|
||||
this._eggMoveIndex = eggOptions?.eggMoveIndex ?? this.rollEggMoveIndex();
|
||||
if (eggOptions?.pulled) {
|
||||
this.increasePullStatistic(eggOptions.scene!); // TODO: is this bang correct?
|
||||
this.addEggToGameData(eggOptions.scene!); // TODO: is this bang correct?
|
||||
}
|
||||
};
|
||||
|
||||
if (eggOptions?.scene) {
|
||||
const seedOverride = Utils.randomString(24);
|
||||
eggOptions?.scene.executeWithSeedOffset(() => {
|
||||
generateEggProperties(eggOptions);
|
||||
}, 0, seedOverride);
|
||||
} else { // For legacy eggs without scene
|
||||
generateEggProperties(eggOptions);
|
||||
}
|
||||
}
|
||||
|
||||
@ -200,37 +211,46 @@ export class Egg {
|
||||
|
||||
// Generates a PlayerPokemon from an egg
|
||||
public generatePlayerPokemon(scene: BattleScene): PlayerPokemon {
|
||||
// Legacy egg wants to hatch. Generate missing properties
|
||||
if (!this._species) {
|
||||
this._isShiny = this.rollShiny();
|
||||
this._species = this.rollSpecies(scene!)!; // TODO: are these bangs correct?
|
||||
}
|
||||
let ret: PlayerPokemon;
|
||||
|
||||
let pokemonSpecies = getPokemonSpecies(this._species);
|
||||
// Special condition to have Phione eggs also have a chance of generating Manaphy
|
||||
if (this._species === Species.PHIONE) {
|
||||
pokemonSpecies = getPokemonSpecies(Utils.randSeedInt(MANAPHY_EGG_MANAPHY_RATE) ? Species.PHIONE : Species.MANAPHY);
|
||||
}
|
||||
const generatePlayerPokemonHelper = (scene: BattleScene) => {
|
||||
// Legacy egg wants to hatch. Generate missing properties
|
||||
if (!this._species) {
|
||||
this._isShiny = this.rollShiny();
|
||||
this._species = this.rollSpecies(scene!)!; // TODO: are these bangs correct?
|
||||
}
|
||||
|
||||
// Sets the hidden ability if a hidden ability exists and
|
||||
// the override is set or the egg hits the chance
|
||||
let abilityIndex: number | undefined = undefined;
|
||||
const sameSpeciesEggHACheck = (this._sourceType === EggSourceType.SAME_SPECIES_EGG && !Utils.randSeedInt(SAME_SPECIES_EGG_HA_RATE));
|
||||
const gachaEggHACheck = (!(this._sourceType === EggSourceType.SAME_SPECIES_EGG) && !Utils.randSeedInt(GACHA_EGG_HA_RATE));
|
||||
if (pokemonSpecies.abilityHidden && (this._overrideHiddenAbility || sameSpeciesEggHACheck || gachaEggHACheck)) {
|
||||
abilityIndex = 2;
|
||||
}
|
||||
let pokemonSpecies = getPokemonSpecies(this._species);
|
||||
// Special condition to have Phione eggs also have a chance of generating Manaphy
|
||||
if (this._species === Species.PHIONE && this._sourceType === EggSourceType.SAME_SPECIES_EGG) {
|
||||
pokemonSpecies = getPokemonSpecies(Utils.randSeedInt(MANAPHY_EGG_MANAPHY_RATE) ? Species.PHIONE : Species.MANAPHY);
|
||||
}
|
||||
|
||||
// This function has way to many optional parameters
|
||||
const ret: PlayerPokemon = scene.addPlayerPokemon(pokemonSpecies, 1, abilityIndex, undefined, undefined, false);
|
||||
ret.shiny = this._isShiny;
|
||||
ret.variant = this._variantTier;
|
||||
// Sets the hidden ability if a hidden ability exists and
|
||||
// the override is set or the egg hits the chance
|
||||
let abilityIndex: number | undefined = undefined;
|
||||
const sameSpeciesEggHACheck = (this._sourceType === EggSourceType.SAME_SPECIES_EGG && !Utils.randSeedInt(SAME_SPECIES_EGG_HA_RATE));
|
||||
const gachaEggHACheck = (!(this._sourceType === EggSourceType.SAME_SPECIES_EGG) && !Utils.randSeedInt(GACHA_EGG_HA_RATE));
|
||||
if (pokemonSpecies.abilityHidden && (this._overrideHiddenAbility || sameSpeciesEggHACheck || gachaEggHACheck)) {
|
||||
abilityIndex = 2;
|
||||
}
|
||||
|
||||
const secondaryIvs = Utils.getIvsFromId(Utils.randSeedInt(4294967295));
|
||||
// This function has way to many optional parameters
|
||||
ret = scene.addPlayerPokemon(pokemonSpecies, 1, abilityIndex, undefined, undefined, false);
|
||||
ret.shiny = this._isShiny;
|
||||
ret.variant = this._variantTier;
|
||||
|
||||
for (let s = 0; s < ret.ivs.length; s++) {
|
||||
ret.ivs[s] = Math.max(ret.ivs[s], secondaryIvs[s]);
|
||||
}
|
||||
const secondaryIvs = Utils.getIvsFromId(Utils.randSeedInt(4294967295));
|
||||
|
||||
for (let s = 0; s < ret.ivs.length; s++) {
|
||||
ret.ivs[s] = Math.max(ret.ivs[s], secondaryIvs[s]);
|
||||
}
|
||||
};
|
||||
|
||||
ret = ret!; // Tell TS compiler it's defined now
|
||||
scene.executeWithSeedOffset(() => {
|
||||
generatePlayerPokemonHelper(scene);
|
||||
}, this._id, EGG_SEED.toString());
|
||||
|
||||
return ret;
|
||||
}
|
||||
@ -306,7 +326,8 @@ export class Egg {
|
||||
break;
|
||||
}
|
||||
|
||||
return Utils.randSeedInt(baseChance * Math.pow(2, 3 - this.tier)) ? Utils.randSeedInt(3) : 3;
|
||||
const tierMultiplier = this.isManaphyEgg() ? 2 : Math.pow(2, 3 - this.tier);
|
||||
return Utils.randSeedInt(baseChance * tierMultiplier) ? Utils.randSeedInt(3) : 3;
|
||||
}
|
||||
|
||||
private getEggTierDefaultHatchWaves(eggTier?: EggTier): number {
|
||||
@ -341,7 +362,12 @@ export class Egg {
|
||||
* the species that was the legendary focus at the time
|
||||
*/
|
||||
if (this.isManaphyEgg()) {
|
||||
const rand = Utils.randSeedInt(MANAPHY_EGG_MANAPHY_RATE);
|
||||
/**
|
||||
* Adding a technicality to make unit tests easier: By making this check pass
|
||||
* when Utils.randSeedInt(8) = 1, and by making the generatePlayerPokemon() species
|
||||
* check pass when Utils.randSeedInt(8) = 0, we can tell them apart during tests.
|
||||
*/
|
||||
const rand = (Utils.randSeedInt(MANAPHY_EGG_MANAPHY_RATE) !== 1);
|
||||
return rand ? Species.PHIONE : Species.MANAPHY;
|
||||
} else if (this.tier === EggTier.MASTER
|
||||
&& this._sourceType === EggSourceType.GACHA_LEGENDARY) {
|
||||
|
974
src/data/move.ts
@ -1,9 +1,9 @@
|
||||
import { Stat, getStatName } from "./pokemon-stat";
|
||||
import * as Utils from "../utils";
|
||||
import { TextStyle, getBBCodeFrag } from "../ui/text";
|
||||
import { Nature } from "#enums/nature";
|
||||
import { UiTheme } from "#enums/ui-theme";
|
||||
import i18next from "i18next";
|
||||
import { Stat, EFFECTIVE_STATS, getShortenedStatKey } from "#app/enums/stat";
|
||||
|
||||
export { Nature };
|
||||
|
||||
@ -14,10 +14,9 @@ export function getNatureName(nature: Nature, includeStatEffects: boolean = fals
|
||||
ret = i18next.t("nature:" + ret as any);
|
||||
}
|
||||
if (includeStatEffects) {
|
||||
const stats = Utils.getEnumValues(Stat).slice(1);
|
||||
let increasedStat: Stat | null = null;
|
||||
let decreasedStat: Stat | null = null;
|
||||
for (const stat of stats) {
|
||||
for (const stat of EFFECTIVE_STATS) {
|
||||
const multiplier = getNatureStatMultiplier(nature, stat);
|
||||
if (multiplier > 1) {
|
||||
increasedStat = stat;
|
||||
@ -28,7 +27,7 @@ export function getNatureName(nature: Nature, includeStatEffects: boolean = fals
|
||||
const textStyle = forStarterSelect ? TextStyle.SUMMARY_ALT : TextStyle.WINDOW;
|
||||
const getTextFrag = !ignoreBBCode ? (text: string, style: TextStyle) => getBBCodeFrag(text, style, uiTheme) : (text: string, style: TextStyle) => text;
|
||||
if (increasedStat && decreasedStat) {
|
||||
ret = `${getTextFrag(`${ret}${!forStarterSelect ? "\n" : " "}(`, textStyle)}${getTextFrag(`+${getStatName(increasedStat, true)}`, TextStyle.SUMMARY_PINK)}${getTextFrag("/", textStyle)}${getTextFrag(`-${getStatName(decreasedStat, true)}`, TextStyle.SUMMARY_BLUE)}${getTextFrag(")", textStyle)}`;
|
||||
ret = `${getTextFrag(`${ret}${!forStarterSelect ? "\n" : " "}(`, textStyle)}${getTextFrag(`+${i18next.t(getShortenedStatKey(increasedStat))}`, TextStyle.SUMMARY_PINK)}${getTextFrag("/", textStyle)}${getTextFrag(`-${i18next.t(getShortenedStatKey(decreasedStat))}`, TextStyle.SUMMARY_BLUE)}${getTextFrag(")", textStyle)}`;
|
||||
} else {
|
||||
ret = getTextFrag(`${ret}${!forStarterSelect ? "\n" : " "}(-)`, textStyle);
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Gender } from "./gender";
|
||||
import { PokeballType } from "./pokeball";
|
||||
import Pokemon from "../field/pokemon";
|
||||
import { Stat } from "./pokemon-stat";
|
||||
import { Stat } from "#enums/stat";
|
||||
import { Type } from "./type";
|
||||
import * as Utils from "../utils";
|
||||
import { SpeciesFormKey } from "./pokemon-species";
|
||||
|
@ -8,7 +8,7 @@ import { Abilities } from "#enums/abilities";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
import { TimeOfDay } from "#enums/time-of-day";
|
||||
import { getPokemonNameWithAffix } from "#app/messages.js";
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
import i18next from "i18next";
|
||||
import { WeatherType } from "./weather";
|
||||
|
||||
|
@ -14,7 +14,7 @@ import { GrowthRate } from "./exp";
|
||||
import { EvolutionLevel, SpeciesWildEvolutionDelay, pokemonEvolutions, pokemonPrevolutions } from "./pokemon-evolutions";
|
||||
import { Type } from "./type";
|
||||
import { LevelMoves, pokemonFormLevelMoves, pokemonFormLevelMoves as pokemonSpeciesFormLevelMoves, pokemonSpeciesLevelMoves } from "./pokemon-level-moves";
|
||||
import { Stat } from "./pokemon-stat";
|
||||
import { Stat } from "#enums/stat";
|
||||
import { Variant, VariantSet, variantColorCache, variantData } from "./variant";
|
||||
|
||||
export enum Region {
|
||||
@ -944,7 +944,7 @@ export function initSpecies() {
|
||||
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.TINTED_LENS, Abilities.TINTED_LENS, Abilities.TINTED_LENS, 495, 85, 35, 80, 120, 90, 85, 45, 50, 198, 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 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),
|
||||
|
@ -1,29 +0,0 @@
|
||||
import { Stat } from "#enums/stat";
|
||||
import i18next from "i18next";
|
||||
|
||||
export { Stat };
|
||||
|
||||
export function getStatName(stat: Stat, shorten: boolean = false) {
|
||||
let ret: string = "";
|
||||
switch (stat) {
|
||||
case Stat.HP:
|
||||
ret = !shorten ? i18next.t("pokemonInfo:Stat.HP") : i18next.t("pokemonInfo:Stat.HPshortened");
|
||||
break;
|
||||
case Stat.ATK:
|
||||
ret = !shorten ? i18next.t("pokemonInfo:Stat.ATK") : i18next.t("pokemonInfo:Stat.ATKshortened");
|
||||
break;
|
||||
case Stat.DEF:
|
||||
ret = !shorten ? i18next.t("pokemonInfo:Stat.DEF") : i18next.t("pokemonInfo:Stat.DEFshortened");
|
||||
break;
|
||||
case Stat.SPATK:
|
||||
ret = !shorten ? i18next.t("pokemonInfo:Stat.SPATK") : i18next.t("pokemonInfo:Stat.SPATKshortened");
|
||||
break;
|
||||
case Stat.SPDEF:
|
||||
ret = !shorten ? i18next.t("pokemonInfo:Stat.SPDEF") : i18next.t("pokemonInfo:Stat.SPDEFshortened");
|
||||
break;
|
||||
case Stat.SPD:
|
||||
ret = !shorten ? i18next.t("pokemonInfo:Stat.SPD") : i18next.t("pokemonInfo:Stat.SPDshortened");
|
||||
break;
|
||||
}
|
||||
return ret;
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
import { BattleStat, getBattleStatName } from "./battle-stat";
|
||||
import i18next from "i18next";
|
||||
|
||||
export enum TempBattleStat {
|
||||
ATK,
|
||||
DEF,
|
||||
SPATK,
|
||||
SPDEF,
|
||||
SPD,
|
||||
ACC,
|
||||
CRIT
|
||||
}
|
||||
|
||||
export function getTempBattleStatName(tempBattleStat: TempBattleStat) {
|
||||
if (tempBattleStat === TempBattleStat.CRIT) {
|
||||
return i18next.t("modifierType:TempBattleStatBoosterStatName.CRIT");
|
||||
}
|
||||
return getBattleStatName(tempBattleStat as integer as BattleStat);
|
||||
}
|
||||
|
||||
export function getTempBattleStatBoosterItemName(tempBattleStat: TempBattleStat) {
|
||||
switch (tempBattleStat) {
|
||||
case TempBattleStat.ATK:
|
||||
return "X Attack";
|
||||
case TempBattleStat.DEF:
|
||||
return "X Defense";
|
||||
case TempBattleStat.SPATK:
|
||||
return "X Sp. Atk";
|
||||
case TempBattleStat.SPDEF:
|
||||
return "X Sp. Def";
|
||||
case TempBattleStat.SPD:
|
||||
return "X Speed";
|
||||
case TempBattleStat.ACC:
|
||||
return "X Accuracy";
|
||||
case TempBattleStat.CRIT:
|
||||
return "Dire Hit";
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@ import { Type } from "./type";
|
||||
import * as Utils from "../utils";
|
||||
import { ChangeMovePriorityAbAttr, applyAbAttrs } from "./ability";
|
||||
import { ProtectAttr } from "./move";
|
||||
import { BattlerIndex } from "#app/battle.js";
|
||||
import { BattlerIndex } from "#app/battle";
|
||||
import i18next from "i18next";
|
||||
|
||||
export enum TerrainType {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { VariantTier } from "#app/enums/variant-tier.js";
|
||||
import { VariantTier } from "#app/enums/variant-tier";
|
||||
|
||||
export type Variant = 0 | 1 | 2;
|
||||
|
||||
|
@ -64,6 +64,7 @@ export enum BattlerTagType {
|
||||
STOCKPILING = "STOCKPILING",
|
||||
RECEIVE_DOUBLE_DAMAGE = "RECEIVE_DOUBLE_DAMAGE",
|
||||
ALWAYS_GET_HIT = "ALWAYS_GET_HIT",
|
||||
DISABLED = "DISABLED",
|
||||
IGNORE_GHOST = "IGNORE_GHOST",
|
||||
IGNORE_DARK = "IGNORE_DARK",
|
||||
GULP_MISSILE_ARROKUDA = "GULP_MISSILE_ARROKUDA",
|
||||
@ -72,5 +73,8 @@ export enum BattlerTagType {
|
||||
SHELL_TRAP = "SHELL_TRAP",
|
||||
DRAGON_CHEER = "DRAGON_CHEER",
|
||||
NO_RETREAT = "NO_RETREAT",
|
||||
GORILLA_TACTICS = "GORILLA_TACTICS",
|
||||
THROAT_CHOPPED = "THROAT_CHOPPED",
|
||||
TAR_SHOT = "TAR_SHOT",
|
||||
BURNED_UP = "BURNED_UP",
|
||||
}
|
||||
|
@ -1,13 +1,13 @@
|
||||
/**
|
||||
* Determines the cursor target when entering the shop phase.
|
||||
* Determines the row cursor target when entering the shop phase.
|
||||
*/
|
||||
export enum ShopCursorTarget {
|
||||
/** Cursor points to Reroll */
|
||||
/** Cursor points to Reroll row */
|
||||
REROLL,
|
||||
/** Cursor points to Items */
|
||||
ITEMS,
|
||||
/** Cursor points to Shop */
|
||||
/** Cursor points to Rewards row */
|
||||
REWARDS,
|
||||
/** Cursor points to Shop row */
|
||||
SHOP,
|
||||
/** Cursor points to Check Team */
|
||||
/** Cursor points to Check Team row */
|
||||
CHECK_TEAM
|
||||
}
|
||||
|
@ -1,8 +1,75 @@
|
||||
/** Enum that comprises all possible stat-related attributes, in-battle and permanent, of a Pokemon. */
|
||||
export enum Stat {
|
||||
/** Hit Points */
|
||||
HP = 0,
|
||||
/** Attack */
|
||||
ATK,
|
||||
/** Defense */
|
||||
DEF,
|
||||
/** Special Attack */
|
||||
SPATK,
|
||||
/** Special Defense */
|
||||
SPDEF,
|
||||
/** Speed */
|
||||
SPD,
|
||||
/** Accuracy */
|
||||
ACC,
|
||||
/** Evasiveness */
|
||||
EVA
|
||||
}
|
||||
|
||||
/** A constant array comprised of the {@linkcode Stat} values that make up {@linkcode PermanentStat}. */
|
||||
export const PERMANENT_STATS = [ Stat.HP, Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ] as const;
|
||||
/** Type used to describe the core, permanent stats of a Pokemon. */
|
||||
export type PermanentStat = typeof PERMANENT_STATS[number];
|
||||
|
||||
/** A constant array comprised of the {@linkcode Stat} values that make up {@linkcode EFfectiveStat}. */
|
||||
export const EFFECTIVE_STATS = [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ] as const;
|
||||
/** Type used to describe the intersection of core stats and stats that have stages in battle. */
|
||||
export type EffectiveStat = typeof EFFECTIVE_STATS[number];
|
||||
|
||||
/** A constant array comprised of {@linkcode Stat} the values that make up {@linkcode BattleStat}. */
|
||||
export const BATTLE_STATS = [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD, Stat.ACC, Stat.EVA ] as const;
|
||||
/** Type used to describe the stats that have stages which can be incremented and decremented in battle. */
|
||||
export type BattleStat = typeof BATTLE_STATS[number];
|
||||
|
||||
/** A constant array comprised of {@linkcode Stat} the values that make up {@linkcode TempBattleStat}. */
|
||||
export const TEMP_BATTLE_STATS = [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD, Stat.ACC ] as const;
|
||||
/** Type used to describe the stats that have X item (`TEMP_STAT_STAGE_BOOSTER`) equivalents. */
|
||||
export type TempBattleStat = typeof TEMP_BATTLE_STATS[number];
|
||||
|
||||
/**
|
||||
* Provides the translation key corresponding to the amount of stat stages and whether those stat stages
|
||||
* are positive or negative.
|
||||
* @param stages the amount of stages
|
||||
* @param isIncrease dictates a negative (`false`) or a positive (`true`) stat stage change
|
||||
* @returns the translation key fitting the conditions described by {@linkcode stages} and {@linkcode isIncrease}
|
||||
*/
|
||||
export function getStatStageChangeDescriptionKey(stages: number, isIncrease: boolean) {
|
||||
if (stages === 1) {
|
||||
return isIncrease ? "battle:statRose" : "battle:statFell";
|
||||
} else if (stages === 2) {
|
||||
return isIncrease ? "battle:statSharplyRose" : "battle:statHarshlyFell";
|
||||
} else if (stages <= 6) {
|
||||
return isIncrease ? "battle:statRoseDrastically" : "battle:statSeverelyFell";
|
||||
}
|
||||
return isIncrease ? "battle:statWontGoAnyHigher" : "battle:statWontGoAnyLower";
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the translation key corresponding to a given stat which can be translated into its full name.
|
||||
* @param stat the {@linkcode Stat} to be translated
|
||||
* @returns the translation key corresponding to the given {@linkcode Stat}
|
||||
*/
|
||||
export function getStatKey(stat: Stat) {
|
||||
return `pokemonInfo:Stat.${Stat[stat]}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the translation key corresponding to a given stat which can be translated into its shortened name.
|
||||
* @param stat the {@linkcode Stat} to be translated
|
||||
* @returns the translation key corresponding to the given {@linkcode Stat}
|
||||
*/
|
||||
export function getShortenedStatKey(stat: PermanentStat) {
|
||||
return `pokemonInfo:Stat.${Stat[stat]}shortened`;
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { ArenaTagSide } from "#app/data/arena-tag.js";
|
||||
import { ArenaTagSide } from "#app/data/arena-tag";
|
||||
import { ArenaTagType } from "#enums/arena-tag-type";
|
||||
import { TerrainType } from "#app/data/terrain.js";
|
||||
import { WeatherType } from "#app/data/weather.js";
|
||||
import { TerrainType } from "#app/data/terrain";
|
||||
import { WeatherType } from "#app/data/weather";
|
||||
|
||||
/** Alias for all {@linkcode ArenaEvent} type strings */
|
||||
export enum ArenaEventType {
|
||||
|
@ -60,7 +60,7 @@ export class Arena {
|
||||
this.scene.arenaBg.setTexture(`${biomeKey}_bg`);
|
||||
this.scene.arenaBgTransition.setTexture(`${biomeKey}_bg`);
|
||||
|
||||
// Redo this on initialise because during save/load the current wave isn't always
|
||||
// Redo this on initialize because during save/load the current wave isn't always
|
||||
// set correctly during construction
|
||||
this.updatePoolsForTimeOfDay();
|
||||
}
|
||||
@ -289,7 +289,7 @@ export class Arena {
|
||||
|
||||
/**
|
||||
* Sets weather to the override specified in overrides.ts
|
||||
* @param weather new weather to set of type WeatherType
|
||||
* @param weather new {@linkcode WeatherType} to set
|
||||
* @returns true to force trySetWeather to return true
|
||||
*/
|
||||
trySetWeatherOverride(weather: WeatherType): boolean {
|
||||
@ -301,8 +301,8 @@ export class Arena {
|
||||
|
||||
/**
|
||||
* Attempts to set a new weather to the battle
|
||||
* @param weather new weather to set of type WeatherType
|
||||
* @param hasPokemonSource is the new weather from a pokemon
|
||||
* @param weather {@linkcode WeatherType} new {@linkcode WeatherType} to set
|
||||
* @param hasPokemonSource boolean if the new weather is from a pokemon
|
||||
* @returns true if new weather set, false if no weather provided or attempting to set the same weather as currently in use
|
||||
*/
|
||||
trySetWeather(weather: WeatherType, hasPokemonSource: boolean): boolean {
|
||||
@ -573,6 +573,12 @@ export class Arena {
|
||||
this.ignoreAbilities = ignoreAbilities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies each `ArenaTag` in this Arena, based on which side (self, enemy, or both) is passed in as a parameter
|
||||
* @param tagType Either an {@linkcode ArenaTagType} string, or an actual {@linkcode ArenaTag} class to filter which ones to apply
|
||||
* @param side {@linkcode ArenaTagSide} which side's arena tags to apply
|
||||
* @param args array of parameters that the called upon tags may need
|
||||
*/
|
||||
applyTagsForSide(tagType: ArenaTagType | Constructor<ArenaTag>, side: ArenaTagSide, ...args: unknown[]): void {
|
||||
let tags = typeof tagType === "string"
|
||||
? this.tags.filter(t => t.tagType === tagType)
|
||||
@ -583,11 +589,28 @@ export class Arena {
|
||||
tags.forEach(t => t.apply(this, args));
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the specified tag to both sides (ie: both user and trainer's tag that match the Tag specified)
|
||||
* by calling {@linkcode applyTagsForSide()}
|
||||
* @param tagType Either an {@linkcode ArenaTagType} string, or an actual {@linkcode ArenaTag} class to filter which ones to apply
|
||||
* @param args array of parameters that the called upon tags may need
|
||||
*/
|
||||
applyTags(tagType: ArenaTagType | Constructor<ArenaTag>, ...args: unknown[]): void {
|
||||
this.applyTagsForSide(tagType, ArenaTagSide.BOTH, ...args);
|
||||
}
|
||||
|
||||
addTag(tagType: ArenaTagType, turnCount: integer, sourceMove: Moves | undefined, sourceId: integer, side: ArenaTagSide = ArenaTagSide.BOTH, quiet: boolean = false, targetIndex?: BattlerIndex): boolean {
|
||||
/**
|
||||
* Adds a new tag to the arena
|
||||
* @param tagType {@linkcode ArenaTagType} the tag being added
|
||||
* @param turnCount How many turns the tag lasts
|
||||
* @param sourceMove {@linkcode Moves} the move the tag came from, or `undefined` if not from a move
|
||||
* @param sourceId The ID of the pokemon in play the tag came from (see {@linkcode BattleScene.getPokemonById})
|
||||
* @param side {@linkcode ArenaTagSide} which side(s) the tag applies to
|
||||
* @param quiet If a message should be queued on screen to announce the tag being added
|
||||
* @param targetIndex The {@linkcode BattlerIndex} of the target pokemon
|
||||
* @returns `false` if there already exists a tag of this type in the Arena
|
||||
*/
|
||||
addTag(tagType: ArenaTagType, turnCount: number, sourceMove: Moves | undefined, sourceId: number, side: ArenaTagSide = ArenaTagSide.BOTH, quiet: boolean = false, targetIndex?: BattlerIndex): boolean {
|
||||
const existingTag = this.getTagOnSide(tagType, side);
|
||||
if (existingTag) {
|
||||
existingTag.onOverlap(this);
|
||||
@ -600,6 +623,7 @@ export class Arena {
|
||||
return false;
|
||||
}
|
||||
|
||||
// creates a new tag object
|
||||
const newTag = getArenaTag(tagType, turnCount || 0, sourceMove, sourceId, targetIndex, side);
|
||||
if (newTag) {
|
||||
this.tags.push(newTag);
|
||||
@ -613,6 +637,11 @@ export class Arena {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to get a tag from the Arena via {@linkcode getTagOnSide} that applies to both sides
|
||||
* @param tagType The {@linkcode ArenaTagType} or {@linkcode ArenaTag} to get
|
||||
* @returns either the {@linkcode ArenaTag}, or `undefined` if it isn't there
|
||||
*/
|
||||
getTag(tagType: ArenaTagType | Constructor<ArenaTag>): ArenaTag | undefined {
|
||||
return this.getTagOnSide(tagType, ArenaTagSide.BOTH);
|
||||
}
|
||||
@ -621,16 +650,35 @@ export class Arena {
|
||||
return !!this.getTag(tagType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to get a tag from the Arena from a specific side (the tag passed in has to either apply to both sides, or the specific side only)
|
||||
*
|
||||
* eg: `MIST` only applies to the user's side, while `MUD_SPORT` applies to both user and enemy side
|
||||
* @param tagType The {@linkcode ArenaTagType} or {@linkcode ArenaTag} to get
|
||||
* @param side The {@linkcode ArenaTagSide} to look at
|
||||
* @returns either the {@linkcode ArenaTag}, or `undefined` if it isn't there
|
||||
*/
|
||||
getTagOnSide(tagType: ArenaTagType | Constructor<ArenaTag>, side: ArenaTagSide): ArenaTag | undefined {
|
||||
return typeof(tagType) === "string"
|
||||
? this.tags.find(t => t.tagType === tagType && (side === ArenaTagSide.BOTH || t.side === ArenaTagSide.BOTH || t.side === side))
|
||||
: this.tags.find(t => t instanceof tagType && (side === ArenaTagSide.BOTH || t.side === ArenaTagSide.BOTH || t.side === side));
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses {@linkcode findTagsOnSide} to filter (using the parameter function) for specific tags that apply to both sides
|
||||
* @param tagPredicate a function mapping {@linkcode ArenaTag}s to `boolean`s
|
||||
* @returns array of {@linkcode ArenaTag}s from which the Arena's tags return true and apply to both sides
|
||||
*/
|
||||
findTags(tagPredicate: (t: ArenaTag) => boolean): ArenaTag[] {
|
||||
return this.findTagsOnSide(tagPredicate, ArenaTagSide.BOTH);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns specific tags from the arena that pass the `tagPredicate` function passed in as a parameter, and apply to the given side
|
||||
* @param tagPredicate a function mapping {@linkcode ArenaTag}s to `boolean`s
|
||||
* @param side The {@linkcode ArenaTagSide} to look at
|
||||
* @returns array of {@linkcode ArenaTag}s from which the Arena's tags return `true` and apply to the given side
|
||||
*/
|
||||
findTagsOnSide(tagPredicate: (t: ArenaTag) => boolean, side: ArenaTagSide): ArenaTag[] {
|
||||
return this.tags.filter(t => tagPredicate(t) && (side === ArenaTagSide.BOTH || t.side === ArenaTagSide.BOTH || t.side === side));
|
||||
}
|
||||
|
@ -3,26 +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, IgnoreOpponentStatChangesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatChangeAttr, 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 } 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 * as Utils from "../utils";
|
||||
import { Type, TypeDamageMultiplier, getTypeDamageMultiplier, getTypeRgb } from "../data/type";
|
||||
import { getLevelTotalExp } from "../data/exp";
|
||||
import { Stat } from "../data/pokemon-stat";
|
||||
import { DamageMoneyRewardModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, HiddenAbilityRateBoosterModifier, PokemonBaseStatModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonNatureWeightModifier, ShinyRateBoosterModifier, SurviveDamageModifier, TempBattleStatBoosterModifier, StatBoosterModifier, CritBoosterModifier, TerastallizeModifier } from "../modifier/modifier";
|
||||
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 { 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 { BattleStat } from "../data/battle-stat";
|
||||
import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag } from "../data/battler-tags";
|
||||
import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, MoveRestrictionBattlerTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag, TarShotTag } from "../data/battler-tags";
|
||||
import { WeatherType } from "../data/weather";
|
||||
import { TempBattleStat } from "../data/temp-battle-stat";
|
||||
import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "../data/arena-tag";
|
||||
import { Ability, AbAttr, BattleStatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatChangesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyBattleStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldBattleStatMultiplierAbAttrs, FieldMultiplyBattleStatAbAttr, AddSecondStrikeAbAttr, IgnoreOpponentEvasionAbAttr, 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 } from "../data/ability";
|
||||
import PokemonData from "../system/pokemon-data";
|
||||
import { BattlerIndex } from "../battle";
|
||||
import { Mode } from "../ui/ui";
|
||||
@ -40,7 +38,7 @@ import Overrides from "#app/overrides";
|
||||
import i18next from "i18next";
|
||||
import { speciesEggMoves } from "../data/egg-moves";
|
||||
import { ModifierTier } from "../modifier/modifier-tier";
|
||||
import { applyChallenges, ChallengeType } from "#app/data/challenge.js";
|
||||
import { applyChallenges, ChallengeType } from "#app/data/challenge";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { ArenaTagType } from "#enums/arena-tag-type";
|
||||
import { BattleSpec } from "#enums/battle-spec";
|
||||
@ -49,17 +47,18 @@ import { BerryType } from "#enums/berry-type";
|
||||
import { Biome } from "#enums/biome";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
import { DamagePhase } from "#app/phases/damage-phase";
|
||||
import { FaintPhase } from "#app/phases/faint-phase";
|
||||
import { LearnMovePhase } from "#app/phases/learn-move-phase";
|
||||
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
|
||||
import { MoveEndPhase } from "#app/phases/move-end-phase";
|
||||
import { ObtainStatusEffectPhase } from "#app/phases/obtain-status-effect-phase";
|
||||
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
|
||||
import { SwitchSummonPhase } from "#app/phases/switch-summon-phase";
|
||||
import { ToggleDoublePositionPhase } from "#app/phases/toggle-double-position-phase";
|
||||
import { Challenges } from "#enums/challenges";
|
||||
import { getPokemonNameWithAffix } from "#app/messages.js";
|
||||
import { DamagePhase } from "#app/phases/damage-phase.js";
|
||||
import { FaintPhase } from "#app/phases/faint-phase.js";
|
||||
import { LearnMovePhase } from "#app/phases/learn-move-phase.js";
|
||||
import { MoveEffectPhase } from "#app/phases/move-effect-phase.js";
|
||||
import { MoveEndPhase } from "#app/phases/move-end-phase.js";
|
||||
import { ObtainStatusEffectPhase } from "#app/phases/obtain-status-effect-phase.js";
|
||||
import { StatChangePhase } from "#app/phases/stat-change-phase.js";
|
||||
import { SwitchSummonPhase } from "#app/phases/switch-summon-phase.js";
|
||||
import { ToggleDoublePositionPhase } from "#app/phases/toggle-double-position-phase.js";
|
||||
import { PLAYER_PARTY_MAX_SIZE } from "#app/constants";
|
||||
|
||||
export enum FieldPosition {
|
||||
CENTER,
|
||||
@ -676,49 +675,139 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
});
|
||||
}
|
||||
|
||||
getStat(stat: Stat): integer {
|
||||
/**
|
||||
* Retrieves the entire set of stats of the {@linkcode Pokemon}.
|
||||
* @param bypassSummonData prefer actual stats (`true` by default) or in-battle overriden stats (`false`)
|
||||
* @returns the numeric values of the {@linkcode Pokemon}'s stats
|
||||
*/
|
||||
getStats(bypassSummonData: boolean = true): number[] {
|
||||
if (!bypassSummonData && this.summonData?.stats) {
|
||||
return this.summonData.stats;
|
||||
}
|
||||
return this.stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the corresponding {@linkcode PermanentStat} of the {@linkcode Pokemon}.
|
||||
* @param stat the desired {@linkcode PermanentStat}
|
||||
* @param bypassSummonData prefer actual stats (`true` by default) or in-battle overridden stats (`false`)
|
||||
* @returns the numeric value of the desired {@linkcode Stat}
|
||||
*/
|
||||
getStat(stat: PermanentStat, bypassSummonData: boolean = true): number {
|
||||
if (!bypassSummonData && this.summonData && (this.summonData.stats[stat] !== 0)) {
|
||||
return this.summonData.stats[stat];
|
||||
}
|
||||
return this.stats[stat];
|
||||
}
|
||||
|
||||
getBattleStat(stat: Stat, opponent?: Pokemon, move?: Move, isCritical: boolean = false): integer {
|
||||
if (stat === Stat.HP) {
|
||||
return this.getStat(Stat.HP);
|
||||
}
|
||||
const battleStat = (stat - 1) as BattleStat;
|
||||
const statLevel = new Utils.IntegerHolder(this.summonData.battleStats[battleStat]);
|
||||
if (opponent) {
|
||||
if (isCritical) {
|
||||
switch (stat) {
|
||||
case Stat.ATK:
|
||||
case Stat.SPATK:
|
||||
statLevel.value = Math.max(statLevel.value, 0);
|
||||
break;
|
||||
case Stat.DEF:
|
||||
case Stat.SPDEF:
|
||||
statLevel.value = Math.min(statLevel.value, 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
applyAbAttrs(IgnoreOpponentStatChangesAbAttr, opponent, null, false, statLevel);
|
||||
if (move) {
|
||||
applyMoveAttrs(IgnoreOpponentStatChangesAttr, this, opponent, move, statLevel);
|
||||
/**
|
||||
* Writes the value to the corrseponding {@linkcode PermanentStat} of the {@linkcode Pokemon}.
|
||||
*
|
||||
* Note that this does nothing if {@linkcode value} is less than 0.
|
||||
* @param stat the desired {@linkcode PermanentStat} to be overwritten
|
||||
* @param value the desired numeric value
|
||||
* @param bypassSummonData write to actual stats (`true` by default) or in-battle overridden stats (`false`)
|
||||
*/
|
||||
setStat(stat: PermanentStat, value: number, bypassSummonData: boolean = true): void {
|
||||
if (value >= 0) {
|
||||
if (!bypassSummonData && this.summonData) {
|
||||
this.summonData.stats[stat] = value;
|
||||
} else {
|
||||
this.stats[stat] = value;
|
||||
}
|
||||
}
|
||||
if (this.isPlayer()) {
|
||||
this.scene.applyModifiers(TempBattleStatBoosterModifier, this.isPlayer(), battleStat as integer as TempBattleStat, statLevel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the entire set of in-battle stat stages of the {@linkcode Pokemon}.
|
||||
* @returns the numeric values of the {@linkcode Pokemon}'s in-battle stat stages if available, a fresh stat stage array otherwise
|
||||
*/
|
||||
getStatStages(): number[] {
|
||||
return this.summonData ? this.summonData.statStages : [ 0, 0, 0, 0, 0, 0, 0 ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the in-battle stage of the specified {@linkcode BattleStat}.
|
||||
* @param stat the {@linkcode BattleStat} whose stage is desired
|
||||
* @returns the stage of the desired {@linkcode BattleStat} if available, 0 otherwise
|
||||
*/
|
||||
getStatStage(stat: BattleStat): number {
|
||||
return this.summonData ? this.summonData.statStages[stat - 1] : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the value to the in-battle stage of the corresponding {@linkcode BattleStat} of the {@linkcode Pokemon}.
|
||||
*
|
||||
* Note that, if the value is not within a range of [-6, 6], it will be forced to the closest range bound.
|
||||
* @param stat the {@linkcode BattleStat} whose stage is to be overwritten
|
||||
* @param value the desired numeric value
|
||||
*/
|
||||
setStatStage(stat: BattleStat, value: number): void {
|
||||
if (this.summonData) {
|
||||
if (value >= -6) {
|
||||
this.summonData.statStages[stat - 1] = Math.min(value, 6);
|
||||
} else {
|
||||
this.summonData.statStages[stat - 1] = Math.max(value, -6);
|
||||
}
|
||||
}
|
||||
const statValue = new Utils.NumberHolder(this.getStat(stat));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the critical-hit stage considering the move used and the Pokemon
|
||||
* who used it.
|
||||
* @param source the {@linkcode Pokemon} who using the move
|
||||
* @param move the {@linkcode Move} being used
|
||||
* @returns the final critical-hit stage value
|
||||
*/
|
||||
getCritStage(source: Pokemon, move: Move): number {
|
||||
const critStage = new Utils.IntegerHolder(0);
|
||||
applyMoveAttrs(HighCritAttr, source, this, move, critStage);
|
||||
this.scene.applyModifiers(CritBoosterModifier, source.isPlayer(), source, critStage);
|
||||
this.scene.applyModifiers(TempCritBoosterModifier, source.isPlayer(), critStage);
|
||||
const bonusCrit = new Utils.BooleanHolder(false);
|
||||
//@ts-ignore
|
||||
if (applyAbAttrs(BonusCritAbAttr, source, null, false, bonusCrit)) { // TODO: resolve ts-ignore. This is a promise. Checking a promise is bogus.
|
||||
if (bonusCrit.value) {
|
||||
critStage.value += 1;
|
||||
}
|
||||
}
|
||||
const critBoostTag = source.getTag(CritBoostTag);
|
||||
if (critBoostTag) {
|
||||
if (critBoostTag instanceof DragonCheerTag) {
|
||||
critStage.value += critBoostTag.typesOnAdd.includes(Type.DRAGON) ? 2 : 1;
|
||||
} else {
|
||||
critStage.value += 2;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`crit stage: +${critStage.value}`);
|
||||
return critStage.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates and retrieves the final value of a stat considering any held
|
||||
* items, move effects, opponent abilities, and whether there was a critical
|
||||
* hit.
|
||||
* @param stat the desired {@linkcode EffectiveStat}
|
||||
* @param opponent the target {@linkcode Pokemon}
|
||||
* @param move the {@linkcode Move} being used
|
||||
* @param isCritical determines whether a critical hit has occurred or not (`false` by default)
|
||||
* @returns the final in-battle value of a stat
|
||||
*/
|
||||
getEffectiveStat(stat: EffectiveStat, opponent?: Pokemon, move?: Move, isCritical: boolean = false): integer {
|
||||
const statValue = new Utils.NumberHolder(this.getStat(stat, false));
|
||||
this.scene.applyModifiers(StatBoosterModifier, this.isPlayer(), this, stat, statValue);
|
||||
|
||||
const fieldApplied = new Utils.BooleanHolder(false);
|
||||
for (const pokemon of this.scene.getField(true)) {
|
||||
applyFieldBattleStatMultiplierAbAttrs(FieldMultiplyBattleStatAbAttr, pokemon, stat, statValue, this, fieldApplied);
|
||||
applyFieldStatMultiplierAbAttrs(FieldMultiplyStatAbAttr, pokemon, stat, statValue, this, fieldApplied);
|
||||
if (fieldApplied.value) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
applyBattleStatMultiplierAbAttrs(BattleStatMultiplierAbAttr, this, battleStat, statValue);
|
||||
let ret = statValue.value * (Math.max(2, 2 + statLevel.value) / Math.max(2, 2 - statLevel.value));
|
||||
applyStatMultiplierAbAttrs(StatMultiplierAbAttr, this, stat, statValue);
|
||||
let ret = statValue.value * this.getStatStageMultiplier(stat, opponent, move, isCritical);
|
||||
|
||||
switch (stat) {
|
||||
case Stat.ATK:
|
||||
if (this.getTag(BattlerTagType.SLOW_START)) {
|
||||
@ -765,24 +854,25 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
if (!this.stats) {
|
||||
this.stats = [ 0, 0, 0, 0, 0, 0 ];
|
||||
}
|
||||
const baseStats = this.getSpeciesForm().baseStats.slice(0);
|
||||
if (this.fusionSpecies) {
|
||||
const fusionBaseStats = this.getFusionSpeciesForm().baseStats;
|
||||
for (let s = 0; s < this.stats.length; s++) {
|
||||
|
||||
// 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 (let s = 0; s < this.stats.length; s++) {
|
||||
for (const s of PERMANENT_STATS) {
|
||||
baseStats[s] = Math.ceil(baseStats[s] / 2);
|
||||
}
|
||||
}
|
||||
this.scene.applyModifiers(PokemonBaseStatModifier, this.isPlayer(), this, baseStats);
|
||||
const stats = Utils.getEnumValues(Stat);
|
||||
for (const s of stats) {
|
||||
const isHp = s === Stat.HP;
|
||||
const baseStat = baseStats[s];
|
||||
let value = Math.floor(((2 * baseStat + this.ivs[s]) * this.level) * 0.01);
|
||||
if (isHp) {
|
||||
this.scene.applyModifiers(BaseStatModifier, this.isPlayer(), this, baseStats);
|
||||
|
||||
// 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);
|
||||
if (s === Stat.HP) {
|
||||
value = value + this.level + 10;
|
||||
if (this.hasAbility(Abilities.WONDER_GUARD, false, true)) {
|
||||
value = 1;
|
||||
@ -803,7 +893,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
value = Math.max(Math[natureStatMultiplier.value > 1 ? "ceil" : "floor"](value * natureStatMultiplier.value), 1);
|
||||
}
|
||||
}
|
||||
this.stats[s] = value;
|
||||
|
||||
this.setStat(s, value);
|
||||
}
|
||||
}
|
||||
|
||||
@ -893,10 +984,16 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
: this.moveset;
|
||||
|
||||
// Overrides moveset based on arrays specified in overrides.ts
|
||||
const overrideArray: Array<Moves> = this.isPlayer() ? Overrides.MOVESET_OVERRIDE : Overrides.OPP_MOVESET_OVERRIDE;
|
||||
let overrideArray: Moves | Array<Moves> = this.isPlayer() ? Overrides.MOVESET_OVERRIDE : Overrides.OPP_MOVESET_OVERRIDE;
|
||||
if (!Array.isArray(overrideArray)) {
|
||||
overrideArray = [overrideArray];
|
||||
}
|
||||
if (overrideArray.length > 0) {
|
||||
if (!this.isPlayer()) {
|
||||
this.moveset = [];
|
||||
}
|
||||
overrideArray.forEach((move: Moves, index: number) => {
|
||||
const ppUsed = this.moveset[index]?.ppUsed || 0;
|
||||
const ppUsed = this.moveset[index]?.ppUsed ?? 0;
|
||||
this.moveset[index] = new PokemonMove(move, Math.min(ppUsed, allMoves[move].pp));
|
||||
});
|
||||
}
|
||||
@ -959,6 +1056,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
const teraType = this.getTeraType();
|
||||
if (teraType !== Type.UNKNOWN) {
|
||||
types.push(teraType);
|
||||
if (forDefend) {
|
||||
return types;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1230,9 +1330,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
}
|
||||
|
||||
const trappedByAbility = new Utils.BooleanHolder(false);
|
||||
const opposingField = this.isPlayer() ? this.scene.getEnemyField() : this.scene.getPlayerField();
|
||||
|
||||
this.scene.getEnemyField()!.forEach(enemyPokemon =>
|
||||
applyCheckTrappedAbAttrs(CheckTrappedAbAttr, enemyPokemon, trappedByAbility, this, trappedAbMessages, simulated)
|
||||
opposingField.forEach(opponent =>
|
||||
applyCheckTrappedAbAttrs(CheckTrappedAbAttr, opponent, trappedByAbility, this, trappedAbMessages, simulated)
|
||||
);
|
||||
|
||||
return (trappedByAbility.value || !!this.getTag(TrappedTag));
|
||||
@ -1258,7 +1359,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
|
||||
/**
|
||||
* Calculates the effectiveness of a move against the Pokémon.
|
||||
*
|
||||
* This includes modifiers from move and ability attributes.
|
||||
* @param source {@linkcode Pokemon} The attacking Pokémon.
|
||||
* @param move {@linkcode Move} The move being used by the attacking Pokémon.
|
||||
* @param ignoreAbility Whether to ignore abilities that might affect type effectiveness or immunity (defaults to `false`).
|
||||
@ -1278,10 +1379,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
: 1);
|
||||
|
||||
applyMoveAttrs(VariableMoveTypeMultiplierAttr, source, this, move, typeMultiplier);
|
||||
if (this.getTypes().find(t => move.isTypeImmune(source, this, t))) {
|
||||
if (this.getTypes(true, true).find(t => move.isTypeImmune(source, this, t))) {
|
||||
typeMultiplier.value = 0;
|
||||
}
|
||||
|
||||
if (this.getTag(TarShotTag) && (this.getMoveType(move) === Type.FIRE)) {
|
||||
typeMultiplier.value *= 2;
|
||||
}
|
||||
|
||||
const cancelledHolder = cancelled ?? new Utils.BooleanHolder(false);
|
||||
if (!ignoreAbility) {
|
||||
applyPreDefendAbAttrs(TypeImmunityAbAttr, this, source, move, cancelledHolder, simulated, typeMultiplier);
|
||||
@ -1313,7 +1418,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the type effectiveness multiplier for an attack type
|
||||
* Calculates the move's type effectiveness multiplier based on the target's type/s.
|
||||
* @param moveType {@linkcode Type} the type of the move being used
|
||||
* @param source {@linkcode Pokemon} the Pokemon using the move
|
||||
* @param ignoreStrongWinds whether or not this ignores strong winds (anticipation, forewarn, stealth rocks)
|
||||
@ -1337,22 +1442,26 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
}
|
||||
|
||||
let multiplier = types.map(defType => {
|
||||
const multiplier = new Utils.NumberHolder(getTypeDamageMultiplier(moveType, defType));
|
||||
applyChallenges(this.scene.gameMode, ChallengeType.TYPE_EFFECTIVENESS, multiplier);
|
||||
if (source) {
|
||||
const ignoreImmunity = new Utils.BooleanHolder(false);
|
||||
if (source.isActive(true) && source.hasAbilityWithAttr(IgnoreTypeImmunityAbAttr)) {
|
||||
applyAbAttrs(IgnoreTypeImmunityAbAttr, source, ignoreImmunity, simulated, moveType, defType);
|
||||
}
|
||||
if (ignoreImmunity.value) {
|
||||
return 1;
|
||||
if (multiplier.value === 0) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
const exposedTags = this.findTags(tag => tag instanceof ExposedTag) as ExposedTag[];
|
||||
if (exposedTags.some(t => t.ignoreImmunity(defType, moveType))) {
|
||||
return 1;
|
||||
if (multiplier.value === 0) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
const multiplier = new Utils.NumberHolder(getTypeDamageMultiplier(moveType, defType));
|
||||
applyChallenges(this.scene.gameMode, ChallengeType.TYPE_EFFECTIVENESS, multiplier);
|
||||
return multiplier.value;
|
||||
}).reduce((acc, cur) => acc * cur, 1) as TypeDamageMultiplier;
|
||||
|
||||
@ -1378,7 +1487,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
const types = this.getTypes(true);
|
||||
const enemyTypes = opponent.getTypes(true, true);
|
||||
/** Is this Pokemon faster than the opponent? */
|
||||
const outspeed = (this.isActive(true) ? this.getBattleStat(Stat.SPD, opponent) : this.getStat(Stat.SPD)) >= opponent.getBattleStat(Stat.SPD, this);
|
||||
const outspeed = (this.isActive(true) ? this.getEffectiveStat(Stat.SPD, opponent) : this.getStat(Stat.SPD, false)) >= opponent.getEffectiveStat(Stat.SPD, this);
|
||||
/**
|
||||
* Based on how effective this Pokemon's types are offensively against the opponent's types.
|
||||
* This score is increased by 25 percent if this Pokemon is faster than the opponent.
|
||||
@ -1630,7 +1739,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
};
|
||||
|
||||
this.fusionSpecies = this.scene.randomSpecies(this.scene.currentBattle?.waveIndex || 0, this.level, false, filter, true);
|
||||
this.fusionAbilityIndex = (this.fusionSpecies.abilityHidden && hasHiddenAbility ? this.fusionSpecies.ability2 ? 2 : 1 : this.fusionSpecies.ability2 ? randAbilityIndex : 0);
|
||||
this.fusionAbilityIndex = (this.fusionSpecies.abilityHidden && hasHiddenAbility ? 2 : this.fusionSpecies.ability2 !== this.fusionSpecies.ability1 ? randAbilityIndex : 0);
|
||||
this.fusionShiny = this.shiny;
|
||||
this.fusionVariant = this.variant;
|
||||
|
||||
@ -1757,7 +1866,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
movePool = movePool.map(m => [m[0], m[1] * (!!allMoves[m[0]].hasAttr(SacrificialAttr) ? 0.5 : 1)]);
|
||||
movePool = movePool.map(m => [m[0], m[1] * (!!allMoves[m[0]].hasAttr(SacrificialAttrOnHit) ? 0.5 : 1)]);
|
||||
// Trainers get a weight bump to stat buffing moves
|
||||
movePool = movePool.map(m => [m[0], m[1] * (allMoves[m[0]].getAttrs(StatChangeAttr).some(a => a.levels > 1 && a.selfTarget) ? 1.25 : 1)]);
|
||||
movePool = movePool.map(m => [m[0], m[1] * (allMoves[m[0]].getAttrs(StatStageChangeAttr).some(a => a.stages > 1 && a.selfTarget) ? 1.25 : 1)]);
|
||||
// Trainers get a weight decrease to multiturn moves
|
||||
movePool = movePool.map(m => [m[0], m[1] * (!!allMoves[m[0]].hasAttr(ChargeAttr) || !!allMoves[m[0]].hasAttr(RechargeAttr) ? 0.7 : 1)]);
|
||||
}
|
||||
@ -1769,8 +1878,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
movePool = movePool.map(m => [m[0], m[1] * (allMoves[m[0]].category === MoveCategory.STATUS ? 1 : Math.max(Math.min(allMoves[m[0]].power/maxPower, 1), 0.5))]);
|
||||
|
||||
// Weight damaging moves against the lower stat
|
||||
const worseCategory: MoveCategory = this.stats[Stat.ATK] > this.stats[Stat.SPATK] ? MoveCategory.SPECIAL : MoveCategory.PHYSICAL;
|
||||
const statRatio = worseCategory === MoveCategory.PHYSICAL ? this.stats[Stat.ATK]/this.stats[Stat.SPATK] : this.stats[Stat.SPATK]/this.stats[Stat.ATK];
|
||||
const atk = this.getStat(Stat.ATK);
|
||||
const spAtk = this.getStat(Stat.SPATK);
|
||||
const worseCategory: MoveCategory = atk > spAtk ? MoveCategory.SPECIAL : MoveCategory.PHYSICAL;
|
||||
const statRatio = worseCategory === MoveCategory.PHYSICAL ? atk / spAtk : spAtk / atk;
|
||||
movePool = movePool.map(m => [m[0], m[1] * (allMoves[m[0]].category === worseCategory ? statRatio : 1)]);
|
||||
|
||||
let weightMultiplier = 0.9; // The higher this is the more the game weights towards higher level moves. At 0 all moves are equal weight.
|
||||
@ -1956,6 +2067,48 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
return this instanceof PlayerPokemon ? this.scene.getPlayerField() : this.scene.getEnemyField();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the stat stage multiplier of the user against an opponent.
|
||||
*
|
||||
* Note that this does not apply to evasion or accuracy
|
||||
* @see {@linkcode getAccuracyMultiplier}
|
||||
* @param stat the desired {@linkcode EffectiveStat}
|
||||
* @param opponent the target {@linkcode Pokemon}
|
||||
* @param move the {@linkcode Move} being used
|
||||
* @param isCritical determines whether a critical hit has occurred or not (`false` 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 {
|
||||
const statStage = new Utils.IntegerHolder(this.getStatStage(stat));
|
||||
const ignoreStatStage = new Utils.BooleanHolder(false);
|
||||
|
||||
if (opponent) {
|
||||
if (isCritical) {
|
||||
switch (stat) {
|
||||
case Stat.ATK:
|
||||
case Stat.SPATK:
|
||||
statStage.value = Math.max(statStage.value, 0);
|
||||
break;
|
||||
case Stat.DEF:
|
||||
case Stat.SPDEF:
|
||||
statStage.value = Math.min(statStage.value, 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
applyAbAttrs(IgnoreOpponentStatStagesAbAttr, opponent, null, false, stat, ignoreStatStage);
|
||||
if (move) {
|
||||
applyMoveAttrs(IgnoreOpponentStatStagesAttr, this, opponent, move, ignoreStatStage);
|
||||
}
|
||||
}
|
||||
|
||||
if (!ignoreStatStage.value) {
|
||||
const statStageMultiplier = new Utils.NumberHolder(Math.max(2, 2 + statStage.value) / Math.max(2, 2 - statStage.value));
|
||||
this.scene.applyModifiers(TempStatStageBoosterModifier, this.isPlayer(), stat, statStageMultiplier);
|
||||
return Math.min(statStageMultiplier.value, 4);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the accuracy multiplier of the user against a target.
|
||||
*
|
||||
@ -1972,34 +2125,38 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const userAccuracyLevel = new Utils.IntegerHolder(this.summonData.battleStats[BattleStat.ACC]);
|
||||
const targetEvasionLevel = new Utils.IntegerHolder(target.summonData.battleStats[BattleStat.EVA]);
|
||||
const userAccStage = new Utils.IntegerHolder(this.getStatStage(Stat.ACC));
|
||||
const targetEvaStage = new Utils.IntegerHolder(target.getStatStage(Stat.EVA));
|
||||
|
||||
applyAbAttrs(IgnoreOpponentStatChangesAbAttr, target, null, false, userAccuracyLevel);
|
||||
applyAbAttrs(IgnoreOpponentStatChangesAbAttr, this, null, false, targetEvasionLevel);
|
||||
applyAbAttrs(IgnoreOpponentEvasionAbAttr, this, null, false, targetEvasionLevel);
|
||||
applyMoveAttrs(IgnoreOpponentStatChangesAttr, this, target, sourceMove, targetEvasionLevel);
|
||||
this.scene.applyModifiers(TempBattleStatBoosterModifier, this.isPlayer(), TempBattleStat.ACC, userAccuracyLevel);
|
||||
const ignoreAccStatStage = new Utils.BooleanHolder(false);
|
||||
const ignoreEvaStatStage = new Utils.BooleanHolder(false);
|
||||
|
||||
applyAbAttrs(IgnoreOpponentStatStagesAbAttr, target, null, false, Stat.ACC, ignoreAccStatStage);
|
||||
applyAbAttrs(IgnoreOpponentStatStagesAbAttr, this, null, false, Stat.EVA, ignoreEvaStatStage);
|
||||
applyMoveAttrs(IgnoreOpponentStatStagesAttr, this, target, sourceMove, ignoreEvaStatStage);
|
||||
|
||||
this.scene.applyModifiers(TempStatStageBoosterModifier, this.isPlayer(), Stat.ACC, userAccStage);
|
||||
|
||||
userAccStage.value = ignoreAccStatStage.value ? 0 : Math.min(userAccStage.value, 6);
|
||||
targetEvaStage.value = ignoreEvaStatStage.value ? 0 : targetEvaStage.value;
|
||||
|
||||
if (target.findTag(t => t instanceof ExposedTag)) {
|
||||
targetEvasionLevel.value = Math.min(0, targetEvasionLevel.value);
|
||||
targetEvaStage.value = Math.min(0, targetEvaStage.value);
|
||||
}
|
||||
|
||||
const accuracyMultiplier = new Utils.NumberHolder(1);
|
||||
if (userAccuracyLevel.value !== targetEvasionLevel.value) {
|
||||
accuracyMultiplier.value = userAccuracyLevel.value > targetEvasionLevel.value
|
||||
? (3 + Math.min(userAccuracyLevel.value - targetEvasionLevel.value, 6)) / 3
|
||||
: 3 / (3 + Math.min(targetEvasionLevel.value - userAccuracyLevel.value, 6));
|
||||
if (userAccStage.value !== targetEvaStage.value) {
|
||||
accuracyMultiplier.value = userAccStage.value > targetEvaStage.value
|
||||
? (3 + Math.min(userAccStage.value - targetEvaStage.value, 6)) / 3
|
||||
: 3 / (3 + Math.min(targetEvaStage.value - userAccStage.value, 6));
|
||||
}
|
||||
|
||||
applyBattleStatMultiplierAbAttrs(BattleStatMultiplierAbAttr, this, BattleStat.ACC, accuracyMultiplier, false, sourceMove);
|
||||
applyStatMultiplierAbAttrs(StatMultiplierAbAttr, this, Stat.ACC, accuracyMultiplier, false, sourceMove);
|
||||
|
||||
const evasionMultiplier = new Utils.NumberHolder(1);
|
||||
applyBattleStatMultiplierAbAttrs(BattleStatMultiplierAbAttr, target, BattleStat.EVA, evasionMultiplier);
|
||||
applyStatMultiplierAbAttrs(StatMultiplierAbAttr, target, Stat.EVA, evasionMultiplier);
|
||||
|
||||
accuracyMultiplier.value /= evasionMultiplier.value;
|
||||
|
||||
return accuracyMultiplier.value;
|
||||
return accuracyMultiplier.value / evasionMultiplier.value;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -2086,29 +2243,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
if (critOnly.value || critAlways) {
|
||||
isCritical = true;
|
||||
} else {
|
||||
const critLevel = new Utils.IntegerHolder(0);
|
||||
applyMoveAttrs(HighCritAttr, source, this, move, critLevel);
|
||||
this.scene.applyModifiers(CritBoosterModifier, source.isPlayer(), source, critLevel);
|
||||
this.scene.applyModifiers(TempBattleStatBoosterModifier, source.isPlayer(), TempBattleStat.CRIT, critLevel);
|
||||
const bonusCrit = new Utils.BooleanHolder(false);
|
||||
//@ts-ignore
|
||||
if (applyAbAttrs(BonusCritAbAttr, source, null, false, bonusCrit)) { // TODO: resolve ts-ignore. This is a promise. Checking a promise is bogus.
|
||||
if (bonusCrit.value) {
|
||||
critLevel.value += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const critBoostTag = source.getTag(CritBoostTag);
|
||||
if (critBoostTag) {
|
||||
if (critBoostTag instanceof DragonCheerTag) {
|
||||
critLevel.value += critBoostTag.typesOnAdd.includes(Type.DRAGON) ? 2 : 1;
|
||||
} else {
|
||||
critLevel.value += 2;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`crit stage: +${critLevel.value}`);
|
||||
const critChance = [24, 8, 2, 1][Math.max(0, Math.min(critLevel.value, 3))];
|
||||
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;
|
||||
@ -2122,8 +2257,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
isCritical = false;
|
||||
}
|
||||
}
|
||||
const sourceAtk = new Utils.IntegerHolder(source.getBattleStat(isPhysical ? Stat.ATK : Stat.SPATK, this, undefined, isCritical));
|
||||
const targetDef = new Utils.IntegerHolder(this.getBattleStat(isPhysical ? Stat.DEF : Stat.SPDEF, source, move, isCritical));
|
||||
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);
|
||||
@ -2162,7 +2297,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
|
||||
if (!isTypeImmune) {
|
||||
const levelMultiplier = (2 * source.level / 5 + 2);
|
||||
const randomMultiplier = ((this.scene.randBattleSeedInt(16) + 85) / 100);
|
||||
const randomMultiplier = (this.randSeedIntRange(85, 100) / 100);
|
||||
damage.value = Utils.toDmgValue((((levelMultiplier * power * sourceAtk.value / targetDef.value) / 50) + 2)
|
||||
* stabMultiplier.value
|
||||
* typeMultiplier
|
||||
@ -2534,27 +2669,53 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
* @param source {@linkcode Pokemon} the pokemon whose stats/Tags are to be passed on from, ie: the Pokemon using Baton Pass
|
||||
*/
|
||||
transferSummon(source: Pokemon): void {
|
||||
const battleStats = Utils.getEnumValues(BattleStat);
|
||||
for (const stat of battleStats) {
|
||||
this.summonData.battleStats[stat] = source.summonData.battleStats[stat];
|
||||
// Copy all stat stages
|
||||
for (const s of BATTLE_STATS) {
|
||||
const sourceStage = source.getStatStage(s);
|
||||
if ((this instanceof PlayerPokemon) && (sourceStage === 6)) {
|
||||
this.scene.validateAchv(achvs.TRANSFER_MAX_STAT_STAGE);
|
||||
}
|
||||
this.setStatStage(s, sourceStage);
|
||||
}
|
||||
|
||||
for (const tag of source.summonData.tags) {
|
||||
|
||||
// bypass those can not be passed via Baton Pass
|
||||
const excludeTagTypes = new Set([BattlerTagType.DROWSY, BattlerTagType.INFATUATED, BattlerTagType.FIRE_BOOST]);
|
||||
|
||||
if (excludeTagTypes.has(tag.tagType)) {
|
||||
if (!tag.isBatonPassable) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.summonData.tags.push(tag);
|
||||
}
|
||||
if (this instanceof PlayerPokemon && source.summonData.battleStats.find(bs => bs === 6)) {
|
||||
this.scene.validateAchv(achvs.TRANSFER_MAX_BATTLE_STAT);
|
||||
}
|
||||
|
||||
this.updateInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether the given move is currently disabled for this Pokemon.
|
||||
*
|
||||
* @param {Moves} moveId {@linkcode Moves} ID of the move to check
|
||||
* @returns {boolean} `true` if the move is disabled for this Pokemon, otherwise `false`
|
||||
*
|
||||
* @see {@linkcode MoveRestrictionBattlerTag}
|
||||
*/
|
||||
isMoveRestricted(moveId: Moves): boolean {
|
||||
return this.getRestrictingTag(moveId) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link MoveRestrictionBattlerTag} that is restricting a move, if it exists.
|
||||
*
|
||||
* @param {Moves} moveId {@linkcode Moves} ID of the move to check
|
||||
* @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 {
|
||||
for (const tag of this.findTags(t => t instanceof MoveRestrictionBattlerTag)) {
|
||||
if ((tag as MoveRestrictionBattlerTag).isMoveRestricted(moveId)) {
|
||||
return tag as MoveRestrictionBattlerTag;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getMoveHistory(): TurnMove[] {
|
||||
return this.battleSummonData.moveHistory;
|
||||
}
|
||||
@ -3306,12 +3467,30 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
fusionCanvas.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random number using the current battle's seed, or the global seed if `this.scene.currentBattle` is falsy
|
||||
* <!-- @import "../battle".Battle -->
|
||||
* This calls either {@linkcode BattleScene.randBattleSeedInt}({@linkcode range}, {@linkcode min}) in `src/battle-scene.ts`
|
||||
* which calls {@linkcode Battle.randSeedInt}(`scene`, {@linkcode range}, {@linkcode min}) in `src/battle.ts`
|
||||
* which calls {@linkcode Utils.randSeedInt randSeedInt}({@linkcode range}, {@linkcode min}) in `src/utils.ts`,
|
||||
* or it directly calls {@linkcode Utils.randSeedInt randSeedInt}({@linkcode range}, {@linkcode min}) in `src/utils.ts` if there is no current battle
|
||||
*
|
||||
* @param range How large of a range of random numbers to choose from. If {@linkcode range} <= 1, returns {@linkcode min}
|
||||
* @param min The minimum integer to pick, default `0`
|
||||
* @returns A random integer between {@linkcode min} and ({@linkcode min} + {@linkcode range} - 1)
|
||||
*/
|
||||
randSeedInt(range: integer, min: integer = 0): integer {
|
||||
return this.scene.currentBattle
|
||||
? this.scene.randBattleSeedInt(range, min)
|
||||
: Utils.randSeedInt(range, min);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random number using the current battle's seed, or the global seed if `this.scene.currentBattle` is falsy
|
||||
* @param min The minimum integer to generate
|
||||
* @param max The maximum integer to generate
|
||||
* @returns a random integer between {@linkcode min} and {@linkcode max} inclusive
|
||||
*/
|
||||
randSeedIntRange(min: integer, max: integer): integer {
|
||||
return this.randSeedInt((max - min) + 1, min);
|
||||
}
|
||||
@ -3729,16 +3908,17 @@ export class PlayerPokemon extends Pokemon {
|
||||
this.scene.gameData.gameStats.pokemonFused++;
|
||||
|
||||
// Store the average HP% that each Pokemon has
|
||||
const newHpPercent = ((pokemon.hp / pokemon.stats[Stat.HP]) + (this.hp / this.stats[Stat.HP])) / 2;
|
||||
const maxHp = this.getMaxHp();
|
||||
const newHpPercent = ((pokemon.hp / pokemon.getMaxHp()) + (this.hp / maxHp)) / 2;
|
||||
|
||||
this.generateName();
|
||||
this.calculateStats();
|
||||
|
||||
// Set this Pokemon's HP to the average % of both fusion components
|
||||
this.hp = Math.round(this.stats[Stat.HP] * newHpPercent);
|
||||
this.hp = Math.round(maxHp * newHpPercent);
|
||||
if (!this.isFainted()) {
|
||||
// If this Pokemon hasn't fainted, make sure the HP wasn't set over the new maximum
|
||||
this.hp = Math.min(this.hp, this.stats[Stat.HP]);
|
||||
this.hp = Math.min(this.hp, maxHp);
|
||||
this.status = getRandomStatus(this.status, pokemon.status); // Get a random valid status between the two
|
||||
} else if (!pokemon.isFainted()) {
|
||||
// If this Pokemon fainted but the other hasn't, make sure the HP wasn't set to zero
|
||||
@ -4231,39 +4411,40 @@ export class EnemyPokemon extends Pokemon {
|
||||
|
||||
handleBossSegmentCleared(segmentIndex: integer): void {
|
||||
while (segmentIndex - 1 < this.bossSegmentIndex) {
|
||||
let boostedStat = BattleStat.RAND;
|
||||
// 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));
|
||||
|
||||
const battleStats = Utils.getEnumValues(BattleStat).slice(0, -3);
|
||||
const statWeights = new Array().fill(battleStats.length).filter((bs: BattleStat) => this.summonData.battleStats[bs] < 6).map((bs: BattleStat) => this.getStat(bs + 1));
|
||||
const statThresholds: integer[] = [];
|
||||
let boostedStat: EffectiveStat;
|
||||
const statThresholds: number[] = [];
|
||||
let totalWeight = 0;
|
||||
for (const bs of battleStats) {
|
||||
totalWeight += statWeights[bs];
|
||||
|
||||
for (const i in statWeights) {
|
||||
totalWeight += statWeights[i];
|
||||
statThresholds.push(totalWeight);
|
||||
}
|
||||
|
||||
// Pick a random stat from the leftover stats to increase its stages
|
||||
const randInt = Utils.randSeedInt(totalWeight);
|
||||
|
||||
for (const bs of battleStats) {
|
||||
if (randInt < statThresholds[bs]) {
|
||||
boostedStat = bs;
|
||||
for (const i in statThresholds) {
|
||||
if (randInt < statThresholds[i]) {
|
||||
boostedStat = leftoverStats[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let statLevels = 1;
|
||||
let stages = 1;
|
||||
|
||||
// increase the boost if the boss has at least 3 segments and we passed last shield
|
||||
if (this.bossSegments >= 3 && this.bossSegmentIndex === 1) {
|
||||
statLevels++;
|
||||
stages++;
|
||||
}
|
||||
// increase the boost if the boss has at least 5 segments and we passed the second to last shield
|
||||
if (this.bossSegments >= 5 && this.bossSegmentIndex === 2) {
|
||||
statLevels++;
|
||||
stages++;
|
||||
}
|
||||
|
||||
this.scene.unshiftPhase(new StatChangePhase(this.scene, this.getBattlerIndex(), true, [ boostedStat ], statLevels, true, true));
|
||||
|
||||
this.scene.unshiftPhase(new StatStageChangePhase(this.scene, this.getBattlerIndex(), true, [ boostedStat! ], stages, true, true));
|
||||
this.bossSegmentIndex--;
|
||||
}
|
||||
}
|
||||
@ -4296,17 +4477,29 @@ export class EnemyPokemon extends Pokemon {
|
||||
return BattlerIndex.ENEMY + this.getFieldIndex();
|
||||
}
|
||||
|
||||
addToParty(pokeballType: PokeballType) {
|
||||
/**
|
||||
* Add a new pokemon to the player's party (at `slotIndex` if set).
|
||||
* @param pokeballType the type of pokeball the pokemon was caught with
|
||||
* @param slotIndex an optional index to place the pokemon in the party
|
||||
* @returns the pokemon that was added or null if the pokemon could not be added
|
||||
*/
|
||||
addToParty(pokeballType: PokeballType, slotIndex: number = -1) {
|
||||
const party = this.scene.getParty();
|
||||
let ret: PlayerPokemon | null = null;
|
||||
|
||||
if (party.length < 6) {
|
||||
if (party.length < PLAYER_PARTY_MAX_SIZE) {
|
||||
this.pokeball = pokeballType;
|
||||
this.metLevel = this.level;
|
||||
this.metBiome = this.scene.arena.biomeType;
|
||||
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);
|
||||
party.push(newPokemon);
|
||||
|
||||
if (Utils.isBetween(slotIndex, 0, PLAYER_PARTY_MAX_SIZE - 1)) {
|
||||
party.splice(slotIndex, 0, newPokemon);
|
||||
} else {
|
||||
party.push(newPokemon);
|
||||
}
|
||||
|
||||
ret = newPokemon;
|
||||
this.scene.triggerPokemonFormChange(newPokemon, SpeciesFormChangeActiveTrigger, true);
|
||||
}
|
||||
@ -4339,10 +4532,9 @@ export interface AttackMoveResult {
|
||||
}
|
||||
|
||||
export class PokemonSummonData {
|
||||
public battleStats: number[] = [ 0, 0, 0, 0, 0, 0, 0 ];
|
||||
/** [Atk, Def, SpAtk, SpDef, Spd, Acc, Eva] */
|
||||
public statStages: number[] = [ 0, 0, 0, 0, 0, 0, 0 ];
|
||||
public moveQueue: QueuedMove[] = [];
|
||||
public disabledMove: Moves = Moves.NONE;
|
||||
public disabledTurns: number = 0;
|
||||
public tags: BattlerTag[] = [];
|
||||
public abilitySuppressed: boolean = false;
|
||||
public abilitiesApplied: Abilities[] = [];
|
||||
@ -4352,7 +4544,7 @@ export class PokemonSummonData {
|
||||
public ability: Abilities = Abilities.NONE;
|
||||
public gender: Gender;
|
||||
public fusionGender: Gender;
|
||||
public stats: number[];
|
||||
public stats: number[] = [ 0, 0, 0, 0, 0, 0 ];
|
||||
public moveset: (PokemonMove | null)[];
|
||||
// If not initialized this value will not be populated from save data.
|
||||
public types: Type[] = [];
|
||||
@ -4383,8 +4575,8 @@ export class PokemonTurnData {
|
||||
public damageTaken: number = 0;
|
||||
public attacksReceived: AttackMoveResult[] = [];
|
||||
public order: number;
|
||||
public battleStatsIncreased: boolean = false;
|
||||
public battleStatsDecreased: boolean = false;
|
||||
public statStagesIncreased: boolean = false;
|
||||
public statStagesDecreased: boolean = false;
|
||||
}
|
||||
|
||||
export enum AiType {
|
||||
@ -4423,7 +4615,7 @@ export type DamageResult = HitResult.EFFECTIVE | HitResult.SUPER_EFFECTIVE | Hit
|
||||
* It links to {@linkcode Move} class via the move ID.
|
||||
* Compared to {@linkcode Move}, this class also tracks if a move has received.
|
||||
* PP Ups, amount of PP used, and things like that.
|
||||
* @see {@linkcode isUsable} - checks if move is disabled, out of PP, or not implemented.
|
||||
* @see {@linkcode isUsable} - checks if move is restricted, out of PP, or not implemented.
|
||||
* @see {@linkcode getMove} - returns {@linkcode Move} object by looking it up via ID.
|
||||
* @see {@linkcode usePp} - removes a point of PP from the move.
|
||||
* @see {@linkcode getMovePp} - returns amount of PP a move currently has.
|
||||
@ -4443,11 +4635,25 @@ export class PokemonMove {
|
||||
this.virtual = !!virtual;
|
||||
}
|
||||
|
||||
isUsable(pokemon: Pokemon, ignorePp?: boolean): boolean {
|
||||
if (this.moveId && pokemon.summonData?.disabledMove === this.moveId) {
|
||||
/**
|
||||
* Checks whether the move can be selected or performed by a Pokemon, without consideration for the move's targets.
|
||||
* The move is unusable if it is out of PP, restricted by an effect, or unimplemented.
|
||||
*
|
||||
* @param {Pokemon} pokemon {@linkcode Pokemon} that would be using this move
|
||||
* @param {boolean} ignorePp If `true`, skips the PP check
|
||||
* @param {boolean} ignoreRestrictionTags If `true`, skips the check for move restriction tags (see {@link MoveRestrictionBattlerTag})
|
||||
* @returns `true` if the move can be selected and used by the Pokemon, otherwise `false`.
|
||||
*/
|
||||
isUsable(pokemon: Pokemon, ignorePp?: boolean, ignoreRestrictionTags?: boolean): boolean {
|
||||
if (this.moveId && !ignoreRestrictionTags && pokemon.isMoveRestricted(this.moveId)) {
|
||||
return false;
|
||||
}
|
||||
return (ignorePp || this.ppUsed < this.getMovePp() || this.getMove().pp === -1) && !this.getMove().name.endsWith(" (N)");
|
||||
|
||||
if (this.getMove().name.endsWith(" (N)")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (ignorePp || this.ppUsed < this.getMovePp() || this.getMove().pp === -1);
|
||||
}
|
||||
|
||||
getMove(): Move {
|
||||
|
@ -16,7 +16,7 @@ import {
|
||||
getIconForLatestInput, swap,
|
||||
} from "#app/configs/inputs/configHandler";
|
||||
import BattleScene from "./battle-scene";
|
||||
import {SettingGamepad} from "#app/system/settings/settings-gamepad.js";
|
||||
import {SettingGamepad} from "#app/system/settings/settings-gamepad";
|
||||
import {SettingKeyboard} from "#app/system/settings/settings-keyboard";
|
||||
import TouchControl from "#app/touch-controls";
|
||||
import { Button } from "#enums/buttons";
|
||||
|
@ -37,8 +37,7 @@ export interface ModifierTypeTranslationEntries {
|
||||
ModifierType: { [key: string]: ModifierTypeTranslationEntry },
|
||||
SpeciesBoosterItem: { [key: string]: ModifierTypeTranslationEntry },
|
||||
AttackTypeBoosterItem: SimpleTranslationEntries,
|
||||
TempBattleStatBoosterItem: SimpleTranslationEntries,
|
||||
TempBattleStatBoosterStatName: SimpleTranslationEntries,
|
||||
TempStatStageBoosterItem: SimpleTranslationEntries,
|
||||
BaseStatBoosterItem: SimpleTranslationEntries,
|
||||
EvolutionItem: SimpleTranslationEntries,
|
||||
FormChangeItem: SimpleTranslationEntries,
|
||||
|
@ -78,6 +78,7 @@ export class LoadingScene extends SceneBase {
|
||||
this.loadAtlas("overlay_hp_boss", "ui");
|
||||
this.loadImage("overlay_exp", "ui");
|
||||
this.loadImage("icon_owned", "ui");
|
||||
this.loadImage("icon_egg_move", "ui");
|
||||
this.loadImage("ability_bar_left", "ui");
|
||||
this.loadImage("bgm_bar", "ui");
|
||||
this.loadImage("party_exp_bar", "ui");
|
||||
@ -164,6 +165,7 @@ export class LoadingScene extends SceneBase {
|
||||
this.loadImage("saving_icon", "ui");
|
||||
this.loadImage("discord", "ui");
|
||||
this.loadImage("google", "ui");
|
||||
this.loadImage("settings_icon", "ui");
|
||||
|
||||
this.loadImage("default_bg", "arenas");
|
||||
// Load arena images
|
||||
@ -246,7 +248,7 @@ export class LoadingScene extends SceneBase {
|
||||
} else {
|
||||
this.loadAtlas("types", "");
|
||||
}
|
||||
const availableLangs = ["en", "de", "it", "fr", "ja", "ko", "es", "pt_BR", "zh_CN"];
|
||||
const availableLangs = ["en", "de", "it", "fr", "ja", "ko", "es", "pt-BR", "zh-CN"];
|
||||
if (lang && availableLangs.includes(lang)) {
|
||||
this.loadImage("september-update-"+lang, "events");
|
||||
} else {
|
||||
@ -272,6 +274,7 @@ export class LoadingScene extends SceneBase {
|
||||
this.loadImage("gacha_knob", "egg");
|
||||
|
||||
this.loadImage("egg_list_bg", "ui");
|
||||
this.loadImage("egg_summary_bg", "ui");
|
||||
|
||||
this.loadImage("end_m", "cg");
|
||||
this.loadImage("end_f", "cg");
|
||||
|
@ -12,6 +12,7 @@
|
||||
"typeImmunityHeal": "{{abilityName}} von {{pokemonNameWithAffix}} füllte einige KP auf!",
|
||||
"nonSuperEffectiveImmunity": "{{pokemonNameWithAffix}} vermeidet Schaden mit {{abilityName}}!",
|
||||
"disguiseAvoidedDamage": "Die Tarnung von {{pokemonNameWithAffix}} ist aufgeflogen!!",
|
||||
"fullHpResistType": "Der Panzer von {{pokemonNameWithAffix}} funkelt und verzerrt die Wechselwirkungen zwischen den Typen!",
|
||||
"moveImmunity": "Es hat keine Wirkung auf {{pokemonNameWithAffix}}...",
|
||||
"reverseDrain": "{{pokemonNameWithAffix}} saugt Kloakensoße auf!",
|
||||
"postDefendTypeChange": "{{abilityName}} von {{pokemonNameWithAffix}} macht es zu einem {{typeName}}-Typ!",
|
||||
@ -51,6 +52,7 @@
|
||||
"postSummonTeravolt": "{{pokemonNameWithAffix}} strahlt eine knisternde Aura aus!",
|
||||
"postSummonDarkAura": "{{pokemonNameWithAffix}} strahlt eine dunkle Aura aus!",
|
||||
"postSummonFairyAura": "{{pokemonNameWithAffix}} strahlt eine Feenaura aus!",
|
||||
"postSummonAuraBreak": "{{pokemonNameWithAffix}} kehrt die Wirkung aller Aura-Fähigkeiten um!",
|
||||
"postSummonNeutralizingGas": "Reaktionsgas von {{pokemonNameWithAffix}} hat sich in der Umgebung ausgebreitet!",
|
||||
"postSummonAsOneGlastrier": "{{pokemonNameWithAffix}} verfügt über zwei Fähigkeiten!",
|
||||
"postSummonAsOneSpectrier": "{{pokemonNameWithAffix}} verfügt über zwei Fähigkeiten!",
|
||||
@ -59,4 +61,4 @@
|
||||
"postSummonTabletsOfRuin": "Unheilstafeln von {{pokemonNameWithAffix}} schwächt {{statName}} aller Pokémon im Umkreis!",
|
||||
"postSummonBeadsOfRuin": "Unheilsjuwelen von {{pokemonNameWithAffix}} schwächt {{statName}} aller Pokémon im Umkreis!",
|
||||
"preventBerryUse": "{{pokemonNameWithAffix}} kriegt vor Anspannung keine Beeren mehr runter!"
|
||||
}
|
||||
}
|
||||
|
@ -89,7 +89,7 @@
|
||||
"name": "Bänder-Meister",
|
||||
"name_female": "Bänder-Meisterin"
|
||||
},
|
||||
"TRANSFER_MAX_BATTLE_STAT": {
|
||||
"TRANSFER_MAX_STAT_STAGE": {
|
||||
"name": "Teamwork",
|
||||
"description": "Nutze Staffette, während der Anwender mindestens eines Statuswertes maximiert hat."
|
||||
},
|
||||
@ -274,4 +274,4 @@
|
||||
"name": "Spieglein, Spieglein an der Wand",
|
||||
"description": "Schließe die 'Umkehrkampf' Herausforderung ab"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -36,5 +36,6 @@
|
||||
"matBlock": "Tatami-Schild",
|
||||
"craftyShield": "Trickschutz",
|
||||
"tailwind": "Rückenwind",
|
||||
"happyHour": "Goldene Zeiten"
|
||||
}
|
||||
"happyHour": "Goldene Zeiten",
|
||||
"safeguard": "Bodyguard"
|
||||
}
|
||||
|
@ -44,6 +44,7 @@
|
||||
"moveNotImplemented": "{{moveName}} ist noch nicht implementiert und kann nicht ausgewählt werden.",
|
||||
"moveNoPP": "Es sind keine AP für diese Attacke mehr übrig!",
|
||||
"moveDisabled": "{{moveName}} ist deaktiviert!",
|
||||
"disableInterruptedMove": "{{moveName}} von {{pokemonNameWithAffix}} ist blockiert!",
|
||||
"noPokeballForce": "Eine unsichtbare Kraft verhindert die Nutzung von Pokébällen.",
|
||||
"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!",
|
||||
@ -94,5 +95,6 @@
|
||||
"retryBattle": "Möchtest du vom Beginn des Kampfes neustarten?",
|
||||
"unlockedSomething": "{{unlockedThing}} wurde freigeschaltet.",
|
||||
"congratulations": "Glückwunsch!",
|
||||
"beatModeFirstTime": "{{speciesName}} hat den {{gameMode}} Modus zum ersten Mal beendet! Du erhältst {{newModifier}}!"
|
||||
}
|
||||
"beatModeFirstTime": "{{speciesName}} hat den {{gameMode}} Modus zum ersten Mal beendet! Du erhältst {{newModifier}}!",
|
||||
"eggSkipPrompt": "Zur Ei-Zusammenfassung springen?"
|
||||
}
|
||||
|
@ -67,5 +67,7 @@
|
||||
"saltCuredLapse": "{{pokemonNameWithAffix}} wurde durch {{moveName}} verletzt!",
|
||||
"cursedOnAdd": "{{pokemonNameWithAffix}} nimmt einen Teil seiner KP und legt einen Fluch auf {{pokemonName}}!",
|
||||
"cursedLapse": "{{pokemonNameWithAffix}} wurde durch den Fluch verletzt!",
|
||||
"stockpilingOnAdd": "{{pokemonNameWithAffix}} hortet {{stockpiledCount}}!"
|
||||
}
|
||||
"stockpilingOnAdd": "{{pokemonNameWithAffix}} hortet {{stockpiledCount}}!",
|
||||
"disabledOnAdd": " {{moveName}} von {{pokemonNameWithAffix}} wurde blockiert!",
|
||||
"disabledLapse": "{{moveName}} von {{pokemonNameWithAffix}} ist nicht länger blockiert!"
|
||||
}
|
||||
|
@ -1,10 +1,11 @@
|
||||
{
|
||||
"noneSelected": "Keine ausgewählt",
|
||||
"title": "Herausforderungsmodifikatoren",
|
||||
"illegalEvolution": "{{pokemon}} hat sich in ein Pokémon verwandelt, dass für diese Herausforderung nicht zulässig ist!",
|
||||
"singleGeneration": {
|
||||
"name": "Mono-Generation",
|
||||
"desc": "Du kannst nur Pokémon aus der {{gen}} Generation verwenden.",
|
||||
"desc_default": "Du kannst nur Pokémon gewählten Generation verwenden.",
|
||||
"desc_default": "Du kannst nur Pokémon aus der gewählten Generation verwenden.",
|
||||
"gen_1": "ersten",
|
||||
"gen_2": "zweiten",
|
||||
"gen_3": "dritten",
|
||||
|
@ -3,5 +3,6 @@
|
||||
"power": "Stärke",
|
||||
"accuracy": "Genauigkeit",
|
||||
"abilityFlyInText": "{{passive}}{{abilityName}} von {{pokemonName}} wirkt!",
|
||||
"passive": "Passive Fähigkeit "
|
||||
"passive": "Passive Fähigkeit ",
|
||||
"teraHover": "Tera-Typ {{type}}"
|
||||
}
|
@ -25,5 +25,6 @@
|
||||
"unlinkGoogle": "Google trennen",
|
||||
"cancel": "Abbrechen",
|
||||
"losingProgressionWarning": "Du wirst jeglichen Fortschritt seit Anfang dieses Kampfes verlieren. Fortfahren?",
|
||||
"noEggs": "Du brütest aktuell keine Eier aus!"
|
||||
"noEggs": "Du brütest aktuell keine Eier aus!",
|
||||
"donate": "Spenden"
|
||||
}
|
@ -51,5 +51,7 @@
|
||||
"renamePokemon": "Pokémon umbennenen",
|
||||
"rename": "Umbenennen",
|
||||
"nickname": "Spitzname",
|
||||
"errorServerDown": "Ups! Es gab einen Fehler beim Versuch\nden Server zu kontaktieren\nLasse dieses Fenster offen\nDu wirst automatisch neu verbunden."
|
||||
}
|
||||
"errorServerDown": "Ups! Es gab einen Fehler beim Versuch\nden Server zu kontaktieren\nLasse dieses Fenster offen\nDu wirst automatisch neu verbunden.",
|
||||
"noSaves": "Du hast keine gespeicherten Dateien!",
|
||||
"tooManySaves": "Du hast zu viele gespeicherte Dateien!"
|
||||
}
|
||||
|
@ -47,10 +47,14 @@
|
||||
"description": "Ändert das Wesen zu {{natureName}}. Schaltet dieses Wesen permanent für diesen Starter frei."
|
||||
},
|
||||
"DoubleBattleChanceBoosterModifierType": {
|
||||
"description": "Verdoppelt die Wahrscheinlichkeit, dass die nächsten {{battleCount}} Begegnungen mit wilden Pokémon ein Doppelkampf sind."
|
||||
"description": "Vervierfacht die Chance, dass ein Kampf ein Doppelkampf wird, für bis zu {{battleCount}} Kämpfe."
|
||||
},
|
||||
"TempBattleStatBoosterModifierType": {
|
||||
"description": "Erhöht die {{tempBattleStatName}} aller Teammitglieder für 5 Kämpfe um eine Stufe."
|
||||
"TempStatStageBoosterModifierType": {
|
||||
"description": "Erhöht {{stat}} aller Teammitglieder um {{amount}} für bis zu 5 Kämpfe.",
|
||||
"extra": {
|
||||
"stage": "eine Stufe",
|
||||
"percentage": "30%"
|
||||
}
|
||||
},
|
||||
"AttackTypeBoosterModifierType": {
|
||||
"description": "Erhöht die Stärke aller {{moveType}}-Attacken eines Pokémon um 20%."
|
||||
@ -61,8 +65,8 @@
|
||||
"AllPokemonLevelIncrementModifierType": {
|
||||
"description": "Erhöht das Level aller Teammitglieder um {{levels}}."
|
||||
},
|
||||
"PokemonBaseStatBoosterModifierType": {
|
||||
"description": "Erhöht den {{statName}} Basiswert des Trägers um 10%. Das Stapellimit erhöht sich, je höher dein IS-Wert ist."
|
||||
"BaseStatBoosterModifierType": {
|
||||
"description": "Erhöht den {{stat}} Basiswert des Trägers um 10%. Das Stapellimit erhöht sich, je höher dein IS-Wert ist."
|
||||
},
|
||||
"AllPokemonFullHpRestoreModifierType": {
|
||||
"description": "Stellt 100% der KP aller Pokémon her."
|
||||
@ -248,6 +252,12 @@
|
||||
"name": "Scope-Linse",
|
||||
"description": "Ein Item zum Tragen. Es erhöht die Volltrefferquote."
|
||||
},
|
||||
"DIRE_HIT": {
|
||||
"name": "X-Volltreffer",
|
||||
"extra": {
|
||||
"raises": "Volltrefferquote"
|
||||
}
|
||||
},
|
||||
"LEEK": {
|
||||
"name": "Lauchstange",
|
||||
"description": "Ein Item, das von Porenta getragen werden kann. Diese lange Lauchstange erhöht die Volltrefferquote stark."
|
||||
@ -411,25 +421,13 @@
|
||||
"description": "Ein Item, das Ditto zum Tragen gegeben werden kann. Fein und doch hart, erhöht dieses sonderbare Pulver die Initiative."
|
||||
}
|
||||
},
|
||||
"TempBattleStatBoosterItem": {
|
||||
"TempStatStageBoosterItem": {
|
||||
"x_attack": "X-Angriff",
|
||||
"x_defense": "X-Verteidigung",
|
||||
"x_sp_atk": "X-Sp.-Ang.",
|
||||
"x_sp_def": "X-Sp.-Vert.",
|
||||
"x_speed": "X-Tempo",
|
||||
"x_accuracy": "X-Treffer",
|
||||
"dire_hit": "X-Volltreffer"
|
||||
},
|
||||
"TempBattleStatBoosterStatName": {
|
||||
"ATK": "Angriff",
|
||||
"DEF": "Verteidigung",
|
||||
"SPATK": "Sp. Ang",
|
||||
"SPDEF": "Sp. Vert",
|
||||
"SPD": "Initiative",
|
||||
"ACC": "Genauigkeit",
|
||||
"CRIT": "Volltrefferquote",
|
||||
"EVA": "Fluchtwert",
|
||||
"DEFAULT": "???"
|
||||
"x_accuracy": "X-Treffer"
|
||||
},
|
||||
"AttackTypeBoosterItem": {
|
||||
"silk_scarf": "Seidenschal",
|
||||
@ -606,4 +604,4 @@
|
||||
"FAIRY_MEMORY": "Feen-Disc",
|
||||
"NORMAL_MEMORY": "Normal-Disc"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
"turnHealApply": "{{typeName}} von {{pokemonNameWithAffix}} füllt einige KP auf!",
|
||||
"hitHealApply": "{{typeName}} von {{pokemonNameWithAffix}} füllt einige KP auf!",
|
||||
"pokemonInstantReviveApply": "{{pokemonNameWithAffix}} wurde durch {{typeName}} wiederbelebt!",
|
||||
"pokemonResetNegativeStatStageApply": "Die negative Statuswertveränderung von {{pokemonNameWithAffix}} wurde durch {{typeName}} aufgehoben!",
|
||||
"resetNegativeStatStageApply": "Die negative Statuswertveränderung von {{pokemonNameWithAffix}} wurde durch {{typeName}} aufgehoben!",
|
||||
"moneyInterestApply": "Du erhählst {{moneyAmount}} ₽ durch das Item {{typeName}}!",
|
||||
"turnHeldItemTransferApply": "{{itemName}} von {{pokemonNameWithAffix}} wurde durch {{typeName}} von {{pokemonName}} absorbiert!",
|
||||
"contactHeldItemTransferApply": "{{itemName}} von {{pokemonNameWithAffix}} wurde durch {{typeName}} von {{pokemonName}} geklaut!",
|
||||
|
@ -3,6 +3,10 @@
|
||||
"cutHpPowerUpMove": "{{pokemonName}} nutzt seine KP um seine Attacke zu verstärken!",
|
||||
"absorbedElectricity": "{{pokemonName}} absorbiert elektrische Energie!",
|
||||
"switchedStatChanges": "{{pokemonName}} tauschte die Statuswerteveränderungen mit dem Ziel!",
|
||||
"switchedTwoStatChanges": "{{pokemonName}} tauscht Veränderungen an {{firstStat}} und {{secondStat}} mit dem Ziel!",
|
||||
"switchedStat": "{{pokemonName}} tauscht seinen {{stat}}-Wert mit dem des Zieles!",
|
||||
"sharedGuard": "{{pokemonName}} addiert seine Schutzkräfte mit jenen des Zieles und teilt sie gerecht auf!",
|
||||
"sharedPower": "{{pokemonName}} addiert seine Kräfte mit jenen des Zieles und teilt sie gerecht auf!",
|
||||
"goingAllOutForAttack": "{{pokemonName}} legt sich ins Zeug!",
|
||||
"regainedHealth": "{{pokemonName}} erholt sich!",
|
||||
"keptGoingAndCrashed": "{{pokemonName}} springt daneben und verletzt sich!",
|
||||
@ -62,5 +66,6 @@
|
||||
"revivalBlessing": "{{pokemonName}} ist wieder fit und kampfbereit!",
|
||||
"swapArenaTags": "{{pokemonName}} hat die Effekte, die auf den beiden Seiten des Kampffeldes wirken, miteinander getauscht!",
|
||||
"exposedMove": "{{pokemonName}} erkennt {{targetPokemonName}}!",
|
||||
"safeguard": "{{targetName}} wird durch Bodyguard geschützt!"
|
||||
}
|
||||
"safeguard": "{{targetName}} wird durch Bodyguard geschützt!",
|
||||
"afterYou": "{{targetName}} lässt sich auf Galanterie ein!"
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
{
|
||||
"Stat": {
|
||||
"HP": "KP",
|
||||
"HPStat": "KP",
|
||||
"HPshortened": "KP",
|
||||
"ATK": "Angriff",
|
||||
"ATKshortened": "Ang",
|
||||
|
@ -100,7 +100,7 @@
|
||||
"moveTouchControls": "Bewegung Touch Steuerung",
|
||||
"shopOverlayOpacity": "Shop Overlay Deckkraft",
|
||||
"shopCursorTarget": "Shop-Cursor Ziel",
|
||||
"items": "Items",
|
||||
"rewards": "Items",
|
||||
"reroll": "Neu rollen",
|
||||
"shop": "Shop",
|
||||
"checkTeam": "Team überprüfen"
|
||||
|
@ -3,7 +3,7 @@
|
||||
"badDreams": "{{pokemonName}} is tormented!",
|
||||
"costar": "{{pokemonName}} copied {{allyName}}'s stat changes!",
|
||||
"iceFaceAvoidedDamage": "{{pokemonNameWithAffix}} avoided\ndamage with {{abilityName}}!",
|
||||
"perishBody": "{{pokemonName}}'s {{abilityName}}\nwill faint both pokemon in 3 turns!",
|
||||
"perishBody": "{{pokemonName}}'s {{abilityName}}\nwill faint both Pokémon in 3 turns!",
|
||||
"poisonHeal": "{{pokemonName}}'s {{abilityName}}\nrestored its HP a little!",
|
||||
"trace": "{{pokemonName}} copied {{targetName}}'s\n{{abilityName}}!",
|
||||
"windPowerCharged": "Being hit by {{moveName}} charged {{pokemonName}} with power!",
|
||||
@ -52,6 +52,7 @@
|
||||
"postSummonTeravolt": "{{pokemonNameWithAffix}} is radiating a bursting aura!",
|
||||
"postSummonDarkAura": "{{pokemonNameWithAffix}} is radiating a Dark Aura!",
|
||||
"postSummonFairyAura": "{{pokemonNameWithAffix}} is radiating a Fairy Aura!",
|
||||
"postSummonAuraBreak": "{{pokemonNameWithAffix}} reversed all other Pokémon's auras!",
|
||||
"postSummonNeutralizingGas": "{{pokemonNameWithAffix}}'s Neutralizing Gas filled the area!",
|
||||
"postSummonAsOneGlastrier": "{{pokemonNameWithAffix}} has two Abilities!",
|
||||
"postSummonAsOneSpectrier": "{{pokemonNameWithAffix}} has two Abilities!",
|
||||
@ -60,4 +61,4 @@
|
||||
"postSummonTabletsOfRuin": "{{pokemonNameWithAffix}}'s Tablets of Ruin lowered the {{statName}}\nof all surrounding Pokémon!",
|
||||
"postSummonBeadsOfRuin": "{{pokemonNameWithAffix}}'s Beads of Ruin lowered the {{statName}}\nof all surrounding Pokémon!",
|
||||
"preventBerryUse": "{{pokemonNameWithAffix}} is too\nnervous to eat berries!"
|
||||
}
|
||||
}
|
||||
|
@ -97,9 +97,9 @@
|
||||
"name": "Master League Champion",
|
||||
"name_female": "Master League Champion"
|
||||
},
|
||||
"TRANSFER_MAX_BATTLE_STAT": {
|
||||
"TRANSFER_MAX_STAT_STAGE": {
|
||||
"name": "Teamwork",
|
||||
"description": "Baton pass to another party member with at least one stat maxed out"
|
||||
"description": "Baton pass to another party member with at least one stat stage maxed out"
|
||||
},
|
||||
"MAX_FRIENDSHIP": {
|
||||
"name": "Friendmaxxing",
|
||||
@ -284,4 +284,4 @@
|
||||
"name": "Mirror rorriM",
|
||||
"description": "Complete the Inverse Battle challenge.\n.egnellahc elttaB esrevnI eht etelpmoC"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -39,5 +39,6 @@
|
||||
"matBlock": "Mat Block",
|
||||
"craftyShield": "Crafty Shield",
|
||||
"tailwind": "Tailwind",
|
||||
"happyHour": "Happy Hour"
|
||||
}
|
||||
"happyHour": "Happy Hour",
|
||||
"safeguard": "Safeguard"
|
||||
}
|
@ -47,5 +47,11 @@
|
||||
"tailwindOnRemovePlayer": "Your team's Tailwind petered out!",
|
||||
"tailwindOnRemoveEnemy": "The opposing team's Tailwind petered out!",
|
||||
"happyHourOnAdd": "Everyone is caught up in the happy atmosphere!",
|
||||
"happyHourOnRemove": "The atmosphere returned to normal."
|
||||
"happyHourOnRemove": "The atmosphere returned to normal.",
|
||||
"safeguardOnAdd": "The whole field is cloaked in a mystical veil!",
|
||||
"safeguardOnAddPlayer": "Your team cloaked itself in a mystical veil!",
|
||||
"safeguardOnAddEnemy": "The opposing team cloaked itself in a mystical veil!",
|
||||
"safeguardOnRemove": "The field is no longer protected by Safeguard!",
|
||||
"safeguardOnRemovePlayer": "Your team is no longer protected by Safeguard!",
|
||||
"safeguardOnRemoveEnemy": "The opposing team is no longer protected by Safeguard!"
|
||||
}
|
@ -44,6 +44,10 @@
|
||||
"moveNotImplemented": "{{moveName}} is not yet implemented and cannot be selected.",
|
||||
"moveNoPP": "There's no PP left for\nthis move!",
|
||||
"moveDisabled": "{{moveName}} is disabled!",
|
||||
"canOnlyUseMove": "{{pokemonName}} can only use {{moveName}}!",
|
||||
"moveCannotBeSelected": "{{moveName}} cannot be selected!",
|
||||
"disableInterruptedMove": "{{pokemonNameWithAffix}}'s {{moveName}}\nis disabled!",
|
||||
"throatChopInterruptedMove": "The effects of Throat Chop prevent\n{{pokemonName}} from using certain moves!",
|
||||
"noPokeballForce": "An unseen force\nprevents using Poké Balls.",
|
||||
"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!",
|
||||
@ -61,6 +65,7 @@
|
||||
"skipItemQuestion": "Are you sure you want to skip taking an item?",
|
||||
"itemStackFull": "The stack for {{fullItemName}} is full.\nYou will receive {{itemName}} instead.",
|
||||
"eggHatching": "Oh?",
|
||||
"eggSkipPrompt": "Skip to egg summary?",
|
||||
"ivScannerUseQuestion": "Use IV Scanner on {{pokemonName}}?",
|
||||
"wildPokemonWithAffix": "Wild {{pokemonName}}",
|
||||
"foePokemonWithAffix": "Foe {{pokemonName}}",
|
||||
|
@ -67,5 +67,8 @@
|
||||
"saltCuredLapse": "{{pokemonNameWithAffix}} is hurt by {{moveName}}!",
|
||||
"cursedOnAdd": "{{pokemonNameWithAffix}} cut its own HP and put a curse on the {{pokemonName}}!",
|
||||
"cursedLapse": "{{pokemonNameWithAffix}} is afflicted by the Curse!",
|
||||
"stockpilingOnAdd": "{{pokemonNameWithAffix}} stockpiled {{stockpiledCount}}!"
|
||||
}
|
||||
"stockpilingOnAdd": "{{pokemonNameWithAffix}} stockpiled {{stockpiledCount}}!",
|
||||
"disabledOnAdd": "{{pokemonNameWithAffix}}'s {{moveName}}\nwas disabled!",
|
||||
"disabledLapse": "{{pokemonNameWithAffix}}'s {{moveName}}\nis no longer disabled.",
|
||||
"tarShotOnAdd": "{{pokemonNameWithAffix}} became weaker to fire!"
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"title": "Challenge Modifiers",
|
||||
"illegalEvolution": "{{pokemon}} changed into an ineligble pokémon\nfor this challenge!",
|
||||
"illegalEvolution": "{{pokemon}} changed into an ineligible Pokémon\nfor this challenge!",
|
||||
"noneSelected": "None Selected",
|
||||
"singleGeneration": {
|
||||
"name": "Mono Gen",
|
||||
@ -34,4 +34,4 @@
|
||||
"value.0": "Off",
|
||||
"value.1": "On"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -413,7 +413,7 @@
|
||||
},
|
||||
"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.",
|
||||
"1": "Hold it right there!\nWe can't have 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!"
|
||||
},
|
||||
|
@ -3,5 +3,6 @@
|
||||
"power": "Power",
|
||||
"accuracy": "Accuracy",
|
||||
"abilityFlyInText": " {{pokemonName}}'s {{passive}}{{abilityName}}",
|
||||
"passive": "Passive "
|
||||
"passive": "Passive ",
|
||||
"teraHover": "{{type}} Terastallized"
|
||||
}
|
@ -51,5 +51,7 @@
|
||||
"renamePokemon": "Rename Pokémon",
|
||||
"rename": "Rename",
|
||||
"nickname": "Nickname",
|
||||
"errorServerDown": "Oops! There was an issue contacting the server.\n\nYou may leave this window open,\nthe game will automatically reconnect."
|
||||
"errorServerDown": "Oops! There was an issue contacting the server.\n\nYou may leave this window open,\nthe game will automatically reconnect.",
|
||||
"noSaves": "You don't have any save files on record!",
|
||||
"tooManySaves": "You have too many save files on record!"
|
||||
}
|
@ -47,10 +47,14 @@
|
||||
"description": "Changes a Pokémon's nature to {{natureName}} and permanently unlocks the nature for the starter."
|
||||
},
|
||||
"DoubleBattleChanceBoosterModifierType": {
|
||||
"description": "Doubles the chance of an encounter being a double battle for {{battleCount}} battles."
|
||||
"description": "Quadruples the chance of an encounter being a double battle for up to {{battleCount}} battles."
|
||||
},
|
||||
"TempBattleStatBoosterModifierType": {
|
||||
"description": "Increases the {{tempBattleStatName}} of all party members by 1 stage for 5 battles."
|
||||
"TempStatStageBoosterModifierType": {
|
||||
"description": "Increases the {{stat}} of all party members by {{amount}} for up to 5 battles.",
|
||||
"extra": {
|
||||
"stage": "1 stage",
|
||||
"percentage": "30%"
|
||||
}
|
||||
},
|
||||
"AttackTypeBoosterModifierType": {
|
||||
"description": "Increases the power of a Pokémon's {{moveType}}-type moves by 20%."
|
||||
@ -61,8 +65,8 @@
|
||||
"AllPokemonLevelIncrementModifierType": {
|
||||
"description": "Increases all party members' level by {{levels}}."
|
||||
},
|
||||
"PokemonBaseStatBoosterModifierType": {
|
||||
"description": "Increases the holder's base {{statName}} by 10%. The higher your IVs, the higher the stack limit."
|
||||
"BaseStatBoosterModifierType": {
|
||||
"description": "Increases the holder's base {{stat}} by 10%. The higher your IVs, the higher the stack limit."
|
||||
},
|
||||
"AllPokemonFullHpRestoreModifierType": {
|
||||
"description": "Restores 100% HP for all Pokémon."
|
||||
@ -183,6 +187,7 @@
|
||||
"SOOTHE_BELL": { "name": "Soothe Bell" },
|
||||
|
||||
"SCOPE_LENS": { "name": "Scope Lens", "description": "It's a lens for scoping out weak points. It boosts the holder's critical-hit ratio."},
|
||||
"DIRE_HIT": { "name": "Dire Hit", "extra": { "raises": "Critical Hit Ratio" } },
|
||||
"LEEK": { "name": "Leek", "description": "This very long and stiff stalk of leek boosts the critical-hit ratio of Farfetch'd's moves."},
|
||||
|
||||
"EVIOLITE": { "name": "Eviolite", "description": "This mysterious evolutionary lump boosts the Defense and Sp. Def stats when held by a Pokémon that can still evolve." },
|
||||
@ -250,28 +255,14 @@
|
||||
"METAL_POWDER": { "name": "Metal Powder", "description": "Extremely fine yet hard, this odd powder boosts Ditto's Defense stat." },
|
||||
"QUICK_POWDER": { "name": "Quick Powder", "description": "Extremely fine yet hard, this odd powder boosts Ditto's Speed stat." }
|
||||
},
|
||||
"TempBattleStatBoosterItem": {
|
||||
"TempStatStageBoosterItem": {
|
||||
"x_attack": "X Attack",
|
||||
"x_defense": "X Defense",
|
||||
"x_sp_atk": "X Sp. Atk",
|
||||
"x_sp_def": "X Sp. Def",
|
||||
"x_speed": "X Speed",
|
||||
"x_accuracy": "X Accuracy",
|
||||
"dire_hit": "Dire Hit"
|
||||
"x_accuracy": "X Accuracy"
|
||||
},
|
||||
|
||||
"TempBattleStatBoosterStatName": {
|
||||
"ATK": "Attack",
|
||||
"DEF": "Defense",
|
||||
"SPATK": "Sp. Atk",
|
||||
"SPDEF": "Sp. Def",
|
||||
"SPD": "Speed",
|
||||
"ACC": "Accuracy",
|
||||
"CRIT": "Critical Hit Ratio",
|
||||
"EVA": "Evasiveness",
|
||||
"DEFAULT": "???"
|
||||
},
|
||||
|
||||
"AttackTypeBoosterItem": {
|
||||
"silk_scarf": "Silk Scarf",
|
||||
"black_belt": "Black Belt",
|
||||
|
@ -3,7 +3,7 @@
|
||||
"turnHealApply": "{{pokemonNameWithAffix}} restored a little HP using\nits {{typeName}}!",
|
||||
"hitHealApply": "{{pokemonNameWithAffix}} restored a little HP using\nits {{typeName}}!",
|
||||
"pokemonInstantReviveApply": "{{pokemonNameWithAffix}} was revived\nby its {{typeName}}!",
|
||||
"pokemonResetNegativeStatStageApply": "{{pokemonNameWithAffix}}'s lowered stats were restored\nby its {{typeName}}!",
|
||||
"resetNegativeStatStageApply": "{{pokemonNameWithAffix}}'s lowered stats were restored\nby its {{typeName}}!",
|
||||
"moneyInterestApply": "You received interest of ₽{{moneyAmount}}\nfrom the {{typeName}}!",
|
||||
"turnHeldItemTransferApply": "{{pokemonNameWithAffix}}'s {{itemName}} was absorbed\nby {{pokemonName}}'s {{typeName}}!",
|
||||
"contactHeldItemTransferApply": "{{pokemonNameWithAffix}}'s {{itemName}} was snatched\nby {{pokemonName}}'s {{typeName}}!",
|
||||
|
@ -3,6 +3,11 @@
|
||||
"cutHpPowerUpMove": "{{pokemonName}} cut its own HP to power up its move!",
|
||||
"absorbedElectricity": "{{pokemonName}} absorbed electricity!",
|
||||
"switchedStatChanges": "{{pokemonName}} switched stat changes with the target!",
|
||||
"switchedTwoStatChanges": "{{pokemonName}} switched all changes to its {{firstStat}}\nand {{secondStat}} with its target!",
|
||||
"switchedStat": "{{pokemonName}} switched {{stat}} with its target!",
|
||||
"sharedGuard": "{{pokemonName}} shared its guard with the target!",
|
||||
"sharedPower": "{{pokemonName}} shared its power with the target!",
|
||||
"shiftedStats": "{{pokemonName}} switched its {{statToSwitch}} and {{statToSwitchWith}}!",
|
||||
"goingAllOutForAttack": "{{pokemonName}} is going all out for this attack!",
|
||||
"regainedHealth": "{{pokemonName}} regained\nhealth!",
|
||||
"keptGoingAndCrashed": "{{pokemonName}} kept going\nand crashed!",
|
||||
@ -61,5 +66,7 @@
|
||||
"suppressAbilities": "{{pokemonName}}'s ability\nwas suppressed!",
|
||||
"revivalBlessing": "{{pokemonName}} was revived!",
|
||||
"swapArenaTags": "{{pokemonName}} swapped the battle effects affecting each side of the field!",
|
||||
"exposedMove": "{{pokemonName}} identified\n{{targetPokemonName}}!"
|
||||
"exposedMove": "{{pokemonName}} identified\n{{targetPokemonName}}!",
|
||||
"safeguard": "{{targetName}} is protected by Safeguard!",
|
||||
"afterYou": "{{pokemonName}} took the kind offer!"
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"Stat": {
|
||||
"HP": "Max. HP",
|
||||
"HPshortened": "MaxHP",
|
||||
"HPshortened": "HP",
|
||||
"ATK": "Attack",
|
||||
"ATKshortened": "Atk",
|
||||
"DEF": "Defense",
|
||||
@ -13,8 +13,7 @@
|
||||
"SPD": "Speed",
|
||||
"SPDshortened": "Spd",
|
||||
"ACC": "Accuracy",
|
||||
"EVA": "Evasiveness",
|
||||
"HPStat": "HP"
|
||||
"EVA": "Evasiveness"
|
||||
},
|
||||
"Type": {
|
||||
"UNKNOWN": "Unknown",
|
||||
@ -38,4 +37,4 @@
|
||||
"FAIRY": "Fairy",
|
||||
"STELLAR": "Stellar"
|
||||
}
|
||||
}
|
||||
}
|
||||
|