mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-06-21 00:52:47 +02:00
Merge remote-tracking branch 'upstream/beta' into waveData
This commit is contained in:
commit
d1e5cd7067
@ -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
18
package-lock.json
generated
@ -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",
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "pokemon-rogue-battle",
|
||||
"private": true,
|
||||
"version": "1.8.4",
|
||||
"version": "1.8.5",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
@ -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"
|
||||
|
@ -516,8 +516,36 @@
|
||||
"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": {
|
||||
"app": "https://www.aseprite.org/",
|
||||
"version": "1.3.7-dev",
|
||||
|
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
|
@ -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";
|
||||
@ -1299,6 +1298,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;
|
||||
}
|
||||
|
||||
newBattle(
|
||||
waveIndex?: number,
|
||||
battleType?: BattleType,
|
||||
@ -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 (newBattleType === BattleType.TRAINER) {
|
||||
newDouble = newTrainer?.variant === TrainerVariant.DOUBLE;
|
||||
}
|
||||
} else if (double === undefined && 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();
|
||||
|
@ -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,
|
||||
|
@ -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",
|
||||
|
@ -77,6 +77,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() {
|
||||
@ -3176,6 +3177,7 @@ export class PreSetStatusAbAttr extends AbAttr {
|
||||
*/
|
||||
export class PreSetStatusEffectImmunityAbAttr extends PreSetStatusAbAttr {
|
||||
protected immuneEffects: StatusEffect[];
|
||||
private lastEffect: StatusEffect;
|
||||
|
||||
/**
|
||||
* @param immuneEffects - The status effects to which the Pokémon is immune.
|
||||
@ -3201,6 +3203,7 @@ export class PreSetStatusEffectImmunityAbAttr extends PreSetStatusAbAttr {
|
||||
*/
|
||||
override applyPreSetStatus(pokemon: Pokemon, passive: boolean, simulated: boolean, effect: StatusEffect, cancelled: BooleanHolder, args: any[]): void {
|
||||
cancelled.value = true;
|
||||
this.lastEffect = effect;
|
||||
}
|
||||
|
||||
getTriggerMessage(pokemon: Pokemon, abilityName: string, ...args: any[]): string {
|
||||
@ -3208,7 +3211,7 @@ export class PreSetStatusEffectImmunityAbAttr extends PreSetStatusAbAttr {
|
||||
i18next.t("abilityTriggers:statusEffectImmunityWithName", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||
abilityName,
|
||||
statusEffectName: getStatusEffectDescriptor(args[0] as StatusEffect)
|
||||
statusEffectName: getStatusEffectDescriptor(this.lastEffect)
|
||||
}) :
|
||||
i18next.t("abilityTriggers:statusEffectImmunity", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||
@ -5576,6 +5579,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());
|
||||
}
|
||||
}
|
||||
@ -7310,7 +7318,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)
|
||||
@ -7495,4 +7503,4 @@ export function initAbilities() {
|
||||
.unreplaceable() // TODO is this true?
|
||||
.attr(ConfusionOnStatusEffectAbAttr, StatusEffect.POISON, StatusEffect.TOXIC)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,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";
|
||||
|
@ -123,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;
|
||||
@ -2458,14 +2459,7 @@ export class StatusEffectAttr extends MoveEffectAttr {
|
||||
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))
|
||||
@ -6355,6 +6349,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());
|
||||
}
|
||||
}
|
||||
@ -7689,20 +7688,6 @@ 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, {firstHitOnly: true });
|
||||
@ -10546,8 +10531,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)
|
||||
|
@ -416,6 +416,7 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig):
|
||||
console.log(
|
||||
`Pokemon: ${getPokemonNameWithAffix(enemyPokemon)}`,
|
||||
`| Species ID: ${enemyPokemon.species.speciesId}`,
|
||||
`| Level: ${enemyPokemon.level}`,
|
||||
`| Nature: ${getNatureName(enemyPokemon.nature, true, true, true)}`,
|
||||
);
|
||||
console.log(`Stats (IVs): ${stats}`);
|
||||
|
@ -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,19 +224,18 @@ export const trainerPartyTemplates = {
|
||||
*/
|
||||
export function getEvilGruntPartyTemplate(): TrainerPartyTemplate {
|
||||
const waveIndex = globalScene.currentBattle?.waveIndex;
|
||||
if (waveIndex < 40) {
|
||||
return trainerPartyTemplates.TWO_AVG;
|
||||
switch (waveIndex) {
|
||||
case ClassicFixedBossWaves.EVIL_GRUNT_1:
|
||||
return trainerPartyTemplates.TWO_AVG;
|
||||
case ClassicFixedBossWaves.EVIL_GRUNT_2:
|
||||
return trainerPartyTemplates.THREE_AVG;
|
||||
case ClassicFixedBossWaves.EVIL_GRUNT_3:
|
||||
return trainerPartyTemplates.TWO_AVG_ONE_STRONG;
|
||||
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
|
||||
}
|
||||
if (waveIndex < 63) {
|
||||
return trainerPartyTemplates.THREE_AVG;
|
||||
}
|
||||
if (waveIndex < 65) {
|
||||
return trainerPartyTemplates.TWO_AVG_ONE_STRONG;
|
||||
}
|
||||
if (waveIndex < 112) {
|
||||
return trainerPartyTemplates.GYM_LEADER_4; // 3avg 1 strong 1 stronger
|
||||
}
|
||||
return trainerPartyTemplates.GYM_LEADER_5; // 3 avg 2 strong 1 stronger
|
||||
}
|
||||
|
||||
export function getWavePartyTemplate(...templates: TrainerPartyTemplate[]) {
|
||||
@ -245,11 +246,36 @@ export function getWavePartyTemplate(...templates: TrainerPartyTemplate[]) {
|
||||
}
|
||||
|
||||
export function getGymLeaderPartyTemplate() {
|
||||
return getWavePartyTemplate(
|
||||
trainerPartyTemplates.GYM_LEADER_1,
|
||||
trainerPartyTemplates.GYM_LEADER_2,
|
||||
trainerPartyTemplates.GYM_LEADER_3,
|
||||
trainerPartyTemplates.GYM_LEADER_4,
|
||||
trainerPartyTemplates.GYM_LEADER_5,
|
||||
);
|
||||
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,
|
||||
trainerPartyTemplates.GYM_LEADER_3,
|
||||
trainerPartyTemplates.GYM_LEADER_4,
|
||||
trainerPartyTemplates.GYM_LEADER_5,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
22
src/enums/fixed-boss-waves.ts
Normal file
22
src/enums/fixed-boss-waves.ts
Normal 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
|
||||
}
|
@ -249,6 +249,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,
|
||||
@ -4618,7 +4619,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
};
|
||||
}
|
||||
|
||||
/** Calculate whether the given move critically hits this pokemon
|
||||
/** 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`)
|
||||
@ -4647,7 +4648,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
applyAbAttrs(BlockCritAbAttr, this, null, simulated, isCritical);
|
||||
|
||||
return isCritical.value;
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@ -4713,7 +4714,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
@ -5385,6 +5386,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.
|
||||
*
|
||||
@ -5403,7 +5416,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
): boolean {
|
||||
if (effect !== StatusEffect.FAINT) {
|
||||
if (overrideStatus ? this.status?.effect === effect : this.status) {
|
||||
// Only 1 non-volatile status per pokemon (subsequent attempts always fail)
|
||||
this.queueImmuneMessage(quiet, effect);
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
@ -5411,34 +5424,24 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
!ignoreField &&
|
||||
globalScene.arena.terrain?.terrainType === TerrainType.MISTY
|
||||
) {
|
||||
// Misty terrain prevents status application
|
||||
this.queueImmuneMessage(quiet, effect);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
sourcePokemon &&
|
||||
sourcePokemon !== this &&
|
||||
this.isSafeguarded(sourcePokemon)
|
||||
) {
|
||||
// Safeguard blocks all non-self inflicted status effects
|
||||
return false;
|
||||
}
|
||||
|
||||
const types = this.getTypes(true, true);
|
||||
|
||||
switch (effect) {
|
||||
case StatusEffect.POISON:
|
||||
case StatusEffect.TOXIC:
|
||||
// Check whether any of the Pokemon's types is immune to Poison/Toxic
|
||||
// and not being ignored by the source pokemon's ability
|
||||
const typeImmune = types.some(defType => {
|
||||
// Check if the Pokemon is immune to Poison/Toxic or if the source pokemon is canceling the immunity
|
||||
const poisonImmunity = types.map(defType => {
|
||||
// Check if the Pokemon is not immune to Poison/Toxic
|
||||
if (defType !== PokemonType.POISON && defType !== PokemonType.STEEL) {
|
||||
// type not immune to poison
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the source Pokemon has an ability that cancels the immunity
|
||||
// Check if the source Pokemon has an ability that cancels the Poison/Toxic immunity
|
||||
const cancelImmunity = new BooleanHolder(false);
|
||||
if (sourcePokemon) {
|
||||
applyAbAttrs(
|
||||
@ -5449,17 +5452,24 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
effect,
|
||||
defType,
|
||||
);
|
||||
if (cancelImmunity.value) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return !cancelImmunity.value;
|
||||
return true;
|
||||
});
|
||||
|
||||
if (typeImmune) {
|
||||
return false;
|
||||
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;
|
||||
@ -5468,6 +5478,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;
|
||||
@ -5480,17 +5491,18 @@ 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;
|
||||
}
|
||||
|
||||
// Check any status immunity abilities from the user or its allies
|
||||
const cancelled = new BooleanHolder(false);
|
||||
applyPreSetStatusAbAttrs(
|
||||
StatusEffectImmunityAbAttr,
|
||||
@ -5512,10 +5524,27 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
quiet, this, sourcePokemon,
|
||||
)
|
||||
if (cancelled.value) {
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (cancelled.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
sourcePokemon &&
|
||||
sourcePokemon !== this &&
|
||||
this.isSafeguarded(sourcePokemon)
|
||||
) {
|
||||
if(!quiet){
|
||||
globalScene.queueMessage(
|
||||
i18next.t("moveTriggers:safeguard", { targetName: getPokemonNameWithAffix(this)
|
||||
}));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -5527,7 +5556,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) {
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -194,6 +194,7 @@ export class EncounterPhase extends BattlePhase {
|
||||
console.log(
|
||||
`Pokemon: ${getPokemonNameWithAffix(enemyPokemon)}`,
|
||||
`| Species ID: ${enemyPokemon.species.speciesId}`,
|
||||
`| Level: ${enemyPokemon.level}`,
|
||||
`| Nature: ${getNatureName(enemyPokemon.nature, true, true, true)}`,
|
||||
);
|
||||
console.log(`Stats (IVs): ${stats}`);
|
||||
|
@ -859,7 +859,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
});
|
||||
|
||||
if (isCritical) {
|
||||
globalScene.queueMessage(i18next.t("battle:criticalHit"));
|
||||
globalScene.queueMessage(i18next.t("battle:hitResultCriticalHit"));
|
||||
}
|
||||
|
||||
if (damage <= 0) {
|
||||
|
@ -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();
|
||||
};
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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;
|
||||
|
@ -15,14 +15,17 @@ export class PokerogueSystemSavedataApi extends ApiBase {
|
||||
/**
|
||||
* Get a system savedata.
|
||||
* @param params The {@linkcode GetSystemSavedataRequest} to send
|
||||
* @returns The system savedata as `string` or `null` on error
|
||||
* @returns The system savedata as `string` or either the status code or `null` on error
|
||||
*/
|
||||
public async get(params: GetSystemSavedataRequest) {
|
||||
public async get(params: GetSystemSavedataRequest): Promise<string | number | null> {
|
||||
try {
|
||||
const urlSearchParams = this.toUrlSearchParams(params);
|
||||
const response = await this.doGet(`/savedata/system/get?${urlSearchParams}`);
|
||||
const rawSavedata = await response.text();
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn("Could not get system savedata!", response.status, rawSavedata);
|
||||
return response.status;
|
||||
}
|
||||
return rawSavedata;
|
||||
} catch (err) {
|
||||
console.warn("Could not get system savedata!", err);
|
||||
|
@ -462,8 +462,13 @@ export class GameData {
|
||||
|
||||
if (!bypassLogin) {
|
||||
pokerogueApi.savedata.system.get({ clientSessionId }).then(saveDataOrErr => {
|
||||
if (!saveDataOrErr || saveDataOrErr.length === 0 || saveDataOrErr[0] !== "{") {
|
||||
if (saveDataOrErr?.startsWith("sql: no rows in result set")) {
|
||||
if (
|
||||
typeof saveDataOrErr === "number" ||
|
||||
!saveDataOrErr ||
|
||||
saveDataOrErr.length === 0 ||
|
||||
saveDataOrErr[0] !== "{"
|
||||
) {
|
||||
if (saveDataOrErr === 404) {
|
||||
globalScene.queueMessage(
|
||||
"Save data could not be found. If this is a new account, you can safely ignore this message.",
|
||||
null,
|
||||
@ -471,7 +476,7 @@ export class GameData {
|
||||
);
|
||||
return resolve(true);
|
||||
}
|
||||
if (saveDataOrErr?.includes("Too many connections")) {
|
||||
if (typeof saveDataOrErr === "string" && saveDataOrErr?.includes("Too many connections")) {
|
||||
globalScene.queueMessage(
|
||||
"Too many people are trying to connect and the server is overloaded. Please try again later.",
|
||||
null,
|
||||
@ -479,7 +484,6 @@ export class GameData {
|
||||
);
|
||||
return resolve(false);
|
||||
}
|
||||
console.error(saveDataOrErr);
|
||||
return resolve(false);
|
||||
}
|
||||
|
||||
@ -1505,7 +1509,7 @@ export class GameData {
|
||||
link.remove();
|
||||
};
|
||||
if (!bypassLogin && dataType < GameDataType.SETTINGS) {
|
||||
let promise: Promise<string | null> = Promise.resolve(null);
|
||||
let promise: Promise<string | null | number> = Promise.resolve(null);
|
||||
|
||||
if (dataType === GameDataType.SYSTEM) {
|
||||
promise = pokerogueApi.savedata.system.get({ clientSessionId });
|
||||
@ -1517,7 +1521,7 @@ export class GameData {
|
||||
}
|
||||
|
||||
promise.then(response => {
|
||||
if (!response?.length || response[0] !== "{") {
|
||||
if (typeof response === "number" || !response?.length || response[0] !== "{") {
|
||||
console.error(response);
|
||||
resolve(false);
|
||||
return;
|
||||
|
@ -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`);
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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 =>
|
||||
|
@ -596,6 +596,13 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
|
||||
this.iconAnimHandler = new PokemonIconAnimHandler();
|
||||
this.iconAnimHandler.setup();
|
||||
|
||||
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.pokemonNumberText = addTextObject(17, 1, "0000", TextStyle.SUMMARY);
|
||||
this.pokemonNumberText.setOrigin(0, 0);
|
||||
this.starterSelectContainer.add(this.pokemonNumberText);
|
||||
@ -825,13 +832,6 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
|
||||
return icon;
|
||||
});
|
||||
|
||||
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);
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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,
|
||||
|
@ -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) {
|
||||
|
@ -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) {}
|
||||
}
|
||||
|
@ -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);
|
||||
|
1
test/testUtils/saves/data_pokedex_tests.prsv
Normal file
1
test/testUtils/saves/data_pokedex_tests.prsv
Normal file
File diff suppressed because one or more lines are too long
@ -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
492
test/ui/pokedex.test.ts
Normal 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);
|
||||
},
|
||||
);
|
||||
});
|
Loading…
Reference in New Issue
Block a user