Merge remote-tracking branch 'upstream/beta' into markdown

This commit is contained in:
Bertie690 2025-04-26 09:48:15 -04:00
commit 59f4e7827c
93 changed files with 1966 additions and 1273 deletions

View File

@ -1,40 +1,34 @@
# ESLint
# Biome
## Key Features
1. **Automation**:
- A pre-commit hook has been added to automatically run ESLint on the added or modified files, ensuring code quality before commits.
- A pre-commit hook has been added to automatically run Biome on the added or modified files, ensuring code quality before commits.
2. **Manual Usage**:
- If you prefer not to use the pre-commit hook, you can manually run ESLint to automatically fix issues using the command:
- If you prefer not to use the pre-commit hook, you can manually run biome to automatically fix issues using the command:
```sh
npx eslint --fix . or npm run eslint
npx @biomejs/biome --write
```
- Running this command will lint all files in the repository.
3. **GitHub Action**:
- A GitHub Action has been added to automatically run ESLint on every push and pull request, ensuring code quality in the CI/CD pipeline.
- A GitHub Action has been added to automatically run Biome on every push and pull request, ensuring code quality in the CI/CD pipeline.
## Summary of ESLint Rules
If you are getting linting errors from biome and want to see which files they are coming from, you can find that out by running biome in a way that is configured to only show the errors for that specific rule: ``npx @biomejs/biome lint --only=category/ruleName``
1. **General Rules**:
- **Equality**: Use `===` and `!==` instead of `==` and `!=` (`eqeqeq`).
- **Indentation**: Enforce 2-space indentation (`indent`).
- **Quotes**: Use doublequotes for strings (`quotes`).
- **Variable Declarations**:
- Disallow `var`; use `let` or `const` (`no-var`).
- Prefer `const` for variables that are never reassigned (`prefer-const`).
- **Unused Variables**: Allow unused function parameters but enforce error for other unused variables (`@typescript-eslint/no-unused-vars`).
- **End of Line**: Ensure at least one newline at the end of files (`eol-last`).
- **Curly Braces**: Enforce the use of curly braces for all control statements (`curly`).
- **Brace Style**: Use one true brace style (`1tbs`) for TypeScript-specific syntax (`@typescript-eslint/brace-style`).
## Summary of Biome Rules
2. **TypeScript-Specific Rules**:
- **Semicolons**:
- Enforce semicolons for TypeScript-specific syntax (`@typescript-eslint/semi`).
- Disallow unnecessary semicolons (`@typescript-eslint/no-extra-semi`).
We use the [recommended ruleset](https://biomejs.dev/linter/rules/) for Biome, with some customizations to better suit our project's needs.
## Benefits
For a complete list of rules and their configurations, refer to the `biome.jsonc` file in the project root.
- **Consistency**: Ensures consistent coding style across the project.
- **Code Quality**: Helps catch potential errors and improve overall code quality.
- **Readability**: Makes the codebase easier to read and maintain.
Some things to consider:
- We have disabled rules that prioritize style over performance, such as `useTemplate`
- Some rules are currently marked as warnings (`warn`) to allow for gradual refactoring without blocking development. Do not write new code that triggers these warnings.
- The linter is configured to ignore specific files and folders, such as large or complex files that are pending refactors, to improve performance and focus on actionable areas.
Formatting is also handled by Biome. You should not have to worry about manually formatting your code.

18
package-lock.json generated
View File

@ -18,8 +18,8 @@
"i18next-korean-postposition-processor": "^1.0.0",
"json-stable-stringify": "^1.2.0",
"jszip": "^3.10.1",
"phaser": "^3.70.0",
"phaser3-rex-plugins": "^1.80.14"
"phaser": "^3.88.2",
"phaser3-rex-plugins": "^1.80.15"
},
"devDependencies": {
"@biomejs/biome": "1.9.4",
@ -48,7 +48,7 @@
"vitest-canvas-mock": "^0.3.3"
},
"engines": {
"node": ">=20.0.0"
"node": ">=22.0.0"
}
},
"node_modules/@ampproject/remapping": {
@ -6227,18 +6227,18 @@
}
},
"node_modules/phaser": {
"version": "3.80.1",
"resolved": "https://registry.npmjs.org/phaser/-/phaser-3.80.1.tgz",
"integrity": "sha512-VQGAWoDOkEpAWYkI+PUADv5Ql+SM0xpLuAMBJHz9tBcOLqjJ2wd8bUhxJgOqclQlLTg97NmMd9MhS75w16x1Cw==",
"version": "3.88.2",
"resolved": "https://registry.npmjs.org/phaser/-/phaser-3.88.2.tgz",
"integrity": "sha512-UBgd2sAFuRJbF2xKaQ5jpMWB8oETncChLnymLGHcrnT53vaqiGrQWbUKUDBawKLm24sghjKo4Bf+/xfv8espZQ==",
"license": "MIT",
"dependencies": {
"eventemitter3": "^5.0.1"
}
},
"node_modules/phaser3-rex-plugins": {
"version": "1.80.14",
"resolved": "https://registry.npmjs.org/phaser3-rex-plugins/-/phaser3-rex-plugins-1.80.14.tgz",
"integrity": "sha512-eHi3VgryO9umNu6D1yQU5IS6tH4TyC2Y6RgJ495nNp37X2fdYnmYpBfgFg+YaumvtaoOvCkUVyi/YqWNPf2X2A==",
"version": "1.80.15",
"resolved": "https://registry.npmjs.org/phaser3-rex-plugins/-/phaser3-rex-plugins-1.80.15.tgz",
"integrity": "sha512-Ur973N1W5st6XEYBcJko8eTcEbdDHMM+m7VqvT3j/EJeJwYyJ3bVb33JJDsFgefk3A2iAz2itP/UY7CzxJOJVA==",
"license": "MIT",
"dependencies": {
"dagre": "^0.8.5",

View File

@ -63,8 +63,8 @@
"i18next-korean-postposition-processor": "^1.0.0",
"json-stable-stringify": "^1.2.0",
"jszip": "^3.10.1",
"phaser": "^3.70.0",
"phaser3-rex-plugins": "^1.80.14"
"phaser": "^3.88.2",
"phaser3-rex-plugins": "^1.80.15"
},
"engines": {
"node": ">=22.0.0"

View File

@ -516,6 +516,34 @@
"trimmed": true,
"spriteSourceSize": { "x": 0, "y": 0, "w": 28, "h": 11 },
"sourceSize": { "w": 28, "h": 11 }
},
"BACK_SLASH.png": {
"frame": { "x": 147, "y": 66, "w": 12, "h": 11 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 0, "y": 0, "w": 12, "h": 11 },
"sourceSize": { "w": 12, "h": 11 }
},
"FORWARD_SLASH.png": {
"frame": { "x": 144, "y": 55, "w": 12, "h": 11 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 0, "y": 0, "w": 12, "h": 11 },
"sourceSize": { "w": 12, "h": 11 }
},
"COMMA.png": {
"frame": { "x": 144, "y": 44, "w": 12, "h": 11 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 0, "y": 0, "w": 12, "h": 11 },
"sourceSize": { "w": 12, "h": 11 }
},
"PERIOD.png": {
"frame": { "x": 143, "y": 22, "w": 11, "h": 11 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 0, "y": 0, "w": 11, "h": 11 },
"sourceSize": { "w": 11, "h": 11 }
}
},
"meta": {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

@ -1 +1 @@
Subproject commit e98f0eb9c2022bc78b53f0444424c636498e725a
Subproject commit 18c1963ef309612a5a7fef76f9879709a7202189

View File

@ -151,7 +151,6 @@ import { NextEncounterPhase } from "#app/phases/next-encounter-phase";
import { PokemonAnimPhase } from "#app/phases/pokemon-anim-phase";
import { QuietFormChangePhase } from "#app/phases/quiet-form-change-phase";
import { ReturnPhase } from "#app/phases/return-phase";
import { SelectBiomePhase } from "#app/phases/select-biome-phase";
import { ShowTrainerPhase } from "#app/phases/show-trainer-phase";
import { SummonPhase } from "#app/phases/summon-phase";
import { SwitchPhase } from "#app/phases/switch-phase";
@ -1298,6 +1297,16 @@ export default class BattleScene extends SceneBase {
return Math.max(doubleChance.value, 1);
}
isNewBiome(currentBattle = this.currentBattle) {
const isWaveIndexMultipleOfTen = !(currentBattle.waveIndex % 10);
const isEndlessOrDaily = this.gameMode.hasShortBiomes || this.gameMode.isDaily;
const isEndlessFifthWave = this.gameMode.hasShortBiomes && currentBattle.waveIndex % 5 === 0;
const isWaveIndexMultipleOfFiftyMinusOne = currentBattle.waveIndex % 50 === 49;
const isNewBiome =
isWaveIndexMultipleOfTen || isEndlessFifthWave || (isEndlessOrDaily && isWaveIndexMultipleOfFiftyMinusOne);
return isNewBiome;
}
// TODO: ...this never actually returns `null`, right?
newBattle(
waveIndex?: number,
@ -1385,9 +1394,9 @@ export default class BattleScene extends SceneBase {
if (double === undefined && newWaveIndex > 1) {
if (newBattleType === BattleType.WILD && !this.gameMode.isWaveFinal(newWaveIndex)) {
newDouble = !randSeedInt(this.getDoubleBattleChance(newWaveIndex, playerField));
}
} else if (double === undefined && newBattleType === BattleType.TRAINER) {
} else if (newBattleType === BattleType.TRAINER) {
newDouble = newTrainer?.variant === TrainerVariant.DOUBLE;
}
} else if (!battleConfig) {
newDouble = !!double;
}
@ -1461,12 +1470,7 @@ export default class BattleScene extends SceneBase {
}
if (!waveIndex && lastBattle) {
const isWaveIndexMultipleOfTen = !(lastBattle.waveIndex % 10);
const isEndlessOrDaily = this.gameMode.hasShortBiomes || this.gameMode.isDaily;
const isEndlessFifthWave = this.gameMode.hasShortBiomes && lastBattle.waveIndex % 5 === 0;
const isWaveIndexMultipleOfFiftyMinusOne = lastBattle.waveIndex % 50 === 49;
const isNewBiome =
isWaveIndexMultipleOfTen || isEndlessFifthWave || (isEndlessOrDaily && isWaveIndexMultipleOfFiftyMinusOne);
const isNewBiome = this.isNewBiome(lastBattle);
const resetArenaState =
isNewBiome ||
[BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER].includes(this.currentBattle.battleType) ||
@ -1515,7 +1519,6 @@ export default class BattleScene extends SceneBase {
if (!this.gameMode.hasRandomBiomes && !isNewBiome) {
this.pushPhase(new NextEncounterPhase());
} else {
this.pushPhase(new SelectBiomePhase());
this.pushPhase(new NewBiomeEncounterPhase());
const newMaxExpLevel = this.getMaxExpLevel();

View File

@ -31,29 +31,7 @@ import type { CustomModifierSettings } from "#app/modifier/modifier-type";
import { ModifierTier } from "#app/modifier/modifier-tier";
import type { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { BattleType } from "#enums/battle-type";
export enum ClassicFixedBossWaves {
TOWN_YOUNGSTER = 5,
RIVAL_1 = 8,
RIVAL_2 = 25,
EVIL_GRUNT_1 = 35,
RIVAL_3 = 55,
EVIL_GRUNT_2 = 62,
EVIL_GRUNT_3 = 64,
EVIL_ADMIN_1 = 66,
RIVAL_4 = 95,
EVIL_GRUNT_4 = 112,
EVIL_ADMIN_2 = 114,
EVIL_BOSS_1 = 115,
RIVAL_5 = 145,
EVIL_BOSS_2 = 165,
ELITE_FOUR_1 = 182,
ELITE_FOUR_2 = 184,
ELITE_FOUR_3 = 186,
ELITE_FOUR_4 = 188,
CHAMPION = 190,
RIVAL_6 = 195,
}
import { ClassicFixedBossWaves } from "#enums/fixed-boss-waves";
export enum BattlerIndex {
ATTACKER = -1,

View File

@ -31,6 +31,7 @@ const cfg_keyboard_qwerty = {
KEY_X: Phaser.Input.Keyboard.KeyCodes.X,
KEY_Y: Phaser.Input.Keyboard.KeyCodes.Y,
KEY_Z: Phaser.Input.Keyboard.KeyCodes.Z,
KEY_0: Phaser.Input.Keyboard.KeyCodes.ZERO,
KEY_1: Phaser.Input.Keyboard.KeyCodes.ONE,
KEY_2: Phaser.Input.Keyboard.KeyCodes.TWO,
@ -41,11 +42,7 @@ const cfg_keyboard_qwerty = {
KEY_7: Phaser.Input.Keyboard.KeyCodes.SEVEN,
KEY_8: Phaser.Input.Keyboard.KeyCodes.EIGHT,
KEY_9: Phaser.Input.Keyboard.KeyCodes.NINE,
KEY_CTRL: Phaser.Input.Keyboard.KeyCodes.CTRL,
KEY_DEL: Phaser.Input.Keyboard.KeyCodes.DELETE,
KEY_END: Phaser.Input.Keyboard.KeyCodes.END,
KEY_ENTER: Phaser.Input.Keyboard.KeyCodes.ENTER,
KEY_ESC: Phaser.Input.Keyboard.KeyCodes.ESC,
KEY_F1: Phaser.Input.Keyboard.KeyCodes.F1,
KEY_F2: Phaser.Input.Keyboard.KeyCodes.F2,
KEY_F3: Phaser.Input.Keyboard.KeyCodes.F3,
@ -58,24 +55,41 @@ const cfg_keyboard_qwerty = {
KEY_F10: Phaser.Input.Keyboard.KeyCodes.F10,
KEY_F11: Phaser.Input.Keyboard.KeyCodes.F11,
KEY_F12: Phaser.Input.Keyboard.KeyCodes.F12,
KEY_HOME: Phaser.Input.Keyboard.KeyCodes.HOME,
KEY_INSERT: Phaser.Input.Keyboard.KeyCodes.INSERT,
KEY_PAGE_DOWN: Phaser.Input.Keyboard.KeyCodes.PAGE_DOWN,
KEY_PAGE_UP: Phaser.Input.Keyboard.KeyCodes.PAGE_UP,
KEY_CTRL: Phaser.Input.Keyboard.KeyCodes.CTRL,
KEY_DEL: Phaser.Input.Keyboard.KeyCodes.DELETE,
KEY_END: Phaser.Input.Keyboard.KeyCodes.END,
KEY_ENTER: Phaser.Input.Keyboard.KeyCodes.ENTER,
KEY_ESC: Phaser.Input.Keyboard.KeyCodes.ESC,
KEY_HOME: Phaser.Input.Keyboard.KeyCodes.HOME,
KEY_INSERT: Phaser.Input.Keyboard.KeyCodes.INSERT,
KEY_PLUS: Phaser.Input.Keyboard.KeyCodes.NUMPAD_ADD, // Assuming numpad plus
KEY_MINUS: Phaser.Input.Keyboard.KeyCodes.NUMPAD_SUBTRACT, // Assuming numpad minus
KEY_QUOTATION: Phaser.Input.Keyboard.KeyCodes.QUOTES,
KEY_SHIFT: Phaser.Input.Keyboard.KeyCodes.SHIFT,
KEY_SPACE: Phaser.Input.Keyboard.KeyCodes.SPACE,
KEY_TAB: Phaser.Input.Keyboard.KeyCodes.TAB,
KEY_TILDE: Phaser.Input.Keyboard.KeyCodes.BACKTICK,
KEY_ARROW_UP: Phaser.Input.Keyboard.KeyCodes.UP,
KEY_ARROW_DOWN: Phaser.Input.Keyboard.KeyCodes.DOWN,
KEY_ARROW_LEFT: Phaser.Input.Keyboard.KeyCodes.LEFT,
KEY_ARROW_RIGHT: Phaser.Input.Keyboard.KeyCodes.RIGHT,
KEY_LEFT_BRACKET: Phaser.Input.Keyboard.KeyCodes.OPEN_BRACKET,
KEY_RIGHT_BRACKET: Phaser.Input.Keyboard.KeyCodes.CLOSED_BRACKET,
KEY_SEMICOLON: Phaser.Input.Keyboard.KeyCodes.SEMICOLON,
KEY_COMMA: Phaser.Input.Keyboard.KeyCodes.COMMA,
KEY_PERIOD: Phaser.Input.Keyboard.KeyCodes.PERIOD,
KEY_BACK_SLASH: Phaser.Input.Keyboard.KeyCodes.BACK_SLASH,
KEY_FORWARD_SLASH: Phaser.Input.Keyboard.KeyCodes.FORWARD_SLASH,
KEY_BACKSPACE: Phaser.Input.Keyboard.KeyCodes.BACKSPACE,
KEY_ALT: Phaser.Input.Keyboard.KeyCodes.ALT,
},
@ -160,6 +174,10 @@ const cfg_keyboard_qwerty = {
KEY_RIGHT_BRACKET: "RIGHT_BRACKET.png",
KEY_SEMICOLON: "SEMICOLON.png",
KEY_COMMA: "COMMA.png",
KEY_PERIOD: "PERIOD.png",
KEY_BACK_SLASH: "BACK_SLASH.png",
KEY_FORWARD_SLASH: "FORWARD_SLASH.png",
KEY_BACKSPACE: "BACK.png",
KEY_ALT: "ALT.png",

View File

@ -9,3 +9,8 @@ export const SESSION_ID_COOKIE_NAME: string = "pokerogue_sessionId";
/** Max value for an integer attribute in {@linkcode SystemSaveData} */
export const MAX_INT_ATTR_VALUE = 0x80000000;
/** The min and max waves for mystery encounters to spawn in classic mode */
export const CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES: [number, number] = [10, 180] as const;
/** The min and max waves for mystery encounters to spawn in challenge mode */
export const CHALLENGE_MODE_MYSTERY_ENCOUNTER_WAVES: [number, number] = [10, 180] as const;

View File

@ -72,6 +72,7 @@ import type { AbAttrCondition, PokemonDefendCondition, PokemonStatStageChangeCon
import type { BattlerIndex } from "#app/battle";
import type Move from "#app/data/moves/move";
import type { ArenaTrapTag, SuppressAbilitiesTag } from "#app/data/arena-tag";
import { SelectBiomePhase } from "#app/phases/select-biome-phase";
export class BlockRecoilDamageAttr extends AbAttr {
constructor() {
@ -653,8 +654,8 @@ export class MoveImmunityStatStageChangeAbAttr extends MoveImmunityAbAttr {
*/
export class ReverseDrainAbAttr extends PostDefendAbAttr {
override canApplyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult | null, args: any[]): boolean {
return move.hasAttr(HitHealAttr) && !move.hitsSubstitute(attacker, pokemon);
override canApplyPostDefend(_pokemon: Pokemon, _passive: boolean, _simulated: boolean, _attacker: Pokemon, move: Move, _hitResult: HitResult | null, args: any[]): boolean {
return move.hasAttr(HitHealAttr);
}
/**
@ -693,7 +694,7 @@ export class PostDefendStatStageChangeAbAttr extends PostDefendAbAttr {
}
override canApplyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult | null, args: any[]): boolean {
return this.condition(pokemon, attacker, move) && !move.hitsSubstitute(attacker, pokemon);
return this.condition(pokemon, attacker, move);
}
override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, _args: any[]): void {
@ -734,7 +735,7 @@ export class PostDefendHpGatedStatStageChangeAbAttr extends PostDefendAbAttr {
const hpGateFlat: number = Math.ceil(pokemon.getMaxHp() * this.hpGate);
const lastAttackReceived = pokemon.turnData.attacksReceived[pokemon.turnData.attacksReceived.length - 1];
const damageReceived = lastAttackReceived?.damage || 0;
return this.condition(pokemon, attacker, move) && (pokemon.hp <= hpGateFlat && (pokemon.hp + damageReceived) > hpGateFlat) && !move.hitsSubstitute(attacker, pokemon);
return this.condition(pokemon, attacker, move) && (pokemon.hp <= hpGateFlat && (pokemon.hp + damageReceived) > hpGateFlat);
}
override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, _args: any[]): void {
@ -757,7 +758,7 @@ export class PostDefendApplyArenaTrapTagAbAttr extends PostDefendAbAttr {
override canApplyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult | null, args: any[]): boolean {
const tag = globalScene.arena.getTag(this.tagType) as ArenaTrapTag;
return (this.condition(pokemon, attacker, move) && !move.hitsSubstitute(attacker, pokemon))
return (this.condition(pokemon, attacker, move))
&& (!globalScene.arena.getTag(this.tagType) || tag.layers < tag.maxLayers);
}
@ -779,7 +780,7 @@ export class PostDefendApplyBattlerTagAbAttr extends PostDefendAbAttr {
}
override canApplyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult | null, args: any[]): boolean {
return this.condition(pokemon, attacker, move) && !move.hitsSubstitute(attacker, pokemon);
return this.condition(pokemon, attacker, move);
}
override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, _args: any[]): void {
@ -796,7 +797,7 @@ export class PostDefendTypeChangeAbAttr extends PostDefendAbAttr {
override canApplyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean {
this.type = attacker.getMoveType(move);
const pokemonTypes = pokemon.getTypes(true);
return hitResult < HitResult.NO_EFFECT && !move.hitsSubstitute(attacker, pokemon) && (simulated || pokemonTypes.length !== 1 || pokemonTypes[0] !== this.type);
return hitResult < HitResult.NO_EFFECT && (simulated || pokemonTypes.length !== 1 || pokemonTypes[0] !== this.type);
}
override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, _args: any[]): void {
@ -823,7 +824,7 @@ export class PostDefendTerrainChangeAbAttr extends PostDefendAbAttr {
}
override canApplyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean {
return hitResult < HitResult.NO_EFFECT && !move.hitsSubstitute(attacker, pokemon) && globalScene.arena.canSetTerrain(this.terrainType);
return hitResult < HitResult.NO_EFFECT && globalScene.arena.canSetTerrain(this.terrainType);
}
override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, _args: any[]): void {
@ -847,7 +848,7 @@ export class PostDefendContactApplyStatusEffectAbAttr extends PostDefendAbAttr {
override canApplyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult | null, args: any[]): boolean {
const effect = this.effects.length === 1 ? this.effects[0] : this.effects[pokemon.randSeedInt(this.effects.length)];
return move.doesFlagEffectApply({flag: MoveFlags.MAKES_CONTACT, user: attacker, target: pokemon}) && !attacker.status
&& (this.chance === -1 || pokemon.randSeedInt(100) < this.chance) && !move.hitsSubstitute(attacker, pokemon)
&& (this.chance === -1 || pokemon.randSeedInt(100) < this.chance)
&& attacker.canSetStatus(effect, true, false, pokemon);
}
@ -887,7 +888,7 @@ export class PostDefendContactApplyTagChanceAbAttr extends PostDefendAbAttr {
override canApplyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult | null, args: any[]): boolean {
return move.doesFlagEffectApply({flag: MoveFlags.MAKES_CONTACT, user: attacker, target: pokemon}) && pokemon.randSeedInt(100) < this.chance
&& !move.hitsSubstitute(attacker, pokemon) && attacker.canAddTag(this.tagType);
&& attacker.canAddTag(this.tagType);
}
override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, _args: any[]): void {
@ -908,10 +909,6 @@ export class PostDefendCritStatStageChangeAbAttr extends PostDefendAbAttr {
this.stages = stages;
}
override canApplyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult | null, args: any[]): boolean {
return !move.hitsSubstitute(attacker, pokemon);
}
override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, _args: any[]): void {
if (!simulated) {
globalScene.unshiftPhase(new StatStageChangePhase(pokemon.getBattlerIndex(), true, [ this.stat ], this.stages));
@ -934,7 +931,7 @@ export class PostDefendContactDamageAbAttr extends PostDefendAbAttr {
override canApplyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult | null, args: any[]): boolean {
return !simulated && move.doesFlagEffectApply({flag: MoveFlags.MAKES_CONTACT, user: attacker, target: pokemon})
&& !attacker.hasAbilityWithAttr(BlockNonDirectDamageAbAttr) && !move.hitsSubstitute(attacker, pokemon);
&& !attacker.hasAbilityWithAttr(BlockNonDirectDamageAbAttr);
}
override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, _args: any[]): void {
@ -993,7 +990,7 @@ export class PostDefendWeatherChangeAbAttr extends PostDefendAbAttr {
}
override canApplyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult | null, args: any[]): boolean {
return (!(this.condition && !this.condition(pokemon, attacker, move) || move.hitsSubstitute(attacker, pokemon))
return (!(this.condition && !this.condition(pokemon, attacker, move))
&& !globalScene.arena.weather?.isImmutable() && globalScene.arena.canSetWeather(this.weatherType));
}
@ -1011,7 +1008,7 @@ export class PostDefendAbilitySwapAbAttr extends PostDefendAbAttr {
override canApplyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult | null, args: any[]): boolean {
return move.doesFlagEffectApply({flag: MoveFlags.MAKES_CONTACT, user: attacker, target: pokemon})
&& attacker.getAbility().isSwappable && !move.hitsSubstitute(attacker, pokemon);
&& attacker.getAbility().isSwappable;
}
override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, args: any[]): void {
@ -1037,10 +1034,10 @@ export class PostDefendAbilityGiveAbAttr extends PostDefendAbAttr {
override canApplyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult | null, args: any[]): boolean {
return move.doesFlagEffectApply({flag: MoveFlags.MAKES_CONTACT, user: attacker, target: pokemon}) && attacker.getAbility().isSuppressable
&& !attacker.getAbility().hasAttr(PostDefendAbilityGiveAbAttr) && !move.hitsSubstitute(attacker, pokemon);
&& !attacker.getAbility().hasAttr(PostDefendAbilityGiveAbAttr);
}
override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, _args: any[]): void {
override applyPostDefend(_pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, _args: any[]): void {
if (!simulated) {
attacker.setTempAbility(allAbilities[this.ability]);
}
@ -1066,7 +1063,7 @@ export class PostDefendMoveDisableAbAttr extends PostDefendAbAttr {
}
override canApplyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult | null, args: any[]): boolean {
return attacker.getTag(BattlerTagType.DISABLED) === null && !move.hitsSubstitute(attacker, pokemon)
return attacker.getTag(BattlerTagType.DISABLED) === null
&& move.doesFlagEffectApply({flag: MoveFlags.MAKES_CONTACT, user: attacker, target: pokemon}) && (this.chance === -1 || pokemon.randSeedInt(100) < this.chance);
}
@ -1770,7 +1767,6 @@ export class PostAttackApplyStatusEffectAbAttr extends PostAttackAbAttr {
override canApplyPostAttack(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult | null, args: any[]): boolean {
if (
super.canApplyPostAttack(pokemon, passive, simulated, attacker, move, hitResult, args)
&& !(pokemon !== attacker && move.hitsSubstitute(attacker, pokemon))
&& (simulated || !attacker.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && pokemon !== attacker
&& (!this.contactRequired || move.doesFlagEffectApply({flag: MoveFlags.MAKES_CONTACT, user: attacker, target: pokemon})) && pokemon.randSeedInt(100) < this.chance && !pokemon.status)
) {
@ -1837,8 +1833,7 @@ export class PostDefendStealHeldItemAbAttr extends PostDefendAbAttr {
if (
!simulated &&
hitResult < HitResult.NO_EFFECT &&
(!this.condition || this.condition(pokemon, attacker, move)) &&
!move.hitsSubstitute(attacker, pokemon)
(!this.condition || this.condition(pokemon, attacker, move))
) {
const heldItems = this.getTargetHeldItems(attacker).filter((i) => i.isTransferable);
if (heldItems.length) {
@ -5063,6 +5058,8 @@ export class PostSummonStatStageChangeOnArenaAbAttr extends PostSummonStatStageC
/**
* Takes no damage from the first hit of a damaging move.
* This is used in the Disguise and Ice Face abilities.
*
* Does not apply to a user's substitute
* @extends ReceivedMoveDamageMultiplierAbAttr
*/
export class FormBlockDamageAbAttr extends ReceivedMoveDamageMultiplierAbAttr {
@ -5487,6 +5484,11 @@ class ForceSwitchOutHelper {
if (switchOutTarget.hp) {
globalScene.pushPhase(new BattleEndPhase(false));
if (globalScene.gameMode.hasRandomBiomes || globalScene.isNewBiome()) {
globalScene.pushPhase(new SelectBiomePhase());
}
globalScene.pushPhase(new NewBattlePhase());
}
}
@ -7225,7 +7227,7 @@ export function initAbilities() {
new Ability(Abilities.CURIOUS_MEDICINE, 8)
.attr(PostSummonClearAllyStatStagesAbAttr),
new Ability(Abilities.TRANSISTOR, 8)
.attr(MoveTypePowerBoostAbAttr, PokemonType.ELECTRIC),
.attr(MoveTypePowerBoostAbAttr, PokemonType.ELECTRIC, 1.3),
new Ability(Abilities.DRAGONS_MAW, 8)
.attr(MoveTypePowerBoostAbAttr, PokemonType.DRAGON),
new Ability(Abilities.CHILLING_NEIGH, 8)

View File

@ -7,7 +7,7 @@ import { MoveTarget } from "#enums/MoveTarget";
import { MoveCategory } from "#enums/MoveCategory";
import { getPokemonNameWithAffix } from "#app/messages";
import type Pokemon from "#app/field/pokemon";
import { HitResult, PokemonMove } from "#app/field/pokemon";
import { HitResult } from "#app/field/pokemon";
import { StatusEffect } from "#enums/status-effect";
import type { BattlerIndex } from "#app/battle";
import {
@ -335,7 +335,7 @@ export class ConditionalProtectTag extends ArenaTag {
* @param arena the {@linkcode Arena} containing this tag
* @param simulated `true` if the tag is applied quietly; `false` otherwise.
* @param isProtected a {@linkcode BooleanHolder} used to flag if the move is protected against
* @param attacker the attacking {@linkcode Pokemon}
* @param _attacker the attacking {@linkcode Pokemon}
* @param defender the defending {@linkcode Pokemon}
* @param moveId the {@linkcode Moves | identifier} for the move being used
* @param ignoresProtectBypass a {@linkcode BooleanHolder} used to flag if a protection effect supercedes effects that ignore protection
@ -345,7 +345,7 @@ export class ConditionalProtectTag extends ArenaTag {
arena: Arena,
simulated: boolean,
isProtected: BooleanHolder,
attacker: Pokemon,
_attacker: Pokemon,
defender: Pokemon,
moveId: Moves,
ignoresProtectBypass: BooleanHolder,
@ -354,8 +354,6 @@ export class ConditionalProtectTag extends ArenaTag {
if (!isProtected.value) {
isProtected.value = true;
if (!simulated) {
attacker.stopMultiHit(defender);
new CommonBattleAnim(CommonAnim.PROTECT, defender).play();
globalScene.queueMessage(
i18next.t("arenaTag:conditionalProtectApply", {
@ -899,7 +897,7 @@ export class DelayedAttackTag extends ArenaTag {
if (!ret) {
globalScene.unshiftPhase(
new MoveEffectPhase(this.sourceId!, [this.targetIndex], new PokemonMove(this.sourceMove!, 0, 0, true)),
new MoveEffectPhase(this.sourceId!, [this.targetIndex], allMoves[this.sourceMove!], false, true),
); // TODO: are those bangs correct?
}

View File

@ -2637,7 +2637,7 @@ export class GulpMissileTag extends BattlerTag {
return false;
}
if (moveEffectPhase.move.getMove().hitsSubstitute(attacker, pokemon)) {
if (moveEffectPhase.move.hitsSubstitute(attacker, pokemon)) {
return true;
}
@ -2993,7 +2993,7 @@ export class SubstituteTag extends BattlerTag {
if (!attacker) {
return;
}
const move = moveEffectPhase.move.getMove();
const move = moveEffectPhase.move;
const firstHit = attacker.turnData.hitCount === attacker.turnData.hitsLeft;
if (firstHit && move.hitsSubstitute(attacker, pokemon)) {
@ -3681,7 +3681,7 @@ function getMoveEffectPhaseData(_pokemon: Pokemon): { phase: MoveEffectPhase; at
return {
phase: phase,
attacker: phase.getPokemon(),
move: phase.move.getMove(),
move: phase.move,
};
}
return null;

View File

@ -8,7 +8,8 @@ import { speciesStarterCosts } from "#app/data/balance/starters";
import type Pokemon from "#app/field/pokemon";
import { PokemonMove } from "#app/field/pokemon";
import type { FixedBattleConfig } from "#app/battle";
import { ClassicFixedBossWaves, getRandomTrainerFunc } from "#app/battle";
import { getRandomTrainerFunc } from "#app/battle";
import { ClassicFixedBossWaves } from "#enums/fixed-boss-waves";
import { BattleType } from "#enums/battle-type";
import Trainer, { TrainerVariant } from "#app/field/trainer";
import { PokemonType } from "#enums/pokemon-type";

View File

@ -0,0 +1,20 @@
import { MoveTarget } from "#enums/MoveTarget";
import type Move from "./move";
/**
* Return whether the move targets the field
*
* Examples include
* - Hazard moves like spikes
* - Weather moves like rain dance
* - User side moves like reflect and safeguard
*/
export function isFieldTargeted(move: Move): boolean {
switch (move.moveTarget) {
case MoveTarget.BOTH_SIDES:
case MoveTarget.USER_SIDE:
case MoveTarget.ENEMY_SIDE:
return true;
}
return false;
}

View File

@ -60,6 +60,7 @@ import {
MoveTypeChangeAbAttr,
PostDamageForceSwitchAbAttr,
PostItemLostAbAttr,
ReflectStatusMoveAbAttr,
ReverseDrainAbAttr,
UserFieldMoveTypePowerBoostAbAttr,
VariableMovePowerAbAttr,
@ -122,6 +123,7 @@ import { MoveEffectTrigger } from "#enums/MoveEffectTrigger";
import { MultiHitType } from "#enums/MultiHitType";
import { invalidAssistMoves, invalidCopycatMoves, invalidMetronomeMoves, invalidMirrorMoveMoves, invalidSleepTalkMoves } from "./invalid-moves";
import { TrainerVariant } from "#app/field/trainer";
import { SelectBiomePhase } from "#app/phases/select-biome-phase";
type MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => boolean;
type UserMoveConditionFunc = (user: Pokemon, move: Move) => boolean;
@ -665,6 +667,17 @@ export default class Move implements Localizable {
return true;
}
break;
case MoveFlags.REFLECTABLE:
// If the target is not semi-invulnerable and either has magic coat active or an unignored magic bounce ability
if (
target?.getTag(SemiInvulnerableTag) ||
!(target?.getTag(BattlerTagType.MAGIC_COAT) ||
(!this.doesFlagEffectApply({ flag: MoveFlags.IGNORE_ABILITIES, user, target }) &&
target?.hasAbilityWithAttr(ReflectStatusMoveAbAttr)))
) {
return false;
}
break;
}
return !!(this.flags & flag);
@ -1716,7 +1729,7 @@ export class SacrificialAttr extends MoveEffectAttr {
**/
export class SacrificialAttrOnHit extends MoveEffectAttr {
constructor() {
super(true, { trigger: MoveEffectTrigger.HIT });
super(true);
}
/**
@ -1955,6 +1968,14 @@ export class PartyStatusCureAttr extends MoveEffectAttr {
* @extends MoveEffectAttr
*/
export class FlameBurstAttr extends MoveEffectAttr {
constructor() {
/**
* This is self-targeted to bypass immunity to target-facing secondary
* effects when the target has an active Substitute doll.
* TODO: Find a more intuitive way to implement Substitute bypassing.
*/
super(true);
}
/**
* @param user - n/a
* @param target - The target Pokémon.
@ -2177,7 +2198,7 @@ export class HitHealAttr extends MoveEffectAttr {
private healStat: EffectiveStat | null;
constructor(healRatio?: number | null, healStat?: EffectiveStat) {
super(true, { trigger: MoveEffectTrigger.HIT });
super(true);
this.healRatio = healRatio ?? 0.5;
this.healStat = healStat ?? null;
@ -2426,7 +2447,7 @@ export class StatusEffectAttr extends MoveEffectAttr {
public overrideStatus: boolean = false;
constructor(effect: StatusEffect, selfTarget?: boolean, turnsRemaining?: number, overrideStatus: boolean = false) {
super(selfTarget, { trigger: MoveEffectTrigger.HIT });
super(selfTarget);
this.effect = effect;
this.turnsRemaining = turnsRemaining;
@ -2434,22 +2455,11 @@ export class StatusEffectAttr extends MoveEffectAttr {
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (!this.selfTarget && move.hitsSubstitute(user, target)) {
return false;
}
const moveChance = this.getMoveChance(user, target, move, this.selfTarget, true);
const statusCheck = moveChance < 0 || moveChance === 100 || user.randSeedInt(100) < moveChance;
if (statusCheck) {
const pokemon = this.selfTarget ? user : target;
if (pokemon.status && !this.overrideStatus) {
return false;
}
if (user !== target && target.isSafeguarded(user)) {
if (move.category === MoveCategory.STATUS) {
globalScene.queueMessage(i18next.t("moveTriggers:safeguard", { targetName: getPokemonNameWithAffix(target) }));
}
if (user !== target && move.category === MoveCategory.STATUS && !target.canSetStatus(this.effect, false, false, user, true)) {
return false;
}
if (((!pokemon.status || this.overrideStatus) || (pokemon.status.effect === this.effect && moveChance < 0))
@ -2495,7 +2505,7 @@ export class MultiStatusEffectAttr extends StatusEffectAttr {
export class PsychoShiftEffectAttr extends MoveEffectAttr {
constructor() {
super(false, { trigger: MoveEffectTrigger.HIT });
super(false);
}
/**
@ -2534,15 +2544,11 @@ export class StealHeldItemChanceAttr extends MoveEffectAttr {
private chance: number;
constructor(chance: number) {
super(false, { trigger: MoveEffectTrigger.HIT });
super(false);
this.chance = chance;
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (move.hitsSubstitute(user, target)) {
return false;
}
const rand = Phaser.Math.RND.realInRange(0, 1);
if (rand >= this.chance) {
return false;
@ -2590,7 +2596,7 @@ export class RemoveHeldItemAttr extends MoveEffectAttr {
private berriesOnly: boolean;
constructor(berriesOnly: boolean) {
super(false, { trigger: MoveEffectTrigger.HIT });
super(false);
this.berriesOnly = berriesOnly;
}
@ -2600,17 +2606,13 @@ export class RemoveHeldItemAttr extends MoveEffectAttr {
* @param target Target {@linkcode Pokemon} that the moves applies to
* @param move {@linkcode Move} that is used
* @param args N/A
* @returns {boolean} True if an item was removed
* @returns True if an item was removed
*/
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (!this.berriesOnly && target.isPlayer()) { // "Wild Pokemon cannot knock off Player Pokemon's held items" (See Bulbapedia)
return false;
}
if (move.hitsSubstitute(user, target)) {
return false;
}
const cancelled = new BooleanHolder(false);
applyAbAttrs(BlockItemTheftAbAttr, target, cancelled); // Check for abilities that block item theft
@ -2664,8 +2666,8 @@ export class RemoveHeldItemAttr extends MoveEffectAttr {
*/
export class EatBerryAttr extends MoveEffectAttr {
protected chosenBerry: BerryModifier | undefined;
constructor() {
super(true, { trigger: MoveEffectTrigger.HIT });
constructor(selfTarget: boolean) {
super(selfTarget);
}
/**
* Causes the target to eat a berry.
@ -2680,17 +2682,20 @@ export class EatBerryAttr extends MoveEffectAttr {
return false;
}
const heldBerries = this.getTargetHeldBerries(target);
const pokemon = this.selfTarget ? user : target;
const heldBerries = this.getTargetHeldBerries(pokemon);
if (heldBerries.length <= 0) {
return false;
}
this.chosenBerry = heldBerries[user.randSeedInt(heldBerries.length)];
const preserve = new BooleanHolder(false);
globalScene.applyModifiers(PreserveBerryModifier, target.isPlayer(), target, preserve); // check for berry pouch preservation
// check for berry pouch preservation
globalScene.applyModifiers(PreserveBerryModifier, pokemon.isPlayer(), pokemon, preserve);
if (!preserve.value) {
this.reduceBerryModifier(target);
this.reduceBerryModifier(pokemon);
}
this.eatBerry(target);
this.eatBerry(pokemon);
return true;
}
@ -2718,20 +2723,17 @@ export class EatBerryAttr extends MoveEffectAttr {
*/
export class StealEatBerryAttr extends EatBerryAttr {
constructor() {
super();
super(false);
}
/**
* User steals a random berry from the target and then eats it.
* @param {Pokemon} user Pokemon that used the move and will eat the stolen berry
* @param {Pokemon} target Pokemon that will have its berry stolen
* @param {Move} move Move being used
* @param {any[]} args Unused
* @returns {boolean} true if the function succeeds
* @param user - Pokemon that used the move and will eat the stolen berry
* @param target - Pokemon that will have its berry stolen
* @param move - Move being used
* @param args Unused
* @returns true if the function succeeds
*/
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (move.hitsSubstitute(user, target)) {
return false;
}
const cancelled = new BooleanHolder(false);
applyAbAttrs(BlockItemTheftAbAttr, target, cancelled); // check for abilities that block item theft
if (cancelled.value === true) {
@ -2782,10 +2784,6 @@ export class HealStatusEffectAttr extends MoveEffectAttr {
return false;
}
if (!this.selfTarget && move.hitsSubstitute(user, target)) {
return false;
}
// Special edge case for shield dust blocking Sparkling Aria curing burn
const moveTargets = getMoveTargets(user, move.id);
if (target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && move.id === Moves.SPARKLING_ARIA && moveTargets.targets.length === 1) {
@ -3163,14 +3161,6 @@ export class StatStageChangeAttr extends MoveEffectAttr {
return this.options?.showMessage ?? true;
}
/**
* Indicates when the stat change should trigger
* @default MoveEffectTrigger.HIT
*/
public override get trigger () {
return this.options?.trigger ?? MoveEffectTrigger.HIT;
}
/**
* Attempts to change stats of the user or target (depending on value of selfTarget) if conditions are met
* @param user {@linkcode Pokemon} the user of the move
@ -3184,10 +3174,6 @@ export class StatStageChangeAttr extends MoveEffectAttr {
return false;
}
if (!this.selfTarget && move.hitsSubstitute(user, target)) {
return false;
}
const moveChance = this.getMoveChance(user, target, move, this.selfTarget, true);
if (moveChance < 0 || moveChance === 100 || user.randSeedInt(100) < moveChance) {
const stages = this.getLevels(user);
@ -3471,7 +3457,7 @@ export class CutHpStatStageBoostAttr extends StatStageChangeAttr {
*/
export class OrderUpStatBoostAttr extends MoveEffectAttr {
constructor() {
super(true, { trigger: MoveEffectTrigger.HIT });
super(true);
}
override apply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean {
@ -3548,18 +3534,16 @@ export class ResetStatsAttr extends MoveEffectAttr {
this.targetAllPokemon = targetAllPokemon;
}
override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
override apply(_user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean {
if (this.targetAllPokemon) {
// Target all pokemon on the field when Freezy Frost or Haze are used
const activePokemon = globalScene.getField(true);
activePokemon.forEach((p) => this.resetStats(p));
globalScene.queueMessage(i18next.t("moveTriggers:statEliminated"));
} else { // Affects only the single target when Clear Smog is used
if (!move.hitsSubstitute(user, target)) {
this.resetStats(target);
globalScene.queueMessage(i18next.t("moveTriggers:resetStats", { pokemonName: getPokemonNameWithAffix(target) }));
}
}
return true;
}
@ -4217,7 +4201,8 @@ export class PresentPowerAttr extends VariablePowerAttr {
(args[0] as NumberHolder).value = 120;
} else if (80 < powerSeed && powerSeed <= 100) {
// If this move is multi-hit, disable all other hits
user.stopMultiHit();
user.turnData.hitCount = 1;
user.turnData.hitsLeft = 1;
globalScene.unshiftPhase(new PokemonHealPhase(target.getBattlerIndex(),
toDmgValue(target.getMaxHp() / 4), i18next.t("moveTriggers:regainedHealth", { pokemonName: getPokemonNameWithAffix(target) }), true));
}
@ -4811,8 +4796,8 @@ export class ShellSideArmCategoryAttr extends VariableMoveCategoryAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const category = (args[0] as NumberHolder);
const predictedPhysDmg = target.getBaseDamage(user, move, MoveCategory.PHYSICAL, true, true, true, true);
const predictedSpecDmg = target.getBaseDamage(user, move, MoveCategory.SPECIAL, true, true, true, true);
const predictedPhysDmg = target.getBaseDamage({source: user, move, moveCategory: MoveCategory.PHYSICAL, ignoreAbility: true, ignoreSourceAbility: true, ignoreAllyAbility: true, ignoreSourceAllyAbility: true, simulated: true});
const predictedSpecDmg = target.getBaseDamage({source: user, move, moveCategory: MoveCategory.SPECIAL, ignoreAbility: true, ignoreSourceAbility: true, ignoreAllyAbility: true, ignoreSourceAllyAbility: true, simulated: true});
if (predictedPhysDmg > predictedSpecDmg) {
category.value = MoveCategory.PHYSICAL;
@ -5371,7 +5356,7 @@ export class BypassRedirectAttr extends MoveAttr {
export class FrenzyAttr extends MoveEffectAttr {
constructor() {
super(true, { trigger: MoveEffectTrigger.HIT, lastHitOnly: true });
super(true, { lastHitOnly: true });
}
canApply(user: Pokemon, target: Pokemon, move: Move, args: any[]) {
@ -5443,22 +5428,20 @@ export class AddBattlerTagAttr extends MoveEffectAttr {
protected cancelOnFail: boolean;
private failOnOverlap: boolean;
constructor(tagType: BattlerTagType, selfTarget: boolean = false, failOnOverlap: boolean = false, turnCountMin: number = 0, turnCountMax?: number, lastHitOnly: boolean = false, cancelOnFail: boolean = false) {
constructor(tagType: BattlerTagType, selfTarget: boolean = false, failOnOverlap: boolean = false, turnCountMin: number = 0, turnCountMax?: number, lastHitOnly: boolean = false) {
super(selfTarget, { lastHitOnly: lastHitOnly });
this.tagType = tagType;
this.turnCountMin = turnCountMin;
this.turnCountMax = turnCountMax !== undefined ? turnCountMax : turnCountMin;
this.failOnOverlap = !!failOnOverlap;
this.cancelOnFail = cancelOnFail;
}
canApply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (!super.canApply(user, target, move, args) || (this.cancelOnFail === true && user.getLastXMoves(1)[0]?.result === MoveResult.FAIL)) {
if (!super.canApply(user, target, move, args)) {
return false;
} else {
return true;
}
return true;
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
@ -5549,19 +5532,6 @@ export class LeechSeedAttr extends AddBattlerTagAttr {
constructor() {
super(BattlerTagType.SEEDED);
}
/**
* Adds a Seeding effect to the target if the target does not have an active Substitute.
* @param user the {@linkcode Pokemon} using the move
* @param target the {@linkcode Pokemon} targeted by the move
* @param move the {@linkcode Move} invoking this effect
* @param args n/a
* @returns `true` if the effect successfully applies; `false` otherwise
*/
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
return !move.hitsSubstitute(user, target)
&& super.apply(user, target, move, args);
}
}
/**
@ -5737,13 +5707,6 @@ export class FlinchAttr extends AddBattlerTagAttr {
constructor() {
super(BattlerTagType.FLINCHED, false);
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (!move.hitsSubstitute(user, target)) {
return super.apply(user, target, move, args);
}
return false;
}
}
export class ConfuseAttr extends AddBattlerTagAttr {
@ -5759,16 +5722,13 @@ export class ConfuseAttr extends AddBattlerTagAttr {
return false;
}
if (!move.hitsSubstitute(user, target)) {
return super.apply(user, target, move, args);
}
return false;
}
}
export class RechargeAttr extends AddBattlerTagAttr {
constructor() {
super(BattlerTagType.RECHARGING, true, false, 1, 1, true, true);
super(BattlerTagType.RECHARGING, true, false, 1, 1, true);
}
}
@ -6151,7 +6111,7 @@ export class AddPledgeEffectAttr extends AddArenaTagAttr {
* @see {@linkcode apply}
*/
export class RevivalBlessingAttr extends MoveEffectAttr {
constructor(user?: boolean) {
constructor() {
super(true);
}
@ -6366,6 +6326,11 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
if (!allyPokemon?.isActive(true) && switchOutTarget.hp) {
globalScene.pushPhase(new BattleEndPhase(false));
if (globalScene.gameMode.hasRandomBiomes || globalScene.isNewBiome()) {
globalScene.pushPhase(new SelectBiomePhase());
}
globalScene.pushPhase(new NewBattlePhase());
}
}
@ -6392,10 +6357,6 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
const player = switchOutTarget instanceof PlayerPokemon;
if (!this.selfSwitch) {
if (move.hitsSubstitute(user, target)) {
return false;
}
// Dondozo with an allied Tatsugiri in its mouth cannot be forced out
const commandedTag = switchOutTarget.getTag(BattlerTagType.COMMANDED);
if (commandedTag?.getSourcePokemon()?.isActive(true)) {
@ -6650,7 +6611,7 @@ export class ChangeTypeAttr extends MoveEffectAttr {
private type: PokemonType;
constructor(type: PokemonType) {
super(false, { trigger: MoveEffectTrigger.HIT });
super(false);
this.type = type;
}
@ -6673,7 +6634,7 @@ export class AddTypeAttr extends MoveEffectAttr {
private type: PokemonType;
constructor(type: PokemonType) {
super(false, { trigger: MoveEffectTrigger.HIT });
super(false);
this.type = type;
}
@ -7369,7 +7330,7 @@ export class AbilityChangeAttr extends MoveEffectAttr {
public ability: Abilities;
constructor(ability: Abilities, selfTarget?: boolean) {
super(selfTarget, { trigger: MoveEffectTrigger.HIT });
super(selfTarget);
this.ability = ability;
}
@ -7400,7 +7361,7 @@ export class AbilityCopyAttr extends MoveEffectAttr {
public copyToPartner: boolean;
constructor(copyToPartner: boolean = false) {
super(false, { trigger: MoveEffectTrigger.HIT });
super(false);
this.copyToPartner = copyToPartner;
}
@ -7441,7 +7402,7 @@ export class AbilityGiveAttr extends MoveEffectAttr {
public copyToPartner: boolean;
constructor() {
super(false, { trigger: MoveEffectTrigger.HIT });
super(false);
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
@ -7704,23 +7665,9 @@ export class AverageStatsAttr extends MoveEffectAttr {
}
}
export class DiscourageFrequentUseAttr extends MoveAttr {
getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
const lastMoves = user.getLastXMoves(4);
console.log(lastMoves);
for (let m = 0; m < lastMoves.length; m++) {
if (lastMoves[m].move === move.id) {
return (4 - (m + 1)) * -10;
}
}
return 0;
}
}
export class MoneyAttr extends MoveEffectAttr {
constructor() {
super(true, { trigger: MoveEffectTrigger.HIT, firstHitOnly: true });
super(true, {firstHitOnly: true });
}
apply(user: Pokemon, target: Pokemon, move: Move): boolean {
@ -7787,7 +7734,7 @@ export class StatusIfBoostedAttr extends MoveEffectAttr {
public effect: StatusEffect;
constructor(effect: StatusEffect) {
super(true, { trigger: MoveEffectTrigger.HIT });
super(true);
this.effect = effect;
}
@ -10556,8 +10503,7 @@ export function initMoves() {
} else {
return 1;
}
})
.attr(DiscourageFrequentUseAttr),
}),
new AttackMove(Moves.SNIPE_SHOT, PokemonType.WATER, MoveCategory.SPECIAL, 80, 100, 15, -1, 0, 8)
.attr(HighCritAttr)
@ -10566,7 +10512,7 @@ export function initMoves() {
.attr(JawLockAttr)
.bitingMove(),
new SelfStatusMove(Moves.STUFF_CHEEKS, PokemonType.NORMAL, -1, 10, -1, 0, 8)
.attr(EatBerryAttr)
.attr(EatBerryAttr, true)
.attr(StatStageChangeAttr, [ Stat.DEF ], 2, true)
.condition((user) => {
const userBerries = globalScene.findModifiers(m => m instanceof BerryModifier, user.isPlayer());
@ -10590,7 +10536,7 @@ export function initMoves() {
.makesContact(false)
.partial(), // smart targetting is unimplemented
new StatusMove(Moves.TEATIME, PokemonType.NORMAL, -1, 10, -1, 0, 8)
.attr(EatBerryAttr)
.attr(EatBerryAttr, false)
.target(MoveTarget.ALL),
new StatusMove(Moves.OCTOLOCK, PokemonType.FIGHTING, 100, 15, -1, 0, 8)
.condition(failIfGhostTypeCondition)

View File

@ -22,7 +22,7 @@ import { EggTier } from "#enums/egg-type";
import { PartyHealPhase } from "#app/phases/party-heal-phase";
import { ModifierTier } from "#app/modifier/modifier-tier";
import { modifierTypes } from "#app/modifier/modifier-type";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
/** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/aTrainersTest";

View File

@ -37,7 +37,7 @@ import type HeldModifierConfig from "#app/interfaces/held-modifier-config";
import type { BerryType } from "#enums/berry-type";
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import { Stat } from "#enums/stat";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import i18next from "i18next";
/** the i18n namespace for this encounter */

View File

@ -23,7 +23,7 @@ import { speciesStarterCosts } from "#app/data/balance/starters";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { ModifierRewardPhase } from "#app/phases/modifier-reward-phase";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import i18next from "i18next";
/** the i18n namespace for this encounter */

View File

@ -36,7 +36,7 @@ import i18next from "#app/plugins/i18n";
import { BerryType } from "#enums/berry-type";
import { PERMANENT_STATS, Stat } from "#enums/stat";
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
/** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/berriesAbound";

View File

@ -52,7 +52,7 @@ import i18next from "i18next";
import MoveInfoOverlay from "#app/ui/move-info-overlay";
import { allMoves } from "#app/data/moves/move";
import { ModifierTier } from "#app/modifier/modifier-tier";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { getSpriteKeysFromSpecies } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
/** the i18n namespace for the encounter */

View File

@ -46,7 +46,7 @@ import { Moves } from "#enums/moves";
import { EncounterBattleAnim } from "#app/data/battle-anims";
import { MoveCategory } from "#enums/MoveCategory";
import { CustomPokemonData } from "#app/data/custom-pokemon-data";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { EncounterAnim } from "#enums/encounter-anims";
import { Challenges } from "#enums/challenges";

View File

@ -24,7 +24,7 @@ import { TrainerSlot } from "#enums/trainer-slot";
import type { PlayerPokemon } from "#app/field/pokemon";
import type Pokemon from "#app/field/pokemon";
import { EnemyPokemon, PokemonMove } from "#app/field/pokemon";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { modifierTypes } from "#app/modifier/modifier-type";
import { LearnMovePhase } from "#app/phases/learn-move-phase";
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";

View File

@ -19,7 +19,7 @@ import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode
import { ModifierRewardPhase } from "#app/phases/modifier-reward-phase";
import type { PokemonHeldItemModifier } from "#app/modifier/modifier";
import { PokemonFormChangeItemModifier } from "#app/modifier/modifier";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { Challenges } from "#enums/challenges";
/** i18n namespace for encounter */

View File

@ -18,7 +18,7 @@ import { applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/u
import { getPokemonSpecies } from "#app/data/pokemon-species";
import type { PlayerPokemon } from "#app/field/pokemon";
import type Pokemon from "#app/field/pokemon";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import type { PokemonHeldItemModifier, PokemonInstantReviveModifier } from "#app/modifier/modifier";
import {
BerryModifier,

View File

@ -10,7 +10,7 @@ import { Species } from "#enums/species";
import type MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter";
import { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
/** i18n namespace for encounter */
const namespace = "mysteryEncounters/departmentStoreSale";

View File

@ -18,7 +18,7 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { Stat } from "#enums/stat";
import i18next from "i18next";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
/** i18n namespace for the encounter */
const namespace = "mysteryEncounters/fieldTrip";

View File

@ -41,7 +41,7 @@ import {
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { EncounterAnim } from "#enums/encounter-anims";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { Abilities } from "#enums/abilities";
import { BattlerTagType } from "#enums/battler-tag-type";
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";

View File

@ -33,7 +33,7 @@ import { BattlerTagType } from "#enums/battler-tag-type";
import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { randSeedInt } from "#app/utils/common";
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
/** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/fightOrFlight";

View File

@ -30,7 +30,7 @@ import { SpeciesFormChangeActiveTrigger } from "#app/data/pokemon-forms";
import { PostSummonPhase } from "#app/phases/post-summon-phase";
import { modifierTypes } from "#app/modifier/modifier-type";
import { Nature } from "#enums/nature";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { isPokemonValidForEncounterOptionSelection } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
/** the i18n namespace for the encounter */

View File

@ -48,7 +48,7 @@ import { Gender, getGenderSymbol } from "#app/data/gender";
import { getNatureName } from "#app/data/nature";
import { getPokeballAtlasKey, getPokeballTintColor } from "#app/data/pokeball";
import { getEncounterText, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { addPokemonDataToDexAndValidateAchievements } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import type { PokeballType } from "#enums/pokeball";
import { doShinySparkleAnim } from "#app/field/anims";

View File

@ -10,7 +10,7 @@ import { leaveEncounterWithoutBattle, setEncounterExp } from "../utils/encounter
import { applyDamageToPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { PokemonMove } from "#app/field/pokemon";
const OPTION_1_REQUIRED_MOVE = Moves.SURF;

View File

@ -16,7 +16,7 @@ import { randSeedInt } from "#app/utils/common";
import type MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter";
import { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
/** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/mysteriousChallengers";

View File

@ -15,7 +15,7 @@ import {
koPlayerPokemon,
} from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { getPokemonSpecies } from "#app/data/pokemon-species";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { ModifierTier } from "#app/modifier/modifier-tier";
import { GameOverPhase } from "#app/phases/game-over-phase";
import { randSeedInt } from "#app/utils/common";

View File

@ -20,7 +20,7 @@ import { showEncounterDialogue, showEncounterText } from "#app/data/mystery-enco
import i18next from "i18next";
import type { PlayerPokemon } from "#app/field/pokemon";
import type Pokemon from "#app/field/pokemon";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { isPokemonValidForEncounterOptionSelection } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
/** the i18n namespace for the encounter */

View File

@ -31,7 +31,7 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { ScanIvsPhase } from "#app/phases/scan-ivs-phase";
import { SummonPhase } from "#app/phases/summon-phase";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { NON_LEGEND_PARADOX_POKEMON } from "#app/data/balance/special-species-groups";
/** the i18n namespace for the encounter */

View File

@ -26,7 +26,7 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import type { Nature } from "#enums/nature";
import { getNatureName } from "#app/data/nature";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import i18next from "i18next";
/** the i18n namespace for this encounter */

View File

@ -26,7 +26,7 @@ import { getPokemonSpecies } from "#app/data/pokemon-species";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { PartyHealPhase } from "#app/phases/party-heal-phase";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { BerryType } from "#enums/berry-type";
import { CustomPokemonData } from "#app/data/custom-pokemon-data";

View File

@ -29,7 +29,7 @@ import { BattlerTagType } from "#enums/battler-tag-type";
import { getPokemonNameWithAffix } from "#app/messages";
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import { Stat } from "#enums/stat";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import {
getEncounterPokemonLevelForWave,
STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER,

View File

@ -11,7 +11,7 @@ import { randSeedShuffle } from "#app/utils/common";
import type MysteryEncounter from "../mystery-encounter";
import { MysteryEncounterBuilder } from "../mystery-encounter";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { Biome } from "#enums/biome";
import { TrainerType } from "#enums/trainer-type";
import i18next from "i18next";

View File

@ -3,7 +3,7 @@ import {
transitionMysteryEncounterIntroVisuals,
updatePlayerMoney,
} from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { isNullOrUndefined, randSeedInt } from "#app/utils/common";
import { isNullOrUndefined, NumberHolder, randSeedInt, randSeedItem } from "#app/utils/common";
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { globalScene } from "#app/global-scene";
import type MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter";
@ -26,9 +26,10 @@ import { showEncounterDialogue } from "#app/data/mystery-encounters/utils/encoun
import PokemonData from "#app/system/pokemon-data";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { Abilities } from "#enums/abilities";
import { NON_LEGEND_PARADOX_POKEMON } from "#app/data/balance/special-species-groups";
import { NON_LEGEND_PARADOX_POKEMON, NON_LEGEND_ULTRA_BEASTS } from "#app/data/balance/special-species-groups";
import { timedEventManager } from "#app/global-event-manager";
/** the i18n namespace for this encounter */
const namespace = "mysteryEncounters/thePokemonSalesman";
@ -38,6 +39,9 @@ const MAX_POKEMON_PRICE_MULTIPLIER = 4;
/** Odds of shiny magikarp will be 1/value */
const SHINY_MAGIKARP_WEIGHT = 100;
/** Odds of event sale will be value/100 */
const EVENT_THRESHOLD = 50;
/**
* Pokemon Salesman encounter.
* @see {@link https://github.com/pagefaultgames/pokerogue/issues/3799 | GitHub Issue #3799}
@ -82,15 +86,46 @@ export const ThePokemonSalesmanEncounter: MysteryEncounter = MysteryEncounterBui
tries++;
}
const r = randSeedInt(SHINY_MAGIKARP_WEIGHT);
const validEventEncounters = timedEventManager
.getEventEncounters()
.filter(
s =>
!getPokemonSpecies(s.species).legendary &&
!getPokemonSpecies(s.species).subLegendary &&
!getPokemonSpecies(s.species).mythical &&
!NON_LEGEND_PARADOX_POKEMON.includes(s.species) &&
!NON_LEGEND_ULTRA_BEASTS.includes(s.species),
);
let pokemon: PlayerPokemon;
/**
* Mon is determined as follows:
* If you roll the 1% for Shiny Magikarp, you get Magikarp with a random variant
* If an event with more than 1 valid event encounter species is active, you have 20% chance to get one of those
* If the rolled species has no HA, and there are valid event encounters, you will get one of those
* If the rolled species has no HA and there are no valid event encounters, you will get Shiny Magikarp
* Mons rolled from the event encounter pool get 2 extra shiny rolls
*/
if (
randSeedInt(SHINY_MAGIKARP_WEIGHT) === 0 ||
isNullOrUndefined(species.abilityHidden) ||
species.abilityHidden === Abilities.NONE
r === 0 ||
((isNullOrUndefined(species.abilityHidden) || species.abilityHidden === Abilities.NONE) &&
(validEventEncounters.length === 0))
) {
// If no HA mon found or you roll 1%, give shiny Magikarp with random variant
// If you roll 1%, give shiny Magikarp with random variant
species = getPokemonSpecies(Species.MAGIKARP);
pokemon = new PlayerPokemon(species, 5, 2, species.formIndex, undefined, true);
pokemon = new PlayerPokemon(species, 5, 2, undefined, undefined, true);
} else if (
(validEventEncounters.length > 0 && (r <= EVENT_THRESHOLD ||
(isNullOrUndefined(species.abilityHidden) || species.abilityHidden === Abilities.NONE)))
) {
// If you roll 20%, give event encounter with 2 extra shiny rolls and its HA, if it has one
const enc = randSeedItem(validEventEncounters);
species = getPokemonSpecies(enc.species);
pokemon = new PlayerPokemon(species, 5, species.abilityHidden === Abilities.NONE ? undefined : 2, enc.formIndex);
pokemon.trySetShinySeed();
pokemon.trySetShinySeed();
} else {
pokemon = new PlayerPokemon(species, 5, 2, species.formIndex);
}

View File

@ -28,7 +28,7 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { CustomPokemonData } from "#app/data/custom-pokemon-data";
import { Stat } from "#enums/stat";
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
/** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/theStrongStuff";

View File

@ -32,7 +32,7 @@ import { ShowTrainerPhase } from "#app/phases/show-trainer-phase";
import { ReturnPhase } from "#app/phases/return-phase";
import i18next from "i18next";
import { ModifierTier } from "#app/modifier/modifier-tier";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { BattlerTagType } from "#enums/battler-tag-type";
/** the i18n namespace for the encounter */

View File

@ -28,7 +28,7 @@ import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode
import type HeldModifierConfig from "#app/interfaces/held-modifier-config";
import i18next from "i18next";
import { getStatKey } from "#enums/stat";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { isPokemonValidForEncounterOptionSelection } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import type { Nature } from "#enums/nature";

View File

@ -26,7 +26,7 @@ import { getPokemonSpecies } from "#app/data/pokemon-species";
import { Moves } from "#enums/moves";
import { BattlerIndex } from "#app/battle";
import { PokemonMove } from "#app/field/pokemon";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { randSeedInt } from "#app/utils/common";
/** the i18n namespace for this encounter */

View File

@ -37,7 +37,7 @@ import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encoun
import { BerryModifier } from "#app/modifier/modifier";
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import { Stat } from "#enums/stat";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
/** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/uncommonBreed";

View File

@ -1075,8 +1075,8 @@ export function getRandomEncounterSpecies(level: number, isBoss = false, rerollH
ret.formIndex = formIndex;
}
//Reroll shiny for event encounters
if (isEventEncounter && !ret.shiny) {
//Reroll shiny or variant for event encounters
if (isEventEncounter) {
ret.trySetShinySeed();
}
//Reroll hidden ability

View File

@ -27,7 +27,7 @@ import {
} from "#app/data/balance/pokemon-level-moves";
import type { Stat } from "#enums/stat";
import type { Variant, VariantSet } from "#app/sprites/variant";
import { populateVariantColorCache, variantData } from "#app/sprites/variant";
import { populateVariantColorCache, variantColorCache, variantData } from "#app/sprites/variant";
import { speciesStarterCosts, POKERUS_STARTER_COUNT } from "#app/data/balance/starters";
import { SpeciesFormKey } from "#enums/species-form-key";
import { starterPassiveAbilities } from "#app/data/balance/passives";
@ -404,7 +404,7 @@ export abstract class PokemonSpeciesForm {
}
/** Compute the sprite ID of the pokemon form. */
getSpriteId(female: boolean, formIndex?: number, shiny?: boolean, variant = 0, back?: boolean): string {
getSpriteId(female: boolean, formIndex?: number, shiny?: boolean, variant = 0, back = false): string {
const baseSpriteKey = this.getBaseSpriteKey(female, formIndex);
let config = variantData;
@ -594,6 +594,44 @@ export abstract class PokemonSpeciesForm {
return true;
}
/**
* Load the variant colors for the species into the variant color cache
*
* @param spriteKey - The sprite key to use
* @param female - Whether to load female instead of male
* @param back - Whether the back sprite is being loaded
*
*/
async loadVariantColors(
spriteKey: string,
female: boolean,
variant: Variant,
back = false,
formIndex?: number,
): Promise<void> {
let baseSpriteKey = this.getBaseSpriteKey(female, formIndex);
if (back) {
baseSpriteKey = "back__" + baseSpriteKey;
}
if (variantColorCache.hasOwnProperty(baseSpriteKey)) {
// Variant colors have already been loaded
return;
}
const variantInfo = variantData[this.getVariantDataIndex(formIndex)];
// Do nothing if there is no variant information or the variant does not have color replacements
if (!variantInfo || variantInfo[variant] !== 1) {
return;
}
await populateVariantColorCache(
"pkmn__" + baseSpriteKey,
globalScene.experimentalSprites && hasExpSprite(spriteKey),
baseSpriteKey.replace("__", "/"),
);
}
async loadAssets(
female: boolean,
formIndex?: number,
@ -606,15 +644,9 @@ export abstract class PokemonSpeciesForm {
const spriteKey = this.getSpriteKey(female, formIndex, shiny, variant, back);
globalScene.loadPokemonAtlas(spriteKey, this.getSpriteAtlasPath(female, formIndex, shiny, variant, back));
globalScene.load.audio(this.getCryKey(formIndex), `audio/${this.getCryKey(formIndex)}.m4a`);
const baseSpriteKey = this.getBaseSpriteKey(female, formIndex);
// Force the variant color cache to be loaded for the form
await populateVariantColorCache(
"pkmn__" + baseSpriteKey,
globalScene.experimentalSprites && hasExpSprite(spriteKey),
baseSpriteKey,
);
if (!isNullOrUndefined(variant)) {
await this.loadVariantColors(spriteKey, female, variant, back, formIndex);
}
return new Promise<void>(resolve => {
globalScene.load.once(Phaser.Loader.Events.COMPLETE, () => {
const originalWarn = console.warn;

View File

@ -1,6 +1,8 @@
import { startingWave } from "#app/starting-wave";
import { globalScene } from "#app/global-scene";
import { PartyMemberStrength } from "#enums/party-member-strength";
import { GameModes } from "#app/game-mode";
import { ClassicFixedBossWaves } from "#enums/fixed-boss-waves";
export class TrainerPartyTemplate {
public size: number;
@ -222,20 +224,19 @@ export const trainerPartyTemplates = {
*/
export function getEvilGruntPartyTemplate(): TrainerPartyTemplate {
const waveIndex = globalScene.currentBattle?.waveIndex;
if (waveIndex < 40) {
switch (waveIndex) {
case ClassicFixedBossWaves.EVIL_GRUNT_1:
return trainerPartyTemplates.TWO_AVG;
}
if (waveIndex < 63) {
case ClassicFixedBossWaves.EVIL_GRUNT_2:
return trainerPartyTemplates.THREE_AVG;
}
if (waveIndex < 65) {
case ClassicFixedBossWaves.EVIL_GRUNT_3:
return trainerPartyTemplates.TWO_AVG_ONE_STRONG;
}
if (waveIndex < 112) {
case ClassicFixedBossWaves.EVIL_ADMIN_1:
return trainerPartyTemplates.GYM_LEADER_4; // 3avg 1 strong 1 stronger
}
default:
return trainerPartyTemplates.GYM_LEADER_5; // 3 avg 2 strong 1 stronger
}
}
export function getWavePartyTemplate(...templates: TrainerPartyTemplate[]) {
const { currentBattle, gameMode } = globalScene;
@ -245,6 +246,30 @@ export function getWavePartyTemplate(...templates: TrainerPartyTemplate[]) {
}
export function getGymLeaderPartyTemplate() {
const { currentBattle, gameMode } = globalScene;
switch (gameMode.modeId) {
case GameModes.DAILY:
if (currentBattle?.waveIndex <= 20) {
return trainerPartyTemplates.GYM_LEADER_2
}
return trainerPartyTemplates.GYM_LEADER_3;
case GameModes.CHALLENGE: // In the future, there may be a ChallengeType to call here. For now, use classic's.
case GameModes.CLASSIC:
if (currentBattle?.waveIndex <= 20) {
return trainerPartyTemplates.GYM_LEADER_1; // 1 avg 1 strong
}
else if (currentBattle?.waveIndex <= 30) {
return trainerPartyTemplates.GYM_LEADER_2; // 1 avg 1 strong 1 stronger
}
else if (currentBattle?.waveIndex <= 60) { // 50 and 60
return trainerPartyTemplates.GYM_LEADER_3; // 2 avg 1 strong 1 stronger
}
else if (currentBattle?.waveIndex <= 90) { // 80 and 90
return trainerPartyTemplates.GYM_LEADER_4; // 3 avg 1 strong 1 stronger
}
// 110+
return trainerPartyTemplates.GYM_LEADER_5; // 3 avg 2 strong 1 stronger
default:
return getWavePartyTemplate(
trainerPartyTemplates.GYM_LEADER_1,
trainerPartyTemplates.GYM_LEADER_2,
@ -253,3 +278,4 @@ export function getGymLeaderPartyTemplate() {
trainerPartyTemplates.GYM_LEADER_5,
);
}
}

View File

@ -1,7 +1,6 @@
export enum MoveEffectTrigger {
PRE_APPLY,
POST_APPLY,
HIT,
/** Triggers one time after all target effects have applied */
POST_TARGET
}

View File

@ -0,0 +1,22 @@
export enum ClassicFixedBossWaves {
TOWN_YOUNGSTER = 5,
RIVAL_1 = 8,
RIVAL_2 = 25,
EVIL_GRUNT_1 = 35,
RIVAL_3 = 55,
EVIL_GRUNT_2 = 62,
EVIL_GRUNT_3 = 64,
EVIL_ADMIN_1 = 66,
RIVAL_4 = 95,
EVIL_GRUNT_4 = 112,
EVIL_ADMIN_2 = 114,
EVIL_BOSS_1 = 115,
RIVAL_5 = 145,
EVIL_BOSS_2 = 165,
ELITE_FOUR_1 = 182,
ELITE_FOUR_2 = 184,
ELITE_FOUR_3 = 186,
ELITE_FOUR_4 = 188,
CHAMPION = 190,
RIVAL_6 = 195
}

View File

@ -0,0 +1,23 @@
/** The result of a hit check calculation */
export const HitCheckResult = {
/** Hit checks haven't been evaluated yet in this pass */
PENDING: 0,
/** The move hits the target successfully */
HIT: 1,
/** The move has no effect on the target */
NO_EFFECT: 2,
/** The move has no effect on the target, but doesn't proc the default "no effect" message */
NO_EFFECT_NO_MESSAGE: 3,
/** The target protected itself against the move */
PROTECTED: 4,
/** The move missed the target */
MISS: 5,
/** The move is reflected by magic coat or magic bounce */
REFLECTED: 6,
/** The target is no longer on the field */
TARGET_NOT_ON_FIELD: 7,
/** The move failed unexpectedly */
ERROR: 8,
} as const;
export type HitCheckResult = typeof HitCheckResult[keyof typeof HitCheckResult];

View File

@ -248,6 +248,7 @@ import { PLAYER_PARTY_MAX_SIZE } from "#app/constants";
import { CustomPokemonData } from "#app/data/custom-pokemon-data";
import { SwitchType } from "#enums/switch-type";
import { SpeciesFormKey } from "#enums/species-form-key";
import {getStatusEffectOverlapText } from "#app/data/status-effect";
import {
BASE_HIDDEN_ABILITY_CHANCE,
BASE_SHINY_CHANCE,
@ -277,6 +278,36 @@ export enum FieldPosition {
RIGHT,
}
/** Base typeclass for damage parameter methods, used for DRY */
type damageParams = {
/** The attacking {@linkcode Pokemon} */
source: Pokemon;
/** The move used in the attack */
move: Move;
/** The move's {@linkcode MoveCategory} after variable-category effects are applied */
moveCategory: MoveCategory;
/** If `true`, ignores this Pokemon's defensive ability effects */
ignoreAbility?: boolean;
/** If `true`, ignores the attacking Pokemon's ability effects */
ignoreSourceAbility?: boolean;
/** If `true`, ignores the ally Pokemon's ability effects */
ignoreAllyAbility?: boolean;
/** If `true`, ignores the ability effects of the attacking pokemon's ally */
ignoreSourceAllyAbility?: boolean;
/** If `true`, calculates damage for a critical hit */
isCritical?: boolean;
/** If `true`, suppresses changes to game state during the calculation */
simulated?: boolean;
/** If defined, used in place of calculated effectiveness values */
effectiveness?: number;
}
/** Type for the parameters of {@linkcode Pokemon#getBaseDamage | getBaseDamage} */
type getBaseDamageParams = Omit<damageParams, "effectiveness">
/** Type for the parameters of {@linkcode Pokemon#getAttackDamage | getAttackDamage} */
type getAttackDamageParams = Omit<damageParams, "moveCategory">;
export default abstract class Pokemon extends Phaser.GameObjects.Container {
public id: number;
public name: string;
@ -1441,25 +1472,16 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* Calculate the critical-hit stage of a move used against this pokemon by
* the given source
*
* @param source the {@linkcode Pokemon} who using the move
* @param move the {@linkcode Move} being used
* @returns the final critical-hit stage value
* @param source - The {@linkcode Pokemon} who using the move
* @param move - The {@linkcode Move} being used
* @returns The final critical-hit stage value
*/
getCritStage(source: Pokemon, move: Move): number {
const critStage = new NumberHolder(0);
applyMoveAttrs(HighCritAttr, source, this, move, critStage);
globalScene.applyModifiers(
CritBoosterModifier,
source.isPlayer(),
source,
critStage,
);
globalScene.applyModifiers(
TempCritBoosterModifier,
source.isPlayer(),
critStage,
);
applyAbAttrs(BonusCritAbAttr, source, null, false, critStage)
globalScene.applyModifiers(CritBoosterModifier, source.isPlayer(), source, critStage);
globalScene.applyModifiers(TempCritBoosterModifier, source.isPlayer(), critStage);
applyAbAttrs(BonusCritAbAttr, source, null, false, critStage);
const critBoostTag = source.getTag(CritBoostTag);
if (critBoostTag) {
if (critBoostTag instanceof DragonCheerTag) {
@ -1475,6 +1497,19 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
return critStage.value;
}
/**
* Calculates the category of a move when used by this pokemon after
* category-changing move effects are applied.
* @param target - The {@linkcode Pokemon} using the move
* @param move - The {@linkcode Move} being used
* @returns The given move's final category
*/
getMoveCategory(target: Pokemon, move: Move): MoveCategory {
const moveCategory = new NumberHolder(move.category);
applyMoveAttrs(VariableMoveCategoryAttr, this, target, move, moveCategory);
return moveCategory.value;
}
/**
* Calculates and retrieves the final value of a stat considering any held
* items, move effects, opponent abilities, and whether there was a critical
@ -2584,7 +2619,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* @param simulated Whether to apply abilities via simulated calls (defaults to `true`)
* @param cancelled {@linkcode BooleanHolder} Stores whether the move was cancelled by a non-type-based immunity.
* @param useIllusion - Whether we want the attack move effectiveness on the illusion or not
* Currently only used by {@linkcode Pokemon.apply} to determine whether a "No effect" message should be shown.
* @returns The type damage multiplier, indicating the effectiveness of the move
*/
getMoveEffectiveness(
@ -3170,7 +3204,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
/**
* Function that tries to set a Pokemon shiny based on seed.
* For manual use only, usually to roll a Pokemon's shiny chance a second time.
* If it rolls shiny, also sets a random variant and give the Pokemon the associated luck.
* If it rolls shiny, or if it's already shiny, also sets a random variant and give the Pokemon the associated luck.
*
* The base shiny odds are {@linkcode BASE_SHINY_CHANCE} / `65536`
* @param thresholdOverride number that is divided by `2^16` (`65536`) to get the shiny chance, overrides {@linkcode shinyThreshold} if set (bypassing shiny rate modifiers such as Shiny Charm)
@ -3181,6 +3215,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
thresholdOverride?: number,
applyModifiersToOverride?: boolean,
): boolean {
if (!this.shiny) {
const shinyThreshold = new NumberHolder(BASE_SHINY_CHANCE);
if (thresholdOverride === undefined || applyModifiersToOverride) {
if (thresholdOverride !== undefined && applyModifiersToOverride) {
@ -3189,21 +3224,22 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (timedEventManager.isEventActive()) {
shinyThreshold.value *= timedEventManager.getShinyMultiplier();
}
if (!this.hasTrainer()) {
globalScene.applyModifiers(
ShinyRateBoosterModifier,
true,
shinyThreshold,
);
}
} else {
else {
shinyThreshold.value = thresholdOverride;
}
this.shiny = randSeedInt(65536) < shinyThreshold.value;
}
if (this.shiny) {
this.variant = this.generateShinyVariant();
this.variant = this.variant ?? 0;
this.variant = Math.max(this.generateShinyVariant(), this.variant) as Variant; // Don't set a variant lower than the current one
this.luck =
this.variant + 1 + (this.fusionShiny ? this.fusionVariant + 1 : 0);
this.initShinySparkle();
@ -4073,27 +4109,28 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
/**
* Calculates the base damage of the given move against this Pokemon when attacked by the given source.
* Used during damage calculation and for Shell Side Arm's forecasting effect.
* @param source the attacking {@linkcode Pokemon}.
* @param move the {@linkcode Move} used in the attack.
* @param moveCategory the move's {@linkcode MoveCategory} after variable-category effects are applied.
* @param ignoreAbility if `true`, ignores this Pokemon's defensive ability effects (defaults to `false`).
* @param ignoreSourceAbility if `true`, ignore's the attacking Pokemon's ability effects (defaults to `false`).
* @param ignoreAllyAbility if `true`, ignores the ally Pokemon's ability effects (defaults to `false`).
* @param ignoreSourceAllyAbility if `true`, ignores the attacking Pokemon's ally's ability effects (defaults to `false`).
* @param isCritical if `true`, calculates effective stats as if the hit were critical (defaults to `false`).
* @param simulated if `true`, suppresses changes to game state during calculation (defaults to `true`).
* @param source - The attacking {@linkcode Pokemon}.
* @param move - The {@linkcode Move} used in the attack.
* @param moveCategory - The move's {@linkcode MoveCategory} after variable-category effects are applied.
* @param ignoreAbility - If `true`, ignores this Pokemon's defensive ability effects (defaults to `false`).
* @param ignoreSourceAbility - If `true`, ignore's the attacking Pokemon's ability effects (defaults to `false`).
* @param ignoreAllyAbility - If `true`, ignores the ally Pokemon's ability effects (defaults to `false`).
* @param ignoreSourceAllyAbility - If `true`, ignores the attacking Pokemon's ally's ability effects (defaults to `false`).
* @param isCritical - if `true`, calculates effective stats as if the hit were critical (defaults to `false`).
* @param simulated - if `true`, suppresses changes to game state during calculation (defaults to `true`).
* @returns The move's base damage against this Pokemon when used by the source Pokemon.
*/
getBaseDamage(
source: Pokemon,
move: Move,
moveCategory: MoveCategory,
{
source,
move,
moveCategory,
ignoreAbility = false,
ignoreSourceAbility = false,
ignoreAllyAbility = false,
ignoreSourceAllyAbility = false,
isCritical = false,
simulated = true,
simulated = true}: getBaseDamageParams
): number {
const isPhysical = moveCategory === MoveCategory.PHYSICAL;
@ -4220,27 +4257,27 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
/**
* Calculates the damage of an attack made by another Pokemon against this Pokemon
* @param source {@linkcode Pokemon} the attacking Pokemon
* @param move {@linkcode Pokemon} the move used in the attack
* @param move The {@linkcode Move} used in the attack
* @param ignoreAbility If `true`, ignores this Pokemon's defensive ability effects
* @param ignoreSourceAbility If `true`, ignores the attacking Pokemon's ability effects
* @param ignoreAllyAbility If `true`, ignores the ally Pokemon's ability effects
* @param ignoreSourceAllyAbility If `true`, ignores the ability effects of the attacking pokemon's ally
* @param isCritical If `true`, calculates damage for a critical hit.
* @param simulated If `true`, suppresses changes to game state during the calculation.
* @returns a {@linkcode DamageCalculationResult} object with three fields:
* - `cancelled`: `true` if the move was cancelled by another effect.
* - `result`: {@linkcode HitResult} indicates the attack's type effectiveness.
* - `damage`: `number` the attack's final damage output.
* @param effectiveness If defined, used in place of calculated effectiveness values
* @returns The {@linkcode DamageCalculationResult}
*/
getAttackDamage(
source: Pokemon,
move: Move,
{
source,
move,
ignoreAbility = false,
ignoreSourceAbility = false,
ignoreAllyAbility = false,
ignoreSourceAllyAbility = false,
isCritical = false,
simulated = true,
effectiveness}: getAttackDamageParams,
): DamageCalculationResult {
const damage = new NumberHolder(0);
const defendingSide = this.isPlayer()
@ -4270,7 +4307,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
*
* Note that the source's abilities are not ignored here
*/
const typeMultiplier = this.getMoveEffectiveness(
const typeMultiplier = effectiveness ?? this.getMoveEffectiveness(
source,
move,
ignoreAbility,
@ -4342,7 +4379,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* The attack's base damage, as determined by the source's level, move power
* and Attack stat as well as this Pokemon's Defense stat
*/
const baseDamage = this.getBaseDamage(
const baseDamage = this.getBaseDamage({
source,
move,
moveCategory,
@ -4352,7 +4389,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
ignoreSourceAllyAbility,
isCritical,
simulated,
);
});
/** 25% damage debuff on moves hitting more than one non-fainted target (regardless of immunities) */
const { targets, multiple } = getMoveTargets(source, move.id);
@ -4563,211 +4600,36 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
};
}
/**
* Applies the results of a move to this pokemon
* @param source The {@linkcode Pokemon} using the move
* @param move The {@linkcode Move} being used
* @returns The {@linkcode HitResult} of the attack
/** Calculate whether the given move critically hits this pokemon
* @param source - The {@linkcode Pokemon} using the move
* @param move - The {@linkcode Move} being used
* @param simulated - If `true`, suppresses changes to game state during calculation (defaults to `true`)
* @returns whether the move critically hits the pokemon
*/
apply(source: Pokemon, move: Move): HitResult {
const defendingSide = this.isPlayer()
? ArenaTagSide.PLAYER
: ArenaTagSide.ENEMY;
const moveCategory = new NumberHolder(move.category);
applyMoveAttrs(VariableMoveCategoryAttr, source, this, move, moveCategory);
if (moveCategory.value === MoveCategory.STATUS) {
const cancelled = new BooleanHolder(false);
const typeMultiplier = this.getMoveEffectiveness(
source,
move,
false,
false,
cancelled,
);
getCriticalHitResult(source: Pokemon, move: Move, simulated: boolean = true): boolean {
const defendingSide = this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
const noCritTag = globalScene.arena.getTagOnSide(NoCritTag, defendingSide);
if (noCritTag || Overrides.NEVER_CRIT_OVERRIDE || move.hasAttr(FixedDamageAttr)) {
return false;
}
const isCritical = new BooleanHolder(false);
if (!cancelled.value && typeMultiplier === 0) {
globalScene.queueMessage(
i18next.t("battle:hitResultNoEffect", {
pokemonName: getPokemonNameWithAffix(this),
}),
);
if (source.getTag(BattlerTagType.ALWAYS_CRIT)) {
isCritical.value = true;
}
return typeMultiplier === 0 ? HitResult.NO_EFFECT : HitResult.STATUS;
}
/** Determines whether the attack critically hits */
let isCritical: boolean;
const critOnly = new BooleanHolder(false);
const critAlways = source.getTag(BattlerTagType.ALWAYS_CRIT);
applyMoveAttrs(CritOnlyAttr, source, this, move, critOnly);
applyAbAttrs(
ConditionalCritAbAttr,
source,
null,
false,
critOnly,
this,
move,
);
if (critOnly.value || critAlways) {
isCritical = true;
} else {
applyMoveAttrs(CritOnlyAttr, source, this, move, isCritical);
applyAbAttrs(ConditionalCritAbAttr, source, null, simulated, isCritical, this, move);
if (!isCritical.value) {
const critChance = [24, 8, 2, 1][
Math.max(0, Math.min(this.getCritStage(source, move), 3))
];
isCritical =
critChance === 1 || !globalScene.randBattleSeedInt(critChance);
isCritical.value = critChance === 1 || !globalScene.randBattleSeedInt(critChance);
}
const noCritTag = globalScene.arena.getTagOnSide(NoCritTag, defendingSide);
const blockCrit = new BooleanHolder(false);
applyAbAttrs(BlockCritAbAttr, this, null, false, blockCrit);
if (noCritTag || blockCrit.value || Overrides.NEVER_CRIT_OVERRIDE) {
isCritical = false;
}
applyAbAttrs(BlockCritAbAttr, this, null, simulated, isCritical);
/**
* Applies stat changes from {@linkcode move} and gives it to {@linkcode source}
* before damage calculation
*/
applyMoveAttrs(StatChangeBeforeDmgCalcAttr, source, this, move);
return isCritical.value;
const {
cancelled,
result,
damage: dmg,
} = this.getAttackDamage(source, move, false, false, false, false, isCritical, false);
const typeBoost = source.findTag(
t =>
t instanceof TypeBoostTag && t.boostedType === source.getMoveType(move),
) as TypeBoostTag;
if (typeBoost?.oneUse) {
source.removeTag(typeBoost.tagType);
}
if (
cancelled ||
result === HitResult.IMMUNE ||
result === HitResult.NO_EFFECT
) {
source.stopMultiHit(this);
if (!cancelled) {
if (result === HitResult.IMMUNE) {
globalScene.queueMessage(
i18next.t("battle:hitResultImmune", {
pokemonName: getPokemonNameWithAffix(this),
}),
);
} else {
globalScene.queueMessage(
i18next.t("battle:hitResultNoEffect", {
pokemonName: getPokemonNameWithAffix(this),
}),
);
}
}
return result;
}
// In case of fatal damage, this tag would have gotten cleared before we could lapse it.
const destinyTag = this.getTag(BattlerTagType.DESTINY_BOND);
const grudgeTag = this.getTag(BattlerTagType.GRUDGE);
if (dmg) {
this.lapseTags(BattlerTagLapseType.HIT);
const substitute = this.getTag(SubstituteTag);
const isBlockedBySubstitute =
!!substitute && move.hitsSubstitute(source, this);
if (isBlockedBySubstitute) {
substitute.hp -= dmg;
}
if (!this.isPlayer() && dmg >= this.hp) {
globalScene.applyModifiers(EnemyEndureChanceModifier, false, this);
}
/**
* We explicitly require to ignore the faint phase here, as we want to show the messages
* about the critical hit and the super effective/not very effective messages before the faint phase.
*/
const damage = this.damageAndUpdate(isBlockedBySubstitute ? 0 : dmg,
{
result: result as DamageResult,
isCritical,
ignoreFaintPhase: true,
source
});
if (damage > 0) {
if (source.isPlayer()) {
globalScene.validateAchvs(DamageAchv, new NumberHolder(damage));
if (damage > globalScene.gameData.gameStats.highestDamage) {
globalScene.gameData.gameStats.highestDamage = damage;
}
}
source.turnData.totalDamageDealt += damage;
source.turnData.singleHitDamageDealt = damage;
this.turnData.damageTaken += damage;
this.battleData.hitCount++;
const attackResult = {
move: move.id,
result: result as DamageResult,
damage: damage,
critical: isCritical,
sourceId: source.id,
sourceBattlerIndex: source.getBattlerIndex(),
};
this.turnData.attacksReceived.unshift(attackResult);
if (source.isPlayer() && !this.isPlayer()) {
globalScene.applyModifiers(
DamageMoneyRewardModifier,
true,
source,
new NumberHolder(damage),
);
}
}
}
if (isCritical) {
globalScene.queueMessage(i18next.t("battle:hitResultCriticalHit"));
}
// want to include is.Fainted() in case multi hit move ends early, still want to render message
if (source.turnData.hitsLeft === 1 || this.isFainted()) {
switch (result) {
case HitResult.SUPER_EFFECTIVE:
globalScene.queueMessage(i18next.t("battle:hitResultSuperEffective"));
break;
case HitResult.NOT_VERY_EFFECTIVE:
globalScene.queueMessage(
i18next.t("battle:hitResultNotVeryEffective"),
);
break;
case HitResult.ONE_HIT_KO:
globalScene.queueMessage(i18next.t("battle:hitResultOneHitKO"));
break;
}
}
if (this.isFainted()) {
// set splice index here, so future scene queues happen before FaintedPhase
globalScene.setPhaseQueueSplice();
globalScene.unshiftPhase(
new FaintPhase(
this.getBattlerIndex(),
false,
source,
),
);
this.destroySubstitute();
this.lapseTag(BattlerTagType.COMMANDED);
}
return result;
}
/**
@ -4831,7 +4693,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
/**
* Called by apply(), given the damage, adds a new DamagePhase and actually updates HP values, etc.
* Given the damage, adds a new DamagePhase and update HP values, etc.
*
* Checks for 'Indirect' HitResults to account for Endure/Reviver Seed applying correctly
* @param damage integer - passed to damage()
* @param result an enum if it's super effective, not very, etc.
@ -5134,8 +4997,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
/**
* Gets whether the given move is currently disabled for this Pokemon.
*
* @param {Moves} moveId {@linkcode Moves} ID of the move to check
* @returns {boolean} `true` if the move is disabled for this Pokemon, otherwise `false`
* @param moveId - The {@linkcode Moves} ID of the move to check
* @returns `true` if the move is disabled for this Pokemon, otherwise `false`
*
* @see {@linkcode MoveRestrictionBattlerTag}
*/
@ -5146,9 +5009,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
/**
* Gets whether the given move is currently disabled for the user based on the player's target selection
*
* @param {Moves} moveId {@linkcode Moves} ID of the move to check
* @param {Pokemon} user {@linkcode Pokemon} the move user
* @param {Pokemon} target {@linkcode Pokemon} the target of the move
* @param moveId - The {@linkcode Moves} ID of the move to check
* @param user - The move user
* @param target - The target of the move
*
* @returns {boolean} `true` if the move is disabled for this Pokemon due to the player's target selection
*
@ -5178,10 +5041,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
/**
* Gets the {@link MoveRestrictionBattlerTag} that is restricting a move, if it exists.
*
* @param {Moves} moveId {@linkcode Moves} ID of the move to check
* @param {Pokemon} user {@linkcode Pokemon} the move user, optional and used when the target is a factor in the move's restricted status
* @param {Pokemon} target {@linkcode Pokemon} the target of the move, optional and used when the target is a factor in the move's restricted status
* @returns {MoveRestrictionBattlerTag | null} the first tag on this Pokemon that restricts the move, or `null` if the move is not restricted.
* @param moveId - {@linkcode Moves} ID of the move to check
* @param user - {@linkcode Pokemon} the move user, optional and used when the target is a factor in the move's restricted status
* @param target - {@linkcode Pokemon} the target of the move, optional and used when the target is a factor in the move's restricted status
* @returns The first tag on this Pokemon that restricts the move, or `null` if the move is not restricted.
*/
getRestrictingTag(
moveId: Moves,
@ -5243,20 +5106,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
return this.summonData.moveQueue;
}
/**
* If this Pokemon is using a multi-hit move, cancels all subsequent strikes
* @param {Pokemon} target If specified, this only cancels subsequent strikes against the given target
*/
stopMultiHit(target?: Pokemon): void {
const effectPhase = globalScene.getCurrentPhase();
if (
effectPhase instanceof MoveEffectPhase &&
effectPhase.getUserPokemon() === this
) {
effectPhase.stopMultiHit(target);
}
}
changeForm(formChange: SpeciesFormChange): Promise<void> {
return new Promise(resolve => {
this.formIndex = Math.max(
@ -5516,6 +5365,18 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
);
}
queueImmuneMessage(quiet: boolean, effect?: StatusEffect): void {
if (!effect || quiet) {
return;
}
const message = effect && this.status?.effect === effect
? getStatusEffectOverlapText(effect ?? StatusEffect.NONE, getPokemonNameWithAffix(this))
: i18next.t("abilityTriggers:moveImmunity", {
pokemonNameWithAffix: getPokemonNameWithAffix(this),
});
globalScene.queueMessage(message);
}
/**
* Checks if a status effect can be applied to the Pokemon.
*
@ -5534,6 +5395,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
): boolean {
if (effect !== StatusEffect.FAINT) {
if (overrideStatus ? this.status?.effect === effect : this.status) {
this.queueImmuneMessage(quiet, effect);
return false;
}
if (
@ -5541,18 +5403,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
!ignoreField &&
globalScene.arena.terrain?.terrainType === TerrainType.MISTY
) {
this.queueImmuneMessage(quiet, effect);
return false;
}
}
if (
sourcePokemon &&
sourcePokemon !== this &&
this.isSafeguarded(sourcePokemon)
) {
return false;
}
const types = this.getTypes(true, true);
switch (effect) {
@ -5586,12 +5441,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (this.isOfType(PokemonType.POISON) || this.isOfType(PokemonType.STEEL)) {
if (poisonImmunity.includes(true)) {
this.queueImmuneMessage(quiet, effect);
return false;
}
}
break;
case StatusEffect.PARALYSIS:
if (this.isOfType(PokemonType.ELECTRIC)) {
this.queueImmuneMessage(quiet, effect);
return false;
}
break;
@ -5600,6 +5457,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.isGrounded() &&
globalScene.arena.terrain?.terrainType === TerrainType.ELECTRIC
) {
this.queueImmuneMessage(quiet, effect);
return false;
}
break;
@ -5612,11 +5470,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
globalScene.arena.weather.weatherType,
))
) {
this.queueImmuneMessage(quiet, effect);
return false;
}
break;
case StatusEffect.BURN:
if (this.isOfType(PokemonType.FIRE)) {
this.queueImmuneMessage(quiet, effect);
return false;
}
break;
@ -5651,6 +5511,19 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
return false;
}
if (
sourcePokemon &&
sourcePokemon !== this &&
this.isSafeguarded(sourcePokemon)
) {
if(!quiet){
globalScene.queueMessage(
i18next.t("moveTriggers:safeguard", { targetName: getPokemonNameWithAffix(this)
}));
}
return false;
}
return true;
}
@ -5662,7 +5535,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
sourceText: string | null = null,
overrideStatus?: boolean
): boolean {
if (!this.canSetStatus(effect, asPhase, overrideStatus, sourcePokemon)) {
if (!this.canSetStatus(effect, false, overrideStatus, sourcePokemon)) {
return false;
}
if (this.isFainted() && effect !== StatusEffect.FAINT) {
@ -5674,7 +5547,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* cancel the attack's subsequent hits.
*/
if (effect === StatusEffect.SLEEP || effect === StatusEffect.FREEZE) {
this.stopMultiHit();
const currentPhase = globalScene.getCurrentPhase();
if (currentPhase instanceof MoveEffectPhase && currentPhase.getUserPokemon() === this) {
this.turnData.hitCount = 1;
this.turnData.hitsLeft = 1;
}
}
if (asPhase) {
@ -7309,14 +7186,15 @@ export class EnemyPokemon extends Pokemon {
].includes(move.id);
return (
doesNotFail &&
p.getAttackDamage(
this,
p.getAttackDamage({
source: this,
move,
!p.battleData.abilityRevealed,
false,
!p.getAlly()?.battleData.abilityRevealed,
false,
ignoreAbility: !p.battleData.abilityRevealed,
ignoreSourceAbility: false,
ignoreAllyAbility: !p.getAlly()?.battleData.abilityRevealed,
ignoreSourceAllyAbility: false,
isCritical,
}
).damage >= p.hp
);
})

View File

@ -13,6 +13,7 @@ import { Species } from "#enums/species";
import { Challenges } from "./enums/challenges";
import { globalScene } from "#app/global-scene";
import { getDailyStartingBiome } from "./data/daily-run";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES, CHALLENGE_MODE_MYSTERY_ENCOUNTER_WAVES } from "./constants";
export enum GameModes {
CLASSIC,
@ -36,10 +37,6 @@ interface GameModeConfig {
hasMysteryEncounters?: boolean;
}
// Describes min and max waves for MEs in specific game modes
export const CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES: [number, number] = [10, 180];
export const CHALLENGE_MODE_MYSTERY_ENCOUNTER_WAVES: [number, number] = [10, 180];
export class GameMode implements GameModeConfig {
public modeId: GameModes;
public isClassic: boolean;

View File

@ -93,7 +93,7 @@ const startGame = async (manifest?: any) => {
dom: {
createContainer: true,
},
pixelArt: true,
antialias: false,
pipeline: [InvertPostFX] as unknown as Phaser.Types.Core.PipelineConfig,
scene: [LoadingScene, BattleScene],
version: version,

View File

@ -14,6 +14,7 @@ import { BattleEndPhase } from "./battle-end-phase";
import { NewBattlePhase } from "./new-battle-phase";
import { PokemonPhase } from "./pokemon-phase";
import { globalScene } from "#app/global-scene";
import { SelectBiomePhase } from "./select-biome-phase";
export class AttemptRunPhase extends PokemonPhase {
/** For testing purposes: this is to force the pokemon to fail and escape */
@ -59,6 +60,11 @@ export class AttemptRunPhase extends PokemonPhase {
});
globalScene.pushPhase(new BattleEndPhase(false));
if (globalScene.gameMode.hasRandomBiomes || globalScene.isNewBiome()) {
globalScene.pushPhase(new SelectBiomePhase());
}
globalScene.pushPhase(new NewBattlePhase());
} else {
playerPokemon.turnData.failedRunAway = true;

View File

@ -35,19 +35,19 @@ import { BattlerTagType } from "#enums/battler-tag-type";
export class FaintPhase extends PokemonPhase {
/**
* Whether or not enduring (for this phase's purposes, Reviver Seed) should be prevented
* Whether or not instant revive should be prevented
*/
private preventEndure: boolean;
private preventInstantRevive: boolean;
/**
* The source Pokemon that dealt fatal damage
*/
private source?: Pokemon;
constructor(battlerIndex: BattlerIndex, preventEndure = false, source?: Pokemon) {
constructor(battlerIndex: BattlerIndex, preventInstantRevive = false, source?: Pokemon) {
super(battlerIndex);
this.preventEndure = preventEndure;
this.preventInstantRevive = preventInstantRevive;
this.source = source;
}
@ -63,7 +63,7 @@ export class FaintPhase extends PokemonPhase {
faintPokemon.resetSummonData();
if (!this.preventEndure) {
if (!this.preventInstantRevive) {
const instantReviveModifier = globalScene.applyModifier(
PokemonInstantReviveModifier,
this.player,

File diff suppressed because it is too large Load Diff

View File

@ -404,9 +404,10 @@ export class MovePhase extends BattlePhase {
* if the move fails.
*/
if (success) {
applyPreAttackAbAttrs(PokemonTypeChangeAbAttr, this.pokemon, null, this.move.getMove());
const move = this.move.getMove();
applyPreAttackAbAttrs(PokemonTypeChangeAbAttr, this.pokemon, null, move);
globalScene.unshiftPhase(
new MoveEffectPhase(this.pokemon.getBattlerIndex(), this.targets, this.move, this.reflected),
new MoveEffectPhase(this.pokemon.getBattlerIndex(), this.targets, move, this.reflected, this.move.virtual),
);
} else {
if ([Moves.ROAR, Moves.WHIRLWIND, Moves.TRICK_OR_TREAT, Moves.FORESTS_CURSE].includes(this.move.moveId)) {

View File

@ -27,6 +27,7 @@ import { IvScannerModifier } from "../modifier/modifier";
import { Phase } from "../phase";
import { UiMode } from "#enums/ui-mode";
import { isNullOrUndefined, randSeedItem } from "#app/utils/common";
import { SelectBiomePhase } from "./select-biome-phase";
/**
* Will handle (in order):
@ -612,6 +613,10 @@ export class PostMysteryEncounterPhase extends Phase {
*/
continueEncounter() {
const endPhase = () => {
if (globalScene.gameMode.hasRandomBiomes || globalScene.isNewBiome()) {
globalScene.pushPhase(new SelectBiomePhase());
}
globalScene.pushPhase(new NewBattlePhase());
this.end();
};

View File

@ -14,9 +14,10 @@ export class SelectBiomePhase extends BattlePhase {
super.start();
const currentBiome = globalScene.arena.biomeType;
const nextWaveIndex = globalScene.currentBattle.waveIndex + 1;
const setNextBiome = (nextBiome: Biome) => {
if (globalScene.currentBattle.waveIndex % 10 === 1) {
if (nextWaveIndex % 10 === 1) {
globalScene.applyModifiers(MoneyInterestModifier, true);
globalScene.unshiftPhase(new PartyHealPhase(false));
}
@ -25,13 +26,13 @@ export class SelectBiomePhase extends BattlePhase {
};
if (
(globalScene.gameMode.isClassic && globalScene.gameMode.isWaveFinal(globalScene.currentBattle.waveIndex + 9)) ||
(globalScene.gameMode.isDaily && globalScene.gameMode.isWaveFinal(globalScene.currentBattle.waveIndex)) ||
(globalScene.gameMode.hasShortBiomes && !(globalScene.currentBattle.waveIndex % 50))
(globalScene.gameMode.isClassic && globalScene.gameMode.isWaveFinal(nextWaveIndex + 9)) ||
(globalScene.gameMode.isDaily && globalScene.gameMode.isWaveFinal(nextWaveIndex)) ||
(globalScene.gameMode.hasShortBiomes && !(nextWaveIndex % 50))
) {
setNextBiome(Biome.END);
} else if (globalScene.gameMode.hasRandomBiomes) {
setNextBiome(this.generateNextBiome());
setNextBiome(this.generateNextBiome(nextWaveIndex));
} else if (Array.isArray(biomeLinks[currentBiome])) {
const biomes: Biome[] = (biomeLinks[currentBiome] as (Biome | [Biome, number])[])
.filter(b => !Array.isArray(b) || !randSeedInt(b[1]))
@ -59,14 +60,14 @@ export class SelectBiomePhase extends BattlePhase {
} else if (biomeLinks.hasOwnProperty(currentBiome)) {
setNextBiome(biomeLinks[currentBiome] as Biome);
} else {
setNextBiome(this.generateNextBiome());
setNextBiome(this.generateNextBiome(nextWaveIndex));
}
}
generateNextBiome(): Biome {
if (!(globalScene.currentBattle.waveIndex % 50)) {
generateNextBiome(waveIndex: number): Biome {
if (!(waveIndex % 50)) {
return Biome.END;
}
return globalScene.generateRandomBiome(globalScene.currentBattle.waveIndex);
return globalScene.generateRandomBiome(waveIndex);
}
}

View File

@ -19,6 +19,10 @@ export class SwitchBiomePhase extends BattlePhase {
return this.end();
}
// Before switching biomes, make sure to set the last encounter for other phases that need it too.
globalScene.lastEnemyTrainer = globalScene.currentBattle?.trainer ?? null;
globalScene.lastMysteryEncounter = globalScene.currentBattle?.mysteryEncounter;
globalScene.tweens.add({
targets: [globalScene.arenaEnemy, globalScene.lastEnemyTrainer],
x: "+=300",

View File

@ -1,5 +1,5 @@
import type { BattlerIndex } from "#app/battle";
import { ClassicFixedBossWaves } from "#app/battle";
import { ClassicFixedBossWaves } from "#enums/fixed-boss-waves";
import { BattleType } from "#enums/battle-type";
import type { CustomModifierSettings } from "#app/modifier/modifier-type";
import { modifierTypes } from "#app/modifier/modifier-type";
@ -15,6 +15,7 @@ import { TrainerVictoryPhase } from "./trainer-victory-phase";
import { handleMysteryEncounterVictory } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { globalScene } from "#app/global-scene";
import { timedEventManager } from "#app/global-event-manager";
import { SelectBiomePhase } from "./select-biome-phase";
export class VictoryPhase extends PokemonPhase {
/** If true, indicates that the phase is intended for EXP purposes only, and not to continue a battle to next phase */
@ -111,6 +112,11 @@ export class VictoryPhase extends PokemonPhase {
globalScene.pushPhase(new AddEnemyBuffModifierPhase());
}
}
if (globalScene.gameMode.hasRandomBiomes || globalScene.isNewBiome()) {
globalScene.pushPhase(new SelectBiomePhase());
}
globalScene.pushPhase(new NewBattlePhase());
} else {
globalScene.currentBattle.battleType = BattleType.CLEAR;

View File

@ -557,11 +557,11 @@ export default class BattleInfo extends Phaser.GameObjects.Container {
this.ownedIcon,
this.championRibbon,
this.statusIndicator,
this.levelContainer,
this.statValuesContainer,
].map(e => (e.x += 48 * (boss ? -1 : 1)));
this.hpBar.x += 38 * (boss ? -1 : 1);
this.hpBar.y += 2 * (this.boss ? -1 : 1);
this.levelContainer.x += 2 * (boss ? -1 : 1);
this.hpBar.setTexture(`overlay_hp${boss ? "_boss" : ""}`);
this.box.setTexture(this.getTextureName());
this.statsBox.setTexture(`${this.getTextureName()}_stats`);

View File

@ -20,7 +20,6 @@ export class FilterText extends Phaser.GameObjects.Container {
private window: Phaser.GameObjects.NineSlice;
private labels: Phaser.GameObjects.Text[] = [];
private selections: Phaser.GameObjects.Text[] = [];
private selectionStrings: string[] = [];
private rows: FilterTextRow[] = [];
public cursorObj: Phaser.GameObjects.Image;
public numFilters = 0;
@ -112,8 +111,6 @@ export class FilterText extends Phaser.GameObjects.Container {
this.selections.push(filterTypesSelection);
this.add(filterTypesSelection);
this.selectionStrings.push("");
this.calcFilterPositions();
this.numFilters++;
@ -122,7 +119,6 @@ export class FilterText extends Phaser.GameObjects.Container {
resetSelection(index: number): void {
this.selections[index].setText(this.defaultText);
this.selectionStrings[index] = "";
this.onChange();
}
@ -204,6 +200,17 @@ export class FilterText extends Phaser.GameObjects.Container {
return this.selections[row].getWrappedText()[0];
}
/**
* Forcibly set the selection text for a specific filter row and then call the `onChange` function
*
* @param row - The filter row to set the text for
* @param value - The text to set for the filter row
*/
setValue(row: FilterTextRow, value: string) {
this.selections[row].setText(value);
this.onChange();
}
/**
* Find the nearest filter to the provided container on the y-axis
* @param container the StarterContainer to compare position against

View File

@ -292,6 +292,13 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
starterSelectBg.setOrigin(0, 0);
this.starterSelectContainer.add(starterSelectBg);
this.pokemonSprite = globalScene.add.sprite(53, 63, "pkmn__sub");
this.pokemonSprite.setPipeline(globalScene.spritePipeline, {
tone: [0.0, 0.0, 0.0, 0.0],
ignoreTimeTint: true,
});
this.starterSelectContainer.add(this.pokemonSprite);
this.shinyOverlay = globalScene.add.image(6, 6, "summary_overlay_shiny");
this.shinyOverlay.setOrigin(0, 0);
this.shinyOverlay.setVisible(false);
@ -343,13 +350,6 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
this.starterSelectContainer.add(starterBoxContainer);
this.pokemonSprite = globalScene.add.sprite(53, 63, "pkmn__sub");
this.pokemonSprite.setPipeline(globalScene.spritePipeline, {
tone: [0.0, 0.0, 0.0, 0.0],
ignoreTimeTint: true,
});
this.starterSelectContainer.add(this.pokemonSprite);
this.type1Icon = globalScene.add.sprite(8, 98, getLocalizedSpriteKey("types"));
this.type1Icon.setScale(0.5);
this.type1Icon.setOrigin(0, 0);

View File

@ -37,10 +37,9 @@ import { addWindow } from "./ui-theme";
import type { OptionSelectConfig } from "./abstact-option-select-ui-handler";
import { FilterText, FilterTextRow } from "./filter-text";
import { allAbilities } from "#app/data/data-lists";
import { starterPassiveAbilities } from "#app/data/balance/passives";
import { allMoves } from "#app/data/moves/move";
import { speciesTmMoves } from "#app/data/balance/tms";
import { pokemonPrevolutions, pokemonStarters } from "#app/data/balance/pokemon-evolutions";
import { pokemonStarters } from "#app/data/balance/pokemon-evolutions";
import { Biome } from "#enums/biome";
import { globalScene } from "#app/global-scene";
@ -174,7 +173,6 @@ export default class PokedexUiHandler extends MessageUiHandler {
private scrollCursor: number;
private oldCursor = -1;
private allSpecies: PokemonSpecies[] = [];
private lastSpecies: PokemonSpecies;
private speciesLoaded: Map<Species, boolean> = new Map<Species, boolean>();
private pokerusSpecies: PokemonSpecies[] = [];
@ -493,12 +491,11 @@ export default class PokedexUiHandler extends MessageUiHandler {
for (const species of allSpecies) {
this.speciesLoaded.set(species.speciesId, false);
this.allSpecies.push(species);
}
// Here code to declare 81 containers
for (let i = 0; i < 81; i++) {
const pokemonContainer = new PokedexMonContainer(this.allSpecies[i]).setVisible(false);
const pokemonContainer = new PokedexMonContainer(allSpecies[i]).setVisible(false);
const pos = calcStarterPosition(i);
pokemonContainer.setPosition(pos.x, pos.y);
this.iconAnimHandler.addOrUpdate(pokemonContainer.icon, PokemonIconAnimMode.NONE);
@ -1342,7 +1339,7 @@ export default class PokedexUiHandler extends MessageUiHandler {
this.filteredPokemonData = [];
this.allSpecies.forEach(species => {
allSpecies.forEach(species => {
const starterId = this.getStarterSpeciesId(species.speciesId);
const currentDexAttr = this.getCurrentDexProps(species.speciesId);
@ -1412,12 +1409,11 @@ export default class PokedexUiHandler extends MessageUiHandler {
// Ability filter
const abilities = [species.ability1, species.ability2, species.abilityHidden].map(a => allAbilities[a].name);
const passiveId = starterPassiveAbilities.hasOwnProperty(species.speciesId)
? species.speciesId
: starterPassiveAbilities.hasOwnProperty(starterId)
? starterId
: pokemonPrevolutions[starterId];
const passives = starterPassiveAbilities[passiveId];
// get the passive ability for the species
const passives = [species.getPassiveAbility()];
for (const form of species.forms) {
passives.push(form.getPassiveAbility());
}
const selectedAbility1 = this.filterText.getValue(FilterTextRow.ABILITY_1);
const fitsFormAbility1 = species.forms.some(form =>

View File

@ -186,7 +186,7 @@ describe("Abilities - Disguise", () => {
await game.toNextTurn();
game.move.select(Moves.SPLASH);
await game.doKillOpponents();
await game.phaseInterceptor.to("PartyHealPhase");
await game.phaseInterceptor.to("QuietFormChangePhase");
expect(mimikyu1.formIndex).toBe(disguisedForm);
});

View File

@ -50,7 +50,11 @@ describe("Moves - Friend Guard", () => {
// Get the last return value from `getAttackDamage`
const turn1Damage = spy.mock.results[spy.mock.results.length - 1].value.damage;
// Making sure the test is controlled; turn 1 damage is equal to base damage (after rounding)
expect(turn1Damage).toBe(Math.floor(player1.getBaseDamage(enemy1, allMoves[Moves.TACKLE], MoveCategory.PHYSICAL)));
expect(turn1Damage).toBe(
Math.floor(
player1.getBaseDamage({ source: enemy1, move: allMoves[Moves.TACKLE], moveCategory: MoveCategory.PHYSICAL }),
),
);
vi.spyOn(player2, "getAbility").mockReturnValue(allAbilities[Abilities.FRIEND_GUARD]);
@ -64,7 +68,10 @@ describe("Moves - Friend Guard", () => {
const turn2Damage = spy.mock.results[spy.mock.results.length - 1].value.damage;
// With the ally's Friend Guard, damage should have been reduced from base damage by 25%
expect(turn2Damage).toBe(
Math.floor(player1.getBaseDamage(enemy1, allMoves[Moves.TACKLE], MoveCategory.PHYSICAL) * 0.75),
Math.floor(
player1.getBaseDamage({ source: enemy1, move: allMoves[Moves.TACKLE], moveCategory: MoveCategory.PHYSICAL }) *
0.75,
),
);
});

View File

@ -4,7 +4,6 @@ import { PokemonType } from "#enums/pokemon-type";
import { Abilities } from "#app/enums/abilities";
import { Moves } from "#app/enums/moves";
import { Species } from "#app/enums/species";
import { HitResult } from "#app/field/pokemon";
import GameManager from "#test/testUtils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
@ -38,13 +37,13 @@ describe("Abilities - Galvanize", () => {
});
it("should change Normal-type attacks to Electric type and boost their power", async () => {
await game.startBattle();
await game.classicMode.startBattle();
const playerPokemon = game.scene.getPlayerPokemon()!;
vi.spyOn(playerPokemon, "getMoveType");
const enemyPokemon = game.scene.getEnemyPokemon()!;
vi.spyOn(enemyPokemon, "apply");
const spy = vi.spyOn(enemyPokemon, "getMoveEffectiveness");
const move = allMoves[Moves.TACKLE];
vi.spyOn(move, "calculateBattlePower");
@ -54,21 +53,23 @@ describe("Abilities - Galvanize", () => {
await game.phaseInterceptor.to("BerryPhase", false);
expect(playerPokemon.getMoveType).toHaveLastReturnedWith(PokemonType.ELECTRIC);
expect(enemyPokemon.apply).toHaveReturnedWith(HitResult.EFFECTIVE);
expect(spy).toHaveReturnedWith(1);
expect(move.calculateBattlePower).toHaveReturnedWith(48);
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
spy.mockRestore();
});
it("should cause Normal-type attacks to activate Volt Absorb", async () => {
game.override.enemyAbility(Abilities.VOLT_ABSORB);
await game.startBattle();
await game.classicMode.startBattle();
const playerPokemon = game.scene.getPlayerPokemon()!;
vi.spyOn(playerPokemon, "getMoveType");
const enemyPokemon = game.scene.getEnemyPokemon()!;
vi.spyOn(enemyPokemon, "apply");
const spy = vi.spyOn(enemyPokemon, "getMoveEffectiveness");
enemyPokemon.hp = Math.floor(enemyPokemon.getMaxHp() * 0.8);
@ -77,37 +78,37 @@ describe("Abilities - Galvanize", () => {
await game.phaseInterceptor.to("BerryPhase", false);
expect(playerPokemon.getMoveType).toHaveLastReturnedWith(PokemonType.ELECTRIC);
expect(enemyPokemon.apply).toHaveReturnedWith(HitResult.NO_EFFECT);
expect(spy).toHaveReturnedWith(0);
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
});
it("should not change the type of variable-type moves", async () => {
game.override.enemySpecies(Species.MIGHTYENA);
await game.startBattle([Species.ESPEON]);
await game.classicMode.startBattle([Species.ESPEON]);
const playerPokemon = game.scene.getPlayerPokemon()!;
vi.spyOn(playerPokemon, "getMoveType");
const enemyPokemon = game.scene.getEnemyPokemon()!;
vi.spyOn(enemyPokemon, "apply");
const spy = vi.spyOn(enemyPokemon, "getMoveEffectiveness");
game.move.select(Moves.REVELATION_DANCE);
await game.phaseInterceptor.to("BerryPhase", false);
expect(playerPokemon.getMoveType).not.toHaveLastReturnedWith(PokemonType.ELECTRIC);
expect(enemyPokemon.apply).toHaveReturnedWith(HitResult.NO_EFFECT);
expect(spy).toHaveReturnedWith(0);
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
});
it("should affect all hits of a Normal-type multi-hit move", async () => {
await game.startBattle();
await game.classicMode.startBattle();
const playerPokemon = game.scene.getPlayerPokemon()!;
vi.spyOn(playerPokemon, "getMoveType");
const enemyPokemon = game.scene.getEnemyPokemon()!;
vi.spyOn(enemyPokemon, "apply");
const spy = vi.spyOn(enemyPokemon, "getMoveEffectiveness");
game.move.select(Moves.FURY_SWIPES);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
@ -125,6 +126,6 @@ describe("Abilities - Galvanize", () => {
expect(enemyPokemon.hp).toBeLessThan(enemyStartingHp);
}
expect(enemyPokemon.apply).not.toHaveReturnedWith(HitResult.NO_EFFECT);
expect(spy).not.toHaveReturnedWith(0);
});
});

View File

@ -61,11 +61,11 @@ describe("Abilities - Infiltrator", () => {
const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
const preScreenDmg = enemy.getAttackDamage(player, allMoves[move]).damage;
const preScreenDmg = enemy.getAttackDamage({ source: player, move: allMoves[move] }).damage;
game.scene.arena.addTag(tagType, 1, Moves.NONE, enemy.id, ArenaTagSide.ENEMY, true);
const postScreenDmg = enemy.getAttackDamage(player, allMoves[move]).damage;
const postScreenDmg = enemy.getAttackDamage({ source: player, move: allMoves[move] }).damage;
expect(postScreenDmg).toBe(preScreenDmg);
expect(player.battleData.abilitiesApplied[0]).toBe(Abilities.INFILTRATOR);

View File

@ -4,6 +4,7 @@ import { MoveEndPhase } from "#app/phases/move-end-phase";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import { HitCheckResult } from "#enums/hit-check-result";
import GameManager from "#test/testUtils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, it, expect, vi } from "vitest";
@ -28,6 +29,7 @@ describe("Abilities - No Guard", () => {
.moveset(Moves.ZAP_CANNON)
.ability(Abilities.NO_GUARD)
.enemyLevel(200)
.enemySpecies(Species.SNORLAX)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);
});
@ -48,7 +50,7 @@ describe("Abilities - No Guard", () => {
await game.phaseInterceptor.to(MoveEndPhase);
expect(moveEffectPhase.hitCheck).toHaveReturnedWith(true);
expect(moveEffectPhase.hitCheck).toHaveReturnedWith([HitCheckResult.HIT, 1]);
});
it("should guarantee double battle with any one LURE", async () => {

View File

@ -52,7 +52,7 @@ describe("Abilities - Shield Dust", () => {
// Shield Dust negates secondary effect
const phase = game.scene.getCurrentPhase() as MoveEffectPhase;
const move = phase.move.getMove();
const move = phase.move;
expect(move.id).toBe(Moves.AIR_SLASH);
const chance = new NumberHolder(move.chance);

View File

@ -25,7 +25,6 @@ describe("Abilities - Super Luck", () => {
.moveset([Moves.TACKLE])
.ability(Abilities.SUPER_LUCK)
.battleStyle("single")
.disableCrits()
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);

View File

@ -2,7 +2,6 @@ import { BattlerIndex } from "#app/battle";
import { Abilities } from "#app/enums/abilities";
import { Moves } from "#app/enums/moves";
import { Species } from "#app/enums/species";
import { HitResult } from "#app/field/pokemon";
import GameManager from "#test/testUtils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
@ -87,13 +86,15 @@ describe("Abilities - Tera Shell", () => {
await game.classicMode.startBattle([Species.CHARIZARD]);
const playerPokemon = game.scene.getPlayerPokemon()!;
vi.spyOn(playerPokemon, "apply");
const spy = vi.spyOn(playerPokemon, "getMoveEffectiveness");
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to("BerryPhase", false);
expect(playerPokemon.apply).toHaveLastReturnedWith(HitResult.EFFECTIVE);
expect(spy).toHaveLastReturnedWith(1);
expect(playerPokemon.hp).toBe(playerPokemon.getMaxHp() - 40);
spy.mockRestore();
});
it("should change the effectiveness of all strikes of a multi-strike move", async () => {
@ -102,7 +103,7 @@ describe("Abilities - Tera Shell", () => {
await game.classicMode.startBattle([Species.SNORLAX]);
const playerPokemon = game.scene.getPlayerPokemon()!;
vi.spyOn(playerPokemon, "apply");
const spy = vi.spyOn(playerPokemon, "getMoveEffectiveness");
game.move.select(Moves.SPLASH);
@ -110,8 +111,9 @@ describe("Abilities - Tera Shell", () => {
await game.move.forceHit();
for (let i = 0; i < 2; i++) {
await game.phaseInterceptor.to("MoveEffectPhase");
expect(playerPokemon.apply).toHaveLastReturnedWith(HitResult.NOT_VERY_EFFECTIVE);
expect(spy).toHaveLastReturnedWith(0.5);
}
expect(playerPokemon.apply).toHaveReturnedTimes(2);
expect(spy).toHaveReturnedTimes(2);
spy.mockRestore();
});
});

View File

@ -47,7 +47,9 @@ describe("Battle Mechanics - Damage Calculation", () => {
// expected base damage = [(2*level/5 + 2) * power * playerATK / enemyDEF / 50] + 2
// = 31.8666...
expect(enemyPokemon.getAttackDamage(playerPokemon, allMoves[Moves.TACKLE]).damage).toBeCloseTo(31);
expect(enemyPokemon.getAttackDamage({ source: playerPokemon, move: allMoves[Moves.TACKLE] }).damage).toBeCloseTo(
31,
);
});
it("Attacks deal 1 damage at minimum", async () => {
@ -91,7 +93,7 @@ describe("Battle Mechanics - Damage Calculation", () => {
const magikarp = game.scene.getPlayerPokemon()!;
const dragonite = game.scene.getEnemyPokemon()!;
expect(dragonite.getAttackDamage(magikarp, allMoves[Moves.DRAGON_RAGE]).damage).toBe(40);
expect(dragonite.getAttackDamage({ source: magikarp, move: allMoves[Moves.DRAGON_RAGE] }).damage).toBe(40);
});
it("One-hit KO moves ignore damage multipliers", async () => {
@ -102,7 +104,7 @@ describe("Battle Mechanics - Damage Calculation", () => {
const magikarp = game.scene.getPlayerPokemon()!;
const aggron = game.scene.getEnemyPokemon()!;
expect(aggron.getAttackDamage(magikarp, allMoves[Moves.FISSURE]).damage).toBe(aggron.hp);
expect(aggron.getAttackDamage({ source: magikarp, move: allMoves[Moves.FISSURE] }).damage).toBe(aggron.hp);
});
it("When the user fails to use Jump Kick with Wonder Guard ability, the damage should be 1.", async () => {

View File

@ -1,5 +1,5 @@
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { PokemonTurnData, TurnMove, PokemonMove } from "#app/field/pokemon";
import type { PokemonTurnData, TurnMove } from "#app/field/pokemon";
import type Pokemon from "#app/field/pokemon";
import { MoveResult } from "#app/field/pokemon";
import type BattleScene from "#app/battle-scene";
@ -186,12 +186,8 @@ describe("BattlerTag - SubstituteTag", () => {
vi.spyOn(mockPokemon.scene as BattleScene, "triggerPokemonBattleAnim").mockReturnValue(true);
vi.spyOn(mockPokemon.scene as BattleScene, "queueMessage").mockReturnValue();
const pokemonMove = {
getMove: vi.fn().mockReturnValue(allMoves[Moves.TACKLE]) as PokemonMove["getMove"],
} as PokemonMove;
const moveEffectPhase = {
move: pokemonMove,
move: allMoves[Moves.TACKLE],
getUserPokemon: vi.fn().mockReturnValue(undefined) as MoveEffectPhase["getUserPokemon"],
} as MoveEffectPhase;

View File

@ -36,8 +36,7 @@ describe("Items - Dire Hit", () => {
.enemyMoveset(Moves.SPLASH)
.moveset([Moves.POUND])
.startingHeldItems([{ name: "DIRE_HIT" }])
.battleStyle("single")
.disableCrits();
.battleStyle("single");
}, 20000);
it("should raise CRIT stage by 1", async () => {

View File

@ -28,7 +28,6 @@ describe("Items - Leek", () => {
.enemyMoveset([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH])
.startingHeldItems([{ name: "LEEK" }])
.moveset([Moves.TACKLE])
.disableCrits()
.battleStyle("single");
});

View File

@ -27,8 +27,7 @@ describe("Items - Scope Lens", () => {
.enemyMoveset(Moves.SPLASH)
.moveset([Moves.POUND])
.startingHeldItems([{ name: "SCOPE_LENS" }])
.battleStyle("single")
.disableCrits();
.battleStyle("single");
}, 20000);
it("should raise CRIT stage by 1", async () => {

View File

@ -97,14 +97,20 @@ describe("Moves - Dig", () => {
const playerPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
const preDigEarthquakeDmg = playerPokemon.getAttackDamage(enemyPokemon, allMoves[Moves.EARTHQUAKE]).damage;
const preDigEarthquakeDmg = playerPokemon.getAttackDamage({
source: enemyPokemon,
move: allMoves[Moves.EARTHQUAKE],
}).damage;
game.move.select(Moves.DIG);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.phaseInterceptor.to("MoveEffectPhase");
const postDigEarthquakeDmg = playerPokemon.getAttackDamage(enemyPokemon, allMoves[Moves.EARTHQUAKE]).damage;
const postDigEarthquakeDmg = playerPokemon.getAttackDamage({
source: enemyPokemon,
move: allMoves[Moves.EARTHQUAKE],
}).damage;
// these hopefully get avoid rounding errors :shrug:
expect(postDigEarthquakeDmg).toBeGreaterThanOrEqual(2 * preDigEarthquakeDmg);
expect(postDigEarthquakeDmg).toBeLessThan(2 * (preDigEarthquakeDmg + 1));

View File

@ -50,7 +50,7 @@ describe("Moves - Dynamax Cannon", () => {
game.move.select(dynamaxCannon.id);
await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(dynamaxCannon.id);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.id).toBe(dynamaxCannon.id);
await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(100);
}, 20000);
@ -62,7 +62,7 @@ describe("Moves - Dynamax Cannon", () => {
game.move.select(dynamaxCannon.id);
await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(dynamaxCannon.id);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.id).toBe(dynamaxCannon.id);
await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(100);
}, 20000);
@ -75,7 +75,7 @@ describe("Moves - Dynamax Cannon", () => {
await game.phaseInterceptor.to(MoveEffectPhase, false);
const phase = game.scene.getCurrentPhase() as MoveEffectPhase;
expect(phase.move.moveId).toBe(dynamaxCannon.id);
expect(phase.move.id).toBe(dynamaxCannon.id);
// Force level cap to be 100
vi.spyOn(game.scene, "getMaxExpLevel").mockReturnValue(100);
await game.phaseInterceptor.to(DamageAnimPhase, false);
@ -90,7 +90,7 @@ describe("Moves - Dynamax Cannon", () => {
await game.phaseInterceptor.to(MoveEffectPhase, false);
const phase = game.scene.getCurrentPhase() as MoveEffectPhase;
expect(phase.move.moveId).toBe(dynamaxCannon.id);
expect(phase.move.id).toBe(dynamaxCannon.id);
// Force level cap to be 100
vi.spyOn(game.scene, "getMaxExpLevel").mockReturnValue(100);
await game.phaseInterceptor.to(DamageAnimPhase, false);
@ -105,7 +105,7 @@ describe("Moves - Dynamax Cannon", () => {
await game.phaseInterceptor.to(MoveEffectPhase, false);
const phase = game.scene.getCurrentPhase() as MoveEffectPhase;
expect(phase.move.moveId).toBe(dynamaxCannon.id);
expect(phase.move.id).toBe(dynamaxCannon.id);
// Force level cap to be 100
vi.spyOn(game.scene, "getMaxExpLevel").mockReturnValue(100);
await game.phaseInterceptor.to(DamageAnimPhase, false);
@ -120,7 +120,7 @@ describe("Moves - Dynamax Cannon", () => {
await game.phaseInterceptor.to(MoveEffectPhase, false);
const phase = game.scene.getCurrentPhase() as MoveEffectPhase;
expect(phase.move.moveId).toBe(dynamaxCannon.id);
expect(phase.move.id).toBe(dynamaxCannon.id);
// Force level cap to be 100
vi.spyOn(game.scene, "getMaxExpLevel").mockReturnValue(100);
await game.phaseInterceptor.to(DamageAnimPhase, false);
@ -135,7 +135,7 @@ describe("Moves - Dynamax Cannon", () => {
await game.phaseInterceptor.to(MoveEffectPhase, false);
const phase = game.scene.getCurrentPhase() as MoveEffectPhase;
expect(phase.move.moveId).toBe(dynamaxCannon.id);
expect(phase.move.id).toBe(dynamaxCannon.id);
// Force level cap to be 100
vi.spyOn(game.scene, "getMaxExpLevel").mockReturnValue(100);
await game.phaseInterceptor.to(DamageAnimPhase, false);
@ -150,7 +150,7 @@ describe("Moves - Dynamax Cannon", () => {
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(dynamaxCannon.id);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.id).toBe(dynamaxCannon.id);
await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(200);
}, 20000);

View File

@ -57,12 +57,12 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => {
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]);
await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionFlare.id);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionFlare.id);
await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(100);
await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionBolt.id);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionBolt.id);
await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionBolt.calculateBattlePower).toHaveLastReturnedWith(200);
}, 20000);
@ -77,12 +77,12 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => {
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]);
await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionBolt.id);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionBolt.id);
await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionBolt.calculateBattlePower).toHaveLastReturnedWith(100);
await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionFlare.id);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionFlare.id);
await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(200);
}, 20000);
@ -97,7 +97,7 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => {
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY]);
await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionFlare.id);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionFlare.id);
await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(100);
@ -107,7 +107,7 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => {
await game.phaseInterceptor.runFrom(MovePhase).to(MoveEndPhase);
await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionBolt.id);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionBolt.id);
await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionBolt.calculateBattlePower).toHaveLastReturnedWith(200);
}, 20000);
@ -123,7 +123,7 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => {
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY]);
await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionFlare.id);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionFlare.id);
await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(100);
@ -132,7 +132,7 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => {
await game.phaseInterceptor.runFrom(MovePhase).to(MoveEndPhase);
await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionBolt.id);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionBolt.id);
await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionBolt.calculateBattlePower).toHaveLastReturnedWith(100);
}, 20000);
@ -147,12 +147,12 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => {
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]);
await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionBolt.id);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionBolt.id);
await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionBolt.calculateBattlePower).toHaveLastReturnedWith(100);
await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionFlare.id);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionFlare.id);
await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(200);
}, 20000);
@ -191,22 +191,22 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => {
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY]);
await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionBolt.id);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionBolt.id);
await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionBolt.calculateBattlePower).toHaveLastReturnedWith(100);
await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionFlare.id);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionFlare.id);
await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(200);
await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionBolt.id);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionBolt.id);
await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionBolt.calculateBattlePower).toHaveLastReturnedWith(200);
await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionFlare.id);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionFlare.id);
await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(200);
}, 20000);
@ -245,22 +245,22 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => {
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY]);
await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionBolt.id);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionBolt.id);
await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionBolt.calculateBattlePower).toHaveLastReturnedWith(100);
await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionFlare.id);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionFlare.id);
await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(200);
await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionBolt.id);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionBolt.id);
await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionBolt.calculateBattlePower).toHaveLastReturnedWith(200);
await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionFlare.id);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionFlare.id);
await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(200);
}, 20000);

View File

@ -71,7 +71,7 @@ describe("Moves - Spectral Thief", () => {
const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
const moveToCheck = allMoves[Moves.SPECTRAL_THIEF];
const dmgBefore = enemy.getAttackDamage(player, moveToCheck, false, false, false, false).damage;
const dmgBefore = enemy.getAttackDamage({ source: player, move: moveToCheck }).damage;
enemy.setStatStage(Stat.ATK, 6);
@ -80,7 +80,7 @@ describe("Moves - Spectral Thief", () => {
game.move.select(Moves.SPECTRAL_THIEF);
await game.phaseInterceptor.to(TurnEndPhase);
expect(dmgBefore).toBeLessThan(enemy.getAttackDamage(player, moveToCheck, false, false, false, false).damage);
expect(dmgBefore).toBeLessThan(enemy.getAttackDamage({ source: player, move: moveToCheck }).damage);
});
it("should steal stat stages as a negative value with Contrary.", async () => {

View File

@ -4,7 +4,6 @@ import { allMoves, TeraMoveCategoryAttr } from "#app/data/moves/move";
import type Move from "#app/data/moves/move";
import { PokemonType } from "#enums/pokemon-type";
import { Abilities } from "#app/enums/abilities";
import { HitResult } from "#app/field/pokemon";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/testUtils/gameManager";
@ -49,9 +48,9 @@ describe("Moves - Tera Blast", () => {
it("changes type to match user's tera type", async () => {
game.override.enemySpecies(Species.FURRET);
await game.startBattle();
await game.classicMode.startBattle();
const enemyPokemon = game.scene.getEnemyPokemon()!;
vi.spyOn(enemyPokemon, "apply");
const spy = vi.spyOn(enemyPokemon, "getMoveEffectiveness");
const playerPokemon = game.scene.getPlayerPokemon()!;
playerPokemon.teraType = PokemonType.FIGHTING;
@ -61,11 +60,11 @@ describe("Moves - Tera Blast", () => {
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.phaseInterceptor.to("MoveEffectPhase");
expect(enemyPokemon.apply).toHaveReturnedWith(HitResult.SUPER_EFFECTIVE);
expect(spy).toHaveReturnedWith(2);
}, 20000);
it("increases power if user is Stellar tera type", async () => {
await game.startBattle();
await game.classicMode.startBattle();
const playerPokemon = game.scene.getPlayerPokemon()!;
playerPokemon.teraType = PokemonType.STELLAR;
@ -79,25 +78,25 @@ describe("Moves - Tera Blast", () => {
}, 20000);
it("is super effective against terastallized targets if user is Stellar tera type", async () => {
await game.startBattle();
await game.classicMode.startBattle();
const playerPokemon = game.scene.getPlayerPokemon()!;
playerPokemon.teraType = PokemonType.STELLAR;
playerPokemon.isTerastallized = true;
const enemyPokemon = game.scene.getEnemyPokemon()!;
vi.spyOn(enemyPokemon, "apply");
const spy = vi.spyOn(enemyPokemon, "getMoveEffectiveness");
enemyPokemon.isTerastallized = true;
game.move.select(Moves.TERA_BLAST);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.phaseInterceptor.to("MoveEffectPhase");
expect(enemyPokemon.apply).toHaveReturnedWith(HitResult.SUPER_EFFECTIVE);
expect(spy).toHaveReturnedWith(2);
});
it("uses the higher ATK for damage calculation", async () => {
await game.startBattle();
await game.classicMode.startBattle();
const playerPokemon = game.scene.getPlayerPokemon()!;
playerPokemon.stats[Stat.ATK] = 100;
@ -112,7 +111,7 @@ describe("Moves - Tera Blast", () => {
});
it("uses the higher SPATK for damage calculation", async () => {
await game.startBattle();
await game.classicMode.startBattle();
const playerPokemon = game.scene.getPlayerPokemon()!;
playerPokemon.stats[Stat.ATK] = 1;
@ -127,7 +126,7 @@ describe("Moves - Tera Blast", () => {
it("should stay as a special move if ATK turns lower than SPATK mid-turn", async () => {
game.override.enemyMoveset([Moves.CHARM]);
await game.startBattle();
await game.classicMode.startBattle();
const playerPokemon = game.scene.getPlayerPokemon()!;
playerPokemon.stats[Stat.ATK] = 51;
@ -145,7 +144,7 @@ describe("Moves - Tera Blast", () => {
game.override
.startingHeldItems([{ name: "SPECIES_STAT_BOOSTER", type: "THICK_CLUB" }])
.starterSpecies(Species.CUBONE);
await game.startBattle();
await game.classicMode.startBattle();
const playerPokemon = game.scene.getPlayerPokemon()!;
@ -163,7 +162,7 @@ describe("Moves - Tera Blast", () => {
it("does not change its move category from stat changes due to abilities", async () => {
game.override.ability(Abilities.HUGE_POWER);
await game.startBattle();
await game.classicMode.startBattle();
const playerPokemon = game.scene.getPlayerPokemon()!;
playerPokemon.stats[Stat.ATK] = 50;
@ -178,7 +177,7 @@ describe("Moves - Tera Blast", () => {
});
it("causes stat drops if user is Stellar tera type", async () => {
await game.startBattle();
await game.classicMode.startBattle();
const playerPokemon = game.scene.getPlayerPokemon()!;
playerPokemon.teraType = PokemonType.STELLAR;

View File

@ -163,6 +163,7 @@ describe("Moves - Whirlwind", () => {
it("should not pull in the other trainer's pokemon in a partner trainer battle", async () => {
game.override
.startingWave(2)
.battleType(BattleType.TRAINER)
.randomTrainer({
trainerType: TrainerType.BREEDER,

View File

@ -21,6 +21,8 @@ import KeyboardPlugin = Phaser.Input.Keyboard.KeyboardPlugin;
import GamepadPlugin = Phaser.Input.Gamepad.GamepadPlugin;
import EventEmitter = Phaser.Events.EventEmitter;
import UpdateList = Phaser.GameObjects.UpdateList;
import { PokedexMonContainer } from "#app/ui/pokedex-mon-container";
import MockContainer from "./mocks/mocksContainer/mockContainer";
// biome-ignore lint/style/noNamespaceImport: Necessary in order to mock the var
import * as bypassLoginModule from "#app/global-vars/bypass-login";
@ -61,6 +63,10 @@ export default class GameWrapper {
}
};
BattleScene.prototype.addPokemonIcon = () => new Phaser.GameObjects.Container(this.scene);
// Pokedex container is not actually mocking container, but the sprites they contain are mocked.
// We need to mock the remove function to not throw an error when removing a sprite.
PokedexMonContainer.prototype.remove = MockContainer.prototype.remove;
}
setScene(scene: BattleScene) {

View File

@ -18,29 +18,29 @@ import { vi } from "vitest";
*/
export class MoveHelper extends GameManagerHelper {
/**
* Intercepts {@linkcode MoveEffectPhase} and mocks the
* {@linkcode MoveEffectPhase.hitCheck | hitCheck}'s return value to `true`.
* Used to force a move to hit.
* Intercepts {@linkcode MoveEffectPhase} and mocks the phase's move's
* accuracy to -1, guaranteeing a hit.
*/
public async forceHit(): Promise<void> {
await this.game.phaseInterceptor.to(MoveEffectPhase, false);
vi.spyOn(this.game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck").mockReturnValue(true);
const moveEffectPhase = this.game.scene.getCurrentPhase() as MoveEffectPhase;
vi.spyOn(moveEffectPhase.move, "calculateBattleAccuracy").mockReturnValue(-1);
}
/**
* Intercepts {@linkcode MoveEffectPhase} and mocks the
* {@linkcode MoveEffectPhase.hitCheck | hitCheck}'s return value to `false`.
* Used to force a move to miss.
* Intercepts {@linkcode MoveEffectPhase} and mocks the phase's move's accuracy
* to 0, guaranteeing a miss.
* @param firstTargetOnly - Whether the move should force miss on the first target only, in the case of multi-target moves.
*/
public async forceMiss(firstTargetOnly = false): Promise<void> {
await this.game.phaseInterceptor.to(MoveEffectPhase, false);
const hitCheck = vi.spyOn(this.game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck");
const moveEffectPhase = this.game.scene.getCurrentPhase() as MoveEffectPhase;
const accuracy = vi.spyOn(moveEffectPhase.move, "calculateBattleAccuracy");
if (firstTargetOnly) {
hitCheck.mockReturnValueOnce(false);
accuracy.mockReturnValueOnce(0);
} else {
hitCheck.mockReturnValue(false);
accuracy.mockReturnValue(0);
}
}

View File

@ -308,5 +308,14 @@ export default class MockText implements MockGameObject {
return this.list;
}
/**
* Runs the word wrap algorithm on the text, then returns an array of the lines
*/
getWrappedText() {
// Returns the wrapped text.
// return this.phaserText.getWrappedText();
return this.runWordWrap(this.text).split("\n");
}
on(_event: string | symbol, _fn: Function, _context?: any) {}
}

View File

@ -205,6 +205,7 @@ export default class PhaseInterceptor {
private phaseFrom;
private inProgress;
private originalSetMode;
private originalSetOverlayMode;
private originalSuperEnd;
/**
@ -442,6 +443,7 @@ export default class PhaseInterceptor {
*/
initPhases() {
this.originalSetMode = UI.prototype.setMode;
this.originalSetOverlayMode = UI.prototype.setOverlayMode;
this.originalSuperEnd = Phase.prototype.end;
UI.prototype.setMode = (mode, ...args) => this.setMode.call(this, mode, ...args);
Phase.prototype.end = () => this.superEndPhase.call(this);
@ -508,6 +510,18 @@ export default class PhaseInterceptor {
return ret;
}
/**
* mock to set overlay mode
* @param mode - The {@linkcode Mode} to set.
* @param args - Additional arguments to pass to the original method.
*/
setOverlayMode(mode: UiMode, ...args: unknown[]): Promise<void> {
const instance = this.scene.ui;
console.log("setOverlayMode", `${UiMode[mode]} (=${mode})`, args);
const ret = this.originalSetOverlayMode.apply(instance, [mode, ...args]);
return ret;
}
/**
* Method to start the prompt handler.
*/
@ -572,6 +586,7 @@ export default class PhaseInterceptor {
phase.prototype.start = this.phases[phase.name].start;
}
UI.prototype.setMode = this.originalSetMode;
UI.prototype.setOverlayMode = this.originalSetOverlayMode;
Phase.prototype.end = this.originalSuperEnd;
clearInterval(this.promptInterval);
clearInterval(this.interval);

File diff suppressed because one or more lines are too long

View File

@ -3,7 +3,7 @@ import { initLoggedInUser } from "#app/account";
import { initAbilities } from "#app/data/abilities/ability";
import { initBiomes } from "#app/data/balance/biomes";
import { initEggMoves } from "#app/data/balance/egg-moves";
import { initPokemonPrevolutions } from "#app/data/balance/pokemon-evolutions";
import { initPokemonPrevolutions, initPokemonStarters } from "#app/data/balance/pokemon-evolutions";
import { initMoves } from "#app/data/moves/move";
import { initMysteryEncounters } from "#app/data/mystery-encounters/mystery-encounters";
import { initPokemonForms } from "#app/data/pokemon-forms";
@ -85,7 +85,6 @@ export function initTestFile() {
HTMLCanvasElement.prototype.getContext = () => mockContext;
// Initialize all of these things if and only if they have not been initialized yet
// initSpecies();
if (!wasInitialized) {
wasInitialized = true;
initI18n();
@ -101,6 +100,8 @@ export function initTestFile() {
initAbilities();
initLoggedInUser();
initMysteryEncounters();
// init the pokemon starters for the pokedex
initPokemonStarters();
}
manageListeners();

492
test/ui/pokedex.test.ts Normal file
View File

@ -0,0 +1,492 @@
import GameManager from "#test/testUtils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, type MockInstance, vi } from "vitest";
import PokedexUiHandler from "#app/ui/pokedex-ui-handler";
import { FilterTextRow } from "#app/ui/filter-text";
import { allAbilities } from "#app/data/data-lists";
import { Abilities } from "#enums/abilities";
import { Species } from "#enums/species";
import { allSpecies, getPokemonSpecies, type PokemonForm } from "#app/data/pokemon-species";
import { Button } from "#enums/buttons";
import { DropDownColumn } from "#app/ui/filter-bar";
import type PokemonSpecies from "#app/data/pokemon-species";
import { PokemonType } from "#enums/pokemon-type";
import { UiMode } from "#enums/ui-mode";
/*
Information for the `data_pokedex_tests.psrv`:
Caterpie - Shiny 0
Rattata - Shiny 1
Ekans - Shiny 2
Chikorita has enough candies to unlock passive
Cyndaquil has first cost reduction unlocked, enough candies to buy the second
Totodile has first cost reduction unlocked, not enough candies to buy the second
Treecko has both cost reduction unlocked
Torchic has enough candies to do anything
Mudkip has passive unlocked
Turtwig has enough candies to purchase an egg
*/
/**
* Return all permutations of elements from an array
*/
function permutations<T>(array: T[], length: number): T[][] {
if (length === 0) {
return [[]];
}
return array.flatMap((item, index) =>
permutations([...array.slice(0, index), ...array.slice(index + 1)], length - 1).map(perm => [item, ...perm]),
);
}
describe("UI - Pokedex", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
const mocks: MockInstance[] = [];
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
while (mocks.length > 0) {
mocks.pop()?.mockRestore();
}
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
});
/**
* Run the game to open the pokedex UI.
* @returns The handler for the pokedex UI.
*/
async function runToOpenPokedex(): Promise<PokedexUiHandler> {
// Open the pokedex UI.
await game.runToTitle();
await game.phaseInterceptor.setOverlayMode(UiMode.POKEDEX);
// Get the handler for the current UI.
const handler = game.scene.ui.getHandler();
expect(handler).toBeInstanceOf(PokedexUiHandler);
return handler as PokedexUiHandler;
}
/**
* Compute a set of pokemon that have a specific ability in allAbilities
* @param ability - The ability to filter for
*/
function getSpeciesWithAbility(ability: Abilities): Set<Species> {
const speciesSet = new Set<Species>();
for (const pkmn of allSpecies) {
if (
[pkmn.ability1, pkmn.ability2, pkmn.getPassiveAbility(), pkmn.abilityHidden].includes(ability) ||
pkmn.forms.some(form =>
[form.ability1, form.ability2, form.abilityHidden, form.getPassiveAbility()].includes(ability),
)
) {
speciesSet.add(pkmn.speciesId);
}
}
return speciesSet;
}
/**
* Compute a set of pokemon that have one of the specified type(s)
*
* Includes all forms of the pokemon
* @param types - The types to filter for
*/
function getSpeciesWithType(...types: PokemonType[]): Set<Species> {
const speciesSet = new Set<Species>();
const tySet = new Set<PokemonType>(types);
// get the pokemon and its forms
outer: for (const pkmn of allSpecies) {
// @ts-expect-error We know that type2 might be null.
if (tySet.has(pkmn.type1) || tySet.has(pkmn.type2)) {
speciesSet.add(pkmn.speciesId);
continue;
}
for (const form of pkmn.forms) {
// @ts-expect-error We know that type2 might be null.
if (tySet.has(form.type1) || tySet.has(form.type2)) {
speciesSet.add(pkmn.speciesId);
continue outer;
}
}
}
return speciesSet;
}
/**
* Create mocks for the abilities of a species.
* This is used to set the abilities of a species to a specific value.
* All abilities are optional. Not providing one will set it to NONE.
*
* This will override the ability of the pokemon species only, unless set forms is true
*
* @param species - The species to set the abilities for
* @param ability - The ability to set for the first ability
* @param ability2 - The ability to set for the second ability
* @param hidden - The ability to set for the hidden ability
* @param passive - The ability to set for the passive ability
* @param setForms - Whether to also overwrite the abilities for each of the species' forms (defaults to `true`)
*/
function createAbilityMocks(
species: Species,
{
ability = Abilities.NONE,
ability2 = Abilities.NONE,
hidden = Abilities.NONE,
passive = Abilities.NONE,
setForms = true,
}: {
ability?: Abilities;
ability2?: Abilities;
hidden?: Abilities;
passive?: Abilities;
setForms?: boolean;
},
) {
const pokemon = getPokemonSpecies(species);
const checks: [PokemonSpecies | PokemonForm] = [pokemon];
if (setForms) {
checks.push(...pokemon.forms);
}
for (const p of checks) {
mocks.push(vi.spyOn(p, "ability1", "get").mockReturnValue(ability));
mocks.push(vi.spyOn(p, "ability2", "get").mockReturnValue(ability2));
mocks.push(vi.spyOn(p, "abilityHidden", "get").mockReturnValue(hidden));
mocks.push(vi.spyOn(p, "getPassiveAbility").mockReturnValue(passive));
}
}
/***************************
* Tests for Filters *
***************************/
it("should filter to show only the pokemon with an ability when filtering by ability", async () => {
// await game.importData("test/testUtils/saves/everything.prsv");
const pokedexHandler = await runToOpenPokedex();
// Get name of overgrow
const overgrow = allAbilities[Abilities.OVERGROW].name;
// @ts-expect-error `filterText` is private
pokedexHandler.filterText.setValue(FilterTextRow.ABILITY_1, overgrow);
// filter all species to be the pokemon that have overgrow
const overgrowSpecies = getSpeciesWithAbility(Abilities.OVERGROW);
// @ts-expect-error - `filteredPokemonData` is private
const filteredSpecies = new Set(pokedexHandler.filteredPokemonData.map(pokemon => pokemon.species.speciesId));
expect(filteredSpecies).toEqual(overgrowSpecies);
});
it("should filter to show only pokemon with ability and passive when filtering by 2 abilities", async () => {
// Setup mocks for the ability and passive combinations
const whitelist: Species[] = [];
const blacklist: Species[] = [];
const filter_ab1 = Abilities.OVERGROW;
const filter_ab2 = Abilities.ADAPTABILITY;
const ab1_instance = allAbilities[filter_ab1];
const ab2_instance = allAbilities[filter_ab2];
// Create a species with passive set and each "ability" field
const baseObj = {
ability: Abilities.BALL_FETCH,
ability2: Abilities.NONE,
hidden: Abilities.BLAZE,
passive: Abilities.TORRENT,
};
// Mock pokemon to have the exhaustive combination of the two selected abilities
const attrs: (keyof typeof baseObj)[] = ["ability", "ability2", "hidden", "passive"];
for (const [idx, value] of permutations(attrs, 2).entries()) {
createAbilityMocks(Species.BULBASAUR + idx, {
...baseObj,
[value[0]]: filter_ab1,
[value[1]]: filter_ab2,
});
if (value.includes("passive")) {
whitelist.push(Species.BULBASAUR + idx);
} else {
blacklist.push(Species.BULBASAUR + idx);
}
}
const pokedexHandler = await runToOpenPokedex();
// @ts-expect-error `filterText` is private
pokedexHandler.filterText.setValue(FilterTextRow.ABILITY_1, ab1_instance.name);
// @ts-expect-error `filterText` is private
pokedexHandler.filterText.setValue(FilterTextRow.ABILITY_2, ab2_instance.name);
let whiteListCount = 0;
// @ts-expect-error `filteredPokemonData` is private
for (const species of pokedexHandler.filteredPokemonData) {
expect(blacklist, "entry must have one of the abilities as a passive").not.toContain(species.species.speciesId);
const rawAbility = [species.species.ability1, species.species.ability2, species.species.abilityHidden];
const rawPassive = species.species.getPassiveAbility();
const c1 = rawPassive === ab1_instance.id && rawAbility.includes(ab2_instance.id);
const c2 = c1 || (rawPassive === ab2_instance.id && rawAbility.includes(ab1_instance.id));
expect(c2, "each filtered entry should have the ability and passive combination").toBe(true);
if (whitelist.includes(species.species.speciesId)) {
whiteListCount++;
}
}
expect(whiteListCount).toBe(whitelist.length);
});
it("should filter to show only the pokemon with a type when filtering by a single type", async () => {
const pokedexHandler = await runToOpenPokedex();
// @ts-expect-error - `filterBar` is private
pokedexHandler.filterBar.getFilter(DropDownColumn.TYPES).toggleOptionState(PokemonType.NORMAL + 1);
const expectedPokemon = getSpeciesWithType(PokemonType.NORMAL);
// @ts-expect-error - `filteredPokemonData` is private
const filteredPokemon = new Set(pokedexHandler.filteredPokemonData.map(pokemon => pokemon.species.speciesId));
expect(filteredPokemon).toEqual(expectedPokemon);
});
// Todo: Pokemon with a mega that adds a type do not show up in the filter, e.g. pinsir.
it.todo("should show only the pokemon with one of the types when filtering by multiple types", async () => {
const pokedexHandler = await runToOpenPokedex();
// @ts-expect-error - `filterBar` is private
pokedexHandler.filterBar.getFilter(DropDownColumn.TYPES).toggleOptionState(PokemonType.NORMAL + 1);
// @ts-expect-error - `filterBar` is private
pokedexHandler.filterBar.getFilter(DropDownColumn.TYPES).toggleOptionState(PokemonType.FLYING + 1);
const expectedPokemon = getSpeciesWithType(PokemonType.NORMAL, PokemonType.FLYING);
// @ts-expect-error - `filteredPokemonData` is private
const filteredPokemon = new Set(pokedexHandler.filteredPokemonData.map(pokemon => pokemon.species.speciesId));
expect(filteredPokemon).toEqual(expectedPokemon);
});
it("filtering for unlockable cost reduction only shows species with sufficient candies", async () => {
// load the save file
await game.importData("./test/testUtils/saves/data_pokedex_tests.prsv");
const pokedexHandler = await runToOpenPokedex();
// @ts-expect-error - `filterBar` is private
const filter = pokedexHandler.filterBar.getFilter(DropDownColumn.UNLOCKS);
// Cycling 4 times to get to the "can unlock" for cost reduction
for (let i = 0; i < 4; i++) {
// index 1 is the cost reduction
filter.toggleOptionState(1);
}
const expectedPokemon = new Set([
Species.CHIKORITA,
Species.CYNDAQUIL,
Species.TORCHIC,
Species.TURTWIG,
Species.EKANS,
Species.MUDKIP,
]);
expect(
// @ts-expect-error - `filteredPokemonData` is private
pokedexHandler.filteredPokemonData.every(pokemon =>
expectedPokemon.has(pokedexHandler.getStarterSpeciesId(pokemon.species.speciesId)),
),
).toBe(true);
});
it("filtering by passive unlocked only shows species that have their passive", async () => {
await game.importData("./test/testUtils/saves/data_pokedex_tests.prsv");
const pokedexHandler = await runToOpenPokedex();
// @ts-expect-error - `filterBar` is private
const filter = pokedexHandler.filterBar.getFilter(DropDownColumn.UNLOCKS);
filter.toggleOptionState(0); // cycle to Passive: Yes
expect(
// @ts-expect-error - `filteredPokemonData` is private
pokedexHandler.filteredPokemonData.every(
pokemon => pokedexHandler.getStarterSpeciesId(pokemon.species.speciesId) === Species.MUDKIP,
),
).toBe(true);
});
it("filtering for pokemon that can unlock passive shows only species with sufficient candies", async () => {
await game.importData("./test/testUtils/saves/data_pokedex_tests.prsv");
const pokedexHandler = await runToOpenPokedex();
// @ts-expect-error - `filterBar` is private
const filter = pokedexHandler.filterBar.getFilter(DropDownColumn.UNLOCKS);
// Cycling 4 times to get to the "can unlock" for passive
const expectedPokemon = new Set([
Species.EKANS,
Species.CHIKORITA,
Species.CYNDAQUIL,
Species.TORCHIC,
Species.TURTWIG,
]);
// cycling twice to get to the "can unlock" for passive
filter.toggleOptionState(0);
filter.toggleOptionState(0);
expect(
// @ts-expect-error - `filteredPokemonData` is private
pokedexHandler.filteredPokemonData.every(pokemon =>
expectedPokemon.has(pokedexHandler.getStarterSpeciesId(pokemon.species.speciesId)),
),
).toBe(true);
});
it("filtering for pokemon that have any cost reduction shows only the species that have unlocked a cost reduction", async () => {
await game.importData("./test/testUtils/saves/data_pokedex_tests.prsv");
const pokedexHandler = await runToOpenPokedex();
const expectedPokemon = new Set([Species.TREECKO, Species.CYNDAQUIL, Species.TOTODILE]);
// @ts-expect-error - `filterBar` is private
const filter = pokedexHandler.filterBar.getFilter(DropDownColumn.UNLOCKS);
// Cycle 1 time for cost reduction
filter.toggleOptionState(1);
expect(
// @ts-expect-error - `filteredPokemonData` is private
pokedexHandler.filteredPokemonData.every(pokemon =>
expectedPokemon.has(pokedexHandler.getStarterSpeciesId(pokemon.species.speciesId)),
),
).toBe(true);
});
it("filtering for pokemon that have a single cost reduction shows only the species that have unlocked a single cost reduction", async () => {
await game.importData("./test/testUtils/saves/data_pokedex_tests.prsv");
const pokedexHandler = await runToOpenPokedex();
const expectedPokemon = new Set([Species.CYNDAQUIL, Species.TOTODILE]);
// @ts-expect-error - `filterBar` is private
const filter = pokedexHandler.filterBar.getFilter(DropDownColumn.UNLOCKS);
// Cycle 2 times for one cost reduction
filter.toggleOptionState(1);
filter.toggleOptionState(1);
expect(
// @ts-expect-error - `filteredPokemonData` is private
pokedexHandler.filteredPokemonData.every(pokemon =>
expectedPokemon.has(pokedexHandler.getStarterSpeciesId(pokemon.species.speciesId)),
),
).toBe(true);
});
it("filtering for pokemon that have two cost reductions sorts only shows the species that have unlocked both cost reductions", async () => {
await game.importData("./test/testUtils/saves/data_pokedex_tests.prsv");
const pokedexHandler = await runToOpenPokedex();
// @ts-expect-error - `filterBar` is private
const filter = pokedexHandler.filterBar.getFilter(DropDownColumn.UNLOCKS);
// Cycle 3 time for two cost reductions
filter.toggleOptionState(1);
filter.toggleOptionState(1);
filter.toggleOptionState(1);
expect(
// @ts-expect-error - `filteredPokemonData` is private
pokedexHandler.filteredPokemonData.every(
pokemon => pokedexHandler.getStarterSpeciesId(pokemon.species.speciesId) === Species.TREECKO,
),
).toBe(true);
});
it("filtering by shiny status shows the caught pokemon with the selected shiny tier", async () => {
await game.importData("./test/testUtils/saves/data_pokedex_tests.prsv");
const pokedexHandler = await runToOpenPokedex();
// @ts-expect-error - `filterBar` is private
const filter = pokedexHandler.filterBar.getFilter(DropDownColumn.CAUGHT);
filter.toggleOptionState(3);
// @ts-expect-error - `filteredPokemonData` is private
let filteredPokemon = pokedexHandler.filteredPokemonData.map(pokemon => pokemon.species.speciesId);
// Red shiny
expect(filteredPokemon.length).toBe(1);
expect(filteredPokemon[0], "tier 1 shiny").toBe(Species.CATERPIE);
// tier 2 shiny
filter.toggleOptionState(3);
filter.toggleOptionState(2);
// @ts-expect-error - `filteredPokemonData` is private
filteredPokemon = pokedexHandler.filteredPokemonData.map(pokemon => pokemon.species.speciesId);
expect(filteredPokemon.length).toBe(1);
expect(filteredPokemon[0], "tier 2 shiny").toBe(Species.RATTATA);
filter.toggleOptionState(2);
filter.toggleOptionState(1);
// @ts-expect-error - `filteredPokemonData` is private
filteredPokemon = pokedexHandler.filteredPokemonData.map(pokemon => pokemon.species.speciesId);
expect(filteredPokemon.length).toBe(1);
expect(filteredPokemon[0], "tier 3 shiny").toBe(Species.EKANS);
// filter by no shiny
filter.toggleOptionState(1);
filter.toggleOptionState(4);
// @ts-expect-error - `filteredPokemonData` is private
filteredPokemon = pokedexHandler.filteredPokemonData.map(pokemon => pokemon.species.speciesId);
expect(filteredPokemon.length).toBe(27);
expect(filteredPokemon, "not shiny").not.toContain(Species.CATERPIE);
expect(filteredPokemon, "not shiny").not.toContain(Species.RATTATA);
expect(filteredPokemon, "not shiny").not.toContain(Species.EKANS);
});
/****************************
* Tests for UI Input *
****************************/
// TODO: fix cursor wrapping
it.todo(
"should wrap the cursor to the top when moving to an empty entry when there are more than 81 pokemon",
async () => {
const pokedexHandler = await runToOpenPokedex();
// Filter by gen 2 so we can pan a specific amount.
// @ts-expect-error `filterBar` is private
pokedexHandler.filterBar.getFilter(DropDownColumn.GEN).options[2].toggleOptionState();
pokedexHandler.updateStarters();
// @ts-expect-error - `filteredPokemonData` is private
expect(pokedexHandler.filteredPokemonData.length, "pokemon in gen2").toBe(100);
// Let's try to pan to the right to see what the pokemon it points to is.
// pan to the right once and down 11 times
pokedexHandler.processInput(Button.RIGHT);
// Nab the pokemon that is selected for comparison later.
// @ts-expect-error - `lastSpecies` is private
const selectedPokemon = pokedexHandler.lastSpecies.speciesId;
for (let i = 0; i < 11; i++) {
pokedexHandler.processInput(Button.DOWN);
}
// @ts-expect-error `lastSpecies` is private
expect(selectedPokemon).toEqual(pokedexHandler.lastSpecies.speciesId);
},
);
});