Merge branch 'beta' into mock-console-log
17
.github/workflows/create-release.yml
vendored
@ -20,6 +20,7 @@ permissions:
|
||||
jobs:
|
||||
create-release:
|
||||
if: github.repository == 'pagefaultgames/pokerogue' && (vars.BETA_DEPLOY_BRANCH == '' || ! startsWith(vars.BETA_DEPLOY_BRANCH, 'release'))
|
||||
timeout-minutes: 10
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed for github cli commands
|
||||
runs-on: ubuntu-latest
|
||||
@ -36,14 +37,24 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- uses: actions/create-github-app-token@v2
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.PAGEFAULT_APP_ID }}
|
||||
private-key: ${{ secrets.PAGEFAULT_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: "recursive"
|
||||
# Always base off of beta branch, regardless of the branch the workflow was triggered from.
|
||||
ref: beta
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
- name: Create release branch
|
||||
run: git checkout -b release
|
||||
|
||||
# In order to be able to open a PR into beta, we need the branch to have at least one change.
|
||||
- name: Overwrite RELEASE file
|
||||
run: |
|
||||
@ -52,11 +63,14 @@ jobs:
|
||||
echo "Release v${{ github.event.inputs.versionName }}" > RELEASE
|
||||
git add RELEASE
|
||||
git commit -m "Stage release v${{ github.event.inputs.versionName }}"
|
||||
|
||||
- name: Push new branch
|
||||
run: git push origin release
|
||||
|
||||
# The repository variable is used by the deploy-beta workflow to determine whether to deploy from beta or release.
|
||||
- name: Set repository variable
|
||||
run: GITHUB_TOKEN="${{ secrets.RW_VARS_PAT }}" gh variable set BETA_DEPLOY_BRANCH --body "release"
|
||||
run: GITHUB_TOKEN="${{ steps.app-token.outputs.token }}" gh variable set BETA_DEPLOY_BRANCH --body "release"
|
||||
|
||||
- name: Create pull request to main
|
||||
run: |
|
||||
gh pr create --base main \
|
||||
@ -64,6 +78,7 @@ jobs:
|
||||
--title "Release v${{ github.event.inputs.versionName }} to main" \
|
||||
--body "This PR is for the release of v${{ github.event.inputs.versionName }}, and was created automatically by the GitHub Actions workflow invoked by ${{ github.actor }}" \
|
||||
--draft
|
||||
|
||||
- name: Create pull request to beta
|
||||
run: |
|
||||
gh pr create --base beta \
|
||||
|
1
.github/workflows/deploy-beta.yml
vendored
@ -12,6 +12,7 @@ on:
|
||||
jobs:
|
||||
deploy:
|
||||
if: github.repository == 'pagefaultgames/pokerogue' && github.ref_name == (vars.BETA_DEPLOY_BRANCH || 'beta')
|
||||
timeout-minutes: 10
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
1
.github/workflows/deploy.yml
vendored
@ -11,6 +11,7 @@ on:
|
||||
jobs:
|
||||
deploy:
|
||||
if: github.repository == 'pagefaultgames/pokerogue'
|
||||
timeout-minutes: 10
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
7
.github/workflows/github-pages.yml
vendored
@ -5,10 +5,14 @@ on:
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
- release
|
||||
- 'hotfix*'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
- release
|
||||
- 'hotfix*'
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
|
||||
@ -16,6 +20,7 @@ jobs:
|
||||
pages:
|
||||
name: Github Pages
|
||||
if: github.repository == 'pagefaultgames/pokerogue'
|
||||
timeout-minutes: 10
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
api-dir: ./
|
||||
@ -65,7 +70,7 @@ jobs:
|
||||
pnpm exec typedoc --out /tmp/docs --githubPages false --entryPoints ./src/
|
||||
|
||||
- name: Commit & Push docs
|
||||
if: github.event_name == 'push'
|
||||
if: github.event_name == 'push' && (github.ref_name == 'beta' || github.ref_name == 'main')
|
||||
run: |
|
||||
cd pokerogue_gh
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
5
.github/workflows/linting.yml
vendored
@ -5,16 +5,21 @@ on:
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
- release
|
||||
- 'hotfix*'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
- release
|
||||
- 'hotfix*'
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
|
||||
jobs:
|
||||
run-linters:
|
||||
name: Run linters
|
||||
timeout-minutes: 10
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
1
.github/workflows/post-release-deleted.yml
vendored
@ -6,6 +6,7 @@ jobs:
|
||||
# Set the BETA_DEPLOY_BRANCH variable to beta when a release branch is deleted
|
||||
update-release-var:
|
||||
if: github.repository == 'pagefaultgames/pokerogue' && github.event.ref_type == 'branch' && github.event.ref == 'release'
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set BETA_DEPLOY_BRANCH to beta
|
||||
|
1
.github/workflows/test-shard-template.yml
vendored
@ -21,6 +21,7 @@ jobs:
|
||||
test:
|
||||
# We can't use dynmically named jobs until https://github.com/orgs/community/discussions/13261 is implemented
|
||||
name: Shard
|
||||
timeout-minutes: 10
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ !inputs.skip }}
|
||||
steps:
|
||||
|
7
.github/workflows/tests.yml
vendored
@ -5,16 +5,21 @@ on:
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
- release
|
||||
- 'hotfix*'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
- release
|
||||
- 'hotfix*'
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
check-path-change-filter:
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: read
|
||||
@ -33,6 +38,8 @@ jobs:
|
||||
name: Run Tests
|
||||
needs: check-path-change-filter
|
||||
strategy:
|
||||
# don't stop upon 1 shard failing
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1, 2, 3, 4, 5]
|
||||
uses: ./.github/workflows/test-shard-template.yml
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "pokemon-rogue-battle",
|
||||
"private": true,
|
||||
"version": "1.9.6",
|
||||
"version": "1.10.4",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
@ -14,6 +14,7 @@
|
||||
"test:watch": "vitest watch --coverage --no-isolate",
|
||||
"test:silent": "vitest run --silent='passed-only' --no-isolate",
|
||||
"test:create": "node scripts/create-test/create-test.js",
|
||||
"scrape-trainers": "node scripts/scrape-trainer-names/main.js",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"biome": "biome check --write --changed --no-errors-on-unmatched --diagnostic-level=error",
|
||||
"biome-ci": "biome ci --diagnostic-level=error --reporter=github --no-errors-on-unmatched",
|
||||
|
@ -1102,6 +1102,112 @@
|
||||
"volume": 100,
|
||||
"pitch": 136,
|
||||
"eventType": "AnimTimedSoundEvent"
|
||||
},
|
||||
{
|
||||
"frameIndex": 0,
|
||||
"resourceName": "PRAS- Strong Winds",
|
||||
"bgX": 0,
|
||||
"bgY": 0,
|
||||
"opacity": 0,
|
||||
"duration": 4,
|
||||
"eventType": "AnimTimedAddBgEvent"
|
||||
},
|
||||
{
|
||||
"frameIndex": 0,
|
||||
"resourceName": "",
|
||||
"bgX": 0,
|
||||
"bgY": 0,
|
||||
"opacity": 32,
|
||||
"duration": 4,
|
||||
"eventType": "AnimTimedUpdateBgEvent"
|
||||
}
|
||||
],
|
||||
"4": [
|
||||
{
|
||||
"frameIndex": 4,
|
||||
"resourceName": "",
|
||||
"bgX": 10,
|
||||
"bgY": 0,
|
||||
"opacity": 64,
|
||||
"duration": 4,
|
||||
"eventType": "AnimTimedUpdateBgEvent"
|
||||
}
|
||||
],
|
||||
"8": [
|
||||
{
|
||||
"frameIndex": 8,
|
||||
"resourceName": "",
|
||||
"bgX": 20,
|
||||
"bgY": 0,
|
||||
"opacity": 128,
|
||||
"duration": 4,
|
||||
"eventType": "AnimTimedUpdateBgEvent"
|
||||
}
|
||||
],
|
||||
"12": [
|
||||
{
|
||||
"frameIndex": 12,
|
||||
"resourceName": "",
|
||||
"bgX": 30,
|
||||
"bgY": 0,
|
||||
"opacity": 128,
|
||||
"duration": 4,
|
||||
"eventType": "AnimTimedUpdateBgEvent"
|
||||
}
|
||||
],
|
||||
"16": [
|
||||
{
|
||||
"frameIndex": 16,
|
||||
"resourceName": "",
|
||||
"bgX": 40,
|
||||
"bgY": 0,
|
||||
"opacity": 128,
|
||||
"duration": 4,
|
||||
"eventType": "AnimTimedUpdateBgEvent"
|
||||
}
|
||||
],
|
||||
"20": [
|
||||
{
|
||||
"frameIndex": 20,
|
||||
"resourceName": "",
|
||||
"bgX": 50,
|
||||
"bgY": 0,
|
||||
"opacity": 128,
|
||||
"duration": 4,
|
||||
"eventType": "AnimTimedUpdateBgEvent"
|
||||
}
|
||||
],
|
||||
"24": [
|
||||
{
|
||||
"frameIndex": 24,
|
||||
"resourceName": "",
|
||||
"bgX": 60,
|
||||
"bgY": 0,
|
||||
"opacity": 64,
|
||||
"duration": 4,
|
||||
"eventType": "AnimTimedUpdateBgEvent"
|
||||
}
|
||||
],
|
||||
"28": [
|
||||
{
|
||||
"frameIndex": 28,
|
||||
"resourceName": "",
|
||||
"bgX": 70,
|
||||
"bgY": 0,
|
||||
"opacity": 32,
|
||||
"duration": 4,
|
||||
"eventType": "AnimTimedUpdateBgEvent"
|
||||
}
|
||||
],
|
||||
"32": [
|
||||
{
|
||||
"frameIndex": 32,
|
||||
"resourceName": "",
|
||||
"bgX": 80,
|
||||
"bgY": 0,
|
||||
"opacity": 0,
|
||||
"duration": 3,
|
||||
"eventType": "AnimTimedUpdateBgEvent"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 308 B |
BIN
public/images/items/calendar.png
Normal file
After Width: | Height: | Size: 244 B |
BIN
public/images/items/common_egg.png
Normal file
After Width: | Height: | Size: 471 B |
BIN
public/images/items/common_ribbon.png
Normal file
After Width: | Height: | Size: 290 B |
BIN
public/images/items/epic_egg.png
Normal file
After Width: | Height: | Size: 247 B |
Before Width: | Height: | Size: 274 B After Width: | Height: | Size: 254 B |
Before Width: | Height: | Size: 316 B After Width: | Height: | Size: 296 B |
BIN
public/images/items/legendary_egg.png
Normal file
After Width: | Height: | Size: 247 B |
BIN
public/images/items/manaphy_egg.png
Normal file
After Width: | Height: | Size: 273 B |
Before Width: | Height: | Size: 315 B After Width: | Height: | Size: 299 B |
BIN
public/images/items/rare_egg.png
Normal file
After Width: | Height: | Size: 247 B |
BIN
public/images/items/rogue_egg.png
Normal file
After Width: | Height: | Size: 247 B |
Before Width: | Height: | Size: 315 B After Width: | Height: | Size: 299 B |
Before Width: | Height: | Size: 314 B After Width: | Height: | Size: 297 B |
Before Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 3.2 KiB |
@ -1,11 +0,0 @@
|
||||
{
|
||||
"0": {
|
||||
"422110": "9d4f62",
|
||||
"4a0808": "7f334a",
|
||||
"dec56b": "ffd5f6",
|
||||
"633121": "c66479",
|
||||
"7b5231": "d98997",
|
||||
"a57b4a": "e1b2d7",
|
||||
"de8c29": "ffecfb"
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
{
|
||||
"0": {
|
||||
"4a1008": "7f334a",
|
||||
"dec56b": "ffd5f6",
|
||||
"633a21": "c66479",
|
||||
"422119": "9d4f62",
|
||||
"de9429": "ffecfb",
|
||||
"7b5a31": "d98997",
|
||||
"a57b4a": "e1b2d7"
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
{
|
||||
"0": {
|
||||
"193a29": "314a29",
|
||||
"088c42": "4a8b4a",
|
||||
"195a31": "416a39",
|
||||
"dec56b": "ffd5f6",
|
||||
"4a1008": "783046",
|
||||
"422119": "944b5c",
|
||||
"633a21": "bb5f73",
|
||||
"7b5a31": "cc818e",
|
||||
"a57b4a": "e1b2d7",
|
||||
"de9429": "ffecfb",
|
||||
"311010": "582333"
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
{
|
||||
"0": {
|
||||
"6b6b6b": "cf9bc4",
|
||||
"424a42": "b184a8",
|
||||
"ffffff": "ffd5f6",
|
||||
"adadad": "e1b2d7",
|
||||
"4a1008": "7f334a",
|
||||
"dec56b": "ffecfb",
|
||||
"422119": "9d4f62",
|
||||
"7b5a31": "d98997",
|
||||
"633a21": "c66479",
|
||||
"a57b4a": "e1b2d7",
|
||||
"de9429": "ffecfb"
|
||||
}
|
||||
}
|
@ -515,14 +515,6 @@
|
||||
"577": [1, 1, 1],
|
||||
"578": [1, 1, 1],
|
||||
"579": [1, 1, 1],
|
||||
"585-autumn": [2, 0, 0],
|
||||
"585-spring": [2, 0, 0],
|
||||
"585-summer": [2, 0, 0],
|
||||
"585-winter": [2, 0, 0],
|
||||
"586-autumn": [1, 0, 0],
|
||||
"586-spring": [1, 0, 0],
|
||||
"586-summer": [1, 0, 0],
|
||||
"586-winter": [1, 0, 0],
|
||||
"587": [0, 1, 1],
|
||||
"588": [0, 1, 1],
|
||||
"589": [0, 1, 1],
|
||||
@ -1521,14 +1513,6 @@
|
||||
"577": [1, 1, 1],
|
||||
"578": [1, 1, 1],
|
||||
"579": [1, 1, 1],
|
||||
"585-autumn": [2, 0, 0],
|
||||
"585-spring": [2, 0, 0],
|
||||
"585-summer": [1, 0, 0],
|
||||
"585-winter": [2, 0, 0],
|
||||
"586-autumn": [1, 0, 0],
|
||||
"586-spring": [1, 0, 0],
|
||||
"586-summer": [1, 0, 0],
|
||||
"586-winter": [1, 0, 0],
|
||||
"587": [0, 1, 1],
|
||||
"588": [0, 1, 1],
|
||||
"589": [0, 1, 1],
|
||||
|
Before Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 2.7 KiB |
@ -1,16 +0,0 @@
|
||||
{
|
||||
"0": {
|
||||
"315231": "314a29",
|
||||
"317b42": "416a39",
|
||||
"42b542": "4a8b4a",
|
||||
"ce9c08": "d89ca6",
|
||||
"7b5210": "c16b7d",
|
||||
"ffde52": "ffffff",
|
||||
"bda58c": "d89ca6",
|
||||
"9c7b5a": "c16b7d",
|
||||
"f7efc5": "ffd5f6",
|
||||
"524219": "783046",
|
||||
"313131": "783046",
|
||||
"525252": "c16b7d"
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 2.7 KiB |
@ -1,10 +0,0 @@
|
||||
{
|
||||
"0": {
|
||||
"7b5231": "d98997",
|
||||
"422110": "9d4f62",
|
||||
"633121": "c66479",
|
||||
"de8c29": "ffecfb",
|
||||
"a57b4a": "e1b2d7",
|
||||
"dec56b": "ffd5f6"
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
{
|
||||
"0": {
|
||||
"311010": "2a1418",
|
||||
"731931": "5e263e",
|
||||
"4a1008": "7f334a",
|
||||
"633a21": "c66479",
|
||||
"ce4263": "c66479",
|
||||
"dec56b": "ffd5f6",
|
||||
"422119": "9d4f62",
|
||||
"7b5a31": "d98997",
|
||||
"de9429": "ffecfb",
|
||||
"dedede": "c8c8c8",
|
||||
"a57b4a": "e1b2d7"
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
{
|
||||
"0": {
|
||||
"193a29": "314a29",
|
||||
"195a31": "416a39",
|
||||
"088c42": "4a8b4a",
|
||||
"4a1008": "7f334a",
|
||||
"7b5a31": "d98997",
|
||||
"422119": "9d4f62",
|
||||
"633a21": "c66479",
|
||||
"de9429": "ffecfb",
|
||||
"a57b4a": "e1b2d7",
|
||||
"dec56b": "ffd5f6"
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
{
|
||||
"0": {
|
||||
"424a42": "99648f",
|
||||
"ffffff": "ffd5f6",
|
||||
"6b6b6b": "b184a8",
|
||||
"adadad": "e1b2d7",
|
||||
"4a1008": "7f334a",
|
||||
"dec56b": "ffecfb",
|
||||
"633a21": "c66479",
|
||||
"422119": "9d4f62",
|
||||
"7b5a31": "d98997",
|
||||
"de9429": "ffecfb",
|
||||
"a57b4a": "e1b2d7"
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 142 B After Width: | Height: | Size: 183 B |
Before Width: | Height: | Size: 151 B After Width: | Height: | Size: 201 B |
Before Width: | Height: | Size: 150 B After Width: | Height: | Size: 201 B |
BIN
public/images/ui/common_egg.png
Normal file
After Width: | Height: | Size: 247 B |
Before Width: | Height: | Size: 142 B After Width: | Height: | Size: 183 B |
Before Width: | Height: | Size: 151 B After Width: | Height: | Size: 201 B |
Before Width: | Height: | Size: 150 B After Width: | Height: | Size: 201 B |
BIN
public/images/ui/legacy/common_egg.png
Normal file
After Width: | Height: | Size: 247 B |
Before Width: | Height: | Size: 225 B |
Before Width: | Height: | Size: 225 B |
@ -1 +1 @@
|
||||
Subproject commit 813e5a34739100efd5936bc8a63301dfe451ff8d
|
||||
Subproject commit 102cbdcd924e2a7cdc7eab64d1ce79f6ec7604ff
|
@ -1,3 +1,7 @@
|
||||
self.addEventListener('install', function () {
|
||||
console.log('Service worker installing...');
|
||||
});
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(self.clients.claim());
|
||||
})
|
179
scripts/helpers/strings.js
Normal file
@ -0,0 +1,179 @@
|
||||
// #region Split string code
|
||||
// Regexps involved with splitting words in various case formats.
|
||||
// Sourced from https://www.npmjs.com/package/change-case (with slight tweaking here and there)
|
||||
|
||||
/**
|
||||
* Regex to split at word boundaries.
|
||||
* @type {RegExp}
|
||||
*/
|
||||
const SPLIT_LOWER_UPPER_RE = /([\p{Ll}\d])(\p{Lu})/gu;
|
||||
/**
|
||||
* Regex to split around single-letter uppercase words.
|
||||
* @type {RegExp}
|
||||
*/
|
||||
const SPLIT_UPPER_UPPER_RE = /(\p{Lu})([\p{Lu}][\p{Ll}])/gu;
|
||||
/**
|
||||
* Regexp involved with stripping non-word delimiters from the result.
|
||||
* @type {RegExp}
|
||||
*/
|
||||
const DELIM_STRIP_REGEXP = /[-_ ]+/giu;
|
||||
// The replacement value for splits.
|
||||
const SPLIT_REPLACE_VALUE = "$1\0$2";
|
||||
|
||||
/**
|
||||
* Split any cased string into an array of its constituent words.
|
||||
* @param {string} value
|
||||
* @returns {string[]} The new string, delimited at each instance of one or more spaces, underscores, hyphens
|
||||
* or lower-to-upper boundaries.
|
||||
*/
|
||||
function splitWords(value) {
|
||||
let result = value.trim();
|
||||
result = result.replace(SPLIT_LOWER_UPPER_RE, SPLIT_REPLACE_VALUE).replace(SPLIT_UPPER_UPPER_RE, SPLIT_REPLACE_VALUE);
|
||||
result = result.replace(DELIM_STRIP_REGEXP, "\0");
|
||||
// Trim the delimiter from around the output string
|
||||
return trimFromStartAndEnd(result, "\0").split(/\0/g);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to remove one or more sequences of characters from either end of a string.
|
||||
* @param {string} str - The string to replace
|
||||
* @param {string} charToTrim - The string to remove
|
||||
* @returns {string} The string having been trimmed
|
||||
*/
|
||||
function trimFromStartAndEnd(str, charToTrim) {
|
||||
let start = 0;
|
||||
let end = str.length;
|
||||
const blockLength = charToTrim.length;
|
||||
while (str.startsWith(charToTrim, start)) {
|
||||
start += blockLength;
|
||||
}
|
||||
if (start - end === blockLength) {
|
||||
// Occurs if the ENTIRE string is made up of charToTrim (at which point we return nothing)
|
||||
return "";
|
||||
}
|
||||
while (str.endsWith(charToTrim, end)) {
|
||||
end -= blockLength;
|
||||
}
|
||||
return str.slice(start, end);
|
||||
}
|
||||
// #endregion Split String code
|
||||
|
||||
/**
|
||||
* Capitalize the first letter of a string.
|
||||
* @example
|
||||
* ```ts
|
||||
* console.log(capitalizeFirstLetter("consectetur adipiscing elit")); // returns "Consectetur adipiscing elit"
|
||||
* ```
|
||||
* @param {string} str - The string whose first letter is to be capitalized
|
||||
* @return {string} The original string with its first letter capitalized.
|
||||
*/
|
||||
export function capitalizeFirstLetter(str) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to convert a string into `Title Case` (such as one used for console logs).
|
||||
* @example
|
||||
* ```ts
|
||||
* console.log(toTitleCase("lorem ipsum dolor sit amet")); // returns "Lorem Ipsum Dolor Sit Amet"
|
||||
* ```
|
||||
* @param {string} str - The string being converted
|
||||
* @returns {string} The result of converting `str` into title case.
|
||||
*/
|
||||
export function toTitleCase(str) {
|
||||
return splitWords(str)
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to convert a string into `camelCase` (such as one used for i18n keys).
|
||||
* @example
|
||||
* ```ts
|
||||
* console.log(toCamelCase("BIG_ANGRY_TRAINER")); // returns "bigAngryTrainer"
|
||||
* ```
|
||||
* @param {string} str - The string being converted
|
||||
* @returns {string} The result of converting `str` into camel case.
|
||||
*/
|
||||
export function toCamelCase(str) {
|
||||
return splitWords(str)
|
||||
.map((word, index) =>
|
||||
index === 0 ? word.toLowerCase() : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(),
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to convert a string into `PascalCase`.
|
||||
* @example
|
||||
* ```ts
|
||||
* console.log(toPascalCase("hi how was your day")); // returns "HiHowWasYourDay"
|
||||
* ```
|
||||
* @param {string} str - The string being converted
|
||||
* @returns {string} The result of converting `str` into pascal case.
|
||||
*/
|
||||
export function toPascalCase(str) {
|
||||
return splitWords(str)
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||
.join("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to convert a string into `kebab-case` (such as one used for filenames).
|
||||
* @example
|
||||
* ```ts
|
||||
* console.log(toKebabCase("not_kebab-caSe String")); // returns "not-kebab-case-string"
|
||||
* ```
|
||||
* @param {string} str - The string being converted
|
||||
* @returns {string} The result of converting `str` into kebab case.
|
||||
*/
|
||||
export function toKebabCase(str) {
|
||||
return splitWords(str)
|
||||
.map(word => word.toLowerCase())
|
||||
.join("-");
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to convert a string into `snake_case` (such as one used for filenames).
|
||||
* @example
|
||||
* ```ts
|
||||
* console.log(toSnakeCase("not-in snake_CaSe")); // returns "not_in_snake_case"
|
||||
* ```
|
||||
* @param {string} str - The string being converted
|
||||
* @returns {string} The result of converting `str` into snake case.
|
||||
*/
|
||||
export function toSnakeCase(str) {
|
||||
return splitWords(str)
|
||||
.map(word => word.toLowerCase())
|
||||
.join("_");
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to convert a string into `UPPER_SNAKE_CASE`.
|
||||
* @example
|
||||
* ```ts
|
||||
* console.log(toUpperSnakeCase("apples bananas_oranGes-PearS")); // returns "APPLES_BANANAS_ORANGES_PEARS"
|
||||
* ```
|
||||
* @param {string} str - The string being converted
|
||||
* @returns {string} The result of converting `str` into upper snake case.
|
||||
*/
|
||||
export function toUpperSnakeCase(str) {
|
||||
return splitWords(str)
|
||||
.map(word => word.toUpperCase())
|
||||
.join("_");
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to convert a string into `Pascal_Snake_Case`.
|
||||
* @example
|
||||
* ```ts
|
||||
* console.log(toPascalSnakeCase("apples-bananas_oranGes Pears")); // returns "Apples_Bananas_Oranges_Pears"
|
||||
* ```
|
||||
* @param {string} str - The string being converted
|
||||
* @returns {string} The result of converting `str` into pascal snake case.
|
||||
*/
|
||||
export function toPascalSnakeCase(str) {
|
||||
return splitWords(str)
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||
.join("_");
|
||||
}
|
53
scripts/scrape-trainer-names/check-gender.js
Normal file
@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Check if the given trainer class is female.
|
||||
* @param {Document} document - The HTML document to scrape
|
||||
* @returns {[gender: boolean, counterpartURLs: string[]]} A 2-length tuple containing:
|
||||
* 1. The trainer class' gender (female or not)
|
||||
* 2. A list of all the current class' opposite-gender counterparts (if the trainer has any).
|
||||
*/
|
||||
export function checkGenderAndType(document) {
|
||||
const infoBox = document.getElementsByClassName("infobox")[0];
|
||||
if (!infoBox) {
|
||||
return [false, []];
|
||||
}
|
||||
// Find the row of the table containing the specified gender
|
||||
const children = [...infoBox.getElementsByTagName("tr")];
|
||||
const genderCell = children.find(node => [...node.childNodes].some(c => c.textContent?.includes("Gender")));
|
||||
const tableBox = genderCell?.querySelector("td");
|
||||
if (!tableBox) {
|
||||
return [false, []];
|
||||
}
|
||||
|
||||
const gender = getGender(tableBox);
|
||||
|
||||
// CHeck the cell's inner HTML for any `href`s to gender counterparts and scrape them too
|
||||
const hrefExtractRegex = /href="\/wiki\/(.*?)_\(Trainer_class\)"/g;
|
||||
const counterpartCell = children.find(node => [...node.childNodes].some(c => c.textContent?.includes("Counterpart")));
|
||||
|
||||
const counterpartURLs = [];
|
||||
for (const url of counterpartCell?.innerHTML?.matchAll(hrefExtractRegex) ?? []) {
|
||||
counterpartURLs.push(url[1]);
|
||||
}
|
||||
|
||||
return [gender, counterpartURLs];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the gender from the given node text.
|
||||
* @param {HTMLTableCellElement} genderCell - The cell to check
|
||||
* @returns {boolean} The gender type
|
||||
* @todo Handle trainers whose gender type has changed across different gens (Artists, etc.)
|
||||
*/
|
||||
function getGender(genderCell) {
|
||||
const gender = genderCell.textContent?.trim().toLowerCase() ?? "";
|
||||
|
||||
switch (gender) {
|
||||
case "female only":
|
||||
return true;
|
||||
case "male only":
|
||||
case "both":
|
||||
case undefined:
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
76
scripts/scrape-trainer-names/fetch-names.js
Normal file
@ -0,0 +1,76 @@
|
||||
/**
|
||||
* @import { parsedNames } from "./types.js";
|
||||
*/
|
||||
|
||||
/**
|
||||
* An error code for a bad URL.
|
||||
*/
|
||||
export const INVALID_URL = /** @type {const} */ ("bad_url_code");
|
||||
|
||||
/**
|
||||
* Fetch a given trainer's names from the given HTML document.
|
||||
* @param {HTMLElement | null | undefined} trainerListHeader - The header containing the trainer lists
|
||||
* @param {boolean} [knownFemale=false] - Whether the class is known to be female; default `false`
|
||||
* @returns {parsedNames | INVALID_URL}
|
||||
* An object containing the parsed names. \
|
||||
* Will instead return with {@linkcode INVALID_URL} if the data is invalid.
|
||||
*/
|
||||
export function fetchNames(trainerListHeader, knownFemale = false) {
|
||||
const trainerNames = /** @type {Set<string>} */ (new Set());
|
||||
const femaleTrainerNames = /** @type {Set<string>} */ (new Set());
|
||||
if (!trainerListHeader?.parentElement?.childNodes) {
|
||||
// Return early if no child nodes (ie tables) can be found
|
||||
return INVALID_URL;
|
||||
}
|
||||
|
||||
const elements = [...trainerListHeader.parentElement.childNodes];
|
||||
|
||||
// Find all elements within the "Trainer Names" header and selectively filter to find the name tables.
|
||||
const startChildIndex = elements.indexOf(trainerListHeader);
|
||||
const endChildIndex = elements.findIndex(h => h.nodeName === "H2" && elements.indexOf(h) > startChildIndex);
|
||||
|
||||
// Grab all the trainer name tables sorted by generation
|
||||
const tables = elements.slice(startChildIndex, endChildIndex).filter(
|
||||
/** @type {(t: ChildNode) => t is Element} */
|
||||
(
|
||||
t =>
|
||||
// Only grab expandable tables within the header block
|
||||
t.nodeName === "TABLE" && t["className"] === "expandable"
|
||||
),
|
||||
);
|
||||
|
||||
parseTable(tables, knownFemale, trainerNames, femaleTrainerNames);
|
||||
return {
|
||||
male: Array.from(trainerNames),
|
||||
female: Array.from(femaleTrainerNames),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the table in question.
|
||||
* @param {Element[]} tables - The array of Elements forming the current table
|
||||
* @param {boolean} isFemale - Whether the trainer is known to be female or not
|
||||
* @param {Set<string>} trainerNames A Set containing the male trainer names
|
||||
* @param {Set<string>} femaleTrainerNames - A Set containing the female trainer names
|
||||
*/
|
||||
function parseTable(tables, isFemale, trainerNames, femaleTrainerNames) {
|
||||
for (const table of tables) {
|
||||
// Grab all rows past the first header with exactly 9 children in them (Name, Battle, Winnings, 6 party slots)
|
||||
const trainerRows = [...table.querySelectorAll("tr:not(:first-child)")].filter(r => r.children.length === 9);
|
||||
for (const row of trainerRows) {
|
||||
const content = row.firstElementChild?.innerHTML;
|
||||
// Skip empty elements & ones without anchors
|
||||
if (!content || content?.indexOf(" <a ") === -1) {
|
||||
continue;
|
||||
}
|
||||
/** Whether the name is female */
|
||||
const female = isFemale || content.includes("♀");
|
||||
// Grab the plaintext name part with an optional ampersand
|
||||
const nameMatch = />([a-z]+(?: & [a-z]+)?)<\/a>/i.exec(content);
|
||||
if (!nameMatch) {
|
||||
continue;
|
||||
}
|
||||
(female ? femaleTrainerNames : trainerNames).add(nameMatch[1].replace("&", "&"));
|
||||
}
|
||||
}
|
||||
}
|
16
scripts/scrape-trainer-names/help-message.js
Normal file
@ -0,0 +1,16 @@
|
||||
import chalk from "chalk";
|
||||
|
||||
/** Show help/usage text for the `scrape-trainers` CLI. */
|
||||
export function showHelpText() {
|
||||
console.log(`
|
||||
Usage: ${chalk.cyan("pnpm scrape-trainers [options] <names>")}
|
||||
Note that all option names are ${chalk.bold("case insensitive")}.
|
||||
|
||||
${chalk.hex("#8a2be2")("Arguments:")}
|
||||
${chalk.hex("#7fff00")("names")} The name of one or more trainer classes to parse.
|
||||
|
||||
${chalk.hex("#ffa500")("Options:")}
|
||||
${chalk.blue("-h, --help")} Show this help message.
|
||||
${chalk.blue("-o, --out, --outfile")} The path to a file to save the output. If not provided, will send directly to stdout.
|
||||
`);
|
||||
}
|
295
scripts/scrape-trainer-names/main.js
Normal file
@ -0,0 +1,295 @@
|
||||
import { existsSync, writeFileSync } from "node:fs";
|
||||
import { format, inspect } from "node:util";
|
||||
import chalk from "chalk";
|
||||
import inquirer from "inquirer";
|
||||
import { JSDOM } from "jsdom";
|
||||
import { toCamelCase, toPascalSnakeCase, toTitleCase } from "../helpers/strings.js";
|
||||
import { checkGenderAndType } from "./check-gender.js";
|
||||
import { fetchNames, INVALID_URL } from "./fetch-names.js";
|
||||
import { showHelpText } from "./help-message.js";
|
||||
|
||||
/**
|
||||
* @packageDocumentation
|
||||
* This script will scrape Bulbapedia for the English names of a given trainer class,
|
||||
* outputting them as JSON.
|
||||
* Usage: `pnpm scrape-trainers`
|
||||
*/
|
||||
|
||||
/**
|
||||
* @import { parsedNames } from "./types.js"
|
||||
*/
|
||||
|
||||
const version = "1.0.0";
|
||||
const OUTFILE_ALIASES = /** @type {const} */ (["-o", "--outfile", "--outFile"]);
|
||||
|
||||
/**
|
||||
* A large object mapping each "base" trainer name to a list of replacements.
|
||||
* Used to allow for trainer classes with different `TrainerType`s than in mainline.
|
||||
* @type {Record<string, string[]>}
|
||||
*/
|
||||
const trainerNamesMap = {
|
||||
pokemonBreeder: ["breeder"],
|
||||
worker: ["worker", "snowWorker"],
|
||||
richBoy: ["richKid"],
|
||||
gentleman: ["rich"],
|
||||
};
|
||||
|
||||
async function main() {
|
||||
console.log(chalk.hex("#FF7F50")(`🍳 Trainer Name Scraper v${version}`));
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const out = getOutfile(args);
|
||||
// Break out if no args remain
|
||||
if (args.length === 0) {
|
||||
console.error(
|
||||
chalk.red.bold(
|
||||
`✗ Error: No trainer classes provided!\nArgs: ${chalk.hex("#7310fdff")(process.argv.slice(2).join(", "))}`,
|
||||
),
|
||||
);
|
||||
showHelpText();
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
const output = await scrapeTrainerNames(args);
|
||||
await tryWriteFile(out, output);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the outfile location from the args array.
|
||||
* @param {string[]} args - The command line arguments
|
||||
* @returns {string | undefined} The outfile location, or `undefined` if none is provided
|
||||
* @remarks
|
||||
* This will mutate the `args` array by removing the outfile from the list of arguments.
|
||||
*/
|
||||
function getOutfile(args) {
|
||||
let /** @type {string} */ outFile;
|
||||
// Extract the outfile as either the form "-o=y" or "-o y".
|
||||
const hasEquals = /^.*=(.+)$/g.exec(args[0]);
|
||||
if (hasEquals) {
|
||||
outFile = hasEquals[1];
|
||||
args.splice(0, 1);
|
||||
} else if (/** @type {readonly string[]} */ (OUTFILE_ALIASES).includes(args[0])) {
|
||||
outFile = args[1];
|
||||
args.splice(0, 2);
|
||||
} else {
|
||||
console.log(chalk.hex("#ffa500")("No outfile detected, logging to stdout..."));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.hex("#ffa500")(`Using outfile: ${chalk.blue(outFile)}`));
|
||||
return outFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrape the requested trainer names and format the resultant output.
|
||||
* @param {string[]} classes The names of the trainer classes to retrieve
|
||||
* @returns {Promise<string>} A Promise that resolves with the finished text.
|
||||
*/
|
||||
async function scrapeTrainerNames(classes) {
|
||||
classes = [...new Set(classes)];
|
||||
|
||||
/**
|
||||
* A Set containing all trainer URLs that have been seen.
|
||||
* @type {Set<string>}
|
||||
*/
|
||||
const seenClasses = new Set();
|
||||
|
||||
/**
|
||||
* A large array of tuples matching each class to their corresponding list of trainer names. \
|
||||
* Trainer classes with only 1 gender will only contain the single array for that gender.
|
||||
* @type {[keyName: string, names: string[] | parsedNames][]}
|
||||
*/
|
||||
const namesTuples = await Promise.all(
|
||||
classes.map(async trainerClass => {
|
||||
try {
|
||||
const [trainerName, names] = await doFetch(trainerClass, seenClasses);
|
||||
const namesObj = names.female.length === 0 ? names.male : names;
|
||||
return /** @type {const} */ ([trainerName, namesObj]);
|
||||
} catch (e) {
|
||||
if (!(e instanceof Error)) {
|
||||
throw new Error(chalk.red.bold("Unrecognized error detected:", inspect(e)));
|
||||
}
|
||||
// If the error contains an HTTP status, attempt to parse the code to give a more friendly
|
||||
// response than JSDOM's "Resource was not loaded"gi
|
||||
const errCode = /Status: (\d*)/g.exec(e.message)?.[1];
|
||||
if (!errCode) {
|
||||
throw e;
|
||||
}
|
||||
/** @type {string} */
|
||||
let reason;
|
||||
switch (+errCode) {
|
||||
case 404:
|
||||
reason = "Page not found";
|
||||
break;
|
||||
case 403:
|
||||
reason = "Access is forbidden";
|
||||
break;
|
||||
default:
|
||||
reason = `Server produced error code of ${+errCode}`;
|
||||
}
|
||||
throw new Error(
|
||||
chalk.red.bold(`Failed to parse URL for ${chalk.hex("#7fff00")(`\"${trainerClass}\"`)}!\nReason: ${reason}`),
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
// Grab all keys inside the name replacement map and change them accordingly.
|
||||
const mappedNames = namesTuples.filter(tuple => tuple[0] in trainerNamesMap);
|
||||
for (const mappedName of mappedNames) {
|
||||
const namesMapping = trainerNamesMap[mappedName[0]];
|
||||
namesTuples.splice(
|
||||
namesTuples.indexOf(mappedName),
|
||||
1,
|
||||
...namesMapping.map(
|
||||
name => /** @type {[keyName: string, names: parsedNames | string[]]} */ ([name, mappedName[1]]),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
namesTuples.sort((a, b) => a[0].localeCompare(b[0]));
|
||||
|
||||
/** @type {Record<string, string[] | parsedNames>} */
|
||||
const namesRecord = Object.fromEntries(namesTuples);
|
||||
|
||||
// Convert all arrays into objects indexed by numbers
|
||||
return JSON.stringify(
|
||||
namesRecord,
|
||||
(_, v) => {
|
||||
if (Array.isArray(v)) {
|
||||
return v.reduce((ret, curr, i) => {
|
||||
ret[i + 1] = curr; // 1 indexed
|
||||
return ret;
|
||||
}, {});
|
||||
}
|
||||
return v;
|
||||
},
|
||||
2,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively scrape names from a given Trainer class and its gender counterparts.
|
||||
* @param {string} trainerClass - The URL to parse
|
||||
* @param {Set<string>} seenClasses - A Set containing all seen class URLs, used for record keeping.
|
||||
* @returns {Promise<[string, parsedNames]>}
|
||||
* A Promise that resolves with:
|
||||
* 1. The name to use for the key.
|
||||
* 2. All fetched names for this trainer class and its gender variants.
|
||||
*/
|
||||
async function doFetch(trainerClass, seenClasses) {
|
||||
let keyName = toCamelCase(trainerClass);
|
||||
// Bulba URLs are in Pascal_Snake_Case (Pokemon_Breeder)
|
||||
const classURL = toPascalSnakeCase(trainerClass);
|
||||
seenClasses.add(classURL);
|
||||
|
||||
// Bulbapedia has redirects mapping basically all variant spellings of each trainer name to the corresponding main page.
|
||||
// We thus rely on it
|
||||
const { document } = (await JSDOM.fromURL(`https://bulbapedia.bulbagarden.net/wiki/${classURL}`)).window;
|
||||
const trainerListHeader = document.querySelector("#Trainer_list")?.parentElement;
|
||||
const [female, counterpartURLs] = checkGenderAndType(document);
|
||||
const names = fetchNames(trainerListHeader, female);
|
||||
if (names === INVALID_URL) {
|
||||
return Promise.reject(
|
||||
new Error(chalk.red.bold(`URL \"${classURL}\" did not correspond to a valid trainer class!`)),
|
||||
);
|
||||
}
|
||||
|
||||
// Recurse into all unseen gender counterparts' URLs, using the first male name we find
|
||||
const counterpartNames = await Promise.all(
|
||||
counterpartURLs
|
||||
.filter(url => !seenClasses.has(url))
|
||||
.map(counterpartURL => {
|
||||
console.log(chalk.green(`Accessing gender counterpart URL: ${toTitleCase(counterpartURL)}`));
|
||||
return doFetch(counterpartURL, seenClasses);
|
||||
}),
|
||||
);
|
||||
let overrodeName = false;
|
||||
for (const [cKeyName, cNameObj] of counterpartNames) {
|
||||
if (!overrodeName && female) {
|
||||
overrodeName = true;
|
||||
console.log(chalk.green(`Using "${cKeyName}" as the name of the JSON key object...`));
|
||||
keyName = cKeyName;
|
||||
}
|
||||
names.male = [...new Set(names.male.concat(cNameObj.male))];
|
||||
names.female = [...new Set(names.female.concat(cNameObj.female))];
|
||||
}
|
||||
return [normalizeDiacritics(keyName), names];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert all diacritical marks within a string into their normalized variants.
|
||||
* @param {string} str - The string to parse
|
||||
* @returns {string} The string with normalized diacritics
|
||||
*/
|
||||
function normalizeDiacritics(str) {
|
||||
// Normalizing to NFKD splits all diacritics into the base letter + grapheme (à -> a + `),
|
||||
// which are conveniently all in their own little Unicode block for easy removal
|
||||
return str.normalize("NFKD").replace(/[\u0300-\u036f]/g, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to write the output to a file (or log it to stdout, as the case may be).
|
||||
* @param {string | undefined} outFile - The outfile
|
||||
* @param {string} output - The scraped output to produce
|
||||
*/
|
||||
async function tryWriteFile(outFile, output) {
|
||||
if (!outFile) {
|
||||
console.log(output);
|
||||
return;
|
||||
}
|
||||
|
||||
if (existsSync(outFile) && !(await promptExisting(outFile))) {
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
writeFileSync(outFile, output);
|
||||
console.log(chalk.green.bold(`✔ Output written to ${chalk.blue(outFile)} successfully!`));
|
||||
} catch (e) {
|
||||
let /** @type {string} */ errStr;
|
||||
if (!(e instanceof Error)) {
|
||||
errStr = format("Unknown error occurred: ", e);
|
||||
} else {
|
||||
// @ts-expect-error - Node.JS file errors always have codes
|
||||
switch (e.code) {
|
||||
case "ENOENT":
|
||||
errStr = `File not found: ${outFile}`;
|
||||
break;
|
||||
case "EACCES":
|
||||
errStr = `Could not write ${outFile}: Permission denied`;
|
||||
break;
|
||||
case "EISDIR":
|
||||
errStr = `Unable to write to ${outFile} as it is a directory`;
|
||||
break;
|
||||
default:
|
||||
errStr = `Error writing file: ${e.message}`;
|
||||
}
|
||||
}
|
||||
console.error(chalk.red.bold(errStr));
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm overwriting an already-existing file.
|
||||
* @param {string} outFile - The outfile
|
||||
* @returns {Promise<boolean>} Whether "Yes" or "No" was selected.
|
||||
*/
|
||||
async function promptExisting(outFile) {
|
||||
return (
|
||||
await inquirer.prompt([
|
||||
{
|
||||
type: "confirm",
|
||||
name: "continue",
|
||||
message: `File ${chalk.blue(outFile)} already exists!` + "\nDo you want to replace it?",
|
||||
default: false,
|
||||
},
|
||||
])
|
||||
).continue;
|
||||
}
|
||||
|
||||
main();
|
9
scripts/scrape-trainer-names/types.js
Normal file
@ -0,0 +1,9 @@
|
||||
/**
|
||||
* @typedef {Object}
|
||||
* parsedNames
|
||||
* A parsed object containing the desired names.
|
||||
* @property {string[]} male
|
||||
* @property {string[]} female
|
||||
*/
|
||||
|
||||
export {};
|
@ -1,8 +1,10 @@
|
||||
import type { ArenaTagTypeMap } from "#data/arena-tag";
|
||||
import type { ArenaTagType } from "#enums/arena-tag-type";
|
||||
// biome-ignore lint/correctness/noUnusedImports: TSDocs
|
||||
import type { SessionSaveData } from "#system/game-data";
|
||||
|
||||
/** Subset of {@linkcode ArenaTagType}s that apply some negative effect to pokemon that switch in ({@link https://bulbapedia.bulbagarden.net/wiki/List_of_moves_that_cause_entry_hazards#List_of_traps | entry hazards} and Imprison. */
|
||||
export type ArenaTrapTagType =
|
||||
export type EntryHazardTagType =
|
||||
| ArenaTagType.STICKY_WEB
|
||||
| ArenaTagType.SPIKES
|
||||
| ArenaTagType.TOXIC_SPIKES
|
||||
@ -19,6 +21,9 @@ export type TurnProtectArenaTagType =
|
||||
| ArenaTagType.MAT_BLOCK
|
||||
| ArenaTagType.CRAFTY_SHIELD;
|
||||
|
||||
/** Subset of {@linkcode ArenaTagType}s that create Trick Room-like effects which are removed upon overlap. */
|
||||
export type RoomArenaTagType = ArenaTagType.TRICK_ROOM;
|
||||
|
||||
/** Subset of {@linkcode ArenaTagType}s that cannot persist across turns, and thus should not be serialized in {@linkcode SessionSaveData}. */
|
||||
export type NonSerializableArenaTagType = ArenaTagType.NONE | TurnProtectArenaTagType | ArenaTagType.ION_DELUGE;
|
||||
|
||||
|
@ -1476,10 +1476,7 @@ export class BattleScene extends SceneBase {
|
||||
pokemon.resetBattleAndWaveData();
|
||||
pokemon.resetTera();
|
||||
applyAbAttrs("PostBattleInitAbAttr", { pokemon });
|
||||
if (
|
||||
pokemon.hasSpecies(SpeciesId.TERAPAGOS) ||
|
||||
(this.gameMode.isClassic && this.currentBattle.waveIndex > 180 && this.currentBattle.waveIndex <= 190)
|
||||
) {
|
||||
if (pokemon.hasSpecies(SpeciesId.TERAPAGOS)) {
|
||||
this.arena.playerTerasUsed = 0;
|
||||
}
|
||||
}
|
||||
@ -3684,15 +3681,8 @@ export class BattleScene extends SceneBase {
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (this.gameMode.modeId === GameModes.CHALLENGE) {
|
||||
const disallowedChallenges = encounterCandidate.disallowedChallenges;
|
||||
if (
|
||||
disallowedChallenges &&
|
||||
disallowedChallenges.length > 0 &&
|
||||
this.gameMode.challenges.some(challenge => disallowedChallenges.includes(challenge.id))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (encounterCandidate.disallowedChallenges?.some(challenge => this.gameMode.hasChallenge(challenge))) {
|
||||
return false;
|
||||
}
|
||||
if (!encounterCandidate.meetsRequirements()) {
|
||||
return false;
|
||||
|
@ -6,7 +6,7 @@ import type { SpeciesFormChangeRevertWeatherFormTrigger } from "#data/form-chang
|
||||
import { applyAbAttrs } from "#abilities/apply-ab-attrs";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
import type { ArenaTrapTag, SuppressAbilitiesTag } from "#data/arena-tag";
|
||||
import type { EntryHazardTag, SuppressAbilitiesTag } from "#data/arena-tag";
|
||||
import type { BattlerTag } from "#data/battler-tags";
|
||||
import { GroundedTag } from "#data/battler-tags";
|
||||
import { getBerryEffectFunc } from "#data/berry";
|
||||
@ -970,6 +970,8 @@ export class MoveImmunityStatStageChangeAbAttr extends MoveImmunityAbAttr {
|
||||
export interface PostMoveInteractionAbAttrParams extends AugmentMoveInteractionAbAttrParams {
|
||||
/** Stores the hit result of the move used in the interaction */
|
||||
readonly hitResult: HitResult;
|
||||
/** The amount of damage dealt in the interaction */
|
||||
readonly damage: number;
|
||||
}
|
||||
|
||||
export class PostDefendAbAttr extends AbAttr {
|
||||
@ -1079,20 +1081,16 @@ export class PostDefendHpGatedStatStageChangeAbAttr extends PostDefendAbAttr {
|
||||
this.selfTarget = selfTarget;
|
||||
}
|
||||
|
||||
override canApply({ pokemon, opponent: attacker, move }: PostMoveInteractionAbAttrParams): boolean {
|
||||
override canApply({ pokemon, opponent: attacker, move, damage }: PostMoveInteractionAbAttrParams): boolean {
|
||||
const hpGateFlat: number = Math.ceil(pokemon.getMaxHp() * this.hpGate);
|
||||
const lastAttackReceived = pokemon.turnData.attacksReceived[pokemon.turnData.attacksReceived.length - 1];
|
||||
const damageReceived = lastAttackReceived?.damage || 0;
|
||||
return (
|
||||
this.condition(pokemon, attacker, move) && pokemon.hp <= hpGateFlat && pokemon.hp + damageReceived > hpGateFlat
|
||||
);
|
||||
return this.condition(pokemon, attacker, move) && pokemon.hp <= hpGateFlat && pokemon.hp + damage > hpGateFlat;
|
||||
}
|
||||
|
||||
override apply({ simulated, pokemon, opponent: attacker }: PostMoveInteractionAbAttrParams): void {
|
||||
override apply({ simulated, pokemon, opponent }: PostMoveInteractionAbAttrParams): void {
|
||||
if (!simulated) {
|
||||
globalScene.phaseManager.unshiftNew(
|
||||
"StatStageChangePhase",
|
||||
(this.selfTarget ? pokemon : attacker).getBattlerIndex(),
|
||||
(this.selfTarget ? pokemon : opponent).getBattlerIndex(),
|
||||
true,
|
||||
this.stats,
|
||||
this.stages,
|
||||
@ -1113,7 +1111,7 @@ export class PostDefendApplyArenaTrapTagAbAttr extends PostDefendAbAttr {
|
||||
}
|
||||
|
||||
override canApply({ pokemon, opponent: attacker, move }: PostMoveInteractionAbAttrParams): boolean {
|
||||
const tag = globalScene.arena.getTag(this.arenaTagType) as ArenaTrapTag;
|
||||
const tag = globalScene.arena.getTag(this.arenaTagType) as EntryHazardTag;
|
||||
return (
|
||||
this.condition(pokemon, attacker, move) &&
|
||||
(!globalScene.arena.getTag(this.arenaTagType) || tag.layers < tag.maxLayers)
|
||||
@ -1235,7 +1233,7 @@ export class PostDefendContactApplyStatusEffectAbAttr extends PostDefendAbAttr {
|
||||
// TODO: Probably want to check against simulated here
|
||||
const effect =
|
||||
this.effects.length === 1 ? this.effects[0] : this.effects[pokemon.randBattleSeedInt(this.effects.length)];
|
||||
attacker.trySetStatus(effect, true, pokemon);
|
||||
attacker.trySetStatus(effect, pokemon);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1263,17 +1261,17 @@ export class PostDefendContactApplyTagChanceAbAttr extends PostDefendAbAttr {
|
||||
this.turnCount = turnCount;
|
||||
}
|
||||
|
||||
override canApply({ move, pokemon, opponent: attacker }: PostMoveInteractionAbAttrParams): boolean {
|
||||
override canApply({ move, pokemon, opponent }: PostMoveInteractionAbAttrParams): boolean {
|
||||
return (
|
||||
move.doesFlagEffectApply({ flag: MoveFlags.MAKES_CONTACT, user: attacker, target: pokemon }) &&
|
||||
move.doesFlagEffectApply({ flag: MoveFlags.MAKES_CONTACT, user: opponent, target: pokemon }) &&
|
||||
pokemon.randBattleSeedInt(100) < this.chance &&
|
||||
attacker.canAddTag(this.tagType)
|
||||
opponent.canAddTag(this.tagType)
|
||||
);
|
||||
}
|
||||
|
||||
override apply({ simulated, opponent: attacker, move }: PostMoveInteractionAbAttrParams): void {
|
||||
override apply({ pokemon, simulated, opponent, move }: PostMoveInteractionAbAttrParams): void {
|
||||
if (!simulated) {
|
||||
attacker.addTag(this.tagType, this.turnCount, move.id, attacker.id);
|
||||
opponent.addTag(this.tagType, this.turnCount, move.id, pokemon.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1758,7 +1756,7 @@ export class PokemonTypeChangeAbAttr extends PreAttackAbAttr {
|
||||
* Parameters for abilities that modify the hit count and damage of a move
|
||||
*/
|
||||
export interface AddSecondStrikeAbAttrParams extends Omit<AugmentMoveInteractionAbAttrParams, "opponent"> {
|
||||
/** Holder for the number of hits. May be modified by ability application */
|
||||
/** Holder for the number of hits. May be modified by ability application */
|
||||
hitCount?: NumberHolder;
|
||||
/** Holder for the damage multiplier _of the current hit_ */
|
||||
multiplier?: NumberHolder;
|
||||
@ -2228,7 +2226,7 @@ export class PostAttackApplyStatusEffectAbAttr extends PostAttackAbAttr {
|
||||
apply({ pokemon, opponent }: PostMoveInteractionAbAttrParams): void {
|
||||
const effect =
|
||||
this.effects.length === 1 ? this.effects[0] : this.effects[pokemon.randBattleSeedInt(this.effects.length)];
|
||||
opponent.trySetStatus(effect, true, pokemon);
|
||||
opponent.trySetStatus(effect, pokemon);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2383,7 +2381,7 @@ export class SynchronizeStatusAbAttr extends PostSetStatusAbAttr {
|
||||
*/
|
||||
override apply({ simulated, effect, sourcePokemon, pokemon }: PostSetStatusAbAttrParams): void {
|
||||
if (!simulated && sourcePokemon) {
|
||||
sourcePokemon.trySetStatus(effect, true, pokemon);
|
||||
sourcePokemon.trySetStatus(effect, pokemon);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3014,41 +3012,44 @@ export class PostSummonFormChangeAbAttr extends PostSummonAbAttr {
|
||||
}
|
||||
}
|
||||
|
||||
/** Attempts to copy a pokemon's ability */
|
||||
/**
|
||||
* Attempts to copy a pokemon's ability
|
||||
*
|
||||
* @remarks
|
||||
* Hardcodes idiosyncrasies specific to trace, so should not be used for other abilities
|
||||
* that might copy abilities in the future
|
||||
* @sealed
|
||||
*/
|
||||
export class PostSummonCopyAbilityAbAttr extends PostSummonAbAttr {
|
||||
private target: Pokemon;
|
||||
private targetAbilityName: string;
|
||||
|
||||
override canApply({ pokemon }: AbAttrBaseParams): boolean {
|
||||
const targets = pokemon.getOpponents();
|
||||
override canApply({ pokemon, simulated }: AbAttrBaseParams): boolean {
|
||||
const targets = pokemon
|
||||
.getOpponents()
|
||||
.filter(t => t.getAbility().isCopiable || t.getAbility().id === AbilityId.WONDER_GUARD);
|
||||
if (!targets.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let target: Pokemon;
|
||||
if (targets.length > 1) {
|
||||
globalScene.executeWithSeedOffset(() => (target = randSeedItem(targets)), globalScene.currentBattle.waveIndex);
|
||||
// simulated call always chooses first target so as to not advance RNG
|
||||
if (targets.length > 1 && !simulated) {
|
||||
target = targets[randSeedInt(targets.length)];
|
||||
} else {
|
||||
target = targets[0];
|
||||
}
|
||||
|
||||
if (
|
||||
!target!.getAbility().isCopiable &&
|
||||
// Wonder Guard is normally uncopiable so has the attribute, but Trace specifically can copy it
|
||||
!(pokemon.hasAbility(AbilityId.TRACE) && target!.getAbility().id === AbilityId.WONDER_GUARD)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.target = target!;
|
||||
this.targetAbilityName = allAbilities[target!.getAbility().id].name;
|
||||
this.target = target;
|
||||
this.targetAbilityName = allAbilities[target.getAbility().id].name;
|
||||
return true;
|
||||
}
|
||||
|
||||
override apply({ pokemon, simulated }: AbAttrBaseParams): void {
|
||||
if (!simulated) {
|
||||
pokemon.setTempAbility(this.target!.getAbility());
|
||||
setAbilityRevealed(this.target!);
|
||||
// Protect against this somehow being called before canApply by ensuring target is defined
|
||||
if (!simulated && this.target) {
|
||||
pokemon.setTempAbility(this.target.getAbility());
|
||||
setAbilityRevealed(this.target);
|
||||
pokemon.updateInfo();
|
||||
}
|
||||
}
|
||||
@ -3659,7 +3660,8 @@ export class PreSetStatusEffectImmunityAbAttr extends PreSetStatusAbAttr {
|
||||
protected immuneEffects: StatusEffect[];
|
||||
|
||||
/**
|
||||
* @param immuneEffects - The status effects to which the Pokémon is immune.
|
||||
* @param immuneEffects - An array of {@linkcode StatusEffect}s to prevent application.
|
||||
* If none are provided, will block **all** status effects regardless of type.
|
||||
*/
|
||||
constructor(...immuneEffects: StatusEffect[]) {
|
||||
super();
|
||||
@ -3668,7 +3670,7 @@ export class PreSetStatusEffectImmunityAbAttr extends PreSetStatusAbAttr {
|
||||
}
|
||||
|
||||
override canApply({ effect }: PreSetStatusAbAttrParams): boolean {
|
||||
return (effect !== StatusEffect.FAINT && this.immuneEffects.length < 1) || this.immuneEffects.includes(effect);
|
||||
return (this.immuneEffects.length === 0 && effect !== StatusEffect.FAINT) || this.immuneEffects.includes(effect);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -3720,6 +3722,11 @@ export interface UserFieldStatusEffectImmunityAbAttrParams extends AbAttrBasePar
|
||||
*/
|
||||
export class UserFieldStatusEffectImmunityAbAttr extends AbAttr {
|
||||
protected immuneEffects: StatusEffect[];
|
||||
|
||||
/**
|
||||
* @param immuneEffects - An array of {@linkcode StatusEffect}s to prevent application.
|
||||
* If none are provided, will block **all** status effects regardless of type.
|
||||
*/
|
||||
constructor(...immuneEffects: StatusEffect[]) {
|
||||
super();
|
||||
|
||||
@ -3728,7 +3735,7 @@ export class UserFieldStatusEffectImmunityAbAttr extends AbAttr {
|
||||
|
||||
override canApply({ effect, cancelled }: UserFieldStatusEffectImmunityAbAttrParams): boolean {
|
||||
return (
|
||||
(!cancelled.value && effect !== StatusEffect.FAINT && this.immuneEffects.length < 1) ||
|
||||
(!cancelled.value && this.immuneEffects.length === 0 && effect !== StatusEffect.FAINT) ||
|
||||
this.immuneEffects.includes(effect)
|
||||
);
|
||||
}
|
||||
@ -3754,6 +3761,10 @@ export class ConditionalUserFieldStatusEffectImmunityAbAttr extends UserFieldSta
|
||||
*/
|
||||
private condition: (target: Pokemon, source: Pokemon | null) => boolean;
|
||||
|
||||
/**
|
||||
* @param immuneEffects - An array of {@linkcode StatusEffect}s to prevent application.
|
||||
* If none are provided, will block **all** status effects regardless of type.
|
||||
*/
|
||||
constructor(condition: (target: Pokemon, source: Pokemon | null) => boolean, ...immuneEffects: StatusEffect[]) {
|
||||
super(...immuneEffects);
|
||||
|
||||
@ -5814,7 +5825,7 @@ export class NoFusionAbilityAbAttr extends AbAttr {
|
||||
export interface IgnoreTypeImmunityAbAttrParams extends AbAttrBaseParams {
|
||||
/** The type of the move being used */
|
||||
readonly moveType: PokemonType;
|
||||
/** The type being checked for */
|
||||
/** The type being checked for */
|
||||
readonly defenderType: PokemonType;
|
||||
/** Holds whether the type immunity should be bypassed */
|
||||
cancelled: BooleanHolder;
|
||||
@ -7481,8 +7492,7 @@ export function initAbilities() {
|
||||
.unsuppressable()
|
||||
.bypassFaint(),
|
||||
new Ability(AbilityId.CORROSION, 7)
|
||||
.attr(IgnoreTypeStatusEffectImmunityAbAttr, [ StatusEffect.POISON, StatusEffect.TOXIC ], [ PokemonType.STEEL, PokemonType.POISON ])
|
||||
.edgeCase(), // Should poison itself with toxic orb.
|
||||
.attr(IgnoreTypeStatusEffectImmunityAbAttr, [ StatusEffect.POISON, StatusEffect.TOXIC ], [ PokemonType.STEEL, PokemonType.POISON ]),
|
||||
new Ability(AbilityId.COMATOSE, 7)
|
||||
.attr(StatusEffectImmunityAbAttr, ...getNonVolatileStatusEffects())
|
||||
.attr(BattlerTagImmunityAbAttr, BattlerTagType.DROWSY)
|
||||
@ -7824,22 +7834,26 @@ export function initAbilities() {
|
||||
new Ability(AbilityId.TOXIC_CHAIN, 9)
|
||||
.attr(PostAttackApplyStatusEffectAbAttr, false, 30, StatusEffect.TOXIC),
|
||||
new Ability(AbilityId.EMBODY_ASPECT_TEAL, 9)
|
||||
.attr(PostTeraFormChangeStatChangeAbAttr, [ Stat.SPD ], 1)
|
||||
.attr(PostTeraFormChangeStatChangeAbAttr, [ Stat.SPD ], 1) // Activates immediately upon Terastallizing, as well as upon switching in while Terastallized
|
||||
.conditionalAttr(pokemon => pokemon.isTerastallized, PostSummonStatStageChangeAbAttr, [ Stat.SPD ], 1, true)
|
||||
.uncopiable()
|
||||
.unreplaceable() // TODO is this true?
|
||||
.attr(NoTransformAbilityAbAttr),
|
||||
new Ability(AbilityId.EMBODY_ASPECT_WELLSPRING, 9)
|
||||
.attr(PostTeraFormChangeStatChangeAbAttr, [ Stat.SPDEF ], 1)
|
||||
.conditionalAttr(pokemon => pokemon.isTerastallized, PostSummonStatStageChangeAbAttr, [ Stat.SPDEF ], 1, true)
|
||||
.uncopiable()
|
||||
.unreplaceable()
|
||||
.attr(NoTransformAbilityAbAttr),
|
||||
new Ability(AbilityId.EMBODY_ASPECT_HEARTHFLAME, 9)
|
||||
.attr(PostTeraFormChangeStatChangeAbAttr, [ Stat.ATK ], 1)
|
||||
.conditionalAttr(pokemon => pokemon.isTerastallized, PostSummonStatStageChangeAbAttr, [ Stat.ATK ], 1, true)
|
||||
.uncopiable()
|
||||
.unreplaceable()
|
||||
.attr(NoTransformAbilityAbAttr),
|
||||
new Ability(AbilityId.EMBODY_ASPECT_CORNERSTONE, 9)
|
||||
.attr(PostTeraFormChangeStatChangeAbAttr, [ Stat.DEF ], 1)
|
||||
.conditionalAttr(pokemon => pokemon.isTerastallized, PostSummonStatStageChangeAbAttr, [ Stat.DEF ], 1, true)
|
||||
.uncopiable()
|
||||
.unreplaceable()
|
||||
.attr(NoTransformAbilityAbAttr),
|
||||
|
@ -24,11 +24,12 @@ import type { Pokemon } from "#field/pokemon";
|
||||
import type {
|
||||
ArenaScreenTagType,
|
||||
ArenaTagTypeData,
|
||||
ArenaTrapTagType,
|
||||
EntryHazardTagType,
|
||||
RoomArenaTagType,
|
||||
SerializableArenaTagType,
|
||||
} from "#types/arena-tags";
|
||||
import type { Mutable } from "#types/type-helpers";
|
||||
import { BooleanHolder, NumberHolder, toDmgValue } from "#utils/common";
|
||||
import { BooleanHolder, type NumberHolder, toDmgValue } from "#utils/common";
|
||||
import i18next from "i18next";
|
||||
|
||||
/**
|
||||
@ -294,7 +295,8 @@ export abstract class WeakenMoveScreenTag extends SerializableArenaTag {
|
||||
if (bypassed.value) {
|
||||
return false;
|
||||
}
|
||||
damageMultiplier.value = globalScene.currentBattle.double ? 2732 / 4096 : 0.5;
|
||||
// Screens are less effective in Double Battles
|
||||
damageMultiplier.value = globalScene.currentBattle.double ? 2 / 3 : 1 / 2;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@ -725,42 +727,79 @@ export class IonDelugeTag extends ArenaTag {
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract class to implement arena traps.
|
||||
* Abstract class to implement [entry hazards](https://bulbapedia.bulbagarden.net/wiki/List_of_moves_that_cause_entry_hazards).
|
||||
* These persistent tags remain on-field across turns and apply effects to any {@linkcode Pokemon} switching in. \
|
||||
* Uniquely, adding a tag multiple times may stack multiple "layers" of the effect, increasing its severity.
|
||||
*/
|
||||
export abstract class ArenaTrapTag extends SerializableArenaTag {
|
||||
abstract readonly tagType: ArenaTrapTagType;
|
||||
public layers: number;
|
||||
public maxLayers: number;
|
||||
|
||||
export abstract class EntryHazardTag extends SerializableArenaTag {
|
||||
public declare abstract readonly tagType: EntryHazardTagType;
|
||||
/**
|
||||
* Creates a new instance of the ArenaTrapTag class.
|
||||
*
|
||||
* @param tagType - The type of the arena tag.
|
||||
* @param sourceMove - The move that created the tag.
|
||||
* @param sourceId - The ID of the source of the tag.
|
||||
* @param side - The side (player or enemy) the tag affects.
|
||||
* @param maxLayers - The maximum amount of layers this tag can have.
|
||||
* The current number of layers this tag has.
|
||||
* Starts at 1 and increases each time the trap is laid.
|
||||
*/
|
||||
constructor(sourceMove: MoveId, sourceId: number | undefined, side: ArenaTagSide, maxLayers: number) {
|
||||
super(0, sourceMove, sourceId, side);
|
||||
|
||||
this.layers = 1;
|
||||
this.maxLayers = maxLayers;
|
||||
public layers = 1;
|
||||
/** The maximum number of layers this tag can have. */
|
||||
public abstract get maxLayers(): number;
|
||||
/** Whether this tag should only affect grounded targets; default `true` */
|
||||
protected get groundedOnly(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
onOverlap(arena: Arena, _source: Pokemon | null): void {
|
||||
if (this.layers < this.maxLayers) {
|
||||
this.layers++;
|
||||
constructor(sourceMove: MoveId, sourceId: number | undefined, side: ArenaTagSide) {
|
||||
super(0, sourceMove, sourceId, side);
|
||||
}
|
||||
|
||||
this.onAdd(arena);
|
||||
// TODO: Add a `canAdd` field to arena tags to remove need for callers to check layer counts
|
||||
|
||||
/**
|
||||
* Display text when this tag is added to the field.
|
||||
* @param _arena - The {@linkcode Arena} at the time of adding this tag
|
||||
* @param quiet - Whether to suppress messages during tag creation; default `false`
|
||||
*/
|
||||
override onAdd(_arena: Arena, quiet = false): void {
|
||||
// Here, `quiet=true` means "just add the tag, no questions asked"
|
||||
if (quiet) {
|
||||
return;
|
||||
}
|
||||
|
||||
const source = this.getSourcePokemon();
|
||||
if (!source) {
|
||||
console.warn(
|
||||
"Failed to get source Pokemon for AernaTrapTag on add message!" +
|
||||
`\nTag type: ${this.tagType}` +
|
||||
`\nPID: ${this.sourceId}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
globalScene.phaseManager.queueMessage(this.getAddMessage(source));
|
||||
}
|
||||
|
||||
/**
|
||||
* Activates the hazard effect onto a Pokemon when it enters the field
|
||||
* @param _arena the {@linkcode Arena} containing this tag
|
||||
* @param simulated if `true`, only checks if the hazard would activate.
|
||||
* @param pokemon the {@linkcode Pokemon} triggering this hazard
|
||||
* Return the text to be displayed upon adding a new layer to this trap.
|
||||
* @param source - The {@linkcode Pokemon} having created this tag
|
||||
* @returns The localized message to be displayed on screen.
|
||||
*/
|
||||
protected abstract getAddMessage(source: Pokemon): string;
|
||||
|
||||
/**
|
||||
* Add a new layer to this tag upon overlap, triggering the tag's normal {@linkcode onAdd} effects upon doing so.
|
||||
* @param arena - The {@linkcode arena} at the time of adding the tag
|
||||
*/
|
||||
override onOverlap(arena: Arena): void {
|
||||
if (this.layers >= this.maxLayers) {
|
||||
return;
|
||||
}
|
||||
this.layers++;
|
||||
|
||||
this.onAdd(arena);
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate the hazard effect onto a Pokemon when it enters the field.
|
||||
* @param _arena - The {@linkcode Arena} at the time of tag activation
|
||||
* @param simulated - Whether to suppress activation effects during execution
|
||||
* @param pokemon - The {@linkcode Pokemon} triggering this hazard
|
||||
* @returns `true` if this hazard affects the given Pokemon; `false` otherwise.
|
||||
*/
|
||||
override apply(_arena: Arena, simulated: boolean, pokemon: Pokemon): boolean {
|
||||
@ -768,12 +807,21 @@ export abstract class ArenaTrapTag extends SerializableArenaTag {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.groundedOnly && !pokemon.isGrounded()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.activateTrap(pokemon, simulated);
|
||||
}
|
||||
|
||||
activateTrap(_pokemon: Pokemon, _simulated: boolean): boolean {
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Activate this trap's effects when a Pokemon switches into it.
|
||||
* @param _pokemon - The {@linkcode Pokemon}
|
||||
* @param _simulated - Whether the activation is simulated
|
||||
* @returns Whether the trap activation succeeded
|
||||
* @todo Do we need the return value? nothing uses it
|
||||
*/
|
||||
protected abstract activateTrap(_pokemon: Pokemon, _simulated: boolean): boolean;
|
||||
|
||||
getMatchupScoreMultiplier(pokemon: Pokemon): number {
|
||||
return pokemon.isGrounded()
|
||||
@ -781,141 +829,186 @@ export abstract class ArenaTrapTag extends SerializableArenaTag {
|
||||
: Phaser.Math.Linear(0, 1 / Math.pow(2, this.layers), Math.min(pokemon.getHpRatio(), 0.5) * 2);
|
||||
}
|
||||
|
||||
public loadTag<T extends this>(source: BaseArenaTag & Pick<T, "tagType" | "layers" | "maxLayers">): void {
|
||||
public loadTag<T extends this>(source: BaseArenaTag & Pick<T, "tagType" | "layers">): void {
|
||||
super.loadTag(source);
|
||||
this.layers = source.layers;
|
||||
this.maxLayers = source.maxLayers;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract class to implement damaging entry hazards.
|
||||
* Currently used for {@linkcode SpikesTag} and {@linkcode StealthRockTag}.
|
||||
*/
|
||||
abstract class DamagingTrapTag extends EntryHazardTag {
|
||||
override activateTrap(pokemon: Pokemon, simulated: boolean): boolean {
|
||||
// Check for magic guard immunity
|
||||
const cancelled = new BooleanHolder(false);
|
||||
applyAbAttrs("BlockNonDirectDamageAbAttr", { pokemon, cancelled });
|
||||
if (cancelled.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (simulated) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Damage the target and trigger a message
|
||||
const damageHpRatio = this.getDamageHpRatio(pokemon);
|
||||
const damage = toDmgValue(pokemon.getMaxHp() * damageHpRatio);
|
||||
|
||||
globalScene.phaseManager.queueMessage(this.getTriggerMessage(pokemon));
|
||||
pokemon.damageAndUpdate(damage, { result: HitResult.INDIRECT });
|
||||
pokemon.turnData.damageTaken += damage;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the text to be displayed when this tag deals damage.
|
||||
* @param _pokemon - The {@linkcode Pokemon} switching in
|
||||
* @returns The localized trigger message to be displayed on-screen.
|
||||
*/
|
||||
protected abstract getTriggerMessage(_pokemon: Pokemon): string;
|
||||
|
||||
/**
|
||||
* Return the amount of damage this tag should deal to the given Pokemon, relative to its maximum HP.
|
||||
* @param _pokemon - The {@linkcode Pokemon} switching in
|
||||
* @returns The percentage of max HP to deal upon activation.
|
||||
*/
|
||||
protected abstract getDamageHpRatio(_pokemon: Pokemon): number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Spikes_(move) Spikes}.
|
||||
* Applies up to 3 layers of Spikes, dealing 1/8th, 1/6th, or 1/4th of the the Pokémon's HP
|
||||
* in damage for 1, 2, or 3 layers of Spikes respectively if they are summoned into this trap.
|
||||
*/
|
||||
class SpikesTag extends ArenaTrapTag {
|
||||
class SpikesTag extends DamagingTrapTag {
|
||||
public readonly tagType = ArenaTagType.SPIKES;
|
||||
override get maxLayers() {
|
||||
return 3 as const;
|
||||
}
|
||||
|
||||
constructor(sourceId: number | undefined, side: ArenaTagSide) {
|
||||
super(MoveId.SPIKES, sourceId, side, 3);
|
||||
super(MoveId.SPIKES, sourceId, side);
|
||||
}
|
||||
|
||||
onAdd(arena: Arena, quiet = false): void {
|
||||
super.onAdd(arena);
|
||||
|
||||
// We assume `quiet=true` means "just add the bloody tag no questions asked"
|
||||
if (quiet) {
|
||||
return;
|
||||
}
|
||||
|
||||
const source = this.getSourcePokemon();
|
||||
if (!source) {
|
||||
console.warn(`Failed to get source Pokemon for SpikesTag on add message; id: ${this.sourceId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
globalScene.phaseManager.queueMessage(
|
||||
i18next.t("arenaTag:spikesOnAdd", {
|
||||
moveName: this.getMoveName(),
|
||||
opponentDesc: source.getOpponentDescriptor(),
|
||||
}),
|
||||
);
|
||||
protected override getAddMessage(source: Pokemon): string {
|
||||
return i18next.t("arenaTag:spikesOnAdd", {
|
||||
moveName: this.getMoveName(),
|
||||
opponentDesc: source.getOpponentDescriptor(),
|
||||
});
|
||||
}
|
||||
|
||||
override activateTrap(pokemon: Pokemon, simulated: boolean): boolean {
|
||||
if (!pokemon.isGrounded()) {
|
||||
return false;
|
||||
}
|
||||
protected override getTriggerMessage(pokemon: Pokemon): string {
|
||||
return i18next.t("arenaTag:spikesActivateTrap", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||
});
|
||||
}
|
||||
|
||||
const cancelled = new BooleanHolder(false);
|
||||
applyAbAttrs("BlockNonDirectDamageAbAttr", { pokemon, cancelled });
|
||||
if (simulated || cancelled.value) {
|
||||
return !cancelled.value;
|
||||
}
|
||||
|
||||
const damageHpRatio = 1 / (10 - 2 * this.layers);
|
||||
const damage = toDmgValue(pokemon.getMaxHp() * damageHpRatio);
|
||||
|
||||
globalScene.phaseManager.queueMessage(
|
||||
i18next.t("arenaTag:spikesActivateTrap", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||
}),
|
||||
);
|
||||
pokemon.damageAndUpdate(damage, { result: HitResult.INDIRECT });
|
||||
pokemon.turnData.damageTaken += damage;
|
||||
return true;
|
||||
protected override getDamageHpRatio(_pokemon: Pokemon): number {
|
||||
// 1/8 for 1 layer, 1/6 for 2, 1/4 for 3
|
||||
return 1 / (10 - 2 * this.layers);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Toxic_Spikes_(move) Toxic Spikes}.
|
||||
* Applies up to 2 layers of Toxic Spikes, poisoning or badly poisoning any Pokémon who is
|
||||
* summoned into this trap if 1 or 2 layers of Toxic Spikes respectively are up. Poison-type
|
||||
* Pokémon summoned into this trap remove it entirely.
|
||||
* Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Stealth_Rock_(move) | Stealth Rock}.
|
||||
* Applies up to 1 layer of Stealth Rocks, dealing percentage-based damage to any Pokémon
|
||||
* who is summoned into the trap based on the Rock type's type effectiveness.
|
||||
*/
|
||||
class ToxicSpikesTag extends ArenaTrapTag {
|
||||
#neutralized: boolean;
|
||||
public readonly tagType = ArenaTagType.TOXIC_SPIKES;
|
||||
class StealthRockTag extends DamagingTrapTag {
|
||||
public readonly tagType = ArenaTagType.STEALTH_ROCK;
|
||||
public override get maxLayers() {
|
||||
return 1 as const;
|
||||
}
|
||||
protected override get groundedOnly() {
|
||||
return false;
|
||||
}
|
||||
|
||||
constructor(sourceId: number | undefined, side: ArenaTagSide) {
|
||||
super(MoveId.TOXIC_SPIKES, sourceId, side, 2);
|
||||
this.#neutralized = false;
|
||||
super(MoveId.STEALTH_ROCK, sourceId, side);
|
||||
}
|
||||
|
||||
onAdd(arena: Arena, quiet = false): void {
|
||||
super.onAdd(arena);
|
||||
|
||||
if (quiet) {
|
||||
// We assume `quiet=true` means "just add the bloody tag no questions asked"
|
||||
return;
|
||||
}
|
||||
|
||||
const source = this.getSourcePokemon();
|
||||
if (!source) {
|
||||
console.warn(`Failed to get source Pokemon for ToxicSpikesTag on add message; id: ${this.sourceId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
globalScene.phaseManager.queueMessage(
|
||||
i18next.t("arenaTag:toxicSpikesOnAdd", {
|
||||
moveName: this.getMoveName(),
|
||||
opponentDesc: source.getOpponentDescriptor(),
|
||||
}),
|
||||
);
|
||||
protected override getAddMessage(source: Pokemon): string {
|
||||
return i18next.t("arenaTag:stealthRockOnAdd", {
|
||||
opponentDesc: source.getOpponentDescriptor(),
|
||||
});
|
||||
}
|
||||
|
||||
onRemove(arena: Arena): void {
|
||||
protected override getTriggerMessage(pokemon: Pokemon): string {
|
||||
return i18next.t("arenaTag:stealthRockActivateTrap", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||
});
|
||||
}
|
||||
|
||||
protected override getDamageHpRatio(pokemon: Pokemon): number {
|
||||
const effectiveness = pokemon.getAttackTypeEffectiveness(PokemonType.ROCK, undefined, true);
|
||||
return 0.125 * effectiveness;
|
||||
}
|
||||
|
||||
getMatchupScoreMultiplier(pokemon: Pokemon): number {
|
||||
const damageHpRatio = this.getDamageHpRatio(pokemon);
|
||||
return Phaser.Math.Linear(super.getMatchupScoreMultiplier(pokemon), 1, 1 - Math.pow(damageHpRatio, damageHpRatio));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Toxic_Spikes_(move) | Toxic Spikes}.
|
||||
* Applies up to 2 layers of Toxic Spikes, poisoning or badly poisoning any Pokémon switched in
|
||||
* based on the current layer count. \
|
||||
* Poison-type Pokémon will remove it entirely upon switch-in.
|
||||
*/
|
||||
class ToxicSpikesTag extends EntryHazardTag {
|
||||
/**
|
||||
* Whether the tag is currently in the process of being neutralized by a Poison-type.
|
||||
* @defaultValue `false`
|
||||
*/
|
||||
#neutralized = false;
|
||||
public readonly tagType = ArenaTagType.TOXIC_SPIKES;
|
||||
override get maxLayers() {
|
||||
return 2 as const;
|
||||
}
|
||||
|
||||
constructor(sourceId: number | undefined, side: ArenaTagSide) {
|
||||
super(MoveId.TOXIC_SPIKES, sourceId, side);
|
||||
}
|
||||
|
||||
protected override getAddMessage(source: Pokemon): string {
|
||||
return i18next.t("arenaTag:toxicSpikesOnAdd", {
|
||||
moveName: this.getMoveName(),
|
||||
opponentDesc: source.getOpponentDescriptor(),
|
||||
});
|
||||
}
|
||||
|
||||
// Override remove function to only display text when not neutralized
|
||||
override onRemove(arena: Arena): void {
|
||||
if (!this.#neutralized) {
|
||||
super.onRemove(arena);
|
||||
}
|
||||
}
|
||||
|
||||
override activateTrap(pokemon: Pokemon, simulated: boolean): boolean {
|
||||
if (pokemon.isGrounded()) {
|
||||
if (simulated) {
|
||||
return true;
|
||||
}
|
||||
if (pokemon.isOfType(PokemonType.POISON)) {
|
||||
this.#neutralized = true;
|
||||
if (globalScene.arena.removeTag(this.tagType)) {
|
||||
globalScene.phaseManager.queueMessage(
|
||||
i18next.t("arenaTag:toxicSpikesActivateTrapPoison", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||
moveName: this.getMoveName(),
|
||||
}),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
} else if (!pokemon.status) {
|
||||
const toxic = this.layers > 1;
|
||||
if (
|
||||
pokemon.trySetStatus(!toxic ? StatusEffect.POISON : StatusEffect.TOXIC, true, null, 0, this.getMoveName())
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (simulated) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
if (pokemon.isOfType(PokemonType.POISON)) {
|
||||
// Neutralize the tag and remove it from the field.
|
||||
// Message cannot be moved to `onRemove` as that requires a reference to the neutralizing pokemon
|
||||
this.#neutralized = true;
|
||||
globalScene.arena.removeTagOnSide(this.tagType, this.side);
|
||||
globalScene.phaseManager.queueMessage(
|
||||
i18next.t("arenaTag:toxicSpikesActivateTrapPoison", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||
moveName: this.getMoveName(),
|
||||
}),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Attempt to poison the target, suppressing any status effect messages
|
||||
const effect = this.layers === 1 ? StatusEffect.POISON : StatusEffect.TOXIC;
|
||||
return pokemon.trySetStatus(effect, null, 0, this.getMoveName(), false, true);
|
||||
}
|
||||
|
||||
getMatchupScoreMultiplier(pokemon: Pokemon): number {
|
||||
@ -930,71 +1023,37 @@ class ToxicSpikesTag extends ArenaTrapTag {
|
||||
}
|
||||
|
||||
/**
|
||||
* Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Stealth_Rock_(move) Stealth Rock}.
|
||||
* Applies up to 1 layer of Stealth Rocks, dealing percentage-based damage to any Pokémon
|
||||
* who is summoned into the trap, based on the Rock type's type effectiveness.
|
||||
* Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Sticky_Web_(move) | Sticky Web}.
|
||||
* Applies a single-layer trap that lowers the Speed of all grounded Pokémon switching in.
|
||||
*/
|
||||
class StealthRockTag extends ArenaTrapTag {
|
||||
public readonly tagType = ArenaTagType.STEALTH_ROCK;
|
||||
class StickyWebTag extends EntryHazardTag {
|
||||
public readonly tagType = ArenaTagType.STICKY_WEB;
|
||||
public override get maxLayers() {
|
||||
return 1 as const;
|
||||
}
|
||||
|
||||
constructor(sourceId: number | undefined, side: ArenaTagSide) {
|
||||
super(MoveId.STEALTH_ROCK, sourceId, side, 1);
|
||||
super(MoveId.STICKY_WEB, sourceId, side);
|
||||
}
|
||||
|
||||
onAdd(arena: Arena, quiet = false): void {
|
||||
super.onAdd(arena);
|
||||
|
||||
if (quiet) {
|
||||
return;
|
||||
}
|
||||
|
||||
const source = this.getSourcePokemon();
|
||||
if (!quiet && source) {
|
||||
globalScene.phaseManager.queueMessage(
|
||||
i18next.t("arenaTag:stealthRockOnAdd", {
|
||||
opponentDesc: source.getOpponentDescriptor(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
getDamageHpRatio(pokemon: Pokemon): number {
|
||||
const effectiveness = pokemon.getAttackTypeEffectiveness(PokemonType.ROCK, undefined, true);
|
||||
|
||||
let damageHpRatio = 0;
|
||||
|
||||
switch (effectiveness) {
|
||||
case 0:
|
||||
damageHpRatio = 0;
|
||||
break;
|
||||
case 0.25:
|
||||
damageHpRatio = 0.03125;
|
||||
break;
|
||||
case 0.5:
|
||||
damageHpRatio = 0.0625;
|
||||
break;
|
||||
case 1:
|
||||
damageHpRatio = 0.125;
|
||||
break;
|
||||
case 2:
|
||||
damageHpRatio = 0.25;
|
||||
break;
|
||||
case 4:
|
||||
damageHpRatio = 0.5;
|
||||
break;
|
||||
}
|
||||
|
||||
return damageHpRatio;
|
||||
protected override getAddMessage(source: Pokemon): string {
|
||||
return i18next.t("arenaTag:stickyWebOnAdd", {
|
||||
moveName: this.getMoveName(),
|
||||
opponentDesc: source.getOpponentDescriptor(),
|
||||
});
|
||||
}
|
||||
|
||||
override activateTrap(pokemon: Pokemon, simulated: boolean): boolean {
|
||||
const cancelled = new BooleanHolder(false);
|
||||
applyAbAttrs("BlockNonDirectDamageAbAttr", { pokemon, cancelled });
|
||||
if (cancelled.value) {
|
||||
return false;
|
||||
}
|
||||
// TODO: Does this need to pass `simulated` as a parameter?
|
||||
applyAbAttrs("ProtectStatAbAttr", {
|
||||
pokemon,
|
||||
cancelled,
|
||||
stat: Stat.SPD,
|
||||
stages: -1,
|
||||
});
|
||||
|
||||
const damageHpRatio = this.getDamageHpRatio(pokemon);
|
||||
if (!damageHpRatio) {
|
||||
if (cancelled.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -1002,95 +1061,112 @@ class StealthRockTag extends ArenaTrapTag {
|
||||
return true;
|
||||
}
|
||||
|
||||
const damage = toDmgValue(pokemon.getMaxHp() * damageHpRatio);
|
||||
globalScene.phaseManager.queueMessage(
|
||||
i18next.t("arenaTag:stealthRockActivateTrap", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||
i18next.t("arenaTag:stickyWebActivateTrap", {
|
||||
pokemonName: pokemon.getNameToRender(),
|
||||
}),
|
||||
);
|
||||
pokemon.damageAndUpdate(damage, { result: HitResult.INDIRECT });
|
||||
pokemon.turnData.damageTaken += damage;
|
||||
return true;
|
||||
}
|
||||
|
||||
getMatchupScoreMultiplier(pokemon: Pokemon): number {
|
||||
const damageHpRatio = this.getDamageHpRatio(pokemon);
|
||||
return Phaser.Math.Linear(super.getMatchupScoreMultiplier(pokemon), 1, 1 - Math.pow(damageHpRatio, damageHpRatio));
|
||||
globalScene.phaseManager.unshiftNew(
|
||||
"StatStageChangePhase",
|
||||
pokemon.getBattlerIndex(),
|
||||
false,
|
||||
[Stat.SPD],
|
||||
-1,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
null,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Sticky_Web_(move) Sticky Web}.
|
||||
* Applies up to 1 layer of Sticky Web, which lowers the Speed by one stage
|
||||
* to any Pokémon who is summoned into this trap.
|
||||
* This arena tag facilitates the application of the move Imprison
|
||||
* Imprison remains in effect as long as the source Pokemon is active and present on the field.
|
||||
* Imprison will apply to any opposing Pokemon that switch onto the field as well.
|
||||
*/
|
||||
class StickyWebTag extends ArenaTrapTag {
|
||||
public readonly tagType = ArenaTagType.STICKY_WEB;
|
||||
class ImprisonTag extends EntryHazardTag {
|
||||
public readonly tagType = ArenaTagType.IMPRISON;
|
||||
public override get maxLayers() {
|
||||
return 1 as const;
|
||||
}
|
||||
|
||||
constructor(sourceId: number | undefined, side: ArenaTagSide) {
|
||||
super(MoveId.STICKY_WEB, sourceId, side, 1);
|
||||
super(MoveId.IMPRISON, sourceId, side);
|
||||
}
|
||||
|
||||
onAdd(arena: Arena, quiet = false): void {
|
||||
super.onAdd(arena);
|
||||
/**
|
||||
* Apply the effects of Imprison to all opposing on-field Pokemon.
|
||||
*/
|
||||
override onAdd(_arena: Arena, quiet = false) {
|
||||
super.onAdd(_arena, quiet);
|
||||
|
||||
// We assume `quiet=true` means "just add the bloody tag no questions asked"
|
||||
if (quiet) {
|
||||
return;
|
||||
}
|
||||
const party = this.getAffectedPokemon();
|
||||
party.forEach(p => {
|
||||
if (p.isAllowedInBattle()) {
|
||||
p.addTag(BattlerTagType.IMPRISON, 1, MoveId.IMPRISON, this.sourceId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected override getAddMessage(source: Pokemon): string {
|
||||
return i18next.t("battlerTags:imprisonOnAdd", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(source),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the source Pokemon is still active on the field
|
||||
* @param _arena
|
||||
* @returns `true` if the source of the tag is still active on the field | `false` if not
|
||||
*/
|
||||
override lapse(): boolean {
|
||||
const source = this.getSourcePokemon();
|
||||
if (!source) {
|
||||
console.warn(`Failed to get source Pokemon for SpikesTag on add message; id: ${this.sourceId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
globalScene.phaseManager.queueMessage(
|
||||
i18next.t("arenaTag:stickyWebOnAdd", {
|
||||
moveName: this.getMoveName(),
|
||||
opponentDesc: source.getOpponentDescriptor(),
|
||||
}),
|
||||
);
|
||||
return !!source?.isActive(true);
|
||||
}
|
||||
|
||||
override activateTrap(pokemon: Pokemon, simulated: boolean): boolean {
|
||||
if (pokemon.isGrounded()) {
|
||||
const cancelled = new BooleanHolder(false);
|
||||
applyAbAttrs("ProtectStatAbAttr", {
|
||||
pokemon,
|
||||
cancelled,
|
||||
stat: Stat.SPD,
|
||||
stages: -1,
|
||||
});
|
||||
|
||||
if (simulated) {
|
||||
return !cancelled.value;
|
||||
}
|
||||
|
||||
if (!cancelled.value) {
|
||||
globalScene.phaseManager.queueMessage(
|
||||
i18next.t("arenaTag:stickyWebActivateTrap", {
|
||||
pokemonName: pokemon.getNameToRender(),
|
||||
}),
|
||||
);
|
||||
const stages = new NumberHolder(-1);
|
||||
globalScene.phaseManager.unshiftNew(
|
||||
"StatStageChangePhase",
|
||||
pokemon.getBattlerIndex(),
|
||||
false,
|
||||
[Stat.SPD],
|
||||
stages.value,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
null,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* This applies the effects of Imprison to any opposing Pokemon that switch into the field while the source Pokemon is still active
|
||||
* @param {Pokemon} pokemon the Pokemon Imprison is applied to
|
||||
* @returns `true`
|
||||
*/
|
||||
override activateTrap(pokemon: Pokemon): boolean {
|
||||
const source = this.getSourcePokemon();
|
||||
if (source?.isActive(true) && pokemon.isAllowedInBattle()) {
|
||||
pokemon.addTag(BattlerTagType.IMPRISON, 1, MoveId.IMPRISON, this.sourceId);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
/**
|
||||
* When the arena tag is removed, it also attempts to remove any related Battler Tags if they haven't already been removed from the affected Pokemon
|
||||
* @param arena
|
||||
*/
|
||||
override onRemove(): void {
|
||||
const party = this.getAffectedPokemon();
|
||||
party.forEach(p => {
|
||||
p.removeTag(BattlerTagType.IMPRISON);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract base class for all Room {@linkcode ArenaTag}s, characterized by their immediate removal
|
||||
* upon overlap.
|
||||
*/
|
||||
abstract class RoomArenaTag extends SerializableArenaTag {
|
||||
declare abstract tagType: RoomArenaTagType;
|
||||
|
||||
/**
|
||||
* Immediately remove this Tag upon overlapping.
|
||||
* @sealed
|
||||
*/
|
||||
override onOverlap(): void {
|
||||
globalScene.arena.removeTagOnSide(this.tagType, this.side);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1099,7 +1175,7 @@ class StickyWebTag extends ArenaTrapTag {
|
||||
* Reverses the Speed stats for all Pokémon on the field as long as this arena tag is up,
|
||||
* also reversing the turn order for all Pokémon on the field as well.
|
||||
*/
|
||||
export class TrickRoomTag extends SerializableArenaTag {
|
||||
export class TrickRoomTag extends RoomArenaTag {
|
||||
public readonly tagType = ArenaTagType.TRICK_ROOM;
|
||||
constructor(turnCount: number, sourceId?: number) {
|
||||
super(turnCount, MoveId.TRICK_ROOM, sourceId);
|
||||
@ -1129,7 +1205,7 @@ export class TrickRoomTag extends SerializableArenaTag {
|
||||
|
||||
globalScene.phaseManager.queueMessage(
|
||||
i18next.t("arenaTag:trickRoomOnAdd", {
|
||||
moveName: this.getMoveName(),
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(source),
|
||||
}),
|
||||
);
|
||||
}
|
||||
@ -1287,75 +1363,6 @@ class NoneTag extends ArenaTag {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This arena tag facilitates the application of the move Imprison
|
||||
* Imprison remains in effect as long as the source Pokemon is active and present on the field.
|
||||
* Imprison will apply to any opposing Pokemon that switch onto the field as well.
|
||||
*/
|
||||
class ImprisonTag extends ArenaTrapTag {
|
||||
public readonly tagType = ArenaTagType.IMPRISON;
|
||||
constructor(sourceId: number | undefined, side: ArenaTagSide) {
|
||||
super(MoveId.IMPRISON, sourceId, side, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the effects of Imprison to all opposing on-field Pokemon.
|
||||
*/
|
||||
override onAdd() {
|
||||
const source = this.getSourcePokemon();
|
||||
if (!source) {
|
||||
return;
|
||||
}
|
||||
|
||||
const party = this.getAffectedPokemon();
|
||||
party.forEach(p => {
|
||||
if (p.isAllowedInBattle()) {
|
||||
p.addTag(BattlerTagType.IMPRISON, 1, MoveId.IMPRISON, this.sourceId);
|
||||
}
|
||||
});
|
||||
|
||||
globalScene.phaseManager.queueMessage(
|
||||
i18next.t("battlerTags:imprisonOnAdd", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(source),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the source Pokemon is still active on the field
|
||||
* @param _arena
|
||||
* @returns `true` if the source of the tag is still active on the field | `false` if not
|
||||
*/
|
||||
override lapse(): boolean {
|
||||
const source = this.getSourcePokemon();
|
||||
return !!source?.isActive(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* This applies the effects of Imprison to any opposing Pokemon that switch into the field while the source Pokemon is still active
|
||||
* @param {Pokemon} pokemon the Pokemon Imprison is applied to
|
||||
* @returns `true`
|
||||
*/
|
||||
override activateTrap(pokemon: Pokemon): boolean {
|
||||
const source = this.getSourcePokemon();
|
||||
if (source?.isActive(true) && pokemon.isAllowedInBattle()) {
|
||||
pokemon.addTag(BattlerTagType.IMPRISON, 1, MoveId.IMPRISON, this.sourceId);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* When the arena tag is removed, it also attempts to remove any related Battler Tags if they haven't already been removed from the affected Pokemon
|
||||
* @param arena
|
||||
*/
|
||||
override onRemove(): void {
|
||||
const party = this.getAffectedPokemon();
|
||||
party.forEach(p => {
|
||||
p.removeTag(BattlerTagType.IMPRISON);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Arena Tag implementing the "sea of fire" effect from the combination
|
||||
* of {@link https://bulbapedia.bulbagarden.net/wiki/Fire_Pledge_(move) | Fire Pledge}
|
||||
|
@ -6,7 +6,7 @@ import { toTitleCase } from "#utils/strings";
|
||||
|
||||
export const speciesEggMoves = {
|
||||
[SpeciesId.BULBASAUR]: [ MoveId.SAPPY_SEED, MoveId.MALIGNANT_CHAIN, MoveId.EARTH_POWER, MoveId.MATCHA_GOTCHA ],
|
||||
[SpeciesId.CHARMANDER]: [ MoveId.DRAGON_DANCE, MoveId.BITTER_BLADE, MoveId.EARTH_POWER, MoveId.OBLIVION_WING ],
|
||||
[SpeciesId.CHARMANDER]: [ MoveId.DRAGON_DANCE, MoveId.AEROBLAST, MoveId.EARTH_POWER, MoveId.BITTER_BLADE ],
|
||||
[SpeciesId.SQUIRTLE]: [ MoveId.FREEZE_DRY, MoveId.ARMOR_CANNON, MoveId.SHORE_UP, MoveId.ORIGIN_PULSE ],
|
||||
[SpeciesId.CATERPIE]: [ MoveId.SANDSEAR_STORM, MoveId.SILK_TRAP, MoveId.TWIN_BEAM, MoveId.BLEAKWIND_STORM ],
|
||||
[SpeciesId.WEEDLE]: [ MoveId.THOUSAND_ARROWS, MoveId.NOXIOUS_TORQUE, MoveId.ATTACK_ORDER, MoveId.VICTORY_DANCE ],
|
||||
@ -29,7 +29,7 @@ export const speciesEggMoves = {
|
||||
[SpeciesId.GROWLITHE]: [ MoveId.ZING_ZAP, MoveId.DRAGON_DANCE, MoveId.MORNING_SUN, MoveId.SACRED_FIRE ],
|
||||
[SpeciesId.POLIWAG]: [ MoveId.SLACK_OFF, MoveId.WILDBOLT_STORM, MoveId.DRAIN_PUNCH, MoveId.SURGING_STRIKES ],
|
||||
[SpeciesId.ABRA]: [ MoveId.AURA_SPHERE, MoveId.BADDY_BAD, MoveId.ICE_BEAM, MoveId.PSYSTRIKE ],
|
||||
[SpeciesId.MACHOP]: [ MoveId.COMBAT_TORQUE, MoveId.METEOR_MASH, MoveId.MOUNTAIN_GALE, MoveId.FISSURE ],
|
||||
[SpeciesId.MACHOP]: [ MoveId.DRAIN_PUNCH, MoveId.METEOR_MASH, MoveId.MOUNTAIN_GALE, MoveId.FISSURE ],
|
||||
[SpeciesId.BELLSPROUT]: [ MoveId.SOLAR_BLADE, MoveId.STRENGTH_SAP, MoveId.FIRE_LASH, MoveId.VICTORY_DANCE ],
|
||||
[SpeciesId.TENTACOOL]: [ MoveId.BANEFUL_BUNKER, MoveId.MALIGNANT_CHAIN, MoveId.BOUNCY_BUBBLE, MoveId.STRENGTH_SAP ],
|
||||
[SpeciesId.GEODUDE]: [ MoveId.FLARE_BLITZ, MoveId.HEAD_SMASH, MoveId.SHORE_UP, MoveId.SHELL_SMASH ],
|
||||
@ -142,7 +142,7 @@ export const speciesEggMoves = {
|
||||
[SpeciesId.RALTS]: [ MoveId.PSYBLADE, MoveId.BITTER_BLADE, MoveId.NO_RETREAT, MoveId.BOOMBURST ],
|
||||
[SpeciesId.SURSKIT]: [ MoveId.POLLEN_PUFF, MoveId.FIERY_DANCE, MoveId.BOUNCY_BUBBLE, MoveId.AEROBLAST ],
|
||||
[SpeciesId.SHROOMISH]: [ MoveId.ACCELEROCK, MoveId.TRAILBLAZE, MoveId.STORM_THROW, MoveId.SAPPY_SEED ],
|
||||
[SpeciesId.SLAKOTH]: [ MoveId.CRUSH_GRIP, MoveId.DRAIN_PUNCH, MoveId.PARTING_SHOT, MoveId.SKILL_SWAP ],
|
||||
[SpeciesId.SLAKOTH]: [ MoveId.ENTRAINMENT, MoveId.DRAIN_PUNCH, MoveId.PARTING_SHOT, MoveId.CRUSH_GRIP ],
|
||||
[SpeciesId.NINCADA]: [ MoveId.BULLDOZE, MoveId.STICKY_WEB, MoveId.SHADOW_BONE, MoveId.SHELL_SMASH ],
|
||||
[SpeciesId.WHISMUR]: [ MoveId.ALLURING_VOICE, MoveId.SHIFT_GEAR, MoveId.SPARKLING_ARIA, MoveId.TORCH_SONG ],
|
||||
[SpeciesId.MAKUHITA]: [ MoveId.COMBAT_TORQUE, MoveId.SLACK_OFF, MoveId.HEAT_CRASH, MoveId.DOUBLE_IRON_BASH ],
|
||||
@ -187,7 +187,7 @@ export const speciesEggMoves = {
|
||||
[SpeciesId.WYNAUT]: [ MoveId.RECOVER, MoveId.SHED_TAIL, MoveId.TAUNT, MoveId.COMEUPPANCE ],
|
||||
[SpeciesId.SNORUNT]: [ MoveId.SPARKLY_SWIRL, MoveId.NASTY_PLOT, MoveId.EARTH_POWER, MoveId.BLOOD_MOON ],
|
||||
[SpeciesId.SPHEAL]: [ MoveId.FLIP_TURN, MoveId.FREEZE_DRY, MoveId.SLACK_OFF, MoveId.STEAM_ERUPTION ],
|
||||
[SpeciesId.CLAMPERL]: [ MoveId.SHELL_SIDE_ARM, MoveId.BOUNCY_BUBBLE, MoveId.FREEZE_DRY, MoveId.STEAM_ERUPTION ],
|
||||
[SpeciesId.CLAMPERL]: [ MoveId.SHELL_SIDE_ARM, MoveId.SNIPE_SHOT, MoveId.GIGA_DRAIN, MoveId.BOUNCY_BUBBLE ],
|
||||
[SpeciesId.RELICANTH]: [ MoveId.DRAGON_DANCE, MoveId.SHORE_UP, MoveId.WAVE_CRASH, MoveId.DIAMOND_STORM ],
|
||||
[SpeciesId.LUVDISC]: [ MoveId.BATON_PASS, MoveId.HEART_SWAP, MoveId.GLITZY_GLOW, MoveId.REVIVAL_BLESSING ],
|
||||
[SpeciesId.BAGON]: [ MoveId.HEADLONG_RUSH, MoveId.FIRE_LASH, MoveId.DRAGON_DANCE, MoveId.DRAGON_DARTS ],
|
||||
@ -229,7 +229,7 @@ export const speciesEggMoves = {
|
||||
[SpeciesId.MIME_JR]: [ MoveId.CHILLY_RECEPTION, MoveId.MOONBLAST, MoveId.FROST_BREATH, MoveId.LUMINA_CRASH ],
|
||||
[SpeciesId.HAPPINY]: [ MoveId.COTTON_GUARD, MoveId.SEISMIC_TOSS, MoveId.SIZZLY_SLIDE, MoveId.REVIVAL_BLESSING ],
|
||||
[SpeciesId.CHATOT]: [ MoveId.SPARKLING_ARIA, MoveId.BOOMBURST, MoveId.BATON_PASS, MoveId.TORCH_SONG ],
|
||||
[SpeciesId.SPIRITOMB]: [ MoveId.PARTING_SHOT, MoveId.BADDY_BAD, MoveId.STRENGTH_SAP, MoveId.SPECTRAL_THIEF ],
|
||||
[SpeciesId.SPIRITOMB]: [ MoveId.PARTING_SHOT, MoveId.BADDY_BAD, MoveId.BITTER_MALICE, MoveId.STRENGTH_SAP ],
|
||||
[SpeciesId.GIBLE]: [ MoveId.METEOR_MASH, MoveId.BITTER_BLADE, MoveId.LANDS_WRATH, MoveId.DRAGON_DANCE ],
|
||||
[SpeciesId.MUNCHLAX]: [ MoveId.STUFF_CHEEKS, MoveId.GRAV_APPLE, MoveId.SLACK_OFF, MoveId.EXTREME_SPEED ],
|
||||
[SpeciesId.RIOLU]: [ MoveId.THUNDEROUS_KICK, MoveId.TACHYON_CUTTER, MoveId.TRIPLE_AXEL, MoveId.SUNSTEEL_STRIKE ],
|
||||
@ -247,7 +247,7 @@ export const speciesEggMoves = {
|
||||
[SpeciesId.DIALGA]: [ MoveId.CORE_ENFORCER, MoveId.TAKE_HEART, MoveId.RECOVER, MoveId.MAKE_IT_RAIN ],
|
||||
[SpeciesId.PALKIA]: [ MoveId.MALIGNANT_CHAIN, MoveId.TAKE_HEART, MoveId.RECOVER, MoveId.ORIGIN_PULSE ],
|
||||
[SpeciesId.HEATRAN]: [ MoveId.ENERGY_BALL, MoveId.RECOVER, MoveId.ERUPTION, MoveId.TACHYON_CUTTER ],
|
||||
[SpeciesId.REGIGIGAS]: [ MoveId.SKILL_SWAP, MoveId.RECOVER, MoveId.EXTREME_SPEED, MoveId.GIGATON_HAMMER ],
|
||||
[SpeciesId.REGIGIGAS]: [ MoveId.OBSTRUCT, MoveId.RECOVER, MoveId.STORM_THROW, MoveId.LANDS_WRATH ],
|
||||
[SpeciesId.GIRATINA]: [ MoveId.DRAGON_DANCE, MoveId.SPECTRAL_THIEF, MoveId.RECOVER, MoveId.COLLISION_COURSE ],
|
||||
[SpeciesId.CRESSELIA]: [ MoveId.COSMIC_POWER, MoveId.BODY_PRESS, MoveId.SIZZLY_SLIDE, MoveId.LUMINA_CRASH ],
|
||||
[SpeciesId.PHIONE]: [ MoveId.BOUNCY_BUBBLE, MoveId.FREEZE_DRY, MoveId.STORED_POWER, MoveId.ORIGIN_PULSE ],
|
||||
@ -318,7 +318,7 @@ export const speciesEggMoves = {
|
||||
[SpeciesId.MIENFOO]: [ MoveId.GUNK_SHOT, MoveId.SUPERCELL_SLAM, MoveId.MOUNTAIN_GALE, MoveId.WICKED_BLOW ],
|
||||
[SpeciesId.DRUDDIGON]: [ MoveId.FIRE_LASH, MoveId.MORNING_SUN, MoveId.DRAGON_DARTS, MoveId.CLANGOROUS_SOUL ],
|
||||
[SpeciesId.GOLETT]: [ MoveId.SHIFT_GEAR, MoveId.DRAIN_PUNCH, MoveId.HEADLONG_RUSH, MoveId.RAGE_FIST ],
|
||||
[SpeciesId.PAWNIARD]: [ MoveId.SUCKER_PUNCH, MoveId.CEASELESS_EDGE, MoveId.BITTER_BLADE, MoveId.LAST_RESPECTS ],
|
||||
[SpeciesId.PAWNIARD]: [ MoveId.SUCKER_PUNCH, MoveId.SPIRIT_BREAK, MoveId.LAST_RESPECTS, MoveId.BITTER_BLADE ],
|
||||
[SpeciesId.BOUFFALANT]: [ MoveId.HORN_LEECH, MoveId.HIGH_JUMP_KICK, MoveId.HEAD_SMASH, MoveId.FLARE_BLITZ ],
|
||||
[SpeciesId.RUFFLET]: [ MoveId.FLOATY_FALL, MoveId.AURA_SPHERE, MoveId.NO_RETREAT, MoveId.BOLT_BEAK ],
|
||||
[SpeciesId.VULLABY]: [ MoveId.FOUL_PLAY, MoveId.BODY_PRESS, MoveId.ROOST, MoveId.RUINATION ],
|
||||
@ -333,7 +333,7 @@ export const speciesEggMoves = {
|
||||
[SpeciesId.THUNDURUS]: [ MoveId.SANDSEAR_STORM, MoveId.HURRICANE, MoveId.FROST_BREATH, MoveId.ELECTRO_SHOT ],
|
||||
[SpeciesId.RESHIRAM]: [ MoveId.ENERGY_BALL, MoveId.TAKE_HEART, MoveId.FICKLE_BEAM, MoveId.ERUPTION ],
|
||||
[SpeciesId.ZEKROM]: [ MoveId.TRIPLE_AXEL, MoveId.THUNDEROUS_KICK, MoveId.DRAGON_HAMMER, MoveId.DRAGON_ENERGY ],
|
||||
[SpeciesId.LANDORUS]: [ MoveId.STONE_AXE, MoveId.FLOATY_FALL, MoveId.ROOST, MoveId.BLEAKWIND_STORM ],
|
||||
[SpeciesId.LANDORUS]: [ MoveId.STONE_AXE, MoveId.FLOATY_FALL, MoveId.ROOST, MoveId.THOUSAND_WAVES ],
|
||||
[SpeciesId.KYUREM]: [ MoveId.DRAGON_DARTS, MoveId.GLACIAL_LANCE, MoveId.NO_RETREAT, MoveId.DRAGON_ENERGY ],
|
||||
[SpeciesId.KELDEO]: [ MoveId.BOUNCY_BUBBLE, MoveId.THUNDERBOLT, MoveId.ICE_BEAM, MoveId.STEAM_ERUPTION ],
|
||||
[SpeciesId.MELOETTA]: [ MoveId.BODY_SLAM, MoveId.PSYCHIC_NOISE, MoveId.TRIPLE_ARROWS, MoveId.TORCH_SONG ],
|
||||
@ -351,7 +351,7 @@ export const speciesEggMoves = {
|
||||
[SpeciesId.PANCHAM]: [ MoveId.DRAIN_PUNCH, MoveId.SUCKER_PUNCH, MoveId.METEOR_MASH, MoveId.WICKED_BLOW ],
|
||||
[SpeciesId.FURFROU]: [ MoveId.TIDY_UP, MoveId.SLACK_OFF, MoveId.COMBAT_TORQUE, MoveId.MULTI_ATTACK ],
|
||||
[SpeciesId.ESPURR]: [ MoveId.LUSTER_PURGE, MoveId.MOONBLAST, MoveId.AURA_SPHERE, MoveId.DARK_VOID ],
|
||||
[SpeciesId.HONEDGE]: [ MoveId.TACHYON_CUTTER, MoveId.SHADOW_BONE, MoveId.BITTER_BLADE, MoveId.BEHEMOTH_BLADE ],
|
||||
[SpeciesId.HONEDGE]: [ MoveId.TACHYON_CUTTER, MoveId.SPIRIT_SHACKLE, MoveId.BITTER_BLADE, MoveId.BEHEMOTH_BLADE ],
|
||||
[SpeciesId.SPRITZEE]: [ MoveId.SPEED_SWAP, MoveId.REVIVAL_BLESSING, MoveId.ROOST, MoveId.TORCH_SONG ],
|
||||
[SpeciesId.SWIRLIX]: [ MoveId.BELLY_DRUM, MoveId.HEADLONG_RUSH, MoveId.MAGICAL_TORQUE, MoveId.REVIVAL_BLESSING ],
|
||||
[SpeciesId.INKAY]: [ MoveId.POWER_TRIP, MoveId.SPIN_OUT, MoveId.RECOVER, MoveId.PSYCHO_BOOST ],
|
||||
@ -429,7 +429,7 @@ export const speciesEggMoves = {
|
||||
[SpeciesId.MAGEARNA]: [ MoveId.STRENGTH_SAP, MoveId.EARTH_POWER, MoveId.MOONBLAST, MoveId.MAKE_IT_RAIN ],
|
||||
[SpeciesId.MARSHADOW]: [ MoveId.POWER_UP_PUNCH, MoveId.BONEMERANG, MoveId.METEOR_MASH, MoveId.TRIPLE_AXEL ],
|
||||
[SpeciesId.POIPOLE]: [ MoveId.MALIGNANT_CHAIN, MoveId.ICE_BEAM, MoveId.ARMOR_CANNON, MoveId.CLANGING_SCALES ],
|
||||
[SpeciesId.STAKATAKA]: [ MoveId.HEAVY_SLAM, MoveId.SHORE_UP, MoveId.CURSE, MoveId.SALT_CURE ],
|
||||
[SpeciesId.STAKATAKA]: [ MoveId.HEAVY_SLAM, MoveId.HEAL_ORDER, MoveId.CURSE, MoveId.SALT_CURE ],
|
||||
[SpeciesId.BLACEPHALON]: [ MoveId.STEEL_BEAM, MoveId.MOONBLAST, MoveId.CHLOROBLAST, MoveId.MOONGEIST_BEAM ],
|
||||
[SpeciesId.ZERAORA]: [ MoveId.SWORDS_DANCE, MoveId.FIRE_LASH, MoveId.COLLISION_COURSE, MoveId.TRIPLE_AXEL ],
|
||||
[SpeciesId.MELTAN]: [ MoveId.BULLET_PUNCH, MoveId.DRAIN_PUNCH, MoveId.BULK_UP, MoveId.PLASMA_FISTS ],
|
||||
@ -482,7 +482,7 @@ export const speciesEggMoves = {
|
||||
[SpeciesId.ZAMAZENTA]: [ MoveId.BULK_UP, MoveId.BODY_PRESS, MoveId.POWER_TRIP, MoveId.SLACK_OFF ],
|
||||
[SpeciesId.ETERNATUS]: [ MoveId.BODY_PRESS, MoveId.NASTY_PLOT, MoveId.MALIGNANT_CHAIN, MoveId.DRAGON_ENERGY ],
|
||||
[SpeciesId.KUBFU]: [ MoveId.METEOR_MASH, MoveId.DRAIN_PUNCH, MoveId.JET_PUNCH, MoveId.DRAGON_DANCE ],
|
||||
[SpeciesId.ZARUDE]: [ MoveId.SAPPY_SEED, MoveId.MIGHTY_CLEAVE, MoveId.WICKED_BLOW, MoveId.VICTORY_DANCE ],
|
||||
[SpeciesId.ZARUDE]: [ MoveId.LEAF_BLADE, MoveId.HEADLONG_RUSH, MoveId.WICKED_BLOW, MoveId.VICTORY_DANCE ],
|
||||
[SpeciesId.REGIELEKI]: [ MoveId.NASTY_PLOT, MoveId.ICE_BEAM, MoveId.EARTH_POWER, MoveId.ELECTRO_DRIFT ],
|
||||
[SpeciesId.REGIDRAGO]: [ MoveId.SHELL_SIDE_ARM, MoveId.FLAMETHROWER, MoveId.TAKE_HEART, MoveId.DRAGON_DARTS ],
|
||||
[SpeciesId.GLASTRIER]: [ MoveId.SPEED_SWAP, MoveId.SLACK_OFF, MoveId.HIGH_HORSEPOWER, MoveId.GLACIAL_LANCE ],
|
||||
|
@ -208,7 +208,7 @@ export const starterPassiveAbilities: StarterPassiveAbilities = {
|
||||
[SpeciesId.XATU]: { 0: AbilityId.SHEER_FORCE },
|
||||
[SpeciesId.MAREEP]: { 0: AbilityId.ELECTROMORPHOSIS },
|
||||
[SpeciesId.FLAAFFY]: { 0: AbilityId.ELECTROMORPHOSIS },
|
||||
[SpeciesId.AMPHAROS]: { 0: AbilityId.ELECTROMORPHOSIS, 1: AbilityId.ELECTROMORPHOSIS },
|
||||
[SpeciesId.AMPHAROS]: { 0: AbilityId.ELECTROMORPHOSIS, 1: AbilityId.FLUFFY },
|
||||
[SpeciesId.HOPPIP]: { 0: AbilityId.WIND_RIDER },
|
||||
[SpeciesId.SKIPLOOM]: { 0: AbilityId.WIND_RIDER },
|
||||
[SpeciesId.JUMPLUFF]: { 0: AbilityId.FLUFFY },
|
||||
@ -402,7 +402,7 @@ export const starterPassiveAbilities: StarterPassiveAbilities = {
|
||||
[SpeciesId.SPHEAL]: { 0: AbilityId.UNAWARE },
|
||||
[SpeciesId.SEALEO]: { 0: AbilityId.UNAWARE },
|
||||
[SpeciesId.WALREIN]: { 0: AbilityId.UNAWARE },
|
||||
[SpeciesId.CLAMPERL]: { 0: AbilityId.DAUNTLESS_SHIELD },
|
||||
[SpeciesId.CLAMPERL]: { 0: AbilityId.OVERCOAT },
|
||||
[SpeciesId.GOREBYSS]: { 0: AbilityId.ARENA_TRAP },
|
||||
[SpeciesId.HUNTAIL]: { 0: AbilityId.ARENA_TRAP },
|
||||
[SpeciesId.RELICANTH]: { 0: AbilityId.PRIMORDIAL_SEA },
|
||||
@ -416,8 +416,8 @@ export const starterPassiveAbilities: StarterPassiveAbilities = {
|
||||
[SpeciesId.REGIROCK]: { 0: AbilityId.SAND_STREAM },
|
||||
[SpeciesId.REGICE]: { 0: AbilityId.SNOW_WARNING },
|
||||
[SpeciesId.REGISTEEL]: { 0: AbilityId.STEELY_SPIRIT },
|
||||
[SpeciesId.LATIAS]: { 0: AbilityId.SPEED_BOOST, 1: AbilityId.PRISM_ARMOR },
|
||||
[SpeciesId.LATIOS]: { 0: AbilityId.SPEED_BOOST, 1: AbilityId.TINTED_LENS },
|
||||
[SpeciesId.LATIAS]: { 0: AbilityId.PRISM_ARMOR, 1: AbilityId.PRISM_ARMOR },
|
||||
[SpeciesId.LATIOS]: { 0: AbilityId.NEUROFORCE, 1: AbilityId.NEUROFORCE },
|
||||
[SpeciesId.KYOGRE]: { 0: AbilityId.MOLD_BREAKER, 1: AbilityId.TERAVOLT },
|
||||
[SpeciesId.GROUDON]: { 0: AbilityId.MOLD_BREAKER, 1: AbilityId.TURBOBLAZE },
|
||||
[SpeciesId.RAYQUAZA]: { 0: AbilityId.UNNERVE, 1: AbilityId.UNNERVE },
|
||||
@ -521,7 +521,7 @@ export const starterPassiveAbilities: StarterPassiveAbilities = {
|
||||
[SpeciesId.SHAYMIN]: { 0: AbilityId.GRASSY_SURGE, 1: AbilityId.DELTA_STREAM },
|
||||
[SpeciesId.ARCEUS]: { 0: AbilityId.ADAPTABILITY, 1: AbilityId.ADAPTABILITY, 2: AbilityId.ADAPTABILITY, 3: AbilityId.ADAPTABILITY, 4: AbilityId.ADAPTABILITY, 5: AbilityId.ADAPTABILITY, 6: AbilityId.ADAPTABILITY, 7: AbilityId.ADAPTABILITY, 8: AbilityId.ADAPTABILITY, 9: AbilityId.ADAPTABILITY, 10: AbilityId.ADAPTABILITY, 11: AbilityId.ADAPTABILITY, 12: AbilityId.ADAPTABILITY, 13: AbilityId.ADAPTABILITY, 14: AbilityId.ADAPTABILITY, 15: AbilityId.ADAPTABILITY, 16: AbilityId.ADAPTABILITY, 17: AbilityId.ADAPTABILITY },
|
||||
|
||||
[SpeciesId.VICTINI]: { 0: AbilityId.SHEER_FORCE },
|
||||
[SpeciesId.VICTINI]: { 0: AbilityId.SERENE_GRACE },
|
||||
[SpeciesId.SNIVY]: { 0: AbilityId.MULTISCALE },
|
||||
[SpeciesId.SERVINE]: { 0: AbilityId.MULTISCALE },
|
||||
[SpeciesId.SERPERIOR]: { 0: AbilityId.MULTISCALE },
|
||||
@ -585,7 +585,7 @@ export const starterPassiveAbilities: StarterPassiveAbilities = {
|
||||
[SpeciesId.KROKOROK]: { 0: AbilityId.TOUGH_CLAWS },
|
||||
[SpeciesId.KROOKODILE]: { 0: AbilityId.TOUGH_CLAWS },
|
||||
[SpeciesId.DARUMAKA]: { 0: AbilityId.GORILLA_TACTICS },
|
||||
[SpeciesId.DARMANITAN]: { 0: AbilityId.GORILLA_TACTICS, 1: AbilityId.SOLID_ROCK },
|
||||
[SpeciesId.DARMANITAN]: { 0: AbilityId.GORILLA_TACTICS, 1: AbilityId.PSYCHIC_SURGE },
|
||||
[SpeciesId.MARACTUS]: { 0: AbilityId.WELL_BAKED_BODY },
|
||||
[SpeciesId.DWEBBLE]: { 0: AbilityId.ROCKY_PAYLOAD },
|
||||
[SpeciesId.CRUSTLE]: { 0: AbilityId.ROCKY_PAYLOAD },
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { defaultStarterSpecies } from "#app/constants";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import { speciesStarterCosts } from "#balance/starters";
|
||||
import { allMoves } from "#data/data-lists";
|
||||
@ -76,7 +77,8 @@ export enum EvolutionItem {
|
||||
LEADERS_CREST
|
||||
}
|
||||
|
||||
type TyrogueMove = MoveId.LOW_SWEEP | MoveId.MACH_PUNCH | MoveId.RAPID_SPIN;
|
||||
const tyrogueMoves = [MoveId.LOW_SWEEP, MoveId.MACH_PUNCH, MoveId.RAPID_SPIN] as const;
|
||||
type TyrogueMove = typeof tyrogueMoves[number];
|
||||
|
||||
/**
|
||||
* Pokemon Evolution tuple type consisting of:
|
||||
@ -191,7 +193,7 @@ export class SpeciesEvolutionCondition {
|
||||
case EvoCondKey.WEATHER:
|
||||
return cond.weather.includes(globalScene.arena.getWeatherType());
|
||||
case EvoCondKey.TYROGUE:
|
||||
return pokemon.getMoveset(true).find(m => m.moveId as TyrogueMove)?.moveId === cond.move;
|
||||
return pokemon.getMoveset(true).find(m => (tyrogueMoves as readonly MoveId[]) .includes(m.moveId))?.moveId === cond.move;
|
||||
case EvoCondKey.NATURE:
|
||||
return cond.nature.includes(pokemon.getNature());
|
||||
case EvoCondKey.RANDOM_FORM: {
|
||||
@ -652,7 +654,7 @@ export const pokemonEvolutions: PokemonEvolutions = {
|
||||
],
|
||||
[SpeciesId.KIRLIA]: [
|
||||
new SpeciesEvolution(SpeciesId.GARDEVOIR, 30, null, null),
|
||||
new SpeciesEvolution(SpeciesId.GALLADE, 1, EvolutionItem.DAWN_STONE, {key: EvoCondKey.GENDER, gender: Gender.MALE})
|
||||
new SpeciesEvolution(SpeciesId.GALLADE, 1, EvolutionItem.DAWN_STONE, {key: EvoCondKey.GENDER, gender: Gender.MALE}, SpeciesWildEvolutionDelay.LONG),
|
||||
],
|
||||
[SpeciesId.SURSKIT]: [
|
||||
new SpeciesEvolution(SpeciesId.MASQUERAIN, 22, null, null)
|
||||
@ -741,7 +743,7 @@ export const pokemonEvolutions: PokemonEvolutions = {
|
||||
],
|
||||
[SpeciesId.SNORUNT]: [
|
||||
new SpeciesEvolution(SpeciesId.GLALIE, 42, null, null),
|
||||
new SpeciesEvolution(SpeciesId.FROSLASS, 1, EvolutionItem.DAWN_STONE, {key: EvoCondKey.GENDER, gender: Gender.FEMALE})
|
||||
new SpeciesEvolution(SpeciesId.FROSLASS, 1, EvolutionItem.DAWN_STONE, {key: EvoCondKey.GENDER, gender: Gender.FEMALE}, SpeciesWildEvolutionDelay.LONG),
|
||||
],
|
||||
[SpeciesId.SPHEAL]: [
|
||||
new SpeciesEvolution(SpeciesId.SEALEO, 32, null, null)
|
||||
@ -1591,7 +1593,7 @@ export const pokemonEvolutions: PokemonEvolutions = {
|
||||
new SpeciesEvolution(SpeciesId.WHIMSICOTT, 1, EvolutionItem.SUN_STONE, null, SpeciesWildEvolutionDelay.LONG)
|
||||
],
|
||||
[SpeciesId.PETILIL]: [
|
||||
new SpeciesEvolution(SpeciesId.HISUI_LILLIGANT, 1, EvolutionItem.SHINY_STONE, null, SpeciesWildEvolutionDelay.LONG),
|
||||
new SpeciesEvolution(SpeciesId.HISUI_LILLIGANT, 1, EvolutionItem.DAWN_STONE, null, SpeciesWildEvolutionDelay.LONG),
|
||||
new SpeciesEvolution(SpeciesId.LILLIGANT, 1, EvolutionItem.SUN_STONE, null, SpeciesWildEvolutionDelay.LONG)
|
||||
],
|
||||
[SpeciesId.BASCULIN]: [
|
||||
@ -1883,6 +1885,15 @@ export function initPokemonPrevolutions(): void {
|
||||
// TODO: This may cause funny business for double starters such as Pichu/Pikachu
|
||||
export const pokemonStarters: PokemonPrevolutions = {};
|
||||
|
||||
/**
|
||||
* The default species and all their evolutions
|
||||
*/
|
||||
export const defaultStarterSpeciesAndEvolutions: SpeciesId[] = defaultStarterSpecies.flatMap(id => {
|
||||
const stage2ids = pokemonEvolutions[id]?.map(e => e.speciesId) ?? [];
|
||||
const stage3ids = stage2ids.flatMap(s2id => pokemonEvolutions[s2id]?.map(e => e.speciesId) ?? []);
|
||||
return [id, ...stage2ids, ...stage3ids];
|
||||
});
|
||||
|
||||
export function initPokemonStarters(): void {
|
||||
const starterKeys = Object.keys(pokemonPrevolutions);
|
||||
starterKeys.forEach(pk => {
|
||||
|
@ -42,10 +42,10 @@ export const SAME_SPECIES_EGG_HA_RATE = 8;
|
||||
export const MANAPHY_EGG_MANAPHY_RATE = 8;
|
||||
export const GACHA_EGG_HA_RATE = 192;
|
||||
|
||||
// 1/x for legendary eggs, 1/x*2 for epic eggs, 1/x*4 for rare eggs, and 1/x*8 for common eggs
|
||||
export const GACHA_DEFAULT_RARE_EGGMOVE_RATE = 6;
|
||||
export const SAME_SPECIES_EGG_RARE_EGGMOVE_RATE = 3;
|
||||
export const GACHA_MOVE_UP_RARE_EGGMOVE_RATE = 3;
|
||||
// Odds are 1/x
|
||||
// [COMMON, RARE, EPIC/MANAPHY, LEGEND]
|
||||
export const RARE_EGGMOVE_RATES: readonly number[] = [48, 24, 12, 6];
|
||||
export const BOOSTED_RARE_EGGMOVE_RATES: readonly number[] = [16, 12, 6, 3];
|
||||
|
||||
// #region Variant properties
|
||||
// The chance x/10 of a shiny being a variant, then of being specifically an epic variant
|
||||
|
@ -564,7 +564,7 @@ export class BeakBlastChargingTag extends BattlerTag {
|
||||
target: pokemon,
|
||||
})
|
||||
) {
|
||||
phaseData.attacker.trySetStatus(StatusEffect.BURN, true, pokemon);
|
||||
phaseData.attacker.trySetStatus(StatusEffect.BURN, pokemon);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@ -1058,8 +1058,7 @@ export class SeedTag extends SerializableBattlerTag {
|
||||
// Check which opponent to restore HP to
|
||||
const source = pokemon.getOpponents().find(o => o.getBattlerIndex() === this.sourceIndex);
|
||||
if (!source) {
|
||||
console.warn(`Failed to get source Pokemon for SeedTag lapse; id: ${this.sourceId}`);
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
const cancelled = new BooleanHolder(false);
|
||||
@ -1510,7 +1509,7 @@ export class DrowsyTag extends SerializableBattlerTag {
|
||||
|
||||
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
||||
if (!super.lapse(pokemon, lapseType)) {
|
||||
pokemon.trySetStatus(StatusEffect.SLEEP, true);
|
||||
pokemon.trySetStatus(StatusEffect.SLEEP);
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -1860,7 +1859,7 @@ export class ContactSetStatusProtectedTag extends DamageProtectedTag {
|
||||
* @param user - The pokemon that is being attacked and has the tag
|
||||
*/
|
||||
override onContact(attacker: Pokemon, user: Pokemon): void {
|
||||
attacker.trySetStatus(this.#statusEffect, true, user);
|
||||
attacker.trySetStatus(this.#statusEffect, user);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2331,10 +2330,10 @@ export class CritBoostTag extends SerializableBattlerTag {
|
||||
super.onAdd(pokemon);
|
||||
|
||||
// Dragon cheer adds +2 crit stages if the pokemon is a Dragon type when the tag is added
|
||||
if (this.tagType === BattlerTagType.DRAGON_CHEER && pokemon.getTypes(true).includes(PokemonType.DRAGON)) {
|
||||
(this as Mutable<this>).critStages = 2;
|
||||
} else {
|
||||
if (this.tagType === BattlerTagType.DRAGON_CHEER && !pokemon.getTypes(true, true).includes(PokemonType.DRAGON)) {
|
||||
(this as Mutable<this>).critStages = 1;
|
||||
} else {
|
||||
(this as Mutable<this>).critStages = 2;
|
||||
}
|
||||
|
||||
globalScene.phaseManager.queueMessage(
|
||||
@ -2804,7 +2803,7 @@ export class GulpMissileTag extends SerializableBattlerTag {
|
||||
if (this.tagType === BattlerTagType.GULP_MISSILE_ARROKUDA) {
|
||||
globalScene.phaseManager.unshiftNew("StatStageChangePhase", attacker.getBattlerIndex(), false, [Stat.DEF], -1);
|
||||
} else {
|
||||
attacker.trySetStatus(StatusEffect.PARALYSIS, true, pokemon);
|
||||
attacker.trySetStatus(StatusEffect.PARALYSIS, pokemon);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
@ -1,11 +1,13 @@
|
||||
import type { FixedBattleConfig } from "#app/battle";
|
||||
import { getRandomTrainerFunc } from "#app/battle";
|
||||
import { defaultStarterSpecies } from "#app/constants";
|
||||
import { defaultStarterSpeciesAndEvolutions } from "#balance/pokemon-evolutions";
|
||||
import { speciesStarterCosts } from "#balance/starters";
|
||||
import type { PokemonSpecies } from "#data/pokemon-species";
|
||||
import { AbilityAttr } from "#enums/ability-attr";
|
||||
import { BattleType } from "#enums/battle-type";
|
||||
import { Challenges } from "#enums/challenges";
|
||||
import { TypeColor, TypeShadow } from "#enums/color";
|
||||
import { DexAttr } from "#enums/dex-attr";
|
||||
import { ClassicFixedBossWaves } from "#enums/fixed-boss-waves";
|
||||
import { ModifierTier } from "#enums/modifier-tier";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
@ -19,8 +21,9 @@ import type { EnemyPokemon, PlayerPokemon, Pokemon } from "#field/pokemon";
|
||||
import { Trainer } from "#field/trainer";
|
||||
import type { ModifierTypeOption } from "#modifiers/modifier-type";
|
||||
import { PokemonMove } from "#moves/pokemon-move";
|
||||
import type { DexAttrProps, GameData } from "#system/game-data";
|
||||
import type { DexAttrProps, GameData, StarterDataEntry } from "#system/game-data";
|
||||
import { RibbonData, type RibbonFlag } from "#system/ribbons/ribbon-data";
|
||||
import type { DexEntry } from "#types/dex-data";
|
||||
import { type BooleanHolder, isBetween, type NumberHolder, randSeedItem } from "#utils/common";
|
||||
import { deepCopy } from "#utils/data";
|
||||
import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils";
|
||||
@ -237,6 +240,15 @@ export abstract class Challenge {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* An apply function for STARTER_SELECT_MODIFY challenges. Derived classes should alter this.
|
||||
* @param _pokemon {@link Pokemon} The starter pokemon to modify.
|
||||
* @returns {@link boolean} Whether this function did anything.
|
||||
*/
|
||||
applyStarterSelectModify(_speciesId: SpeciesId, _dexEntry: DexEntry, _starterDataEntry: StarterDataEntry): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* An apply function for STARTER_MODIFY challenges. Derived classes should alter this.
|
||||
* @param _pokemon {@link Pokemon} The starter pokemon to modify.
|
||||
@ -785,7 +797,7 @@ export class FreshStartChallenge extends Challenge {
|
||||
}
|
||||
|
||||
applyStarterChoice(pokemon: PokemonSpecies, valid: BooleanHolder): boolean {
|
||||
if (this.value === 1 && !defaultStarterSpecies.includes(pokemon.speciesId)) {
|
||||
if (this.value === 1 && !defaultStarterSpeciesAndEvolutions.includes(pokemon.speciesId)) {
|
||||
valid.value = false;
|
||||
return true;
|
||||
}
|
||||
@ -797,10 +809,61 @@ export class FreshStartChallenge extends Challenge {
|
||||
return true;
|
||||
}
|
||||
|
||||
applyStarterSelectModify(speciesId: SpeciesId, dexEntry: DexEntry, starterDataEntry: StarterDataEntry): boolean {
|
||||
// Remove all egg moves
|
||||
starterDataEntry.eggMoves = 0;
|
||||
|
||||
// Remove hidden and passive ability
|
||||
const defaultAbilities = AbilityAttr.ABILITY_1 | AbilityAttr.ABILITY_2;
|
||||
starterDataEntry.abilityAttr &= defaultAbilities;
|
||||
starterDataEntry.passiveAttr = 0;
|
||||
|
||||
// Remove cost reduction
|
||||
starterDataEntry.valueReduction = 0;
|
||||
|
||||
// Remove natures except for the default ones
|
||||
const neutralNaturesAttr =
|
||||
(1 << (Nature.HARDY + 1)) |
|
||||
(1 << (Nature.DOCILE + 1)) |
|
||||
(1 << (Nature.SERIOUS + 1)) |
|
||||
(1 << (Nature.BASHFUL + 1)) |
|
||||
(1 << (Nature.QUIRKY + 1));
|
||||
dexEntry.natureAttr &= neutralNaturesAttr;
|
||||
|
||||
// Cap all ivs at 15
|
||||
for (let i = 0; i < 6; i++) {
|
||||
dexEntry.ivs[i] = Math.min(dexEntry.ivs[i], 15);
|
||||
}
|
||||
|
||||
// Removes shiny and variants
|
||||
dexEntry.caughtAttr &= ~DexAttr.SHINY;
|
||||
dexEntry.caughtAttr &= ~(DexAttr.VARIANT_2 | DexAttr.VARIANT_3);
|
||||
|
||||
// Remove unlocked forms for specific species
|
||||
if (speciesId === SpeciesId.ZYGARDE) {
|
||||
// Sets ability from power construct to aura break
|
||||
const formMask = (DexAttr.DEFAULT_FORM << 2n) - 1n;
|
||||
dexEntry.caughtAttr &= formMask;
|
||||
} else if (
|
||||
[
|
||||
SpeciesId.PIKACHU,
|
||||
SpeciesId.EEVEE,
|
||||
SpeciesId.PICHU,
|
||||
SpeciesId.ROTOM,
|
||||
SpeciesId.MELOETTA,
|
||||
SpeciesId.FROAKIE,
|
||||
].includes(speciesId)
|
||||
) {
|
||||
const formMask = (DexAttr.DEFAULT_FORM << 1n) - 1n; // These mons are set to form 0 because they're meant to be unlocks or mid-run form changes
|
||||
dexEntry.caughtAttr &= formMask;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
applyStarterModify(pokemon: Pokemon): boolean {
|
||||
pokemon.abilityIndex = pokemon.abilityIndex % 2; // Always base ability, if you set it to hidden it wraps to first ability
|
||||
pokemon.passive = false; // Passive isn't unlocked
|
||||
pokemon.nature = Nature.HARDY; // Neutral nature
|
||||
let validMoves = pokemon.species
|
||||
.getLevelMoves()
|
||||
.filter(m => isBetween(m[0], 1, 5))
|
||||
@ -827,12 +890,14 @@ export class FreshStartChallenge extends Challenge {
|
||||
SpeciesId.ROTOM,
|
||||
SpeciesId.MELOETTA,
|
||||
SpeciesId.FROAKIE,
|
||||
SpeciesId.ROCKRUFF,
|
||||
].includes(pokemon.species.speciesId)
|
||||
) {
|
||||
pokemon.formIndex = 0; // These mons are set to form 0 because they're meant to be unlocks or mid-run form changes
|
||||
}
|
||||
pokemon.ivs = [15, 15, 15, 15, 15, 15]; // Default IVs of 15 for all stats (Updated to 15 from 10 in 1.2.0)
|
||||
// Cap all ivs at 15
|
||||
for (let i = 0; i < 6; i++) {
|
||||
pokemon.ivs[i] = Math.min(pokemon.ivs[i], 15);
|
||||
}
|
||||
pokemon.teraType = pokemon.species.type1; // Always primary tera type
|
||||
return true;
|
||||
}
|
||||
|
@ -3,17 +3,16 @@ import { globalScene } from "#app/global-scene";
|
||||
import Overrides from "#app/overrides";
|
||||
import { pokemonPrevolutions } from "#balance/pokemon-evolutions";
|
||||
import {
|
||||
BOOSTED_RARE_EGGMOVE_RATES,
|
||||
EGG_PITY_EPIC_THRESHOLD,
|
||||
EGG_PITY_LEGENDARY_THRESHOLD,
|
||||
EGG_PITY_RARE_THRESHOLD,
|
||||
GACHA_DEFAULT_COMMON_EGG_THRESHOLD,
|
||||
GACHA_DEFAULT_EPIC_EGG_THRESHOLD,
|
||||
GACHA_DEFAULT_RARE_EGG_THRESHOLD,
|
||||
GACHA_DEFAULT_RARE_EGGMOVE_RATE,
|
||||
GACHA_DEFAULT_SHINY_RATE,
|
||||
GACHA_EGG_HA_RATE,
|
||||
GACHA_LEGENDARY_UP_THRESHOLD_OFFSET,
|
||||
GACHA_MOVE_UP_RARE_EGGMOVE_RATE,
|
||||
GACHA_SHINY_UP_SHINY_RATE,
|
||||
HATCH_WAVES_COMMON_EGG,
|
||||
HATCH_WAVES_EPIC_EGG,
|
||||
@ -21,8 +20,8 @@ import {
|
||||
HATCH_WAVES_MANAPHY_EGG,
|
||||
HATCH_WAVES_RARE_EGG,
|
||||
MANAPHY_EGG_MANAPHY_RATE,
|
||||
RARE_EGGMOVE_RATES,
|
||||
SAME_SPECIES_EGG_HA_RATE,
|
||||
SAME_SPECIES_EGG_RARE_EGGMOVE_RATE,
|
||||
SAME_SPECIES_EGG_SHINY_RATE,
|
||||
SHINY_EPIC_CHANCE,
|
||||
SHINY_VARIANT_CHANCE,
|
||||
@ -355,21 +354,22 @@ export class Egg {
|
||||
// #region Private methods
|
||||
////
|
||||
|
||||
/**
|
||||
* Rolls which egg move slot the egg will have.
|
||||
* 1/x chance for rare, (x-1)/3 chance for each common move.
|
||||
* x is determined by Egg Tier. Boosted rates used for eggs obtained through Move Up Gacha and Candy.
|
||||
* @returns the slot for the egg move
|
||||
*/
|
||||
private rollEggMoveIndex() {
|
||||
let baseChance = GACHA_DEFAULT_RARE_EGGMOVE_RATE;
|
||||
switch (this._sourceType) {
|
||||
case EggSourceType.SAME_SPECIES_EGG:
|
||||
baseChance = SAME_SPECIES_EGG_RARE_EGGMOVE_RATE;
|
||||
break;
|
||||
case EggSourceType.GACHA_MOVE:
|
||||
baseChance = GACHA_MOVE_UP_RARE_EGGMOVE_RATE;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
const tierNum = this.isManaphyEgg() ? 2 : this.tier;
|
||||
let baseChance: number;
|
||||
if (this._sourceType === EggSourceType.SAME_SPECIES_EGG || this._sourceType === EggSourceType.GACHA_MOVE) {
|
||||
baseChance = BOOSTED_RARE_EGGMOVE_RATES[tierNum];
|
||||
} else {
|
||||
baseChance = RARE_EGGMOVE_RATES[tierNum];
|
||||
}
|
||||
|
||||
const tierMultiplier = this.isManaphyEgg() ? 2 : Math.pow(2, 3 - this.tier);
|
||||
return randSeedInt(baseChance * tierMultiplier) ? randSeedInt(3) : 3;
|
||||
return randSeedInt(baseChance) ? randSeedInt(3) : 3;
|
||||
}
|
||||
|
||||
private getEggTierDefaultHatchWaves(eggTier?: EggTier): number {
|
||||
|
@ -6,7 +6,7 @@ import { loggedInUser } from "#app/account";
|
||||
import type { GameMode } from "#app/game-mode";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
import type { ArenaTrapTag } from "#data/arena-tag";
|
||||
import type { EntryHazardTag } from "#data/arena-tag";
|
||||
import { WeakenMoveTypeTag } from "#data/arena-tag";
|
||||
import { MoveChargeAnim } from "#data/battle-anims";
|
||||
import {
|
||||
@ -88,7 +88,7 @@ import type { AttackMoveResult } from "#types/attack-move-result";
|
||||
import type { Localizable } from "#types/locales";
|
||||
import type { ChargingMove, MoveAttrMap, MoveAttrString, MoveClassMap, MoveKindString, MoveMessageFunc } from "#types/move-types";
|
||||
import type { TurnMove } from "#types/turn-move";
|
||||
import { BooleanHolder, type Constructor, isNullOrUndefined, NumberHolder, randSeedFloat, randSeedInt, randSeedItem, toDmgValue } from "#utils/common";
|
||||
import { BooleanHolder, coerceArray, type Constructor, isNullOrUndefined, NumberHolder, randSeedFloat, randSeedInt, randSeedItem, toDmgValue } from "#utils/common";
|
||||
import { getEnumValues } from "#utils/enums";
|
||||
import { toCamelCase, toTitleCase } from "#utils/strings";
|
||||
import i18next from "i18next";
|
||||
@ -1190,8 +1190,9 @@ export abstract class MoveAttr {
|
||||
}
|
||||
|
||||
/**
|
||||
* @virtual
|
||||
* @returns the {@linkcode MoveCondition} or {@linkcode MoveConditionFunc} for this {@linkcode Move}
|
||||
* Return this `MoveAttr`'s associated {@linkcode MoveCondition} or {@linkcode MoveConditionFunc}.
|
||||
* The specified condition will be added to all {@linkcode Move}s with this attribute,
|
||||
* and moves **will fail upon use** if _at least 1_ of their attached conditions returns `false`.
|
||||
*/
|
||||
getCondition(): MoveCondition | MoveConditionFunc | null {
|
||||
return null;
|
||||
@ -1304,15 +1305,21 @@ export class MoveEffectAttr extends MoveAttr {
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the {@linkcode Move}'s effects are valid to {@linkcode apply}
|
||||
* @virtual
|
||||
* @param user {@linkcode Pokemon} using the move
|
||||
* @param target {@linkcode Pokemon} target of the move
|
||||
* @param move {@linkcode Move} with this attribute
|
||||
* @param args Set of unique arguments needed by this attribute
|
||||
* @returns true if basic application of the ability attribute should be possible
|
||||
* Determine whether this {@linkcode MoveAttr}'s effects are able to {@linkcode apply | be applied} to the target.
|
||||
*
|
||||
* Will **NOT** cause the move to fail upon returning `false` (unlike {@linkcode getCondition};
|
||||
* merely that the effect for this attribute will be nullified.
|
||||
* @param user - The {@linkcode Pokemon} using the move
|
||||
* @param target - The {@linkcode Pokemon} being targeted by the move, or {@linkcode user} if the move is
|
||||
* {@linkcode selfTarget | self-targeting}
|
||||
* @param move - The {@linkcode Move} being used
|
||||
* @param _args - Set of unique arguments needed by this attribute
|
||||
* @returns `true` if basic application of this `MoveAttr`s effects should be possible
|
||||
*/
|
||||
canApply(user: Pokemon, target: Pokemon, move: Move, args?: any[]) {
|
||||
// TODO: Decouple this check from the `apply` step
|
||||
// TODO: Make non-damaging moves fail by default if none of their attributes can apply
|
||||
canApply(user: Pokemon, target: Pokemon, move: Move, _args?: any[]) {
|
||||
// TODO: These checks seem redundant
|
||||
return !! (this.selfTarget ? user.hp && !user.getTag(BattlerTagType.FRENZY) : target.hp)
|
||||
&& (this.selfTarget || !target.getTag(BattlerTagType.PROTECTED) ||
|
||||
move.doesFlagEffectApply({ flag: MoveFlags.IGNORE_PROTECT, user, target }));
|
||||
@ -1961,19 +1968,17 @@ export class AddSubstituteAttr extends MoveEffectAttr {
|
||||
* @see {@linkcode apply}
|
||||
*/
|
||||
export class HealAttr extends MoveEffectAttr {
|
||||
/** The percentage of {@linkcode Stat.HP} to heal */
|
||||
private healRatio: number;
|
||||
/** Should an animation be shown? */
|
||||
private showAnim: boolean;
|
||||
|
||||
constructor(healRatio?: number, showAnim?: boolean, selfTarget?: boolean) {
|
||||
super(selfTarget === undefined || selfTarget);
|
||||
|
||||
this.healRatio = healRatio || 1;
|
||||
this.showAnim = !!showAnim;
|
||||
constructor(
|
||||
/** The percentage of {@linkcode Stat.HP} to heal. */
|
||||
private healRatio: number,
|
||||
/** Whether to display a healing animation when healing the target; default `false` */
|
||||
private showAnim = false,
|
||||
selfTarget = true
|
||||
) {
|
||||
super(selfTarget);
|
||||
}
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean {
|
||||
this.addHealPhase(this.selfTarget ? user : target, this.healRatio);
|
||||
return true;
|
||||
}
|
||||
@ -1982,15 +1987,69 @@ export class HealAttr extends MoveEffectAttr {
|
||||
* Creates a new {@linkcode PokemonHealPhase}.
|
||||
* This heals the target and shows the appropriate message.
|
||||
*/
|
||||
addHealPhase(target: Pokemon, healRatio: number) {
|
||||
protected addHealPhase(target: Pokemon, healRatio: number) {
|
||||
globalScene.phaseManager.unshiftNew("PokemonHealPhase", target.getBattlerIndex(),
|
||||
toDmgValue(target.getMaxHp() * healRatio), i18next.t("moveTriggers:healHp", { pokemonName: getPokemonNameWithAffix(target) }), true, !this.showAnim);
|
||||
}
|
||||
|
||||
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
|
||||
override getTargetBenefitScore(user: Pokemon, target: Pokemon, _move: Move): number {
|
||||
const score = ((1 - (this.selfTarget ? user : target).getHpRatio()) * 20) - this.healRatio * 10;
|
||||
return Math.round(score / (1 - this.healRatio / 2));
|
||||
}
|
||||
|
||||
// TODO: Change to fail move
|
||||
override canApply(user: Pokemon, target: Pokemon, _move: Move, _args?: any[]): boolean {
|
||||
if (!super.canApply(user, target, _move, _args)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const healedPokemon = this.selfTarget ? user : target;
|
||||
if (healedPokemon.isFullHp()) {
|
||||
// Ensure the fail message isn't displayed when checking the move conditions outside of the move execution
|
||||
// TOOD: Fix this in PR#6276
|
||||
if (globalScene.phaseManager.getCurrentPhase()?.is("MovePhase")) {
|
||||
globalScene.phaseManager.queueMessage(i18next.t("battle:hpIsFull", {
|
||||
pokemonName: getPokemonNameWithAffix(healedPokemon),
|
||||
}))
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute to put the user to sleep for a fixed duration, fully heal them and cure their status.
|
||||
* Used for {@linkcode MoveId.REST}.
|
||||
*/
|
||||
export class RestAttr extends HealAttr {
|
||||
private duration: number;
|
||||
|
||||
constructor(duration: number) {
|
||||
super(1, true);
|
||||
this.duration = duration;
|
||||
}
|
||||
|
||||
override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
const wasSet = user.trySetStatus(StatusEffect.SLEEP, user, this.duration, null, true, true,
|
||||
i18next.t("moveTriggers:restBecameHealthy", {
|
||||
pokemonName: getPokemonNameWithAffix(user),
|
||||
}));
|
||||
return wasSet && super.apply(user, target, move, args);
|
||||
}
|
||||
|
||||
override addHealPhase(user: Pokemon): void {
|
||||
globalScene.phaseManager.unshiftNew("PokemonHealPhase", user.getBattlerIndex(), user.getMaxHp(), null)
|
||||
}
|
||||
|
||||
// TODO: change after HealAttr is changed to fail move
|
||||
override getCondition(): MoveConditionFunc {
|
||||
return (user, target, move) =>
|
||||
super.canApply(user, target, move, [])
|
||||
// Intentionally suppress messages here as we display generic fail msg
|
||||
// TODO: This might have order-of-operation jank
|
||||
&& user.canSetStatus(StatusEffect.SLEEP, true, true, user)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -2262,20 +2321,16 @@ export class BoostHealAttr extends HealAttr {
|
||||
* @see {@linkcode apply}
|
||||
*/
|
||||
export class HealOnAllyAttr extends HealAttr {
|
||||
/**
|
||||
* @param user {@linkcode Pokemon} using the move
|
||||
* @param target {@linkcode Pokemon} target of the move
|
||||
* @param move {@linkcode Move} with this attribute
|
||||
* @param args N/A
|
||||
* @returns true if the function succeeds
|
||||
*/
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
if (user.getAlly() === target) {
|
||||
super.apply(user, target, move, args);
|
||||
return true;
|
||||
}
|
||||
override canApply(user: Pokemon, target: Pokemon, _move: Move, _args?: any[]): boolean {
|
||||
// Don't trigger if not targeting an ally
|
||||
return target === user.getAlly() && super.canApply(user, target, _move, _args);
|
||||
}
|
||||
|
||||
return false;
|
||||
override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean {
|
||||
if (user.isOpponent(target)) {
|
||||
return false;
|
||||
}
|
||||
return super.apply(user, target, _move, _args);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2286,6 +2341,7 @@ export class HealOnAllyAttr extends HealAttr {
|
||||
* @see {@linkcode apply}
|
||||
* @see {@linkcode getUserBenefitScore}
|
||||
*/
|
||||
// TODO: Make Strength Sap its own attribute that extends off of this one
|
||||
export class HitHealAttr extends MoveEffectAttr {
|
||||
private healRatio: number;
|
||||
private healStat: EffectiveStat | null;
|
||||
@ -2536,49 +2592,50 @@ export class WaterShurikenMultiHitTypeAttr extends ChangeMultiHitTypeAttr {
|
||||
|
||||
export class StatusEffectAttr extends MoveEffectAttr {
|
||||
public effect: StatusEffect;
|
||||
public turnsRemaining?: number;
|
||||
public overrideStatus: boolean = false;
|
||||
|
||||
constructor(effect: StatusEffect, selfTarget?: boolean, turnsRemaining?: number, overrideStatus: boolean = false) {
|
||||
constructor(effect: StatusEffect, selfTarget = false) {
|
||||
super(selfTarget);
|
||||
|
||||
this.effect = effect;
|
||||
this.turnsRemaining = turnsRemaining;
|
||||
this.overrideStatus = overrideStatus;
|
||||
}
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
const moveChance = this.getMoveChance(user, target, move, this.selfTarget, true);
|
||||
const statusCheck = moveChance < 0 || moveChance === 100 || user.randBattleSeedInt(100) < moveChance;
|
||||
if (!statusCheck) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// non-status moves don't play sound effects for failures
|
||||
const quiet = move.category !== MoveCategory.STATUS;
|
||||
if (statusCheck) {
|
||||
const pokemon = this.selfTarget ? user : target;
|
||||
if (user !== target && move.category === MoveCategory.STATUS && !target.canSetStatus(this.effect, quiet, false, user, true)) {
|
||||
return false;
|
||||
}
|
||||
if (((!pokemon.status || this.overrideStatus) || (pokemon.status.effect === this.effect && moveChance < 0))
|
||||
&& pokemon.trySetStatus(this.effect, true, user, this.turnsRemaining, null, this.overrideStatus, quiet)) {
|
||||
applyAbAttrs("ConfusionOnStatusEffectAbAttr", {pokemon: user, opponent: target, move, effect: this.effect});
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
target.trySetStatus(this.effect, user, undefined, null, false, quiet)
|
||||
) {
|
||||
applyAbAttrs("ConfusionOnStatusEffectAbAttr", {pokemon: user, opponent: target, move, effect: this.effect});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
|
||||
const moveChance = this.getMoveChance(user, target, move, this.selfTarget, false);
|
||||
const score = (moveChance < 0) ? -10 : Math.floor(moveChance * -0.1);
|
||||
const score = moveChance < 0 ? -10 : Math.floor(moveChance * -0.1);
|
||||
const pokemon = this.selfTarget ? user : target;
|
||||
|
||||
return !pokemon.status && pokemon.canSetStatus(this.effect, true, false, user) ? score : 0;
|
||||
return pokemon.canSetStatus(this.effect, true, false, user) ? score : 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute to randomly apply one of several statuses to the target.
|
||||
* Used for {@linkcode Moves.TRI_ATTACK} and {@linkcode Moves.DIRE_CLAW}.
|
||||
*/
|
||||
export class MultiStatusEffectAttr extends StatusEffectAttr {
|
||||
public effects: StatusEffect[];
|
||||
|
||||
constructor(effects: StatusEffect[], selfTarget?: boolean, turnsRemaining?: number, overrideStatus?: boolean) {
|
||||
super(effects[0], selfTarget, turnsRemaining, overrideStatus);
|
||||
constructor(effects: StatusEffect[], selfTarget?: boolean) {
|
||||
super(effects[0], selfTarget);
|
||||
this.effects = effects;
|
||||
}
|
||||
|
||||
@ -2611,26 +2668,41 @@ export class PsychoShiftEffectAttr extends MoveEffectAttr {
|
||||
* @returns - Whether the effect was successfully applied to the target.
|
||||
*/
|
||||
apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean {
|
||||
const statusToApply: StatusEffect | undefined = user.status?.effect ?? (user.hasAbility(AbilityId.COMATOSE) ? StatusEffect.SLEEP : undefined);
|
||||
const statusToApply = user.status?.effect ??
|
||||
(user.hasAbility(AbilityId.COMATOSE) ? StatusEffect.SLEEP : StatusEffect.NONE);
|
||||
|
||||
if (target.status || !statusToApply) {
|
||||
// Bang is justified as condition func returns early if no status is found
|
||||
if (!target.trySetStatus(statusToApply, user)) {
|
||||
return false;
|
||||
} else {
|
||||
const canSetStatus = target.canSetStatus(statusToApply, true, false, user);
|
||||
const trySetStatus = canSetStatus ? target.trySetStatus(statusToApply, true, user) : false;
|
||||
}
|
||||
|
||||
if (trySetStatus && user.status) {
|
||||
// PsychoShiftTag is added to the user if move succeeds so that the user is healed of its status effect after its move
|
||||
user.addTag(BattlerTagType.PSYCHO_SHIFT);
|
||||
if (user.status) {
|
||||
// Add tag to user to heal its status effect after the move ends (unless we have comatose);
|
||||
// occurs after move use to ensure correct Synchronize timing
|
||||
user.addTag(BattlerTagType.PSYCHO_SHIFT)
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
getCondition(): MoveConditionFunc {
|
||||
return (user, target) => {
|
||||
if (target.status?.effect) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return trySetStatus;
|
||||
const statusToApply = user.status?.effect ?? (user.hasAbility(AbilityId.COMATOSE) ? StatusEffect.SLEEP : StatusEffect.NONE);
|
||||
return !!statusToApply && target.canSetStatus(statusToApply, false, false, user);
|
||||
}
|
||||
}
|
||||
|
||||
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
|
||||
const statusToApply = user.status?.effect ?? (user.hasAbility(AbilityId.COMATOSE) ? StatusEffect.SLEEP : undefined);
|
||||
return !target.status && statusToApply && target.canSetStatus(statusToApply, true, false, user) ? -10 : 0;
|
||||
const statusToApply =
|
||||
user.status?.effect ??
|
||||
(user.hasAbility(AbilityId.COMATOSE) ? StatusEffect.SLEEP : StatusEffect.NONE);
|
||||
|
||||
// TODO: Give this a positive user benefit score
|
||||
return !target.status?.effect && statusToApply && target.canSetStatus(statusToApply, true, false, user) ? -10 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -2690,7 +2762,7 @@ export class StealHeldItemChanceAttr extends MoveEffectAttr {
|
||||
* Used for Incinerate and Knock Off.
|
||||
* Not Implemented Cases: (Same applies for Thief)
|
||||
* "If the user faints due to the target's Ability (Rough Skin or Iron Barbs) or held Rocky Helmet, it cannot remove the target's held item."
|
||||
* "If the Pokémon is knocked out by the attack, Sticky Hold does not protect the held item.""
|
||||
* "If the Pokémon is knocked out by the attack, Sticky Hold does not protect the held item."
|
||||
*/
|
||||
export class RemoveHeldItemAttr extends MoveEffectAttr {
|
||||
|
||||
@ -2900,7 +2972,7 @@ export class HealStatusEffectAttr extends MoveEffectAttr {
|
||||
*/
|
||||
constructor(selfTarget: boolean, effects: StatusEffect | StatusEffect[]) {
|
||||
super(selfTarget, { lastHitOnly: true });
|
||||
this.effects = [ effects ].flat(1);
|
||||
this.effects = coerceArray(effects)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -3205,7 +3277,6 @@ export class DelayedAttackAttr extends OverrideMoveEffectAttr {
|
||||
)
|
||||
)
|
||||
|
||||
user.pushMoveHistory({move: move.id, targets: [target.getBattlerIndex()], result: MoveResult.OTHER, useMode, turn: globalScene.currentBattle.turn})
|
||||
user.pushMoveHistory({move: move.id, targets: [target.getBattlerIndex()], result: MoveResult.OTHER, useMode, turn: globalScene.currentBattle.turn})
|
||||
// Queue up an attack on the given slot.
|
||||
globalScene.arena.positionalTagManager.addTag<PositionalTagType.DELAYED_ATTACK>({
|
||||
@ -3939,22 +4010,36 @@ export class BeatUpAttr extends VariablePowerAttr {
|
||||
}
|
||||
}
|
||||
|
||||
const doublePowerChanceMessageFunc = (user: Pokemon, target: Pokemon, move: Move) => {
|
||||
let message: string = "";
|
||||
globalScene.executeWithSeedOffset(() => {
|
||||
const rand = randSeedInt(100);
|
||||
if (rand < move.chance) {
|
||||
message = i18next.t("moveTriggers:goingAllOutForAttack", { pokemonName: getPokemonNameWithAffix(user) });
|
||||
}
|
||||
}, globalScene.currentBattle.turn << 6, globalScene.waveSeed);
|
||||
return message;
|
||||
};
|
||||
/**
|
||||
* Message function for {@linkcode MoveId.FICKLE_BEAM} that shows a message before move use if
|
||||
* the move's power would be boosted.
|
||||
* @todo Find another way to synchronize the RNG calls of Fickle Beam with its message
|
||||
* than using a seed offset
|
||||
*/
|
||||
function doublePowerChanceMessageFunc(chance: number) {
|
||||
return (user: Pokemon, target: Pokemon, move: Move) => {
|
||||
let message: string = "";
|
||||
globalScene.executeWithSeedOffset(() => {
|
||||
const rand = randSeedInt(100);
|
||||
if (rand < chance) {
|
||||
message = i18next.t("moveTriggers:goingAllOutForAttack", { pokemonName: getPokemonNameWithAffix(user) });
|
||||
}
|
||||
}, globalScene.currentBattle.turn << 6, globalScene.waveSeed);
|
||||
return message;
|
||||
};
|
||||
}
|
||||
|
||||
export class DoublePowerChanceAttr extends VariablePowerAttr {
|
||||
private chance: number;
|
||||
constructor(chance: number) {
|
||||
super(false)
|
||||
this.chance = chance
|
||||
}
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
let rand: number;
|
||||
let rand = 0;
|
||||
globalScene.executeWithSeedOffset(() => rand = randSeedInt(100), globalScene.currentBattle.turn << 6, globalScene.waveSeed);
|
||||
if (rand! < move.chance) {
|
||||
if (rand < this.chance) {
|
||||
const power = args[0] as NumberHolder;
|
||||
power.value *= 2;
|
||||
return true;
|
||||
@ -4427,6 +4512,10 @@ export class SpitUpPowerAttr extends VariablePowerAttr {
|
||||
* Does NOT remove stockpiled stacks.
|
||||
*/
|
||||
export class SwallowHealAttr extends HealAttr {
|
||||
constructor() {
|
||||
super(1)
|
||||
}
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
const stockpilingTag = user.getTag(StockpilingTag);
|
||||
|
||||
@ -6083,7 +6172,7 @@ export class AddArenaTrapTagAttr extends AddArenaTagAttr {
|
||||
getCondition(): MoveConditionFunc {
|
||||
return (user, target, move) => {
|
||||
const side = (this.selfSideTarget !== user.isPlayer()) ? ArenaTagSide.ENEMY : ArenaTagSide.PLAYER;
|
||||
const tag = globalScene.arena.getTagOnSide(this.tagType, side) as ArenaTrapTag;
|
||||
const tag = globalScene.arena.getTagOnSide(this.tagType, side) as EntryHazardTag;
|
||||
if (!tag) {
|
||||
return true;
|
||||
}
|
||||
@ -6107,7 +6196,7 @@ export class AddArenaTrapTagHitAttr extends AddArenaTagAttr {
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
const moveChance = this.getMoveChance(user, target, move, this.selfTarget, true);
|
||||
const side = (this.selfSideTarget ? user : target).isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
|
||||
const tag = globalScene.arena.getTagOnSide(this.tagType, side) as ArenaTrapTag;
|
||||
const tag = globalScene.arena.getTagOnSide(this.tagType, side) as EntryHazardTag;
|
||||
if ((moveChance < 0 || moveChance === 100 || user.randBattleSeedInt(100) < moveChance) && user.getLastXMoves(1)[0]?.result === MoveResult.SUCCESS) {
|
||||
globalScene.arena.addTag(this.tagType, 0, move.id, user.id, side);
|
||||
if (!tag) {
|
||||
@ -7909,7 +7998,7 @@ export class StatusIfBoostedAttr extends MoveEffectAttr {
|
||||
*/
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
if (target.turnData.statStagesIncreased) {
|
||||
target.trySetStatus(this.effect, true, user);
|
||||
target.trySetStatus(this.effect, user);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@ -8056,11 +8145,11 @@ const failIfDampCondition: MoveConditionFunc = (user, target, move) => {
|
||||
return !cancelled.value;
|
||||
};
|
||||
|
||||
const userSleptOrComatoseCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => user.status?.effect === StatusEffect.SLEEP || user.hasAbility(AbilityId.COMATOSE);
|
||||
const userSleptOrComatoseCondition: MoveConditionFunc = (user) => user.status?.effect === StatusEffect.SLEEP || user.hasAbility(AbilityId.COMATOSE);
|
||||
|
||||
const targetSleptOrComatoseCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => target.status?.effect === StatusEffect.SLEEP || target.hasAbility(AbilityId.COMATOSE);
|
||||
const targetSleptOrComatoseCondition: MoveConditionFunc = (_user: Pokemon, target: Pokemon, _move: Move) => target.status?.effect === StatusEffect.SLEEP || target.hasAbility(AbilityId.COMATOSE);
|
||||
|
||||
const failIfLastCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => globalScene.phaseManager.phaseQueue.find(phase => phase.is("MovePhase")) !== undefined;
|
||||
const failIfLastCondition: MoveConditionFunc = () => globalScene.phaseManager.findPhase(phase => phase.is("MovePhase")) !== undefined;
|
||||
|
||||
const failIfLastInPartyCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => {
|
||||
const party: Pokemon[] = user.isPlayer() ? globalScene.getPlayerParty() : globalScene.getEnemyParty();
|
||||
@ -8483,8 +8572,6 @@ const MoveAttrs = Object.freeze({
|
||||
/** Map of of move attribute names to their constructors */
|
||||
export type MoveAttrConstructorMap = typeof MoveAttrs;
|
||||
|
||||
export const selfStatLowerMoves: MoveId[] = [];
|
||||
|
||||
export function initMoves() {
|
||||
allMoves.push(
|
||||
new SelfStatusMove(MoveId.NONE, PokemonType.NORMAL, MoveCategory.STATUS, -1, -1, 0, 1),
|
||||
@ -8938,9 +9025,7 @@ export function initMoves() {
|
||||
.attr(MultiHitAttr, MultiHitType._2)
|
||||
.makesContact(false),
|
||||
new SelfStatusMove(MoveId.REST, PokemonType.PSYCHIC, -1, 5, -1, 0, 1)
|
||||
.attr(StatusEffectAttr, StatusEffect.SLEEP, true, 3, true)
|
||||
.attr(HealAttr, 1, true)
|
||||
.condition((user, target, move) => !user.isFullHp() && user.canSetStatus(StatusEffect.SLEEP, true, true, user))
|
||||
.attr(RestAttr, 3)
|
||||
.triageMove(),
|
||||
new AttackMove(MoveId.ROCK_SLIDE, PokemonType.ROCK, MoveCategory.PHYSICAL, 75, 90, 10, 30, 0, 1)
|
||||
.attr(FlinchAttr)
|
||||
@ -9286,14 +9371,16 @@ export function initMoves() {
|
||||
.condition(user => (user.getTag(StockpilingTag)?.stockpiledCount ?? 0) < 3)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.STOCKPILING, true),
|
||||
new AttackMove(MoveId.SPIT_UP, PokemonType.NORMAL, MoveCategory.SPECIAL, -1, 100, 10, -1, 0, 3)
|
||||
.condition(hasStockpileStacksCondition)
|
||||
.attr(SpitUpPowerAttr, 100)
|
||||
.condition(hasStockpileStacksCondition)
|
||||
.attr(RemoveBattlerTagAttr, [ BattlerTagType.STOCKPILING ], true),
|
||||
new SelfStatusMove(MoveId.SWALLOW, PokemonType.NORMAL, -1, 10, -1, 0, 3)
|
||||
.condition(hasStockpileStacksCondition)
|
||||
.attr(SwallowHealAttr)
|
||||
.condition(hasStockpileStacksCondition)
|
||||
.attr(RemoveBattlerTagAttr, [ BattlerTagType.STOCKPILING ], true)
|
||||
.triageMove(),
|
||||
.triageMove()
|
||||
// TODO: Verify if using Swallow at full HP still consumes stacks or not
|
||||
.edgeCase(),
|
||||
new AttackMove(MoveId.HEAT_WAVE, PokemonType.FIRE, MoveCategory.SPECIAL, 95, 90, 10, 10, 0, 3)
|
||||
.attr(HealStatusEffectAttr, true, StatusEffect.FREEZE)
|
||||
.attr(StatusEffectAttr, StatusEffect.BURN)
|
||||
@ -9679,14 +9766,8 @@ export function initMoves() {
|
||||
.unimplemented(),
|
||||
new StatusMove(MoveId.PSYCHO_SHIFT, PokemonType.PSYCHIC, 100, 10, -1, 0, 4)
|
||||
.attr(PsychoShiftEffectAttr)
|
||||
.condition((user, target, move) => {
|
||||
let statusToApply = user.hasAbility(AbilityId.COMATOSE) ? StatusEffect.SLEEP : undefined;
|
||||
if (user.status?.effect && isNonVolatileStatusEffect(user.status.effect)) {
|
||||
statusToApply = user.status.effect;
|
||||
}
|
||||
return !!statusToApply && target.canSetStatus(statusToApply, false, false, user);
|
||||
}
|
||||
),
|
||||
// TODO: Verify status applied if a statused pokemon obtains Comatose (via Transform) and uses Psycho Shift
|
||||
.edgeCase(),
|
||||
new AttackMove(MoveId.TRUMP_CARD, PokemonType.NORMAL, MoveCategory.SPECIAL, -1, -1, 5, -1, 0, 4)
|
||||
.makesContact()
|
||||
.attr(LessPPMorePowerAttr),
|
||||
@ -11387,9 +11468,9 @@ export function initMoves() {
|
||||
new AttackMove(MoveId.RUINATION, PokemonType.DARK, MoveCategory.SPECIAL, -1, 90, 10, -1, 0, 9)
|
||||
.attr(TargetHalfHpDamageAttr),
|
||||
new AttackMove(MoveId.COLLISION_COURSE, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 100, 100, 5, -1, 0, 9)
|
||||
.attr(MovePowerMultiplierAttr, (user, target, move) => target.getAttackTypeEffectiveness(move.type, user) >= 2 ? 5461 / 4096 : 1),
|
||||
.attr(MovePowerMultiplierAttr, (user, target, move) => target.getAttackTypeEffectiveness(move.type, user) >= 2 ? 4 / 3 : 1),
|
||||
new AttackMove(MoveId.ELECTRO_DRIFT, PokemonType.ELECTRIC, MoveCategory.SPECIAL, 100, 100, 5, -1, 0, 9)
|
||||
.attr(MovePowerMultiplierAttr, (user, target, move) => target.getAttackTypeEffectiveness(move.type, user) >= 2 ? 5461 / 4096 : 1)
|
||||
.attr(MovePowerMultiplierAttr, (user, target, move) => target.getAttackTypeEffectiveness(move.type, user) >= 2 ? 4 / 3 : 1)
|
||||
.makesContact(),
|
||||
new SelfStatusMove(MoveId.SHED_TAIL, PokemonType.NORMAL, -1, 10, -1, 0, 9)
|
||||
.attr(AddSubstituteAttr, 0.5, true)
|
||||
@ -11494,10 +11575,9 @@ export function initMoves() {
|
||||
.attr(TeraStarstormTypeAttr)
|
||||
.attr(VariableTargetAttr, (user, target, move) => user.hasSpecies(SpeciesId.TERAPAGOS) && (user.isTerastallized || globalScene.currentBattle.preTurnCommands[user.getFieldIndex()]?.command === Command.TERA) ? MoveTarget.ALL_NEAR_ENEMIES : MoveTarget.NEAR_OTHER)
|
||||
.partial(), /** Does not ignore abilities that affect stats, relevant in determining the move's category {@see TeraMoveCategoryAttr} */
|
||||
new AttackMove(MoveId.FICKLE_BEAM, PokemonType.DRAGON, MoveCategory.SPECIAL, 80, 100, 5, 30, 0, 9)
|
||||
.attr(PreMoveMessageAttr, doublePowerChanceMessageFunc)
|
||||
.attr(DoublePowerChanceAttr)
|
||||
.edgeCase(), // Should not interact with Sheer Force
|
||||
new AttackMove(MoveId.FICKLE_BEAM, PokemonType.DRAGON, MoveCategory.SPECIAL, 80, 100, 5, -1, 0, 9)
|
||||
.attr(PreMoveMessageAttr, doublePowerChanceMessageFunc(30))
|
||||
.attr(DoublePowerChanceAttr, 30),
|
||||
new SelfStatusMove(MoveId.BURNING_BULWARK, PokemonType.FIRE, -1, 10, -1, 4, 9)
|
||||
.attr(ProtectAttr, BattlerTagType.BURNING_BULWARK)
|
||||
.condition(failIfLastCondition),
|
||||
@ -11540,9 +11620,4 @@ export function initMoves() {
|
||||
new AttackMove(MoveId.MALIGNANT_CHAIN, PokemonType.POISON, MoveCategory.SPECIAL, 100, 100, 5, 50, 0, 9)
|
||||
.attr(StatusEffectAttr, StatusEffect.TOXIC)
|
||||
);
|
||||
allMoves.map(m => {
|
||||
if (m.getAttrs("StatStageChangeAttr").some(a => a.selfTarget && a.stages < 0)) {
|
||||
selfStatLowerMoves.push(m.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -237,7 +237,7 @@ export const BerriesAboundEncounter: MysteryEncounter = MysteryEncounterBuilder.
|
||||
const config = globalScene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0];
|
||||
config.pokemonConfigs![0].tags = [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON];
|
||||
config.pokemonConfigs![0].mysteryEncounterBattleEffects = (pokemon: Pokemon) => {
|
||||
queueEncounterMessage(`${namespace}:option.2.boss_enraged`);
|
||||
queueEncounterMessage(`${namespace}:option.2.bossEnraged`);
|
||||
globalScene.phaseManager.unshiftNew(
|
||||
"StatStageChangePhase",
|
||||
pokemon.getBattlerIndex(),
|
||||
|
@ -243,8 +243,9 @@ export const FieryFalloutEncounter: MysteryEncounter = MysteryEncounterBuilder.w
|
||||
if (burnable?.length > 0) {
|
||||
const roll = randSeedInt(burnable.length);
|
||||
const chosenPokemon = burnable[roll];
|
||||
if (chosenPokemon.trySetStatus(StatusEffect.BURN)) {
|
||||
if (chosenPokemon.canSetStatus(StatusEffect.BURN, true)) {
|
||||
// Burn applied
|
||||
chosenPokemon.doSetStatus(StatusEffect.BURN);
|
||||
encounter.setDialogueToken("burnedPokemon", chosenPokemon.getNameToRender());
|
||||
encounter.setDialogueToken("abilityName", allAbilities[AbilityId.HEATPROOF].name);
|
||||
queueEncounterMessage(`${namespace}:option.2.targetBurned`);
|
||||
|
@ -44,7 +44,10 @@ import { PokemonData } from "#system/pokemon-data";
|
||||
import { MusicPreference } from "#system/settings";
|
||||
import type { OptionSelectItem } from "#ui/abstract-option-select-ui-handler";
|
||||
import { isNullOrUndefined, NumberHolder, randInt, randSeedInt, randSeedItem, randSeedShuffle } from "#utils/common";
|
||||
import { getEnumKeys } from "#utils/enums";
|
||||
import { getRandomLocaleEntry } from "#utils/i18n";
|
||||
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
||||
import { toCamelCase } from "#utils/strings";
|
||||
import i18next from "i18next";
|
||||
|
||||
/** the i18n namespace for the encounter */
|
||||
@ -984,14 +987,17 @@ function doTradeReceivedSequence(
|
||||
}
|
||||
|
||||
function generateRandomTraderName() {
|
||||
const length = TrainerType.YOUNGSTER - TrainerType.ACE_TRAINER + 1;
|
||||
// +1 avoids TrainerType.UNKNOWN
|
||||
const classKey = `trainersCommon:${TrainerType[randInt(length) + 1]}`;
|
||||
// Some trainers have 2 gendered pools, some do not
|
||||
const genderKey = i18next.exists(`${classKey}.MALE`) ? (randInt(2) === 0 ? ".MALE" : ".FEMALE") : "";
|
||||
const trainerNameKey = randSeedItem(Object.keys(i18next.t(`${classKey}${genderKey}`, { returnObjects: true })));
|
||||
const trainerNameString = i18next.t(`${classKey}${genderKey}.${trainerNameKey}`);
|
||||
// Some names have an '&' symbol and need to be trimmed to a single name instead of a double name
|
||||
const trainerNames = trainerNameString.split(" & ");
|
||||
return trainerNames[randInt(trainerNames.length)];
|
||||
const allTrainerNames = getEnumKeys(TrainerType);
|
||||
// Exclude TrainerType.UNKNOWN and everything after Ace Trainers (grunts and unique trainers)
|
||||
const eligibleNames = allTrainerNames.slice(
|
||||
1,
|
||||
allTrainerNames.indexOf(TrainerType[TrainerType.YOUNGSTER] as keyof typeof TrainerType),
|
||||
);
|
||||
const randomTrainer = toCamelCase(randSeedItem(eligibleNames));
|
||||
const classKey = `trainersCommon:${randomTrainer}`;
|
||||
// Pick a random gender for ones with gendered pools, or access the raw object for ones without.
|
||||
const genderKey = i18next.exists(`${classKey}.male`) ? randSeedItem([".male", ".female"]) : "";
|
||||
const trainerNameString = getRandomLocaleEntry(`${classKey}${genderKey}`)[1];
|
||||
// Split the string by &s (for duo trainers)
|
||||
return randSeedItem(trainerNameString.split(" & "));
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import { modifierTypes } from "#data/data-lists";
|
||||
import type { IEggOptions } from "#data/egg";
|
||||
import { getPokeballTintColor } from "#data/pokeball";
|
||||
import { BiomeId } from "#enums/biome-id";
|
||||
import { Challenges } from "#enums/challenges";
|
||||
import { EggSourceType } from "#enums/egg-source-types";
|
||||
import { EggTier } from "#enums/egg-type";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
@ -130,6 +131,7 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter = MysteryEncount
|
||||
MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER,
|
||||
)
|
||||
.withEncounterTier(MysteryEncounterTier.ULTRA)
|
||||
.withDisallowedChallenges(Challenges.HARDCORE)
|
||||
.withSceneWaveRangeRequirement(25, 180)
|
||||
.withScenePartySizeRequirement(4, 6, true) // Must have at least 4 legal pokemon in party
|
||||
.withIntroSpriteConfigs([]) // These are set in onInit()
|
||||
|
@ -153,7 +153,7 @@ export const TrashToTreasureEncounter: MysteryEncounter = MysteryEncounterBuilde
|
||||
doGarbageDig();
|
||||
})
|
||||
.withOptionPhase(async () => {
|
||||
// Gain 2 Leftovers and 1 Shell Bell
|
||||
// Gain 1 Leftovers and 1 Shell Bell
|
||||
await transitionMysteryEncounterIntroVisuals();
|
||||
await tryApplyDigRewardItems();
|
||||
|
||||
@ -231,21 +231,7 @@ async function tryApplyDigRewardItems() {
|
||||
const party = globalScene.getPlayerParty();
|
||||
|
||||
// Iterate over the party until an item was successfully given
|
||||
// First leftovers
|
||||
for (const pokemon of party) {
|
||||
const heldItems = globalScene.findModifiers(
|
||||
m => m instanceof PokemonHeldItemModifier && m.pokemonId === pokemon.id,
|
||||
true,
|
||||
) as PokemonHeldItemModifier[];
|
||||
const existingLeftovers = heldItems.find(m => m instanceof TurnHealModifier) as TurnHealModifier;
|
||||
|
||||
if (!existingLeftovers || existingLeftovers.getStackCount() < existingLeftovers.getMaxStackCount()) {
|
||||
await applyModifierTypeToPlayerPokemon(pokemon, leftovers);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Second leftovers
|
||||
// Only Leftovers
|
||||
for (const pokemon of party) {
|
||||
const heldItems = globalScene.findModifiers(
|
||||
m => m instanceof PokemonHeldItemModifier && m.pokemonId === pokemon.id,
|
||||
@ -263,7 +249,7 @@ async function tryApplyDigRewardItems() {
|
||||
await showEncounterText(
|
||||
i18next.t("battle:rewardGainCount", {
|
||||
modifierName: leftovers.name,
|
||||
count: 2,
|
||||
count: 1,
|
||||
}),
|
||||
null,
|
||||
undefined,
|
||||
|
@ -2,6 +2,7 @@ import { globalScene } from "#app/global-scene";
|
||||
import { allSpecies, modifierTypes } from "#data/data-lists";
|
||||
import { getLevelTotalExp } from "#data/exp";
|
||||
import type { PokemonSpecies } from "#data/pokemon-species";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { Challenges } from "#enums/challenges";
|
||||
import { ModifierTier } from "#enums/modifier-tier";
|
||||
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
|
||||
@ -10,8 +11,9 @@ import { MysteryEncounterType } from "#enums/mystery-encounter-type";
|
||||
import { Nature } from "#enums/nature";
|
||||
import { PartyMemberStrength } from "#enums/party-member-strength";
|
||||
import { PlayerGender } from "#enums/player-gender";
|
||||
import { PokemonType } from "#enums/pokemon-type";
|
||||
import { MAX_POKEMON_TYPE, PokemonType } from "#enums/pokemon-type";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import { TrainerType } from "#enums/trainer-type";
|
||||
import type { PlayerPokemon, Pokemon } from "#field/pokemon";
|
||||
import type { PokemonHeldItemModifier } from "#modifiers/modifier";
|
||||
@ -219,6 +221,7 @@ export const WeirdDreamEncounter: MysteryEncounter = MysteryEncounterBuilder.wit
|
||||
await showEncounterText(`${namespace}:option.1.dreamComplete`);
|
||||
|
||||
await doNewTeamPostProcess(transformations);
|
||||
globalScene.phaseManager.unshiftNew("PartyHealPhase", true);
|
||||
setEncounterRewards({
|
||||
guaranteedModifierTypeFuncs: [
|
||||
modifierTypes.MEMORY_MUSHROOM,
|
||||
@ -230,7 +233,7 @@ export const WeirdDreamEncounter: MysteryEncounter = MysteryEncounterBuilder.wit
|
||||
],
|
||||
fillRemaining: false,
|
||||
});
|
||||
leaveEncounterWithoutBattle(true);
|
||||
leaveEncounterWithoutBattle(false);
|
||||
})
|
||||
.build(),
|
||||
)
|
||||
@ -431,6 +434,8 @@ function getTeamTransformations(): PokemonTransformation[] {
|
||||
newAbilityIndex,
|
||||
undefined,
|
||||
);
|
||||
|
||||
transformation.newPokemon.teraType = randSeedInt(MAX_POKEMON_TYPE);
|
||||
}
|
||||
|
||||
return pokemonTransformations;
|
||||
@ -440,6 +445,8 @@ async function doNewTeamPostProcess(transformations: PokemonTransformation[]) {
|
||||
let atLeastOneNewStarter = false;
|
||||
for (const transformation of transformations) {
|
||||
const previousPokemon = transformation.previousPokemon;
|
||||
const oldHpRatio = previousPokemon.getHpRatio(true);
|
||||
const oldStatus = previousPokemon.status;
|
||||
const newPokemon = transformation.newPokemon;
|
||||
const speciesRootForm = newPokemon.species.getRootSpeciesId();
|
||||
|
||||
@ -462,6 +469,19 @@ async function doNewTeamPostProcess(transformations: PokemonTransformation[]) {
|
||||
}
|
||||
|
||||
newPokemon.calculateStats();
|
||||
if (oldHpRatio > 0) {
|
||||
newPokemon.hp = Math.ceil(oldHpRatio * newPokemon.getMaxHp());
|
||||
// Assume that the `status` instance can always safely be transferred to the new pokemon
|
||||
// This is the case (as of version 1.10.4)
|
||||
// Safeguard against COMATOSE here
|
||||
if (!newPokemon.hasAbility(AbilityId.COMATOSE, false, true)) {
|
||||
newPokemon.status = oldStatus;
|
||||
}
|
||||
} else {
|
||||
newPokemon.hp = 0;
|
||||
newPokemon.doSetStatus(StatusEffect.FAINT);
|
||||
}
|
||||
|
||||
await newPokemon.updateInfo();
|
||||
}
|
||||
|
||||
|
@ -309,7 +309,7 @@ export function getRandomSpeciesByStarterCost(
|
||||
*/
|
||||
export function koPlayerPokemon(pokemon: PlayerPokemon) {
|
||||
pokemon.hp = 0;
|
||||
pokemon.trySetStatus(StatusEffect.FAINT);
|
||||
pokemon.doSetStatus(StatusEffect.FAINT);
|
||||
pokemon.updateInfo();
|
||||
queueEncounterMessage(
|
||||
i18next.t("battle:fainted", {
|
||||
|
@ -44,6 +44,34 @@ export abstract class PhasePriorityQueue {
|
||||
public clear(): void {
|
||||
this.queue.splice(0, this.queue.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to remove one or more Phases from the current queue.
|
||||
* @param phaseFilter - The function to select phases for removal
|
||||
* @param removeCount - The maximum number of phases to remove, or `all` to remove all matching phases;
|
||||
* default `1`
|
||||
* @returns The number of successfully removed phases
|
||||
* @todo Remove this eventually once the patchwork bug this is used for is fixed
|
||||
*/
|
||||
public tryRemovePhase(phaseFilter: (phase: Phase) => boolean, removeCount: number | "all" = 1): number {
|
||||
if (removeCount === "all") {
|
||||
removeCount = this.queue.length;
|
||||
} else if (removeCount < 1) {
|
||||
return 0;
|
||||
}
|
||||
let numRemoved = 0;
|
||||
|
||||
do {
|
||||
const phaseIndex = this.queue.findIndex(phaseFilter);
|
||||
if (phaseIndex === -1) {
|
||||
break;
|
||||
}
|
||||
this.queue.splice(phaseIndex, 1);
|
||||
numRemoved++;
|
||||
} while (numRemoved < removeCount && this.queue.length > 0);
|
||||
|
||||
return numRemoved;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -795,7 +795,7 @@ export class PokemonSpecies extends PokemonSpeciesForm implements Localizable {
|
||||
return Gender.GENDERLESS;
|
||||
}
|
||||
|
||||
if (randSeedFloat() <= this.malePercent) {
|
||||
if (randSeedFloat() * 100 <= this.malePercent) {
|
||||
return Gender.MALE;
|
||||
}
|
||||
return Gender.FEMALE;
|
||||
|
@ -11,6 +11,7 @@ import type { MoveId } from "#enums/move-id";
|
||||
import type { Nature } from "#enums/nature";
|
||||
import type { PokemonType } from "#enums/pokemon-type";
|
||||
import type { SpeciesId } from "#enums/species-id";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import type { AttackMoveResult } from "#types/attack-move-result";
|
||||
import type { IllusionData } from "#types/illusion-data";
|
||||
import type { TurnMove } from "#types/turn-move";
|
||||
@ -326,6 +327,14 @@ export class PokemonTurnData {
|
||||
public switchedInThisTurn = false;
|
||||
public failedRunAway = false;
|
||||
public joinedRound = false;
|
||||
/** Tracker for a pending status effect
|
||||
*
|
||||
* @remarks
|
||||
* Set whenever {@linkcode Pokemon#trySetStatus} succeeds in order to prevent subsequent status effects
|
||||
* from being applied. Necessary because the status is not actually set until the {@linkcode ObtainStatusEffectPhase} runs,
|
||||
* which may not happen before another status effect is attempted to be applied.
|
||||
*/
|
||||
public pendingStatus: StatusEffect = StatusEffect.NONE;
|
||||
/**
|
||||
* The amount of times this Pokemon has acted again and used a move in the current turn.
|
||||
* Used to make sure multi-hits occur properly when the user is
|
||||
|
@ -1,165 +0,0 @@
|
||||
import { TrainerType } from "#enums/trainer-type";
|
||||
import { toPascalSnakeCase } from "#utils/strings";
|
||||
|
||||
class TrainerNameConfig {
|
||||
public urls: string[];
|
||||
public femaleUrls: string[] | null;
|
||||
|
||||
constructor(type: TrainerType, ...urls: string[]) {
|
||||
this.urls = urls.length ? urls : [toPascalSnakeCase(TrainerType[type])];
|
||||
}
|
||||
|
||||
hasGenderVariant(...femaleUrls: string[]): TrainerNameConfig {
|
||||
this.femaleUrls = femaleUrls.length ? femaleUrls : null;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
interface TrainerNameConfigs {
|
||||
[key: number]: TrainerNameConfig;
|
||||
}
|
||||
|
||||
// used in a commented code
|
||||
// biome-ignore lint/correctness/noUnusedVariables: Used by commented code
|
||||
const trainerNameConfigs: TrainerNameConfigs = {
|
||||
[TrainerType.ACE_TRAINER]: new TrainerNameConfig(TrainerType.ACE_TRAINER),
|
||||
[TrainerType.ARTIST]: new TrainerNameConfig(TrainerType.ARTIST),
|
||||
[TrainerType.BACKERS]: new TrainerNameConfig(TrainerType.BACKERS),
|
||||
[TrainerType.BACKPACKER]: new TrainerNameConfig(TrainerType.BACKPACKER),
|
||||
[TrainerType.BAKER]: new TrainerNameConfig(TrainerType.BAKER),
|
||||
[TrainerType.BEAUTY]: new TrainerNameConfig(TrainerType.BEAUTY),
|
||||
[TrainerType.BIKER]: new TrainerNameConfig(TrainerType.BIKER),
|
||||
[TrainerType.BLACK_BELT]: new TrainerNameConfig(TrainerType.BLACK_BELT).hasGenderVariant("Battle_Girl"),
|
||||
[TrainerType.BREEDER]: new TrainerNameConfig(TrainerType.BREEDER, "Pokémon_Breeder"),
|
||||
[TrainerType.CLERK]: new TrainerNameConfig(TrainerType.CLERK),
|
||||
[TrainerType.CYCLIST]: new TrainerNameConfig(TrainerType.CYCLIST),
|
||||
[TrainerType.DANCER]: new TrainerNameConfig(TrainerType.DANCER),
|
||||
[TrainerType.DEPOT_AGENT]: new TrainerNameConfig(TrainerType.DEPOT_AGENT),
|
||||
[TrainerType.DOCTOR]: new TrainerNameConfig(TrainerType.DOCTOR).hasGenderVariant("Nurse"),
|
||||
[TrainerType.FIREBREATHER]: new TrainerNameConfig(TrainerType.FIREBREATHER),
|
||||
[TrainerType.FISHERMAN]: new TrainerNameConfig(TrainerType.FISHERMAN),
|
||||
[TrainerType.GUITARIST]: new TrainerNameConfig(TrainerType.GUITARIST),
|
||||
[TrainerType.HARLEQUIN]: new TrainerNameConfig(TrainerType.HARLEQUIN),
|
||||
[TrainerType.HIKER]: new TrainerNameConfig(TrainerType.HIKER),
|
||||
[TrainerType.HOOLIGANS]: new TrainerNameConfig(TrainerType.HOOLIGANS),
|
||||
[TrainerType.HOOPSTER]: new TrainerNameConfig(TrainerType.HOOPSTER),
|
||||
[TrainerType.INFIELDER]: new TrainerNameConfig(TrainerType.INFIELDER),
|
||||
[TrainerType.JANITOR]: new TrainerNameConfig(TrainerType.JANITOR),
|
||||
[TrainerType.LINEBACKER]: new TrainerNameConfig(TrainerType.LINEBACKER),
|
||||
[TrainerType.MAID]: new TrainerNameConfig(TrainerType.MAID),
|
||||
[TrainerType.MUSICIAN]: new TrainerNameConfig(TrainerType.MUSICIAN),
|
||||
[TrainerType.HEX_MANIAC]: new TrainerNameConfig(TrainerType.HEX_MANIAC),
|
||||
[TrainerType.NURSERY_AIDE]: new TrainerNameConfig(TrainerType.NURSERY_AIDE),
|
||||
[TrainerType.OFFICER]: new TrainerNameConfig(TrainerType.OFFICER),
|
||||
[TrainerType.PARASOL_LADY]: new TrainerNameConfig(TrainerType.PARASOL_LADY),
|
||||
[TrainerType.PILOT]: new TrainerNameConfig(TrainerType.PILOT),
|
||||
[TrainerType.POKEFAN]: new TrainerNameConfig(TrainerType.POKEFAN, "Poké_Fan"),
|
||||
[TrainerType.PRESCHOOLER]: new TrainerNameConfig(TrainerType.PRESCHOOLER),
|
||||
[TrainerType.PSYCHIC]: new TrainerNameConfig(TrainerType.PSYCHIC),
|
||||
[TrainerType.RANGER]: new TrainerNameConfig(TrainerType.RANGER),
|
||||
[TrainerType.RICH]: new TrainerNameConfig(TrainerType.RICH, "Gentleman").hasGenderVariant("Madame"),
|
||||
[TrainerType.RICH_KID]: new TrainerNameConfig(TrainerType.RICH_KID, "Rich_Boy").hasGenderVariant("Lady"),
|
||||
[TrainerType.ROUGHNECK]: new TrainerNameConfig(TrainerType.ROUGHNECK),
|
||||
[TrainerType.SAILOR]: new TrainerNameConfig(TrainerType.SAILOR),
|
||||
[TrainerType.SCIENTIST]: new TrainerNameConfig(TrainerType.SCIENTIST),
|
||||
[TrainerType.SMASHER]: new TrainerNameConfig(TrainerType.SMASHER),
|
||||
[TrainerType.SNOW_WORKER]: new TrainerNameConfig(TrainerType.SNOW_WORKER, "Worker"),
|
||||
[TrainerType.STRIKER]: new TrainerNameConfig(TrainerType.STRIKER),
|
||||
[TrainerType.SCHOOL_KID]: new TrainerNameConfig(TrainerType.SCHOOL_KID, "School_Kid"),
|
||||
[TrainerType.SWIMMER]: new TrainerNameConfig(TrainerType.SWIMMER),
|
||||
[TrainerType.TWINS]: new TrainerNameConfig(TrainerType.TWINS),
|
||||
[TrainerType.VETERAN]: new TrainerNameConfig(TrainerType.VETERAN),
|
||||
[TrainerType.WAITER]: new TrainerNameConfig(TrainerType.WAITER).hasGenderVariant("Waitress"),
|
||||
[TrainerType.WORKER]: new TrainerNameConfig(TrainerType.WORKER),
|
||||
[TrainerType.YOUNGSTER]: new TrainerNameConfig(TrainerType.YOUNGSTER).hasGenderVariant("Lass"),
|
||||
};
|
||||
|
||||
// function used in a commented code
|
||||
// biome-ignore lint/correctness/noUnusedVariables: TODO make this into a script instead of having it be in src/data...
|
||||
function fetchAndPopulateTrainerNames(
|
||||
url: string,
|
||||
parser: DOMParser,
|
||||
trainerNames: Set<string>,
|
||||
femaleTrainerNames: Set<string>,
|
||||
forceFemale = false,
|
||||
) {
|
||||
return new Promise<void>(resolve => {
|
||||
fetch(`https://bulbapedia.bulbagarden.net/wiki/${url}_(Trainer_class)`)
|
||||
.then(response => response.text())
|
||||
.then(html => {
|
||||
console.log(url);
|
||||
const htmlDoc = parser.parseFromString(html, "text/html");
|
||||
const trainerListHeader = htmlDoc.querySelector("#Trainer_list")?.parentElement;
|
||||
if (!trainerListHeader) {
|
||||
return [];
|
||||
}
|
||||
const elements = [...(trainerListHeader?.parentElement?.childNodes ?? [])];
|
||||
const startChildIndex = elements.indexOf(trainerListHeader);
|
||||
const endChildIndex = elements.findIndex(h => h.nodeName === "H2" && elements.indexOf(h) > startChildIndex);
|
||||
const tables = elements
|
||||
.filter(t => {
|
||||
if (t.nodeName !== "TABLE" || t["className"] !== "expandable") {
|
||||
return false;
|
||||
}
|
||||
const childIndex = elements.indexOf(t);
|
||||
return childIndex > startChildIndex && childIndex < endChildIndex;
|
||||
})
|
||||
.map(t => t as Element);
|
||||
console.log(url, tables);
|
||||
for (const table of tables) {
|
||||
const trainerRows = [...table.querySelectorAll("tr:not(:first-child)")].filter(r => r.children.length === 9);
|
||||
for (const row of trainerRows) {
|
||||
const nameCell = row.firstElementChild;
|
||||
if (!nameCell) {
|
||||
continue;
|
||||
}
|
||||
const content = nameCell.innerHTML;
|
||||
if (content.indexOf(" <a ") > -1) {
|
||||
const female = /♀/.test(content);
|
||||
if (url === "Twins") {
|
||||
console.log(content);
|
||||
}
|
||||
const nameMatch = />([a-z]+(?: & [a-z]+)?)<\/a>/i.exec(content);
|
||||
if (nameMatch) {
|
||||
(female || forceFemale ? femaleTrainerNames : trainerNames).add(nameMatch[1].replace("&", "&"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/*export function scrapeTrainerNames() {
|
||||
const parser = new DOMParser();
|
||||
const trainerTypeNames = {};
|
||||
const populateTrainerNamePromises: Promise<void>[] = [];
|
||||
for (let t of Object.keys(trainerNameConfigs)) {
|
||||
populateTrainerNamePromises.push(new Promise<void>(resolve => {
|
||||
const trainerType = t;
|
||||
trainerTypeNames[trainerType] = [];
|
||||
|
||||
const config = trainerNameConfigs[t] as TrainerNameConfig;
|
||||
const trainerNames = new Set<string>();
|
||||
const femaleTrainerNames = new Set<string>();
|
||||
console.log(config.urls, config.femaleUrls)
|
||||
const trainerClassRequests = config.urls.map(u => fetchAndPopulateTrainerNames(u, parser, trainerNames, femaleTrainerNames));
|
||||
if (config.femaleUrls)
|
||||
trainerClassRequests.push(...config.femaleUrls.map(u => fetchAndPopulateTrainerNames(u, parser, null, femaleTrainerNames, true)));
|
||||
Promise.all(trainerClassRequests).then(() => {
|
||||
console.log(trainerNames, femaleTrainerNames)
|
||||
trainerTypeNames[trainerType] = !femaleTrainerNames.size ? Array.from(trainerNames) : [ Array.from(trainerNames), Array.from(femaleTrainerNames) ];
|
||||
resolve();
|
||||
});
|
||||
}));
|
||||
}
|
||||
Promise.all(populateTrainerNamePromises).then(() => {
|
||||
let output = 'export const trainerNamePools = {';
|
||||
Object.keys(trainerTypeNames).forEach(t => {
|
||||
output += `\n\t[TrainerType.${TrainerType[t]}]: ${JSON.stringify(trainerTypeNames[t])},`;
|
||||
});
|
||||
output += `\n};`;
|
||||
console.log(output);
|
||||
});
|
||||
}*/
|
@ -1346,11 +1346,12 @@ export const trainerConfigs: TrainerConfigs = {
|
||||
[TrainerPoolTier.RARE]: [
|
||||
SpeciesId.YANMA,
|
||||
SpeciesId.NINJASK,
|
||||
SpeciesId.WHIRLIPEDE,
|
||||
SpeciesId.VENIPEDE,
|
||||
SpeciesId.EMOLGA,
|
||||
SpeciesId.SKIDDO,
|
||||
SpeciesId.ROLYCOLY,
|
||||
],
|
||||
[TrainerPoolTier.SUPER_RARE]: [SpeciesId.ACCELGOR, SpeciesId.DREEPY],
|
||||
[TrainerPoolTier.SUPER_RARE]: [SpeciesId.SHELMET, SpeciesId.DREEPY],
|
||||
}),
|
||||
[TrainerType.DANCER]: new TrainerConfig(++t)
|
||||
.setMoneyMultiplier(1.55)
|
||||
@ -1396,7 +1397,6 @@ export const trainerConfigs: TrainerConfigs = {
|
||||
trainerPartyTemplates.ONE_AVG,
|
||||
trainerPartyTemplates.THREE_WEAK_SAME,
|
||||
trainerPartyTemplates.ONE_STRONG,
|
||||
trainerPartyTemplates.SIX_WEAKER,
|
||||
)
|
||||
.setSpeciesPools({
|
||||
[TrainerPoolTier.COMMON]: [
|
||||
@ -1510,7 +1510,6 @@ export const trainerConfigs: TrainerConfigs = {
|
||||
.setMoneyMultiplier(1.1)
|
||||
.setEncounterBgm(TrainerType.POKEFAN)
|
||||
.setPartyTemplates(
|
||||
trainerPartyTemplates.FOUR_WEAKER,
|
||||
trainerPartyTemplates.THREE_WEAK,
|
||||
trainerPartyTemplates.TWO_WEAK_ONE_AVG,
|
||||
trainerPartyTemplates.TWO_AVG,
|
||||
@ -1550,7 +1549,7 @@ export const trainerConfigs: TrainerConfigs = {
|
||||
],
|
||||
[TrainerPoolTier.UNCOMMON]: [SpeciesId.HOUNDOUR, SpeciesId.ROCKRUFF, SpeciesId.MASCHIFF],
|
||||
[TrainerPoolTier.RARE]: [SpeciesId.JOLTEON, SpeciesId.RIOLU],
|
||||
[TrainerPoolTier.SUPER_RARE]: [],
|
||||
[TrainerPoolTier.SUPER_RARE]: [SpeciesId.SLAKOTH],
|
||||
[TrainerPoolTier.ULTRA_RARE]: [SpeciesId.ENTEI, SpeciesId.SUICUNE, SpeciesId.RAIKOU],
|
||||
}),
|
||||
[TrainerType.PARASOL_LADY]: new TrainerConfig(++t)
|
||||
@ -1595,13 +1594,10 @@ export const trainerConfigs: TrainerConfigs = {
|
||||
.setHasDouble("Pokéfan Family")
|
||||
.setEncounterBgm(TrainerType.POKEFAN)
|
||||
.setPartyTemplates(
|
||||
trainerPartyTemplates.SIX_WEAKER,
|
||||
trainerPartyTemplates.FOUR_WEAK,
|
||||
trainerPartyTemplates.TWO_AVG,
|
||||
trainerPartyTemplates.ONE_STRONG,
|
||||
trainerPartyTemplates.FOUR_WEAK_SAME,
|
||||
trainerPartyTemplates.FIVE_WEAK,
|
||||
trainerPartyTemplates.SIX_WEAKER_SAME,
|
||||
)
|
||||
.setSpeciesFilter(s => tmSpecies[MoveId.HELPING_HAND].indexOf(s.speciesId) > -1),
|
||||
[TrainerType.PRESCHOOLER]: new TrainerConfig(++t)
|
||||
@ -1609,12 +1605,7 @@ export const trainerConfigs: TrainerConfigs = {
|
||||
.setEncounterBgm(TrainerType.YOUNGSTER)
|
||||
.setHasGenders("Preschooler Female", "lass")
|
||||
.setHasDouble("Preschoolers")
|
||||
.setPartyTemplates(
|
||||
trainerPartyTemplates.THREE_WEAK,
|
||||
trainerPartyTemplates.FOUR_WEAKER,
|
||||
trainerPartyTemplates.TWO_WEAK_SAME_ONE_AVG,
|
||||
trainerPartyTemplates.FIVE_WEAKER,
|
||||
)
|
||||
.setPartyTemplates(trainerPartyTemplates.THREE_WEAK, trainerPartyTemplates.TWO_WEAK_SAME_ONE_AVG)
|
||||
.setSpeciesPools({
|
||||
[TrainerPoolTier.COMMON]: [
|
||||
SpeciesId.CATERPIE,
|
||||
@ -1748,11 +1739,7 @@ export const trainerConfigs: TrainerConfigs = {
|
||||
.setHasGenders("Lady")
|
||||
.setHasDouble("Rich Kids")
|
||||
.setEncounterBgm(TrainerType.RICH)
|
||||
.setPartyTemplates(
|
||||
trainerPartyTemplates.FOUR_WEAKER,
|
||||
trainerPartyTemplates.THREE_WEAK_SAME,
|
||||
trainerPartyTemplates.TWO_WEAK_SAME_ONE_AVG,
|
||||
)
|
||||
.setPartyTemplates(trainerPartyTemplates.THREE_WEAK_SAME, trainerPartyTemplates.TWO_WEAK_SAME_ONE_AVG)
|
||||
.setSpeciesFilter(s => s.baseTotal <= 460),
|
||||
[TrainerType.ROUGHNECK]: new TrainerConfig(++t)
|
||||
.setMoneyMultiplier(1.4)
|
||||
@ -1866,6 +1853,7 @@ export const trainerConfigs: TrainerConfigs = {
|
||||
SpeciesId.METAPOD,
|
||||
SpeciesId.LEDYBA,
|
||||
SpeciesId.CLEFFA,
|
||||
SpeciesId.MAREEP,
|
||||
SpeciesId.WOOPER,
|
||||
SpeciesId.TEDDIURSA,
|
||||
SpeciesId.REMORAID,
|
||||
@ -1877,6 +1865,7 @@ export const trainerConfigs: TrainerConfigs = {
|
||||
SpeciesId.BONSLY,
|
||||
SpeciesId.PETILIL,
|
||||
SpeciesId.SPRITZEE,
|
||||
SpeciesId.BOUNSWEET,
|
||||
SpeciesId.MILCERY,
|
||||
SpeciesId.PICHU,
|
||||
]),
|
||||
@ -1888,6 +1877,7 @@ export const trainerConfigs: TrainerConfigs = {
|
||||
SpeciesId.KAKUNA,
|
||||
SpeciesId.SPINARAK,
|
||||
SpeciesId.IGGLYBUFF,
|
||||
SpeciesId.MAREEP,
|
||||
SpeciesId.PALDEA_WOOPER,
|
||||
SpeciesId.PHANPY,
|
||||
SpeciesId.MANTYKE,
|
||||
@ -1899,6 +1889,7 @@ export const trainerConfigs: TrainerConfigs = {
|
||||
SpeciesId.MIME_JR,
|
||||
SpeciesId.COTTONEE,
|
||||
SpeciesId.SWIRLIX,
|
||||
SpeciesId.FOMANTIS,
|
||||
SpeciesId.FIDOUGH,
|
||||
SpeciesId.EEVEE,
|
||||
],
|
||||
@ -3836,7 +3827,7 @@ export const trainerConfigs: TrainerConfigs = {
|
||||
p.setBoss(true, 2);
|
||||
}),
|
||||
)
|
||||
.setInstantTera(2), // Tera Fire Arcanine, Tera Grass Exeggutor, Tera Water Gyarados
|
||||
.setInstantTera(1), // Tera Fire Arcanine, Tera Grass Exeggutor, Tera Water Gyarados
|
||||
[TrainerType.RED]: new TrainerConfig(++t)
|
||||
.initForChampion(true)
|
||||
.setBattleBgm("battle_johto_champion")
|
||||
@ -4025,19 +4016,14 @@ export const trainerConfigs: TrainerConfigs = {
|
||||
.setBattleBgm("battle_sinnoh_champion")
|
||||
.setMixedBattleBgm("battle_sinnoh_champion")
|
||||
.setPartyMemberFunc(0, getRandomPartyMemberFunc([SpeciesId.SPIRITOMB]))
|
||||
.setPartyMemberFunc(1, getRandomPartyMemberFunc([SpeciesId.MILOTIC, SpeciesId.ROSERADE, SpeciesId.HISUI_ARCANINE]))
|
||||
.setPartyMemberFunc(
|
||||
1,
|
||||
getRandomPartyMemberFunc(
|
||||
[SpeciesId.MILOTIC, SpeciesId.ROSERADE, SpeciesId.HISUI_ARCANINE],
|
||||
TrainerSlot.TRAINER,
|
||||
true,
|
||||
p => {
|
||||
p.generateAndPopulateMoveset();
|
||||
p.teraType = p.species.type1;
|
||||
},
|
||||
),
|
||||
2,
|
||||
getRandomPartyMemberFunc([SpeciesId.TOGEKISS], TrainerSlot.TRAINER, true, p => {
|
||||
p.generateAndPopulateMoveset();
|
||||
p.teraType = p.species.type1;
|
||||
}),
|
||||
)
|
||||
.setPartyMemberFunc(2, getRandomPartyMemberFunc([SpeciesId.TOGEKISS]))
|
||||
.setPartyMemberFunc(3, getRandomPartyMemberFunc([SpeciesId.LUCARIO]))
|
||||
.setPartyMemberFunc(
|
||||
4,
|
||||
@ -4056,7 +4042,7 @@ export const trainerConfigs: TrainerConfigs = {
|
||||
p.setBoss(true, 2);
|
||||
}),
|
||||
)
|
||||
.setInstantTera(1), // Tera Water Milotic / Grass Roserade / Fire Hisuian Arcanine
|
||||
.setInstantTera(2), // Tera Fairy Togekiss
|
||||
[TrainerType.ALDER]: new TrainerConfig(++t)
|
||||
.initForChampion(true)
|
||||
.setHasDouble("alder_iris_double")
|
||||
@ -5809,7 +5795,7 @@ export const trainerConfigs: TrainerConfigs = {
|
||||
p.generateName();
|
||||
}),
|
||||
)
|
||||
.setInstantTera(3), // Tera Fairy Sylveon
|
||||
.setInstantTera(4), // Tera Fairy Sylveon
|
||||
[TrainerType.PENNY_2]: new TrainerConfig(++t)
|
||||
.setName("Cassiopeia")
|
||||
.initForEvilTeamLeader("Star Boss", [], true)
|
||||
|
@ -1,5 +1,5 @@
|
||||
/**
|
||||
* The index of a given Pokemon on-field.
|
||||
* The index of a given Pokemon on-field. \
|
||||
* Used as an index into `globalScene.getField`, as well as for most target-specifying effects.
|
||||
*/
|
||||
export enum BattlerIndex {
|
||||
|
@ -18,6 +18,11 @@ export enum ChallengeType {
|
||||
* @see {@link Challenge.applyStarterPointCost}
|
||||
*/
|
||||
STARTER_COST,
|
||||
/**
|
||||
* Challenges which modify the starter data in starter select
|
||||
* @see {@link Challenge.applyStarterSelectModify}
|
||||
*/
|
||||
STARTER_SELECT_MODIFY,
|
||||
/**
|
||||
* Challenges which modify your starters in some way
|
||||
* @see {@link Challenge.applyStarterModify}
|
||||
|
@ -20,3 +20,6 @@ export enum PokemonType {
|
||||
FAIRY,
|
||||
STELLAR
|
||||
}
|
||||
|
||||
/** The largest legal value for a {@linkcode PokemonType} (includes Stellar) */
|
||||
export const MAX_POKEMON_TYPE = PokemonType.STELLAR;
|
@ -1,3 +1,5 @@
|
||||
/** Enum representing all non-volatile status effects. */
|
||||
// TODO: Remove StatusEffect.FAINT
|
||||
export enum StatusEffect {
|
||||
NONE,
|
||||
POISON,
|
||||
|
@ -8,7 +8,7 @@ import Overrides from "#app/overrides";
|
||||
import type { BiomeTierTrainerPools, PokemonPools } from "#balance/biomes";
|
||||
import { BiomePoolTier, biomePokemonPools, biomeTrainerPools } from "#balance/biomes";
|
||||
import type { ArenaTag } from "#data/arena-tag";
|
||||
import { ArenaTrapTag, getArenaTag } from "#data/arena-tag";
|
||||
import { EntryHazardTag, getArenaTag } from "#data/arena-tag";
|
||||
import { SpeciesFormChangeRevertWeatherFormTrigger, SpeciesFormChangeWeatherTrigger } from "#data/form-change-triggers";
|
||||
import type { PokemonSpecies } from "#data/pokemon-species";
|
||||
import { PositionalTagManager } from "#data/positional-tags/positional-tag-manager";
|
||||
@ -709,8 +709,8 @@ export class Arena {
|
||||
if (existingTag) {
|
||||
existingTag.onOverlap(this, globalScene.getPokemonById(sourceId));
|
||||
|
||||
if (existingTag instanceof ArenaTrapTag) {
|
||||
const { tagType, side, turnCount, layers, maxLayers } = existingTag as ArenaTrapTag;
|
||||
if (existingTag instanceof EntryHazardTag) {
|
||||
const { tagType, side, turnCount, layers, maxLayers } = existingTag as EntryHazardTag;
|
||||
this.eventTarget.dispatchEvent(new TagAddedEvent(tagType, side, turnCount, layers, maxLayers));
|
||||
}
|
||||
|
||||
@ -723,7 +723,7 @@ export class Arena {
|
||||
newTag.onAdd(this, quiet);
|
||||
this.tags.push(newTag);
|
||||
|
||||
const { layers = 0, maxLayers = 0 } = newTag instanceof ArenaTrapTag ? newTag : {};
|
||||
const { layers = 0, maxLayers = 0 } = newTag instanceof EntryHazardTag ? newTag : {};
|
||||
|
||||
this.eventTarget.dispatchEvent(
|
||||
new TagAddedEvent(newTag.tagType, newTag.side, newTag.turnCount, layers, maxLayers),
|
||||
|
@ -237,6 +237,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
public ivs: number[];
|
||||
public nature: Nature;
|
||||
public moveset: PokemonMove[];
|
||||
/**
|
||||
* This Pokemon's current {@link https://m.bulbapedia.bulbagarden.net/wiki/Status_condition#Non-volatile_status | non-volatile status condition},
|
||||
* or `null` if none exist.
|
||||
* @todo Make private
|
||||
*/
|
||||
public status: Status | null;
|
||||
public friendship: number;
|
||||
public metLevel: number;
|
||||
@ -449,7 +454,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
getNameToRender(useIllusion = true) {
|
||||
const illusion = this.summonData.illusion;
|
||||
const name = useIllusion ? (illusion?.name ?? this.name) : this.name;
|
||||
const nickname: string | undefined = useIllusion ? illusion?.nickname : this.nickname;
|
||||
const nickname: string | undefined = useIllusion ? (illusion?.nickname ?? this.nickname) : this.nickname;
|
||||
try {
|
||||
if (nickname) {
|
||||
return decodeURIComponent(escape(atob(nickname))); // TODO: Remove `atob` and `escape`... eventually...
|
||||
@ -1647,19 +1652,6 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
return precise ? this.hp / this.getMaxHp() : Math.round((this.hp / this.getMaxHp()) * 100) / 100;
|
||||
}
|
||||
|
||||
generateGender(): void {
|
||||
if (this.species.malePercent === null) {
|
||||
this.gender = Gender.GENDERLESS;
|
||||
} else {
|
||||
const genderChance = (this.id % 256) * 0.390625;
|
||||
if (genderChance < this.species.malePercent) {
|
||||
this.gender = Gender.MALE;
|
||||
} else {
|
||||
this.gender = Gender.FEMALE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return this Pokemon's {@linkcode Gender}.
|
||||
* @param ignoreOverride - Whether to ignore any overrides caused by {@linkcode MoveId.TRANSFORM | Transform}; default `false`
|
||||
@ -1776,7 +1768,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
* @returns Whether this Pokemon is currently fused with another species.
|
||||
*/
|
||||
isFusion(useIllusion = false): boolean {
|
||||
return useIllusion ? !!this.summonData.illusion?.fusionSpecies : !!this.fusionSpecies;
|
||||
return !!(useIllusion ? (this.summonData.illusion?.fusionSpecies ?? this.fusionSpecies) : this.fusionSpecies);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -2229,8 +2221,16 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
return this.hasPassive() && (!canApply || this.canApplyAbility(true)) && this.getPassiveAbility().hasAttr(attrType);
|
||||
}
|
||||
|
||||
public getAbilityPriorities(): [number, number] {
|
||||
return [this.getAbility().postSummonPriority, this.getPassiveAbility().postSummonPriority];
|
||||
/**
|
||||
* Return the ability priorities of the pokemon's ability and, if enabled, its passive ability
|
||||
* @returns A tuple containing the ability priorities of the pokemon
|
||||
*/
|
||||
public getAbilityPriorities(): [number] | [activePriority: number, passivePriority: number] {
|
||||
const abilityPriority = this.getAbility().postSummonPriority;
|
||||
if (this.hasPassive()) {
|
||||
return [abilityPriority, this.getPassiveAbility().postSummonPriority];
|
||||
}
|
||||
return [abilityPriority];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -3070,14 +3070,17 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
if (this.level < levelMove[0]) {
|
||||
break;
|
||||
}
|
||||
let weight = levelMove[0];
|
||||
let weight = levelMove[0] + 20;
|
||||
// Evolution Moves
|
||||
if (weight === EVOLVE_MOVE) {
|
||||
weight = 50;
|
||||
if (levelMove[0] === EVOLVE_MOVE) {
|
||||
weight = 70;
|
||||
}
|
||||
// Assume level 1 moves with 80+ BP are "move reminder" moves and bump their weight. Trainers use actual relearn moves.
|
||||
if ((weight === 1 && allMoves[levelMove[1]].power >= 80) || (weight === RELEARN_MOVE && this.hasTrainer())) {
|
||||
weight = 40;
|
||||
if (
|
||||
(levelMove[0] === 1 && allMoves[levelMove[1]].power >= 80) ||
|
||||
(levelMove[0] === RELEARN_MOVE && this.hasTrainer())
|
||||
) {
|
||||
weight = 60;
|
||||
}
|
||||
if (!movePool.some(m => m[0] === levelMove[1]) && !allMoves[levelMove[1]].name.endsWith(" (N)")) {
|
||||
movePool.push([levelMove[1], weight]);
|
||||
@ -3107,11 +3110,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
}
|
||||
if (compatible && !movePool.some(m => m[0] === moveId) && !allMoves[moveId].name.endsWith(" (N)")) {
|
||||
if (tmPoolTiers[moveId] === ModifierTier.COMMON && this.level >= 15) {
|
||||
movePool.push([moveId, 4]);
|
||||
movePool.push([moveId, 24]);
|
||||
} else if (tmPoolTiers[moveId] === ModifierTier.GREAT && this.level >= 30) {
|
||||
movePool.push([moveId, 8]);
|
||||
movePool.push([moveId, 28]);
|
||||
} else if (tmPoolTiers[moveId] === ModifierTier.ULTRA && this.level >= 50) {
|
||||
movePool.push([moveId, 14]);
|
||||
movePool.push([moveId, 34]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3121,7 +3124,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const moveId = speciesEggMoves[this.species.getRootSpeciesId()][i];
|
||||
if (!movePool.some(m => m[0] === moveId) && !allMoves[moveId].name.endsWith(" (N)")) {
|
||||
movePool.push([moveId, 40]);
|
||||
movePool.push([moveId, 60]);
|
||||
}
|
||||
}
|
||||
const moveId = speciesEggMoves[this.species.getRootSpeciesId()][3];
|
||||
@ -3132,13 +3135,13 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
!allMoves[moveId].name.endsWith(" (N)") &&
|
||||
!this.isBoss()
|
||||
) {
|
||||
movePool.push([moveId, 30]);
|
||||
movePool.push([moveId, 50]);
|
||||
}
|
||||
if (this.fusionSpecies) {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const moveId = speciesEggMoves[this.fusionSpecies.getRootSpeciesId()][i];
|
||||
if (!movePool.some(m => m[0] === moveId) && !allMoves[moveId].name.endsWith(" (N)")) {
|
||||
movePool.push([moveId, 40]);
|
||||
movePool.push([moveId, 60]);
|
||||
}
|
||||
}
|
||||
const moveId = speciesEggMoves[this.fusionSpecies.getRootSpeciesId()][3];
|
||||
@ -3149,7 +3152,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
!allMoves[moveId].name.endsWith(" (N)") &&
|
||||
!this.isBoss()
|
||||
) {
|
||||
movePool.push([moveId, 30]);
|
||||
movePool.push([moveId, 50]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3230,6 +3233,18 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
rand -= stabMovePool[index++][1];
|
||||
}
|
||||
this.moveset.push(new PokemonMove(stabMovePool[index][0]));
|
||||
} else {
|
||||
// If there are no damaging STAB moves, just force a random damaging move
|
||||
const attackMovePool = baseWeights.filter(m => allMoves[m[0]].category !== MoveCategory.STATUS);
|
||||
if (attackMovePool.length) {
|
||||
const totalWeight = attackMovePool.reduce((v, m) => v + m[1], 0);
|
||||
let rand = randSeedInt(totalWeight);
|
||||
let index = 0;
|
||||
while (rand > attackMovePool[index][1]) {
|
||||
rand -= attackMovePool[index++][1];
|
||||
}
|
||||
this.moveset.push(new PokemonMove(attackMovePool[index][0], 0, 0));
|
||||
}
|
||||
}
|
||||
|
||||
while (baseWeights.length > this.moveset.length && this.moveset.length < 4) {
|
||||
@ -4746,7 +4761,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
* @param reason - The reason for the status application failure -
|
||||
* can be "overlap" (already has same status), "other" (generic fail message)
|
||||
* or a {@linkcode TerrainType} for terrain-based blockages.
|
||||
* Defaults to "other".
|
||||
* Default `"other"`
|
||||
*/
|
||||
queueStatusImmuneMessage(
|
||||
quiet: boolean,
|
||||
@ -4775,15 +4790,20 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a status effect can be applied to the Pokemon.
|
||||
* Check if a status effect can be applied to this {@linkcode Pokemon}.
|
||||
*
|
||||
* @param effect The {@linkcode StatusEffect} whose applicability is being checked
|
||||
* @param quiet Whether in-battle messages should trigger or not
|
||||
* @param overrideStatus Whether the Pokemon's current status can be overriden
|
||||
* @param sourcePokemon The Pokemon that is setting the status effect
|
||||
* @param ignoreField Whether any field effects (weather, terrain, etc.) should be considered
|
||||
* @param effect - The {@linkcode StatusEffect} whose applicability is being checked
|
||||
* @param quiet - Whether to suppress in-battle messages for status checks; default `false`
|
||||
* @param overrideStatus - Whether to allow overriding the Pokemon's current status with a different one; default `false`
|
||||
* @param sourcePokemon - The {@linkcode Pokemon} applying the status effect to the target,
|
||||
* or `null` if the status is applied from a non-Pokemon source (hazards, etc.); default `null`
|
||||
* @param ignoreField - Whether to ignore field effects (weather, terrain, etc.) preventing status application;
|
||||
* default `false`
|
||||
* @returns Whether {@linkcode effect} can be applied to this Pokemon.
|
||||
*/
|
||||
canSetStatus(
|
||||
// TODO: Review and verify the message order precedence in mainline if multiple status-blocking effects are present at once
|
||||
// TODO: Make argument order consistent with `trySetStatus`
|
||||
public canSetStatus(
|
||||
effect: StatusEffect,
|
||||
quiet = false,
|
||||
overrideStatus = false,
|
||||
@ -4791,7 +4811,9 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
ignoreField = false,
|
||||
): boolean {
|
||||
if (effect !== StatusEffect.FAINT) {
|
||||
if (overrideStatus ? this.status?.effect === effect : this.status) {
|
||||
// Status-overriding moves (i.e. Rest) fail if their respective status already exists;
|
||||
// all other moves fail if the target already has _any_ status
|
||||
if (overrideStatus ? this.status?.effect === effect : this.status || this.turnData.pendingStatus) {
|
||||
this.queueStatusImmuneMessage(quiet, overrideStatus ? "overlap" : "other"); // having different status displays generic fail message
|
||||
return false;
|
||||
}
|
||||
@ -4803,73 +4825,62 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
|
||||
const types = this.getTypes(true, true);
|
||||
|
||||
/* Whether the target is immune to the specific status being applied. */
|
||||
let isImmune = false;
|
||||
/** The reason for a potential blockage; default "other" for type-based. */
|
||||
let reason: "other" | Exclude<TerrainType, TerrainType.NONE> = "other";
|
||||
|
||||
switch (effect) {
|
||||
case StatusEffect.POISON:
|
||||
case StatusEffect.TOXIC: {
|
||||
// Check if the Pokemon is immune to Poison/Toxic or if the source pokemon is canceling the immunity
|
||||
const poisonImmunity = types.map(defType => {
|
||||
// Check if the Pokemon is not immune to Poison/Toxic
|
||||
case StatusEffect.TOXIC:
|
||||
// Check for type based immunities and/or Corrosion from the applier.
|
||||
isImmune = types.some(defType => {
|
||||
// only 1 immunity needed to block
|
||||
if (defType !== PokemonType.POISON && defType !== PokemonType.STEEL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the source Pokemon has an ability that cancels the Poison/Toxic immunity
|
||||
// No source (such as from Toxic Spikes) = blocked by default
|
||||
if (!sourcePokemon) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const cancelImmunity = new BooleanHolder(false);
|
||||
// TODO: Determine if we need to pass `quiet` as the value for simulated in this call
|
||||
if (sourcePokemon) {
|
||||
applyAbAttrs("IgnoreTypeStatusEffectImmunityAbAttr", {
|
||||
pokemon: sourcePokemon,
|
||||
cancelled: cancelImmunity,
|
||||
statusEffect: effect,
|
||||
defenderType: defType,
|
||||
});
|
||||
if (cancelImmunity.value) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
applyAbAttrs("IgnoreTypeStatusEffectImmunityAbAttr", {
|
||||
pokemon: sourcePokemon,
|
||||
cancelled: cancelImmunity,
|
||||
statusEffect: effect,
|
||||
defenderType: defType,
|
||||
});
|
||||
return !cancelImmunity.value;
|
||||
});
|
||||
|
||||
if (this.isOfType(PokemonType.POISON) || this.isOfType(PokemonType.STEEL)) {
|
||||
if (poisonImmunity.includes(true)) {
|
||||
this.queueStatusImmuneMessage(quiet);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case StatusEffect.PARALYSIS:
|
||||
if (this.isOfType(PokemonType.ELECTRIC)) {
|
||||
this.queueStatusImmuneMessage(quiet);
|
||||
return false;
|
||||
}
|
||||
isImmune = this.isOfType(PokemonType.ELECTRIC);
|
||||
break;
|
||||
case StatusEffect.SLEEP:
|
||||
if (this.isGrounded() && globalScene.arena.terrain?.terrainType === TerrainType.ELECTRIC) {
|
||||
this.queueStatusImmuneMessage(quiet, TerrainType.ELECTRIC);
|
||||
return false;
|
||||
}
|
||||
isImmune = this.isGrounded() && globalScene.arena.getTerrainType() === TerrainType.ELECTRIC;
|
||||
reason = TerrainType.ELECTRIC;
|
||||
break;
|
||||
case StatusEffect.FREEZE:
|
||||
if (
|
||||
case StatusEffect.FREEZE: {
|
||||
const weatherType = globalScene.arena.getWeatherType();
|
||||
isImmune =
|
||||
this.isOfType(PokemonType.ICE) ||
|
||||
(!ignoreField &&
|
||||
globalScene?.arena?.weather?.weatherType &&
|
||||
[WeatherType.SUNNY, WeatherType.HARSH_SUN].includes(globalScene.arena.weather.weatherType))
|
||||
) {
|
||||
this.queueStatusImmuneMessage(quiet);
|
||||
return false;
|
||||
}
|
||||
(!ignoreField && (weatherType === WeatherType.SUNNY || weatherType === WeatherType.HARSH_SUN));
|
||||
break;
|
||||
}
|
||||
case StatusEffect.BURN:
|
||||
if (this.isOfType(PokemonType.FIRE)) {
|
||||
this.queueStatusImmuneMessage(quiet);
|
||||
return false;
|
||||
}
|
||||
isImmune = this.isOfType(PokemonType.FIRE);
|
||||
break;
|
||||
}
|
||||
|
||||
if (isImmune) {
|
||||
this.queueStatusImmuneMessage(quiet, reason);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for cancellations from self/ally abilities
|
||||
const cancelled = new BooleanHolder(false);
|
||||
applyAbAttrs("StatusEffectImmunityAbAttr", { pokemon: this, effect, cancelled, simulated: quiet });
|
||||
if (cancelled.value) {
|
||||
@ -4886,14 +4897,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
source: sourcePokemon,
|
||||
});
|
||||
if (cancelled.value) {
|
||||
break;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (cancelled.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Perform safeguard checks
|
||||
if (sourcePokemon && sourcePokemon !== this && this.isSafeguarded(sourcePokemon)) {
|
||||
if (!quiet) {
|
||||
globalScene.phaseManager.queueMessage(
|
||||
@ -4906,18 +4914,36 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
return true;
|
||||
}
|
||||
|
||||
trySetStatus(
|
||||
effect?: StatusEffect,
|
||||
asPhase = false,
|
||||
/**
|
||||
* Attempt to set this Pokemon's status to the specified condition.
|
||||
* Enqueues a new `ObtainStatusEffectPhase` to trigger animations, etc.
|
||||
* @param effect - The {@linkcode StatusEffect} to set
|
||||
* @param sourcePokemon - The {@linkcode Pokemon} applying the status effect to the target,
|
||||
* or `null` if the status is applied from a non-Pokemon source (hazards, etc.); default `null`
|
||||
* @param sleepTurnsRemaining - The number of turns to set {@linkcode StatusEffect.SLEEP} for;
|
||||
* defaults to a random number between 2 and 4 and is unused for non-Sleep statuses
|
||||
* @param sourceText - The text to show for the source of the status effect, if any; default `null`
|
||||
* @param overrideStatus - Whether to allow overriding the Pokemon's current status with a different one; default `false`
|
||||
* @param quiet - Whether to suppress in-battle messages for status checks; default `true`
|
||||
* @param overrideMessage - String containing text to be displayed upon status setting; defaults to normal key for status
|
||||
* and is used exclusively for Rest
|
||||
* @returns Whether the status effect phase was successfully created.
|
||||
* @see {@linkcode doSetStatus} - alternate function that sets status immediately (albeit without condition checks).
|
||||
*/
|
||||
public trySetStatus(
|
||||
effect: StatusEffect,
|
||||
sourcePokemon: Pokemon | null = null,
|
||||
turnsRemaining = 0,
|
||||
sleepTurnsRemaining?: number,
|
||||
sourceText: string | null = null,
|
||||
overrideStatus?: boolean,
|
||||
quiet = true,
|
||||
overrideMessage?: string,
|
||||
): boolean {
|
||||
// TODO: This needs to propagate failure status for status moves
|
||||
if (!effect) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.canSetStatus(effect, quiet, overrideStatus, sourcePokemon)) {
|
||||
return false;
|
||||
}
|
||||
@ -4937,50 +4963,114 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
}
|
||||
}
|
||||
|
||||
if (asPhase) {
|
||||
if (overrideStatus) {
|
||||
this.resetStatus(false);
|
||||
}
|
||||
globalScene.phaseManager.unshiftNew(
|
||||
"ObtainStatusEffectPhase",
|
||||
this.getBattlerIndex(),
|
||||
effect,
|
||||
turnsRemaining,
|
||||
sourceText,
|
||||
sourcePokemon,
|
||||
);
|
||||
return true;
|
||||
if (overrideStatus) {
|
||||
this.resetStatus(false);
|
||||
} else {
|
||||
this.turnData.pendingStatus = effect;
|
||||
}
|
||||
|
||||
let sleepTurnsRemaining: NumberHolder;
|
||||
|
||||
if (effect === StatusEffect.SLEEP) {
|
||||
sleepTurnsRemaining = new NumberHolder(this.randBattleSeedIntRange(2, 4));
|
||||
|
||||
this.setFrameRate(4);
|
||||
|
||||
// If the user is invulnerable, lets remove their invulnerability when they fall asleep
|
||||
const invulnerableTags = [
|
||||
BattlerTagType.UNDERGROUND,
|
||||
BattlerTagType.UNDERWATER,
|
||||
BattlerTagType.HIDDEN,
|
||||
BattlerTagType.FLYING,
|
||||
];
|
||||
|
||||
const tag = invulnerableTags.find(t => this.getTag(t));
|
||||
|
||||
if (tag) {
|
||||
this.removeTag(tag);
|
||||
this.getMoveQueue().pop();
|
||||
}
|
||||
}
|
||||
|
||||
sleepTurnsRemaining = sleepTurnsRemaining!; // tell TS compiler it's defined
|
||||
this.status = new Status(effect, 0, sleepTurnsRemaining?.value);
|
||||
globalScene.phaseManager.unshiftNew(
|
||||
"ObtainStatusEffectPhase",
|
||||
this.getBattlerIndex(),
|
||||
effect,
|
||||
sourcePokemon,
|
||||
sleepTurnsRemaining,
|
||||
sourceText,
|
||||
overrideMessage,
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set this Pokemon's {@linkcode status | non-volatile status condition} to the specified effect.
|
||||
* @param effect - The {@linkcode StatusEffect} to set
|
||||
* @remarks
|
||||
* Clears this pokemon's `pendingStatus` in its {@linkcode Pokemon.turnData | turnData}.
|
||||
*
|
||||
* ⚠️ This method does **not** check for feasibility; that is the responsibility of the caller.
|
||||
*/
|
||||
doSetStatus(effect: Exclude<StatusEffect, StatusEffect.SLEEP>): void;
|
||||
/**
|
||||
* Set this Pokemon's {@linkcode status | non-volatile status condition} to the specified effect.
|
||||
* @param effect - {@linkcode StatusEffect.SLEEP}
|
||||
* @param sleepTurnsRemaining - The number of turns to inflict sleep for; defaults to a random number between 2 and 4
|
||||
* @remarks
|
||||
* Clears this pokemon's `pendingStatus` in its {@linkcode Pokemon#turnData}.
|
||||
*
|
||||
* ⚠️ This method does **not** check for feasibility; that is the responsibility of the caller.
|
||||
*/
|
||||
doSetStatus(effect: StatusEffect.SLEEP, sleepTurnsRemaining?: number): void;
|
||||
/**
|
||||
* Set this Pokemon's {@linkcode status | non-volatile status condition} to the specified effect.
|
||||
* @param effect - The {@linkcode StatusEffect} to set
|
||||
* @param sleepTurnsRemaining - The number of turns to inflict sleep for; defaults to a random number between 2 and 4
|
||||
* and is unused for all non-sleep Statuses
|
||||
* @remarks
|
||||
* Clears this pokemon's `pendingStatus` in its {@linkcode Pokemon#turnData}.
|
||||
*
|
||||
* ⚠️ This method does **not** check for feasibility; that is the responsibility of the caller.
|
||||
*/
|
||||
doSetStatus(effect: StatusEffect, sleepTurnsRemaining?: number): void;
|
||||
/**
|
||||
* Set this Pokemon's {@linkcode status | non-volatile status condition} to the specified effect.
|
||||
* @param effect - The {@linkcode StatusEffect} to set
|
||||
* @param sleepTurnsRemaining - The number of turns to inflict sleep for; defaults to a random number between 2 and 4
|
||||
* and is unused for all non-sleep Statuses
|
||||
* @remarks
|
||||
* Clears this pokemon's `pendingStatus` in its {@linkcode Pokemon#turnData}.
|
||||
*
|
||||
* ⚠️ This method does **not** check for feasibility; that is the responsibility of the caller.
|
||||
* @todo Make this and all related fields private and change tests to use a field-based helper or similar
|
||||
*/
|
||||
doSetStatus(
|
||||
effect: StatusEffect,
|
||||
sleepTurnsRemaining = effect !== StatusEffect.SLEEP ? 0 : this.randBattleSeedIntRange(2, 4),
|
||||
): void {
|
||||
// Reset any pending status
|
||||
this.turnData.pendingStatus = StatusEffect.NONE;
|
||||
switch (effect) {
|
||||
case StatusEffect.POISON:
|
||||
case StatusEffect.TOXIC:
|
||||
this.setFrameRate(8);
|
||||
break;
|
||||
case StatusEffect.PARALYSIS:
|
||||
this.setFrameRate(5);
|
||||
break;
|
||||
case StatusEffect.SLEEP: {
|
||||
this.setFrameRate(3);
|
||||
|
||||
// If the user is semi-invulnerable when put asleep (such as due to Yawm),
|
||||
// remove their invulnerability and cancel the upcoming move from the queue
|
||||
const invulnTagTypes = [
|
||||
BattlerTagType.FLYING,
|
||||
BattlerTagType.UNDERGROUND,
|
||||
BattlerTagType.UNDERWATER,
|
||||
BattlerTagType.HIDDEN,
|
||||
];
|
||||
|
||||
if (this.findTag(t => invulnTagTypes.includes(t.tagType))) {
|
||||
this.findAndRemoveTags(t => invulnTagTypes.includes(t.tagType));
|
||||
this.getMoveQueue().shift();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case StatusEffect.FREEZE:
|
||||
this.setFrameRate(0);
|
||||
break;
|
||||
case StatusEffect.BURN:
|
||||
this.setFrameRate(14);
|
||||
break;
|
||||
case StatusEffect.FAINT:
|
||||
break;
|
||||
default:
|
||||
effect satisfies StatusEffect.NONE;
|
||||
break;
|
||||
}
|
||||
|
||||
this.status = new Status(effect, 0, sleepTurnsRemaining);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the status of a pokemon.
|
||||
* @param revive Whether revive should be cured; defaults to true.
|
||||
@ -5009,8 +5099,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
public clearStatus(confusion: boolean, reloadAssets: boolean) {
|
||||
const lastStatus = this.status?.effect;
|
||||
this.status = null;
|
||||
this.setFrameRate(10);
|
||||
if (lastStatus === StatusEffect.SLEEP) {
|
||||
this.setFrameRate(10);
|
||||
if (this.getTag(BattlerTagType.NIGHTMARE)) {
|
||||
this.lapseTag(BattlerTagType.NIGHTMARE);
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import { pokemonPrevolutions } from "#balance/pokemon-evolutions";
|
||||
import { signatureSpecies } from "#balance/signature-species";
|
||||
import { ArenaTrapTag } from "#data/arena-tag";
|
||||
import { EntryHazardTag } from "#data/arena-tag";
|
||||
import type { PokemonSpecies } from "#data/pokemon-species";
|
||||
import { ArenaTagSide } from "#enums/arena-tag-side";
|
||||
import { PartyMemberStrength } from "#enums/party-member-strength";
|
||||
@ -16,12 +16,9 @@ import type { PersistentModifier } from "#modifiers/modifier";
|
||||
import { getIsInitialized, initI18n } from "#plugins/i18n";
|
||||
import type { TrainerConfig } from "#trainers/trainer-config";
|
||||
import { trainerConfigs } from "#trainers/trainer-config";
|
||||
import {
|
||||
TrainerPartyCompoundTemplate,
|
||||
type TrainerPartyTemplate,
|
||||
trainerPartyTemplates,
|
||||
} from "#trainers/trainer-party-template";
|
||||
import { TrainerPartyCompoundTemplate, type TrainerPartyTemplate } from "#trainers/trainer-party-template";
|
||||
import { randSeedInt, randSeedItem, randSeedWeightedItem } from "#utils/common";
|
||||
import { getRandomLocaleEntry } from "#utils/i18n";
|
||||
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
||||
import { toCamelCase } from "#utils/strings";
|
||||
import i18next from "i18next";
|
||||
@ -35,6 +32,18 @@ export class Trainer extends Phaser.GameObjects.Container {
|
||||
public partnerNameKey: string | undefined;
|
||||
public originalIndexes: { [key: number]: number } = {};
|
||||
|
||||
/**
|
||||
* Create a new Trainer.
|
||||
* @param trainerType - The {@linkcode TrainerType} for this trainer, used to determine
|
||||
* name, sprite, party contents and other details.
|
||||
* @param variant - The {@linkcode TrainerVariant} for this trainer (if any are available)
|
||||
* @param partyTemplateIndex - If provided, will override the trainer's party template with the given
|
||||
* version.
|
||||
* @param nameKey - If provided, will override the name key of the trainer
|
||||
* @param partnerNameKey - If provided, will override the
|
||||
* @param trainerConfigOverride - If provided, will override the trainer config for the given trainer type
|
||||
* @todo Review how many of these parameters we actually need
|
||||
*/
|
||||
constructor(
|
||||
trainerType: TrainerType,
|
||||
variant: TrainerVariant,
|
||||
@ -44,13 +53,11 @@ export class Trainer extends Phaser.GameObjects.Container {
|
||||
trainerConfigOverride?: TrainerConfig,
|
||||
) {
|
||||
super(globalScene, -72, 80);
|
||||
this.config = trainerConfigs.hasOwnProperty(trainerType)
|
||||
? trainerConfigs[trainerType]
|
||||
: trainerConfigs[TrainerType.ACE_TRAINER];
|
||||
|
||||
if (trainerConfigOverride) {
|
||||
this.config = trainerConfigOverride;
|
||||
}
|
||||
this.config =
|
||||
trainerConfigOverride ??
|
||||
(trainerConfigs.hasOwnProperty(trainerType)
|
||||
? trainerConfigs[trainerType]
|
||||
: trainerConfigs[TrainerType.ACE_TRAINER]);
|
||||
|
||||
this.variant = variant;
|
||||
this.partyTemplateIndex = Math.min(
|
||||
@ -59,20 +66,21 @@ export class Trainer extends Phaser.GameObjects.Container {
|
||||
: randSeedWeightedItem(this.config.partyTemplates.map((_, i) => i)),
|
||||
this.config.partyTemplates.length - 1,
|
||||
);
|
||||
const classKey = `trainersCommon:${TrainerType[trainerType]}`;
|
||||
// TODO: Rework this and add actual error handling for missing names
|
||||
const classKey = `trainersCommon:${toCamelCase(TrainerType[trainerType])}`;
|
||||
if (i18next.exists(classKey, { returnObjects: true })) {
|
||||
if (nameKey) {
|
||||
this.nameKey = nameKey;
|
||||
this.name = i18next.t(nameKey);
|
||||
} else {
|
||||
const genderKey = i18next.exists(`${classKey}.MALE`)
|
||||
const genderKey = i18next.exists(`${classKey}.male`)
|
||||
? variant === TrainerVariant.FEMALE
|
||||
? ".FEMALE"
|
||||
: ".MALE"
|
||||
? ".female"
|
||||
: ".male"
|
||||
: "";
|
||||
const trainerKey = randSeedItem(Object.keys(i18next.t(`${classKey}${genderKey}`, { returnObjects: true })));
|
||||
this.nameKey = `${classKey}${genderKey}.${trainerKey}`;
|
||||
[this.nameKey, this.name] = getRandomLocaleEntry(`${classKey}${genderKey}`);
|
||||
}
|
||||
this.name = i18next.t(this.nameKey);
|
||||
|
||||
if (variant === TrainerVariant.DOUBLE) {
|
||||
if (this.config.doubleOnly) {
|
||||
if (partnerNameKey) {
|
||||
@ -82,16 +90,8 @@ export class Trainer extends Phaser.GameObjects.Container {
|
||||
[this.name, this.partnerName] = this.name.split(" & ");
|
||||
}
|
||||
} else {
|
||||
const partnerGenderKey = i18next.exists(`${classKey}.FEMALE`) ? ".FEMALE" : "";
|
||||
const partnerTrainerKey = randSeedItem(
|
||||
Object.keys(
|
||||
i18next.t(`${classKey}${partnerGenderKey}`, {
|
||||
returnObjects: true,
|
||||
}),
|
||||
),
|
||||
);
|
||||
this.partnerNameKey = `${classKey}${partnerGenderKey}.${partnerTrainerKey}`;
|
||||
this.partnerName = i18next.t(this.partnerNameKey);
|
||||
const partnerGenderKey = i18next.exists(`${classKey}.female`) ? ".female" : "";
|
||||
[this.partnerNameKey, this.partnerName] = getRandomLocaleEntry(`${classKey}${partnerGenderKey}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -109,10 +109,6 @@ export class Trainer extends Phaser.GameObjects.Container {
|
||||
break;
|
||||
}
|
||||
|
||||
console.log(
|
||||
Object.keys(trainerPartyTemplates)[Object.values(trainerPartyTemplates).indexOf(this.getPartyTemplate())],
|
||||
);
|
||||
|
||||
const getSprite = (hasShadow?: boolean, forceFemale?: boolean) => {
|
||||
const ret = globalScene.addFieldSprite(
|
||||
0,
|
||||
@ -157,9 +153,9 @@ export class Trainer extends Phaser.GameObjects.Container {
|
||||
|
||||
/**
|
||||
* Returns the name of the trainer based on the provided trainer slot and the option to include a title.
|
||||
* @param {TrainerSlot} trainerSlot - The slot to determine which name to use. Defaults to TrainerSlot.NONE.
|
||||
* @param {boolean} includeTitle - Whether to include the title in the returned name. Defaults to false.
|
||||
* @returns {string} - The formatted name of the trainer.
|
||||
* @param trainerSlot - The slot to determine which name to use; default `TrainerSlot.NONE`
|
||||
* @param includeTitle - Whether to include the title in the returned name; default `false`
|
||||
* @returns - The formatted name of the trainer
|
||||
*/
|
||||
getName(trainerSlot: TrainerSlot = TrainerSlot.NONE, includeTitle = false): string {
|
||||
// Get the base title based on the trainer slot and variant.
|
||||
@ -584,8 +580,8 @@ export class Trainer extends Phaser.GameObjects.Container {
|
||||
score /= playerField.length;
|
||||
if (forSwitch && !p.isOnField()) {
|
||||
globalScene.arena
|
||||
.findTagsOnSide(t => t instanceof ArenaTrapTag, ArenaTagSide.ENEMY)
|
||||
.map(t => (score *= (t as ArenaTrapTag).getMatchupScoreMultiplier(p)));
|
||||
.findTagsOnSide(t => t instanceof EntryHazardTag, ArenaTagSide.ENEMY)
|
||||
.map(t => (score *= (t as EntryHazardTag).getMatchupScoreMultiplier(p)));
|
||||
}
|
||||
}
|
||||
|
||||
|