Merge branch 'beta' into daily-standardization

This commit is contained in:
RedstonewolfX 2024-09-12 10:44:46 -04:00 committed by GitHub
commit 64861447cb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
687 changed files with 23897 additions and 35259 deletions

View File

@ -1,7 +1,7 @@
name: Bug Report
description: Create a report to help us improve
title: "[Bug] "
labels: ["Bug"]
labels: ["Bug", "Triage"]
body:
- type: markdown
attributes:
@ -19,21 +19,12 @@ body:
value: |
---
- type: textarea
id: session-file
id: repro
attributes:
label: Session export file
description: Open Menu → ManageData → Export Session → Select slot. The file should now be in your `/Downloads` directory. Change the file extension type from `.prsv` to `.txt` (How to [Windows](https://www.guidingtech.com/how-to-change-file-type-on-windows/) | [Mac](https://support.apple.com/guide/mac-help/show-or-hide-filename-extensions-on-mac-mchlp2304/mac) | [iOS](https://www.guidingtech.com/change-file-type-extension-on-iphone/)).
placeholder: Focus me and then drop your file here (or use the upload button at the bottom)
label: Reproduction
description: Describe the steps to reproduce this bug. If applicable attach user/session data at the bottom
validations:
required: false
- type: textarea
id: data-file
attributes:
label: User data export file
description: Open Menu → ManageData → Export Data. The file should now be in your `/Downloads` directory. Change the file extension type from `.prsv` to `.txt` (How to [Windows](https://www.guidingtech.com/how-to-change-file-type-on-windows/) | [Mac](https://support.apple.com/guide/mac-help/show-or-hide-filename-extensions-on-mac-mchlp2304/mac) | [iOS](https://www.guidingtech.com/change-file-type-extension-on-iphone/)).
placeholder: Focus me and then drop your file here (or use the upload button at the bottom)
validations:
required: false
required: true
- type: markdown
attributes:
value: |
@ -60,48 +51,20 @@ body:
attributes:
value: |
---
- type: dropdown
id: os
- type: textarea
id: session-file
attributes:
label: What OS did you observe the bug on?
multiple: true
options:
- PC/Windows
- Mac/OSX
- Linux
- iOS
- Android
- Other
validations:
required: true
- type: input
id: os-other
attributes:
label: If other please specify
label: Session export file
description: Open Menu → ManageData → Export Session → Select slot. The file should now be in your `/Downloads` directory. Change the file extension type from `.prsv` to `.txt` (How to [Windows](https://www.guidingtech.com/how-to-change-file-type-on-windows/) | [Mac](https://support.apple.com/guide/mac-help/show-or-hide-filename-extensions-on-mac-mchlp2304/mac) | [iOS](https://www.guidingtech.com/change-file-type-extension-on-iphone/)).
placeholder: Focus me and then drop your file here (or use the upload button at the bottom)
validations:
required: false
- type: markdown
- type: textarea
id: data-file
attributes:
value: |
---
- type: dropdown
id: browser
attributes:
label: Which browser do you use?
multiple: true
options:
- Chrome
- Firefox
- Safari
- Edge
- Opera
- Other
validations:
required: true
- type: input
id: browser-other
attributes:
label: If other please specify
label: User data export file
description: Open Menu → ManageData → Export Data. The file should now be in your `/Downloads` directory. Change the file extension type from `.prsv` to `.txt` (How to [Windows](https://www.guidingtech.com/how-to-change-file-type-on-windows/) | [Mac](https://support.apple.com/guide/mac-help/show-or-hide-filename-extensions-on-mac-mchlp2304/mac) | [iOS](https://www.guidingtech.com/change-file-type-extension-on-iphone/)).
placeholder: Focus me and then drop your file here (or use the upload button at the bottom)
validations:
required: false
- type: markdown

View File

@ -1,7 +1,7 @@
name: Feature Request
description: Suggest an idea for this project
title: "[Feature] "
labels: ["Enhancement"]
labels: ["Enhancement", "Triage"]
body:
- type: markdown
attributes:

View File

@ -30,7 +30,7 @@
- [ ] The PR is self-contained and cannot be split into smaller PRs?
- [ ] Have I provided a clear explanation of the changes?
- [ ] Have I considered writing automated tests for the issue?
- [ ] If I have text, did I add make it translatable and added a key in the English language?
- [ ] If I have text, did I make it translatable and add a key in the English locale file(s)?
- [ ] Have I tested the changes (manually)?
- [ ] Are all unit tests still passing? (`npm run test`)
- [ ] Are the changes visual?

View File

@ -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"

View File

@ -8,6 +8,8 @@ on:
branches:
- main
- beta
merge_group:
types: [checks_requested]
jobs:
pages:

View 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' || '' }}

View File

@ -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

110
create-test-boilerplate.js Normal file
View File

@ -0,0 +1,110 @@
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
/**
* This script creates a test boilerplate file for a move or ability.
* @param {string} type - The type of test to create. Either "move", "ability",
* or "item".
* @param {string} fileName - The name of the file to create.
* @example npm run create-test move tackle
*/
// Get the directory name of the current module file
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Get the arguments from the command line
const args = process.argv.slice(2);
const type = args[0]; // "move" or "ability"
let fileName = args[1]; // The file name
if (!type || !fileName) {
console.error('Please provide a type ("move", "ability", or "item") and a file name.');
process.exit(1);
}
// 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());
// Determine the directory based on the type
let dir;
let description;
if (type === 'move') {
dir = path.join(__dirname, 'src', 'test', 'moves');
description = `Moves - ${formattedName}`;
} else if (type === 'ability') {
dir = path.join(__dirname, 'src', 'test', 'abilities');
description = `Abilities - ${formattedName}`;
} else if (type === "item") {
dir = path.join(__dirname, 'src', 'test', 'items');
description = `Items - ${formattedName}`;
} else {
console.error('Invalid type. Please use "move", "ability", or "item".');
process.exit(1);
}
// Ensure the directory exists
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
// Create the file with the given name
const filePath = path.join(dir, `${fileName}.test.ts`);
if (fs.existsSync(filePath)) {
console.error(`File "${fileName}.test.ts" already exists.`);
process.exit(1);
}
// 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 Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, it, expect } from "vitest";
describe("${description}", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
const TIMEOUT = 20 * 1000;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.moveset([Moves.SPLASH])
.battleType("single")
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);
});
it("test case", async () => {
// await game.classicMode.startBattle([Species.MAGIKARP]);
// game.move.select(Moves.SPLASH);
}, TIMEOUT);
});
`;
// Write the template content to the file
fs.writeFileSync(filePath, content, 'utf8');
console.log(`File created at: ${filePath}`);

View File

@ -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.

View File

@ -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
}
}
]

View File

@ -23,15 +23,6 @@ body {
}
}
#links {
width: 90%;
text-align: center;
position: fixed;
bottom: 0;
display: flex;
justify-content: space-around;
}
#app {
display: flex;
justify-content: center;
@ -93,7 +84,7 @@ input:-internal-autofill-selected {
@media (orientation: landscape) {
#touchControls {
--controls-size: 20vh;
--controls-size: 20vh;
--text-shadow-size: 1.3vh;
--small-button-offset: 4vh;
}

View File

@ -39,7 +39,6 @@
</style>
<link rel="stylesheet" type="text/css" href="./index.css" />
<link rel="manifest" href="./manifest.webmanifest">
<script type="text/javascript" src="https://app.termly.io/resource-blocker/c5dbfa2f-9723-4c0f-a84b-2895124e851f?autoBlock=on"></script>
<script>
if ("serviceWorker" in navigator) {
window.addEventListener("load", function () {
@ -144,13 +143,6 @@
</div>
</div>
<div id="tnc-links">
<a href="#" class="termly-display-preferences" style="display: none;" target="_blank" rel="noreferrer noopener">Consent Preferences</a>
<a href="https://app.termly.io/policy-viewer/policy.html?policyUUID=bc96778b-3f04-4d25-bafc-0deba53e8bec" target="_blank" rel="noreferrer noopener">Privacy Policy</a>
<a href="https://app.termly.io/policy-viewer/policy.html?policyUUID=8b523c05-7ec2-4646-9534-5bd61b386e2a" target="_blank" rel="noreferrer noopener">Cookie Disclaimer</a>
<a href="https://app.termly.io/policy-viewer/policy.html?policyUUID=b01e092a-9721-477f-8356-45576702ff9e" target="_blank" rel="noreferrer noopener">Terms & Conditions</a>
<a href="https://app.termly.io/policy-viewer/policy.html?policyUUID=3b5d1928-3f5b-4ee1-b8df-2d6c276b0bcc" target="_blank" rel="noreferrer noopener">Acceptable Use Policy</a>
</div>
<script type="module" src="./src/main.ts"></script>
<script src="./src/touch-controls.ts" type="module"></script>
<script src="./src/debug.js" type="module"></script>

View File

@ -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
View File

@ -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",

View File

@ -18,7 +18,8 @@
"eslint-ci": "eslint .",
"docs": "typedoc",
"depcruise": "depcruise src",
"depcruise:graph": "depcruise src --output-type dot | node dependency-graph.js > dependency-graph.svg"
"depcruise:graph": "depcruise src --output-type dot | node dependency-graph.js > dependency-graph.svg",
"create-test": "node ./create-test-boilerplate.js"
},
"devDependencies": {
"@eslint/js": "^9.3.0",
@ -31,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",

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 55 KiB

View File

Before

Width:  |  Height:  |  Size: 405 B

After

Width:  |  Height:  |  Size: 405 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -1,38 +1,49 @@
{
"0": {
"bc4524": "af5457",
"31638c": "324a26",
"101010": "101010",
"5aa5ce": "40683c",
"a5e6ff": "b6d9ac",
"7bceef": "789c6e",
"ced6ef": "c09e99",
"737384": "774644",
"ce4252": "af2c4f",
"ffffff": "f1dcd8",
"8c4231": "420b0c",
"ffde4a": "c66f68",
"c57b31": "551917",
"ffffad": "f4bfb6",
"ffde4a": "c66f68",
"7bceef": "789c6e",
"a5e6ff": "b6d9ac",
"737384": "774644",
"f7b531": "af5457",
"ce4252": "af2c4f",
"bc4524": "af5457",
"00e5e7": "00e5e7"
"c57b31": "551917",
"ced6ef": "c09e99"
},
"1": {
"bc4524": "3d325e",
"31638c": "143a72",
"101010": "101010",
"5aa5ce": "4060bc",
"a5e6ff": "b4b3ff",
"7bceef": "657ddf",
"ced6ef": "a8b5dd",
"737384": "737384",
"ce4252": "b75558",
"ffffff": "e5ecff",
"8c4231": "17103f",
"ffde4a": "534e72",
"c57b31": "2a1f50",
"ffffad": "87879b",
"ffde4a": "534e72",
"7bceef": "657ddf",
"a5e6ff": "b4b3ff",
"f7b531": "3d325e",
"ce4252": "b75558",
"bc4524": "3d325e",
"00e5e7": "00e5e7"
"c57b31": "2a1f50",
"ced6ef": "a8b5dd"
},
"2": {
"ce4252": "215991",
"ffde4a": "f16f40",
"ffffad": "ffb274",
"737384": "884c43",
"c57b31": "761c03",
"7bceef": "be3d2f",
"8c4231": "5a0700",
"5aa5ce": "892722",
"8c4232": "761c03",
"ffffff": "f5e1d1",
"a5e6ff": "dd533a",
"f7b531": "bc4524",
"ced6ef": "d19e92",
"31638c": "610f0e"
}
}
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

View 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"
}
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

View File

@ -1017,7 +1017,7 @@
"279": [
1,
1,
2
1
],
"280": [
0,
@ -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,

View File

@ -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"
}
}
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

View 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"
}
}

View File

@ -3,44 +3,36 @@
"bcb9be": "ae4c95",
"f9f2fc": "ffc0e5",
"7b787c": "793d6d",
"e1dfe2": "e88cc5",
"d0cfd0": "ce6bac",
"b8b4ba": "d7d2f6",
"dcd9dd": "e88cc5",
"c9c0ce": "ce6bac",
"cbc2d1": "d7d2f6",
"938f94": "b465b9",
"6c7275": "d3ffff",
"9362e6": "80a4ff",
"fcfcfc": "fcfcfc",
"4a494e": "a7e6e5",
"c6a4ff": "bed5ff",
"101010": "101010",
"3b3a3f": "4d8894",
"aeadae": "e88cc5",
"686568": "686568",
"6f6d71": "793d6d",
"b5b4b6": "ce6bac",
"706e6d": "7d7c75",
"fbf2ff": "d3ffff",
"e66294": "80a4ff",
"c6bbcb": "a7e6e5",
"ffa4c5": "bed5ff",
"7f806a": "4d8894",
"a8a0ac": "e88cc5",
"7c7a78": "793d6d",
"bbb4bc": "ce6bac",
"af9e9e": "42a2b1"
},
"2": {
"bcb9be": "055946",
"f9f2fc": "21be70",
"7b787c": "004140",
"e1dfe2": "12a169",
"d0cfd0": "0a7a57",
"b8b4ba": "567f83",
"dcd9dd": "12a169",
"c9c0ce": "0a7a57",
"cbc2d1": "567f83",
"938f94": "2b5458",
"6c7275": "874059",
"9362e6": "15c05f",
"fcfcfc": "fcfcfc",
"4a494e": "773050",
"c6a4ff": "8ff3a3",
"101010": "101010",
"3b3a3f": "4b1f28",
"aeadae": "12a169",
"686568": "686568",
"6f6d71": "004140",
"b5b4b6": "0a7a57",
"706e6d": "7d7c75",
"fbf2ff": "874059",
"e66294": "15c05f",
"c6bbcb": "773050",
"ffa4c5": "8ff3a3",
"7f806a": "4b1f28",
"a8a0ac": "12a169",
"7c7a78": "004140",
"bbb4bc": "0a7a57",
"af9e9e": "48c492"
}
}
}

View 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"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 799 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 807 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 799 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 800 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 799 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 B

View File

@ -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;

View File

@ -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

View File

@ -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;
@ -161,6 +161,13 @@ export default class BattleScene extends SceneBase {
public moveAnimations: boolean = true;
public expGainsSpeed: integer = 0;
public skipSeenDialogues: boolean = false;
/**
* Determines if the egg hatching animation should be skipped
* - 0 = Never (never skip animation)
* - 1 = Ask (ask to skip animation when hatching 2 or more eggs)
* - 2 = Always (automatically skip animation when hatching 2 or more eggs)
*/
public eggSkipPreference: number = 0;
/**
* Defines the experience gain display mode.
@ -841,12 +848,13 @@ export default class BattleScene extends SceneBase {
}
addEnemyPokemon(species: PokemonSpecies, level: integer, trainerSlot: TrainerSlot, boss: boolean = false, dataSource?: PokemonData, postProcess?: (enemyPokemon: EnemyPokemon) => void): EnemyPokemon {
if (Overrides.OPP_LEVEL_OVERRIDE > 0) {
level = Overrides.OPP_LEVEL_OVERRIDE;
}
if (Overrides.OPP_SPECIES_OVERRIDE) {
species = getPokemonSpecies(Overrides.OPP_SPECIES_OVERRIDE);
}
if (Overrides.OPP_LEVEL_OVERRIDE !== 0) {
level = Overrides.OPP_LEVEL_OVERRIDE;
// The fact that a Pokemon is a boss or not can change based on its Species and level
boss = this.getEncounterBossSegments(this.currentBattle.waveIndex, level, species) > 1;
}
const pokemon = new EnemyPokemon(this, species, level, trainerSlot, boss, dataSource);
@ -854,7 +862,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));
@ -960,6 +968,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);
}
@ -973,6 +991,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;
@ -1110,7 +1129,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);
}
}
@ -1327,6 +1347,13 @@ export default class BattleScene extends SceneBase {
}
getEncounterBossSegments(waveIndex: integer, level: integer, species?: PokemonSpecies, forceBoss: boolean = false): integer {
if (Overrides.OPP_HEALTH_SEGMENTS_OVERRIDE > 1) {
return Overrides.OPP_HEALTH_SEGMENTS_OVERRIDE;
} else if (Overrides.OPP_HEALTH_SEGMENTS_OVERRIDE === 1) {
// The rest of the code expects to be returned 0 and not 1 if the enemy is not a boss
return 0;
}
if (this.gameMode.isDaily && this.gameMode.isWaveFinal(waveIndex)) {
return 5;
}
@ -1791,6 +1818,7 @@ export default class BattleScene extends SceneBase {
config = config ?? {};
try {
const keyDetails = key.split("/");
config["volume"] = config["volume"] ?? 1;
switch (keyDetails[0]) {
case "level_up_fanfare":
case "item_fanfare":
@ -1800,11 +1828,11 @@ export default class BattleScene extends SceneBase {
case "evolution_fanfare":
// These sounds are loaded in as BGM, but played as sound effects
// When these sounds are updated in updateVolume(), they are treated as BGM however because they are placed in the BGM Cache through being called by playSoundWithoutBGM()
config["volume"] = this.masterVolume * this.bgmVolume;
config["volume"] *= (this.masterVolume * this.bgmVolume);
break;
case "battle_anims":
case "cry":
config["volume"] = this.masterVolume * this.fieldVolume;
config["volume"] *= (this.masterVolume * this.fieldVolume);
//PRSFX sound files are unusually loud
if (keyDetails[1].startsWith("PRSFX- ")) {
config["volume"] *= 0.5;
@ -1812,10 +1840,10 @@ export default class BattleScene extends SceneBase {
break;
case "ui":
//As of, right now this applies to the "select", "menu_open", "error" sound effects
config["volume"] = this.masterVolume * this.uiVolume;
config["volume"] *= (this.masterVolume * this.uiVolume);
break;
case "se":
config["volume"] = this.masterVolume * this.seVolume;
config["volume"] *= (this.masterVolume * this.seVolume);
break;
}
this.sound.play(key, config);
@ -2172,8 +2200,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 {
@ -2610,7 +2644,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);
@ -2732,6 +2766,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(p.getSpriteKey(true));
keys.push(p.getBattleSpriteKey(true, true));
keys.push("cry/" + p.species.getCryKey(p.formIndex));
if (p.fusionSpecies) {
keys.push("cry/"+p.fusionSpecies.getCryKey(p.fusionFormIndex));
}
});
// enemyParty has to be operated on separately from playerParty because playerPokemon =/= enemyPokemon
const enemyParty = this.getEnemyParty();
enemyParty.forEach(p => {
keys.push(p.getSpriteKey(true));
keys.push("cry/" + p.species.getCryKey(p.formIndex));
if (p.fusionSpecies) {
keys.push("cry/"+p.fusionSpecies.getCryKey(p.fusionFormIndex));
}
});
return keys;
}
/**
* Initialized the 2nd phase of the final boss (e.g. form-change for Eternatus)
* @param pokemon The (enemy) pokemon

View File

@ -1,12 +1,12 @@
import BattleScene from "./battle-scene";
import { EnemyPokemon, PlayerPokemon, QueuedMove } from "./field/pokemon";
import { Command } from "./ui/command-ui-handler";
import * as Utils from "./utils";
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 Pokemon, { EnemyPokemon, PlayerPokemon, QueuedMove } from "#app/field/pokemon";
import { ArenaTagType } from "#enums/arena-tag-type";
import { BattleSpec } from "#enums/battle-spec";
import { Moves } from "#enums/moves";
@ -31,46 +31,55 @@ export enum BattlerIndex {
export interface TurnCommand {
command: Command;
cursor?: integer;
cursor?: number;
move?: QueuedMove;
targets?: BattlerIndex[];
skip?: boolean;
args?: any[];
}
export interface FaintLogEntry {
pokemon: Pokemon,
turn: number
}
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;
public playerFaintsHistory: FaintLogEntry[] = [];
public enemyFaintsHistory: FaintLogEntry[] = [];
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 +88,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 +99,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 +132,7 @@ export default class Battle {
return rand / value;
}
getBattlerCount(): integer {
getBattlerCount(): number {
return this.double ? 2 : 1;
}
@ -367,7 +361,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 +392,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 +408,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 +430,7 @@ export class FixedBattleConfig {
return this;
}
setSeedOffsetWave(seedOffsetWaveIndex: integer): FixedBattleConfig {
setSeedOffsetWave(seedOffsetWaveIndex: number): FixedBattleConfig {
this.seedOffsetWaveIndex = seedOffsetWaveIndex;
return this;
}
@ -476,7 +476,7 @@ function getRandomTrainerFunc(trainerPool: (TrainerType | TrainerType[])[], rand
}
export interface FixedBattleConfigs {
[key: integer]: FixedBattleConfig
[key: number]: FixedBattleConfig
}
/**
* Youngster/Lass on 5

View File

@ -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
View File

@ -0,0 +1 @@
export const PLAYER_PARTY_MAX_SIZE = 6;

773
src/data/ability.ts Normal file → Executable file

File diff suppressed because it is too large Load Diff

View 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));
}
}
}
@ -905,6 +905,21 @@ class HappyHourTag extends ArenaTag {
}
}
class SafeguardTag extends ArenaTag {
constructor(turnCount: integer, sourceId: integer, side: ArenaTagSide) {
super(ArenaTagType.SAFEGUARD, turnCount, Moves.SAFEGUARD, sourceId, side);
}
onAdd(arena: Arena): void {
arena.scene.queueMessage(i18next.t(`arenaTag:safeguardOnAdd${this.side === ArenaTagSide.PLAYER ? "Player" : this.side === ArenaTagSide.ENEMY ? "Enemy" : ""}`));
}
onRemove(arena: Arena): void {
arena.scene.queueMessage(i18next.t(`arenaTag:safeguardOnRemove${this.side === ArenaTagSide.PLAYER ? "Player" : this.side === ArenaTagSide.ENEMY ? "Enemy" : ""}`));
}
}
export function getArenaTag(tagType: ArenaTagType, turnCount: integer, sourceMove: Moves | undefined, sourceId: integer, targetIndex?: BattlerIndex, side: ArenaTagSide = ArenaTagSide.BOTH): ArenaTag | null {
switch (tagType) {
case ArenaTagType.MIST:
@ -950,6 +965,8 @@ export function getArenaTag(tagType: ArenaTagType, turnCount: integer, sourceMov
return new TailwindTag(turnCount, sourceId, side);
case ArenaTagType.HAPPY_HOUR:
return new HappyHourTag(turnCount, sourceId, side);
case ArenaTagType.SAFEGUARD:
return new SafeguardTag(turnCount, sourceId, side);
default:
return null;
}

View File

@ -788,10 +788,10 @@ export abstract class BattleAnim {
targetSprite.pipelineData["tone"] = [ 0.0, 0.0, 0.0, 0.0 ];
targetSprite.setAngle(0);
if (!this.isHideUser() && userSprite) {
userSprite.setVisible(true);
this.user?.getSprite().setVisible(true); // using this.user to fix context loss due to isOppAnim swap (#481)
}
if (!this.isHideTarget() && (targetSprite !== userSprite || !this.isHideUser())) {
targetSprite.setVisible(true);
this.target?.getSprite().setVisible(true); // using this.target to fix context loss due to isOppAnim swap (#481)
}
for (const ms of Object.values(spriteCache).flat()) {
if (ms) {

View File

@ -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 });
}

View File

@ -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,12 +447,12 @@ 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 {
const isGhost = pokemon.isOfType(Type.GHOST);
const isTrapped = pokemon.getTag(BattlerTagType.TRAPPED);
const isTrapped = pokemon.getTag(TrappedTag);
return !isTrapped && !isGhost;
}
@ -245,6 +485,23 @@ export class TrappedTag extends BattlerTag {
}
}
/**
* BattlerTag implementing No Retreat's trapping effect.
* This is treated separately from other trapping effects to prevent
* Ghost-type Pokemon from being able to reuse the move.
* @extends TrappedTag
*/
class NoRetreatTag extends TrappedTag {
constructor(sourceId: number) {
super(BattlerTagType.NO_RETREAT, BattlerTagLapseType.CUSTOM, 0, Moves.NO_RETREAT, sourceId);
}
/** overrides {@linkcode TrappedTag.apply}, removing the Ghost-type condition */
canAdd(pokemon: Pokemon): boolean {
return !pokemon.getTag(TrappedTag);
}
}
/**
* BattlerTag that represents the {@link https://bulbapedia.bulbagarden.net/wiki/Flinch Flinch} status condition
*/
@ -310,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 {
@ -345,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++;
@ -370,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);
}
/**
@ -489,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);
}
/**
@ -750,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(), true, [BattleStat.DEF, BattleStat.SPDEF], -1));
pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), false, [ Stat.DEF, Stat.SPDEF ], -1));
return true;
}
@ -760,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 {
@ -792,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 {
@ -864,7 +1121,7 @@ export abstract class DamagingTrapTag extends TrappedTag {
}
canAdd(pokemon: Pokemon): boolean {
return !pokemon.isOfType(Type.GHOST) && !pokemon.findTag(t => t instanceof DamagingTrapTag);
return !pokemon.getTag(TrappedTag);
}
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
@ -1041,6 +1298,13 @@ export class ProtectedTag extends BattlerTag {
}
}
/** Base class for `BattlerTag`s that block damaging moves but not status moves */
export class DamageProtectedTag extends ProtectedTag {}
/**
* `BattlerTag` class for moves that block damaging moves damage the enemy if the enemy's move makes contact
* Used by {@linkcode Moves.SPIKY_SHIELD}
*/
export class ContactDamageProtectedTag extends ProtectedTag {
private damageRatio: number;
@ -1076,7 +1340,11 @@ export class ContactDamageProtectedTag extends ProtectedTag {
}
}
export class ContactStatChangeProtectedTag extends ProtectedTag {
/**
* `BattlerTag` class for moves that block damaging moves and lower enemy stats if the enemy's move makes contact
* Used by {@linkcode Moves.KINGS_SHIELD}, {@linkcode Moves.OBSTRUCT}, {@linkcode Moves.SILK_TRAP}
*/
export class ContactStatStageChangeProtectedTag extends DamageProtectedTag {
private stat: BattleStat;
private levels: number;
@ -1093,7 +1361,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;
}
@ -1104,7 +1372,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));
}
}
@ -1132,7 +1400,11 @@ export class ContactPoisonProtectedTag extends ProtectedTag {
}
}
export class ContactBurnProtectedTag extends ProtectedTag {
/**
* `BattlerTag` class for moves that block damaging moves and burn the enemy if the enemy's move makes contact
* Used by {@linkcode Moves.BURNING_BULWARK}
*/
export class ContactBurnProtectedTag extends DamageProtectedTag {
constructor(sourceMove: Moves) {
super(sourceMove, BattlerTagType.BURNING_BULWARK);
}
@ -1190,7 +1462,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 {
@ -1246,7 +1518,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;
}
@ -1331,11 +1603,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;
@ -1353,7 +1624,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 {
@ -1423,7 +1694,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;
}
@ -1487,7 +1758,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 {
@ -1507,6 +1778,25 @@ export class CritBoostTag extends BattlerTag {
}
}
/**
* Tag for the effects of Dragon Cheer, which boosts the critical hit ratio of the user's allies.
* @extends {CritBoostTag}
*/
export class DragonCheerTag extends CritBoostTag {
/** The types of the user's ally when the tag is added */
public typesOnAdd: Type[];
constructor() {
super(BattlerTagType.CRIT_BOOST, Moves.DRAGON_CHEER);
}
onAdd(pokemon: Pokemon): void {
super.onAdd(pokemon);
this.typesOnAdd = pokemon.getTypes(true);
}
}
export class SaltCuredTag extends BattlerTag {
private sourceIndex: number;
@ -1560,7 +1850,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);
}
/**
@ -1678,25 +1968,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]++;
}
};
@ -1704,8 +1994,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,
};
}
@ -1725,9 +2015,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
));
}
}
@ -1741,15 +2031,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));
}
}
}
@ -1827,7 +2117,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:
@ -1864,6 +2185,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source
return new DrowsyTag();
case BattlerTagType.TRAPPED:
return new TrappedTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId);
case BattlerTagType.NO_RETREAT:
return new NoRetreatTag(sourceId);
case BattlerTagType.BIND:
return new BindTag(turnCount, sourceId);
case BattlerTagType.WRAP:
@ -1889,11 +2212,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:
@ -1923,6 +2246,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source
return new TypeBoostTag(tagType, sourceMove, Type.FIRE, 1.5, false);
case BattlerTagType.CRIT_BOOST:
return new CritBoostTag(tagType, sourceMove);
case BattlerTagType.DRAGON_CHEER:
return new DragonCheerTag();
case BattlerTagType.ALWAYS_CRIT:
case BattlerTagType.IGNORE_ACCURACY:
return new BattlerTag(tagType, BattlerTagLapseType.TURN_END, 2, sourceMove);
@ -1955,6 +2280,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:
@ -1962,6 +2289,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);

View File

@ -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) => {

View File

@ -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 ],
@ -7666,7 +7666,7 @@ export function initBiomes() {
if (biome === Biome.END) {
const biomeList = Object.keys(Biome).filter(key => !isNaN(Number(key)));
biomeList.pop(); // Removes Biome.END from the list
const randIndex = Utils.randInt(biomeList.length, 2); // Will never be Biome.TOWN or Biome.PLAINS
const randIndex = Utils.randInt(biomeList.length, 1); // Will never be Biome.TOWN
biome = Biome[biomeList[randIndex]];
}
const linkedBiomes: (Biome | [ Biome, integer ])[] = Array.isArray(biomeLinks[biome])

View File

@ -1,19 +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 { Gender } from "./gender";
import { Moves } from "#app/enums/moves";
import { TypeColor, TypeShadow } from "#app/enums/color";
import { pokemonEvolutions } from "./pokemon-evolutions";
import { pokemonFormChanges } from "./pokemon-forms";
@ -659,7 +658,6 @@ export class FreshStartChallenge extends Challenge {
pokemon.luck = 0; // No luck
pokemon.shiny = false; // Not shiny
pokemon.variant = 0; // Not shiny
pokemon.gender = Gender.MALE; // Starters default to male
pokemon.formIndex = 0; // Froakie should be base form
pokemon.ivs = [10, 10, 10, 10, 10, 10]; // Default IVs of 10 for all stats
return true;

View File

@ -1,6 +1,6 @@
import { BattleSpec } from "#enums/battle-spec";
import { TrainerType } from "#enums/trainer-type";
import {trainerConfigs} from "./trainer-config";
import { trainerConfigs } from "./trainer-config";
export interface TrainerTypeMessages {
encounter?: string | string[],
@ -707,6 +707,20 @@ export const trainerTypeDialogue: TrainerTypeDialogue = {
]
}
],
[TrainerType.ROOD]: [
{
encounter: [
"dialogue:rood.encounter.1",
"dialogue:rood.encounter.2",
"dialogue:rood.encounter.3",
],
victory: [
"dialogue:rood.victory.1",
"dialogue:rood.victory.2",
"dialogue:rood.victory.3",
]
}
],
[TrainerType.FLARE_GRUNT]: [
{
encounter: [
@ -1555,8 +1569,7 @@ export const trainerTypeDialogue: TrainerTypeDialogue = {
"dialogue:roark.victory.1",
"dialogue:roark.victory.2",
"dialogue:roark.victory.3",
"dialogue:roark.victory.4",
"dialogue:roark.victory.5"
"dialogue:roark.victory.4"
],
defeat: [
"dialogue:roark.defeat.1",

View 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();
});
});
});
}
}

Some files were not shown because too many files have changed in this diff Show More