mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-06-20 16:42:45 +02:00
Merge branch 'beta' into turn-start-phase
This commit is contained in:
commit
4ab2a33578
@ -4,7 +4,7 @@ module.exports = {
|
||||
{
|
||||
name: "only-type-imports",
|
||||
severity: "error",
|
||||
comment: "Files in enums and @types may only use type imports.",
|
||||
comment: "Files in 'enums/' and '@types/' must only use type imports.",
|
||||
from: {
|
||||
path: ["(^|/)src/@types", "(^|/)src/enums"],
|
||||
},
|
||||
@ -14,7 +14,7 @@ module.exports = {
|
||||
},
|
||||
{
|
||||
name: "no-circular-at-runtime",
|
||||
severity: "warn",
|
||||
severity: "error",
|
||||
comment:
|
||||
"This dependency is part of a circular relationship. You might want to revise " +
|
||||
"your solution (i.e. use dependency inversion, make sure the modules have a single responsibility) ",
|
||||
@ -34,7 +34,7 @@ module.exports = {
|
||||
"add an exception for it in your dependency-cruiser configuration. By default " +
|
||||
"this rule does not scrutinize dot-files (e.g. .eslintrc.js), TypeScript declaration " +
|
||||
"files (.d.ts), tsconfig.json and some of the babel and webpack configs.",
|
||||
severity: "warn",
|
||||
severity: "error",
|
||||
from: {
|
||||
orphan: true,
|
||||
pathNot: [
|
||||
@ -42,8 +42,7 @@ module.exports = {
|
||||
"[.]d[.]ts$", // TypeScript declaration files
|
||||
"(^|/)tsconfig[.]json$", // TypeScript config
|
||||
"(^|/)(?:babel|webpack)[.]config[.](?:js|cjs|mjs|ts|cts|mts|json)$", // other configs
|
||||
// anything in src/@types
|
||||
"(^|/)src/@types/",
|
||||
"(^|/)test/.+[.]setup[.]ts", // Vitest setup files
|
||||
],
|
||||
},
|
||||
to: {},
|
||||
@ -53,7 +52,7 @@ module.exports = {
|
||||
comment:
|
||||
"A module depends on a node core module that has been deprecated. Find an alternative - these are " +
|
||||
"bound to exist - node doesn't deprecate lightly.",
|
||||
severity: "warn",
|
||||
severity: "error",
|
||||
from: {},
|
||||
to: {
|
||||
dependencyTypes: ["core"],
|
||||
@ -86,7 +85,7 @@ module.exports = {
|
||||
comment:
|
||||
"This module uses a (version of an) npm module that has been deprecated. Either upgrade to a later " +
|
||||
"version of that module, or find an alternative. Deprecated modules are a security risk.",
|
||||
severity: "warn",
|
||||
severity: "error",
|
||||
from: {},
|
||||
to: {
|
||||
dependencyTypes: ["deprecated"],
|
||||
@ -122,7 +121,7 @@ module.exports = {
|
||||
"Likely this module depends on an external ('npm') package that occurs more than once " +
|
||||
"in your package.json i.e. bot as a devDependencies and in dependencies. This will cause " +
|
||||
"maintenance problems later on.",
|
||||
severity: "warn",
|
||||
severity: "error",
|
||||
from: {},
|
||||
to: {
|
||||
moreThanOneDependencyType: true,
|
||||
@ -133,7 +132,7 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
|
||||
/* rules you might want to tweak for your specific situation: */
|
||||
// rules you might want to tweak for your specific situation:
|
||||
|
||||
{
|
||||
name: "not-to-spec",
|
||||
@ -188,7 +187,7 @@ module.exports = {
|
||||
"in your package.json. This makes sense if your package is e.g. a plugin, but in " +
|
||||
"other cases - maybe not so much. If the use of a peer dependency is intentional " +
|
||||
"add an exception to your dependency-cruiser configuration.",
|
||||
severity: "warn",
|
||||
severity: "error",
|
||||
from: {},
|
||||
to: {
|
||||
dependencyTypes: ["npm-peer"],
|
||||
@ -196,6 +195,7 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
options: {
|
||||
exclude: ["src/plugins/vite/*", "src/vite.env.d.ts"],
|
||||
/* Which modules not to follow further when encountered */
|
||||
doNotFollow: {
|
||||
/* path: an array of regular expressions in strings to match against */
|
||||
@ -218,7 +218,7 @@ module.exports = {
|
||||
module systems it knows of. It's the default because it's the safe option
|
||||
It might come at a performance penalty, though.
|
||||
moduleSystems: ['amd', 'cjs', 'es6', 'tsd']
|
||||
|
||||
|
||||
As in practice only commonjs ('cjs') and ecmascript modules ('es6')
|
||||
are widely used, you can limit the moduleSystems to those.
|
||||
*/
|
||||
@ -226,7 +226,7 @@ module.exports = {
|
||||
// moduleSystems: ['cjs', 'es6'],
|
||||
|
||||
/* prefix for links in html and svg output (e.g. 'https://github.com/you/yourrepo/blob/main/'
|
||||
to open it on your online repo or `vscode://file/${process.cwd()}/` to
|
||||
to open it on your online repo or `vscode://file/${process.cwd()}/` to
|
||||
open it in visual studio code),
|
||||
*/
|
||||
// prefix: `vscode://file/${process.cwd()}/`,
|
||||
@ -235,7 +235,7 @@ module.exports = {
|
||||
true: also detect dependencies that only exist before typescript-to-javascript compilation
|
||||
"specify": for each dependency identify whether it only exists before compilation or also after
|
||||
*/
|
||||
// tsPreCompilationDeps: false,
|
||||
tsPreCompilationDeps: true,
|
||||
|
||||
/* list of extensions to scan that aren't javascript or compile-to-javascript.
|
||||
Empty by default. Only put extensions in here that you want to take into
|
||||
@ -271,7 +271,7 @@ module.exports = {
|
||||
to './webpack.conf.js'.
|
||||
|
||||
The (optional) `env` and `arguments` attributes contain the parameters
|
||||
to be passed if your webpack config is a function and takes them (see
|
||||
to be passed if your webpack config is a function and takes them (see
|
||||
webpack documentation for details)
|
||||
*/
|
||||
// webpackConfig: {
|
||||
@ -322,8 +322,8 @@ module.exports = {
|
||||
A list of alias fields in package.jsons
|
||||
See [this specification](https://github.com/defunctzombie/package-browser-field-spec) and
|
||||
the webpack [resolve.alias](https://webpack.js.org/configuration/resolve/#resolvealiasfields)
|
||||
documentation
|
||||
|
||||
documentation
|
||||
|
||||
Defaults to an empty array (= don't use alias fields).
|
||||
*/
|
||||
// aliasFields: ["browser"],
|
||||
|
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@ -8,7 +8,7 @@
|
||||
|
||||
# Art Team
|
||||
/public/**/*.png @pagefaultgames/art-team
|
||||
/public/**/*.json @pagefaultgames/art-team
|
||||
/public/**/*.json @pagefaultgames/art-team
|
||||
/public/images @pagefaultgames/art-team
|
||||
/public/battle-anims @pagefaultgames/art-team
|
||||
|
||||
|
37
.github/pull_request_template.md
vendored
37
.github/pull_request_template.md
vendored
@ -2,25 +2,28 @@
|
||||
<!-- Feel free to look at other PRs for examples -->
|
||||
<!--
|
||||
Make sure the title includes categorization (choose the one that best fits):
|
||||
- [Bug]: If the PR is primarily a bug fix
|
||||
- [Move]: If a move has new or changed functionality
|
||||
- [Ability]: If an ability has new or changed functionality
|
||||
- [Item]: For new or modified items
|
||||
- [Mystery]: For new or modified Mystery Encounters
|
||||
- [Test]: If the PR is primarily adding or modifying tests
|
||||
- [UI/UX]: If the PR is changing UI/UX elements
|
||||
- [Audio]: If the PR is adding or changing music/sfx
|
||||
- [Sprite]: If the PR is adding or changing sprites
|
||||
- [Balance]: If the PR is related to game balance
|
||||
- [Challenge]: If the PR is adding or modifying challenges
|
||||
- [Bug]: If the PR is primarily a bug fix
|
||||
- [Move]: If a move has new or changed functionality
|
||||
- [Ability]: If an ability has new or changed functionality
|
||||
- [Item]: For new or modified items
|
||||
- [Mystery]: For new or modified Mystery Encounters
|
||||
- [Test]: If the PR is primarily adding or modifying tests
|
||||
- [UI/UX]: If the PR is changing UI/UX elements
|
||||
- [Audio]: If the PR is adding or changing music/sfx
|
||||
- [Sprite]: If the PR is adding or changing sprites
|
||||
- [Balance]: If the PR is related to game balance
|
||||
- [Challenge]: If the PR is adding or modifying challenges
|
||||
- [Refactor]: If the PR is primarily rewriting existing code
|
||||
- [Docs]: If the PR is just adding or modifying documentation (such as tsdocs/code comments)
|
||||
- [GitHub]: For changes to GitHub workflows/templates/etc
|
||||
- [Misc]: If no other category fits the PR
|
||||
- [Dev]: If the PR is primarily changing something pertaining to development (lefthook hooks, linter rules, etc.)
|
||||
- [i18n]: If the PR is primarily adding/changing locale keys or key usage (may come with an associated locales PR)
|
||||
- [Docs]: If the PR is adding or modifying documentation (such as tsdocs/code comments)
|
||||
- [GitHub]: For changes to GitHub workflows/templates/etc
|
||||
- [Misc]: If no other category fits the PR
|
||||
-->
|
||||
|
||||
<!--
|
||||
Make sure that this PR is not overlapping with someone else's work
|
||||
Please try to keep the PR self-contained (and small)
|
||||
Please try to keep the PR self-contained (and small!)
|
||||
-->
|
||||
|
||||
## What are the changes the user will see?
|
||||
@ -66,11 +69,11 @@ Do the reviewers need to do something special in order to test your changes?
|
||||
- [ ] Have I provided a clear explanation of the changes?
|
||||
- [ ] Have I tested the changes manually?
|
||||
- [ ] Are all unit tests still passing? (`npm run test:silent`)
|
||||
- [ ] Have I created new automated tests (`npm run create-test`) or updated existing tests related to the PR's changes?
|
||||
- [ ] Have I created new automated tests (`npm run test:create`) or updated existing tests related to the PR's changes?
|
||||
- [ ] Have I provided screenshots/videos of the changes (if applicable)?
|
||||
- [ ] Have I made sure that any UI change works for both UI themes (default and legacy)?
|
||||
|
||||
Are there any localization additions or changes? If so:
|
||||
- [ ] Has a locales PR been created on the [locales](https://github.com/pagefaultgames/pokerogue-locales) repo?
|
||||
- [ ] If so, please leave a link to it here:
|
||||
- [ ] Has the translation team been contacted for proofreading/translation?
|
||||
- [ ] Has the translation team been contacted for proofreading/translation?
|
||||
|
2
.github/workflows/deploy.yml
vendored
2
.github/workflows/deploy.yml
vendored
@ -35,7 +35,7 @@ jobs:
|
||||
ssh-keyscan -H ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts
|
||||
- name: Deploy build on server
|
||||
if: github.event_name == 'push' && github.ref_name == 'main'
|
||||
run: |
|
||||
run: |
|
||||
rsync --del --no-times --checksum -vrm dist/* ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:${{ secrets.DESTINATION_DIR }}
|
||||
ssh -t ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} "~/prmanifest --inpath ${{ secrets.DESTINATION_DIR }} --outpath ${{ secrets.DESTINATION_DIR }}/manifest.json"
|
||||
- name: Purge Cloudflare Cache
|
||||
|
42
.github/workflows/linting.yml
vendored
Normal file
42
.github/workflows/linting.yml
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
name: Linting
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
|
||||
jobs:
|
||||
run-linters:
|
||||
name: Run linters
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install Node.js dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run ESLint
|
||||
run: npm run eslint-ci
|
||||
|
||||
- name: Lint with Biome
|
||||
run: npm run biome-ci
|
||||
|
||||
- name: Check dependencies with depcruise
|
||||
run: npm run depcruise
|
41
.github/workflows/quality.yml
vendored
41
.github/workflows/quality.yml
vendored
@ -1,41 +0,0 @@
|
||||
name: Biome Code Quality
|
||||
|
||||
on:
|
||||
# Trigger the workflow on push or pull request,
|
||||
# but only for the main branch
|
||||
push:
|
||||
branches:
|
||||
- main # Trigger on push events to the main branch
|
||||
- beta # Trigger on push events to the beta branch
|
||||
pull_request:
|
||||
branches:
|
||||
- main # Trigger on pull request events targeting the main branch
|
||||
- beta # Trigger on pull request events targeting the beta branch
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
|
||||
jobs:
|
||||
run-linters: # Define a job named "run-linters"
|
||||
name: Run linters # Human-readable name for the job
|
||||
runs-on: ubuntu-latest # Specify the latest Ubuntu runner for the job
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository # Step to check out the repository
|
||||
uses: actions/checkout@v4 # Use the checkout action version 4
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Set up Node.js # Step to set up Node.js environment
|
||||
uses: actions/setup-node@v4 # Use the setup-node action version 4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install Node.js dependencies # Step to install Node.js dependencies
|
||||
run: npm ci # Use 'npm ci' to install dependencies
|
||||
|
||||
- name: eslint # Step to run linters
|
||||
run: npm run eslint-ci
|
||||
|
||||
- name: Lint with Biome # Step to run linters
|
||||
run: npm run biome-ci
|
29
biome.jsonc
29
biome.jsonc
@ -28,7 +28,6 @@
|
||||
".vscode/*",
|
||||
"*.css", // TODO?
|
||||
"*.html", // TODO?
|
||||
"src/overrides.ts",
|
||||
// TODO: these files are too big and complex, ignore them until their respective refactors
|
||||
"src/data/moves/move.ts",
|
||||
|
||||
@ -47,8 +46,8 @@
|
||||
"correctness": {
|
||||
"noUndeclaredVariables": "off",
|
||||
"noUnusedVariables": "error",
|
||||
"noSwitchDeclarations": "warn", // TODO: refactor and make this an error
|
||||
"noVoidTypeReturn": "warn", // TODO: Refactor and make this an error
|
||||
"noSwitchDeclarations": "error",
|
||||
"noVoidTypeReturn": "error",
|
||||
"noUnusedImports": "error"
|
||||
},
|
||||
"style": {
|
||||
@ -85,7 +84,7 @@
|
||||
"useLiteralKeys": "off",
|
||||
"noForEach": "off", // Foreach vs for of is not that simple.
|
||||
"noUselessSwitchCase": "off", // Explicit > Implicit
|
||||
"noUselessConstructor": "warn", // TODO: Refactor and make this an error
|
||||
"noUselessConstructor": "error",
|
||||
"noBannedTypes": "warn" // TODO: Refactor and make this an error
|
||||
},
|
||||
"nursery": {
|
||||
@ -120,6 +119,28 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Overrides to prevent unused import removal inside `overrides.ts` and enums files (for TSDoc linkcodes)
|
||||
{
|
||||
"include": ["src/overrides.ts", "src/enums/*"],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"correctness": {
|
||||
"noUnusedImports": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"include": ["src/overrides.ts"],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"style": {
|
||||
"useImportType": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
2
global.d.ts
vendored
2
global.d.ts
vendored
@ -7,7 +7,7 @@ declare global {
|
||||
* Only used in testing.
|
||||
* Can technically be undefined/null but for ease of use we are going to assume it is always defined.
|
||||
* Used to load i18n files exclusively.
|
||||
*
|
||||
*
|
||||
* To set up your own server in a test see `game_data.test.ts`
|
||||
*/
|
||||
var server: SetupServerApi;
|
||||
|
@ -18,9 +18,9 @@
|
||||
"eslint": "eslint --fix .",
|
||||
"eslint-ci": "eslint .",
|
||||
"biome": "biome check --write --changed --no-errors-on-unmatched",
|
||||
"biome-ci": "biome ci --diagnostic-level=error --reporter=github --changed --no-errors-on-unmatched",
|
||||
"biome-ci": "biome ci --diagnostic-level=error --reporter=github --no-errors-on-unmatched",
|
||||
"docs": "typedoc",
|
||||
"depcruise": "depcruise src",
|
||||
"depcruise": "depcruise src test",
|
||||
"depcruise:graph": "depcruise src --output-type dot | node dependency-graph.js > dependency-graph.svg",
|
||||
"postinstall": "npx lefthook install && npx lefthook run post-merge",
|
||||
"update-version:patch": "npm version patch --force --no-git-tag-version",
|
||||
|
@ -29,4 +29,4 @@ export type ModifierString = keyof ModifierConstructorMap;
|
||||
|
||||
export type ModifierPool = {
|
||||
[tier: string]: WeightedModifierType[];
|
||||
}
|
||||
};
|
||||
|
@ -7,7 +7,6 @@ import {
|
||||
isNullOrUndefined,
|
||||
randSeedItem,
|
||||
randSeedInt,
|
||||
type Constructor,
|
||||
randSeedFloat,
|
||||
coerceArray,
|
||||
} from "#app/utils/common";
|
||||
@ -35,12 +34,11 @@ import { Command } from "#enums/command";
|
||||
import { BerryModifierType } from "#app/modifier/modifier-type";
|
||||
import { getPokeballName } from "#app/data/pokeball";
|
||||
import { BattleType } from "#enums/battle-type";
|
||||
import type { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import { allAbilities } from "#app/data/data-lists";
|
||||
|
||||
// Enum imports
|
||||
import { Stat, type BattleStat, BATTLE_STATS, EFFECTIVE_STATS, getStatKey, type EffectiveStat } from "#enums/stat";
|
||||
import { Stat, BATTLE_STATS, EFFECTIVE_STATS, getStatKey } from "#enums/stat";
|
||||
import { PokemonType } from "#enums/pokemon-type";
|
||||
import { PokemonAnimType } from "#enums/pokemon-anim-type";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
@ -54,12 +52,16 @@ import { SwitchType } from "#enums/switch-type";
|
||||
import { MoveFlags } from "#enums/MoveFlags";
|
||||
import { MoveTarget } from "#enums/MoveTarget";
|
||||
import { MoveCategory } from "#enums/MoveCategory";
|
||||
import type { BerryType } from "#enums/berry-type";
|
||||
import { CommonAnim } from "#enums/move-anims-common";
|
||||
import { getBerryEffectFunc } from "../berry";
|
||||
import { getBerryEffectFunc } from "#app/data/berry";
|
||||
import { BerryUsedEvent } from "#app/events/battle-scene";
|
||||
import { noAbilityTypeOverrideMoves } from "#app/data/moves/invalid-moves";
|
||||
import { MoveUseMode } from "#enums/move-use-mode";
|
||||
|
||||
// Type imports
|
||||
import type { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
|
||||
import type { BattleStat, EffectiveStat } from "#enums/stat";
|
||||
import type { BerryType } from "#enums/berry-type";
|
||||
import type { EnemyPokemon } from "#app/field/pokemon";
|
||||
import type { PokemonMove } from "../moves/pokemon-move";
|
||||
import type Pokemon from "#app/field/pokemon";
|
||||
@ -76,7 +78,7 @@ import type {
|
||||
import type { BattlerIndex } from "#enums/battler-index";
|
||||
import type Move from "#app/data/moves/move";
|
||||
import type { ArenaTrapTag, SuppressAbilitiesTag } from "#app/data/arena-tag";
|
||||
import { noAbilityTypeOverrideMoves } from "../moves/invalid-moves";
|
||||
import type { Constructor } from "#app/utils/common";
|
||||
import type { Localizable } from "#app/@types/locales";
|
||||
import { applyAbAttrs } from "./apply-ab-attrs";
|
||||
|
||||
@ -1915,7 +1917,7 @@ export class PostDefendMoveDisableAbAttr extends PostDefendAbAttr {
|
||||
_args: any[],
|
||||
): boolean {
|
||||
return (
|
||||
attacker.getTag(BattlerTagType.DISABLED) === null &&
|
||||
isNullOrUndefined(attacker.getTag(BattlerTagType.DISABLED)) &&
|
||||
move.doesFlagEffectApply({ flag: MoveFlags.MAKES_CONTACT, user: attacker, target: pokemon }) &&
|
||||
(this.chance === -1 || pokemon.randBattleSeedInt(100) < this.chance)
|
||||
);
|
||||
@ -2733,7 +2735,6 @@ export class AllyStatMultiplierAbAttr extends AbAttr {
|
||||
/**
|
||||
* Takes effect whenever a move succesfully executes, such as gorilla tactics' move-locking.
|
||||
* (More specifically, whenever a move is pushed to the move history)
|
||||
* @extends AbAttr
|
||||
*/
|
||||
export class ExecutedMoveAbAttr extends AbAttr {
|
||||
canApplyExecutedMove(_pokemon: Pokemon, _simulated: boolean): boolean {
|
||||
@ -2744,16 +2745,16 @@ export class ExecutedMoveAbAttr extends AbAttr {
|
||||
}
|
||||
|
||||
/**
|
||||
* Ability attribute for Gorilla Tactics
|
||||
* @extends ExecutedMoveAbAttr
|
||||
* Ability attribute for {@linkcode AbilityId.GORILLA_TACTICS | Gorilla Tactics}
|
||||
* to lock the user into its first selected move.
|
||||
*/
|
||||
export class GorillaTacticsAbAttr extends ExecutedMoveAbAttr {
|
||||
constructor(showAbility = false) {
|
||||
super(showAbility);
|
||||
}
|
||||
|
||||
override canApplyExecutedMove(pokemon: Pokemon, simulated: boolean): boolean {
|
||||
return simulated || !pokemon.getTag(BattlerTagType.GORILLA_TACTICS);
|
||||
override canApplyExecutedMove(pokemon: Pokemon, _simulated: boolean): boolean {
|
||||
return !pokemon.getTag(BattlerTagType.GORILLA_TACTICS);
|
||||
}
|
||||
|
||||
override applyExecutedMove(pokemon: Pokemon, simulated: boolean): void {
|
||||
@ -6063,14 +6064,19 @@ export class PostDancingMoveAbAttr extends PostMoveUsedAbAttr {
|
||||
): void {
|
||||
if (!simulated) {
|
||||
dancer.turnData.extraTurns++;
|
||||
const phaseManager = globalScene.phaseManager;
|
||||
// If the move is an AttackMove or a StatusMove the Dancer must replicate the move on the source of the Dance
|
||||
if (move.getMove().is("AttackMove") || move.getMove().is("StatusMove")) {
|
||||
const target = this.getTarget(dancer, source, targets);
|
||||
phaseManager.unshiftNew("MovePhase", dancer, target, move, true, true);
|
||||
globalScene.phaseManager.unshiftNew("MovePhase", dancer, target, move, MoveUseMode.INDIRECT);
|
||||
} else if (move.getMove().is("SelfStatusMove")) {
|
||||
// If the move is a SelfStatusMove (ie. Swords Dance) the Dancer should replicate it on itself
|
||||
phaseManager.unshiftNew("MovePhase", dancer, [dancer.getBattlerIndex()], move, true, true);
|
||||
globalScene.phaseManager.unshiftNew(
|
||||
"MovePhase",
|
||||
dancer,
|
||||
[dancer.getBattlerIndex()],
|
||||
move,
|
||||
MoveUseMode.INDIRECT,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -7378,6 +7384,7 @@ class ForceSwitchOutHelper {
|
||||
* @param pokemon The {@linkcode Pokemon} attempting to switch out.
|
||||
* @returns `true` if the switch is successful
|
||||
*/
|
||||
// TODO: Make this cancel pending move phases on the switched out target
|
||||
public switchOutLogic(pokemon: Pokemon): boolean {
|
||||
const switchOutTarget = pokemon;
|
||||
/**
|
||||
@ -8378,10 +8385,10 @@ export function initAbilities() {
|
||||
.attr(WonderSkinAbAttr)
|
||||
.ignorable(),
|
||||
new Ability(AbilityId.ANALYTIC, 5)
|
||||
.attr(MovePowerBoostAbAttr, (user, _target, _move) => {
|
||||
const movePhase = globalScene.phaseManager.findPhase((phase) => phase.is("MovePhase") && phase.pokemon.id !== user?.id);
|
||||
return isNullOrUndefined(movePhase);
|
||||
}, 1.3),
|
||||
.attr(MovePowerBoostAbAttr, (user) =>
|
||||
// Boost power if all other Pokemon have already moved (no other moves are slated to execute)
|
||||
!globalScene.phaseManager.findPhase((phase) => phase.is("MovePhase") && phase.pokemon.id !== user?.id),
|
||||
1.3),
|
||||
new Ability(AbilityId.ILLUSION, 5)
|
||||
// The Pokemon generate an illusion if it's available
|
||||
.attr(IllusionPreSummonAbAttr, false)
|
||||
@ -8655,7 +8662,13 @@ export function initAbilities() {
|
||||
.attr(PostFaintHPDamageAbAttr)
|
||||
.bypassFaint(),
|
||||
new Ability(AbilityId.DANCER, 7)
|
||||
.attr(PostDancingMoveAbAttr),
|
||||
.attr(PostDancingMoveAbAttr)
|
||||
/* Incorrect interations with:
|
||||
* Petal Dance (should not lock in or count down timer; currently does both)
|
||||
* Flinches (due to tag being removed earlier)
|
||||
* Failed/protected moves (should not trigger if original move is protected against)
|
||||
*/
|
||||
.edgeCase(),
|
||||
new Ability(AbilityId.BATTERY, 7)
|
||||
.attr(AllyMoveCategoryPowerBoostAbAttr, [ MoveCategory.SPECIAL ], 1.3),
|
||||
new Ability(AbilityId.FLUFFY, 7)
|
||||
@ -8796,9 +8809,11 @@ export function initAbilities() {
|
||||
new Ability(AbilityId.WANDERING_SPIRIT, 8)
|
||||
.attr(PostDefendAbilitySwapAbAttr)
|
||||
.bypassFaint()
|
||||
.edgeCase(), // interacts incorrectly with rock head. It's meant to switch abilities before recoil would apply so that a pokemon with rock head would lose rock head first and still take the recoil
|
||||
.edgeCase(), // interacts incorrectly with rock head. It's meant to switch abilities before recoil would apply so that a pokemon with rock head would lose rock head first and still take the recoil
|
||||
new Ability(AbilityId.GORILLA_TACTICS, 8)
|
||||
.attr(GorillaTacticsAbAttr),
|
||||
.attr(GorillaTacticsAbAttr)
|
||||
// TODO: Verify whether Gorilla Tactics increases struggle's power or not
|
||||
.edgeCase(),
|
||||
new Ability(AbilityId.NEUTRALIZING_GAS, 8, 2)
|
||||
.attr(PostSummonAddArenaTagAbAttr, true, ArenaTagType.NEUTRALIZING_GAS, 0)
|
||||
.attr(PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr)
|
||||
|
@ -20,6 +20,7 @@ import { ArenaTagType } from "#enums/arena-tag-type";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { ArenaTagSide } from "#enums/arena-tag-side";
|
||||
import { MoveUseMode } from "#enums/move-use-mode";
|
||||
|
||||
export abstract class ArenaTag {
|
||||
constructor(
|
||||
@ -875,13 +876,13 @@ export class DelayedAttackTag extends ArenaTag {
|
||||
const ret = super.lapse(arena);
|
||||
|
||||
if (!ret) {
|
||||
// TODO: This should not add to move history (for Spite)
|
||||
globalScene.phaseManager.unshiftNew(
|
||||
"MoveEffectPhase",
|
||||
this.sourceId!,
|
||||
[this.targetIndex],
|
||||
allMoves[this.sourceMove!],
|
||||
false,
|
||||
true,
|
||||
MoveUseMode.FOLLOW_UP,
|
||||
); // TODO: are those bangs correct?
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,18 @@
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import { allMoves } from "./data-lists";
|
||||
import { allMoves } from "#app/data/data-lists";
|
||||
import { MoveFlags } from "#enums/MoveFlags";
|
||||
import type Pokemon from "../field/pokemon";
|
||||
import { type nil, getFrameMs, getEnumKeys, getEnumValues, animationFileName, coerceArray } from "../utils/common";
|
||||
import type Pokemon from "#app/field/pokemon";
|
||||
import {
|
||||
type nil,
|
||||
getFrameMs,
|
||||
getEnumKeys,
|
||||
getEnumValues,
|
||||
animationFileName,
|
||||
coerceArray,
|
||||
isNullOrUndefined,
|
||||
} from "#app/utils/common";
|
||||
import type { BattlerIndex } from "#enums/battler-index";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SubstituteTag } from "./battler-tags";
|
||||
import { isNullOrUndefined } from "../utils/common";
|
||||
import Phaser from "phaser";
|
||||
import { EncounterAnim } from "#enums/encounter-anims";
|
||||
import { AnimBlendType, AnimFrameTarget, AnimFocus, ChargeAnim, CommonAnim } from "#enums/move-anims-common";
|
||||
@ -845,7 +851,7 @@ export abstract class BattleAnim {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetSubstitute = !!onSubstitute && user !== target ? target.getTag(SubstituteTag) : null;
|
||||
const targetSubstitute = !!onSubstitute && user !== target ? target.getTag(BattlerTagType.SUBSTITUTE) : null;
|
||||
|
||||
const userSprite = user.getSprite();
|
||||
const targetSprite = targetSubstitute?.sprite ?? target.getSprite();
|
||||
|
@ -31,8 +31,15 @@ import { EFFECTIVE_STATS, getStatKey, Stat, type BattleStat, type EffectiveStat
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import { WeatherType } from "#enums/weather-type";
|
||||
import { isNullOrUndefined } from "#app/utils/common";
|
||||
import { MoveUseMode } from "#enums/move-use-mode";
|
||||
import { invalidEncoreMoves } from "./moves/invalid-moves";
|
||||
import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type";
|
||||
|
||||
/**
|
||||
* A {@linkcode BattlerTag} represents a semi-persistent effect that can be attached to a {@linkcode Pokemon}.
|
||||
* Tags can trigger various effects throughout a turn, and are cleared on switching out
|
||||
* or through their respective {@linkcode BattlerTag.lapse | lapse} methods.
|
||||
*/
|
||||
export class BattlerTag {
|
||||
public tagType: BattlerTagType;
|
||||
public lapseTypes: BattlerTagLapseType[];
|
||||
@ -69,7 +76,7 @@ export class BattlerTag {
|
||||
|
||||
/**
|
||||
* Tick down this {@linkcode BattlerTag}'s duration.
|
||||
* @returns `true` if the tag should be kept (`turnCount` > 0`)
|
||||
* @returns `true` if the tag should be kept (`turnCount > 0`)
|
||||
*/
|
||||
lapse(_pokemon: Pokemon, _lapseType: BattlerTagLapseType): boolean {
|
||||
// TODO: Maybe flip this (return `true` if tag needs removal)
|
||||
@ -125,16 +132,6 @@ export interface TerrainBattlerTag {
|
||||
* Players and enemies should not be allowed to select restricted moves.
|
||||
*/
|
||||
export abstract class MoveRestrictionBattlerTag extends BattlerTag {
|
||||
constructor(
|
||||
tagType: BattlerTagType,
|
||||
lapseType: BattlerTagLapseType | BattlerTagLapseType[],
|
||||
turnCount: number,
|
||||
sourceMove?: MoveId,
|
||||
sourceId?: number,
|
||||
) {
|
||||
super(tagType, lapseType, turnCount, sourceMove, sourceId);
|
||||
}
|
||||
|
||||
/** @override */
|
||||
override lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
||||
if (lapseType === BattlerTagLapseType.PRE_MOVE) {
|
||||
@ -277,17 +274,18 @@ export class DisabledTag extends MoveRestrictionBattlerTag {
|
||||
/**
|
||||
* @override
|
||||
*
|
||||
* Ensures that move history exists on `pokemon` and has a valid move. If so, sets the {@linkcode moveId} and shows a message.
|
||||
* Otherwise the move ID will not get assigned and this tag will get removed next turn.
|
||||
* Attempt to disable the target's last move by setting this tag's {@linkcode moveId}
|
||||
* and showing a message.
|
||||
*/
|
||||
override onAdd(pokemon: Pokemon): void {
|
||||
super.onAdd(pokemon);
|
||||
|
||||
const move = pokemon.getLastXMoves(-1).find(m => !m.virtual);
|
||||
if (isNullOrUndefined(move) || move.move === MoveId.STRUGGLE || move.move === MoveId.NONE) {
|
||||
// Disable fails against struggle or an empty move history
|
||||
// TODO: Confirm if this is redundant given Disable/Cursed Body's disable conditions
|
||||
const move = pokemon.getLastNonVirtualMove();
|
||||
if (isNullOrUndefined(move) || move.move === MoveId.STRUGGLE) {
|
||||
return;
|
||||
}
|
||||
|
||||
super.onAdd(pokemon);
|
||||
this.moveId = move.move;
|
||||
|
||||
globalScene.phaseManager.queueMessage(
|
||||
@ -337,7 +335,6 @@ export class DisabledTag extends MoveRestrictionBattlerTag {
|
||||
|
||||
/**
|
||||
* Tag used by Gorilla Tactics to restrict the user to using only one move.
|
||||
* @extends MoveRestrictionBattlerTag
|
||||
*/
|
||||
export class GorillaTacticsTag extends MoveRestrictionBattlerTag {
|
||||
private moveId = MoveId.NONE;
|
||||
@ -346,34 +343,30 @@ export class GorillaTacticsTag extends MoveRestrictionBattlerTag {
|
||||
super(BattlerTagType.GORILLA_TACTICS, BattlerTagLapseType.CUSTOM, 0);
|
||||
}
|
||||
|
||||
/** @override */
|
||||
override isMoveRestricted(move: MoveId): boolean {
|
||||
return move !== this.moveId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @param {Pokemon} pokemon the {@linkcode Pokemon} to check if the tag can be added
|
||||
* @returns `true` if the pokemon has a valid move and no existing {@linkcode GorillaTacticsTag}; `false` otherwise
|
||||
* Ensures that move history exists on {@linkcode Pokemon} and has a valid move to lock into.
|
||||
* @param pokemon - The {@linkcode Pokemon} to add the tag to
|
||||
* @returns `true` if the tag can be added
|
||||
*/
|
||||
override canAdd(pokemon: Pokemon): boolean {
|
||||
return this.getLastValidMove(pokemon) !== undefined && !pokemon.getTag(GorillaTacticsTag);
|
||||
// Choice items ignore struggle, so Gorilla Tactics should too
|
||||
const lastSelectedMove = pokemon.getLastNonVirtualMove();
|
||||
return !isNullOrUndefined(lastSelectedMove) && lastSelectedMove.move !== MoveId.STRUGGLE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that move history exists on {@linkcode Pokemon} and has a valid move.
|
||||
* If so, sets the {@linkcode moveId} and increases the user's Attack by 50%.
|
||||
* @override
|
||||
* @param {Pokemon} pokemon the {@linkcode Pokemon} to add the tag to
|
||||
* Sets this tag's {@linkcode moveId} and increases the user's Attack by 50%.
|
||||
* @param pokemon - The {@linkcode Pokemon} to add the tag to
|
||||
*/
|
||||
override onAdd(pokemon: Pokemon): void {
|
||||
const lastValidMove = this.getLastValidMove(pokemon);
|
||||
super.onAdd(pokemon);
|
||||
|
||||
if (!lastValidMove) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.moveId = lastValidMove;
|
||||
// Bang is justified as tag is not added if prior move doesn't exist
|
||||
this.moveId = pokemon.getLastNonVirtualMove()!.move;
|
||||
pokemon.setStat(Stat.ATK, pokemon.getStat(Stat.ATK, false) * 1.5, false);
|
||||
}
|
||||
|
||||
@ -388,29 +381,16 @@ export class GorillaTacticsTag extends MoveRestrictionBattlerTag {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @override
|
||||
* @param {Pokemon} pokemon n/a
|
||||
* @param {MoveId} _move {@linkcode MoveId} ID of the move being denied
|
||||
* @returns {string} text to display when the move is denied
|
||||
* Return the text displayed when a move is restricted.
|
||||
* @param pokemon - The {@linkcode Pokemon} with this tag.
|
||||
* @returns A string containing the text to display when the move is denied
|
||||
*/
|
||||
override selectionDeniedText(pokemon: Pokemon, _move: MoveId): string {
|
||||
override selectionDeniedText(pokemon: Pokemon): string {
|
||||
return i18next.t("battle:canOnlyUseMove", {
|
||||
moveName: allMoves[this.moveId].name,
|
||||
pokemonName: getPokemonNameWithAffix(pokemon),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the last valid move from the pokemon's move history.
|
||||
* @param {Pokemon} pokemon {@linkcode Pokemon} to get the last valid move from
|
||||
* @returns {MoveId | undefined} the last valid move from the pokemon's move history
|
||||
*/
|
||||
getLastValidMove(pokemon: Pokemon): MoveId | undefined {
|
||||
const move = pokemon.getLastXMoves().find(m => m.move !== MoveId.NONE && m.move !== MoveId.STRUGGLE && !m.virtual);
|
||||
|
||||
return move?.move;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -424,8 +404,8 @@ export class RechargingTag extends BattlerTag {
|
||||
onAdd(pokemon: Pokemon): void {
|
||||
super.onAdd(pokemon);
|
||||
|
||||
// Queue a placeholder move for the Pokemon to "use" next turn
|
||||
pokemon.getMoveQueue().push({ move: MoveId.NONE, targets: [] });
|
||||
// Queue a placeholder move for the Pokemon to "use" next turn.
|
||||
pokemon.pushMoveQueue({ move: MoveId.NONE, targets: [], useMode: MoveUseMode.NORMAL });
|
||||
}
|
||||
|
||||
/** Cancels the source's move this turn and queues a "__ must recharge!" message */
|
||||
@ -670,6 +650,7 @@ export class InterruptedTag extends BattlerTag {
|
||||
move: MoveId.NONE,
|
||||
result: MoveResult.OTHER,
|
||||
targets: [],
|
||||
useMode: MoveUseMode.NORMAL,
|
||||
});
|
||||
}
|
||||
|
||||
@ -995,42 +976,45 @@ export class PowderTag extends BattlerTag {
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies Powder's effects before the tag owner uses a Fire-type move.
|
||||
* Also causes the tag to expire at the end of turn.
|
||||
* @param pokemon {@linkcode Pokemon} the owner of this tag
|
||||
* @param lapseType {@linkcode BattlerTagLapseType} the type of lapse functionality to carry out
|
||||
* @returns `true` if the tag should not expire after this lapse; `false` otherwise.
|
||||
* Applies Powder's effects before the tag owner uses a Fire-type move, damaging and canceling its action.
|
||||
* Lasts until the end of the turn.
|
||||
* @param pokemon - The {@linkcode Pokemon} with this tag.
|
||||
* @param lapseType - The {@linkcode BattlerTagLapseType} dictating how this tag is being activated
|
||||
* @returns `true` if the tag should remain active.
|
||||
*/
|
||||
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
||||
if (lapseType === BattlerTagLapseType.PRE_MOVE) {
|
||||
const movePhase = globalScene.phaseManager.getCurrentPhase();
|
||||
if (movePhase?.is("MovePhase")) {
|
||||
const move = movePhase.move.getMove();
|
||||
const weather = globalScene.arena.weather;
|
||||
if (
|
||||
pokemon.getMoveType(move) === PokemonType.FIRE &&
|
||||
!(weather && weather.weatherType === WeatherType.HEAVY_RAIN && !weather.isEffectSuppressed())
|
||||
) {
|
||||
movePhase.fail();
|
||||
movePhase.showMoveText();
|
||||
const movePhase = globalScene.phaseManager.getCurrentPhase();
|
||||
if (lapseType !== BattlerTagLapseType.PRE_MOVE || !movePhase?.is("MovePhase")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const idx = pokemon.getBattlerIndex();
|
||||
|
||||
globalScene.phaseManager.unshiftNew("CommonAnimPhase", idx, idx, CommonAnim.POWDER);
|
||||
|
||||
const cancelDamage = new BooleanHolder(false);
|
||||
applyAbAttrs("BlockNonDirectDamageAbAttr", pokemon, cancelDamage);
|
||||
if (!cancelDamage.value) {
|
||||
pokemon.damageAndUpdate(Math.floor(pokemon.getMaxHp() / 4), { result: HitResult.INDIRECT });
|
||||
}
|
||||
|
||||
// "When the flame touched the powder\non the Pokémon, it exploded!"
|
||||
globalScene.phaseManager.queueMessage(i18next.t("battlerTags:powderLapse", { moveName: move.name }));
|
||||
}
|
||||
}
|
||||
const move = movePhase.move.getMove();
|
||||
const weather = globalScene.arena.weather;
|
||||
if (
|
||||
pokemon.getMoveType(move) !== PokemonType.FIRE ||
|
||||
(weather?.weatherType === WeatherType.HEAVY_RAIN && !weather.isEffectSuppressed()) // Heavy rain takes priority over powder
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return super.lapse(pokemon, lapseType);
|
||||
|
||||
// Disable the target's fire type move and damage it (subject to Magic Guard)
|
||||
movePhase.showMoveText();
|
||||
movePhase.fail();
|
||||
|
||||
const idx = pokemon.getBattlerIndex();
|
||||
|
||||
globalScene.phaseManager.unshiftNew("CommonAnimPhase", idx, idx, CommonAnim.POWDER);
|
||||
|
||||
const cancelDamage = new BooleanHolder(false);
|
||||
applyAbAttrs("BlockNonDirectDamageAbAttr", pokemon, cancelDamage);
|
||||
if (!cancelDamage.value) {
|
||||
pokemon.damageAndUpdate(Math.floor(pokemon.getMaxHp() / 4), { result: HitResult.INDIRECT });
|
||||
}
|
||||
|
||||
// "When the flame touched the powder\non the Pokémon, it exploded!"
|
||||
globalScene.phaseManager.queueMessage(i18next.t("battlerTags:powderLapse", { moveName: move.name }));
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1125,34 +1109,22 @@ export class EncoreTag extends MoveRestrictionBattlerTag {
|
||||
}
|
||||
|
||||
canAdd(pokemon: Pokemon): boolean {
|
||||
const lastMoves = pokemon.getLastXMoves(1);
|
||||
if (!lastMoves.length) {
|
||||
const lastMove = pokemon.getLastNonVirtualMove();
|
||||
if (!lastMove) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const repeatableMove = lastMoves[0];
|
||||
|
||||
if (!repeatableMove.move || repeatableMove.virtual) {
|
||||
if (invalidEncoreMoves.has(lastMove.move)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (repeatableMove.move) {
|
||||
case MoveId.MIMIC:
|
||||
case MoveId.MIRROR_MOVE:
|
||||
case MoveId.TRANSFORM:
|
||||
case MoveId.STRUGGLE:
|
||||
case MoveId.SKETCH:
|
||||
case MoveId.SLEEP_TALK:
|
||||
case MoveId.ENCORE:
|
||||
return false;
|
||||
}
|
||||
|
||||
this.moveId = repeatableMove.move;
|
||||
this.moveId = lastMove.move;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
onAdd(pokemon: Pokemon): void {
|
||||
// TODO: shouldn't this be `onAdd`?
|
||||
super.onRemove(pokemon);
|
||||
|
||||
globalScene.phaseManager.queueMessage(
|
||||
@ -1168,7 +1140,13 @@ export class EncoreTag extends MoveRestrictionBattlerTag {
|
||||
const lastMove = pokemon.getLastXMoves(1)[0];
|
||||
globalScene.phaseManager.tryReplacePhase(
|
||||
m => m.is("MovePhase") && m.pokemon === pokemon,
|
||||
globalScene.phaseManager.create("MovePhase", pokemon, lastMove.targets ?? [], movesetMove),
|
||||
globalScene.phaseManager.create(
|
||||
"MovePhase",
|
||||
pokemon,
|
||||
lastMove.targets ?? [],
|
||||
movesetMove,
|
||||
MoveUseMode.NORMAL,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1470,16 +1448,6 @@ export class WrapTag extends DamagingTrapTag {
|
||||
}
|
||||
|
||||
export abstract class VortexTrapTag extends DamagingTrapTag {
|
||||
constructor(
|
||||
tagType: BattlerTagType,
|
||||
commonAnim: CommonAnim,
|
||||
turnCount: number,
|
||||
sourceMove: MoveId,
|
||||
sourceId: number,
|
||||
) {
|
||||
super(tagType, commonAnim, turnCount, sourceMove, sourceId);
|
||||
}
|
||||
|
||||
getTrapMessage(pokemon: Pokemon): string {
|
||||
return i18next.t("battlerTags:vortexOnTrap", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||
@ -1904,24 +1872,29 @@ export class TruantTag extends AbilityBattlerTag {
|
||||
|
||||
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
||||
if (!pokemon.hasAbility(AbilityId.TRUANT)) {
|
||||
// remove tag if mon lacks ability
|
||||
return super.lapse(pokemon, lapseType);
|
||||
}
|
||||
const passive = pokemon.getAbility().id !== AbilityId.TRUANT;
|
||||
|
||||
const lastMove = pokemon.getLastXMoves().find(() => true);
|
||||
const lastMove = pokemon.getLastXMoves()[0];
|
||||
|
||||
if (lastMove && lastMove.move !== MoveId.NONE) {
|
||||
(globalScene.phaseManager.getCurrentPhase() as MovePhase).cancel();
|
||||
// TODO: Ability displays should be handled by the ability
|
||||
globalScene.phaseManager.queueAbilityDisplay(pokemon, passive, true);
|
||||
globalScene.phaseManager.queueMessage(
|
||||
i18next.t("battlerTags:truantLapse", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||
}),
|
||||
);
|
||||
globalScene.phaseManager.queueAbilityDisplay(pokemon, passive, false);
|
||||
if (!lastMove) {
|
||||
// Don't interrupt move if last move was `Moves.NONE` OR no prior move was found
|
||||
return true;
|
||||
}
|
||||
|
||||
// Interrupt move usage in favor of slacking off
|
||||
const passive = pokemon.getAbility().id !== AbilityId.TRUANT;
|
||||
(globalScene.phaseManager.getCurrentPhase() as MovePhase).cancel();
|
||||
// TODO: Ability displays should be handled by the ability
|
||||
globalScene.phaseManager.queueAbilityDisplay(pokemon, passive, true);
|
||||
globalScene.phaseManager.queueMessage(
|
||||
i18next.t("battlerTags:truantLapse", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||
}),
|
||||
);
|
||||
globalScene.phaseManager.queueAbilityDisplay(pokemon, passive, false);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { MoveId } from "#enums/move-id";
|
||||
|
||||
/** Set of moves that cannot be called by {@linkcode MoveId.METRONOME Metronome} */
|
||||
/** Set of moves that cannot be called by {@linkcode MoveId.METRONOME | Metronome}. */
|
||||
export const invalidMetronomeMoves: ReadonlySet<MoveId> = new Set([
|
||||
MoveId.AFTER_YOU,
|
||||
MoveId.ASSIST,
|
||||
@ -255,3 +255,28 @@ export const noAbilityTypeOverrideMoves: ReadonlySet<MoveId> = new Set([
|
||||
MoveId.TECHNO_BLAST,
|
||||
MoveId.HIDDEN_POWER,
|
||||
]);
|
||||
|
||||
/** Set of all moves that cannot be copied by {@linkcode Moves.SKETCH}. */
|
||||
export const invalidSketchMoves: ReadonlySet<MoveId> = new Set([
|
||||
MoveId.NONE,
|
||||
MoveId.CHATTER,
|
||||
MoveId.MIRROR_MOVE,
|
||||
MoveId.SLEEP_TALK,
|
||||
MoveId.STRUGGLE,
|
||||
MoveId.SKETCH,
|
||||
MoveId.REVIVAL_BLESSING,
|
||||
MoveId.TERA_STARSTORM,
|
||||
MoveId.BREAKNECK_BLITZ__PHYSICAL,
|
||||
MoveId.BREAKNECK_BLITZ__SPECIAL,
|
||||
]);
|
||||
|
||||
/** Set of all moves that cannot be locked into by {@linkcode Moves.ENCORE}. */
|
||||
export const invalidEncoreMoves: ReadonlySet<MoveId> = new Set([
|
||||
MoveId.MIMIC,
|
||||
MoveId.MIRROR_MOVE,
|
||||
MoveId.TRANSFORM,
|
||||
MoveId.STRUGGLE,
|
||||
MoveId.SKETCH,
|
||||
MoveId.SLEEP_TALK,
|
||||
MoveId.ENCORE,
|
||||
]);
|
||||
|
@ -70,13 +70,9 @@ import {
|
||||
getStatKey,
|
||||
Stat,
|
||||
} from "#app/enums/stat";
|
||||
import { BattleEndPhase } from "#app/phases/battle-end-phase";
|
||||
import { MoveEndPhase } from "#app/phases/move-end-phase";
|
||||
import { MovePhase } from "#app/phases/move-phase";
|
||||
import { NewBattlePhase } from "#app/phases/new-battle-phase";
|
||||
import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase";
|
||||
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
|
||||
import { SwitchPhase } from "#app/phases/switch-phase";
|
||||
import { SwitchSummonPhase } from "#app/phases/switch-summon-phase";
|
||||
import { SpeciesFormChangeRevertWeatherFormTrigger } from "../pokemon-forms/form-change-triggers";
|
||||
import type { GameMode } from "#app/game-mode";
|
||||
@ -85,18 +81,14 @@ import { ChallengeType } from "#enums/challenge-type";
|
||||
import { SwitchType } from "#enums/switch-type";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import { RevivalBlessingPhase } from "#app/phases/revival-blessing-phase";
|
||||
import { LoadMoveAnimPhase } from "#app/phases/load-move-anim-phase";
|
||||
import { PokemonTransformPhase } from "#app/phases/pokemon-transform-phase";
|
||||
import { MoveAnimPhase } from "#app/phases/move-anim-phase";
|
||||
import { loggedInUser } from "#app/account";
|
||||
import { MoveCategory } from "#enums/MoveCategory";
|
||||
import { MoveTarget } from "#enums/MoveTarget";
|
||||
import { MoveFlags } from "#enums/MoveFlags";
|
||||
import { MoveEffectTrigger } from "#enums/MoveEffectTrigger";
|
||||
import { MultiHitType } from "#enums/MultiHitType";
|
||||
import { invalidAssistMoves, invalidCopycatMoves, invalidMetronomeMoves, invalidMirrorMoveMoves, invalidSleepTalkMoves } from "./invalid-moves";
|
||||
import { SelectBiomePhase } from "#app/phases/select-biome-phase";
|
||||
import { invalidAssistMoves, invalidCopycatMoves, invalidMetronomeMoves, invalidMirrorMoveMoves, invalidSleepTalkMoves, invalidSketchMoves } from "./invalid-moves";
|
||||
import { isVirtual, MoveUseMode } from "#enums/move-use-mode";
|
||||
import { ChargingMove, MoveAttrMap, MoveAttrString, MoveKindString, MoveClassMap } from "#app/@types/move-types";
|
||||
import { applyMoveAttrs } from "./apply-attrs";
|
||||
import { frenzyMissFunc, getMoveTargets } from "./move-utils";
|
||||
@ -126,10 +118,10 @@ export default abstract class Move implements Localizable {
|
||||
|
||||
/**
|
||||
* Check if the move is of the given subclass without requiring `instanceof`.
|
||||
*
|
||||
*
|
||||
* ⚠️ Does _not_ work for {@linkcode ChargingAttackMove} and {@linkcode ChargingSelfStatusMove} subclasses. For those,
|
||||
* use {@linkcode isChargingMove} instead.
|
||||
*
|
||||
*
|
||||
* @param moveKind - The string name of the move to check against
|
||||
* @returns Whether this move is of the provided type.
|
||||
*/
|
||||
@ -2156,6 +2148,7 @@ export class PlantHealAttr extends WeatherHealAttr {
|
||||
case WeatherType.SANDSTORM:
|
||||
case WeatherType.HAIL:
|
||||
case WeatherType.SNOW:
|
||||
case WeatherType.FOG:
|
||||
case WeatherType.HEAVY_RAIN:
|
||||
return 0.25;
|
||||
default:
|
||||
@ -3123,7 +3116,7 @@ export class DelayedAttackAttr extends OverrideMoveEffectAttr {
|
||||
overridden.value = true;
|
||||
globalScene.phaseManager.unshiftNew("MoveAnimPhase", new MoveChargeAnim(this.chargeAnim, move.id, user));
|
||||
globalScene.phaseManager.queueMessage(this.chargeText.replace("{TARGET}", getPokemonNameWithAffix(target)).replace("{USER}", getPokemonNameWithAffix(user)));
|
||||
user.pushMoveHistory({ move: move.id, targets: [ target.getBattlerIndex() ], result: MoveResult.OTHER });
|
||||
user.pushMoveHistory({ move: move.id, targets: [ target.getBattlerIndex() ], result: MoveResult.OTHER, useMode: MoveUseMode.NORMAL });
|
||||
const side = target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
|
||||
globalScene.arena.addTag(this.tagType, 3, move.id, user.id, side, false, target.getBattlerIndex());
|
||||
} else {
|
||||
@ -4080,15 +4073,26 @@ export class OpponentHighHpPowerAttr extends VariablePowerAttr {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute to double this move's power if the target hasn't acted yet in the current turn.
|
||||
* Used by {@linkcode Moves.BOLT_BEAK} and {@linkcode Moves.FISHIOUS_REND}
|
||||
*/
|
||||
export class FirstAttackDoublePowerAttr extends VariablePowerAttr {
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
console.log(target.getLastXMoves(1), globalScene.currentBattle.turn);
|
||||
if (!target.getLastXMoves(1).find(m => m.turn === globalScene.currentBattle.turn)) {
|
||||
(args[0] as NumberHolder).value *= 2;
|
||||
return true;
|
||||
/**
|
||||
* Double this move's power if the user is acting before the target.
|
||||
* @param user - Unused
|
||||
* @param target - The {@linkcode Pokemon} being targeted by this move
|
||||
* @param move - Unused
|
||||
* @param args `[0]` - A {@linkcode NumberHolder} containing move base power
|
||||
* @returns Whether the attribute was successfully applied
|
||||
*/
|
||||
apply(_user: Pokemon, target: Pokemon, move: Move, args: [NumberHolder]): boolean {
|
||||
if (target.turnData.acted) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
args[0].value *= 2;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -4157,6 +4161,7 @@ export class AntiSunlightPowerDecreaseAttr extends VariablePowerAttr {
|
||||
case WeatherType.SANDSTORM:
|
||||
case WeatherType.HAIL:
|
||||
case WeatherType.SNOW:
|
||||
case WeatherType.FOG:
|
||||
case WeatherType.HEAVY_RAIN:
|
||||
power.value *= 0.5;
|
||||
return true;
|
||||
@ -5457,13 +5462,20 @@ export class FrenzyAttr extends MoveEffectAttr {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!user.getTag(BattlerTagType.FRENZY) && !user.getMoveQueue().length) {
|
||||
const turnCount = user.randBattleSeedIntRange(1, 2);
|
||||
new Array(turnCount).fill(null).map(() => user.getMoveQueue().push({ move: move.id, targets: [ target.getBattlerIndex() ], ignorePP: true }));
|
||||
// TODO: Disable if used via dancer
|
||||
// TODO: Add support for moves that don't add the frenzy tag (Uproar, Rollout, etc.)
|
||||
|
||||
// If frenzy is not active, add a tag and push 1-2 extra turns of attacks to the user's move queue.
|
||||
// Otherwise, tick down the existing tag.
|
||||
if (!user.getTag(BattlerTagType.FRENZY) && user.getMoveQueue().length === 0) {
|
||||
const turnCount = user.randBattleSeedIntRange(1, 2); // excludes initial use
|
||||
for (let i = 0; i < turnCount; i++) {
|
||||
user.pushMoveQueue({ move: move.id, targets: [ target.getBattlerIndex() ], useMode: MoveUseMode.IGNORE_PP });
|
||||
}
|
||||
user.addTag(BattlerTagType.FRENZY, turnCount, move.id, user.id);
|
||||
} else {
|
||||
applyMoveAttrs("AddBattlerTagAttr", user, target, move, args);
|
||||
user.lapseTag(BattlerTagType.FRENZY); // if FRENZY is already in effect (moveQueue.length > 0), lapse the tag
|
||||
user.lapseTag(BattlerTagType.FRENZY);
|
||||
}
|
||||
|
||||
return true;
|
||||
@ -6218,6 +6230,7 @@ export class RevivalBlessingAttr extends MoveEffectAttr {
|
||||
globalScene.phaseManager.tryRemovePhase((phase: SwitchSummonPhase) => phase.is("SwitchSummonPhase") && phase.getPokemon() === pokemon);
|
||||
// If the pokemon being revived was alive earlier in the turn, cancel its move
|
||||
// (revived pokemon can't move in the turn they're brought back)
|
||||
// TODO: might make sense to move this to `FaintPhase` after checking for Rev Seed (rather than handling it in the move)
|
||||
globalScene.phaseManager.findPhase((phase: MovePhase) => phase.pokemon === pokemon)?.cancel();
|
||||
if (user.fieldPosition === FieldPosition.CENTER) {
|
||||
user.setFieldPosition(FieldPosition.LEFT);
|
||||
@ -6759,20 +6772,26 @@ export class FirstMoveTypeAttr extends MoveEffectAttr {
|
||||
class CallMoveAttr extends OverrideMoveEffectAttr {
|
||||
protected invalidMoves: ReadonlySet<MoveId>;
|
||||
protected hasTarget: boolean;
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
// Get eligible targets for move, failing if we can't target anything
|
||||
const replaceMoveTarget = move.moveTarget === MoveTarget.NEAR_OTHER ? MoveTarget.NEAR_ENEMY : undefined;
|
||||
const moveTargets = getMoveTargets(user, move.id, replaceMoveTarget);
|
||||
if (moveTargets.targets.length === 0) {
|
||||
globalScene.phaseManager.queueMessage(i18next.t("battle:attackFailed"));
|
||||
console.log("CallMoveAttr failed due to no targets.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Spread moves and ones with only 1 valid target will use their normal targeting.
|
||||
// If not, target the Mirror Move recipient or else a random enemy in our target list
|
||||
const targets = moveTargets.multiple || moveTargets.targets.length === 1
|
||||
? moveTargets.targets
|
||||
: [ this.hasTarget ? target.getBattlerIndex() : moveTargets.targets[user.randBattleSeedInt(moveTargets.targets.length)] ]; // account for Mirror Move having a target already
|
||||
user.getMoveQueue().push({ move: move.id, targets: targets, virtual: true, ignorePP: true });
|
||||
: [this.hasTarget
|
||||
? target.getBattlerIndex()
|
||||
: moveTargets.targets[user.randBattleSeedInt(moveTargets.targets.length)]];
|
||||
|
||||
globalScene.phaseManager.unshiftNew("LoadMoveAnimPhase", move.id);
|
||||
globalScene.phaseManager.unshiftNew("MovePhase", user, targets, new PokemonMove(move.id, 0, 0, true), true, true);
|
||||
globalScene.phaseManager.unshiftNew("MovePhase", user, targets, new PokemonMove(move.id), MoveUseMode.FOLLOW_UP);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -6867,9 +6886,10 @@ export class RandomMovesetMoveAttr extends CallMoveAttr {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: extend CallMoveAttr
|
||||
export class NaturePowerAttr extends OverrideMoveEffectAttr {
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
let moveId;
|
||||
let moveId = MoveId.NONE;
|
||||
switch (globalScene.arena.getTerrainType()) {
|
||||
// this allows terrains to 'override' the biome move
|
||||
case TerrainType.NONE:
|
||||
@ -6994,14 +7014,14 @@ export class NaturePowerAttr extends OverrideMoveEffectAttr {
|
||||
moveId = MoveId.PSYCHIC;
|
||||
break;
|
||||
default:
|
||||
// Just in case there's no match
|
||||
// Just in case there's no match
|
||||
moveId = MoveId.TRI_ATTACK;
|
||||
break;
|
||||
}
|
||||
|
||||
user.getMoveQueue().push({ move: moveId, targets: [ target.getBattlerIndex() ], ignorePP: true });
|
||||
// Load the move's animation if we didn't already and unshift a new usage phase
|
||||
globalScene.phaseManager.unshiftNew("LoadMoveAnimPhase", moveId);
|
||||
globalScene.phaseManager.unshiftNew("MovePhase", user, [ target.getBattlerIndex() ], new PokemonMove(moveId, 0, 0, true), true);
|
||||
globalScene.phaseManager.unshiftNew("MovePhase", user, [ target.getBattlerIndex() ], new PokemonMove(moveId), MoveUseMode.FOLLOW_UP);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -7020,64 +7040,63 @@ export class CopyMoveAttr extends CallMoveAttr {
|
||||
this.invalidMoves = invalidMoves;
|
||||
}
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
apply(user: Pokemon, target: Pokemon, _move: Move, args: any[]): boolean {
|
||||
this.hasTarget = this.mirrorMove;
|
||||
const lastMove = this.mirrorMove ? target.getLastXMoves()[0].move : globalScene.currentBattle.lastMove;
|
||||
// bang is correct as condition func returns `false` and fails move if no last move exists
|
||||
const lastMove = this.mirrorMove ? target.getLastNonVirtualMove(false, false)!.move : globalScene.currentBattle.lastMove;
|
||||
return super.apply(user, target, allMoves[lastMove], args);
|
||||
}
|
||||
|
||||
getCondition(): MoveConditionFunc {
|
||||
return (user, target, move) => {
|
||||
if (this.mirrorMove) {
|
||||
const lastMove = target.getLastXMoves()[0]?.move;
|
||||
return !!lastMove && !this.invalidMoves.has(lastMove);
|
||||
} else {
|
||||
const lastMove = globalScene.currentBattle.lastMove;
|
||||
return lastMove !== undefined && !this.invalidMoves.has(lastMove);
|
||||
}
|
||||
return (_user, target, _move) => {
|
||||
const lastMove = this.mirrorMove ? target.getLastNonVirtualMove(false, false)?.move : globalScene.currentBattle.lastMove;
|
||||
return !isNullOrUndefined(lastMove) && !this.invalidMoves.has(lastMove);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute used for moves that causes the target to repeat their last used move.
|
||||
* Attribute used for moves that cause the target to repeat their last used move.
|
||||
*
|
||||
* Used for [Instruct](https://bulbapedia.bulbagarden.net/wiki/Instruct_(move)).
|
||||
*/
|
||||
export class RepeatMoveAttr extends MoveEffectAttr {
|
||||
private movesetMove: PokemonMove;
|
||||
constructor() {
|
||||
super(false, { trigger: MoveEffectTrigger.POST_APPLY }); // needed to ensure correct protect interaction
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces the target to re-use their last used move again
|
||||
*
|
||||
* @param user {@linkcode Pokemon} that used the attack
|
||||
* @param target {@linkcode Pokemon} targeted by the attack
|
||||
* @param move N/A
|
||||
* @param args N/A
|
||||
* Forces the target to re-use their last used move again.
|
||||
* @param user - The {@linkcode Pokemon} using the attack
|
||||
* @param target - The {@linkcode Pokemon} being targeted by the attack
|
||||
* @returns `true` if the move succeeds
|
||||
*/
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
apply(user: Pokemon, target: Pokemon): boolean {
|
||||
// get the last move used (excluding status based failures) as well as the corresponding moveset slot
|
||||
const lastMove = target.getLastXMoves(-1).find(m => m.move !== MoveId.NONE)!;
|
||||
const movesetMove = target.getMoveset().find(m => m.moveId === lastMove.move)!;
|
||||
// If the last move used can hit more than one target or has variable targets,
|
||||
// re-compute the targets for the attack
|
||||
// (mainly for alternating double/single battle shenanigans)
|
||||
// Rampaging moves (e.g. Outrage) are not included due to being incompatible with Instruct
|
||||
// TODO: Fix this once dragon darts gets smart targeting
|
||||
let moveTargets = movesetMove.getMove().isMultiTarget() ? getMoveTargets(target, lastMove.move).targets : lastMove.targets;
|
||||
// bangs are justified as Instruct fails if no prior move or moveset move exists
|
||||
// TODO: How does instruct work when copying a move called via Copycat that the user itself knows?
|
||||
const lastMove = target.getLastNonVirtualMove()!;
|
||||
const movesetMove = target.getMoveset().find(m => m.moveId === lastMove?.move)!
|
||||
|
||||
/** In the event the instructed move's only target is a fainted opponent, redirect it to an alive ally if possible
|
||||
Normally, all yet-unexecuted move phases would swap over when the enemy in question faints
|
||||
(see `redirectPokemonMoves` in `battle-scene.ts`),
|
||||
but since instruct adds a new move phase pre-emptively, we need to handle this interaction manually.
|
||||
*/
|
||||
// If the last move used can hit more than one target or has variable targets,
|
||||
// re-compute the targets for the attack (mainly for alternating double/single battles)
|
||||
// Rampaging moves (e.g. Outrage) are not included due to being incompatible with Instruct,
|
||||
// nor is Dragon Darts (due to its smart targeting bypassing normal target selection)
|
||||
let moveTargets = this.movesetMove.getMove().isMultiTarget() ? getMoveTargets(target, this.movesetMove.moveId).targets : lastMove.targets;
|
||||
|
||||
// In the event the instructed move's only target is a fainted opponent, redirect it to an alive ally if possible.
|
||||
// Normally, all yet-unexecuted move phases would swap targets after any foe faints or flees (see `redirectPokemonMoves` in `battle-scene.ts`),
|
||||
// but since Instruct adds a new move phase _after_ all that occurs, we need to handle this interaction manually.
|
||||
const firstTarget = globalScene.getField()[moveTargets[0]];
|
||||
if (globalScene.currentBattle.double && moveTargets.length === 1 && firstTarget.isFainted() && firstTarget !== target.getAlly()) {
|
||||
if (
|
||||
globalScene.currentBattle.double
|
||||
&& moveTargets.length === 1
|
||||
&& firstTarget.isFainted()
|
||||
&& firstTarget !== target.getAlly()
|
||||
) {
|
||||
const ally = firstTarget.getAlly();
|
||||
if (!isNullOrUndefined(ally) && ally.isActive()) { // ally exists, is not dead and can sponge the blast
|
||||
if (!isNullOrUndefined(ally) && ally.isActive()) {
|
||||
moveTargets = [ ally.getBattlerIndex() ];
|
||||
}
|
||||
}
|
||||
@ -7086,15 +7105,15 @@ export class RepeatMoveAttr extends MoveEffectAttr {
|
||||
userPokemonName: getPokemonNameWithAffix(user),
|
||||
targetPokemonName: getPokemonNameWithAffix(target)
|
||||
}));
|
||||
target.getMoveQueue().unshift({ move: lastMove.move, targets: moveTargets, ignorePP: false });
|
||||
target.turnData.extraTurns++;
|
||||
globalScene.phaseManager.appendNewToPhase("MoveEndPhase", "MovePhase", target, moveTargets, movesetMove);
|
||||
globalScene.phaseManager.appendNewToPhase("MoveEndPhase", "MovePhase", target, moveTargets, movesetMove, MoveUseMode.NORMAL);
|
||||
return true;
|
||||
}
|
||||
|
||||
getCondition(): MoveConditionFunc {
|
||||
return (user, target, move) => {
|
||||
const lastMove = target.getLastXMoves(-1).find(m => m.move !== MoveId.NONE);
|
||||
return (_user, target, _move) => {
|
||||
// TODO: Check instruct behavior with struggle - ignore, fail or success
|
||||
const lastMove = target.getLastNonVirtualMove();
|
||||
const movesetMove = target.getMoveset().find(m => m.moveId === lastMove?.move);
|
||||
const uninstructableMoves = [
|
||||
// Locking/Continually Executed moves
|
||||
@ -7104,6 +7123,7 @@ export class RepeatMoveAttr extends MoveEffectAttr {
|
||||
MoveId.PETAL_DANCE,
|
||||
MoveId.THRASH,
|
||||
MoveId.ICE_BALL,
|
||||
MoveId.UPROAR,
|
||||
// Multi-turn Moves
|
||||
MoveId.BIDE,
|
||||
MoveId.SHELL_TRAP,
|
||||
@ -7141,23 +7161,34 @@ export class RepeatMoveAttr extends MoveEffectAttr {
|
||||
MoveId.SOLAR_BEAM,
|
||||
MoveId.SOLAR_BLADE,
|
||||
MoveId.METEOR_BEAM,
|
||||
// Other moves
|
||||
// Copying/Move-Calling moves
|
||||
MoveId.ASSIST,
|
||||
MoveId.COPYCAT,
|
||||
MoveId.ME_FIRST,
|
||||
MoveId.METRONOME,
|
||||
MoveId.MIRROR_MOVE,
|
||||
MoveId.NATURE_POWER,
|
||||
MoveId.SLEEP_TALK,
|
||||
MoveId.SNATCH,
|
||||
MoveId.INSTRUCT,
|
||||
// Misc moves
|
||||
MoveId.KINGS_SHIELD,
|
||||
MoveId.SKETCH,
|
||||
MoveId.TRANSFORM,
|
||||
MoveId.MIMIC,
|
||||
MoveId.STRUGGLE,
|
||||
// TODO: Add Max/G-Move blockage if or when they are implemented
|
||||
// TODO: Add Max/G-Max/Z-Move blockage if or when they are implemented
|
||||
];
|
||||
|
||||
if (!lastMove?.move // no move to instruct
|
||||
|| !movesetMove // called move not in target's moveset (forgetting the move, etc.)
|
||||
|| movesetMove.ppUsed === movesetMove.getMovePp() // move out of pp
|
||||
// TODO: This next line is likely redundant as all charging moves are in the above list
|
||||
|| allMoves[lastMove.move].isChargingMove() // called move is a charging/recharging move
|
||||
|| uninstructableMoves.includes(lastMove.move)) { // called move is in the banlist
|
||||
return false;
|
||||
}
|
||||
this.movesetMove = movesetMove;
|
||||
return true;
|
||||
};
|
||||
}
|
||||
@ -7188,53 +7219,48 @@ export class ReducePpMoveAttr extends MoveEffectAttr {
|
||||
/**
|
||||
* Reduces the PP of the target's last-used move by an amount based on this attribute instance's {@linkcode reduction}.
|
||||
*
|
||||
* @param user {@linkcode Pokemon} that used the attack
|
||||
* @param target {@linkcode Pokemon} targeted by the attack
|
||||
* @param move N/A
|
||||
* @param args N/A
|
||||
* @returns `true`
|
||||
* @param user - N/A
|
||||
* @param target - The {@linkcode Pokemon} targeted by the attack
|
||||
* @param move - N/A
|
||||
* @param args - N/A
|
||||
* @returns always `true`
|
||||
*/
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
// Null checks can be skipped due to condition function
|
||||
const lastMove = target.getLastXMoves()[0];
|
||||
const movesetMove = target.getMoveset().find(m => m.moveId === lastMove.move)!;
|
||||
/** The last move the target themselves used */
|
||||
const lastMove = target.getLastNonVirtualMove();
|
||||
const movesetMove = target.getMoveset().find(m => m.moveId === lastMove?.move)!; // bang is correct as condition prevents this from being nullish
|
||||
const lastPpUsed = movesetMove.ppUsed;
|
||||
movesetMove.ppUsed = Math.min((lastPpUsed) + this.reduction, movesetMove.getMovePp());
|
||||
movesetMove.ppUsed = Math.min(lastPpUsed + this.reduction, movesetMove.getMovePp());
|
||||
|
||||
const message = i18next.t("battle:ppReduced", { targetName: getPokemonNameWithAffix(target), moveName: movesetMove.getName(), reduction: (movesetMove.ppUsed) - lastPpUsed });
|
||||
globalScene.eventTarget.dispatchEvent(new MoveUsedEvent(target.id, movesetMove.getMove(), movesetMove.ppUsed));
|
||||
globalScene.phaseManager.queueMessage(message);
|
||||
globalScene.phaseManager.queueMessage(i18next.t("battle:ppReduced", { targetName: getPokemonNameWithAffix(target), moveName: movesetMove.getName(), reduction: (movesetMove.ppUsed) - lastPpUsed }));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
getCondition(): MoveConditionFunc {
|
||||
return (user, target, move) => {
|
||||
const lastMove = target.getLastXMoves()[0];
|
||||
if (lastMove) {
|
||||
const movesetMove = target.getMoveset().find(m => m.moveId === lastMove.move);
|
||||
return !!movesetMove?.getPpRatio();
|
||||
}
|
||||
return false;
|
||||
const lastMove = target.getLastNonVirtualMove();
|
||||
const movesetMove = target.getMoveset().find(m => m.moveId === lastMove?.move)
|
||||
return !!movesetMove?.getPpRatio();
|
||||
};
|
||||
}
|
||||
|
||||
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
|
||||
const lastMove = target.getLastXMoves()[0];
|
||||
if (lastMove) {
|
||||
const movesetMove = target.getMoveset().find(m => m.moveId === lastMove.move);
|
||||
if (movesetMove) {
|
||||
const maxPp = movesetMove.getMovePp();
|
||||
const ppLeft = maxPp - movesetMove.ppUsed;
|
||||
const value = -(8 - Math.ceil(Math.min(maxPp, 30) / 5));
|
||||
if (ppLeft < 4) {
|
||||
return (value / 4) * ppLeft;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
const lastMove = target.getLastNonVirtualMove();
|
||||
const movesetMove = target.getMoveset().find(m => m.moveId === lastMove?.move)
|
||||
if (!movesetMove) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 0;
|
||||
const maxPp = movesetMove.getMovePp();
|
||||
const ppLeft = maxPp - movesetMove.ppUsed;
|
||||
const value = -(8 - Math.ceil(Math.min(maxPp, 30) / 5));
|
||||
if (ppLeft < 4) {
|
||||
return (value / 4) * ppLeft;
|
||||
}
|
||||
return value;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -7250,40 +7276,36 @@ export class AttackReducePpMoveAttr extends ReducePpMoveAttr {
|
||||
/**
|
||||
* Checks if the target has used a move prior to the attack. PP-reduction is applied through the super class if so.
|
||||
*
|
||||
* @param user {@linkcode Pokemon} that used the attack
|
||||
* @param target {@linkcode Pokemon} targeted by the attack
|
||||
* @param move {@linkcode Move} being used
|
||||
* @param args N/A
|
||||
* @returns {boolean} true
|
||||
* @param user - The {@linkcode Pokemon} using the move
|
||||
* @param target -The {@linkcode Pokemon} targeted by the attack
|
||||
* @param move - The {@linkcode Move} being used
|
||||
* @param args - N/A
|
||||
* @returns - always `true`
|
||||
*/
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
const lastMove = target.getLastXMoves().find(() => true);
|
||||
if (lastMove) {
|
||||
const movesetMove = target.getMoveset().find(m => m.moveId === lastMove.move);
|
||||
if (Boolean(movesetMove?.getPpRatio())) {
|
||||
super.apply(user, target, move, args);
|
||||
}
|
||||
const lastMove = target.getLastNonVirtualMove();
|
||||
const movesetMove = target.getMoveset().find(m => m.moveId === lastMove?.move);
|
||||
if (movesetMove?.getPpRatio()) {
|
||||
super.apply(user, target, move, args);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Override condition function to always perform damage. Instead, perform pp-reduction condition check in apply function above
|
||||
getCondition(): MoveConditionFunc {
|
||||
return (user, target, move) => true;
|
||||
/**
|
||||
* Override condition function to always perform damage.
|
||||
* Instead, perform pp-reduction condition check in {@linkcode apply}.
|
||||
* (A failed condition will prevent damage which is not what we want here)
|
||||
* @returns always `true`
|
||||
*/
|
||||
override getCondition(): MoveConditionFunc {
|
||||
return () => true;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Review this
|
||||
const targetMoveCopiableCondition: MoveConditionFunc = (user, target, move) => {
|
||||
const targetMoves = target.getMoveHistory().filter(m => !m.virtual);
|
||||
if (!targetMoves.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const copiableMove = targetMoves[0];
|
||||
|
||||
if (!copiableMove.move) {
|
||||
const copiableMove = target.getLastNonVirtualMove();
|
||||
if (!copiableMove?.move) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -7296,14 +7318,18 @@ const targetMoveCopiableCondition: MoveConditionFunc = (user, target, move) => {
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Attribute to temporarily copy the last move in the target's moveset.
|
||||
* Used by {@linkcode Moves.MIMIC}.
|
||||
*/
|
||||
export class MovesetCopyMoveAttr extends OverrideMoveEffectAttr {
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
const targetMoves = target.getMoveHistory().filter(m => !m.virtual);
|
||||
if (!targetMoves.length) {
|
||||
const lastMove = target.getLastNonVirtualMove()
|
||||
if (!lastMove?.move) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const copiedMove = allMoves[targetMoves[0].move];
|
||||
const copiedMove = allMoves[lastMove.move];
|
||||
|
||||
const thisMoveIndex = user.getMoveset().findIndex(m => m.moveId === move.id);
|
||||
|
||||
@ -7311,8 +7337,9 @@ export class MovesetCopyMoveAttr extends OverrideMoveEffectAttr {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Populate summon data with a copy of the current moveset, replacing the copying move with the copied move
|
||||
user.summonData.moveset = user.getMoveset().slice(0);
|
||||
user.summonData.moveset[thisMoveIndex] = new PokemonMove(copiedMove.id, 0, 0);
|
||||
user.summonData.moveset[thisMoveIndex] = new PokemonMove(copiedMove.id);
|
||||
|
||||
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:copiedMove", { pokemonName: getPokemonNameWithAffix(user), moveName: copiedMove.name }));
|
||||
|
||||
@ -7350,9 +7377,9 @@ export class SketchAttr extends MoveEffectAttr {
|
||||
return false;
|
||||
}
|
||||
|
||||
const targetMove = target.getLastXMoves(-1)
|
||||
.find(m => m.move !== MoveId.NONE && m.move !== MoveId.STRUGGLE && !m.virtual);
|
||||
const targetMove = target.getLastNonVirtualMove()
|
||||
if (!targetMove) {
|
||||
// failsafe for TS compiler
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -7375,28 +7402,10 @@ export class SketchAttr extends MoveEffectAttr {
|
||||
return false;
|
||||
}
|
||||
|
||||
const targetMove = target.getMoveHistory().filter(m => !m.virtual).at(-1);
|
||||
if (!targetMove) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const unsketchableMoves = [
|
||||
MoveId.CHATTER,
|
||||
MoveId.MIRROR_MOVE,
|
||||
MoveId.SLEEP_TALK,
|
||||
MoveId.STRUGGLE,
|
||||
MoveId.SKETCH,
|
||||
MoveId.REVIVAL_BLESSING,
|
||||
MoveId.TERA_STARSTORM,
|
||||
MoveId.BREAKNECK_BLITZ__PHYSICAL,
|
||||
MoveId.BREAKNECK_BLITZ__SPECIAL
|
||||
];
|
||||
|
||||
if (unsketchableMoves.includes(targetMove.move)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !user.getMoveset().some(m => m.moveId === targetMove.move);
|
||||
const targetMove = target.getLastNonVirtualMove();
|
||||
return !isNullOrUndefined(targetMove)
|
||||
&& !invalidSketchMoves.has(targetMove.move)
|
||||
&& user.getMoveset().every(m => m.moveId !== targetMove.move)
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -7836,19 +7845,19 @@ export class LastResortAttr extends MoveAttr {
|
||||
// TODO: Verify behavior as Bulbapedia page is _extremely_ poorly documented
|
||||
getCondition(): MoveConditionFunc {
|
||||
return (user: Pokemon, _target: Pokemon, move: Move) => {
|
||||
const movesInMoveset = new Set<MoveId>(user.getMoveset().map(m => m.moveId));
|
||||
if (!movesInMoveset.delete(move.id) || !movesInMoveset.size) {
|
||||
const otherMovesInMoveset = new Set<MoveId>(user.getMoveset().map(m => m.moveId));
|
||||
if (!otherMovesInMoveset.delete(move.id) || !otherMovesInMoveset.size) {
|
||||
return false; // Last resort fails if used when not in user's moveset or no other moves exist
|
||||
}
|
||||
|
||||
const movesInHistory = new Set(
|
||||
const movesInHistory = new Set<MoveId>(
|
||||
user.getMoveHistory()
|
||||
.filter(m => !m.virtual) // TODO: Change to (m) => m < MoveUseType.INDIRECT after Dancer PR refactors virtual into enum
|
||||
.filter(m => !isVirtual(m.useMode)) // Last resort ignores virtual moves
|
||||
.map(m => m.move)
|
||||
);
|
||||
|
||||
// Since `Set.intersection()` is only present in ESNext, we have to coerce it to an array to check inclusion
|
||||
return [...movesInMoveset].every(m => movesInHistory.has(m))
|
||||
// Since `Set.intersection()` is only present in ESNext, we have to do this to check inclusion
|
||||
return [...otherMovesInMoveset].every(m => movesInHistory.has(m))
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -7870,27 +7879,26 @@ export class VariableTargetAttr extends MoveAttr {
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute for {@linkcode MoveId.AFTER_YOU}
|
||||
* Attribute to cause the target to move immediately after the user.
|
||||
*
|
||||
* [After You - Move | Bulbapedia](https://bulbapedia.bulbagarden.net/wiki/After_You_(move))
|
||||
* Used by {@linkcode Moves.AFTER_YOU}.
|
||||
*/
|
||||
export class AfterYouAttr extends MoveEffectAttr {
|
||||
/**
|
||||
* Allows the target of this move to act right after the user.
|
||||
*
|
||||
* @param user {@linkcode Pokemon} that is using the move.
|
||||
* @param target {@linkcode Pokemon} that will move right after this move is used.
|
||||
* @param move {@linkcode Move} {@linkcode MoveId.AFTER_YOU}
|
||||
* @param _args N/A
|
||||
* @returns true
|
||||
* Cause the target of this move to act right after the user.
|
||||
* @param user - Unused
|
||||
* @param target - The {@linkcode Pokemon} targeted by this move
|
||||
* @param _move - Unused
|
||||
* @param _args - Unused
|
||||
* @returns `true`
|
||||
*/
|
||||
override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean {
|
||||
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:afterYou", { targetName: getPokemonNameWithAffix(target) }));
|
||||
|
||||
//Will find next acting phase of the targeted pokémon, delete it and queue it next on successful delete.
|
||||
const nextAttackPhase = globalScene.phaseManager.findPhase<MovePhase>((phase) => phase.pokemon === target);
|
||||
if (nextAttackPhase && globalScene.phaseManager.tryRemovePhase((phase: MovePhase) => phase.pokemon === target)) {
|
||||
globalScene.phaseManager.prependNewToPhase("MovePhase", "MovePhase", target, [ ...nextAttackPhase.targets ], nextAttackPhase.move);
|
||||
// Will find next acting phase of the targeted pokémon, delete it and queue it right after us.
|
||||
const targetNextPhase = globalScene.phaseManager.findPhase<MovePhase>(phase => phase.pokemon === target);
|
||||
if (targetNextPhase && globalScene.phaseManager.tryRemovePhase((phase: MovePhase) => phase.pokemon === target)) {
|
||||
globalScene.phaseManager.prependToPhase(targetNextPhase, "MovePhase");
|
||||
}
|
||||
|
||||
return true;
|
||||
@ -7915,6 +7923,7 @@ export class ForceLastAttr extends MoveEffectAttr {
|
||||
override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean {
|
||||
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:forceLast", { targetPokemonName: getPokemonNameWithAffix(target) }));
|
||||
|
||||
// TODO: Refactor this to be more readable and less janky
|
||||
const targetMovePhase = globalScene.phaseManager.findPhase<MovePhase>((phase) => phase.pokemon === target);
|
||||
if (targetMovePhase && !targetMovePhase.isForcedLast() && globalScene.phaseManager.tryRemovePhase((phase: MovePhase) => phase.pokemon === target)) {
|
||||
// Finding the phase to insert the move in front of -
|
||||
@ -7927,7 +7936,7 @@ export class ForceLastAttr extends MoveEffectAttr {
|
||||
globalScene.phaseManager.phaseQueue.splice(
|
||||
globalScene.phaseManager.phaseQueue.indexOf(prependPhase),
|
||||
0,
|
||||
globalScene.phaseManager.create("MovePhase", target, [ ...targetMovePhase.targets ], targetMovePhase.move, false, false, false, true)
|
||||
globalScene.phaseManager.create("MovePhase", target, [ ...targetMovePhase.targets ], targetMovePhase.move, targetMovePhase.useMode, true)
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -7935,7 +7944,13 @@ export class ForceLastAttr extends MoveEffectAttr {
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns whether a {@linkcode MovePhase} has been forced last and the corresponding pokemon is slower than {@linkcode target} */
|
||||
/**
|
||||
* Returns whether a {@linkcode MovePhase} has been forced last and the corresponding pokemon is slower than {@linkcode target}.
|
||||
|
||||
* TODO:
|
||||
- Make this a class method
|
||||
- Make this look at speed order from TurnStartPhase
|
||||
*/
|
||||
const phaseForcedSlower = (phase: MovePhase, target: Pokemon, trickRoom: boolean): boolean => {
|
||||
let slower: boolean;
|
||||
// quashed pokemon still have speed ties
|
||||
@ -8030,8 +8045,7 @@ export class UpperHandCondition extends MoveCondition {
|
||||
super((user, target, move) => {
|
||||
const targetCommand = globalScene.currentBattle.turnCommands[target.getBattlerIndex()];
|
||||
|
||||
return !!targetCommand
|
||||
&& targetCommand.command === Command.FIGHT
|
||||
return targetCommand?.command === Command.FIGHT
|
||||
&& !target.turnData.acted
|
||||
&& !!targetCommand.move?.move
|
||||
&& allMoves[targetCommand.move.move].category !== MoveCategory.STATUS
|
||||
@ -8064,6 +8078,7 @@ export class ResistLastMoveTypeAttr extends MoveEffectAttr {
|
||||
constructor() {
|
||||
super(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* User changes its type to a random type that resists the target's last used move
|
||||
* @param {Pokemon} user Pokemon that used the move and will change types
|
||||
@ -8077,7 +8092,8 @@ export class ResistLastMoveTypeAttr extends MoveEffectAttr {
|
||||
return false;
|
||||
}
|
||||
|
||||
const [ targetMove ] = target.getLastXMoves(1); // target's most recent move
|
||||
// TODO: Confirm how this interacts with status-induced failures and called moves
|
||||
const targetMove = target.getLastXMoves(1)[0]; // target's most recent move
|
||||
if (!targetMove) {
|
||||
return false;
|
||||
}
|
||||
@ -8119,9 +8135,9 @@ export class ResistLastMoveTypeAttr extends MoveEffectAttr {
|
||||
}
|
||||
|
||||
getCondition(): MoveConditionFunc {
|
||||
// TODO: Does this count dancer?
|
||||
return (user, target, move) => {
|
||||
const moveHistory = target.getLastXMoves();
|
||||
return moveHistory.length !== 0;
|
||||
return target.getLastXMoves(-1).some(tm => tm.move !== MoveId.NONE);
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -8539,9 +8555,9 @@ export function initMoves() {
|
||||
.attr(FixedDamageAttr, 20),
|
||||
new StatusMove(MoveId.DISABLE, PokemonType.NORMAL, 100, 20, -1, 0, 1)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.DISABLED, false, true)
|
||||
.condition((user, target, move) => {
|
||||
const lastRealMove = target.getLastXMoves(-1).find(m => !m.virtual);
|
||||
return !isNullOrUndefined(lastRealMove) && lastRealMove.move !== MoveId.NONE && lastRealMove.move !== MoveId.STRUGGLE;
|
||||
.condition((_user, target, _move) => {
|
||||
const lastNonVirtualMove = target.getLastNonVirtualMove();
|
||||
return !isNullOrUndefined(lastNonVirtualMove) && lastNonVirtualMove.move !== MoveId.STRUGGLE;
|
||||
})
|
||||
.ignoresSubstitute()
|
||||
.reflectable(),
|
||||
@ -9090,7 +9106,10 @@ export function initMoves() {
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.ENCORE, false, true)
|
||||
.ignoresSubstitute()
|
||||
.condition((user, target, move) => new EncoreTag(user.id).canAdd(target))
|
||||
.reflectable(),
|
||||
.reflectable()
|
||||
// Can lock infinitely into struggle; has incorrect interactions with Blood Moon/Gigaton Hammer
|
||||
// Also may or may not incorrectly select targets for replacement move (needs verification)
|
||||
.edgeCase(),
|
||||
new AttackMove(MoveId.PURSUIT, PokemonType.DARK, MoveCategory.PHYSICAL, 40, 100, 20, -1, 0, 2)
|
||||
.partial(), // No effect implemented
|
||||
new AttackMove(MoveId.RAPID_SPIN, PokemonType.NORMAL, MoveCategory.PHYSICAL, 50, 100, 40, 100, 0, 2)
|
||||
@ -9972,7 +9991,14 @@ export function initMoves() {
|
||||
.chargeAttr(SemiInvulnerableAttr, BattlerTagType.FLYING)
|
||||
.condition(failOnGravityCondition)
|
||||
.condition((user, target, move) => !target.getTag(BattlerTagType.SUBSTITUTE))
|
||||
.partial(), // Should immobilize the target, Flying types should take no damage. cf https://bulbapedia.bulbagarden.net/wiki/Sky_Drop_(move) and https://www.smogon.com/dex/sv/moves/sky-drop/
|
||||
/*
|
||||
* Cf https://bulbapedia.bulbagarden.net/wiki/Sky_Drop_(move) and https://www.smogon.com/dex/sv/moves/sky-drop/:
|
||||
* Should immobilize and give target semi-invulnerability
|
||||
* Flying types should take no damage
|
||||
* Should fail on targets above a certain weight threshold
|
||||
* Should remove all redirection effects on successful takeoff (Rage Poweder, etc.)
|
||||
*/
|
||||
.partial(),
|
||||
new SelfStatusMove(MoveId.SHIFT_GEAR, PokemonType.STEEL, -1, 10, -1, 0, 5)
|
||||
.attr(StatStageChangeAttr, [ Stat.ATK ], 1, true)
|
||||
.attr(StatStageChangeAttr, [ Stat.SPD ], 2, true),
|
||||
@ -10543,9 +10569,12 @@ export function initMoves() {
|
||||
new StatusMove(MoveId.INSTRUCT, PokemonType.PSYCHIC, -1, 15, -1, 0, 7)
|
||||
.ignoresSubstitute()
|
||||
.attr(RepeatMoveAttr)
|
||||
// incorrect interactions with Gigaton Hammer, Blood Moon & Torment
|
||||
// Also has incorrect interactions with Dancer due to the latter
|
||||
// erroneously adding copied moves to move history.
|
||||
/*
|
||||
* Incorrect interactions with Gigaton Hammer, Blood Moon & Torment due to them _failing on use_, not merely being unselectable.
|
||||
* Incorrectly ticks down Encore's fail counter
|
||||
* TODO: Verify whether Instruct can repeat Struggle
|
||||
* TODO: Verify whether Instruct can fail when using a copied move also in one's own moveset
|
||||
*/
|
||||
.edgeCase(),
|
||||
new AttackMove(MoveId.BEAK_BLAST, PokemonType.FLYING, MoveCategory.PHYSICAL, 100, 100, 15, -1, -3, 7)
|
||||
.attr(BeakBlastHeaderAttr)
|
||||
@ -10603,7 +10632,13 @@ export function initMoves() {
|
||||
.bitingMove()
|
||||
.attr(RemoveScreensAttr),
|
||||
new AttackMove(MoveId.STOMPING_TANTRUM, PokemonType.GROUND, MoveCategory.PHYSICAL, 75, 100, 10, -1, 0, 7)
|
||||
.attr(MovePowerMultiplierAttr, (user, target, move) => user.getLastXMoves(2)[1]?.result === MoveResult.MISS || user.getLastXMoves(2)[1]?.result === MoveResult.FAIL ? 2 : 1),
|
||||
.attr(MovePowerMultiplierAttr, (user) => {
|
||||
// Stomping tantrum triggers on most failures (including sleep/freeze)
|
||||
const lastNonDancerMove = user.getLastXMoves(2)[1] as TurnMove | undefined;
|
||||
return lastNonDancerMove && (lastNonDancerMove.result === MoveResult.MISS || lastNonDancerMove.result === MoveResult.FAIL) ? 2 : 1
|
||||
})
|
||||
// TODO: Review mainline accuracy and draft tests as needed
|
||||
.edgeCase(),
|
||||
new AttackMove(MoveId.SHADOW_BONE, PokemonType.GHOST, MoveCategory.PHYSICAL, 85, 100, 10, 20, 0, 7)
|
||||
.attr(StatStageChangeAttr, [ Stat.DEF ], -1)
|
||||
.makesContact(false),
|
||||
|
@ -21,7 +21,6 @@ export class PokemonMove {
|
||||
public moveId: MoveId;
|
||||
public ppUsed: number;
|
||||
public ppUp: number;
|
||||
public virtual: boolean;
|
||||
|
||||
/**
|
||||
* If defined and nonzero, overrides the maximum PP of the move (e.g., due to move being copied by Transform).
|
||||
@ -29,11 +28,10 @@ export class PokemonMove {
|
||||
*/
|
||||
public maxPpOverride?: number;
|
||||
|
||||
constructor(moveId: MoveId, ppUsed = 0, ppUp = 0, virtual = false, maxPpOverride?: number) {
|
||||
constructor(moveId: MoveId, ppUsed = 0, ppUp = 0, maxPpOverride?: number) {
|
||||
this.moveId = moveId;
|
||||
this.ppUsed = ppUsed;
|
||||
this.ppUp = ppUp;
|
||||
this.virtual = virtual;
|
||||
this.maxPpOverride = maxPpOverride;
|
||||
}
|
||||
|
||||
@ -47,6 +45,7 @@ export class PokemonMove {
|
||||
* @returns `true` if the move can be selected and used by the Pokemon, otherwise `false`.
|
||||
*/
|
||||
isUsable(pokemon: Pokemon, ignorePp = false, ignoreRestrictionTags = false): boolean {
|
||||
// TODO: Add Sky Drop's 1 turn stall
|
||||
if (this.moveId && !ignoreRestrictionTags && pokemon.isMoveRestricted(this.moveId, pokemon)) {
|
||||
return false;
|
||||
}
|
||||
@ -88,6 +87,6 @@ export class PokemonMove {
|
||||
* @returns A valid {@linkcode PokemonMove} object
|
||||
*/
|
||||
static loadMove(source: PokemonMove | any): PokemonMove {
|
||||
return new PokemonMove(source.moveId, source.ppUsed, source.ppUp, source.virtual, source.maxPpOverride);
|
||||
return new PokemonMove(source.moveId, source.ppUsed, source.ppUp, source.maxPpOverride);
|
||||
}
|
||||
}
|
||||
|
@ -38,6 +38,7 @@ import type HeldModifierConfig from "#app/@types/held-modifier-config";
|
||||
import type { BerryType } from "#enums/berry-type";
|
||||
import { Stat } from "#enums/stat";
|
||||
import i18next from "i18next";
|
||||
import { MoveUseMode } from "#enums/move-use-mode";
|
||||
|
||||
/** the i18n namespace for this encounter */
|
||||
const namespace = "mysteryEncounters/absoluteAvarice";
|
||||
@ -307,7 +308,7 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = MysteryEncounterBuilde
|
||||
sourceBattlerIndex: BattlerIndex.ENEMY,
|
||||
targets: [BattlerIndex.ENEMY],
|
||||
move: new PokemonMove(MoveId.STUFF_CHEEKS),
|
||||
ignorePp: true,
|
||||
useMode: MoveUseMode.IGNORE_PP,
|
||||
});
|
||||
|
||||
await transitionMysteryEncounterIntroVisuals(true, true, 500);
|
||||
|
@ -13,7 +13,6 @@ import { TrainerPartyCompoundTemplate } from "#app/data/trainers/TrainerPartyTem
|
||||
import { TrainerPartyTemplate } from "#app/data/trainers/TrainerPartyTemplate";
|
||||
import { ModifierTier } from "#enums/modifier-tier";
|
||||
import type { PokemonHeldItemModifierType } from "#app/modifier/modifier-type";
|
||||
import { modifierTypes } from "#app/data/data-lists";
|
||||
import { ModifierPoolType } from "#enums/modifier-pool-type";
|
||||
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
|
||||
import { PartyMemberStrength } from "#enums/party-member-strength";
|
||||
@ -49,7 +48,8 @@ import { CustomPokemonData } from "#app/data/custom-pokemon-data";
|
||||
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
|
||||
import { EncounterAnim } from "#enums/encounter-anims";
|
||||
import { Challenges } from "#enums/challenges";
|
||||
import { allAbilities } from "#app/data/data-lists";
|
||||
import { MoveUseMode } from "#enums/move-use-mode";
|
||||
import { allAbilities, modifierTypes } from "#app/data/data-lists";
|
||||
|
||||
/** the i18n namespace for the encounter */
|
||||
const namespace = "mysteryEncounters/clowningAround";
|
||||
@ -210,19 +210,19 @@ export const ClowningAroundEncounter: MysteryEncounter = MysteryEncounterBuilder
|
||||
sourceBattlerIndex: BattlerIndex.ENEMY,
|
||||
targets: [BattlerIndex.ENEMY_2],
|
||||
move: new PokemonMove(MoveId.ROLE_PLAY),
|
||||
ignorePp: true,
|
||||
useMode: MoveUseMode.IGNORE_PP,
|
||||
},
|
||||
{
|
||||
sourceBattlerIndex: BattlerIndex.ENEMY_2,
|
||||
targets: [BattlerIndex.PLAYER],
|
||||
move: new PokemonMove(MoveId.TAUNT),
|
||||
ignorePp: true,
|
||||
useMode: MoveUseMode.IGNORE_PP,
|
||||
},
|
||||
{
|
||||
sourceBattlerIndex: BattlerIndex.ENEMY_2,
|
||||
targets: [BattlerIndex.PLAYER_2],
|
||||
move: new PokemonMove(MoveId.TAUNT),
|
||||
ignorePp: true,
|
||||
useMode: MoveUseMode.IGNORE_PP,
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -40,6 +40,7 @@ import { PokeballType } from "#enums/pokeball";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { Stat } from "#enums/stat";
|
||||
import i18next from "i18next";
|
||||
import { MoveUseMode } from "#enums/move-use-mode";
|
||||
|
||||
/** the i18n namespace for this encounter */
|
||||
const namespace = "mysteryEncounters/dancingLessons";
|
||||
@ -214,7 +215,7 @@ export const DancingLessonsEncounter: MysteryEncounter = MysteryEncounterBuilder
|
||||
sourceBattlerIndex: BattlerIndex.ENEMY,
|
||||
targets: [BattlerIndex.PLAYER],
|
||||
move: new PokemonMove(MoveId.REVELATION_DANCE),
|
||||
ignorePp: true,
|
||||
useMode: MoveUseMode.IGNORE_PP,
|
||||
});
|
||||
|
||||
await hideOricorioPokemon();
|
||||
|
@ -10,7 +10,6 @@ import {
|
||||
generateModifierType,
|
||||
} from "#app/data/mystery-encounters/utils/encounter-phase-utils";
|
||||
import type { AttackTypeBoosterModifierType } from "#app/modifier/modifier-type";
|
||||
import { modifierTypes } from "#app/data/data-lists";
|
||||
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import type MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter";
|
||||
@ -46,7 +45,8 @@ import { AbilityId } from "#enums/ability-id";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { Stat } from "#enums/stat";
|
||||
import { FIRE_RESISTANT_ABILITIES } from "#app/data/mystery-encounters/requirements/requirement-groups";
|
||||
import { allAbilities } from "#app/data/data-lists";
|
||||
import { MoveUseMode } from "#enums/move-use-mode";
|
||||
import { allAbilities, modifierTypes } from "#app/data/data-lists";
|
||||
|
||||
/** the i18n namespace for the encounter */
|
||||
const namespace = "mysteryEncounters/fieryFallout";
|
||||
@ -201,13 +201,13 @@ export const FieryFalloutEncounter: MysteryEncounter = MysteryEncounterBuilder.w
|
||||
sourceBattlerIndex: BattlerIndex.ENEMY,
|
||||
targets: [BattlerIndex.PLAYER],
|
||||
move: new PokemonMove(MoveId.FIRE_SPIN),
|
||||
ignorePp: true,
|
||||
useMode: MoveUseMode.IGNORE_PP,
|
||||
},
|
||||
{
|
||||
sourceBattlerIndex: BattlerIndex.ENEMY_2,
|
||||
targets: [BattlerIndex.PLAYER_2],
|
||||
move: new PokemonMove(MoveId.FIRE_SPIN),
|
||||
ignorePp: true,
|
||||
useMode: MoveUseMode.IGNORE_PP,
|
||||
},
|
||||
);
|
||||
await initBattleWithEnemyConfig(globalScene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0]);
|
||||
|
@ -11,10 +11,7 @@ import { STEALING_MOVES } from "#app/data/mystery-encounters/requirements/requir
|
||||
import type Pokemon from "#app/field/pokemon";
|
||||
import { ModifierTier } from "#enums/modifier-tier";
|
||||
import type { ModifierTypeOption } from "#app/modifier/modifier-type";
|
||||
import {
|
||||
getPlayerModifierTypeOptions,
|
||||
regenerateModifierPoolThresholds,
|
||||
} from "#app/modifier/modifier-type";
|
||||
import { getPlayerModifierTypeOptions, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type";
|
||||
import { ModifierPoolType } from "#enums/modifier-pool-type";
|
||||
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
|
@ -7,10 +7,7 @@ import { TrainerSlot } from "#enums/trainer-slot";
|
||||
import { ModifierTier } from "#enums/modifier-tier";
|
||||
import { MusicPreference } from "#app/system/settings/settings";
|
||||
import type { ModifierTypeOption } from "#app/modifier/modifier-type";
|
||||
import {
|
||||
getPlayerModifierTypeOptions,
|
||||
regenerateModifierPoolThresholds,
|
||||
} from "#app/modifier/modifier-type";
|
||||
import { getPlayerModifierTypeOptions, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type";
|
||||
import { ModifierPoolType } from "#enums/modifier-pool-type";
|
||||
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
|
@ -31,6 +31,7 @@ import { BerryType } from "#enums/berry-type";
|
||||
import { Stat } from "#enums/stat";
|
||||
import { CustomPokemonData } from "#app/data/custom-pokemon-data";
|
||||
import { randSeedInt } from "#app/utils/common";
|
||||
import { MoveUseMode } from "#enums/move-use-mode";
|
||||
|
||||
/** i18n namespace for the encounter */
|
||||
const namespace = "mysteryEncounters/slumberingSnorlax";
|
||||
@ -137,7 +138,7 @@ export const SlumberingSnorlaxEncounter: MysteryEncounter = MysteryEncounterBuil
|
||||
sourceBattlerIndex: BattlerIndex.ENEMY,
|
||||
targets: [BattlerIndex.PLAYER],
|
||||
move: new PokemonMove(MoveId.SNORE),
|
||||
ignorePp: true,
|
||||
useMode: MoveUseMode.IGNORE_PP,
|
||||
});
|
||||
await initBattleWithEnemyConfig(encounter.enemyPartyConfigs[0]);
|
||||
},
|
||||
|
@ -28,6 +28,7 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
|
||||
import { CustomPokemonData } from "#app/data/custom-pokemon-data";
|
||||
import { Stat } from "#enums/stat";
|
||||
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
|
||||
import { MoveUseMode } from "#enums/move-use-mode";
|
||||
|
||||
/** the i18n namespace for the encounter */
|
||||
const namespace = "mysteryEncounters/theStrongStuff";
|
||||
@ -214,13 +215,13 @@ export const TheStrongStuffEncounter: MysteryEncounter = MysteryEncounterBuilder
|
||||
sourceBattlerIndex: BattlerIndex.ENEMY,
|
||||
targets: [BattlerIndex.PLAYER],
|
||||
move: new PokemonMove(MoveId.GASTRO_ACID),
|
||||
ignorePp: true,
|
||||
useMode: MoveUseMode.IGNORE_PP,
|
||||
},
|
||||
{
|
||||
sourceBattlerIndex: BattlerIndex.ENEMY,
|
||||
targets: [BattlerIndex.PLAYER],
|
||||
move: new PokemonMove(MoveId.STEALTH_ROCK),
|
||||
ignorePp: true,
|
||||
useMode: MoveUseMode.IGNORE_PP,
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -28,6 +28,7 @@ import { BattlerIndex } from "#enums/battler-index";
|
||||
import { PokemonMove } from "#app/data/moves/pokemon-move";
|
||||
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
|
||||
import { randSeedInt } from "#app/utils/common";
|
||||
import { MoveUseMode } from "#enums/move-use-mode";
|
||||
|
||||
/** the i18n namespace for this encounter */
|
||||
const namespace = "mysteryEncounters/trashToTreasure";
|
||||
@ -207,13 +208,13 @@ export const TrashToTreasureEncounter: MysteryEncounter = MysteryEncounterBuilde
|
||||
sourceBattlerIndex: BattlerIndex.ENEMY,
|
||||
targets: [BattlerIndex.PLAYER],
|
||||
move: new PokemonMove(MoveId.TOXIC),
|
||||
ignorePp: true,
|
||||
useMode: MoveUseMode.IGNORE_PP,
|
||||
},
|
||||
{
|
||||
sourceBattlerIndex: BattlerIndex.ENEMY,
|
||||
targets: [BattlerIndex.ENEMY],
|
||||
move: new PokemonMove(MoveId.STOCKPILE),
|
||||
ignorePp: true,
|
||||
useMode: MoveUseMode.IGNORE_PP,
|
||||
},
|
||||
);
|
||||
await initBattleWithEnemyConfig(encounter.enemyPartyConfigs[0]);
|
||||
|
@ -36,6 +36,7 @@ import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encoun
|
||||
import { BerryModifier } from "#app/modifier/modifier";
|
||||
import { Stat } from "#enums/stat";
|
||||
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
|
||||
import { MoveUseMode } from "#enums/move-use-mode";
|
||||
|
||||
/** the i18n namespace for the encounter */
|
||||
const namespace = "mysteryEncounters/uncommonBreed";
|
||||
@ -180,7 +181,7 @@ export const UncommonBreedEncounter: MysteryEncounter = MysteryEncounterBuilder.
|
||||
sourceBattlerIndex: BattlerIndex.ENEMY,
|
||||
targets: [target],
|
||||
move: pokemonMove,
|
||||
ignorePp: true,
|
||||
useMode: MoveUseMode.IGNORE_PP,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -29,14 +29,14 @@ import type { GameModes } from "#enums/game-modes";
|
||||
import type { EncounterAnim } from "#enums/encounter-anims";
|
||||
import type { Challenges } from "#enums/challenges";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import type { MoveUseMode } from "#enums/move-use-mode";
|
||||
|
||||
export interface EncounterStartOfBattleEffect {
|
||||
sourcePokemon?: Pokemon;
|
||||
sourceBattlerIndex?: BattlerIndex;
|
||||
targets: BattlerIndex[];
|
||||
move: PokemonMove;
|
||||
ignorePp: boolean;
|
||||
followUp?: boolean;
|
||||
useMode: MoveUseMode; // TODO: This should always be ignore PP...
|
||||
}
|
||||
|
||||
const DEFAULT_MAX_ALLOWED_ENCOUNTERS = 2;
|
||||
@ -254,7 +254,7 @@ export default class MysteryEncounter implements IMysteryEncounter {
|
||||
*/
|
||||
selectedOption?: MysteryEncounterOption;
|
||||
/**
|
||||
* Will be set by option select handlers automatically, and can be used to refer to which option was chosen by later phases
|
||||
* Array containing data pertaining to free moves used at the start of a battle mystery envounter.
|
||||
*/
|
||||
startOfBattleEffects: EncounterStartOfBattleEffect[] = [];
|
||||
/**
|
||||
|
@ -1,5 +1,4 @@
|
||||
import type Battle from "#app/battle";
|
||||
import { BattlerIndex } from "#enums/battler-index";
|
||||
import { BattleType } from "#enums/battle-type";
|
||||
import { biomeLinks, BiomePoolTier } from "#app/data/balance/biomes";
|
||||
import type MysteryEncounterOption from "#app/data/mystery-encounters/mystery-encounter-option";
|
||||
@ -974,33 +973,8 @@ export function handleMysteryEncounterBattleStartEffects() {
|
||||
) {
|
||||
const effects = encounter.startOfBattleEffects;
|
||||
effects.forEach(effect => {
|
||||
let source: EnemyPokemon | Pokemon;
|
||||
if (effect.sourcePokemon) {
|
||||
source = effect.sourcePokemon;
|
||||
} else if (!isNullOrUndefined(effect.sourceBattlerIndex)) {
|
||||
if (effect.sourceBattlerIndex === BattlerIndex.ATTACKER) {
|
||||
source = globalScene.getEnemyField()[0];
|
||||
} else if (effect.sourceBattlerIndex === BattlerIndex.ENEMY) {
|
||||
source = globalScene.getEnemyField()[0];
|
||||
} else if (effect.sourceBattlerIndex === BattlerIndex.ENEMY_2) {
|
||||
source = globalScene.getEnemyField()[1];
|
||||
} else if (effect.sourceBattlerIndex === BattlerIndex.PLAYER) {
|
||||
source = globalScene.getPlayerField()[0];
|
||||
} else if (effect.sourceBattlerIndex === BattlerIndex.PLAYER_2) {
|
||||
source = globalScene.getPlayerField()[1];
|
||||
}
|
||||
} else {
|
||||
source = globalScene.getEnemyField()[0];
|
||||
}
|
||||
globalScene.phaseManager.pushNew(
|
||||
"MovePhase",
|
||||
// @ts-expect-error: source is guaranteed to be defined
|
||||
source,
|
||||
effect.targets,
|
||||
effect.move,
|
||||
effect.followUp,
|
||||
effect.ignorePp,
|
||||
);
|
||||
const source = effect.sourcePokemon ?? globalScene.getField()[effect.sourceBattlerIndex ?? 0];
|
||||
globalScene.phaseManager.pushNew("MovePhase", source, effect.targets, effect.move, effect.useMode);
|
||||
});
|
||||
|
||||
// Pseudo turn end phase to reset flinch states, Endure, etc.
|
||||
|
@ -84,13 +84,14 @@ export const normalForm: SpeciesId[] = [
|
||||
|
||||
/**
|
||||
* Gets the {@linkcode PokemonSpecies} object associated with the {@linkcode SpeciesId} enum given
|
||||
* @param species The species to fetch
|
||||
* @param species - The {@linkcode SpeciesId} to fetch.
|
||||
* If an array of `SpeciesId`s is passed (such as for named trainer spawn pools),
|
||||
* one will be selected at random.
|
||||
* @returns The associated {@linkcode PokemonSpecies} object
|
||||
*/
|
||||
export function getPokemonSpecies(species: SpeciesId | SpeciesId[]): PokemonSpecies {
|
||||
// If a special pool (named trainers) is used here it CAN happen that they have a array as species (which means choose one of those two). So we catch that with this code block
|
||||
if (Array.isArray(species)) {
|
||||
// Pick a random species from the list
|
||||
// TODO: this RNG roll should not be handled by this function
|
||||
species = species[Math.floor(Math.random() * species.length)];
|
||||
}
|
||||
if (species >= 2000) {
|
||||
|
@ -1,12 +1,37 @@
|
||||
/**
|
||||
* Enum representing the possible ways a given BattlerTag can activate and/or tick down.
|
||||
* Each tag can have multiple different behaviors attached to different lapse types.
|
||||
*/
|
||||
export enum BattlerTagLapseType {
|
||||
// TODO: This is unused...
|
||||
FAINT,
|
||||
/**
|
||||
* Tag activate before the holder uses a non-virtual move, possibly interrupting its action.
|
||||
* @see MoveUseMode for more information
|
||||
*/
|
||||
MOVE,
|
||||
/** Tag activates before the holder uses **any** move, triggering effects or interrupting its action. */
|
||||
PRE_MOVE,
|
||||
/** Tag activates immediately after the holder's move finishes triggering (successful or not). */
|
||||
AFTER_MOVE,
|
||||
/**
|
||||
* Tag activates before move effects are applied.
|
||||
* TODO: Stop using this as a catch-all "semi-invulnerability" tag
|
||||
*/
|
||||
MOVE_EFFECT,
|
||||
/** Tag activates at the end of the turn. */
|
||||
TURN_END,
|
||||
/**
|
||||
* Tag activates after the holder is hit by an attack, but before damage is applied.
|
||||
* Occurs even if the user's {@linkcode SubstituteTag | Substitute} is hit.
|
||||
*/
|
||||
HIT,
|
||||
/** Tag lapses AFTER_HIT, applying its effects even if the user faints */
|
||||
/**
|
||||
* Tag activates after the holder is directly hit by an attack.
|
||||
* Does **not** occur on hits to the holder's {@linkcode SubstituteTag | Substitute},
|
||||
* but still triggers on being KO'd.
|
||||
*/
|
||||
AFTER_HIT,
|
||||
CUSTOM
|
||||
/** The tag has some other custom activation or removal condition. */
|
||||
CUSTOM,
|
||||
}
|
||||
|
@ -1,13 +0,0 @@
|
||||
// biome-ignore lint/correctness/noUnusedImports: Used in tsdoc
|
||||
import type ConfirmUiHandler from "#app/ui/confirm-ui-handler";
|
||||
|
||||
/**
|
||||
* Used by {@linkcode ConfirmUiHandler} to determine whether the cursor should start on Yes or No
|
||||
*/
|
||||
export const ConfirmUiMode = Object.freeze({
|
||||
/** Start cursor on Yes */
|
||||
DEFAULT_YES: 1,
|
||||
/** Start cursor on No */
|
||||
DEFAULT_NO: 2
|
||||
});
|
||||
export type ConfirmUiMode = typeof ConfirmUiMode[keyof typeof ConfirmUiMode];
|
149
src/enums/move-use-mode.ts
Normal file
149
src/enums/move-use-mode.ts
Normal file
@ -0,0 +1,149 @@
|
||||
import type { PostDancingMoveAbAttr } from "#app/data/abilities/ability";
|
||||
import type { BattlerTagLapseType } from "#enums/battler-tag-lapse-type";
|
||||
|
||||
/**
|
||||
* Enum representing all the possible means through which a given move can be executed.
|
||||
* Each one inherits the properties (or exclusions) of all types preceding it.
|
||||
* Properties newly found on a given use mode will be **bolded**,
|
||||
* while oddities breaking a previous trend will be listed in _italics_.
|
||||
|
||||
* Callers should refrain from performing non-equality checks on `MoveUseMode`s directly,
|
||||
* instead using the available helper functions
|
||||
* ({@linkcode isVirtual}, {@linkcode isIgnoreStatus}, {@linkcode isIgnorePP} and {@linkcode isReflected}).
|
||||
*/
|
||||
export const MoveUseMode = {
|
||||
/**
|
||||
* This move was used normally (i.e. clicking on the button) or called via Instruct.
|
||||
* It deducts PP from the user's moveset (failing if out of PP), and interacts normally with other moves and abilities.
|
||||
*/
|
||||
NORMAL: 1,
|
||||
|
||||
/**
|
||||
* This move was called by an effect that ignores PP, such as a consecutively executed move (e.g. Outrage).
|
||||
*
|
||||
* PP-ignoring moves (as their name implies) **do not consume PP** when used
|
||||
* and **will not fail** if none is left prior to execution.
|
||||
* All other effects remain identical to {@linkcode MoveUseMode.NORMAL}.
|
||||
*
|
||||
* PP can still be reduced by other effects (such as Spite or Eerie Spell).
|
||||
*/
|
||||
IGNORE_PP: 2,
|
||||
|
||||
/**
|
||||
* This move was called indirectly by an out-of-turn effect other than Instruct or the user's previous move.
|
||||
* Currently only used by {@linkcode PostDancingMoveAbAttr | Dancer}.
|
||||
*
|
||||
* Indirect moves ignore PP checks similar to {@linkcode MoveUseMode.IGNORE_PP}, but additionally **cannot be copied**
|
||||
* by all move-copying effects (barring reflection).
|
||||
* They are also **"skipped over" by most moveset and move history-related effects** (PP reduction, Last Resort, etc).
|
||||
*
|
||||
* They still respect the user's volatile status conditions and confusion (though will uniquely _cure freeze and sleep before use_).
|
||||
*/
|
||||
INDIRECT: 3,
|
||||
|
||||
/**
|
||||
* This move was called as part of another move's effect (such as for most {@link https://bulbapedia.bulbagarden.net/wiki/Category:Moves_that_call_other_moves | Move-calling moves}).
|
||||
|
||||
* Follow-up moves **bypass cancellation** from all **non-volatile status conditions** and **{@linkcode BattlerTagLapseType.MOVE}-type effects**
|
||||
* (having been checked already on the calling move).
|
||||
|
||||
* They are _not ignored_ by other move-calling moves and abilities (unlike {@linkcode MoveUseMode.FOLLOW_UP} and {@linkcode MoveUseMode.REFLECTED}),
|
||||
* but still inherit the former's disregard for moveset-related effects.
|
||||
*/
|
||||
FOLLOW_UP: 4,
|
||||
|
||||
/**
|
||||
* This move was reflected by Magic Coat or Magic Bounce.
|
||||
|
||||
* Reflected moves ignore all the same cancellation checks as {@linkcode MoveUseMode.INDIRECT}
|
||||
* and retain the same copy prevention as {@linkcode MoveUseMode.FOLLOW_UP}, but additionally
|
||||
* **cannot be reflected by other reflecting effects**.
|
||||
*/
|
||||
REFLECTED: 5
|
||||
// TODO: Add use type TRANSPARENT for Future Sight and Doom Desire to prevent move history pushing
|
||||
} as const;
|
||||
|
||||
export type MoveUseMode = (typeof MoveUseMode)[keyof typeof MoveUseMode];
|
||||
|
||||
// # HELPER FUNCTIONS
|
||||
// Please update the markdown tables if any new `MoveUseMode`s get added.
|
||||
|
||||
/**
|
||||
* Check if a given {@linkcode MoveUseMode} is virtual (i.e. called by another move or effect).
|
||||
* Virtual moves are ignored by most moveset-related effects due to not being executed directly.
|
||||
* @returns Whether {@linkcode useMode} is virtual.
|
||||
* @remarks
|
||||
* This function is equivalent to the following truth table:
|
||||
*
|
||||
* | Use Type | Returns |
|
||||
* |------------------------------------|---------|
|
||||
* | {@linkcode MoveUseMode.NORMAL} | `false` |
|
||||
* | {@linkcode MoveUseMode.IGNORE_PP} | `false` |
|
||||
* | {@linkcode MoveUseMode.INDIRECT} | `true` |
|
||||
* | {@linkcode MoveUseMode.FOLLOW_UP} | `true` |
|
||||
* | {@linkcode MoveUseMode.REFLECTED} | `true` |
|
||||
*/
|
||||
export function isVirtual(useMode: MoveUseMode): boolean {
|
||||
return useMode >= MoveUseMode.INDIRECT
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given {@linkcode MoveUseMode} should ignore pre-move cancellation checks
|
||||
* from {@linkcode StatusEffect.PARALYSIS} and {@linkcode BattlerTagLapseType.MOVE}-type effects.
|
||||
* @param useMode - The {@linkcode MoveUseMode} to check.
|
||||
* @returns Whether {@linkcode useMode} should ignore status and otehr cancellation checks.
|
||||
* @remarks
|
||||
* This function is equivalent to the following truth table:
|
||||
*
|
||||
* | Use Type | Returns |
|
||||
* |------------------------------------|---------|
|
||||
* | {@linkcode MoveUseMode.NORMAL} | `false` |
|
||||
* | {@linkcode MoveUseMode.IGNORE_PP} | `false` |
|
||||
* | {@linkcode MoveUseMode.INDIRECT} | `false` |
|
||||
* | {@linkcode MoveUseMode.FOLLOW_UP} | `true` |
|
||||
* | {@linkcode MoveUseMode.REFLECTED} | `true` |
|
||||
*/
|
||||
export function isIgnoreStatus(useMode: MoveUseMode): boolean {
|
||||
return useMode >= MoveUseMode.FOLLOW_UP;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given {@linkcode MoveUseMode} should ignore PP.
|
||||
* PP-ignoring moves will ignore normal PP consumption as well as associated failure checks.
|
||||
* @param useMode - The {@linkcode MoveUseMode} to check.
|
||||
* @returns Whether {@linkcode useMode} ignores PP.
|
||||
* @remarks
|
||||
* This function is equivalent to the following truth table:
|
||||
*
|
||||
* | Use Type | Returns |
|
||||
* |------------------------------------|---------|
|
||||
* | {@linkcode MoveUseMode.NORMAL} | `false` |
|
||||
* | {@linkcode MoveUseMode.IGNORE_PP} | `true` |
|
||||
* | {@linkcode MoveUseMode.INDIRECT} | `true` |
|
||||
* | {@linkcode MoveUseMode.FOLLOW_UP} | `true` |
|
||||
* | {@linkcode MoveUseMode.REFLECTED} | `true` |
|
||||
*/
|
||||
export function isIgnorePP(useMode: MoveUseMode): boolean {
|
||||
return useMode >= MoveUseMode.IGNORE_PP;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given {@linkcode MoveUseMode} is reflected.
|
||||
* Reflected moves cannot be reflected, copied, or cancelled by status effects,
|
||||
* nor will they trigger {@linkcode PostDancingMoveAbAttr | Dancer}.
|
||||
* @param useMode - The {@linkcode MoveUseMode} to check.
|
||||
* @returns Whether {@linkcode useMode} is reflected.
|
||||
* @remarks
|
||||
* This function is equivalent to the following truth table:
|
||||
*
|
||||
* | Use Type | Returns |
|
||||
* |------------------------------------|---------|
|
||||
* | {@linkcode MoveUseMode.NORMAL} | `false` |
|
||||
* | {@linkcode MoveUseMode.IGNORE_PP} | `false` |
|
||||
* | {@linkcode MoveUseMode.INDIRECT} | `false` |
|
||||
* | {@linkcode MoveUseMode.FOLLOW_UP} | `false` |
|
||||
* | {@linkcode MoveUseMode.REFLECTED} | `true` |
|
||||
*/
|
||||
export function isReflected(useMode: MoveUseMode): boolean {
|
||||
return useMode === MoveUseMode.REFLECTED;
|
||||
}
|
@ -262,7 +262,7 @@ export class Arena {
|
||||
return 5;
|
||||
}
|
||||
break;
|
||||
case SpeciesId.LYCANROC:
|
||||
case SpeciesId.LYCANROC: {
|
||||
const timeOfDay = this.getTimeOfDay();
|
||||
switch (timeOfDay) {
|
||||
case TimeOfDay.DAY:
|
||||
@ -274,6 +274,7 @@ export class Arena {
|
||||
return 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
@ -764,6 +765,9 @@ export class Arena {
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Add an overload similar to `Array.prototype.find` if the predicate func is of the form
|
||||
// `(x): x is T`
|
||||
|
||||
/**
|
||||
* Uses {@linkcode findTagsOnSide} to filter (using the parameter function) for specific tags that apply to both sides
|
||||
* @param tagPredicate a function mapping {@linkcode ArenaTag}s to `boolean`s
|
||||
|
@ -38,7 +38,6 @@ import {
|
||||
deltaRgb,
|
||||
isBetween,
|
||||
randSeedFloat,
|
||||
type nil,
|
||||
type Constructor,
|
||||
randSeedIntRange,
|
||||
coerceArray,
|
||||
@ -186,6 +185,7 @@ import { doShinySparkleAnim } from "#app/field/anims";
|
||||
import { MoveFlags } from "#enums/MoveFlags";
|
||||
import { timedEventManager } from "#app/global-event-manager";
|
||||
import { loadMoveAnimations } from "#app/sprites/pokemon-asset-loader";
|
||||
import { isVirtual, isIgnorePP, MoveUseMode } from "#enums/move-use-mode";
|
||||
import { FieldPosition } from "#enums/field-position";
|
||||
import { LearnMoveSituation } from "#enums/learn-move-situation";
|
||||
import { HitResult } from "#enums/hit-result";
|
||||
@ -324,7 +324,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
super(globalScene, x, y);
|
||||
|
||||
if (!species.isObtainable() && this.isPlayer()) {
|
||||
throw `Cannot create a player Pokemon for species '${species.getName(formIndex)}'`;
|
||||
throw `Cannot create a player Pokemon for species "${species.getName(formIndex)}"`;
|
||||
}
|
||||
|
||||
this.species = species;
|
||||
@ -3137,7 +3137,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
while (rand > stabMovePool[index][1]) {
|
||||
rand -= stabMovePool[index++][1];
|
||||
}
|
||||
this.moveset.push(new PokemonMove(stabMovePool[index][0], 0, 0));
|
||||
this.moveset.push(new PokemonMove(stabMovePool[index][0]));
|
||||
}
|
||||
|
||||
while (baseWeights.length > this.moveset.length && this.moveset.length < 4) {
|
||||
@ -3190,7 +3190,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
while (rand > movePool[index][1]) {
|
||||
rand -= movePool[index++][1];
|
||||
}
|
||||
this.moveset.push(new PokemonMove(movePool[index][0], 0, 0));
|
||||
this.moveset.push(new PokemonMove(movePool[index][0]));
|
||||
}
|
||||
|
||||
// Trigger FormChange, except for enemy Pokemon during Mystery Encounters, to avoid crashes
|
||||
@ -4109,7 +4109,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
}
|
||||
|
||||
/**@overload */
|
||||
getTag(tagType: BattlerTagType.GRUDGE): GrudgeTag | nil;
|
||||
getTag(tagType: BattlerTagType.GRUDGE): GrudgeTag | undefined;
|
||||
|
||||
/** @overload */
|
||||
getTag(tagType: BattlerTagType.SUBSTITUTE): SubstituteTag | undefined;
|
||||
@ -4326,10 +4326,41 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
return moveHistory.slice(0).reverse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the most recently executed {@linkcode TurnMove} this {@linkcode Pokemon} has used that is:
|
||||
* - Not {@linkcode MoveId.NONE}
|
||||
* - Non-virtual ({@linkcode MoveUseMode | useMode} < {@linkcode MoveUseMode.INDIRECT})
|
||||
* @param ignoreStruggle - Whether to additionally ignore {@linkcode Moves.STRUGGLE}; default `false`
|
||||
* @param ignoreFollowUp - Whether to ignore moves with a use type of {@linkcode MoveUseMode.FOLLOW_UP}
|
||||
* (e.g. ones called by Copycat/Mirror Move); default `true`.
|
||||
* @returns The last move this Pokemon has used satisfying the aforementioned conditions,
|
||||
* or `undefined` if no applicable moves have been used since switching in.
|
||||
*/
|
||||
getLastNonVirtualMove(ignoreStruggle = false, ignoreFollowUp = true): TurnMove | undefined {
|
||||
return this.getLastXMoves(-1).find(
|
||||
m =>
|
||||
m.move !== MoveId.NONE &&
|
||||
(!ignoreStruggle || m.move !== MoveId.STRUGGLE) &&
|
||||
(!isVirtual(m.useMode) || (!ignoreFollowUp && m.useMode === MoveUseMode.FOLLOW_UP)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return this Pokemon's move queue, consisting of all the moves it is slated to perform.
|
||||
* @returns An array of {@linkcode TurnMove}, as described above
|
||||
*/
|
||||
getMoveQueue(): TurnMove[] {
|
||||
return this.summonData.moveQueue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new entry to the end of this Pokemon's move queue.
|
||||
* @param queuedMove - A {@linkcode TurnMove} to push to this Pokemon's queue.
|
||||
*/
|
||||
pushMoveQueue(queuedMove: TurnMove): void {
|
||||
this.summonData.moveQueue.push(queuedMove);
|
||||
}
|
||||
|
||||
changeForm(formChange: SpeciesFormChange): Promise<void> {
|
||||
return new Promise(resolve => {
|
||||
this.formIndex = Math.max(
|
||||
@ -4385,14 +4416,16 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
// biome-ignore lint: there are a ton of issues..
|
||||
faintCry(callback: Function): void {
|
||||
if (this.fusionSpecies && this.getSpeciesForm() !== this.getFusionSpeciesForm()) {
|
||||
return this.fusionFaintCry(callback);
|
||||
this.fusionFaintCry(callback);
|
||||
return;
|
||||
}
|
||||
|
||||
const key = this.species.getCryKey(this.formIndex);
|
||||
let rate = 0.85;
|
||||
const cry = globalScene.playSound(key, { rate: rate }) as AnySound;
|
||||
if (!cry || globalScene.fieldVolume === 0) {
|
||||
return callback();
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
const sprite = this.getSprite();
|
||||
const tintSprite = this.getTintSprite();
|
||||
@ -4460,7 +4493,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
rate: rate,
|
||||
}) as AnySound;
|
||||
if (!cry || !fusionCry || globalScene.fieldVolume === 0) {
|
||||
return callback();
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
fusionCry.stop();
|
||||
duration = Math.min(duration, fusionCry.totalDuration * 1000);
|
||||
@ -5984,7 +6018,7 @@ export class PlayerPokemon extends Pokemon {
|
||||
copyMoveset(): PokemonMove[] {
|
||||
const newMoveset: PokemonMove[] = [];
|
||||
this.moveset.forEach(move => {
|
||||
newMoveset.push(new PokemonMove(move.moveId, 0, move.ppUp, move.virtual, move.maxPpOverride));
|
||||
newMoveset.push(new PokemonMove(move.moveId, 0, move.ppUp, move.maxPpOverride));
|
||||
});
|
||||
|
||||
return newMoveset;
|
||||
@ -6164,33 +6198,39 @@ export class EnemyPokemon extends Pokemon {
|
||||
* the Pokemon the move will target.
|
||||
* @returns this Pokemon's next move in the format {move, moveTargets}
|
||||
*/
|
||||
// TODO: split this up and move it elsewhere
|
||||
getNextMove(): TurnMove {
|
||||
// If this Pokemon has a move already queued, return it.
|
||||
// If this Pokemon has a usable move already queued, return it,
|
||||
// removing all unusable moves before it in the queue.
|
||||
const moveQueue = this.getMoveQueue();
|
||||
if (moveQueue.length !== 0) {
|
||||
const queuedMove = moveQueue[0];
|
||||
if (queuedMove) {
|
||||
const moveIndex = this.getMoveset().findIndex(m => m.moveId === queuedMove.move);
|
||||
if (
|
||||
(moveIndex > -1 && this.getMoveset()[moveIndex].isUsable(this, queuedMove.ignorePP)) ||
|
||||
queuedMove.virtual
|
||||
) {
|
||||
return queuedMove;
|
||||
}
|
||||
this.getMoveQueue().shift();
|
||||
return this.getNextMove();
|
||||
for (const [i, queuedMove] of moveQueue.entries()) {
|
||||
const movesetMove = this.getMoveset().find(m => m.moveId === queuedMove.move);
|
||||
// If the queued move was called indirectly, ignore all PP and usability checks.
|
||||
// Otherwise, ensure that the move being used is actually usable & in our moveset.
|
||||
// TODO: What should happen if a pokemon forgets a charging move mid-use?
|
||||
if (isVirtual(queuedMove.useMode) || movesetMove?.isUsable(this, isIgnorePP(queuedMove.useMode))) {
|
||||
moveQueue.splice(0, i); // TODO: This should not be done here
|
||||
return queuedMove;
|
||||
}
|
||||
}
|
||||
|
||||
// We went through the entire queue without a match; clear the entire thing.
|
||||
this.summonData.moveQueue = [];
|
||||
|
||||
// Filter out any moves this Pokemon cannot use
|
||||
let movePool = this.getMoveset().filter(m => m.isUsable(this));
|
||||
// If no moves are left, use Struggle. Otherwise, continue with move selection
|
||||
if (movePool.length) {
|
||||
// If there's only 1 move in the move pool, use it.
|
||||
if (movePool.length === 1) {
|
||||
return { move: movePool[0].moveId, targets: this.getNextTargets(movePool[0].moveId) };
|
||||
return {
|
||||
move: movePool[0].moveId,
|
||||
targets: this.getNextTargets(movePool[0].moveId),
|
||||
useMode: MoveUseMode.NORMAL,
|
||||
};
|
||||
}
|
||||
// If a move is forced because of Encore, use it.
|
||||
// Said moves are executed normally
|
||||
const encoreTag = this.getTag(EncoreTag) as EncoreTag;
|
||||
if (encoreTag) {
|
||||
const encoreMove = movePool.find(m => m.moveId === encoreTag.moveId);
|
||||
@ -6198,6 +6238,7 @@ export class EnemyPokemon extends Pokemon {
|
||||
return {
|
||||
move: encoreMove.moveId,
|
||||
targets: this.getNextTargets(encoreMove.moveId),
|
||||
useMode: MoveUseMode.NORMAL,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -6205,7 +6246,7 @@ export class EnemyPokemon extends Pokemon {
|
||||
// No enemy should spawn with this AI type in-game
|
||||
case AiType.RANDOM: {
|
||||
const moveId = movePool[globalScene.randBattleSeedInt(movePool.length)].moveId;
|
||||
return { move: moveId, targets: this.getNextTargets(moveId) };
|
||||
return { move: moveId, targets: this.getNextTargets(moveId), useMode: MoveUseMode.NORMAL };
|
||||
}
|
||||
case AiType.SMART_RANDOM:
|
||||
case AiType.SMART: {
|
||||
@ -6374,14 +6415,20 @@ export class EnemyPokemon extends Pokemon {
|
||||
r,
|
||||
sortedMovePool.map(m => m.getName()),
|
||||
);
|
||||
return { move: sortedMovePool[r]!.moveId, targets: moveTargets[sortedMovePool[r]!.moveId] };
|
||||
return {
|
||||
move: sortedMovePool[r]!.moveId,
|
||||
targets: moveTargets[sortedMovePool[r]!.moveId],
|
||||
useMode: MoveUseMode.NORMAL,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No moves left means struggle
|
||||
return {
|
||||
move: MoveId.STRUGGLE,
|
||||
targets: this.getNextTargets(MoveId.STRUGGLE),
|
||||
useMode: MoveUseMode.IGNORE_PP,
|
||||
};
|
||||
}
|
||||
|
||||
@ -6727,10 +6774,9 @@ interface IllusionData {
|
||||
export interface TurnMove {
|
||||
move: MoveId;
|
||||
targets: BattlerIndex[];
|
||||
useMode: MoveUseMode;
|
||||
result?: MoveResult;
|
||||
virtual?: boolean;
|
||||
turn?: number;
|
||||
ignorePP?: boolean;
|
||||
}
|
||||
|
||||
export interface AttackMoveResult {
|
||||
@ -6749,6 +6795,12 @@ export interface AttackMoveResult {
|
||||
export class PokemonSummonData {
|
||||
/** [Atk, Def, SpAtk, SpDef, Spd, Acc, Eva] */
|
||||
public statStages: number[] = [0, 0, 0, 0, 0, 0, 0];
|
||||
/**
|
||||
* A queue of moves yet to be executed, used by charging, recharging and frenzy moves.
|
||||
* So long as this array is nonempty, this Pokemon's corresponding `CommandPhase` will be skipped over entirely
|
||||
* in favor of using the queued move.
|
||||
* TODO: Clean up a lot of the code surrounding the move queue.
|
||||
*/
|
||||
public moveQueue: TurnMove[] = [];
|
||||
public tags: BattlerTag[] = [];
|
||||
public abilitySuppressed = false;
|
||||
@ -6868,7 +6920,6 @@ export class PokemonWaveData {
|
||||
* Resets at the start of a new turn, as well as on switch.
|
||||
*/
|
||||
export class PokemonTurnData {
|
||||
public flinched = false;
|
||||
public acted = false;
|
||||
/** How many times the current move should hit the target(s) */
|
||||
public hitCount = 0;
|
||||
@ -6890,8 +6941,9 @@ export class PokemonTurnData {
|
||||
public failedRunAway = false;
|
||||
public joinedRound = false;
|
||||
/**
|
||||
* 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
|
||||
* forced to act again in the same turn
|
||||
* forced to act again in the same turn, and **must be incremented** by any effects that grant extra actions.
|
||||
*/
|
||||
public extraTurns = 0;
|
||||
/**
|
||||
|
@ -2393,8 +2393,6 @@ export interface ModifierPool {
|
||||
[tier: string]: WeightedModifierType[];
|
||||
}
|
||||
|
||||
const modifierPool: ModifierPool = {};
|
||||
|
||||
let modifierPoolThresholds = {};
|
||||
let ignoredPoolIndexes = {};
|
||||
|
||||
@ -2859,7 +2857,7 @@ function getNewModifierTypeOption(
|
||||
}
|
||||
|
||||
tier += upgradeCount;
|
||||
while (tier && (!modifierPool.hasOwnProperty(tier) || !modifierPool[tier].length)) {
|
||||
while (tier && (!pool.hasOwnProperty(tier) || !pool[tier].length)) {
|
||||
tier--;
|
||||
if (upgradeCount) {
|
||||
upgradeCount--;
|
||||
@ -2870,7 +2868,7 @@ function getNewModifierTypeOption(
|
||||
if (tier < ModifierTier.MASTER && allowLuckUpgrades) {
|
||||
const partyLuckValue = getPartyLuckValue(party);
|
||||
const upgradeOdds = Math.floor(128 / ((partyLuckValue + 4) / 4));
|
||||
while (modifierPool.hasOwnProperty(tier + upgradeCount + 1) && modifierPool[tier + upgradeCount + 1].length) {
|
||||
while (pool.hasOwnProperty(tier + upgradeCount + 1) && pool[tier + upgradeCount + 1].length) {
|
||||
if (randSeedInt(upgradeOdds) < 4) {
|
||||
upgradeCount++;
|
||||
} else {
|
||||
@ -2920,6 +2918,7 @@ function getNewModifierTypeOption(
|
||||
}
|
||||
|
||||
export function getDefaultModifierTypeForTier(tier: ModifierTier): ModifierType {
|
||||
const modifierPool = getModifierPoolForType(ModifierPoolType.PLAYER);
|
||||
let modifierType: ModifierType | WeightedModifierType = modifierPool[tier || ModifierTier.COMMON][0];
|
||||
if (modifierType instanceof WeightedModifierType) {
|
||||
modifierType = (modifierType as WeightedModifierType).modifierType;
|
||||
|
@ -272,7 +272,7 @@ class DefaultOverrides {
|
||||
|
||||
/**
|
||||
* Set all non-scripted waves to use the selected battle type.
|
||||
*
|
||||
*
|
||||
* Ignored if set to {@linkcode BattleType.TRAINER} and `DISABLE_STANDARD_TRAINERS_OVERRIDE` is `true`.
|
||||
*/
|
||||
readonly BATTLE_TYPE_OVERRIDE: Exclude<BattleType, BattleType.CLEAR> | null = null;
|
||||
@ -285,17 +285,17 @@ export const defaultOverrides = new DefaultOverrides();
|
||||
|
||||
export default {
|
||||
...defaultOverrides,
|
||||
...overrides
|
||||
...overrides,
|
||||
} satisfies InstanceType<typeof DefaultOverrides>;
|
||||
|
||||
export type BattleStyle = "double" | "single" | "even-doubles" | "odd-doubles";
|
||||
|
||||
export type RandomTrainerOverride = {
|
||||
/** The Type of trainer to force */
|
||||
trainerType: Exclude<TrainerType, TrainerType.UNKNOWN>,
|
||||
trainerType: Exclude<TrainerType, TrainerType.UNKNOWN>;
|
||||
/* If the selected trainer type has a double version, it will always use its double version. */
|
||||
alwaysDouble?: boolean
|
||||
}
|
||||
alwaysDouble?: boolean;
|
||||
};
|
||||
|
||||
/** The type of the {@linkcode DefaultOverrides} class */
|
||||
export type OverridesType = typeof DefaultOverrides;
|
||||
export type OverridesType = typeof DefaultOverrides;
|
||||
|
@ -397,7 +397,7 @@ export class PhaseManager {
|
||||
* @returns the found phase or undefined if none found
|
||||
*/
|
||||
findPhase<P extends Phase = Phase>(phaseFilter: (phase: P) => boolean): P | undefined {
|
||||
return this.phaseQueue.find(phaseFilter) as P;
|
||||
return this.phaseQueue.find(phaseFilter) as P | undefined;
|
||||
}
|
||||
|
||||
tryReplacePhase(phaseFilter: (phase: Phase) => boolean, phase: Phase): boolean {
|
||||
|
@ -1,8 +1,5 @@
|
||||
import { ModifierTier } from "#enums/modifier-tier";
|
||||
import {
|
||||
regenerateModifierPoolThresholds,
|
||||
getEnemyBuffModifierForWave,
|
||||
} from "#app/modifier/modifier-type";
|
||||
import { regenerateModifierPoolThresholds, getEnemyBuffModifierForWave } from "#app/modifier/modifier-type";
|
||||
import { ModifierPoolType } from "#enums/modifier-pool-type";
|
||||
import { EnemyPersistentModifier } from "#app/modifier/modifier";
|
||||
import { Phase } from "#app/phase";
|
||||
|
@ -22,6 +22,7 @@ import { MysteryEncounterMode } from "#enums/mystery-encounter-mode";
|
||||
import { isNullOrUndefined } from "#app/utils/common";
|
||||
import { ArenaTagSide } from "#enums/arena-tag-side";
|
||||
import { ArenaTagType } from "#app/enums/arena-tag-type";
|
||||
import { isVirtual, isIgnorePP, MoveUseMode } from "#enums/move-use-mode";
|
||||
|
||||
export class CommandPhase extends FieldPhase {
|
||||
public readonly phaseName = "CommandPhase";
|
||||
@ -80,7 +81,7 @@ export class CommandPhase extends FieldPhase {
|
||||
) {
|
||||
globalScene.currentBattle.turnCommands[this.fieldIndex] = {
|
||||
command: Command.FIGHT,
|
||||
move: { move: MoveId.NONE, targets: [] },
|
||||
move: { move: MoveId.NONE, targets: [], useMode: MoveUseMode.NORMAL },
|
||||
skip: true,
|
||||
};
|
||||
}
|
||||
@ -103,29 +104,31 @@ export class CommandPhase extends FieldPhase {
|
||||
moveQueue.length &&
|
||||
moveQueue[0] &&
|
||||
moveQueue[0].move &&
|
||||
!moveQueue[0].virtual &&
|
||||
!isVirtual(moveQueue[0].useMode) &&
|
||||
(!playerPokemon.getMoveset().find(m => m.moveId === moveQueue[0].move) ||
|
||||
!playerPokemon
|
||||
.getMoveset()
|
||||
[playerPokemon.getMoveset().findIndex(m => m.moveId === moveQueue[0].move)].isUsable(
|
||||
playerPokemon,
|
||||
moveQueue[0].ignorePP,
|
||||
isIgnorePP(moveQueue[0].useMode),
|
||||
))
|
||||
) {
|
||||
moveQueue.shift();
|
||||
}
|
||||
|
||||
// TODO: Refactor this. I did a few simple find/replace matches but this is just ABHORRENTLY structured
|
||||
if (moveQueue.length > 0) {
|
||||
const queuedMove = moveQueue[0];
|
||||
if (!queuedMove.move) {
|
||||
this.handleCommand(Command.FIGHT, -1);
|
||||
this.handleCommand(Command.FIGHT, -1, MoveUseMode.NORMAL);
|
||||
} else {
|
||||
const moveIndex = playerPokemon.getMoveset().findIndex(m => m.moveId === queuedMove.move);
|
||||
if (
|
||||
(moveIndex > -1 && playerPokemon.getMoveset()[moveIndex].isUsable(playerPokemon, queuedMove.ignorePP)) ||
|
||||
queuedMove.virtual
|
||||
(moveIndex > -1 &&
|
||||
playerPokemon.getMoveset()[moveIndex].isUsable(playerPokemon, isIgnorePP(queuedMove.useMode))) ||
|
||||
isVirtual(queuedMove.useMode)
|
||||
) {
|
||||
this.handleCommand(Command.FIGHT, moveIndex, queuedMove.ignorePP, queuedMove);
|
||||
this.handleCommand(Command.FIGHT, moveIndex, queuedMove.useMode, queuedMove);
|
||||
} else {
|
||||
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
|
||||
}
|
||||
@ -143,18 +146,23 @@ export class CommandPhase extends FieldPhase {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Remove `args` and clean this thing up
|
||||
* Code will need to be copied over from pkty except replacing the `virtual` and `ignorePP` args with a corresponding `MoveUseMode`.
|
||||
*/
|
||||
handleCommand(command: Command, cursor: number, ...args: any[]): boolean {
|
||||
const playerPokemon = globalScene.getPlayerField()[this.fieldIndex];
|
||||
let success = false;
|
||||
|
||||
switch (command) {
|
||||
// TODO: We don't need 2 args for this - moveUseMode is carried over from queuedMove
|
||||
case Command.TERA:
|
||||
case Command.FIGHT:
|
||||
case Command.FIGHT: {
|
||||
let useStruggle = false;
|
||||
const turnMove: TurnMove | undefined = args.length === 2 ? (args[1] as TurnMove) : undefined;
|
||||
if (
|
||||
cursor === -1 ||
|
||||
playerPokemon.trySelectMove(cursor, args[0] as boolean) ||
|
||||
playerPokemon.trySelectMove(cursor, isIgnorePP(args[0] as MoveUseMode)) ||
|
||||
(useStruggle = cursor > -1 && !playerPokemon.getMoveset().filter(m => m.isUsable(playerPokemon)).length)
|
||||
) {
|
||||
let moveId: MoveId;
|
||||
@ -171,7 +179,7 @@ export class CommandPhase extends FieldPhase {
|
||||
const turnCommand: TurnCommand = {
|
||||
command: Command.FIGHT,
|
||||
cursor: cursor,
|
||||
move: { move: moveId, targets: [], ignorePP: args[0] },
|
||||
move: { move: moveId, targets: [], useMode: args[0] },
|
||||
args: args,
|
||||
};
|
||||
const preTurnCommand: TurnCommand = {
|
||||
@ -233,7 +241,8 @@ export class CommandPhase extends FieldPhase {
|
||||
);
|
||||
}
|
||||
break;
|
||||
case Command.BALL:
|
||||
}
|
||||
case Command.BALL: {
|
||||
const notInDex =
|
||||
globalScene
|
||||
.getEnemyField()
|
||||
@ -337,8 +346,9 @@ export class CommandPhase extends FieldPhase {
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Command.POKEMON:
|
||||
case Command.RUN:
|
||||
case Command.RUN: {
|
||||
const isSwitch = command === Command.POKEMON;
|
||||
const { currentBattle, arena } = globalScene;
|
||||
const mysteryEncounterFleeAllowed = currentBattle.mysteryEncounter?.fleeAllowed;
|
||||
@ -445,6 +455,7 @@ export class CommandPhase extends FieldPhase {
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (success) {
|
||||
|
@ -12,7 +12,6 @@ import { UiMode } from "#enums/ui-mode";
|
||||
import i18next from "i18next";
|
||||
import { PlayerPartyMemberPokemonPhase } from "#app/phases/player-party-member-pokemon-phase";
|
||||
import type Pokemon from "#app/field/pokemon";
|
||||
import { ConfirmUiMode } from "#enums/confirm-ui-mode";
|
||||
import { LearnMoveType } from "#enums/learn-move-type";
|
||||
|
||||
export class LearnMovePhase extends PlayerPartyMemberPokemonPhase {
|
||||
@ -164,10 +163,6 @@ export class LearnMovePhase extends PlayerPartyMemberPokemonPhase {
|
||||
globalScene.ui.setMode(this.messageMode);
|
||||
this.replaceMoveCheck(move, pokemon);
|
||||
},
|
||||
false,
|
||||
0,
|
||||
0,
|
||||
ConfirmUiMode.DEFAULT_NO,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -8,10 +8,11 @@ import { MoveResult } from "#enums/move-result";
|
||||
import { BooleanHolder } from "#app/utils/common";
|
||||
import { PokemonPhase } from "#app/phases/pokemon-phase";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import type { MoveUseMode } from "#enums/move-use-mode";
|
||||
import type { ChargingMove } from "#app/@types/move-types";
|
||||
|
||||
/**
|
||||
* Phase for the "charging turn" of two-turn moves (e.g. Dig).
|
||||
* @extends {@linkcode PokemonPhase}
|
||||
*/
|
||||
export class MoveChargePhase extends PokemonPhase {
|
||||
public readonly phaseName = "MoveChargePhase";
|
||||
@ -20,10 +21,21 @@ export class MoveChargePhase extends PokemonPhase {
|
||||
/** The field index targeted by the move (Charging moves assume single target) */
|
||||
public targetIndex: BattlerIndex;
|
||||
|
||||
constructor(battlerIndex: BattlerIndex, targetIndex: BattlerIndex, move: PokemonMove) {
|
||||
/** The {@linkcode MoveUseMode} of the move that triggered the charge; passed on from move phase */
|
||||
private useMode: MoveUseMode;
|
||||
|
||||
/**
|
||||
* Create a new MoveChargePhase.
|
||||
* @param battlerIndex - The {@linkcode BattlerIndex} of the user.
|
||||
* @param targetIndex - The {@linkcode BattlerIndex} of the target.
|
||||
* @param move - The {@linkcode PokemonMove} being used
|
||||
* @param useMode - The move's {@linkcode MoveUseMode}
|
||||
*/
|
||||
constructor(battlerIndex: BattlerIndex, targetIndex: BattlerIndex, move: PokemonMove, useMode: MoveUseMode) {
|
||||
super(battlerIndex);
|
||||
this.move = move;
|
||||
this.targetIndex = targetIndex;
|
||||
this.useMode = useMode;
|
||||
}
|
||||
|
||||
public override start() {
|
||||
@ -37,7 +49,8 @@ export class MoveChargePhase extends PokemonPhase {
|
||||
// immediately end this phase.
|
||||
if (!target || !move.isChargingMove()) {
|
||||
console.warn("Invalid parameters for MoveChargePhase");
|
||||
return super.end();
|
||||
super.end();
|
||||
return;
|
||||
}
|
||||
|
||||
new MoveChargeAnim(move.chargeAnim, move.id, user).play(false, () => {
|
||||
@ -52,29 +65,30 @@ export class MoveChargePhase extends PokemonPhase {
|
||||
/** Checks the move's instant charge conditions, then ends this phase. */
|
||||
public override end() {
|
||||
const user = this.getUserPokemon();
|
||||
const move = this.move.getMove();
|
||||
// Checked for `ChargingMove` in `this.start()`
|
||||
const move = this.move.getMove() as ChargingMove;
|
||||
|
||||
if (move.isChargingMove()) {
|
||||
const instantCharge = new BooleanHolder(false);
|
||||
const instantCharge = new BooleanHolder(false);
|
||||
applyMoveChargeAttrs("InstantChargeAttr", user, null, move, instantCharge);
|
||||
|
||||
applyMoveChargeAttrs("InstantChargeAttr", user, null, move, instantCharge);
|
||||
|
||||
if (instantCharge.value) {
|
||||
// this MoveEndPhase will be duplicated by the queued MovePhase if not removed
|
||||
globalScene.phaseManager.tryRemovePhase(phase => phase.is("MoveEndPhase") && phase.getPokemon() === user);
|
||||
// queue a new MovePhase for this move's attack phase
|
||||
globalScene.phaseManager.unshiftNew("MovePhase", user, [this.targetIndex], this.move, false);
|
||||
} else {
|
||||
user.getMoveQueue().push({ move: move.id, targets: [this.targetIndex] });
|
||||
}
|
||||
|
||||
// Add this move's charging phase to the user's move history
|
||||
user.pushMoveHistory({
|
||||
move: this.move.moveId,
|
||||
targets: [this.targetIndex],
|
||||
result: MoveResult.OTHER,
|
||||
});
|
||||
// If instantly charging, remove the pending MoveEndPhase and queue a new MovePhase for the "attack" portion of the move.
|
||||
// Otherwise, add the attack portion to the user's move queue to execute next turn.
|
||||
// TODO: This checks status twice for a single-turn usage...
|
||||
if (instantCharge.value) {
|
||||
globalScene.phaseManager.tryRemovePhase(phase => phase.is("MoveEndPhase") && phase.getPokemon() === user);
|
||||
globalScene.phaseManager.unshiftNew("MovePhase", user, [this.targetIndex], this.move, this.useMode);
|
||||
} else {
|
||||
user.pushMoveQueue({ move: move.id, targets: [this.targetIndex], useMode: this.useMode });
|
||||
}
|
||||
|
||||
// Add this move's charging phase to the user's move history
|
||||
user.pushMoveHistory({
|
||||
move: this.move.moveId,
|
||||
targets: [this.targetIndex],
|
||||
result: MoveResult.OTHER,
|
||||
useMode: this.useMode,
|
||||
});
|
||||
|
||||
super.end();
|
||||
}
|
||||
|
||||
|
@ -54,20 +54,25 @@ import { HitCheckResult } from "#enums/hit-check-result";
|
||||
import type Move from "#app/data/moves/move";
|
||||
import { isFieldTargeted } from "#app/data/moves/move-utils";
|
||||
import { DamageAchv } from "#app/system/achv";
|
||||
import { isVirtual, isReflected, MoveUseMode } from "#enums/move-use-mode";
|
||||
|
||||
type HitCheckEntry = [HitCheckResult, TypeDamageMultiplier];
|
||||
export type HitCheckEntry = [HitCheckResult, TypeDamageMultiplier];
|
||||
|
||||
export class MoveEffectPhase extends PokemonPhase {
|
||||
public readonly phaseName = "MoveEffectPhase";
|
||||
public move: Move;
|
||||
private virtual = false;
|
||||
protected targets: BattlerIndex[];
|
||||
protected reflected = false;
|
||||
protected useMode: MoveUseMode;
|
||||
|
||||
/** The result of the hit check against each target */
|
||||
private hitChecks: HitCheckEntry[];
|
||||
|
||||
/** The move history entry for the move */
|
||||
/**
|
||||
* Log to be entered into the user's move history once the move result is resolved.
|
||||
|
||||
* Note that `result` logs whether the move was successfully
|
||||
* used in the sense of "Does it have an effect on the user?".
|
||||
*/
|
||||
private moveHistoryEntry: TurnMove;
|
||||
|
||||
/** Is this the first strike of a move? */
|
||||
@ -75,19 +80,20 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
/** Is this the last strike of a move? */
|
||||
private lastHit: boolean;
|
||||
|
||||
/** Phases queued during moves */
|
||||
/**
|
||||
* Phases queued during moves; used to add a new MovePhase for reflected moves after triggering.
|
||||
* TODO: Remove this and move the reflection logic to ability-side
|
||||
*/
|
||||
private queuedPhases: Phase[] = [];
|
||||
|
||||
/**
|
||||
* @param reflected Indicates that the move was reflected by the user due to magic coat or magic bounce
|
||||
* @param virtual Indicates that the move is a virtual move (i.e. called by metronome)
|
||||
* @param useMode - The {@linkcode MoveUseMode} corresponding to how this move was used.
|
||||
*/
|
||||
constructor(battlerIndex: BattlerIndex, targets: BattlerIndex[], move: Move, reflected = false, virtual = false) {
|
||||
constructor(battlerIndex: BattlerIndex, targets: BattlerIndex[], move: Move, useMode: MoveUseMode) {
|
||||
super(battlerIndex);
|
||||
this.move = move;
|
||||
this.virtual = virtual;
|
||||
this.useMode = useMode;
|
||||
|
||||
this.reflected = reflected;
|
||||
/**
|
||||
* In double battles, if the right Pokemon selects a spread move and the left Pokemon dies
|
||||
* with no party members available to switch in, then the right Pokemon takes the index
|
||||
@ -158,7 +164,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
* Queue the phaes that should occur when the target reflects the move back to the user
|
||||
* @param user - The {@linkcode Pokemon} using this phase's invoked move
|
||||
* @param target - The {@linkcode Pokemon} that is reflecting the move
|
||||
*
|
||||
* TODO: Rework this to use `onApply` of Magic Coat
|
||||
*/
|
||||
private queueReflectedMove(user: Pokemon, target: Pokemon): void {
|
||||
const newTargets = this.move.isMultiTarget()
|
||||
@ -181,10 +187,8 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
"MovePhase",
|
||||
target,
|
||||
newTargets,
|
||||
new PokemonMove(this.move.id, 0, 0, true),
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
new PokemonMove(this.move.id),
|
||||
MoveUseMode.REFLECTED,
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -278,8 +282,18 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
const overridden = new BooleanHolder(false);
|
||||
const move = this.move;
|
||||
|
||||
// Assume single target for override
|
||||
applyMoveAttrs("OverrideMoveEffectAttr", user, this.getFirstTarget() ?? null, move, overridden, this.virtual);
|
||||
// Apply effects to override a move effect.
|
||||
// Assuming single target here works as this is (currently)
|
||||
// only used for Future Sight, calling and Pledge moves.
|
||||
// TODO: change if any other move effect overrides are introduced
|
||||
applyMoveAttrs(
|
||||
"OverrideMoveEffectAttr",
|
||||
user,
|
||||
this.getFirstTarget() ?? null,
|
||||
move,
|
||||
overridden,
|
||||
isVirtual(this.useMode),
|
||||
);
|
||||
|
||||
// If other effects were overriden, stop this phase before they can be applied
|
||||
if (overridden.value) {
|
||||
@ -290,8 +304,8 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
// Lapse `MOVE_EFFECT` effects (i.e. semi-invulnerability) when applicable
|
||||
user.lapseTags(BattlerTagLapseType.MOVE_EFFECT);
|
||||
|
||||
// If the user is acting again (such as due to Instruct), reset hitsLeft/hitCount so that
|
||||
// the move executes correctly (ensures all hits of a multi-hit are properly calculated)
|
||||
// If the user is acting again (such as due to Instruct or Dancer), reset hitsLeft/hitCount and
|
||||
// recalculate hit count for multi-hit moves.
|
||||
if (user.turnData.hitsLeft === 0 && user.turnData.hitCount > 0 && user.turnData.extraTurns > 0) {
|
||||
user.turnData.hitsLeft = -1;
|
||||
user.turnData.hitCount = 0;
|
||||
@ -316,16 +330,11 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
user.turnData.hitsLeft = hitCount.value;
|
||||
}
|
||||
|
||||
/*
|
||||
* Log to be entered into the user's move history once the move result is resolved.
|
||||
* Note that `result` logs whether the move was successfully
|
||||
* used in the sense of "Does it have an effect on the user?".
|
||||
*/
|
||||
this.moveHistoryEntry = {
|
||||
move: this.move.id,
|
||||
targets: this.targets,
|
||||
result: MoveResult.PENDING,
|
||||
virtual: this.virtual,
|
||||
useMode: this.useMode,
|
||||
};
|
||||
|
||||
const fieldMove = isFieldTargeted(move);
|
||||
@ -390,29 +399,35 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
|
||||
public override end(): void {
|
||||
const user = this.getUserPokemon();
|
||||
/**
|
||||
* If this phase isn't for the invoked move's last strike,
|
||||
* unshift another MoveEffectPhase for the next strike.
|
||||
* Otherwise, queue a message indicating the number of times the move has struck
|
||||
* (if the move has struck more than once), then apply the heal from Shell Bell
|
||||
* to the user.
|
||||
*/
|
||||
if (user) {
|
||||
if (user.turnData.hitsLeft && --user.turnData.hitsLeft >= 1 && this.getFirstTarget()?.isActive()) {
|
||||
globalScene.phaseManager.unshiftPhase(this.getNewHitPhase());
|
||||
} else {
|
||||
// Queue message for number of hits made by multi-move
|
||||
// If multi-hit attack only hits once, still want to render a message
|
||||
const hitsTotal = user.turnData.hitCount - Math.max(user.turnData.hitsLeft, 0);
|
||||
if (hitsTotal > 1 || (user.turnData.hitsLeft && user.turnData.hitsLeft > 0)) {
|
||||
// If there are multiple hits, or if there are hits of the multi-hit move left
|
||||
globalScene.phaseManager.queueMessage(i18next.t("battle:attackHitsCount", { count: hitsTotal }));
|
||||
}
|
||||
globalScene.applyModifiers(HitHealModifier, this.player, user);
|
||||
this.getTargets().forEach(target => (target.turnData.moveEffectiveness = null));
|
||||
}
|
||||
if (!user) {
|
||||
super.end();
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* If this phase isn't for the invoked move's last strike (and we still have something to hit),
|
||||
* unshift another MoveEffectPhase for the next strike before ending this phase.
|
||||
*/
|
||||
if (--user.turnData.hitsLeft >= 1 && this.getFirstTarget()) {
|
||||
this.addNextHitPhase();
|
||||
super.end();
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* All hits of the move have resolved by now.
|
||||
* Queue message for multi-strike moves before applying Shell Bell heals & proccing Dancer-like effects.
|
||||
*/
|
||||
const hitsTotal = user.turnData.hitCount - Math.max(user.turnData.hitsLeft, 0);
|
||||
if (hitsTotal > 1 || user.turnData.hitsLeft > 0) {
|
||||
// Queue message if multiple hits occurred or were slated to occur (such as a Triple Axel miss)
|
||||
globalScene.phaseManager.queueMessage(i18next.t("battle:attackHitsCount", { count: hitsTotal }));
|
||||
}
|
||||
|
||||
globalScene.applyModifiers(HitHealModifier, this.player, user);
|
||||
this.getTargets().forEach(target => {
|
||||
target.turnData.moveEffectiveness = null;
|
||||
});
|
||||
super.end();
|
||||
}
|
||||
|
||||
@ -422,7 +437,6 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
* @param user - The {@linkcode Pokemon} using this phase's invoked move
|
||||
* @param target - {@linkcode Pokemon} the current target of this phase's invoked move
|
||||
* @param hitResult - The {@linkcode HitResult} of the attempted move
|
||||
* @returns a `Promise` intended to be passed into a `then()` call.
|
||||
*/
|
||||
protected applyOnGetHitAbEffects(user: Pokemon, target: Pokemon, hitResult: HitResult): void {
|
||||
applyPostDefendAbAttrs("PostDefendAbAttr", target, user, this.move, hitResult);
|
||||
@ -434,7 +448,6 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
* @param user - The {@linkcode Pokemon} using this phase's invoked move
|
||||
* @param target - {@linkcode Pokemon} the current target of this phase's invoked move
|
||||
* @param dealsDamage - `true` if the attempted move successfully dealt damage
|
||||
* @returns a function intended to be passed into a `then()` call.
|
||||
*/
|
||||
protected applyHeldItemFlinchCheck(user: Pokemon, target: Pokemon, dealsDamage: boolean): void {
|
||||
if (this.move.hasAttr("FlinchAttr")) {
|
||||
@ -458,8 +471,9 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
* @param user - The {@linkcode Pokemon} using this phase's invoked move
|
||||
* @param target - {@linkcode Pokemon} the target to check for protection
|
||||
* @param move - The {@linkcode Move} being used
|
||||
* @returns Whether the pokemon was protected
|
||||
*/
|
||||
private protectedCheck(user: Pokemon, target: Pokemon) {
|
||||
private protectedCheck(user: Pokemon, target: Pokemon): boolean {
|
||||
/** The {@linkcode ArenaTagSide} to which the target belongs */
|
||||
const targetSide = target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
|
||||
/** Has the invoked move been cancelled by conditional protection (e.g Quick Guard)? */
|
||||
@ -480,14 +494,15 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Break up this chunky boolean to make it more palatable
|
||||
return (
|
||||
![MoveTarget.ENEMY_SIDE, MoveTarget.BOTH_SIDES].includes(this.move.moveTarget) &&
|
||||
(bypassIgnoreProtect.value || !this.move.doesFlagEffectApply({ flag: MoveFlags.IGNORE_PROTECT, user, target })) &&
|
||||
(hasConditionalProtectApplied.value ||
|
||||
(!target.findTags(t => t instanceof DamageProtectedTag).length &&
|
||||
target.findTags(t => t instanceof ProtectedTag).find(t => target.lapseTag(t.tagType))) ||
|
||||
target.findTags(t => t instanceof ProtectedTag).some(t => target.lapseTag(t.tagType))) ||
|
||||
(this.move.category !== MoveCategory.STATUS &&
|
||||
target.findTags(t => t instanceof DamageProtectedTag).find(t => target.lapseTag(t.tagType))))
|
||||
target.findTags(t => t instanceof DamageProtectedTag).some(t => target.lapseTag(t.tagType))))
|
||||
);
|
||||
}
|
||||
|
||||
@ -547,7 +562,8 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
return [HitCheckResult.PROTECTED, 0];
|
||||
}
|
||||
|
||||
if (!this.reflected && move.doesFlagEffectApply({ flag: MoveFlags.REFLECTABLE, user, target })) {
|
||||
// Reflected moves cannot be reflected again
|
||||
if (!isReflected(this.useMode) && move.doesFlagEffectApply({ flag: MoveFlags.REFLECTABLE, user, target })) {
|
||||
return [HitCheckResult.REFLECTED, 0];
|
||||
}
|
||||
|
||||
@ -660,12 +676,17 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
return (this.player ? globalScene.getPlayerField() : globalScene.getEnemyField())[this.fieldIndex];
|
||||
}
|
||||
|
||||
/** @returns An array of all {@linkcode Pokemon} targeted by this phase's invoked move */
|
||||
/**
|
||||
* @returns An array of {@linkcode Pokemon} that are:
|
||||
* - On-field and active
|
||||
* - Non-fainted
|
||||
* - Targeted by this phase's invoked move
|
||||
*/
|
||||
public getTargets(): Pokemon[] {
|
||||
return globalScene.getField(true).filter(p => this.targets.indexOf(p.getBattlerIndex()) > -1);
|
||||
}
|
||||
|
||||
/** @returns The first target of this phase's invoked move */
|
||||
/** @returns The first active, non-fainted target of this phase's invoked move. */
|
||||
public getFirstTarget(): Pokemon | undefined {
|
||||
return this.getTargets()[0];
|
||||
}
|
||||
@ -705,9 +726,12 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
}
|
||||
}
|
||||
|
||||
/** @returns A new `MoveEffectPhase` with the same properties as this phase */
|
||||
protected getNewHitPhase(): MoveEffectPhase {
|
||||
return new MoveEffectPhase(this.battlerIndex, this.targets, this.move, this.reflected, this.virtual);
|
||||
/**
|
||||
* Unshifts a new `MoveEffectPhase` with the same properties as this phase.
|
||||
* Used to queue the next hit of multi-strike moves.
|
||||
*/
|
||||
protected addNextHitPhase(): void {
|
||||
globalScene.phaseManager.unshiftNew("MoveEffectPhase", this.battlerIndex, this.targets, this.move, this.useMode);
|
||||
}
|
||||
|
||||
/** Removes all substitutes that were broken by this phase's invoked move */
|
||||
@ -729,7 +753,6 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
* @param firstTarget Whether the target is the first to be hit by the current strike
|
||||
* @param selfTarget If defined, limits the effects triggered to either self-targeted
|
||||
* effects (if set to `true`) or targeted effects (if set to `false`).
|
||||
* @returns a `Promise` applying the relevant move effects.
|
||||
*/
|
||||
protected triggerMoveEffects(
|
||||
triggerType: MoveEffectTrigger,
|
||||
@ -775,6 +798,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
|
||||
const hitResult = this.applyMove(user, target, effectiveness);
|
||||
|
||||
// Apply effects to the user (always) and the target (if not blocked by substitute).
|
||||
this.triggerMoveEffects(MoveEffectTrigger.POST_APPLY, user, target, firstTarget, true);
|
||||
if (!this.move.hitsSubstitute(user, target)) {
|
||||
this.applyOnTargetEffects(user, target, hitResult, firstTarget);
|
||||
|
@ -25,9 +25,9 @@ export class MoveEndPhase extends PokemonPhase {
|
||||
if (!this.wasFollowUp && pokemon?.isActive(true)) {
|
||||
pokemon.lapseTags(BattlerTagLapseType.AFTER_MOVE);
|
||||
}
|
||||
globalScene.arena.setIgnoreAbilities(false);
|
||||
|
||||
// Remove effects which were set on a Pokemon which removes them on summon (i.e. via Mold Breaker)
|
||||
globalScene.arena.setIgnoreAbilities(false);
|
||||
for (const target of this.targets) {
|
||||
if (target) {
|
||||
applyPostSummonAbAttrs("PostSummonRemoveEffectAbAttr", target);
|
||||
|
@ -5,7 +5,6 @@ import type { DelayedAttackTag } from "#app/data/arena-tag";
|
||||
import { CommonAnim } from "#enums/move-anims-common";
|
||||
import { CenterOfAttentionTag } from "#app/data/battler-tags";
|
||||
import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type";
|
||||
import type { HealStatusEffectAttr } from "#app/data/moves/move";
|
||||
import { applyMoveAttrs } from "#app/data/moves/apply-attrs";
|
||||
import { allMoves } from "#app/data/data-lists";
|
||||
import { MoveFlags } from "#enums/MoveFlags";
|
||||
@ -20,13 +19,14 @@ import { MoveResult } from "#enums/move-result";
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
import Overrides from "#app/overrides";
|
||||
import { BattlePhase } from "#app/phases/battle-phase";
|
||||
import { NumberHolder } from "#app/utils/common";
|
||||
import { enumValueToKey, NumberHolder } from "#app/utils/common";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { ArenaTagType } from "#enums/arena-tag-type";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import i18next from "i18next";
|
||||
import { isVirtual, isIgnorePP, isReflected, MoveUseMode, isIgnoreStatus } from "#enums/move-use-mode";
|
||||
import { frenzyMissFunc } from "#app/data/moves/move-utils";
|
||||
|
||||
export class MovePhase extends BattlePhase {
|
||||
@ -34,17 +34,19 @@ export class MovePhase extends BattlePhase {
|
||||
protected _pokemon: Pokemon;
|
||||
protected _move: PokemonMove;
|
||||
protected _targets: BattlerIndex[];
|
||||
protected followUp: boolean;
|
||||
protected ignorePp: boolean;
|
||||
public readonly useMode: MoveUseMode; // Made public for quash
|
||||
protected forcedLast: boolean;
|
||||
|
||||
/** Whether the current move should fail but still use PP */
|
||||
protected failed = false;
|
||||
/** Whether the current move should cancel and retain PP */
|
||||
protected cancelled = false;
|
||||
protected reflected = false;
|
||||
|
||||
public get pokemon(): Pokemon {
|
||||
return this._pokemon;
|
||||
}
|
||||
|
||||
// TODO: Do we need public getters but only protected setters?
|
||||
protected set pokemon(pokemon: Pokemon) {
|
||||
this._pokemon = pokemon;
|
||||
}
|
||||
@ -66,51 +68,42 @@ export class MovePhase extends BattlePhase {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param followUp Indicates that the move being used is a "follow-up" - for example, a move being used by Metronome or Dancer.
|
||||
* Follow-ups bypass a few failure conditions, including flinches, sleep/paralysis/freeze and volatile status checks, etc.
|
||||
* @param reflected Indicates that the move was reflected by Magic Coat or Magic Bounce.
|
||||
* Reflected moves cannot be reflected again and will not trigger Dancer.
|
||||
* Create a new MovePhase for using moves.
|
||||
* @param pokemon - The {@linkcode Pokemon} using the move
|
||||
* @param move - The {@linkcode PokemonMove} to use
|
||||
* @param useMode - The {@linkcode MoveUseMode} corresponding to this move's means of execution (usually `MoveUseMode.NORMAL`).
|
||||
* Not marked optional to ensure callers correctly pass on `useModes`.
|
||||
* @param forcedLast - Whether to force this phase to occur last in order (for {@linkcode MoveId.QUASH}); default `false`
|
||||
*/
|
||||
|
||||
constructor(
|
||||
pokemon: Pokemon,
|
||||
targets: BattlerIndex[],
|
||||
move: PokemonMove,
|
||||
followUp = false,
|
||||
ignorePp = false,
|
||||
reflected = false,
|
||||
forcedLast = false,
|
||||
) {
|
||||
constructor(pokemon: Pokemon, targets: BattlerIndex[], move: PokemonMove, useMode: MoveUseMode, forcedLast = false) {
|
||||
super();
|
||||
|
||||
this.pokemon = pokemon;
|
||||
this.targets = targets;
|
||||
this.move = move;
|
||||
this.followUp = followUp;
|
||||
this.ignorePp = ignorePp;
|
||||
this.reflected = reflected;
|
||||
this.useMode = useMode;
|
||||
this.forcedLast = forcedLast;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the pokemon is active, if the move is usable, and that the move is targetting something.
|
||||
* Checks if the pokemon is active, if the move is usable, and that the move is targeting something.
|
||||
* @param ignoreDisableTags `true` to not check if the move is disabled
|
||||
* @returns `true` if all the checks pass
|
||||
*/
|
||||
public canMove(ignoreDisableTags = false): boolean {
|
||||
return (
|
||||
this.pokemon.isActive(true) &&
|
||||
this.move.isUsable(this.pokemon, this.ignorePp, ignoreDisableTags) &&
|
||||
!!this.targets.length
|
||||
this.move.isUsable(this.pokemon, isIgnorePP(this.useMode), ignoreDisableTags) &&
|
||||
this.targets.length > 0
|
||||
);
|
||||
}
|
||||
|
||||
/**Signifies the current move should fail but still use PP */
|
||||
/** Signifies the current move should fail but still use PP */
|
||||
public fail(): void {
|
||||
this.failed = true;
|
||||
}
|
||||
|
||||
/**Signifies the current move should cancel and retain PP */
|
||||
/** Signifies the current move should cancel and retain PP */
|
||||
public cancel(): void {
|
||||
this.cancelled = true;
|
||||
}
|
||||
@ -118,7 +111,7 @@ export class MovePhase extends BattlePhase {
|
||||
/**
|
||||
* Shows whether the current move has been forced to the end of the turn
|
||||
* Needed for speed order, see {@linkcode MoveId.QUASH}
|
||||
* */
|
||||
*/
|
||||
public isForcedLast(): boolean {
|
||||
return this.forcedLast;
|
||||
}
|
||||
@ -126,35 +119,37 @@ export class MovePhase extends BattlePhase {
|
||||
public start(): void {
|
||||
super.start();
|
||||
|
||||
console.log(MoveId[this.move.moveId]);
|
||||
console.log(MoveId[this.move.moveId], enumValueToKey(MoveUseMode, this.useMode));
|
||||
|
||||
// Check if move is unusable (e.g. because it's out of PP due to a mid-turn Spite).
|
||||
// Check if move is unusable (e.g. running out of PP due to a mid-turn Spite
|
||||
// or the user no longer being on field), ending the phase early if not.
|
||||
if (!this.canMove(true)) {
|
||||
if (this.pokemon.isActive(true)) {
|
||||
this.fail();
|
||||
this.showMoveText();
|
||||
this.showFailedText();
|
||||
}
|
||||
return this.end();
|
||||
this.end();
|
||||
return;
|
||||
}
|
||||
|
||||
this.pokemon.turnData.acted = true;
|
||||
|
||||
// Reset hit-related turn data when starting follow-up moves (e.g. Metronomed moves, Dancer repeats)
|
||||
if (this.followUp) {
|
||||
if (isVirtual(this.useMode)) {
|
||||
this.pokemon.turnData.hitsLeft = -1;
|
||||
this.pokemon.turnData.hitCount = 0;
|
||||
}
|
||||
|
||||
// Check move to see if arena.ignoreAbilities should be true.
|
||||
if (!this.followUp || this.reflected) {
|
||||
if (
|
||||
this.move
|
||||
.getMove()
|
||||
.doesFlagEffectApply({ flag: MoveFlags.IGNORE_ABILITIES, user: this.pokemon, isFollowUp: this.followUp })
|
||||
) {
|
||||
globalScene.arena.setIgnoreAbilities(true, this.pokemon.getBattlerIndex());
|
||||
}
|
||||
if (
|
||||
this.move.getMove().doesFlagEffectApply({
|
||||
flag: MoveFlags.IGNORE_ABILITIES,
|
||||
user: this.pokemon,
|
||||
isFollowUp: isVirtual(this.useMode), // Sunsteel strike and co. don't work when called indirectly
|
||||
})
|
||||
) {
|
||||
globalScene.arena.setIgnoreAbilities(true, this.pokemon.getBattlerIndex());
|
||||
}
|
||||
|
||||
this.resolveRedirectTarget();
|
||||
@ -187,7 +182,7 @@ export class MovePhase extends BattlePhase {
|
||||
|
||||
if (
|
||||
(targets.length === 0 && !this.move.getMove().hasAttr("AddArenaTrapTagAttr")) ||
|
||||
(moveQueue.length && moveQueue[0].move === MoveId.NONE)
|
||||
(moveQueue.length > 0 && moveQueue[0].move === MoveId.NONE)
|
||||
) {
|
||||
this.showMoveText();
|
||||
this.showFailedText();
|
||||
@ -200,83 +195,98 @@ export class MovePhase extends BattlePhase {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles {@link StatusEffect.SLEEP Sleep}/{@link StatusEffect.PARALYSIS Paralysis}/{@link StatusEffect.FREEZE Freeze} rolls and side effects.
|
||||
* Handles {@link StatusEffect.SLEEP | Sleep}/{@link StatusEffect.PARALYSIS | Paralysis}/{@link StatusEffect.FREEZE | Freeze} rolls and side effects.
|
||||
*/
|
||||
protected resolvePreMoveStatusEffects(): void {
|
||||
if (!this.followUp && this.pokemon.status && !this.pokemon.status.isPostTurn()) {
|
||||
this.pokemon.status.incrementTurn();
|
||||
let activated = false;
|
||||
let healed = false;
|
||||
// Skip for follow ups/reflected moves, no status condition or post turn statuses (e.g. Poison/Toxic)
|
||||
if (!this.pokemon.status?.effect || this.pokemon.status.isPostTurn() || isIgnoreStatus(this.useMode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (this.pokemon.status.effect) {
|
||||
case StatusEffect.PARALYSIS:
|
||||
activated =
|
||||
(!this.pokemon.randBattleSeedInt(4) || Overrides.STATUS_ACTIVATION_OVERRIDE === true) &&
|
||||
Overrides.STATUS_ACTIVATION_OVERRIDE !== false;
|
||||
break;
|
||||
case StatusEffect.SLEEP: {
|
||||
applyMoveAttrs("BypassSleepAttr", this.pokemon, null, this.move.getMove());
|
||||
const turnsRemaining = new NumberHolder(this.pokemon.status.sleepTurnsRemaining ?? 0);
|
||||
applyAbAttrs(
|
||||
"ReduceStatusEffectDurationAbAttr",
|
||||
this.pokemon,
|
||||
null,
|
||||
false,
|
||||
this.pokemon.status.effect,
|
||||
turnsRemaining,
|
||||
);
|
||||
this.pokemon.status.sleepTurnsRemaining = turnsRemaining.value;
|
||||
healed = this.pokemon.status.sleepTurnsRemaining <= 0;
|
||||
activated = !healed && !this.pokemon.getTag(BattlerTagType.BYPASS_SLEEP);
|
||||
break;
|
||||
}
|
||||
case StatusEffect.FREEZE:
|
||||
healed =
|
||||
!!this.move
|
||||
.getMove()
|
||||
.findAttr(
|
||||
attr =>
|
||||
attr.is("HealStatusEffectAttr") &&
|
||||
attr.selfTarget &&
|
||||
(attr as unknown as HealStatusEffectAttr).isOfEffect(StatusEffect.FREEZE),
|
||||
) ||
|
||||
(!this.pokemon.randBattleSeedInt(5) && Overrides.STATUS_ACTIVATION_OVERRIDE !== true) ||
|
||||
Overrides.STATUS_ACTIVATION_OVERRIDE === false;
|
||||
if (
|
||||
this.useMode === MoveUseMode.INDIRECT &&
|
||||
[StatusEffect.SLEEP, StatusEffect.FREEZE].includes(this.pokemon.status.effect)
|
||||
) {
|
||||
// Dancer thaws out or wakes up a frozen/sleeping user prior to use
|
||||
this.pokemon.resetStatus(false);
|
||||
return;
|
||||
}
|
||||
|
||||
activated = !healed;
|
||||
break;
|
||||
this.pokemon.status.incrementTurn();
|
||||
|
||||
/** Whether to prevent us from using the move */
|
||||
let activated = false;
|
||||
/** Whether to cure the status */
|
||||
let healed = false;
|
||||
|
||||
switch (this.pokemon.status.effect) {
|
||||
case StatusEffect.PARALYSIS:
|
||||
activated =
|
||||
(this.pokemon.randBattleSeedInt(4) === 0 || Overrides.STATUS_ACTIVATION_OVERRIDE === true) &&
|
||||
Overrides.STATUS_ACTIVATION_OVERRIDE !== false;
|
||||
break;
|
||||
case StatusEffect.SLEEP: {
|
||||
applyMoveAttrs("BypassSleepAttr", this.pokemon, null, this.move.getMove());
|
||||
const turnsRemaining = new NumberHolder(this.pokemon.status.sleepTurnsRemaining ?? 0);
|
||||
applyAbAttrs(
|
||||
"ReduceStatusEffectDurationAbAttr",
|
||||
this.pokemon,
|
||||
null,
|
||||
false,
|
||||
this.pokemon.status.effect,
|
||||
turnsRemaining,
|
||||
);
|
||||
this.pokemon.status.sleepTurnsRemaining = turnsRemaining.value;
|
||||
healed = this.pokemon.status.sleepTurnsRemaining <= 0;
|
||||
activated = !healed && !this.pokemon.getTag(BattlerTagType.BYPASS_SLEEP);
|
||||
break;
|
||||
}
|
||||
case StatusEffect.FREEZE:
|
||||
healed =
|
||||
!!this.move
|
||||
.getMove()
|
||||
.findAttr(
|
||||
attr => attr.is("HealStatusEffectAttr") && attr.selfTarget && attr.isOfEffect(StatusEffect.FREEZE),
|
||||
) ||
|
||||
(!this.pokemon.randBattleSeedInt(5) && Overrides.STATUS_ACTIVATION_OVERRIDE !== true) ||
|
||||
Overrides.STATUS_ACTIVATION_OVERRIDE === false;
|
||||
|
||||
if (activated) {
|
||||
this.cancel();
|
||||
globalScene.phaseManager.queueMessage(
|
||||
getStatusEffectActivationText(this.pokemon.status.effect, getPokemonNameWithAffix(this.pokemon)),
|
||||
);
|
||||
globalScene.phaseManager.unshiftNew(
|
||||
"CommonAnimPhase",
|
||||
this.pokemon.getBattlerIndex(),
|
||||
undefined,
|
||||
CommonAnim.POISON + (this.pokemon.status.effect - 1),
|
||||
);
|
||||
} else if (healed) {
|
||||
globalScene.phaseManager.queueMessage(
|
||||
getStatusEffectHealText(this.pokemon.status.effect, getPokemonNameWithAffix(this.pokemon)),
|
||||
);
|
||||
this.pokemon.resetStatus();
|
||||
this.pokemon.updateInfo();
|
||||
}
|
||||
activated = !healed;
|
||||
break;
|
||||
}
|
||||
|
||||
if (activated) {
|
||||
// Cancel move activation and play effect
|
||||
this.cancel();
|
||||
globalScene.phaseManager.queueMessage(
|
||||
getStatusEffectActivationText(this.pokemon.status.effect, getPokemonNameWithAffix(this.pokemon)),
|
||||
);
|
||||
globalScene.phaseManager.unshiftNew(
|
||||
"CommonAnimPhase",
|
||||
this.pokemon.getBattlerIndex(),
|
||||
undefined,
|
||||
CommonAnim.POISON + (this.pokemon.status.effect - 1), // offset anim # by effect #
|
||||
);
|
||||
} else if (healed) {
|
||||
// cure status and play effect
|
||||
globalScene.phaseManager.queueMessage(
|
||||
getStatusEffectHealText(this.pokemon.status.effect, getPokemonNameWithAffix(this.pokemon)),
|
||||
);
|
||||
this.pokemon.resetStatus();
|
||||
this.pokemon.updateInfo();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lapse {@linkcode BattlerTagLapseType.PRE_MOVE PRE_MOVE} tags that trigger before a move is used, regardless of whether or not it failed.
|
||||
* Also lapse {@linkcode BattlerTagLapseType.MOVE MOVE} tags if the move should be successful.
|
||||
* Lapse {@linkcode BattlerTagLapseType.PRE_MOVE | PRE_MOVE} tags that trigger before a move is used, regardless of whether or not it failed.
|
||||
* Also lapse {@linkcode BattlerTagLapseType.MOVE | MOVE} tags if the move is successful and not called indirectly.
|
||||
*/
|
||||
protected lapsePreMoveAndMoveTags(): void {
|
||||
this.pokemon.lapseTags(BattlerTagLapseType.PRE_MOVE);
|
||||
|
||||
// TODO: does this intentionally happen before the no targets/MoveId.NONE on queue cancellation case is checked?
|
||||
if (!this.followUp && this.canMove() && !this.cancelled) {
|
||||
// (In other words, check if truant can proc on a move w/o targets)
|
||||
if (!isIgnoreStatus(this.useMode) && this.canMove() && !this.cancelled) {
|
||||
this.pokemon.lapseTags(BattlerTagLapseType.MOVE);
|
||||
}
|
||||
}
|
||||
@ -284,11 +294,12 @@ export class MovePhase extends BattlePhase {
|
||||
protected useMove(): void {
|
||||
const targets = this.getActiveTargetPokemon();
|
||||
const moveQueue = this.pokemon.getMoveQueue();
|
||||
const move = this.move.getMove();
|
||||
|
||||
// form changes happen even before we know that the move wll execute.
|
||||
globalScene.triggerPokemonFormChange(this.pokemon, SpeciesFormChangePreMoveTrigger);
|
||||
|
||||
const isDelayedAttack = this.move.getMove().hasAttr("DelayedAttackAttr");
|
||||
const isDelayedAttack = move.hasAttr("DelayedAttackAttr");
|
||||
if (isDelayedAttack) {
|
||||
// Check the player side arena if future sight is active
|
||||
const futureSightTags = globalScene.arena.findTags(t => t.tagType === ArenaTagType.FUTURE_SIGHT);
|
||||
@ -310,7 +321,8 @@ export class MovePhase extends BattlePhase {
|
||||
if (fail) {
|
||||
this.showMoveText();
|
||||
this.showFailedText();
|
||||
return this.end();
|
||||
this.end();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@ -327,21 +339,21 @@ export class MovePhase extends BattlePhase {
|
||||
this.showMoveText();
|
||||
}
|
||||
|
||||
if (moveQueue.length > 0) {
|
||||
// Using .shift here clears out two turn moves once they've been used
|
||||
this.ignorePp = moveQueue.shift()?.ignorePP ?? false;
|
||||
}
|
||||
|
||||
// Clear out any two turn moves once they've been used.
|
||||
// TODO: Refactor move queues and remove this assignment;
|
||||
// Move queues should be handled by the calling `CommandPhase` or a manager for it
|
||||
// @ts-expect-error - useMode is readonly and shouldn't normally be assigned to
|
||||
this.useMode = moveQueue.shift()?.useMode ?? this.useMode;
|
||||
if (this.pokemon.getTag(BattlerTagType.CHARGING)?.sourceMove === this.move.moveId) {
|
||||
this.pokemon.lapseTag(BattlerTagType.CHARGING);
|
||||
}
|
||||
|
||||
// "commit" to using the move, deducting PP.
|
||||
if (!this.ignorePp) {
|
||||
if (!isIgnorePP(this.useMode)) {
|
||||
// "commit" to using the move, deducting PP.
|
||||
const ppUsed = 1 + this.getPpIncreaseFromPressure(targets);
|
||||
|
||||
this.move.usePp(ppUsed);
|
||||
globalScene.eventTarget.dispatchEvent(new MoveUsedEvent(this.pokemon?.id, this.move.getMove(), this.move.ppUsed));
|
||||
globalScene.eventTarget.dispatchEvent(new MoveUsedEvent(this.pokemon?.id, move, this.move.ppUsed));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -355,8 +367,6 @@ export class MovePhase extends BattlePhase {
|
||||
* TODO: These steps are straightforward, but the implementation below is extremely convoluted.
|
||||
*/
|
||||
|
||||
const move = this.move.getMove();
|
||||
|
||||
/**
|
||||
* Move conditions assume the move has a single target
|
||||
* TODO: is this sustainable?
|
||||
@ -392,8 +402,7 @@ export class MovePhase extends BattlePhase {
|
||||
this.pokemon.getBattlerIndex(),
|
||||
this.targets,
|
||||
move,
|
||||
this.reflected,
|
||||
this.move.virtual,
|
||||
this.useMode,
|
||||
);
|
||||
} else {
|
||||
if ([MoveId.ROAR, MoveId.WHIRLWIND, MoveId.TRICK_OR_TREAT, MoveId.FORESTS_CURSE].includes(this.move.moveId)) {
|
||||
@ -404,7 +413,7 @@ export class MovePhase extends BattlePhase {
|
||||
move: this.move.moveId,
|
||||
targets: this.targets,
|
||||
result: MoveResult.FAIL,
|
||||
virtual: this.move.virtual,
|
||||
useMode: this.useMode,
|
||||
});
|
||||
|
||||
const failureMessage = move.getFailedText(this.pokemon, targets[0], move);
|
||||
@ -424,8 +433,10 @@ export class MovePhase extends BattlePhase {
|
||||
}
|
||||
|
||||
// Handle Dancer, which triggers immediately after a move is used (rather than waiting on `this.end()`).
|
||||
// Note that the `!this.followUp` check here prevents an infinite Dancer loop.
|
||||
if (this.move.getMove().hasFlag(MoveFlags.DANCE_MOVE) && !this.followUp) {
|
||||
// Note the MoveUseMode check here prevents an infinite Dancer loop.
|
||||
const dancerModes: MoveUseMode[] = [MoveUseMode.INDIRECT, MoveUseMode.REFLECTED] as const;
|
||||
if (this.move.getMove().hasFlag(MoveFlags.DANCE_MOVE) && !dancerModes.includes(this.useMode)) {
|
||||
// TODO: Fix in dancer PR to move to MEP for hit checks
|
||||
globalScene.getField(true).forEach(pokemon => {
|
||||
applyPostMoveUsedAbAttrs("PostMoveUsedAbAttr", pokemon, this.move, this.pokemon, this.targets);
|
||||
});
|
||||
@ -437,23 +448,16 @@ export class MovePhase extends BattlePhase {
|
||||
const move = this.move.getMove();
|
||||
const targets = this.getActiveTargetPokemon();
|
||||
|
||||
if (move.applyConditions(this.pokemon, targets[0], move)) {
|
||||
// Protean and Libero apply on the charging turn of charge moves
|
||||
applyPreAttackAbAttrs("PokemonTypeChangeAbAttr", this.pokemon, null, this.move.getMove());
|
||||
this.showMoveText();
|
||||
|
||||
this.showMoveText();
|
||||
globalScene.phaseManager.unshiftNew(
|
||||
"MoveChargePhase",
|
||||
this.pokemon.getBattlerIndex(),
|
||||
this.targets[0],
|
||||
this.move,
|
||||
);
|
||||
} else {
|
||||
// Conditions currently assume single target
|
||||
// TODO: Is this sustainable?
|
||||
if (!move.applyConditions(this.pokemon, targets[0], move)) {
|
||||
this.pokemon.pushMoveHistory({
|
||||
move: this.move.moveId,
|
||||
targets: this.targets,
|
||||
result: MoveResult.FAIL,
|
||||
virtual: this.move.virtual,
|
||||
useMode: this.useMode,
|
||||
});
|
||||
|
||||
const failureMessage = move.getFailedText(this.pokemon, targets[0], move);
|
||||
@ -462,7 +466,19 @@ export class MovePhase extends BattlePhase {
|
||||
|
||||
// Remove the user from its semi-invulnerable state (if applicable)
|
||||
this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT);
|
||||
return;
|
||||
}
|
||||
|
||||
// Protean and Libero apply on the charging turn of charge moves
|
||||
applyPreAttackAbAttrs("PokemonTypeChangeAbAttr", this.pokemon, null, this.move.getMove());
|
||||
|
||||
globalScene.phaseManager.unshiftNew(
|
||||
"MoveChargePhase",
|
||||
this.pokemon.getBattlerIndex(),
|
||||
this.targets[0],
|
||||
this.move,
|
||||
this.useMode,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -473,7 +489,7 @@ export class MovePhase extends BattlePhase {
|
||||
"MoveEndPhase",
|
||||
this.pokemon.getBattlerIndex(),
|
||||
this.getActiveTargetPokemon(),
|
||||
this.followUp,
|
||||
isVirtual(this.useMode),
|
||||
);
|
||||
|
||||
super.end();
|
||||
@ -605,7 +621,7 @@ export class MovePhase extends BattlePhase {
|
||||
protected handlePreMoveFailures(): void {
|
||||
if (this.cancelled || this.failed) {
|
||||
if (this.failed) {
|
||||
const ppUsed = this.ignorePp ? 0 : 1;
|
||||
const ppUsed = isIgnorePP(this.useMode) ? 0 : 1;
|
||||
|
||||
if (ppUsed) {
|
||||
this.move.usePp();
|
||||
@ -622,6 +638,7 @@ export class MovePhase extends BattlePhase {
|
||||
move: MoveId.NONE,
|
||||
result: MoveResult.FAIL,
|
||||
targets: this.targets,
|
||||
useMode: this.useMode,
|
||||
});
|
||||
|
||||
this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT);
|
||||
@ -645,7 +662,7 @@ export class MovePhase extends BattlePhase {
|
||||
}
|
||||
|
||||
globalScene.phaseManager.queueMessage(
|
||||
i18next.t(this.reflected ? "battle:magicCoatActivated" : "battle:useMove", {
|
||||
i18next.t(isReflected(this.useMode) ? "battle:magicCoatActivated" : "battle:useMove", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(this.pokemon),
|
||||
moveName: this.move.getName(),
|
||||
}),
|
||||
|
@ -53,7 +53,8 @@ export class PokemonAnimPhase extends BattlePhase {
|
||||
private doSubstituteAddAnim(): void {
|
||||
const substitute = this.pokemon.getTag(SubstituteTag);
|
||||
if (isNullOrUndefined(substitute)) {
|
||||
return this.end();
|
||||
this.end();
|
||||
return;
|
||||
}
|
||||
|
||||
const getSprite = () => {
|
||||
@ -116,12 +117,14 @@ export class PokemonAnimPhase extends BattlePhase {
|
||||
|
||||
private doSubstitutePreMoveAnim(): void {
|
||||
if (this.fieldAssets.length !== 1) {
|
||||
return this.end();
|
||||
this.end();
|
||||
return;
|
||||
}
|
||||
|
||||
const subSprite = this.fieldAssets[0];
|
||||
if (subSprite === undefined) {
|
||||
return this.end();
|
||||
this.end();
|
||||
return;
|
||||
}
|
||||
|
||||
globalScene.tweens.add({
|
||||
@ -145,12 +148,14 @@ export class PokemonAnimPhase extends BattlePhase {
|
||||
|
||||
private doSubstitutePostMoveAnim(): void {
|
||||
if (this.fieldAssets.length !== 1) {
|
||||
return this.end();
|
||||
this.end();
|
||||
return;
|
||||
}
|
||||
|
||||
const subSprite = this.fieldAssets[0];
|
||||
if (subSprite === undefined) {
|
||||
return this.end();
|
||||
this.end();
|
||||
return;
|
||||
}
|
||||
|
||||
globalScene.tweens.add({
|
||||
@ -174,12 +179,14 @@ export class PokemonAnimPhase extends BattlePhase {
|
||||
|
||||
private doSubstituteRemoveAnim(): void {
|
||||
if (this.fieldAssets.length !== 1) {
|
||||
return this.end();
|
||||
this.end();
|
||||
return;
|
||||
}
|
||||
|
||||
const subSprite = this.fieldAssets[0];
|
||||
if (subSprite === undefined) {
|
||||
return this.end();
|
||||
this.end();
|
||||
return;
|
||||
}
|
||||
|
||||
const getSprite = () => {
|
||||
@ -244,12 +251,14 @@ export class PokemonAnimPhase extends BattlePhase {
|
||||
|
||||
private doCommanderApplyAnim(): void {
|
||||
if (!globalScene.currentBattle?.double) {
|
||||
return this.end();
|
||||
this.end();
|
||||
return;
|
||||
}
|
||||
const dondozo = this.pokemon.getAlly();
|
||||
|
||||
if (dondozo?.species?.speciesId !== SpeciesId.DONDOZO) {
|
||||
return this.end();
|
||||
this.end();
|
||||
return;
|
||||
}
|
||||
|
||||
const tatsugiriX = this.pokemon.x + this.pokemon.getSprite().x;
|
||||
@ -329,7 +338,8 @@ export class PokemonAnimPhase extends BattlePhase {
|
||||
const tatsugiri = this.pokemon.getAlly();
|
||||
if (isNullOrUndefined(tatsugiri)) {
|
||||
console.warn("Aborting COMMANDER_REMOVE anim: Tatsugiri is undefined");
|
||||
return this.end();
|
||||
this.end();
|
||||
return;
|
||||
}
|
||||
|
||||
const tatsuSprite = globalScene.addPokemonSprite(
|
||||
|
@ -4,6 +4,10 @@ import type Pokemon from "#app/field/pokemon";
|
||||
import { FieldPhase } from "./field-phase";
|
||||
|
||||
export abstract class PokemonPhase extends FieldPhase {
|
||||
/**
|
||||
* The battler index this phase refers to, or the pokemon ID if greater than 3.
|
||||
* TODO: Make this either use IDs or `BattlerIndex`es, not a weird mix of both
|
||||
*/
|
||||
protected battlerIndex: BattlerIndex | number;
|
||||
public player: boolean;
|
||||
public fieldIndex: number;
|
||||
@ -15,10 +19,12 @@ export abstract class PokemonPhase extends FieldPhase {
|
||||
battlerIndex ??
|
||||
globalScene
|
||||
.getField()
|
||||
.find(p => p?.isActive())! // TODO: is the bang correct here?
|
||||
.getBattlerIndex();
|
||||
.find(p => p?.isActive())
|
||||
?.getBattlerIndex();
|
||||
if (battlerIndex === undefined) {
|
||||
console.warn("There are no Pokemon on the field!"); // TODO: figure out a suitable fallback behavior
|
||||
// TODO: figure out a suitable fallback behavior
|
||||
console.warn("There are no Pokemon on the field!");
|
||||
battlerIndex = BattlerIndex.PLAYER;
|
||||
}
|
||||
|
||||
this.battlerIndex = battlerIndex;
|
||||
|
@ -29,7 +29,8 @@ export class PokemonTransformPhase extends PokemonPhase {
|
||||
const target = globalScene.getField(true).find(p => p.getBattlerIndex() === this.targetIndex);
|
||||
|
||||
if (!target) {
|
||||
return this.end();
|
||||
this.end();
|
||||
return;
|
||||
}
|
||||
|
||||
user.summonData.speciesForm = target.getSpeciesForm();
|
||||
@ -52,7 +53,7 @@ export class PokemonTransformPhase extends PokemonPhase {
|
||||
user.summonData.moveset = target.getMoveset().map(m => {
|
||||
if (m) {
|
||||
// If PP value is less than 5, do nothing. If greater, we need to reduce the value to 5.
|
||||
return new PokemonMove(m.moveId, 0, 0, false, Math.min(m.getMove().pp, 5));
|
||||
return new PokemonMove(m.moveId, 0, 0, Math.min(m.getMove().pp, 5));
|
||||
}
|
||||
console.warn(`Transform: somehow iterating over a ${m} value when copying moveset!`);
|
||||
return new PokemonMove(MoveId.NONE);
|
||||
|
@ -29,7 +29,8 @@ export class QuietFormChangePhase extends BattlePhase {
|
||||
super.start();
|
||||
|
||||
if (this.pokemon.formIndex === this.pokemon.species.forms.findIndex(f => f.formKey === this.formChange.formKey)) {
|
||||
return this.end();
|
||||
this.end();
|
||||
return;
|
||||
}
|
||||
|
||||
const preName = getPokemonNameWithAffix(this.pokemon);
|
||||
|
@ -65,7 +65,7 @@ export class TurnStartPhase extends FieldPhase {
|
||||
// This occurs before the main loop because of battles with more than two Pokemon
|
||||
const battlerBypassSpeed = {};
|
||||
|
||||
globalScene.getField(true).map(p => {
|
||||
globalScene.getField(true).forEach(p => {
|
||||
const bypassSpeed = new BooleanHolder(false);
|
||||
const canCheckHeldItems = new BooleanHolder(true);
|
||||
applyAbAttrs("BypassSpeedChanceAbAttr", p, null, false, bypassSpeed);
|
||||
@ -126,6 +126,8 @@ export class TurnStartPhase extends FieldPhase {
|
||||
return moveOrder;
|
||||
}
|
||||
|
||||
// TODO: Refactor this alongside `CommandPhase.handleCommand` to use SEPARATE METHODS
|
||||
// Also need a clearer distinction between "turn command" and queued moves
|
||||
start() {
|
||||
super.start();
|
||||
|
||||
@ -156,8 +158,9 @@ export class TurnStartPhase extends FieldPhase {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: This logic is questionable and needs to be redone,
|
||||
// especially given the fact that `order` is used for exactly 1 thing...
|
||||
// TODO: Remove `turnData.order` -
|
||||
// it is used exclusively for Fusion Flare/Bolt
|
||||
// and uses a really jank implementation
|
||||
if (turnCommand.command === Command.FIGHT) {
|
||||
pokemon.turnData.order = index;
|
||||
}
|
||||
@ -245,7 +248,7 @@ export class TurnStartPhase extends FieldPhase {
|
||||
turnCommand.targets ?? turnCommand.move.targets,
|
||||
move,
|
||||
false,
|
||||
queuedMove.ignorePP,
|
||||
queuedMove.useMode,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ float hue2rgb(float f1, float f2, float hue) {
|
||||
|
||||
vec3 rgb2hsl(vec3 color) {
|
||||
vec3 hsl;
|
||||
|
||||
|
||||
float fmin = min(min(color.r, color.g), color.b);
|
||||
float fmax = max(max(color.r, color.g), color.b);
|
||||
float delta = fmax - fmin;
|
||||
@ -66,7 +66,7 @@ vec3 rgb2hsl(vec3 color) {
|
||||
hsl.y = delta / (fmax + fmin);
|
||||
else
|
||||
hsl.y = delta / (2.0 - fmax - fmin);
|
||||
|
||||
|
||||
float deltaR = (((fmax - color.r) / 6.0) + (delta / 2.0)) / delta;
|
||||
float deltaG = (((fmax - color.g) / 6.0) + (delta / 2.0)) / delta;
|
||||
float deltaB = (((fmax - color.b) / 6.0) + (delta / 2.0)) / delta;
|
||||
@ -89,24 +89,24 @@ vec3 rgb2hsl(vec3 color) {
|
||||
|
||||
vec3 hsl2rgb(vec3 hsl) {
|
||||
vec3 rgb;
|
||||
|
||||
|
||||
if (hsl.y == 0.0)
|
||||
rgb = vec3(hsl.z);
|
||||
else {
|
||||
float f2;
|
||||
|
||||
|
||||
if (hsl.z < 0.5)
|
||||
f2 = hsl.z * (1.0 + hsl.y);
|
||||
else
|
||||
f2 = (hsl.z + hsl.y) - (hsl.y * hsl.z);
|
||||
|
||||
|
||||
float f1 = 2.0 * hsl.z - f2;
|
||||
|
||||
|
||||
rgb.r = hue2rgb(f1, f2, hsl.x + (1.0/3.0));
|
||||
rgb.g = hue2rgb(f1, f2, hsl.x);
|
||||
rgb.b = hue2rgb(f1, f2, hsl.x - (1.0/3.0));
|
||||
}
|
||||
|
||||
|
||||
return rgb;
|
||||
}
|
||||
|
||||
|
@ -83,7 +83,7 @@ vec3 rgb2hsl(vec3 color) {
|
||||
hsl.y = delta / (fmax + fmin);
|
||||
else
|
||||
hsl.y = delta / (2.0 - fmax - fmin);
|
||||
|
||||
|
||||
float deltaR = (((fmax - color.r) / 6.0) + (delta / 2.0)) / delta;
|
||||
float deltaG = (((fmax - color.g) / 6.0) + (delta / 2.0)) / delta;
|
||||
float deltaB = (((fmax - color.b) / 6.0) + (delta / 2.0)) / delta;
|
||||
@ -106,24 +106,24 @@ vec3 rgb2hsl(vec3 color) {
|
||||
|
||||
vec3 hsl2rgb(vec3 hsl) {
|
||||
vec3 rgb;
|
||||
|
||||
|
||||
if (hsl.y == 0.0)
|
||||
rgb = vec3(hsl.z);
|
||||
else {
|
||||
float f2;
|
||||
|
||||
|
||||
if (hsl.z < 0.5)
|
||||
f2 = hsl.z * (1.0 + hsl.y);
|
||||
else
|
||||
f2 = (hsl.z + hsl.y) - (hsl.y * hsl.z);
|
||||
|
||||
|
||||
float f1 = 2.0 * hsl.z - f2;
|
||||
|
||||
|
||||
rgb.r = hue2rgb(f1, f2, hsl.x + (1.0/3.0));
|
||||
rgb.g = hue2rgb(f1, f2, hsl.x);
|
||||
rgb.b= hue2rgb(f1, f2, hsl.x - (1.0/3.0));
|
||||
}
|
||||
|
||||
|
||||
return rgb;
|
||||
}
|
||||
|
||||
|
@ -174,7 +174,24 @@ export async function initI18n(): Promise<void> {
|
||||
"es-MX": ["es-ES", "en"],
|
||||
default: ["en"],
|
||||
},
|
||||
supportedLngs: ["en", "es-ES", "es-MX", "fr", "it", "de", "zh-CN", "zh-TW", "pt-BR", "ko", "ja", "ca", "da", "tr", "ro", "ru"],
|
||||
supportedLngs: [
|
||||
"en",
|
||||
"es-ES",
|
||||
"es-MX",
|
||||
"fr",
|
||||
"it",
|
||||
"de",
|
||||
"zh-CN",
|
||||
"zh-TW",
|
||||
"pt-BR",
|
||||
"ko",
|
||||
"ja",
|
||||
"ca",
|
||||
"da",
|
||||
"tr",
|
||||
"ro",
|
||||
"ru",
|
||||
],
|
||||
backend: {
|
||||
loadPath(lng: string, [ns]: string[]) {
|
||||
let fileName: string;
|
||||
|
@ -32,7 +32,10 @@ const pressAction = i18next.t("settings:pressActionToAssign");
|
||||
|
||||
export const settingGamepadOptions = {
|
||||
[SettingGamepad.Controller]: [i18next.t("settings:controllerDefault"), i18next.t("settings:controllerChange")],
|
||||
[SettingGamepad.Gamepad_Support]: [i18next.t("settings:gamepadSupportAuto"), i18next.t("settings:gamepadSupportDisabled")],
|
||||
[SettingGamepad.Gamepad_Support]: [
|
||||
i18next.t("settings:gamepadSupportAuto"),
|
||||
i18next.t("settings:gamepadSupportDisabled"),
|
||||
],
|
||||
[SettingGamepad.Button_Up]: [`KEY ${Button.UP.toString()}`, pressAction],
|
||||
[SettingGamepad.Button_Down]: [`KEY ${Button.DOWN.toString()}`, pressAction],
|
||||
[SettingGamepad.Button_Left]: [`KEY ${Button.LEFT.toString()}`, pressAction],
|
||||
|
@ -959,7 +959,7 @@ export function setSetting(setting: string, value: number): boolean {
|
||||
},
|
||||
{
|
||||
label: "Türkçe (Needs Help)",
|
||||
handler: () => changeLocaleHandler("tr")
|
||||
handler: () => changeLocaleHandler("tr"),
|
||||
},
|
||||
{
|
||||
label: "Русский (Needs Help)",
|
||||
@ -967,11 +967,11 @@ export function setSetting(setting: string, value: number): boolean {
|
||||
},
|
||||
{
|
||||
label: "Dansk (Needs Help)",
|
||||
handler: () => changeLocaleHandler("da")
|
||||
handler: () => changeLocaleHandler("da"),
|
||||
},
|
||||
{
|
||||
label: "Română (Needs Help)",
|
||||
handler: () => changeLocaleHandler("ro")
|
||||
handler: () => changeLocaleHandler("ro"),
|
||||
},
|
||||
{
|
||||
label: i18next.t("settings:back"),
|
||||
|
@ -63,6 +63,10 @@ import * as v1_8_3 from "./versions/v1_8_3";
|
||||
// biome-ignore lint/style/noNamespaceImport: Convenience
|
||||
import * as v1_9_0 from "./versions/v1_9_0";
|
||||
|
||||
// --- v1.10.0 PATCHES --- //
|
||||
// biome-ignore lint/style/noNamespaceImport: Convenience
|
||||
import * as v1_10_0 from "./versions/v1_10_0";
|
||||
|
||||
/** Current game version */
|
||||
const LATEST_VERSION = version;
|
||||
|
||||
@ -85,6 +89,7 @@ const sessionMigrators: SessionSaveMigrator[] = [];
|
||||
sessionMigrators.push(...v1_0_4.sessionMigrators);
|
||||
sessionMigrators.push(...v1_7_0.sessionMigrators);
|
||||
sessionMigrators.push(...v1_9_0.sessionMigrators);
|
||||
sessionMigrators.push(...v1_10_0.sessionMigrators);
|
||||
|
||||
/** All settings migrators */
|
||||
const settingsMigrators: SettingsSaveMigrator[] = [];
|
||||
|
48
src/system/version_migration/versions/v1_10_0.ts
Normal file
48
src/system/version_migration/versions/v1_10_0.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import type { SessionSaveMigrator } from "#app/@types/SessionSaveMigrator";
|
||||
import type { BattlerIndex } from "#enums/battler-index";
|
||||
import type { TurnMove } from "#app/field/pokemon";
|
||||
import type { MoveResult } from "#enums/move-result";
|
||||
import type { SessionSaveData } from "#app/system/game-data";
|
||||
import { MoveUseMode } from "#enums/move-use-mode";
|
||||
import type { MoveId } from "#enums/move-id";
|
||||
|
||||
/** Prior signature of `TurnMove`; used to ensure parity */
|
||||
interface OldTurnMove {
|
||||
move: MoveId;
|
||||
targets: BattlerIndex[];
|
||||
result?: MoveResult;
|
||||
turn?: number;
|
||||
virtual?: boolean;
|
||||
ignorePP?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix player pokemon move history entries with updated `MoveUseModes`,
|
||||
* based on the prior values of `virtual` and `ignorePP`.
|
||||
* Needed to ensure Last Resort and move-calling moves still work OK.
|
||||
* @param data - {@linkcode SystemSaveData}
|
||||
*/
|
||||
const fixMoveHistory: SessionSaveMigrator = {
|
||||
version: "1.10.0",
|
||||
migrate: (data: SessionSaveData): void => {
|
||||
const mapTurnMove = (tm: OldTurnMove): TurnMove => ({
|
||||
move: tm.move,
|
||||
targets: tm.targets,
|
||||
result: tm.result,
|
||||
turn: tm.turn,
|
||||
// NOTE: This unfortuately has to mis-classify Dancer and Magic Bounce-induced moves as `FOLLOW_UP`,
|
||||
// given we previously had _no way_ of distinguishing them from follow-up moves post hoc.
|
||||
useMode: tm.virtual ? MoveUseMode.FOLLOW_UP : tm.ignorePP ? MoveUseMode.IGNORE_PP : MoveUseMode.NORMAL,
|
||||
});
|
||||
data.party.forEach(pkmn => {
|
||||
pkmn.summonData.moveHistory = (pkmn.summonData.moveHistory as OldTurnMove[]).map(mapTurnMove);
|
||||
pkmn.summonData.moveQueue = (pkmn.summonData.moveQueue as OldTurnMove[]).map(mapTurnMove);
|
||||
});
|
||||
data.enemyParty.forEach(pkmn => {
|
||||
pkmn.summonData.moveHistory = (pkmn.summonData.moveHistory as OldTurnMove[]).map(mapTurnMove);
|
||||
pkmn.summonData.moveQueue = (pkmn.summonData.moveQueue as OldTurnMove[]).map(mapTurnMove);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const sessionMigrators: Readonly<SessionSaveMigrator[]> = [fixMoveHistory] as const;
|
@ -56,10 +56,6 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler {
|
||||
protected defaultTextStyle: TextStyle = TextStyle.WINDOW;
|
||||
protected textContent: string;
|
||||
|
||||
constructor(mode: UiMode | null) {
|
||||
super(mode);
|
||||
}
|
||||
|
||||
abstract getWindowWidth(): number;
|
||||
|
||||
getWindowHeight(): number {
|
||||
|
@ -69,7 +69,7 @@ export default class AdminUiHandler extends FormModalUiHandler {
|
||||
case AdminMode.SEARCH:
|
||||
inputFieldConfigs.push({ label: "Username" });
|
||||
break;
|
||||
case AdminMode.ADMIN:
|
||||
case AdminMode.ADMIN: {
|
||||
const adminResult = this.adminResult ?? {
|
||||
username: "",
|
||||
discordId: "",
|
||||
@ -90,6 +90,7 @@ export default class AdminUiHandler extends FormModalUiHandler {
|
||||
inputFieldConfigs.push({ label: "Last played", isReadOnly: true });
|
||||
inputFieldConfigs.push({ label: "Registered", isReadOnly: true });
|
||||
break;
|
||||
}
|
||||
}
|
||||
return inputFieldConfigs;
|
||||
}
|
||||
|
@ -4,11 +4,8 @@ import { UiMode } from "#enums/ui-mode";
|
||||
import i18next from "i18next";
|
||||
import { Button } from "#enums/buttons";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import { ConfirmUiMode } from "#enums/confirm-ui-mode";
|
||||
|
||||
export default class ConfirmUiHandler extends AbstractOptionSelectUiHandler {
|
||||
private confirmUiMode: ConfirmUiMode;
|
||||
|
||||
public static readonly windowWidth: number = 48;
|
||||
|
||||
private switchCheck: boolean;
|
||||
@ -108,16 +105,7 @@ export default class ConfirmUiHandler extends AbstractOptionSelectUiHandler {
|
||||
|
||||
this.optionSelectContainer.setPosition(globalScene.game.canvas.width / 6 - 1 + xOffset, -48 + yOffset);
|
||||
|
||||
this.confirmUiMode = args.length >= 6 ? (args[5] as ConfirmUiMode) : ConfirmUiMode.DEFAULT_YES;
|
||||
|
||||
switch (this.confirmUiMode) {
|
||||
case ConfirmUiMode.DEFAULT_YES:
|
||||
this.setCursor(this.switchCheck ? this.switchCheckCursor : 0);
|
||||
break;
|
||||
case ConfirmUiMode.DEFAULT_NO:
|
||||
this.setCursor(this.switchCheck ? this.switchCheckCursor : 1);
|
||||
break;
|
||||
}
|
||||
this.setCursor(this.switchCheck ? this.switchCheckCursor : 0);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -169,12 +169,13 @@ export class DailyRunScoreboard extends Phaser.GameObjects.Container {
|
||||
entryContainer.add(scoreLabel);
|
||||
|
||||
switch (this.category) {
|
||||
case ScoreboardCategory.DAILY:
|
||||
case ScoreboardCategory.DAILY: {
|
||||
const waveLabel = addTextObject(68, 0, wave, TextStyle.WINDOW, {
|
||||
fontSize: "54px",
|
||||
});
|
||||
entryContainer.add(waveLabel);
|
||||
break;
|
||||
}
|
||||
case ScoreboardCategory.WEEKLY:
|
||||
scoreLabel.x -= 16;
|
||||
break;
|
||||
|
@ -131,7 +131,7 @@ export default class EggGachaUiHandler extends MessageUiHandler {
|
||||
gachaInfoContainer.add(gachaUpLabel);
|
||||
|
||||
switch (gachaType as GachaType) {
|
||||
case GachaType.LEGENDARY:
|
||||
case GachaType.LEGENDARY: {
|
||||
if (["de", "es-ES"].includes(currentLanguage)) {
|
||||
gachaUpLabel.setAlign("center");
|
||||
gachaUpLabel.setY(0);
|
||||
@ -152,6 +152,7 @@ export default class EggGachaUiHandler extends MessageUiHandler {
|
||||
|
||||
gachaInfoContainer.add(pokemonIcon);
|
||||
break;
|
||||
}
|
||||
case GachaType.MOVE:
|
||||
if (["de", "es-ES", "fr", "pt-BR", "ru"].includes(currentLanguage)) {
|
||||
gachaUpLabel.setAlign("center");
|
||||
@ -623,11 +624,12 @@ export default class EggGachaUiHandler extends MessageUiHandler {
|
||||
updateGachaInfo(gachaType: GachaType): void {
|
||||
const infoContainer = this.gachaInfoContainers[gachaType];
|
||||
switch (gachaType as GachaType) {
|
||||
case GachaType.LEGENDARY:
|
||||
case GachaType.LEGENDARY: {
|
||||
const species = getPokemonSpecies(getLegendaryGachaSpeciesForTimestamp(new Date().getTime()));
|
||||
const pokemonIcon = infoContainer.getAt(1) as Phaser.GameObjects.Sprite;
|
||||
pokemonIcon.setTexture(species.getIconAtlasKey(), species.getIconId(false));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,7 @@ import type Pokemon from "#app/field/pokemon";
|
||||
import type { CommandPhase } from "#app/phases/command-phase";
|
||||
import MoveInfoOverlay from "./move-info-overlay";
|
||||
import { BattleType } from "#enums/battle-type";
|
||||
import { MoveUseMode } from "#enums/move-use-mode";
|
||||
|
||||
export default class FightUiHandler extends UiHandler implements InfoToggle {
|
||||
public static readonly MOVES_CONTAINER_NAME = "moves";
|
||||
@ -139,53 +140,63 @@ export default class FightUiHandler extends UiHandler implements InfoToggle {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the player inputting the selected {@linkcode Button}.
|
||||
* @param button - The {@linkcode Button} being pressed
|
||||
* @returns Whether the input was successful (ie did anything).
|
||||
*/
|
||||
processInput(button: Button): boolean {
|
||||
const ui = this.getUi();
|
||||
|
||||
const cursor = this.getCursor();
|
||||
let success = false;
|
||||
|
||||
const cursor = this.getCursor();
|
||||
|
||||
if (button === Button.CANCEL || button === Button.ACTION) {
|
||||
if (button === Button.ACTION) {
|
||||
switch (button) {
|
||||
case Button.CANCEL:
|
||||
{
|
||||
// Attempts to back out of the move selection pane are blocked in certain MEs
|
||||
// TODO: Should we allow showing the summary menu at least?
|
||||
const { battleType, mysteryEncounter } = globalScene.currentBattle;
|
||||
if (battleType !== BattleType.MYSTERY_ENCOUNTER || !mysteryEncounter?.skipToFightInput) {
|
||||
ui.setMode(UiMode.COMMAND, this.fieldIndex);
|
||||
success = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case Button.ACTION:
|
||||
if (
|
||||
(globalScene.phaseManager.getCurrentPhase() as CommandPhase).handleCommand(this.fromCommand, cursor, false)
|
||||
(globalScene.phaseManager.getCurrentPhase() as CommandPhase).handleCommand(
|
||||
this.fromCommand,
|
||||
cursor,
|
||||
MoveUseMode.NORMAL,
|
||||
)
|
||||
) {
|
||||
success = true;
|
||||
} else {
|
||||
ui.playError();
|
||||
}
|
||||
} else {
|
||||
// Cannot back out of fight menu if skipToFightInput is enabled
|
||||
const { battleType, mysteryEncounter } = globalScene.currentBattle;
|
||||
if (battleType !== BattleType.MYSTERY_ENCOUNTER || !mysteryEncounter?.skipToFightInput) {
|
||||
ui.setMode(UiMode.COMMAND, this.fieldIndex);
|
||||
success = true;
|
||||
break;
|
||||
case Button.UP:
|
||||
if (cursor >= 2) {
|
||||
success = this.setCursor(cursor - 2);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
switch (button) {
|
||||
case Button.UP:
|
||||
if (cursor >= 2) {
|
||||
success = this.setCursor(cursor - 2);
|
||||
}
|
||||
break;
|
||||
case Button.DOWN:
|
||||
if (cursor < 2) {
|
||||
success = this.setCursor(cursor + 2);
|
||||
}
|
||||
break;
|
||||
case Button.LEFT:
|
||||
if (cursor % 2 === 1) {
|
||||
success = this.setCursor(cursor - 1);
|
||||
}
|
||||
break;
|
||||
case Button.RIGHT:
|
||||
if (cursor % 2 === 0) {
|
||||
success = this.setCursor(cursor + 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case Button.DOWN:
|
||||
if (cursor < 2) {
|
||||
success = this.setCursor(cursor + 2);
|
||||
}
|
||||
break;
|
||||
case Button.LEFT:
|
||||
if (cursor % 2 === 1) {
|
||||
success = this.setCursor(cursor - 1);
|
||||
}
|
||||
break;
|
||||
case Button.RIGHT:
|
||||
if (cursor % 2 === 0) {
|
||||
success = this.setCursor(cursor + 1);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// other inputs do nothing while in fight menu
|
||||
}
|
||||
|
||||
if (success) {
|
||||
|
@ -686,7 +686,7 @@ export default class MenuUiHandler extends MessageUiHandler {
|
||||
error = true;
|
||||
}
|
||||
break;
|
||||
case MenuOptions.LOG_OUT:
|
||||
case MenuOptions.LOG_OUT: {
|
||||
success = true;
|
||||
const doLogout = () => {
|
||||
ui.setMode(UiMode.LOADING, {
|
||||
@ -718,6 +718,7 @@ export default class MenuUiHandler extends MessageUiHandler {
|
||||
doLogout();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (button === Button.CANCEL) {
|
||||
success = true;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import type { PlayerPokemon } from "#app/field/pokemon";
|
||||
import type { PlayerPokemon, TurnMove } from "#app/field/pokemon";
|
||||
import type { PokemonMove } from "#app/data/moves/pokemon-move";
|
||||
import type Pokemon from "#app/field/pokemon";
|
||||
import { MoveResult } from "#enums/move-result";
|
||||
@ -1167,13 +1167,13 @@ export default class PartyUiHandler extends MessageUiHandler {
|
||||
}
|
||||
|
||||
// TODO: add FORCED_SWITCH (and perhaps also BATON_PASS_SWITCH) to the modes
|
||||
// TODO: refactor once moves in flight become a thing...
|
||||
private isBatonPassMove(): boolean {
|
||||
const moveHistory = globalScene.getPlayerField()[this.fieldIndex].getMoveHistory();
|
||||
return !!(
|
||||
const lastMove: TurnMove | undefined = globalScene.getPlayerField()[this.fieldIndex].getLastXMoves()[0];
|
||||
return (
|
||||
this.partyUiMode === PartyUiMode.FAINT_SWITCH &&
|
||||
moveHistory.length &&
|
||||
allMoves[moveHistory[moveHistory.length - 1].move].getAttrs("ForceSwitchOutAttr")[0]?.isBatonPass() &&
|
||||
moveHistory[moveHistory.length - 1].result === MoveResult.SUCCESS
|
||||
lastMove?.result === MoveResult.SUCCESS &&
|
||||
allMoves[lastMove.move].getAttrs("ForceSwitchOutAttr")[0]?.isBatonPass()
|
||||
);
|
||||
}
|
||||
|
||||
@ -1385,7 +1385,7 @@ export default class PartyUiHandler extends MessageUiHandler {
|
||||
case PartyOption.MOVE_1:
|
||||
case PartyOption.MOVE_2:
|
||||
case PartyOption.MOVE_3:
|
||||
case PartyOption.MOVE_4:
|
||||
case PartyOption.MOVE_4: {
|
||||
const move = pokemon.moveset[option - PartyOption.MOVE_1];
|
||||
if (this.showMovePp) {
|
||||
const maxPP = move.getMovePp();
|
||||
@ -1395,7 +1395,8 @@ export default class PartyUiHandler extends MessageUiHandler {
|
||||
optionName = move.getName();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
}
|
||||
default: {
|
||||
const formChangeItemModifiers = this.getFormChangeItemsModifiers(pokemon);
|
||||
if (formChangeItemModifiers && option >= PartyOption.FORM_CHANGE_ITEM) {
|
||||
const modifier = formChangeItemModifiers[option - PartyOption.FORM_CHANGE_ITEM];
|
||||
@ -1410,6 +1411,7 @@ export default class PartyUiHandler extends MessageUiHandler {
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (this.partyUiMode === PartyUiMode.REMEMBER_MOVE_MODIFIER) {
|
||||
const learnableLevelMoves = pokemon.getLearnableLevelMoves();
|
||||
|
@ -567,7 +567,7 @@ export default class RunInfoUiHandler extends UiHandler {
|
||||
case GameModes.SPLICED_ENDLESS:
|
||||
modeText.appendText(`${i18next.t("gameMode:endlessSpliced")}`, false);
|
||||
break;
|
||||
case GameModes.CHALLENGE:
|
||||
case GameModes.CHALLENGE: {
|
||||
modeText.appendText(`${i18next.t("gameMode:challenge")}`, false);
|
||||
modeText.appendText(`${i18next.t("runHistory:challengeRules")}: `);
|
||||
modeText.setWrapMode(1); // wrap by word
|
||||
@ -582,6 +582,7 @@ export default class RunInfoUiHandler extends UiHandler {
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case GameModes.ENDLESS:
|
||||
modeText.appendText(`${i18next.t("gameMode:endless")}`, false);
|
||||
break;
|
||||
@ -687,7 +688,7 @@ export default class RunInfoUiHandler extends UiHandler {
|
||||
case Challenges.SINGLE_GENERATION:
|
||||
rules.push(i18next.t(`runHistory:challengeMonoGen${this.runInfo.challenges[i].value}`));
|
||||
break;
|
||||
case Challenges.SINGLE_TYPE:
|
||||
case Challenges.SINGLE_TYPE: {
|
||||
const typeRule = PokemonType[this.runInfo.challenges[i].value - 1];
|
||||
const typeTextColor = `[color=${TypeColor[typeRule]}]`;
|
||||
const typeShadowColor = `[shadow=${TypeShadow[typeRule]}]`;
|
||||
@ -695,16 +696,18 @@ export default class RunInfoUiHandler extends UiHandler {
|
||||
typeTextColor + typeShadowColor + i18next.t(`pokemonInfo:Type.${typeRule}`)! + "[/color]" + "[/shadow]";
|
||||
rules.push(typeText);
|
||||
break;
|
||||
}
|
||||
case Challenges.INVERSE_BATTLE:
|
||||
rules.push(i18next.t("challenges:inverseBattle.shortName"));
|
||||
break;
|
||||
default:
|
||||
default: {
|
||||
const localisationKey = Challenges[this.runInfo.challenges[i].id]
|
||||
.split("_")
|
||||
.map((f, i) => (i ? `${f[0]}${f.slice(1).toLowerCase()}` : f.toLowerCase()))
|
||||
.join("");
|
||||
rules.push(i18next.t(`challenges:${localisationKey}.name`));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -126,6 +126,11 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler
|
||||
);
|
||||
this.actionsBg.setOrigin(0, 0);
|
||||
|
||||
/*
|
||||
* If there isn't enough space to fit all the icons and texts, there will be an overlap
|
||||
* This currently doesn't happen, but it's something to keep in mind.
|
||||
*/
|
||||
|
||||
const iconAction = globalScene.add.sprite(0, 0, "keyboard");
|
||||
iconAction.setOrigin(0, -0.1);
|
||||
iconAction.setPositionRelative(this.actionsBg, this.navigationContainer.width - 32, 4);
|
||||
@ -137,7 +142,7 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler
|
||||
|
||||
const iconCancel = globalScene.add.sprite(0, 0, "keyboard");
|
||||
iconCancel.setOrigin(0, -0.1);
|
||||
iconCancel.setPositionRelative(this.actionsBg, this.navigationContainer.width - 100, 4);
|
||||
iconCancel.setPositionRelative(this.actionsBg, actionText.x - 28, 4);
|
||||
this.navigationIcons["BUTTON_CANCEL"] = iconCancel;
|
||||
|
||||
const cancelText = addTextObject(0, 0, i18next.t("settings:back"), TextStyle.SETTINGS_LABEL);
|
||||
@ -146,7 +151,7 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler
|
||||
|
||||
const iconReset = globalScene.add.sprite(0, 0, "keyboard");
|
||||
iconReset.setOrigin(0, -0.1);
|
||||
iconReset.setPositionRelative(this.actionsBg, this.navigationContainer.width - 180, 4);
|
||||
iconReset.setPositionRelative(this.actionsBg, cancelText.x - 28, 4);
|
||||
this.navigationIcons["BUTTON_HOME"] = iconReset;
|
||||
|
||||
const resetText = addTextObject(0, 0, i18next.t("settings:reset"), TextStyle.SETTINGS_LABEL);
|
||||
|
@ -94,7 +94,7 @@ export default class AbstractSettingsUiHandler extends MessageUiHandler {
|
||||
|
||||
const iconCancel = globalScene.add.sprite(0, 0, "keyboard");
|
||||
iconCancel.setOrigin(0, -0.1);
|
||||
iconCancel.setPositionRelative(actionsBg, this.navigationContainer.width - 100, 4);
|
||||
iconCancel.setPositionRelative(actionsBg, actionText.x - 28, 4);
|
||||
this.navigationIcons["BUTTON_CANCEL"] = iconCancel;
|
||||
|
||||
const cancelText = addTextObject(0, 0, i18next.t("settings:back"), TextStyle.SETTINGS_LABEL);
|
||||
@ -332,12 +332,13 @@ export default class AbstractSettingsUiHandler extends MessageUiHandler {
|
||||
case Button.CYCLE_SHINY:
|
||||
success = this.navigationContainer.navigate(button);
|
||||
break;
|
||||
case Button.ACTION:
|
||||
case Button.ACTION: {
|
||||
const setting: Setting = this.settings[cursor];
|
||||
if (setting?.activatable) {
|
||||
success = this.activateSetting(setting);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -98,7 +98,7 @@ export default class MoveTouchControlsHandler {
|
||||
<div id="cancelButton" class="button">${i18next.t("settings:touchCancel")}</div>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<div class="orientation-label">
|
||||
<div class="orientation-label">
|
||||
${i18next.t("settings:orientation")}
|
||||
<span id="orientation">
|
||||
${this.isLandscapeMode ? i18next.t("settings:landscape") : i18next.t("settings:portrait")}
|
||||
|
@ -1763,7 +1763,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
|
||||
}
|
||||
} else if (this.randomCursorObj.visible) {
|
||||
switch (button) {
|
||||
case Button.ACTION:
|
||||
case Button.ACTION: {
|
||||
if (this.starterSpecies.length >= 6) {
|
||||
error = true;
|
||||
break;
|
||||
@ -1815,6 +1815,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
case Button.UP:
|
||||
this.randomCursorObj.setVisible(false);
|
||||
this.filterBarCursor = this.filterBar.numFilters - 1;
|
||||
|
@ -10,10 +10,6 @@ import { UiMode } from "#enums/ui-mode";
|
||||
export default class TestDialogueUiHandler extends FormModalUiHandler {
|
||||
keys: string[];
|
||||
|
||||
constructor(mode) {
|
||||
super(mode);
|
||||
}
|
||||
|
||||
setup() {
|
||||
super.setup();
|
||||
|
||||
|
@ -620,3 +620,25 @@ export function coerceArray<T>(input: T): T extends any[] ? T : [T];
|
||||
export function coerceArray<T>(input: T): T | [T] {
|
||||
return Array.isArray(input) ? input : [input];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the key that matches the enum [object] value.
|
||||
* @param input - The enum [object] to check
|
||||
* @param val - The value to get the key of
|
||||
* @returns The name of the key with the specified value
|
||||
* @example
|
||||
* const thing = {
|
||||
* one: 1,
|
||||
* two: 2,
|
||||
* } as const;
|
||||
* console.log(enumValueToKey(thing, thing.two)); // output: "two"
|
||||
* @throws An `Error` if an invalid enum value is passed to the function
|
||||
*/
|
||||
export function enumValueToKey<T extends Record<string, string | number>>(input: T, val: T[keyof T]): keyof T {
|
||||
for (const [key, value] of Object.entries(input)) {
|
||||
if (val === value) {
|
||||
return key as keyof T;
|
||||
}
|
||||
}
|
||||
throw new Error(`Invalid value passed to \`enumValueToKey\`! Value: ${val}`);
|
||||
}
|
||||
|
@ -50,5 +50,5 @@ describe("Ability Timing", () => {
|
||||
|
||||
await game.phaseInterceptor.to("MessagePhase");
|
||||
expect(i18next.t).toHaveBeenCalledWith("battle:statFell", expect.objectContaining({ count: 1 }));
|
||||
}, 5000);
|
||||
});
|
||||
});
|
||||
|
@ -26,11 +26,12 @@ describe("Abilities - Battery", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override.battleStyle("double");
|
||||
game.override.enemySpecies(SpeciesId.SHUCKLE);
|
||||
game.override.enemyAbility(AbilityId.BALL_FETCH);
|
||||
game.override.moveset([MoveId.TACKLE, MoveId.BREAKING_SWIPE, MoveId.SPLASH, MoveId.DAZZLING_GLEAM]);
|
||||
game.override.enemyMoveset(MoveId.SPLASH);
|
||||
game.override
|
||||
.battleStyle("double")
|
||||
.enemySpecies(SpeciesId.SHUCKLE)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.moveset([MoveId.TACKLE, MoveId.BREAKING_SWIPE, MoveId.SPLASH, MoveId.DAZZLING_GLEAM])
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
});
|
||||
|
||||
it("raises the power of allies' special moves by 30%", async () => {
|
||||
|
@ -47,7 +47,7 @@ describe("Abilities - Beast Boost", () => {
|
||||
await game.phaseInterceptor.to("VictoryPhase");
|
||||
|
||||
expect(playerPokemon.getStatStage(Stat.DEF)).toBe(1);
|
||||
}, 20000);
|
||||
});
|
||||
|
||||
it("should use in-battle overriden stats when determining the stat stage to raise by 1", async () => {
|
||||
game.override.enemyMoveset([MoveId.GUARD_SPLIT]);
|
||||
@ -66,7 +66,7 @@ describe("Abilities - Beast Boost", () => {
|
||||
await game.phaseInterceptor.to("VictoryPhase");
|
||||
|
||||
expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(1);
|
||||
}, 20000);
|
||||
});
|
||||
|
||||
it("should have order preference in case of stat ties", async () => {
|
||||
// Order preference follows the order of EFFECTIVE_STAT
|
||||
@ -84,5 +84,5 @@ describe("Abilities - Beast Boost", () => {
|
||||
await game.phaseInterceptor.to("VictoryPhase");
|
||||
|
||||
expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(1);
|
||||
}, 20000);
|
||||
});
|
||||
});
|
||||
|
@ -36,7 +36,7 @@ describe("Abilities - Contrary", () => {
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1);
|
||||
}, 20000);
|
||||
});
|
||||
|
||||
describe("With Clear Body", () => {
|
||||
it("should apply positive effects", async () => {
|
||||
|
@ -24,10 +24,11 @@ describe("Abilities - COSTAR", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override.battleStyle("double");
|
||||
game.override.ability(AbilityId.COSTAR);
|
||||
game.override.moveset([MoveId.SPLASH, MoveId.NASTY_PLOT]);
|
||||
game.override.enemyMoveset(MoveId.SPLASH);
|
||||
game.override
|
||||
.battleStyle("double")
|
||||
.ability(AbilityId.COSTAR)
|
||||
.moveset([MoveId.SPLASH, MoveId.NASTY_PLOT])
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
});
|
||||
|
||||
test("ability copies positive stat stages", async () => {
|
||||
|
@ -66,8 +66,7 @@ describe("Abilities - Disguise", () => {
|
||||
});
|
||||
|
||||
it("takes no damage from the first hit of a multihit move and transforms to Busted form, then takes damage from the second hit", async () => {
|
||||
game.override.moveset([MoveId.SURGING_STRIKES]);
|
||||
game.override.enemyLevel(5);
|
||||
game.override.moveset([MoveId.SURGING_STRIKES]).enemyLevel(5);
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const mimikyu = game.scene.getEnemyPokemon()!;
|
||||
@ -106,8 +105,7 @@ describe("Abilities - Disguise", () => {
|
||||
});
|
||||
|
||||
it("persists form change when switched out", async () => {
|
||||
game.override.enemyMoveset([MoveId.SHADOW_SNEAK]);
|
||||
game.override.starterSpecies(0);
|
||||
game.override.enemyMoveset([MoveId.SHADOW_SNEAK]).starterSpecies(0);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.MIMIKYU, SpeciesId.FURRET]);
|
||||
|
||||
@ -131,8 +129,7 @@ describe("Abilities - Disguise", () => {
|
||||
});
|
||||
|
||||
it("persists form change when wave changes with no arena reset", async () => {
|
||||
game.override.starterSpecies(0);
|
||||
game.override.starterForms({
|
||||
game.override.starterSpecies(0).starterForms({
|
||||
[SpeciesId.MIMIKYU]: bustedForm,
|
||||
});
|
||||
await game.classicMode.startBattle([SpeciesId.FURRET, SpeciesId.MIMIKYU]);
|
||||
@ -148,11 +145,12 @@ describe("Abilities - Disguise", () => {
|
||||
});
|
||||
|
||||
it("reverts to Disguised form on arena reset", async () => {
|
||||
game.override.startingWave(4);
|
||||
game.override.starterSpecies(SpeciesId.MIMIKYU);
|
||||
game.override.starterForms({
|
||||
[SpeciesId.MIMIKYU]: bustedForm,
|
||||
});
|
||||
game.override
|
||||
.startingWave(4)
|
||||
.starterSpecies(SpeciesId.MIMIKYU)
|
||||
.starterForms({
|
||||
[SpeciesId.MIMIKYU]: bustedForm,
|
||||
});
|
||||
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
@ -168,11 +166,12 @@ describe("Abilities - Disguise", () => {
|
||||
});
|
||||
|
||||
it("reverts to Disguised form on biome change when fainted", async () => {
|
||||
game.override.startingWave(10);
|
||||
game.override.starterSpecies(0);
|
||||
game.override.starterForms({
|
||||
[SpeciesId.MIMIKYU]: bustedForm,
|
||||
});
|
||||
game.override
|
||||
.startingWave(10)
|
||||
.starterSpecies(0)
|
||||
.starterForms({
|
||||
[SpeciesId.MIMIKYU]: bustedForm,
|
||||
});
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.MIMIKYU, SpeciesId.FURRET]);
|
||||
|
||||
@ -206,8 +205,7 @@ describe("Abilities - Disguise", () => {
|
||||
});
|
||||
|
||||
it("activates when Aerilate circumvents immunity to the move's base type", async () => {
|
||||
game.override.ability(AbilityId.AERILATE);
|
||||
game.override.moveset([MoveId.TACKLE]);
|
||||
game.override.ability(AbilityId.AERILATE).moveset([MoveId.TACKLE]);
|
||||
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
|
@ -44,7 +44,7 @@ describe("Abilities - Flash Fire", () => {
|
||||
game.move.select(MoveId.SPLASH);
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
expect(blissey.hp).toBe(blissey.getMaxHp());
|
||||
}, 20000);
|
||||
});
|
||||
|
||||
it("not activate if the Pokémon is protected from the Fire-type move", async () => {
|
||||
game.override.enemyMoveset([MoveId.EMBER]).moveset([MoveId.PROTECT]);
|
||||
@ -55,7 +55,7 @@ describe("Abilities - Flash Fire", () => {
|
||||
game.move.select(MoveId.PROTECT);
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
expect(blissey!.getTag(BattlerTagType.FIRE_BOOST)).toBeUndefined();
|
||||
}, 20000);
|
||||
});
|
||||
|
||||
it("activated by Will-O-Wisp", async () => {
|
||||
game.override.enemyMoveset([MoveId.WILL_O_WISP]).moveset(MoveId.SPLASH);
|
||||
@ -70,11 +70,10 @@ describe("Abilities - Flash Fire", () => {
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
expect(blissey!.getTag(BattlerTagType.FIRE_BOOST)).toBeDefined();
|
||||
}, 20000);
|
||||
});
|
||||
|
||||
it("activated after being frozen", async () => {
|
||||
game.override.enemyMoveset([MoveId.EMBER]).moveset(MoveId.SPLASH);
|
||||
game.override.statusEffect(StatusEffect.FREEZE);
|
||||
game.override.enemyMoveset([MoveId.EMBER]).moveset(MoveId.SPLASH).statusEffect(StatusEffect.FREEZE);
|
||||
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
||||
|
||||
const blissey = game.scene.getPlayerPokemon()!;
|
||||
@ -83,7 +82,7 @@ describe("Abilities - Flash Fire", () => {
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
expect(blissey!.getTag(BattlerTagType.FIRE_BOOST)).toBeDefined();
|
||||
}, 20000);
|
||||
});
|
||||
|
||||
it("not passing with baton pass", async () => {
|
||||
game.override.enemyMoveset([MoveId.EMBER]).moveset([MoveId.BATON_PASS]);
|
||||
@ -99,11 +98,14 @@ describe("Abilities - Flash Fire", () => {
|
||||
const chansey = game.scene.getPlayerPokemon()!;
|
||||
expect(game.scene.getPlayerPokemon()!.species.speciesId).toBe(SpeciesId.CHANSEY);
|
||||
expect(chansey!.getTag(BattlerTagType.FIRE_BOOST)).toBeUndefined();
|
||||
}, 20000);
|
||||
});
|
||||
|
||||
it("boosts Fire-type move when the ability is activated", async () => {
|
||||
game.override.enemyMoveset([MoveId.FIRE_PLEDGE]).moveset([MoveId.EMBER, MoveId.SPLASH]);
|
||||
game.override.enemyAbility(AbilityId.FLASH_FIRE).ability(AbilityId.NONE);
|
||||
game.override
|
||||
.enemyMoveset([MoveId.FIRE_PLEDGE])
|
||||
.moveset([MoveId.EMBER, MoveId.SPLASH])
|
||||
.enemyAbility(AbilityId.FLASH_FIRE)
|
||||
.ability(AbilityId.NONE);
|
||||
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
||||
const blissey = game.scene.getPlayerPokemon()!;
|
||||
const initialHP = 1000;
|
||||
@ -124,12 +126,15 @@ describe("Abilities - Flash Fire", () => {
|
||||
const flashFireDmg = initialHP - blissey.hp;
|
||||
|
||||
expect(flashFireDmg).toBeGreaterThan(originalDmg);
|
||||
}, 20000);
|
||||
});
|
||||
|
||||
it("still activates regardless of accuracy check", async () => {
|
||||
game.override.moveset(MoveId.FIRE_PLEDGE).enemyMoveset(MoveId.EMBER);
|
||||
game.override.enemyAbility(AbilityId.NONE).ability(AbilityId.FLASH_FIRE);
|
||||
game.override.enemySpecies(SpeciesId.BLISSEY);
|
||||
game.override
|
||||
.moveset(MoveId.FIRE_PLEDGE)
|
||||
.enemyMoveset(MoveId.EMBER)
|
||||
.enemyAbility(AbilityId.NONE)
|
||||
.ability(AbilityId.FLASH_FIRE)
|
||||
.enemySpecies(SpeciesId.BLISSEY);
|
||||
await game.classicMode.startBattle([SpeciesId.RATTATA]);
|
||||
|
||||
const blissey = game.scene.getEnemyPokemon()!;
|
||||
@ -153,5 +158,5 @@ describe("Abilities - Flash Fire", () => {
|
||||
const flashFireDmg = initialHP - blissey.hp;
|
||||
|
||||
expect(flashFireDmg).toBeGreaterThan(originalDmg);
|
||||
}, 20000);
|
||||
});
|
||||
});
|
||||
|
@ -47,9 +47,10 @@ describe("Abilities - Flower Gift", () => {
|
||||
allyAbility = AbilityId.BALL_FETCH,
|
||||
enemyAbility = AbilityId.BALL_FETCH,
|
||||
): Promise<[number, number]> => {
|
||||
game.override.battleStyle("double");
|
||||
game.override.moveset([MoveId.SPLASH, MoveId.SUNNY_DAY, move, MoveId.HEAL_PULSE]);
|
||||
game.override.enemyMoveset([MoveId.SPLASH, MoveId.HEAL_PULSE]);
|
||||
game.override
|
||||
.battleStyle("double")
|
||||
.moveset([MoveId.SPLASH, MoveId.SUNNY_DAY, move, MoveId.HEAL_PULSE])
|
||||
.enemyMoveset([MoveId.SPLASH, MoveId.HEAL_PULSE]);
|
||||
const target_index = allyAttacker ? BattlerIndex.ENEMY : BattlerIndex.PLAYER_2;
|
||||
const attacker_index = allyAttacker ? BattlerIndex.PLAYER_2 : BattlerIndex.ENEMY;
|
||||
const ally_move = allyAttacker ? move : MoveId.SPLASH;
|
||||
|
@ -89,7 +89,6 @@ describe("Abilities - Flower Veil", () => {
|
||||
await game.move.selectEnemyMove(MoveId.THUNDER_WAVE);
|
||||
await game.toNextTurn();
|
||||
expect(game.scene.getPlayerPokemon()!.status).toBeUndefined();
|
||||
vi.spyOn(allMoves[MoveId.THUNDER_WAVE], "accuracy", "get").mockClear();
|
||||
});
|
||||
|
||||
it("should not prevent status conditions for a non-grass user and its non-grass allies", async () => {
|
||||
|
@ -63,9 +63,10 @@ describe("Abilities - Good As Gold", () => {
|
||||
});
|
||||
|
||||
it("should not block any status moves that target the field, one side, or all pokemon", async () => {
|
||||
game.override.battleStyle("double");
|
||||
game.override.enemyMoveset([MoveId.STEALTH_ROCK, MoveId.HAZE]);
|
||||
game.override.moveset([MoveId.SWORDS_DANCE, MoveId.SAFEGUARD]);
|
||||
game.override
|
||||
.battleStyle("double")
|
||||
.enemyMoveset([MoveId.STEALTH_ROCK, MoveId.HAZE])
|
||||
.moveset([MoveId.SWORDS_DANCE, MoveId.SAFEGUARD]);
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.FEEBAS]);
|
||||
const [good_as_gold, ball_fetch] = game.scene.getPlayerField();
|
||||
|
||||
@ -85,8 +86,7 @@ describe("Abilities - Good As Gold", () => {
|
||||
});
|
||||
|
||||
it("should not block field targeted effects in singles", async () => {
|
||||
game.override.battleStyle("single");
|
||||
game.override.enemyMoveset([MoveId.SPIKES]);
|
||||
game.override.battleStyle("single").enemyMoveset([MoveId.SPIKES]);
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
|
||||
game.move.select(MoveId.SPLASH, 0);
|
||||
@ -96,8 +96,7 @@ describe("Abilities - Good As Gold", () => {
|
||||
});
|
||||
|
||||
it("should block the ally's helping hand", async () => {
|
||||
game.override.battleStyle("double");
|
||||
game.override.moveset([MoveId.HELPING_HAND, MoveId.TACKLE]);
|
||||
game.override.battleStyle("double").moveset([MoveId.HELPING_HAND, MoveId.TACKLE]);
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.FEEBAS]);
|
||||
|
||||
game.move.select(MoveId.HELPING_HAND, 0);
|
||||
@ -129,8 +128,7 @@ describe("Abilities - Good As Gold", () => {
|
||||
});
|
||||
|
||||
it("should not block field targeted effects like rain dance", async () => {
|
||||
game.override.battleStyle("single");
|
||||
game.override.enemyMoveset([MoveId.RAIN_DANCE]);
|
||||
game.override.battleStyle("single").enemyMoveset([MoveId.RAIN_DANCE]);
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
|
||||
game.move.use(MoveId.SPLASH, 0);
|
||||
|
@ -1,15 +1,19 @@
|
||||
import { BattlerIndex } from "#enums/battler-index";
|
||||
import { RandomMoveAttr } from "#app/data/moves/move";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { Stat } from "#app/enums/stat";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import GameManager from "#test/testUtils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { MoveResult } from "#enums/move-result";
|
||||
import { MoveUseMode } from "#enums/move-use-mode";
|
||||
|
||||
describe("Abilities - Gorilla Tactics", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
@ -25,10 +29,9 @@ describe("Abilities - Gorilla Tactics", () => {
|
||||
game.override
|
||||
.battleStyle("single")
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.enemyMoveset([MoveId.SPLASH, MoveId.DISABLE])
|
||||
.enemySpecies(SpeciesId.MAGIKARP)
|
||||
.enemyLevel(30)
|
||||
.moveset([MoveId.SPLASH, MoveId.TACKLE, MoveId.GROWL])
|
||||
.moveset([MoveId.SPLASH, MoveId.TACKLE, MoveId.GROWL, MoveId.METRONOME])
|
||||
.ability(AbilityId.GORILLA_TACTICS);
|
||||
});
|
||||
|
||||
@ -39,9 +42,8 @@ describe("Abilities - Gorilla Tactics", () => {
|
||||
const initialAtkStat = darmanitan.getStat(Stat.ATK);
|
||||
|
||||
game.move.select(MoveId.SPLASH);
|
||||
await game.move.selectEnemyMove(MoveId.SPLASH);
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
await game.move.forceEnemyMove(MoveId.SPLASH);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(darmanitan.getStat(Stat.ATK, false)).toBeCloseTo(initialAtkStat * 1.5);
|
||||
// Other moves should be restricted
|
||||
@ -52,32 +54,50 @@ describe("Abilities - Gorilla Tactics", () => {
|
||||
it("should struggle if the only usable move is disabled", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.GALAR_DARMANITAN]);
|
||||
|
||||
const darmanitan = game.scene.getPlayerPokemon()!;
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
const darmanitan = game.field.getPlayerPokemon();
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
|
||||
// First turn, lock move to Growl
|
||||
game.move.select(MoveId.GROWL);
|
||||
await game.move.selectEnemyMove(MoveId.SPLASH);
|
||||
|
||||
// Second turn, Growl is interrupted by Disable
|
||||
await game.move.forceEnemyMove(MoveId.SPLASH);
|
||||
await game.toNextTurn();
|
||||
|
||||
// Second turn, Growl is interrupted by Disable
|
||||
game.move.select(MoveId.GROWL);
|
||||
await game.move.selectEnemyMove(MoveId.DISABLE);
|
||||
await game.move.forceEnemyMove(MoveId.DISABLE);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.toNextTurn();
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(enemy.getStatStage(Stat.ATK)).toBe(-1); // Only the effect of the first Growl should be applied
|
||||
|
||||
// Third turn, Struggle is used
|
||||
await game.toNextTurn();
|
||||
|
||||
game.move.select(MoveId.TACKLE);
|
||||
await game.move.forceEnemyMove(MoveId.SPLASH); //prevent protect from being used by the enemy
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
|
||||
expect(darmanitan.hp).toBeLessThan(darmanitan.getMaxHp());
|
||||
|
||||
await game.toNextTurn();
|
||||
expect(darmanitan.getLastXMoves()[0].move).toBe(MoveId.STRUGGLE);
|
||||
});
|
||||
|
||||
it("should lock into calling moves, even if also in moveset", async () => {
|
||||
vi.spyOn(RandomMoveAttr.prototype, "getMoveOverride").mockReturnValue(MoveId.TACKLE);
|
||||
await game.classicMode.startBattle([SpeciesId.GALAR_DARMANITAN]);
|
||||
|
||||
const darmanitan = game.scene.getPlayerPokemon()!;
|
||||
|
||||
game.move.select(MoveId.METRONOME);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
// Gorilla Tactics should bypass dancer and instruct
|
||||
expect(darmanitan.isMoveRestricted(MoveId.TACKLE)).toBe(true);
|
||||
expect(darmanitan.isMoveRestricted(MoveId.METRONOME)).toBe(false);
|
||||
expect(darmanitan.getLastXMoves(-1)).toEqual([
|
||||
expect.objectContaining({ move: MoveId.TACKLE, result: MoveResult.SUCCESS, useMode: MoveUseMode.FOLLOW_UP }),
|
||||
expect.objectContaining({ move: MoveId.METRONOME, result: MoveResult.SUCCESS, useMode: MoveUseMode.NORMAL }),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should activate when the opponenet protects", async () => {
|
||||
|
@ -4,17 +4,15 @@ import { SpeciesId } from "#enums/species-id";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import GameManager from "#test/testUtils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi, type MockInstance } from "vitest";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { isNullOrUndefined } from "#app/utils/common";
|
||||
import { allAbilities } from "#app/data/data-lists";
|
||||
import type Pokemon from "#app/field/pokemon";
|
||||
import type { PostTurnResetStatusAbAttr } from "#app/@types/ability-types";
|
||||
import { PostTurnResetStatusAbAttr } from "#app/data/abilities/ability";
|
||||
|
||||
describe("Abilities - Healer", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
let healerAttrSpy: MockInstance;
|
||||
let healerAttr: PostTurnResetStatusAbAttr;
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
@ -24,7 +22,6 @@ describe("Abilities - Healer", () => {
|
||||
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
healerAttrSpy.mockRestore();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
@ -38,30 +35,28 @@ describe("Abilities - Healer", () => {
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
|
||||
healerAttr = allAbilities[AbilityId.HEALER].getAttrs("PostTurnResetStatusAbAttr")[0];
|
||||
healerAttrSpy = vi
|
||||
.spyOn(healerAttr, "getCondition")
|
||||
.mockReturnValue((pokemon: Pokemon) => !isNullOrUndefined(pokemon.getAlly()));
|
||||
// Mock healer to have a 100% chance of healing its ally
|
||||
vi.spyOn(allAbilities[AbilityId.HEALER].getAttrs("PostTurnResetStatusAbAttr")[0], "getCondition").mockReturnValue(
|
||||
(pokemon: Pokemon) => !isNullOrUndefined(pokemon.getAlly()),
|
||||
);
|
||||
});
|
||||
|
||||
it("should not queue a message phase for healing if the ally has fainted", async () => {
|
||||
const abSpy = vi.spyOn(PostTurnResetStatusAbAttr.prototype, "canApplyPostTurn");
|
||||
game.override.moveset([MoveId.SPLASH, MoveId.LUNAR_DANCE]);
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MAGIKARP]);
|
||||
|
||||
const user = game.scene.getPlayerPokemon()!;
|
||||
// Only want one magikarp to have the ability.
|
||||
// Only want one magikarp to have the ability
|
||||
vi.spyOn(user, "getAbility").mockReturnValue(allAbilities[AbilityId.HEALER]);
|
||||
game.move.select(MoveId.SPLASH);
|
||||
// faint the ally
|
||||
game.move.select(MoveId.LUNAR_DANCE, 1);
|
||||
const abSpy = vi.spyOn(healerAttr, "canApplyPostTurn");
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
// It's not enough to just test that the ally still has its status.
|
||||
// We need to ensure that the ability failed to meet its condition
|
||||
expect(abSpy).toHaveReturnedWith(false);
|
||||
|
||||
// Explicitly restore the mock to ensure pollution doesn't happen
|
||||
abSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("should heal the status of an ally if the ally has a status", async () => {
|
||||
|
@ -75,8 +75,7 @@ describe("Abilities - Hustle", () => {
|
||||
});
|
||||
|
||||
it("does not affect OHKO moves", async () => {
|
||||
game.override.startingLevel(100);
|
||||
game.override.enemyLevel(30);
|
||||
game.override.startingLevel(100).enemyLevel(30);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.PIKACHU]);
|
||||
const pikachu = game.scene.getPlayerPokemon()!;
|
||||
|
@ -30,10 +30,11 @@ describe("Abilities - Ice Face", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override.battleStyle("single");
|
||||
game.override.enemySpecies(SpeciesId.EISCUE);
|
||||
game.override.enemyAbility(AbilityId.ICE_FACE);
|
||||
game.override.moveset([MoveId.TACKLE, MoveId.ICE_BEAM, MoveId.TOXIC_THREAD, MoveId.HAIL]);
|
||||
game.override
|
||||
.battleStyle("single")
|
||||
.enemySpecies(SpeciesId.EISCUE)
|
||||
.enemyAbility(AbilityId.ICE_FACE)
|
||||
.moveset([MoveId.TACKLE, MoveId.ICE_BEAM, MoveId.TOXIC_THREAD, MoveId.HAIL]);
|
||||
});
|
||||
|
||||
it("takes no damage from physical move and transforms to Noice", async () => {
|
||||
@ -51,8 +52,7 @@ describe("Abilities - Ice Face", () => {
|
||||
});
|
||||
|
||||
it("takes no damage from the first hit of multihit physical move and transforms to Noice", async () => {
|
||||
game.override.moveset([MoveId.SURGING_STRIKES]);
|
||||
game.override.enemyLevel(1);
|
||||
game.override.moveset([MoveId.SURGING_STRIKES]).enemyLevel(1);
|
||||
await game.classicMode.startBattle([SpeciesId.HITMONLEE]);
|
||||
|
||||
game.move.select(MoveId.SURGING_STRIKES);
|
||||
@ -196,12 +196,13 @@ describe("Abilities - Ice Face", () => {
|
||||
});
|
||||
|
||||
it("reverts to Ice Face on arena reset", async () => {
|
||||
game.override.startingWave(4);
|
||||
game.override.startingLevel(4);
|
||||
game.override.enemySpecies(SpeciesId.MAGIKARP);
|
||||
game.override.starterForms({
|
||||
[SpeciesId.EISCUE]: noiceForm,
|
||||
});
|
||||
game.override
|
||||
.startingWave(4)
|
||||
.startingLevel(4)
|
||||
.enemySpecies(SpeciesId.MAGIKARP)
|
||||
.starterForms({
|
||||
[SpeciesId.EISCUE]: noiceForm,
|
||||
});
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.EISCUE]);
|
||||
|
||||
|
@ -116,26 +116,23 @@ describe("Abilities - Illusion", () => {
|
||||
expect(psychicEffectiveness).above(flameThrowerEffectiveness);
|
||||
});
|
||||
|
||||
it("does not break from indirect damage", async () => {
|
||||
game.override.enemySpecies(SpeciesId.GIGALITH);
|
||||
game.override.enemyAbility(AbilityId.SAND_STREAM);
|
||||
game.override.enemyMoveset(MoveId.WILL_O_WISP);
|
||||
game.override.moveset([MoveId.FLARE_BLITZ]);
|
||||
it("should not break from indirect damage from status, weather or recoil", async () => {
|
||||
game.override.enemySpecies(SpeciesId.GIGALITH).enemyAbility(AbilityId.SAND_STREAM);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.ZOROARK, SpeciesId.AZUMARILL]);
|
||||
|
||||
game.move.select(MoveId.FLARE_BLITZ);
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
game.move.use(MoveId.FLARE_BLITZ);
|
||||
await game.move.forceEnemyMove(MoveId.WILL_O_WISP);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
const zoroark = game.scene.getPlayerPokemon()!;
|
||||
|
||||
expect(!!zoroark.summonData.illusion).equals(true);
|
||||
});
|
||||
|
||||
it("copies the the name, nickname, gender, shininess, and pokeball from the illusion source", async () => {
|
||||
game.override.enemyMoveset(MoveId.SPLASH);
|
||||
await game.classicMode.startBattle([SpeciesId.ABRA, SpeciesId.ZOROARK, SpeciesId.AXEW]);
|
||||
|
||||
const axew = game.scene.getPlayerParty().at(2)!;
|
||||
axew.shiny = true;
|
||||
axew.nickname = btoa(unescape(encodeURIComponent("axew nickname")));
|
||||
|
@ -162,8 +162,7 @@ describe("Abilities - Imposter", () => {
|
||||
});
|
||||
|
||||
it("should stay transformed with the correct form after reload", async () => {
|
||||
game.override.moveset([MoveId.ABSORB]);
|
||||
game.override.enemySpecies(SpeciesId.UNOWN);
|
||||
game.override.moveset([MoveId.ABSORB]).enemySpecies(SpeciesId.UNOWN);
|
||||
await game.classicMode.startBattle([SpeciesId.DITTO]);
|
||||
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
|
@ -62,7 +62,7 @@ describe("Abilities - Intimidate", () => {
|
||||
expect(playerPokemon.species.speciesId).toBe(SpeciesId.POOCHYENA);
|
||||
expect(playerPokemon.getStatStage(Stat.ATK)).toBe(0);
|
||||
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-2);
|
||||
}, 20000);
|
||||
});
|
||||
|
||||
it("should lower ATK stat stage by 1 for every enemy Pokemon in a double battle on entry", async () => {
|
||||
game.override.battleStyle("double").startingWave(3);
|
||||
@ -85,11 +85,10 @@ describe("Abilities - Intimidate", () => {
|
||||
expect(enemyField[1].getStatStage(Stat.ATK)).toBe(-2);
|
||||
expect(playerField[0].getStatStage(Stat.ATK)).toBe(-2);
|
||||
expect(playerField[1].getStatStage(Stat.ATK)).toBe(-2);
|
||||
}, 20000);
|
||||
});
|
||||
|
||||
it("should not activate again if there is no switch or new entry", async () => {
|
||||
game.override.startingWave(2);
|
||||
game.override.moveset([MoveId.SPLASH]);
|
||||
game.override.startingWave(2).moveset([MoveId.SPLASH]);
|
||||
await game.classicMode.startBattle([SpeciesId.MIGHTYENA, SpeciesId.POOCHYENA]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
@ -103,7 +102,7 @@ describe("Abilities - Intimidate", () => {
|
||||
|
||||
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-1);
|
||||
expect(playerPokemon.getStatStage(Stat.ATK)).toBe(-1);
|
||||
}, 20000);
|
||||
});
|
||||
|
||||
it("should lower ATK stat stage by 1 for every switch", async () => {
|
||||
game.override.moveset([MoveId.SPLASH]).enemyMoveset([MoveId.VOLT_SWITCH]).startingWave(5);
|
||||
@ -130,5 +129,5 @@ describe("Abilities - Intimidate", () => {
|
||||
|
||||
expect(playerPokemon.getStatStage(Stat.ATK)).toBe(-3);
|
||||
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0);
|
||||
}, 200000);
|
||||
});
|
||||
});
|
||||
|
@ -22,10 +22,11 @@ describe("Abilities - Intrepid Sword", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override.battleStyle("single");
|
||||
game.override.enemySpecies(SpeciesId.ZACIAN);
|
||||
game.override.enemyAbility(AbilityId.INTREPID_SWORD);
|
||||
game.override.ability(AbilityId.INTREPID_SWORD);
|
||||
game.override
|
||||
.battleStyle("single")
|
||||
.enemySpecies(SpeciesId.ZACIAN)
|
||||
.enemyAbility(AbilityId.INTREPID_SWORD)
|
||||
.ability(AbilityId.INTREPID_SWORD);
|
||||
});
|
||||
|
||||
it("should raise ATK stat stage by 1 on entry", async () => {
|
||||
@ -38,5 +39,5 @@ describe("Abilities - Intrepid Sword", () => {
|
||||
|
||||
expect(playerPokemon.getStatStage(Stat.ATK)).toBe(1);
|
||||
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1);
|
||||
}, 20000);
|
||||
});
|
||||
});
|
||||
|
@ -108,8 +108,7 @@ describe("Abilities - Libero", () => {
|
||||
});
|
||||
|
||||
test("ability applies correctly even if the type has changed by another ability", async () => {
|
||||
game.override.moveset([MoveId.TACKLE]);
|
||||
game.override.passiveAbility(AbilityId.REFRIGERATE);
|
||||
game.override.moveset([MoveId.TACKLE]).passiveAbility(AbilityId.REFRIGERATE);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
|
||||
@ -156,8 +155,7 @@ describe("Abilities - Libero", () => {
|
||||
});
|
||||
|
||||
test("ability applies correctly even if the pokemon's move misses", async () => {
|
||||
game.override.moveset([MoveId.TACKLE]);
|
||||
game.override.enemyMoveset(MoveId.SPLASH);
|
||||
game.override.moveset([MoveId.TACKLE]).enemyMoveset(MoveId.SPLASH);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
|
||||
@ -188,8 +186,7 @@ describe("Abilities - Libero", () => {
|
||||
});
|
||||
|
||||
test("ability applies correctly even if the pokemon's move fails because of type immunity", async () => {
|
||||
game.override.moveset([MoveId.TACKLE]);
|
||||
game.override.enemySpecies(SpeciesId.GASTLY);
|
||||
game.override.moveset([MoveId.TACKLE]).enemySpecies(SpeciesId.GASTLY);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
|
||||
@ -262,8 +259,7 @@ describe("Abilities - Libero", () => {
|
||||
});
|
||||
|
||||
test("ability applies correctly even if the pokemon's Trick-or-Treat fails", async () => {
|
||||
game.override.moveset([MoveId.TRICK_OR_TREAT]);
|
||||
game.override.enemySpecies(SpeciesId.GASTLY);
|
||||
game.override.moveset([MoveId.TRICK_OR_TREAT]).enemySpecies(SpeciesId.GASTLY);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
|
||||
|
@ -41,18 +41,16 @@ describe("Abilities - Magic Bounce", () => {
|
||||
it("should reflect basic status moves", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
|
||||
game.move.select(MoveId.GROWL);
|
||||
game.move.use(MoveId.GROWL);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(-1);
|
||||
});
|
||||
|
||||
it("should not bounce moves while the target is in the semi-invulnerable state", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
game.override.moveset([MoveId.GROWL]);
|
||||
game.override.enemyMoveset([MoveId.FLY]);
|
||||
|
||||
game.move.select(MoveId.GROWL);
|
||||
await game.move.selectEnemyMove(MoveId.FLY);
|
||||
game.move.use(MoveId.GROWL);
|
||||
await game.move.forceEnemyMove(MoveId.FLY);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
@ -61,11 +59,10 @@ describe("Abilities - Magic Bounce", () => {
|
||||
|
||||
it("should individually bounce back multi-target moves", async () => {
|
||||
game.override.battleStyle("double");
|
||||
game.override.moveset([MoveId.GROWL, MoveId.SPLASH]);
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MAGIKARP]);
|
||||
|
||||
game.move.select(MoveId.GROWL, 0);
|
||||
game.move.select(MoveId.SPLASH, 1);
|
||||
game.move.use(MoveId.GROWL, 0);
|
||||
game.move.use(MoveId.SPLASH, 1);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
const user = game.scene.getPlayerField()[0];
|
||||
@ -75,9 +72,8 @@ describe("Abilities - Magic Bounce", () => {
|
||||
it("should still bounce back a move that would otherwise fail", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
game.scene.getEnemyPokemon()?.setStatStage(Stat.ATK, -6);
|
||||
game.override.moveset([MoveId.GROWL]);
|
||||
|
||||
game.move.select(MoveId.GROWL);
|
||||
game.move.use(MoveId.GROWL);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(-1);
|
||||
@ -107,29 +103,26 @@ describe("Abilities - Magic Bounce", () => {
|
||||
game.override.ability(AbilityId.MOLD_BREAKER);
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
|
||||
game.move.select(MoveId.GROWL);
|
||||
game.move.use(MoveId.GROWL);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.ATK)).toBe(-1);
|
||||
});
|
||||
|
||||
it("should bounce back a spread status move against both pokemon", async () => {
|
||||
game.override.battleStyle("double");
|
||||
game.override.moveset([MoveId.GROWL, MoveId.SPLASH]);
|
||||
game.override.enemyMoveset([MoveId.SPLASH]);
|
||||
game.override.battleStyle("double").enemyMoveset([MoveId.SPLASH]);
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MAGIKARP]);
|
||||
|
||||
game.move.select(MoveId.GROWL, 0);
|
||||
game.move.select(MoveId.SPLASH, 1);
|
||||
game.move.use(MoveId.GROWL, 0);
|
||||
game.move.use(MoveId.SPLASH, 1);
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
expect(game.scene.getPlayerField().every(p => p.getStatStage(Stat.ATK) === -2)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should only bounce spikes back once in doubles when both targets have magic bounce", async () => {
|
||||
game.override.battleStyle("double");
|
||||
game.override.battleStyle("double").moveset([MoveId.SPIKES]);
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
game.override.moveset([MoveId.SPIKES]);
|
||||
|
||||
game.move.select(MoveId.SPIKES);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
@ -139,8 +132,7 @@ describe("Abilities - Magic Bounce", () => {
|
||||
});
|
||||
|
||||
it("should bounce spikes even when the target is protected", async () => {
|
||||
game.override.moveset([MoveId.SPIKES]);
|
||||
game.override.enemyMoveset([MoveId.PROTECT]);
|
||||
game.override.moveset([MoveId.SPIKES]).enemyMoveset([MoveId.PROTECT]);
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
|
||||
game.move.select(MoveId.SPIKES);
|
||||
@ -149,8 +141,7 @@ describe("Abilities - Magic Bounce", () => {
|
||||
});
|
||||
|
||||
it("should not bounce spikes when the target is in the semi-invulnerable state", async () => {
|
||||
game.override.moveset([MoveId.SPIKES]);
|
||||
game.override.enemyMoveset([MoveId.FLY]);
|
||||
game.override.moveset([MoveId.SPIKES]).enemyMoveset([MoveId.FLY]);
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
|
||||
game.move.select(MoveId.SPIKES);
|
||||
@ -160,9 +151,8 @@ describe("Abilities - Magic Bounce", () => {
|
||||
});
|
||||
|
||||
it("should not bounce back curse", async () => {
|
||||
game.override.starterSpecies(SpeciesId.GASTLY);
|
||||
await game.classicMode.startBattle([SpeciesId.GASTLY]);
|
||||
game.override.moveset([MoveId.CURSE]);
|
||||
await game.classicMode.startBattle([SpeciesId.GASTLY]);
|
||||
|
||||
game.move.select(MoveId.CURSE);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
@ -171,8 +161,7 @@ describe("Abilities - Magic Bounce", () => {
|
||||
});
|
||||
|
||||
it("should not cause encore to be interrupted after bouncing", async () => {
|
||||
game.override.moveset([MoveId.SPLASH, MoveId.GROWL, MoveId.ENCORE]);
|
||||
game.override.enemyMoveset([MoveId.TACKLE, MoveId.GROWL]);
|
||||
game.override.moveset([MoveId.SPLASH, MoveId.GROWL, MoveId.ENCORE]).enemyMoveset([MoveId.TACKLE, MoveId.GROWL]);
|
||||
// game.override.ability(AbilityId.MOLD_BREAKER);
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
@ -199,9 +188,10 @@ describe("Abilities - Magic Bounce", () => {
|
||||
|
||||
// TODO: encore is failing if the last move was virtual.
|
||||
it.todo("should not cause the bounced move to count for encore", async () => {
|
||||
game.override.moveset([MoveId.SPLASH, MoveId.GROWL, MoveId.ENCORE]);
|
||||
game.override.enemyMoveset([MoveId.GROWL, MoveId.TACKLE]);
|
||||
game.override.enemyAbility(AbilityId.MAGIC_BOUNCE);
|
||||
game.override
|
||||
.moveset([MoveId.SPLASH, MoveId.GROWL, MoveId.ENCORE])
|
||||
.enemyMoveset([MoveId.GROWL, MoveId.TACKLE])
|
||||
.enemyAbility(AbilityId.MAGIC_BOUNCE);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
@ -227,9 +217,8 @@ describe("Abilities - Magic Bounce", () => {
|
||||
|
||||
// TODO: stomping tantrum should consider moves that were bounced.
|
||||
it.todo("should cause stomping tantrum to double in power when the last move was bounced", async () => {
|
||||
game.override.battleStyle("single");
|
||||
game.override.battleStyle("single").moveset([MoveId.STOMPING_TANTRUM, MoveId.CHARM]);
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
game.override.moveset([MoveId.STOMPING_TANTRUM, MoveId.CHARM]);
|
||||
|
||||
const stomping_tantrum = allMoves[MoveId.STOMPING_TANTRUM];
|
||||
vi.spyOn(stomping_tantrum, "calculateBattlePower");
|
||||
@ -242,36 +231,30 @@ describe("Abilities - Magic Bounce", () => {
|
||||
expect(stomping_tantrum.calculateBattlePower).toHaveReturnedWith(150);
|
||||
});
|
||||
|
||||
// TODO: stomping tantrum should consider moves that were bounced.
|
||||
it.todo(
|
||||
"should properly cause the enemy's stomping tantrum to be doubled in power after bouncing and failing",
|
||||
async () => {
|
||||
game.override.enemyMoveset([MoveId.STOMPING_TANTRUM, MoveId.SPLASH, MoveId.CHARM]);
|
||||
await game.classicMode.startBattle([SpeciesId.BULBASAUR]);
|
||||
// TODO: stomping tantrum should consider moves that were bounced
|
||||
it.todo("should boost enemy's stomping tantrum after failed bounce", async () => {
|
||||
game.override.enemyMoveset([MoveId.STOMPING_TANTRUM, MoveId.SPLASH, MoveId.CHARM]);
|
||||
await game.classicMode.startBattle([SpeciesId.BULBASAUR]);
|
||||
|
||||
const stomping_tantrum = allMoves[MoveId.STOMPING_TANTRUM];
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
vi.spyOn(stomping_tantrum, "calculateBattlePower");
|
||||
const stomping_tantrum = allMoves[MoveId.STOMPING_TANTRUM];
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
vi.spyOn(stomping_tantrum, "calculateBattlePower");
|
||||
|
||||
game.move.select(MoveId.SPORE);
|
||||
await game.move.selectEnemyMove(MoveId.CHARM);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(enemy.getLastXMoves(1)[0].result).toBe("success");
|
||||
// Spore gets reflected back onto us
|
||||
game.move.select(MoveId.SPORE);
|
||||
await game.move.selectEnemyMove(MoveId.CHARM);
|
||||
await game.toNextTurn();
|
||||
expect(enemy.getLastXMoves(1)[0].result).toBe("success");
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
expect(stomping_tantrum.calculateBattlePower).toHaveReturnedWith(75);
|
||||
|
||||
await game.toNextTurn();
|
||||
game.move.select(MoveId.GROWL);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
expect(stomping_tantrum.calculateBattlePower).toHaveReturnedWith(75);
|
||||
},
|
||||
);
|
||||
game.move.select(MoveId.SPORE);
|
||||
await game.move.selectEnemyMove(MoveId.STOMPING_TANTRUM);
|
||||
await game.toNextTurn();
|
||||
expect(stomping_tantrum.calculateBattlePower).toHaveReturnedWith(150);
|
||||
});
|
||||
|
||||
it("should respect immunities when bouncing a move", async () => {
|
||||
vi.spyOn(allMoves[MoveId.THUNDER_WAVE], "accuracy", "get").mockReturnValue(100);
|
||||
game.override.moveset([MoveId.THUNDER_WAVE, MoveId.GROWL]);
|
||||
game.override.ability(AbilityId.SOUNDPROOF);
|
||||
game.override.moveset([MoveId.THUNDER_WAVE, MoveId.GROWL]).ability(AbilityId.SOUNDPROOF);
|
||||
await game.classicMode.startBattle([SpeciesId.PHANPY]);
|
||||
|
||||
// Turn 1 - thunder wave immunity test
|
||||
@ -309,8 +292,7 @@ describe("Abilities - Magic Bounce", () => {
|
||||
});
|
||||
|
||||
it("should always apply the leftmost available target's magic bounce when bouncing moves like sticky webs in doubles", async () => {
|
||||
game.override.battleStyle("double");
|
||||
game.override.moveset([MoveId.STICKY_WEB, MoveId.SPLASH, MoveId.TRICK_ROOM]);
|
||||
game.override.battleStyle("double").moveset([MoveId.STICKY_WEB, MoveId.SPLASH, MoveId.TRICK_ROOM]);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MAGIKARP]);
|
||||
const [enemy_1, enemy_2] = game.scene.getEnemyField();
|
||||
@ -345,6 +327,7 @@ describe("Abilities - Magic Bounce", () => {
|
||||
it("should not bounce back status moves that hit through semi-invulnerable states", async () => {
|
||||
game.override.moveset([MoveId.TOXIC, MoveId.CHARM]);
|
||||
await game.classicMode.startBattle([SpeciesId.BULBASAUR]);
|
||||
|
||||
game.move.select(MoveId.TOXIC);
|
||||
await game.move.selectEnemyMove(MoveId.FLY);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
|
@ -30,16 +30,16 @@ describe("Abilities - Magic Guard", () => {
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
|
||||
/** Player Pokemon overrides */
|
||||
game.override.ability(AbilityId.MAGIC_GUARD);
|
||||
game.override.moveset([MoveId.SPLASH]);
|
||||
game.override.startingLevel(100);
|
||||
|
||||
/** Enemy Pokemon overrides */
|
||||
game.override.enemySpecies(SpeciesId.SNORLAX);
|
||||
game.override.enemyAbility(AbilityId.INSOMNIA);
|
||||
game.override.enemyMoveset(MoveId.SPLASH);
|
||||
game.override.enemyLevel(100);
|
||||
game.override
|
||||
/** Player Pokemon overrides */
|
||||
.ability(AbilityId.MAGIC_GUARD)
|
||||
.moveset([MoveId.SPLASH])
|
||||
.startingLevel(100)
|
||||
/** Enemy Pokemon overrides */
|
||||
.enemySpecies(SpeciesId.SNORLAX)
|
||||
.enemyAbility(AbilityId.INSOMNIA)
|
||||
.enemyMoveset(MoveId.SPLASH)
|
||||
.enemyLevel(100);
|
||||
});
|
||||
|
||||
//Bulbapedia Reference: https://bulbapedia.bulbagarden.net/wiki/Magic_Guard_(Ability)
|
||||
@ -89,8 +89,9 @@ describe("Abilities - Magic Guard", () => {
|
||||
});
|
||||
|
||||
it("ability effect should not persist when the ability is replaced", async () => {
|
||||
game.override.enemyMoveset([MoveId.WORRY_SEED, MoveId.WORRY_SEED, MoveId.WORRY_SEED, MoveId.WORRY_SEED]);
|
||||
game.override.statusEffect(StatusEffect.POISON);
|
||||
game.override
|
||||
.enemyMoveset([MoveId.WORRY_SEED, MoveId.WORRY_SEED, MoveId.WORRY_SEED, MoveId.WORRY_SEED])
|
||||
.statusEffect(StatusEffect.POISON);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
|
||||
@ -108,8 +109,7 @@ describe("Abilities - Magic Guard", () => {
|
||||
});
|
||||
|
||||
it("Magic Guard prevents damage caused by burn but other non-damaging effects are still applied", async () => {
|
||||
game.override.enemyStatusEffect(StatusEffect.BURN);
|
||||
game.override.enemyAbility(AbilityId.MAGIC_GUARD);
|
||||
game.override.enemyStatusEffect(StatusEffect.BURN).enemyAbility(AbilityId.MAGIC_GUARD);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
|
||||
@ -130,8 +130,7 @@ describe("Abilities - Magic Guard", () => {
|
||||
});
|
||||
|
||||
it("Magic Guard prevents damage caused by toxic but other non-damaging effects are still applied", async () => {
|
||||
game.override.enemyStatusEffect(StatusEffect.TOXIC);
|
||||
game.override.enemyAbility(AbilityId.MAGIC_GUARD);
|
||||
game.override.enemyStatusEffect(StatusEffect.TOXIC).enemyAbility(AbilityId.MAGIC_GUARD);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
|
||||
@ -208,8 +207,7 @@ describe("Abilities - Magic Guard", () => {
|
||||
|
||||
it("Magic Guard prevents against damage from volatile status effects", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.DUSKULL]);
|
||||
game.override.moveset([MoveId.CURSE]);
|
||||
game.override.enemyAbility(AbilityId.MAGIC_GUARD);
|
||||
game.override.moveset([MoveId.CURSE]).enemyAbility(AbilityId.MAGIC_GUARD);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
|
||||
@ -331,8 +329,9 @@ describe("Abilities - Magic Guard", () => {
|
||||
//Tests the ability Bad Dreams
|
||||
game.override.statusEffect(StatusEffect.SLEEP);
|
||||
//enemy pokemon is given Spore just in case player pokemon somehow awakens during test
|
||||
game.override.enemyMoveset([MoveId.SPORE, MoveId.SPORE, MoveId.SPORE, MoveId.SPORE]);
|
||||
game.override.enemyAbility(AbilityId.BAD_DREAMS);
|
||||
game.override
|
||||
.enemyMoveset([MoveId.SPORE, MoveId.SPORE, MoveId.SPORE, MoveId.SPORE])
|
||||
.enemyAbility(AbilityId.BAD_DREAMS);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
|
||||
@ -353,8 +352,7 @@ describe("Abilities - Magic Guard", () => {
|
||||
|
||||
it("Magic Guard prevents damage from abilities with PostFaintContactDamageAbAttr", async () => {
|
||||
//Tests the abilities Innards Out/Aftermath
|
||||
game.override.moveset([MoveId.TACKLE]);
|
||||
game.override.enemyAbility(AbilityId.AFTERMATH);
|
||||
game.override.moveset([MoveId.TACKLE]).enemyAbility(AbilityId.AFTERMATH);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
|
||||
@ -377,8 +375,7 @@ describe("Abilities - Magic Guard", () => {
|
||||
|
||||
it("Magic Guard prevents damage from abilities with PostDefendContactDamageAbAttr", async () => {
|
||||
//Tests the abilities Iron Barbs/Rough Skin
|
||||
game.override.moveset([MoveId.TACKLE]);
|
||||
game.override.enemyAbility(AbilityId.IRON_BARBS);
|
||||
game.override.moveset([MoveId.TACKLE]).enemyAbility(AbilityId.IRON_BARBS);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
|
||||
@ -400,8 +397,7 @@ describe("Abilities - Magic Guard", () => {
|
||||
|
||||
it("Magic Guard prevents damage from abilities with ReverseDrainAbAttr", async () => {
|
||||
//Tests the ability Liquid Ooze
|
||||
game.override.moveset([MoveId.ABSORB]);
|
||||
game.override.enemyAbility(AbilityId.LIQUID_OOZE);
|
||||
game.override.moveset([MoveId.ABSORB]).enemyAbility(AbilityId.LIQUID_OOZE);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
|
||||
@ -422,9 +418,7 @@ describe("Abilities - Magic Guard", () => {
|
||||
});
|
||||
|
||||
it("Magic Guard prevents HP loss from abilities with PostWeatherLapseDamageAbAttr", async () => {
|
||||
//Tests the abilities Solar Power/Dry Skin
|
||||
game.override.passiveAbility(AbilityId.SOLAR_POWER);
|
||||
game.override.weather(WeatherType.SUNNY);
|
||||
game.override.passiveAbility(AbilityId.SOLAR_POWER).weather(WeatherType.SUNNY);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
|
@ -37,8 +37,7 @@ describe("Ability - Mirror Armor", () => {
|
||||
});
|
||||
|
||||
it("Player side + single battle Intimidate - opponent loses stats", async () => {
|
||||
game.override.ability(AbilityId.MIRROR_ARMOR);
|
||||
game.override.enemyAbility(AbilityId.INTIMIDATE);
|
||||
game.override.ability(AbilityId.MIRROR_ARMOR).enemyAbility(AbilityId.INTIMIDATE);
|
||||
await game.classicMode.startBattle([SpeciesId.BULBASAUR]);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
@ -54,8 +53,7 @@ describe("Ability - Mirror Armor", () => {
|
||||
});
|
||||
|
||||
it("Enemy side + single battle Intimidate - player loses stats", async () => {
|
||||
game.override.enemyAbility(AbilityId.MIRROR_ARMOR);
|
||||
game.override.ability(AbilityId.INTIMIDATE);
|
||||
game.override.enemyAbility(AbilityId.MIRROR_ARMOR).ability(AbilityId.INTIMIDATE);
|
||||
await game.classicMode.startBattle([SpeciesId.BULBASAUR]);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
@ -71,9 +69,7 @@ describe("Ability - Mirror Armor", () => {
|
||||
});
|
||||
|
||||
it("Player side + double battle Intimidate - opponents each lose -2 atk", async () => {
|
||||
game.override.battleStyle("double");
|
||||
game.override.ability(AbilityId.MIRROR_ARMOR);
|
||||
game.override.enemyAbility(AbilityId.INTIMIDATE);
|
||||
game.override.battleStyle("double").ability(AbilityId.MIRROR_ARMOR).enemyAbility(AbilityId.INTIMIDATE);
|
||||
await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER]);
|
||||
|
||||
const [enemy1, enemy2] = game.scene.getEnemyField();
|
||||
@ -93,9 +89,7 @@ describe("Ability - Mirror Armor", () => {
|
||||
});
|
||||
|
||||
it("Enemy side + double battle Intimidate - players each lose -2 atk", async () => {
|
||||
game.override.battleStyle("double");
|
||||
game.override.enemyAbility(AbilityId.MIRROR_ARMOR);
|
||||
game.override.ability(AbilityId.INTIMIDATE);
|
||||
game.override.battleStyle("double").enemyAbility(AbilityId.MIRROR_ARMOR).ability(AbilityId.INTIMIDATE);
|
||||
await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER]);
|
||||
|
||||
const [enemy1, enemy2] = game.scene.getEnemyField();
|
||||
@ -115,8 +109,7 @@ describe("Ability - Mirror Armor", () => {
|
||||
});
|
||||
|
||||
it("Player side + single battle Intimidate + Tickle - opponent loses stats", async () => {
|
||||
game.override.ability(AbilityId.MIRROR_ARMOR);
|
||||
game.override.enemyAbility(AbilityId.INTIMIDATE);
|
||||
game.override.ability(AbilityId.MIRROR_ARMOR).enemyAbility(AbilityId.INTIMIDATE);
|
||||
await game.classicMode.startBattle([SpeciesId.BULBASAUR]);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
@ -134,9 +127,7 @@ describe("Ability - Mirror Armor", () => {
|
||||
});
|
||||
|
||||
it("Player side + double battle Intimidate + Tickle - opponents each lose -3 atk, -1 def", async () => {
|
||||
game.override.battleStyle("double");
|
||||
game.override.ability(AbilityId.MIRROR_ARMOR);
|
||||
game.override.enemyAbility(AbilityId.INTIMIDATE);
|
||||
game.override.battleStyle("double").ability(AbilityId.MIRROR_ARMOR).enemyAbility(AbilityId.INTIMIDATE);
|
||||
await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER]);
|
||||
|
||||
const [enemy1, enemy2] = game.scene.getEnemyField();
|
||||
@ -159,8 +150,7 @@ describe("Ability - Mirror Armor", () => {
|
||||
});
|
||||
|
||||
it("Enemy side + single battle Intimidate + Tickle - player loses stats", async () => {
|
||||
game.override.enemyAbility(AbilityId.MIRROR_ARMOR);
|
||||
game.override.ability(AbilityId.INTIMIDATE);
|
||||
game.override.enemyAbility(AbilityId.MIRROR_ARMOR).ability(AbilityId.INTIMIDATE);
|
||||
await game.classicMode.startBattle([SpeciesId.BULBASAUR]);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
@ -178,8 +168,7 @@ describe("Ability - Mirror Armor", () => {
|
||||
});
|
||||
|
||||
it("Player side + single battle Intimidate + oppoenent has white smoke - no one loses stats", async () => {
|
||||
game.override.enemyAbility(AbilityId.WHITE_SMOKE);
|
||||
game.override.ability(AbilityId.MIRROR_ARMOR);
|
||||
game.override.enemyAbility(AbilityId.WHITE_SMOKE).ability(AbilityId.MIRROR_ARMOR);
|
||||
await game.classicMode.startBattle([SpeciesId.BULBASAUR]);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
@ -197,8 +186,7 @@ describe("Ability - Mirror Armor", () => {
|
||||
});
|
||||
|
||||
it("Enemy side + single battle Intimidate + player has white smoke - no one loses stats", async () => {
|
||||
game.override.ability(AbilityId.WHITE_SMOKE);
|
||||
game.override.enemyAbility(AbilityId.MIRROR_ARMOR);
|
||||
game.override.ability(AbilityId.WHITE_SMOKE).enemyAbility(AbilityId.MIRROR_ARMOR);
|
||||
await game.classicMode.startBattle([SpeciesId.BULBASAUR]);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
@ -252,9 +240,7 @@ describe("Ability - Mirror Armor", () => {
|
||||
});
|
||||
|
||||
it("Both sides have mirror armor - does not loop, player loses attack", async () => {
|
||||
game.override.enemyAbility(AbilityId.MIRROR_ARMOR);
|
||||
game.override.ability(AbilityId.MIRROR_ARMOR);
|
||||
game.override.ability(AbilityId.INTIMIDATE);
|
||||
game.override.enemyAbility(AbilityId.MIRROR_ARMOR).ability(AbilityId.MIRROR_ARMOR).ability(AbilityId.INTIMIDATE);
|
||||
await game.classicMode.startBattle([SpeciesId.BULBASAUR]);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
@ -288,8 +274,7 @@ describe("Ability - Mirror Armor", () => {
|
||||
});
|
||||
|
||||
it("Double battle + sticky web applied player side - player switches out and enemy 1 should lose -1 speed", async () => {
|
||||
game.override.battleStyle("double");
|
||||
game.override.ability(AbilityId.MIRROR_ARMOR);
|
||||
game.override.battleStyle("double").ability(AbilityId.MIRROR_ARMOR);
|
||||
await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER, SpeciesId.SQUIRTLE]);
|
||||
|
||||
const [enemy1, enemy2] = game.scene.getEnemyField();
|
||||
|
@ -27,13 +27,14 @@ describe("Abilities - Moxie", () => {
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
const moveToUse = MoveId.AERIAL_ACE;
|
||||
game.override.battleStyle("single");
|
||||
game.override.enemySpecies(SpeciesId.RATTATA);
|
||||
game.override.enemyAbility(AbilityId.MOXIE);
|
||||
game.override.ability(AbilityId.MOXIE);
|
||||
game.override.startingLevel(2000);
|
||||
game.override.moveset([moveToUse]);
|
||||
game.override.enemyMoveset(MoveId.SPLASH);
|
||||
game.override
|
||||
.battleStyle("single")
|
||||
.enemySpecies(SpeciesId.RATTATA)
|
||||
.enemyAbility(AbilityId.MOXIE)
|
||||
.ability(AbilityId.MOXIE)
|
||||
.startingLevel(2000)
|
||||
.moveset([moveToUse])
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
});
|
||||
|
||||
it("should raise ATK stat stage by 1 when winning a battle", async () => {
|
||||
@ -48,7 +49,7 @@ describe("Abilities - Moxie", () => {
|
||||
await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(VictoryPhase);
|
||||
|
||||
expect(playerPokemon.getStatStage(Stat.ATK)).toBe(1);
|
||||
}, 20000);
|
||||
});
|
||||
|
||||
// TODO: Activate this test when MOXIE is corrected to work on faint and not on battle victory
|
||||
it.todo(
|
||||
|
@ -63,7 +63,7 @@ describe("Abilities - Mycelium Might", () => {
|
||||
|
||||
// Despite the opponent's ability (Clear Body), its ATK stat stage is still reduced.
|
||||
expect(enemyPokemon?.getStatStage(Stat.ATK)).toBe(-1);
|
||||
}, 20000);
|
||||
});
|
||||
|
||||
it("will still go first if a status move that is in a higher priority bracket than the opponent's move is used", async () => {
|
||||
game.override.enemyMoveset(MoveId.TACKLE);
|
||||
@ -86,7 +86,7 @@ describe("Abilities - Mycelium Might", () => {
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
// Despite the opponent's ability (Clear Body), its ATK stat stage is still reduced.
|
||||
expect(enemyPokemon?.getStatStage(Stat.ATK)).toBe(-1);
|
||||
}, 20000);
|
||||
});
|
||||
|
||||
it("will not affect non-status moves", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.REGIELEKI]);
|
||||
@ -105,5 +105,5 @@ describe("Abilities - Mycelium Might", () => {
|
||||
// This means that the commandOrder should be identical to the speedOrder
|
||||
expect(speedOrder).toEqual([playerIndex, enemyIndex]);
|
||||
expect(commandOrder).toEqual([playerIndex, enemyIndex]);
|
||||
}, 20000);
|
||||
});
|
||||
});
|
||||
|
@ -45,8 +45,9 @@ describe("Abilities - Normalize", () => {
|
||||
});
|
||||
|
||||
it("should not apply the old type boost item after changing a move's type", async () => {
|
||||
game.override.startingHeldItems([{ name: "ATTACK_TYPE_BOOSTER", count: 1, type: PokemonType.GRASS }]);
|
||||
game.override.moveset([MoveId.LEAFAGE]);
|
||||
game.override
|
||||
.startingHeldItems([{ name: "ATTACK_TYPE_BOOSTER", count: 1, type: PokemonType.GRASS }])
|
||||
.moveset([MoveId.LEAFAGE]);
|
||||
|
||||
const powerSpy = vi.spyOn(allMoves[MoveId.LEAFAGE], "calculateBattlePower");
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
@ -58,8 +59,9 @@ describe("Abilities - Normalize", () => {
|
||||
});
|
||||
|
||||
it("should apply silk scarf's power boost after changing a move's type", async () => {
|
||||
game.override.startingHeldItems([{ name: "ATTACK_TYPE_BOOSTER", count: 1, type: PokemonType.NORMAL }]);
|
||||
game.override.moveset([MoveId.LEAFAGE]);
|
||||
game.override
|
||||
.startingHeldItems([{ name: "ATTACK_TYPE_BOOSTER", count: 1, type: PokemonType.NORMAL }])
|
||||
.moveset([MoveId.LEAFAGE]);
|
||||
|
||||
const powerSpy = vi.spyOn(allMoves[MoveId.LEAFAGE], "calculateBattlePower");
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
|
@ -26,14 +26,15 @@ describe("Abilities - Parental Bond", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override.battleStyle("single");
|
||||
game.override.disableCrits();
|
||||
game.override.ability(AbilityId.PARENTAL_BOND);
|
||||
game.override.enemySpecies(SpeciesId.SNORLAX);
|
||||
game.override.enemyAbility(AbilityId.FUR_COAT);
|
||||
game.override.enemyMoveset(MoveId.SPLASH);
|
||||
game.override.startingLevel(100);
|
||||
game.override.enemyLevel(100);
|
||||
game.override
|
||||
.battleStyle("single")
|
||||
.disableCrits()
|
||||
.ability(AbilityId.PARENTAL_BOND)
|
||||
.enemySpecies(SpeciesId.SNORLAX)
|
||||
.enemyAbility(AbilityId.FUR_COAT)
|
||||
.enemyMoveset(MoveId.SPLASH)
|
||||
.startingLevel(100)
|
||||
.enemyLevel(100);
|
||||
});
|
||||
|
||||
it("should add second strike to attack move", async () => {
|
||||
@ -61,8 +62,7 @@ describe("Abilities - Parental Bond", () => {
|
||||
});
|
||||
|
||||
it("should apply secondary effects to both strikes", async () => {
|
||||
game.override.moveset([MoveId.POWER_UP_PUNCH]);
|
||||
game.override.enemySpecies(SpeciesId.AMOONGUSS);
|
||||
game.override.moveset([MoveId.POWER_UP_PUNCH]).enemySpecies(SpeciesId.AMOONGUSS);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
|
||||
@ -148,8 +148,7 @@ describe("Abilities - Parental Bond", () => {
|
||||
});
|
||||
|
||||
it("should not apply multiplier to counter moves", async () => {
|
||||
game.override.moveset([MoveId.COUNTER]);
|
||||
game.override.enemyMoveset([MoveId.TACKLE]);
|
||||
game.override.moveset([MoveId.COUNTER]).enemyMoveset([MoveId.TACKLE]);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.SHUCKLE]);
|
||||
|
||||
@ -167,9 +166,7 @@ describe("Abilities - Parental Bond", () => {
|
||||
});
|
||||
|
||||
it("should not apply to multi-target moves", async () => {
|
||||
game.override.battleStyle("double");
|
||||
game.override.moveset([MoveId.EARTHQUAKE]);
|
||||
game.override.passiveAbility(AbilityId.LEVITATE);
|
||||
game.override.battleStyle("double").moveset([MoveId.EARTHQUAKE]).passiveAbility(AbilityId.LEVITATE);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.FEEBAS]);
|
||||
|
||||
@ -237,8 +234,7 @@ describe("Abilities - Parental Bond", () => {
|
||||
});
|
||||
|
||||
it("Moves boosted by this ability and Multi-Lens should strike 3 times", async () => {
|
||||
game.override.moveset([MoveId.TACKLE]);
|
||||
game.override.startingHeldItems([{ name: "MULTI_LENS", count: 1 }]);
|
||||
game.override.moveset([MoveId.TACKLE]).startingHeldItems([{ name: "MULTI_LENS", count: 1 }]);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
|
||||
@ -252,8 +248,7 @@ describe("Abilities - Parental Bond", () => {
|
||||
});
|
||||
|
||||
it("Seismic Toss boosted by this ability and Multi-Lens should strike 3 times", async () => {
|
||||
game.override.moveset([MoveId.SEISMIC_TOSS]);
|
||||
game.override.startingHeldItems([{ name: "MULTI_LENS", count: 1 }]);
|
||||
game.override.moveset([MoveId.SEISMIC_TOSS]).startingHeldItems([{ name: "MULTI_LENS", count: 1 }]);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
|
||||
@ -378,8 +373,7 @@ describe("Abilities - Parental Bond", () => {
|
||||
});
|
||||
|
||||
it("should not cause user to hit into King's Shield more than once", async () => {
|
||||
game.override.moveset([MoveId.TACKLE]);
|
||||
game.override.enemyMoveset([MoveId.KINGS_SHIELD]);
|
||||
game.override.moveset([MoveId.TACKLE]).enemyMoveset([MoveId.KINGS_SHIELD]);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
|
||||
@ -393,8 +387,7 @@ describe("Abilities - Parental Bond", () => {
|
||||
});
|
||||
|
||||
it("should not cause user to hit into Storm Drain more than once", async () => {
|
||||
game.override.moveset([MoveId.WATER_GUN]);
|
||||
game.override.enemyAbility(AbilityId.STORM_DRAIN);
|
||||
game.override.moveset([MoveId.WATER_GUN]).enemyAbility(AbilityId.STORM_DRAIN);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
|
||||
|
@ -21,19 +21,18 @@ describe("Abilities - Perish Song", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override.battleStyle("single");
|
||||
game.override.disableCrits();
|
||||
|
||||
game.override.enemySpecies(SpeciesId.MAGIKARP);
|
||||
game.override.enemyAbility(AbilityId.BALL_FETCH);
|
||||
|
||||
game.override.starterSpecies(SpeciesId.CURSOLA);
|
||||
game.override.ability(AbilityId.PERISH_BODY);
|
||||
game.override.moveset([MoveId.SPLASH]);
|
||||
game.override
|
||||
.battleStyle("single")
|
||||
.disableCrits()
|
||||
.enemySpecies(SpeciesId.MAGIKARP)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.starterSpecies(SpeciesId.CURSOLA)
|
||||
.ability(AbilityId.PERISH_BODY)
|
||||
.moveset([MoveId.SPLASH])
|
||||
.enemyMoveset([MoveId.AQUA_JET]);
|
||||
});
|
||||
|
||||
it("should trigger when hit with damaging move", async () => {
|
||||
game.override.enemyMoveset([MoveId.AQUA_JET]);
|
||||
await game.classicMode.startBattle();
|
||||
const cursola = game.scene.getPlayerPokemon();
|
||||
const magikarp = game.scene.getEnemyPokemon();
|
||||
@ -46,7 +45,7 @@ describe("Abilities - Perish Song", () => {
|
||||
});
|
||||
|
||||
it("should trigger even when fainting", async () => {
|
||||
game.override.enemyMoveset([MoveId.AQUA_JET]).enemyLevel(100).startingLevel(1);
|
||||
game.override.enemyLevel(100).startingLevel(1);
|
||||
await game.classicMode.startBattle([SpeciesId.CURSOLA, SpeciesId.FEEBAS]);
|
||||
const magikarp = game.scene.getEnemyPokemon();
|
||||
|
||||
@ -87,9 +86,10 @@ describe("Abilities - Perish Song", () => {
|
||||
});
|
||||
|
||||
it("should activate if cursola already has perish song, but not reset its counter", async () => {
|
||||
game.override.enemyMoveset([MoveId.PERISH_SONG, MoveId.AQUA_JET, MoveId.SPLASH]);
|
||||
game.override.moveset([MoveId.WHIRLWIND, MoveId.SPLASH]);
|
||||
game.override.startingWave(5);
|
||||
game.override
|
||||
.enemyMoveset([MoveId.PERISH_SONG, MoveId.AQUA_JET, MoveId.SPLASH])
|
||||
.moveset([MoveId.WHIRLWIND, MoveId.SPLASH])
|
||||
.startingWave(5);
|
||||
await game.classicMode.startBattle([SpeciesId.CURSOLA]);
|
||||
const cursola = game.scene.getPlayerPokemon();
|
||||
|
||||
|
@ -35,8 +35,7 @@ describe("Abilities - POWER CONSTRUCT", () => {
|
||||
test("check if fainted 50% Power Construct Pokemon switches to base form on arena reset", async () => {
|
||||
const baseForm = 2,
|
||||
completeForm = 4;
|
||||
game.override.startingWave(4);
|
||||
game.override.starterForms({
|
||||
game.override.startingWave(4).starterForms({
|
||||
[SpeciesId.ZYGARDE]: completeForm,
|
||||
});
|
||||
|
||||
@ -62,8 +61,7 @@ describe("Abilities - POWER CONSTRUCT", () => {
|
||||
test("check if fainted 10% Power Construct Pokemon switches to base form on arena reset", async () => {
|
||||
const baseForm = 3,
|
||||
completeForm = 5;
|
||||
game.override.startingWave(4);
|
||||
game.override.starterForms({
|
||||
game.override.startingWave(4).starterForms({
|
||||
[SpeciesId.ZYGARDE]: completeForm,
|
||||
});
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user