mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-08-19 13:59:27 +02:00
Merge branch 'beta' into deprecated-func
This commit is contained in:
commit
b2d5a6ccff
@ -81,7 +81,7 @@ For example, here is how you could test a scenario where the player Pokemon has
|
||||
```typescript
|
||||
const overrides = {
|
||||
ABILITY_OVERRIDE: AbilityId.DROUGHT,
|
||||
OPP_MOVESET_OVERRIDE: MoveId.WATER_GUN,
|
||||
ENEMY_MOVESET_OVERRIDE: MoveId.WATER_GUN,
|
||||
} satisfies Partial<InstanceType<typeof DefaultOverrides>>;
|
||||
```
|
||||
|
||||
|
@ -90,9 +90,13 @@ If this feature requires new text, the text should be integrated into the code w
|
||||
- For any feature pulled from the mainline Pokémon games (e.g. a Move or Ability implementation), it's best practice to include a source link for any added text.
|
||||
[Poké Corpus](https://abcboy101.github.io/poke-corpus/) is a great resource for finding text from the mainline games; otherwise, a video/picture showing the text being displayed should suffice.
|
||||
- You should also [notify the current Head of Translation](#notifying-translation) to ensure a fast response.
|
||||
3. At this point, you may begin [testing locales integration in your main PR](#documenting-locales-changes).
|
||||
4. The Translation Team will approve the locale PR (after corrections, if necessary), then merge it into `pokerogue-locales`.
|
||||
5. The Dev Team will approve your main PR for your feature, then merge it into PokéRogue's beta environment.
|
||||
3. Your locales should use the following format:
|
||||
- File names should be in `kebab-case`. Example: `trainer-names.json`
|
||||
- Key names should be in `camelCase`. Example: `aceTrainer`
|
||||
- If you make use of i18next's inbuilt [context support](https://www.i18next.com/translation-function/context), you need to use `snake_case` for the context key. Example: `aceTrainer_male`
|
||||
4. At this point, you may begin [testing locales integration in your main PR](#documenting-locales-changes).
|
||||
5. The Translation Team will approve the locale PR (after corrections, if necessary), then merge it into `pokerogue-locales`.
|
||||
6. The Dev Team will approve your main PR for your feature, then merge it into PokéRogue's beta environment.
|
||||
|
||||
[^2]: For those wondering, the reason for choosing English specifically is due to it being the master language set in Pontoon (the program used by the Translation Team to perform locale updates).
|
||||
If a key is present in any language _except_ the master language, it won't appear anywhere else in the translation tool, rendering missing English keys quite a hassle.
|
||||
|
@ -15,8 +15,6 @@
|
||||
"test:silent": "vitest run --silent='passed-only' --no-isolate",
|
||||
"test:create": "node scripts/create-test/create-test.js",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"eslint": "eslint --fix .",
|
||||
"eslint-ci": "eslint .",
|
||||
"biome": "biome check --write --changed --no-errors-on-unmatched --diagnostic-level=error",
|
||||
"biome-ci": "biome ci --diagnostic-level=error --reporter=github --no-errors-on-unmatched",
|
||||
"docs": "typedoc",
|
||||
@ -29,6 +27,7 @@
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.0.0",
|
||||
"@ls-lint/ls-lint": "2.3.1",
|
||||
"@types/crypto-js": "^4.2.0",
|
||||
"@types/jsdom": "^21.1.7",
|
||||
"@types/node": "^22.16.5",
|
||||
"@vitest/coverage-istanbul": "^3.2.4",
|
||||
|
@ -48,6 +48,9 @@ importers:
|
||||
'@ls-lint/ls-lint':
|
||||
specifier: 2.3.1
|
||||
version: 2.3.1
|
||||
'@types/crypto-js':
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.2
|
||||
'@types/jsdom':
|
||||
specifier: ^21.1.7
|
||||
version: 21.1.7
|
||||
@ -718,6 +721,9 @@ packages:
|
||||
'@types/cookie@0.6.0':
|
||||
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
|
||||
|
||||
'@types/crypto-js@4.2.2':
|
||||
resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==}
|
||||
|
||||
'@types/deep-eql@4.0.2':
|
||||
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
|
||||
|
||||
@ -2525,6 +2531,8 @@ snapshots:
|
||||
|
||||
'@types/cookie@0.6.0': {}
|
||||
|
||||
'@types/crypto-js@4.2.2': {}
|
||||
|
||||
'@types/deep-eql@4.0.2': {}
|
||||
|
||||
'@types/estree@1.0.8': {}
|
||||
|
Binary file not shown.
Binary file not shown.
BIN
public/images/ui/champion_ribbon_emerald.png
Normal file
BIN
public/images/ui/champion_ribbon_emerald.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 225 B |
BIN
public/images/ui/legacy/champion_ribbon_emerald.png
Normal file
BIN
public/images/ui/legacy/champion_ribbon_emerald.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 225 B |
Binary file not shown.
Before Width: | Height: | Size: 837 B After Width: | Height: | Size: 799 B |
146
public/images/ui/party_slot_main_short.json
Normal file
146
public/images/ui/party_slot_main_short.json
Normal file
@ -0,0 +1,146 @@
|
||||
{
|
||||
"textures": [
|
||||
{
|
||||
"image": "party_slot_main_short.png",
|
||||
"format": "RGBA8888",
|
||||
"size": {
|
||||
"w": 110,
|
||||
"h": 294
|
||||
},
|
||||
"scale": 1,
|
||||
"frames": [
|
||||
{
|
||||
"filename": "party_slot_main_short",
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"sourceSize": {
|
||||
"w": 110,
|
||||
"h": 41
|
||||
},
|
||||
"spriteSourceSize": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 110,
|
||||
"h": 41
|
||||
},
|
||||
"frame": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 110,
|
||||
"h": 41
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "party_slot_main_short_sel",
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"sourceSize": {
|
||||
"w": 110,
|
||||
"h": 41
|
||||
},
|
||||
"spriteSourceSize": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 110,
|
||||
"h": 41
|
||||
},
|
||||
"frame": {
|
||||
"x": 0,
|
||||
"y": 41,
|
||||
"w": 110,
|
||||
"h": 41
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "party_slot_main_short_fnt",
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"sourceSize": {
|
||||
"w": 110,
|
||||
"h": 41
|
||||
},
|
||||
"spriteSourceSize": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 110,
|
||||
"h": 41
|
||||
},
|
||||
"frame": {
|
||||
"x": 0,
|
||||
"y": 82,
|
||||
"w": 110,
|
||||
"h": 41
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "party_slot_main_short_fnt_sel",
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"sourceSize": {
|
||||
"w": 110,
|
||||
"h": 41
|
||||
},
|
||||
"spriteSourceSize": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 110,
|
||||
"h": 41
|
||||
},
|
||||
"frame": {
|
||||
"x": 0,
|
||||
"y": 123,
|
||||
"w": 110,
|
||||
"h": 41
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "party_slot_main_short_swap",
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"sourceSize": {
|
||||
"w": 110,
|
||||
"h": 41
|
||||
},
|
||||
"spriteSourceSize": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 110,
|
||||
"h": 41
|
||||
},
|
||||
"frame": {
|
||||
"x": 0,
|
||||
"y": 164,
|
||||
"w": 110,
|
||||
"h": 41
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "party_slot_main_short_swap_sel",
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"sourceSize": {
|
||||
"w": 110,
|
||||
"h": 41
|
||||
},
|
||||
"spriteSourceSize": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 110,
|
||||
"h": 41
|
||||
},
|
||||
"frame": {
|
||||
"x": 0,
|
||||
"y": 205,
|
||||
"w": 110,
|
||||
"h": 41
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"app": "https://www.codeandweb.com/texturepacker",
|
||||
"version": "3.0",
|
||||
"smartupdate": "$TexturePacker:SmartUpdate:29685f2f538901cf5bf7f0ed2ea867c3:a080ea6c8cccd1e03244214053e79796:565f7afc5ca419b6ba8dbce51ea30818$"
|
||||
}
|
||||
}
|
BIN
public/images/ui/party_slot_main_short.png
Normal file
BIN
public/images/ui/party_slot_main_short.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.0 KiB |
@ -1 +1 @@
|
||||
Subproject commit ab2716d5440c25f73986664aa3f3131821c3c392
|
||||
Subproject commit 1ea8f865e30d1940caa0fceeabf37ae2e4689471
|
@ -1,3 +1,5 @@
|
||||
import type { RibbonData } from "#system/ribbons/ribbon-data";
|
||||
|
||||
export interface DexData {
|
||||
[key: number]: DexEntry;
|
||||
}
|
||||
@ -10,4 +12,5 @@ export interface DexEntry {
|
||||
caughtCount: number;
|
||||
hatchedCount: number;
|
||||
ivs: number[];
|
||||
ribbons: RibbonData;
|
||||
}
|
||||
|
@ -103,3 +103,12 @@ export type CoerceNullPropertiesToUndefined<T extends object> = {
|
||||
* @typeParam T - The type to render partial
|
||||
*/
|
||||
export type AtLeastOne<T extends object> = Partial<T> & ObjectValues<{ [K in keyof T]: Pick<Required<T>, K> }>;
|
||||
|
||||
/** Type helper that adds a brand to a type, used for nominal typing.
|
||||
*
|
||||
* @remarks
|
||||
* Brands should be either a string or unique symbol. This prevents overlap with other types.
|
||||
*/
|
||||
export declare class Brander<B> {
|
||||
private __brand: B;
|
||||
}
|
||||
|
@ -1,13 +1,24 @@
|
||||
import type { Pokemon } from "#field/pokemon";
|
||||
import type {
|
||||
AttackMove,
|
||||
ChargingAttackMove,
|
||||
ChargingSelfStatusMove,
|
||||
Move,
|
||||
MoveAttr,
|
||||
MoveAttrConstructorMap,
|
||||
SelfStatusMove,
|
||||
StatusMove,
|
||||
} from "#moves/move";
|
||||
|
||||
/**
|
||||
* A generic function producing a message during a Move's execution.
|
||||
* @param user - The {@linkcode Pokemon} using the move
|
||||
* @param target - The {@linkcode Pokemon} targeted by the move
|
||||
* @param move - The {@linkcode Move} being used
|
||||
* @returns a string
|
||||
*/
|
||||
export type MoveMessageFunc = (user: Pokemon, target: Pokemon, move: Move) => string;
|
||||
|
||||
export type MoveAttrFilter = (attr: MoveAttr) => boolean;
|
||||
|
||||
export type * from "#moves/move";
|
||||
|
@ -17,45 +17,42 @@ export function initLoggedInUser(): void {
|
||||
};
|
||||
}
|
||||
|
||||
export function updateUserInfo(): Promise<[boolean, number]> {
|
||||
return new Promise<[boolean, number]>(resolve => {
|
||||
if (bypassLogin) {
|
||||
loggedInUser = {
|
||||
username: "Guest",
|
||||
lastSessionSlot: -1,
|
||||
discordId: "",
|
||||
googleId: "",
|
||||
hasAdminRole: false,
|
||||
};
|
||||
let lastSessionSlot = -1;
|
||||
for (let s = 0; s < 5; s++) {
|
||||
if (localStorage.getItem(`sessionData${s ? s : ""}_${loggedInUser.username}`)) {
|
||||
lastSessionSlot = s;
|
||||
break;
|
||||
}
|
||||
export async function updateUserInfo(): Promise<[boolean, number]> {
|
||||
if (bypassLogin) {
|
||||
loggedInUser = {
|
||||
username: "Guest",
|
||||
lastSessionSlot: -1,
|
||||
discordId: "",
|
||||
googleId: "",
|
||||
hasAdminRole: false,
|
||||
};
|
||||
let lastSessionSlot = -1;
|
||||
for (let s = 0; s < 5; s++) {
|
||||
if (localStorage.getItem(`sessionData${s ? s : ""}_${loggedInUser.username}`)) {
|
||||
lastSessionSlot = s;
|
||||
break;
|
||||
}
|
||||
loggedInUser.lastSessionSlot = lastSessionSlot;
|
||||
// Migrate old data from before the username was appended
|
||||
["data", "sessionData", "sessionData1", "sessionData2", "sessionData3", "sessionData4"].map(d => {
|
||||
const lsItem = localStorage.getItem(d);
|
||||
if (lsItem && !!loggedInUser?.username) {
|
||||
const lsUserItem = localStorage.getItem(`${d}_${loggedInUser.username}`);
|
||||
if (lsUserItem) {
|
||||
localStorage.setItem(`${d}_${loggedInUser.username}_bak`, lsUserItem);
|
||||
}
|
||||
localStorage.setItem(`${d}_${loggedInUser.username}`, lsItem);
|
||||
localStorage.removeItem(d);
|
||||
}
|
||||
});
|
||||
return resolve([true, 200]);
|
||||
}
|
||||
pokerogueApi.account.getInfo().then(([accountInfo, status]) => {
|
||||
if (!accountInfo) {
|
||||
resolve([false, status]);
|
||||
return;
|
||||
loggedInUser.lastSessionSlot = lastSessionSlot;
|
||||
// Migrate old data from before the username was appended
|
||||
["data", "sessionData", "sessionData1", "sessionData2", "sessionData3", "sessionData4"].forEach(d => {
|
||||
const lsItem = localStorage.getItem(d);
|
||||
if (lsItem && !!loggedInUser?.username) {
|
||||
const lsUserItem = localStorage.getItem(`${d}_${loggedInUser.username}`);
|
||||
if (lsUserItem) {
|
||||
localStorage.setItem(`${d}_${loggedInUser.username}_bak`, lsUserItem);
|
||||
}
|
||||
localStorage.setItem(`${d}_${loggedInUser.username}`, lsItem);
|
||||
localStorage.removeItem(d);
|
||||
}
|
||||
loggedInUser = accountInfo;
|
||||
resolve([true, 200]);
|
||||
});
|
||||
});
|
||||
return [true, 200];
|
||||
}
|
||||
|
||||
const [accountInfo, status] = await pokerogueApi.account.getInfo();
|
||||
if (!accountInfo) {
|
||||
return [false, status];
|
||||
}
|
||||
loggedInUser = accountInfo;
|
||||
return [true, 200];
|
||||
}
|
||||
|
@ -27,13 +27,7 @@ import { UiInputs } from "#app/ui-inputs";
|
||||
import { biomeDepths, getBiomeName } from "#balance/biomes";
|
||||
import { pokemonPrevolutions } from "#balance/pokemon-evolutions";
|
||||
import { FRIENDSHIP_GAIN_FROM_BATTLE } from "#balance/starters";
|
||||
import {
|
||||
initCommonAnims,
|
||||
initMoveAnim,
|
||||
loadCommonAnimAssets,
|
||||
loadMoveAnimAssets,
|
||||
populateAnims,
|
||||
} from "#data/battle-anims";
|
||||
import { initCommonAnims, initMoveAnim, loadCommonAnimAssets, loadMoveAnimAssets } from "#data/battle-anims";
|
||||
import { allAbilities, allMoves, allSpecies, modifierTypes } from "#data/data-lists";
|
||||
import { battleSpecDialogue } from "#data/dialogue";
|
||||
import type { SpeciesFormChangeTrigger } from "#data/form-change-triggers";
|
||||
@ -104,6 +98,7 @@ import {
|
||||
getLuckString,
|
||||
getLuckTextTint,
|
||||
getPartyLuckValue,
|
||||
type ModifierType,
|
||||
PokemonHeldItemModifierType,
|
||||
} from "#modifiers/modifier-type";
|
||||
import { MysteryEncounter } from "#mystery-encounters/mystery-encounter";
|
||||
@ -387,7 +382,6 @@ export class BattleScene extends SceneBase {
|
||||
const defaultMoves = [MoveId.TACKLE, MoveId.TAIL_WHIP, MoveId.FOCUS_ENERGY, MoveId.STRUGGLE];
|
||||
|
||||
await Promise.all([
|
||||
populateAnims(),
|
||||
this.initVariantData(),
|
||||
initCommonAnims().then(() => loadCommonAnimAssets(true)),
|
||||
Promise.all(defaultMoves.map(m => initMoveAnim(m))).then(() => loadMoveAnimAssets(defaultMoves, true)),
|
||||
@ -943,17 +937,17 @@ export class BattleScene extends SceneBase {
|
||||
dataSource?: PokemonData,
|
||||
postProcess?: (enemyPokemon: EnemyPokemon) => void,
|
||||
): EnemyPokemon {
|
||||
if (Overrides.OPP_LEVEL_OVERRIDE > 0) {
|
||||
level = Overrides.OPP_LEVEL_OVERRIDE;
|
||||
if (Overrides.ENEMY_LEVEL_OVERRIDE > 0) {
|
||||
level = Overrides.ENEMY_LEVEL_OVERRIDE;
|
||||
}
|
||||
if (Overrides.OPP_SPECIES_OVERRIDE) {
|
||||
species = getPokemonSpecies(Overrides.OPP_SPECIES_OVERRIDE);
|
||||
if (Overrides.ENEMY_SPECIES_OVERRIDE) {
|
||||
species = getPokemonSpecies(Overrides.ENEMY_SPECIES_OVERRIDE);
|
||||
// The fact that a Pokemon is a boss or not can change based on its Species and level
|
||||
boss = this.getEncounterBossSegments(this.currentBattle.waveIndex, level, species) > 1;
|
||||
}
|
||||
|
||||
const pokemon = new EnemyPokemon(species, level, trainerSlot, boss, shinyLock, dataSource);
|
||||
if (Overrides.OPP_FUSION_OVERRIDE) {
|
||||
if (Overrides.ENEMY_FUSION_OVERRIDE) {
|
||||
pokemon.generateFusionSpecies();
|
||||
}
|
||||
|
||||
@ -1203,7 +1197,9 @@ export class BattleScene extends SceneBase {
|
||||
this.updateScoreText();
|
||||
this.scoreText.setVisible(false);
|
||||
|
||||
[this.luckLabelText, this.luckText].map(t => t.setVisible(false));
|
||||
[this.luckLabelText, this.luckText].forEach(t => {
|
||||
t.setVisible(false);
|
||||
});
|
||||
|
||||
this.newArena(Overrides.STARTING_BIOME_OVERRIDE || BiomeId.TOWN);
|
||||
|
||||
@ -1237,8 +1233,7 @@ export class BattleScene extends SceneBase {
|
||||
Object.values(mp)
|
||||
.flat()
|
||||
.map(mt => mt.modifierType)
|
||||
.filter(mt => "localize" in mt)
|
||||
.map(lpb => lpb as unknown as Localizable),
|
||||
.filter((mt): mt is ModifierType & Localizable => "localize" in mt && typeof mt.localize === "function"),
|
||||
),
|
||||
];
|
||||
for (const item of localizable) {
|
||||
@ -1513,8 +1508,8 @@ export class BattleScene extends SceneBase {
|
||||
return this.currentBattle;
|
||||
}
|
||||
|
||||
newArena(biome: BiomeId, playerFaints?: number): Arena {
|
||||
this.arena = new Arena(biome, BiomeId[biome].toLowerCase(), playerFaints);
|
||||
newArena(biome: BiomeId, playerFaints = 0): Arena {
|
||||
this.arena = new Arena(biome, playerFaints);
|
||||
this.eventTarget.dispatchEvent(new NewArenaEvent());
|
||||
|
||||
this.arenaBg.pipelineData = {
|
||||
@ -1764,10 +1759,10 @@ export class BattleScene extends SceneBase {
|
||||
}
|
||||
|
||||
getEncounterBossSegments(waveIndex: number, level: number, species?: PokemonSpecies, forceBoss = false): number {
|
||||
if (Overrides.OPP_HEALTH_SEGMENTS_OVERRIDE > 1) {
|
||||
return Overrides.OPP_HEALTH_SEGMENTS_OVERRIDE;
|
||||
if (Overrides.ENEMY_HEALTH_SEGMENTS_OVERRIDE > 1) {
|
||||
return Overrides.ENEMY_HEALTH_SEGMENTS_OVERRIDE;
|
||||
}
|
||||
if (Overrides.OPP_HEALTH_SEGMENTS_OVERRIDE === 1) {
|
||||
if (Overrides.ENEMY_HEALTH_SEGMENTS_OVERRIDE === 1) {
|
||||
// The rest of the code expects to be returned 0 and not 1 if the enemy is not a boss
|
||||
return 0;
|
||||
}
|
||||
@ -2711,7 +2706,9 @@ export class BattleScene extends SceneBase {
|
||||
}
|
||||
}
|
||||
|
||||
this.party.map(p => p.updateInfo(instant));
|
||||
this.party.forEach(p => {
|
||||
p.updateInfo(instant);
|
||||
});
|
||||
} else {
|
||||
const args = [this];
|
||||
if (modifier.shouldApply(...args)) {
|
||||
|
@ -101,3 +101,9 @@ export const ANTI_VARIANCE_WEIGHT_MODIFIER = 15;
|
||||
* Default: `10000` (0.01%)
|
||||
*/
|
||||
export const FAKE_TITLE_LOGO_CHANCE = 10000;
|
||||
|
||||
/**
|
||||
* The ceiling on friendship amount that can be reached through the use of rare candies.
|
||||
* Using rare candies will never increase friendship beyond this value.
|
||||
*/
|
||||
export const RARE_CANDY_FRIENDSHIP_CAP = 200;
|
||||
|
@ -74,6 +74,7 @@ import {
|
||||
randSeedItem,
|
||||
toDmgValue,
|
||||
} from "#utils/common";
|
||||
import { toCamelCase } from "#utils/strings";
|
||||
import i18next from "i18next";
|
||||
|
||||
export class Ability implements Localizable {
|
||||
@ -109,13 +110,9 @@ export class Ability implements Localizable {
|
||||
}
|
||||
|
||||
localize(): void {
|
||||
const i18nKey = AbilityId[this.id]
|
||||
.split("_")
|
||||
.filter(f => f)
|
||||
.map((f, i) => (i ? `${f[0]}${f.slice(1).toLowerCase()}` : f.toLowerCase()))
|
||||
.join("") as string;
|
||||
const i18nKey = toCamelCase(AbilityId[this.id]);
|
||||
|
||||
this.name = this.id ? `${i18next.t(`ability:${i18nKey}.name`) as string}${this.nameAppend}` : "";
|
||||
this.name = this.id ? `${i18next.t(`ability:${i18nKey}.name`)}${this.nameAppend}` : "";
|
||||
this.description = this.id ? (i18next.t(`ability:${i18nKey}.description`) as string) : "";
|
||||
}
|
||||
|
||||
@ -1670,6 +1667,7 @@ export class MoveTypeChangeAbAttr extends PreAttackAbAttr {
|
||||
constructor(
|
||||
private newType: PokemonType,
|
||||
private powerMultiplier: number,
|
||||
// TODO: all moves with this attr solely check the move being used...
|
||||
private condition?: PokemonAttackCondition,
|
||||
) {
|
||||
super(false);
|
||||
|
@ -1866,17 +1866,16 @@ interface PokemonPrevolutions {
|
||||
export const pokemonPrevolutions: PokemonPrevolutions = {};
|
||||
|
||||
export function initPokemonPrevolutions(): void {
|
||||
const megaFormKeys = [ SpeciesFormKey.MEGA, "", SpeciesFormKey.MEGA_X, "", SpeciesFormKey.MEGA_Y ].map(sfk => sfk as string);
|
||||
const prevolutionKeys = Object.keys(pokemonEvolutions);
|
||||
prevolutionKeys.forEach(pk => {
|
||||
const evolutions = pokemonEvolutions[pk];
|
||||
// TODO: Why do we have empty strings in our array?
|
||||
const megaFormKeys = [ SpeciesFormKey.MEGA, "", SpeciesFormKey.MEGA_X, "", SpeciesFormKey.MEGA_Y ];
|
||||
for (const [pk, evolutions] of Object.entries(pokemonEvolutions)) {
|
||||
for (const ev of evolutions) {
|
||||
if (ev.evoFormKey && megaFormKeys.indexOf(ev.evoFormKey) > -1) {
|
||||
continue;
|
||||
}
|
||||
pokemonPrevolutions[ev.speciesId] = Number.parseInt(pk) as SpeciesId;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -1505,9 +1505,9 @@ export function initSpecies() {
|
||||
new PokemonForm("Hero of Many Battles", "hero-of-many-battles", PokemonType.FIGHTING, null, 2.9, 210, AbilityId.DAUNTLESS_SHIELD, AbilityId.NONE, AbilityId.NONE, 660, 92, 120, 115, 80, 115, 138, 10, 0, 335, false, "", true),
|
||||
new PokemonForm("Crowned", "crowned", PokemonType.FIGHTING, PokemonType.STEEL, 2.9, 785, AbilityId.DAUNTLESS_SHIELD, AbilityId.NONE, AbilityId.NONE, 700, 92, 120, 140, 80, 140, 128, 10, 0, 360)
|
||||
),
|
||||
new PokemonSpecies(SpeciesId.ETERNATUS, 8, false, true, false, "Gigantic Pokémon", PokemonType.POISON, PokemonType.DRAGON, 20, 950, AbilityId.PRESSURE, AbilityId.NONE, AbilityId.NONE, 690, 140, 85, 95, 145, 95, 130, 255, 0, 345, GrowthRate.SLOW, null, false, true,
|
||||
new PokemonForm("Normal", "", PokemonType.POISON, PokemonType.DRAGON, 20, 950, AbilityId.PRESSURE, AbilityId.NONE, AbilityId.NONE, 690, 140, 85, 95, 145, 95, 130, 255, 0, 345, false, null, true),
|
||||
new PokemonForm("E-Max", "eternamax", PokemonType.POISON, PokemonType.DRAGON, 100, 999.9, AbilityId.PRESSURE, AbilityId.NONE, AbilityId.NONE, 1125, 255, 115, 250, 125, 250, 130, 255, 0, 345)
|
||||
new PokemonSpecies(SpeciesId.ETERNATUS, 8, false, true, false, "Gigantic Pokémon", PokemonType.POISON, PokemonType.DRAGON, 20, 950, AbilityId.PRESSURE, AbilityId.NONE, AbilityId.NONE, 690, 140, 85, 95, 145, 95, 130, 45, 0, 345, GrowthRate.SLOW, null, false, true,
|
||||
new PokemonForm("Normal", "", PokemonType.POISON, PokemonType.DRAGON, 20, 950, AbilityId.PRESSURE, AbilityId.NONE, AbilityId.NONE, 690, 140, 85, 95, 145, 95, 130, 45, 0, 345, false, null, true),
|
||||
new PokemonForm("E-Max", "eternamax", PokemonType.POISON, PokemonType.DRAGON, 100, 999.9, AbilityId.PRESSURE, AbilityId.NONE, AbilityId.NONE, 1125, 255, 115, 250, 125, 250, 130, 45, 0, 345)
|
||||
),
|
||||
new PokemonSpecies(SpeciesId.KUBFU, 8, true, false, false, "Wushu Pokémon", PokemonType.FIGHTING, null, 0.6, 12, AbilityId.INNER_FOCUS, AbilityId.NONE, AbilityId.NONE, 385, 60, 90, 60, 53, 50, 72, 3, 50, 77, GrowthRate.SLOW, 87.5, false),
|
||||
new PokemonSpecies(SpeciesId.URSHIFU, 8, true, false, false, "Wushu Pokémon", PokemonType.FIGHTING, PokemonType.DARK, 1.9, 105, AbilityId.UNSEEN_FIST, AbilityId.NONE, AbilityId.NONE, 550, 100, 130, 100, 63, 60, 97, 3, 50, 275, GrowthRate.SLOW, 87.5, false, true,
|
||||
|
@ -45736,6 +45736,285 @@ export const tmSpecies: TmSpecies = {
|
||||
SpeciesId.HISUI_ARCANINE,
|
||||
SpeciesId.HISUI_AVALUGG,
|
||||
],
|
||||
[MoveId.SHOCK_WAVE]: [
|
||||
SpeciesId.RATTATA,
|
||||
SpeciesId.RATICATE,
|
||||
SpeciesId.PIKACHU,
|
||||
SpeciesId.RAICHU,
|
||||
SpeciesId.NIDORAN_F,
|
||||
SpeciesId.NIDORINA,
|
||||
SpeciesId.NIDOQUEEN,
|
||||
SpeciesId.NIDORAN_M,
|
||||
SpeciesId.NIDORINO,
|
||||
SpeciesId.NIDOKING,
|
||||
SpeciesId.CLEFAIRY,
|
||||
SpeciesId.CLEFABLE,
|
||||
SpeciesId.JIGGLYPUFF,
|
||||
SpeciesId.WIGGLYTUFF,
|
||||
SpeciesId.MEOWTH,
|
||||
SpeciesId.PERSIAN,
|
||||
SpeciesId.ABRA,
|
||||
SpeciesId.KADABRA,
|
||||
SpeciesId.ALAKAZAM,
|
||||
SpeciesId.MAGNEMITE,
|
||||
SpeciesId.MAGNETON,
|
||||
SpeciesId.GRIMER,
|
||||
SpeciesId.MUK,
|
||||
SpeciesId.VOLTORB,
|
||||
SpeciesId.ELECTRODE,
|
||||
SpeciesId.LICKITUNG,
|
||||
SpeciesId.KOFFING,
|
||||
SpeciesId.WEEZING,
|
||||
SpeciesId.RHYHORN,
|
||||
SpeciesId.RHYDON,
|
||||
SpeciesId.CHANSEY,
|
||||
SpeciesId.TANGELA,
|
||||
SpeciesId.KANGASKHAN,
|
||||
SpeciesId.MR_MIME,
|
||||
SpeciesId.ELECTABUZZ,
|
||||
SpeciesId.TAUROS,
|
||||
SpeciesId.LAPRAS,
|
||||
SpeciesId.JOLTEON,
|
||||
SpeciesId.PORYGON,
|
||||
SpeciesId.SNORLAX,
|
||||
SpeciesId.ZAPDOS,
|
||||
SpeciesId.DRATINI,
|
||||
SpeciesId.DRAGONAIR,
|
||||
SpeciesId.DRAGONITE,
|
||||
SpeciesId.MEWTWO,
|
||||
SpeciesId.MEW,
|
||||
SpeciesId.SENTRET,
|
||||
SpeciesId.FURRET,
|
||||
SpeciesId.CHINCHOU,
|
||||
SpeciesId.LANTURN,
|
||||
SpeciesId.PICHU,
|
||||
SpeciesId.CLEFFA,
|
||||
SpeciesId.IGGLYBUFF,
|
||||
SpeciesId.TOGEPI,
|
||||
SpeciesId.TOGETIC,
|
||||
SpeciesId.MAREEP,
|
||||
SpeciesId.FLAAFFY,
|
||||
SpeciesId.AMPHAROS,
|
||||
SpeciesId.AIPOM,
|
||||
SpeciesId.MISDREAVUS,
|
||||
SpeciesId.GIRAFARIG,
|
||||
SpeciesId.DUNSPARCE,
|
||||
SpeciesId.SNUBBULL,
|
||||
SpeciesId.GRANBULL,
|
||||
SpeciesId.QWILFISH,
|
||||
SpeciesId.PORYGON2,
|
||||
SpeciesId.STANTLER,
|
||||
SpeciesId.ELEKID,
|
||||
SpeciesId.MILTANK,
|
||||
SpeciesId.BLISSEY,
|
||||
SpeciesId.RAIKOU,
|
||||
SpeciesId.TYRANITAR,
|
||||
SpeciesId.LUGIA,
|
||||
SpeciesId.HO_OH,
|
||||
SpeciesId.CELEBI,
|
||||
SpeciesId.ZIGZAGOON,
|
||||
SpeciesId.LINOONE,
|
||||
SpeciesId.WINGULL,
|
||||
SpeciesId.PELIPPER,
|
||||
SpeciesId.RALTS,
|
||||
SpeciesId.KIRLIA,
|
||||
SpeciesId.GARDEVOIR,
|
||||
SpeciesId.SLAKOTH,
|
||||
SpeciesId.VIGOROTH,
|
||||
SpeciesId.SLAKING,
|
||||
SpeciesId.WHISMUR,
|
||||
SpeciesId.LOUDRED,
|
||||
SpeciesId.EXPLOUD,
|
||||
SpeciesId.NOSEPASS,
|
||||
SpeciesId.SKITTY,
|
||||
SpeciesId.DELCATTY,
|
||||
SpeciesId.SABLEYE,
|
||||
SpeciesId.ARON,
|
||||
SpeciesId.LAIRON,
|
||||
SpeciesId.AGGRON,
|
||||
SpeciesId.ELECTRIKE,
|
||||
SpeciesId.MANECTRIC,
|
||||
SpeciesId.PLUSLE,
|
||||
SpeciesId.MINUN,
|
||||
SpeciesId.VOLBEAT,
|
||||
SpeciesId.ILLUMISE,
|
||||
SpeciesId.GULPIN,
|
||||
SpeciesId.SWALOT,
|
||||
SpeciesId.SPOINK,
|
||||
SpeciesId.GRUMPIG,
|
||||
SpeciesId.SPINDA,
|
||||
SpeciesId.ZANGOOSE,
|
||||
SpeciesId.CASTFORM,
|
||||
SpeciesId.KECLEON,
|
||||
SpeciesId.SHUPPET,
|
||||
SpeciesId.BANETTE,
|
||||
SpeciesId.CHIMECHO,
|
||||
SpeciesId.ABSOL,
|
||||
SpeciesId.REGIROCK,
|
||||
SpeciesId.REGICE,
|
||||
SpeciesId.REGISTEEL,
|
||||
SpeciesId.LATIAS,
|
||||
SpeciesId.LATIOS,
|
||||
SpeciesId.KYOGRE,
|
||||
SpeciesId.GROUDON,
|
||||
SpeciesId.RAYQUAZA,
|
||||
SpeciesId.JIRACHI,
|
||||
SpeciesId.DEOXYS,
|
||||
SpeciesId.BIDOOF,
|
||||
SpeciesId.BIBAREL,
|
||||
SpeciesId.SHINX,
|
||||
SpeciesId.LUXIO,
|
||||
SpeciesId.LUXRAY,
|
||||
SpeciesId.CRANIDOS,
|
||||
SpeciesId.RAMPARDOS,
|
||||
SpeciesId.SHIELDON,
|
||||
SpeciesId.BASTIODON,
|
||||
SpeciesId.PACHIRISU,
|
||||
SpeciesId.AMBIPOM,
|
||||
SpeciesId.DRIFLOON,
|
||||
SpeciesId.DRIFBLIM,
|
||||
SpeciesId.BUNEARY,
|
||||
SpeciesId.LOPUNNY,
|
||||
SpeciesId.MISMAGIUS,
|
||||
SpeciesId.GLAMEOW,
|
||||
SpeciesId.PURUGLY,
|
||||
SpeciesId.CHINGLING,
|
||||
SpeciesId.MIME_JR,
|
||||
SpeciesId.HAPPINY,
|
||||
SpeciesId.SPIRITOMB,
|
||||
SpeciesId.MUNCHLAX,
|
||||
SpeciesId.MAGNEZONE,
|
||||
SpeciesId.LICKILICKY,
|
||||
SpeciesId.RHYPERIOR,
|
||||
SpeciesId.TANGROWTH,
|
||||
SpeciesId.ELECTIVIRE,
|
||||
SpeciesId.TOGEKISS,
|
||||
SpeciesId.PORYGON_Z,
|
||||
SpeciesId.GALLADE,
|
||||
SpeciesId.PROBOPASS,
|
||||
SpeciesId.FROSLASS,
|
||||
SpeciesId.ROTOM,
|
||||
SpeciesId.UXIE,
|
||||
SpeciesId.MESPRIT,
|
||||
SpeciesId.AZELF,
|
||||
SpeciesId.DIALGA,
|
||||
SpeciesId.PALKIA,
|
||||
SpeciesId.REGIGIGAS,
|
||||
SpeciesId.GIRATINA,
|
||||
SpeciesId.DARKRAI,
|
||||
SpeciesId.ARCEUS,
|
||||
SpeciesId.VICTINI,
|
||||
SpeciesId.PATRAT,
|
||||
SpeciesId.WATCHOG,
|
||||
SpeciesId.LILLIPUP,
|
||||
SpeciesId.HERDIER,
|
||||
SpeciesId.STOUTLAND,
|
||||
SpeciesId.MUNNA,
|
||||
SpeciesId.MUSHARNA,
|
||||
SpeciesId.BLITZLE,
|
||||
SpeciesId.ZEBSTRIKA,
|
||||
SpeciesId.WOOBAT,
|
||||
SpeciesId.SWOOBAT,
|
||||
SpeciesId.SIGILYPH,
|
||||
SpeciesId.YAMASK,
|
||||
SpeciesId.COFAGRIGUS,
|
||||
SpeciesId.MINCCINO,
|
||||
SpeciesId.CINCCINO,
|
||||
SpeciesId.GOTHITA,
|
||||
SpeciesId.GOTHORITA,
|
||||
SpeciesId.GOTHITELLE,
|
||||
SpeciesId.SOLOSIS,
|
||||
SpeciesId.DUOSION,
|
||||
SpeciesId.REUNICLUS,
|
||||
SpeciesId.EMOLGA,
|
||||
SpeciesId.FRILLISH,
|
||||
SpeciesId.JELLICENT,
|
||||
SpeciesId.JOLTIK,
|
||||
SpeciesId.GALVANTULA,
|
||||
SpeciesId.KLINK,
|
||||
SpeciesId.KLANG,
|
||||
SpeciesId.KLINKLANG,
|
||||
SpeciesId.EELEKTRIK,
|
||||
SpeciesId.EELEKTROSS,
|
||||
SpeciesId.ELGYEM,
|
||||
SpeciesId.BEHEEYEM,
|
||||
SpeciesId.LITWICK,
|
||||
SpeciesId.LAMPENT,
|
||||
SpeciesId.CHANDELURE,
|
||||
SpeciesId.AXEW,
|
||||
SpeciesId.FRAXURE,
|
||||
SpeciesId.HAXORUS,
|
||||
SpeciesId.STUNFISK,
|
||||
SpeciesId.DRUDDIGON,
|
||||
SpeciesId.GOLETT,
|
||||
SpeciesId.GOLURK,
|
||||
SpeciesId.DEINO,
|
||||
SpeciesId.ZWEILOUS,
|
||||
SpeciesId.HYDREIGON,
|
||||
SpeciesId.THUNDURUS,
|
||||
SpeciesId.ZEKROM,
|
||||
SpeciesId.MELOETTA,
|
||||
SpeciesId.GENESECT,
|
||||
SpeciesId.BRAIXEN,
|
||||
SpeciesId.DELPHOX,
|
||||
SpeciesId.ESPURR,
|
||||
SpeciesId.MEOWSTIC,
|
||||
SpeciesId.HONEDGE,
|
||||
SpeciesId.DOUBLADE,
|
||||
SpeciesId.AEGISLASH,
|
||||
SpeciesId.SKRELP,
|
||||
SpeciesId.DRAGALGE,
|
||||
SpeciesId.HELIOPTILE,
|
||||
SpeciesId.HELIOLISK,
|
||||
SpeciesId.DEDENNE,
|
||||
SpeciesId.GOOMY,
|
||||
SpeciesId.SLIGGOO,
|
||||
SpeciesId.GOODRA,
|
||||
SpeciesId.ZYGARDE,
|
||||
SpeciesId.HOOPA,
|
||||
SpeciesId.YUNGOOS,
|
||||
SpeciesId.GUMSHOOS,
|
||||
SpeciesId.GRUBBIN,
|
||||
SpeciesId.CHARJABUG,
|
||||
SpeciesId.VIKAVOLT,
|
||||
SpeciesId.PASSIMIAN,
|
||||
SpeciesId.TURTONATOR,
|
||||
SpeciesId.TOGEDEMARU,
|
||||
SpeciesId.DRAMPA,
|
||||
SpeciesId.KOMMO_O,
|
||||
SpeciesId.TAPU_KOKO,
|
||||
SpeciesId.SOLGALEO,
|
||||
SpeciesId.LUNALA,
|
||||
SpeciesId.PHEROMOSA,
|
||||
SpeciesId.XURKITREE,
|
||||
SpeciesId.CELESTEELA,
|
||||
SpeciesId.GUZZLORD,
|
||||
SpeciesId.NECROZMA,
|
||||
SpeciesId.MAGEARNA,
|
||||
SpeciesId.NAGANADEL,
|
||||
SpeciesId.ZERAORA,
|
||||
SpeciesId.TOXTRICITY,
|
||||
SpeciesId.MR_RIME,
|
||||
SpeciesId.REGIELEKI,
|
||||
SpeciesId.WYRDEER,
|
||||
SpeciesId.FARIGIRAF,
|
||||
SpeciesId.DUDUNSPARCE,
|
||||
SpeciesId.MIRAIDON,
|
||||
SpeciesId.RAGING_BOLT,
|
||||
SpeciesId.ALOLA_RATTATA,
|
||||
SpeciesId.ALOLA_RATICATE,
|
||||
SpeciesId.ALOLA_RAICHU,
|
||||
SpeciesId.ALOLA_MEOWTH,
|
||||
SpeciesId.ALOLA_PERSIAN,
|
||||
SpeciesId.ALOLA_GRAVELER,
|
||||
SpeciesId.ALOLA_GOLEM,
|
||||
SpeciesId.ALOLA_GRIMER,
|
||||
SpeciesId.ALOLA_MUK,
|
||||
SpeciesId.GALAR_WEEZING,
|
||||
SpeciesId.GALAR_MR_MIME,
|
||||
SpeciesId.HISUI_SLIGGOO,
|
||||
SpeciesId.HISUI_GOODRA,
|
||||
],
|
||||
[MoveId.WATER_PULSE]: [
|
||||
SpeciesId.SQUIRTLE,
|
||||
SpeciesId.WARTORTLE,
|
||||
@ -68747,6 +69026,7 @@ export const tmPoolTiers: TmPoolTiers = {
|
||||
[MoveId.LEAF_BLADE]: ModifierTier.ULTRA,
|
||||
[MoveId.DRAGON_DANCE]: ModifierTier.GREAT,
|
||||
[MoveId.ROCK_BLAST]: ModifierTier.GREAT,
|
||||
[MoveId.SHOCK_WAVE]: ModifierTier.GREAT,
|
||||
[MoveId.WATER_PULSE]: ModifierTier.GREAT,
|
||||
[MoveId.ROOST]: ModifierTier.GREAT,
|
||||
[MoveId.GRAVITY]: ModifierTier.COMMON,
|
||||
|
@ -404,22 +404,18 @@ export const chargeAnims = new Map<ChargeAnim, AnimConfig | [AnimConfig, AnimCon
|
||||
export const commonAnims = new Map<CommonAnim, AnimConfig>();
|
||||
export const encounterAnims = new Map<EncounterAnim, AnimConfig>();
|
||||
|
||||
export function initCommonAnims(): Promise<void> {
|
||||
return new Promise(resolve => {
|
||||
const commonAnimNames = getEnumKeys(CommonAnim);
|
||||
const commonAnimIds = getEnumValues(CommonAnim);
|
||||
const commonAnimFetches: Promise<Map<CommonAnim, AnimConfig>>[] = [];
|
||||
for (let ca = 0; ca < commonAnimIds.length; ca++) {
|
||||
const commonAnimId = commonAnimIds[ca];
|
||||
commonAnimFetches.push(
|
||||
globalScene
|
||||
.cachedFetch(`./battle-anims/common-${toKebabCase(commonAnimNames[ca])}.json`)
|
||||
.then(response => response.json())
|
||||
.then(cas => commonAnims.set(commonAnimId, new AnimConfig(cas))),
|
||||
);
|
||||
}
|
||||
Promise.allSettled(commonAnimFetches).then(() => resolve());
|
||||
});
|
||||
export async function initCommonAnims(): Promise<void> {
|
||||
const commonAnimFetches: Promise<Map<CommonAnim, AnimConfig>>[] = [];
|
||||
for (const commonAnimName of getEnumKeys(CommonAnim)) {
|
||||
const commonAnimId = CommonAnim[commonAnimName];
|
||||
commonAnimFetches.push(
|
||||
globalScene
|
||||
.cachedFetch(`./battle-anims/common-${toKebabCase(commonAnimName)}.json`)
|
||||
.then(response => response.json())
|
||||
.then(cas => commonAnims.set(commonAnimId, new AnimConfig(cas))),
|
||||
);
|
||||
}
|
||||
await Promise.allSettled(commonAnimFetches);
|
||||
}
|
||||
|
||||
export function initMoveAnim(move: MoveId): Promise<void> {
|
||||
@ -1396,279 +1392,3 @@ export class EncounterBattleAnim extends BattleAnim {
|
||||
return this.oppAnim;
|
||||
}
|
||||
}
|
||||
|
||||
export async function populateAnims() {
|
||||
const commonAnimNames = getEnumKeys(CommonAnim).map(k => k.toLowerCase());
|
||||
const commonAnimMatchNames = commonAnimNames.map(k => k.replace(/_/g, ""));
|
||||
const commonAnimIds = getEnumValues(CommonAnim);
|
||||
const chargeAnimNames = getEnumKeys(ChargeAnim).map(k => k.toLowerCase());
|
||||
const chargeAnimMatchNames = chargeAnimNames.map(k => k.replace(/_/g, " "));
|
||||
const chargeAnimIds = getEnumValues(ChargeAnim);
|
||||
const commonNamePattern = /name: (?:Common:)?(Opp )?(.*)/;
|
||||
const moveNameToId = {};
|
||||
// Exclude MoveId.NONE;
|
||||
for (const move of getEnumValues(MoveId).slice(1)) {
|
||||
// KARATE_CHOP => KARATECHOP
|
||||
const moveName = MoveId[move].toUpperCase().replace(/_/g, "");
|
||||
moveNameToId[moveName] = move;
|
||||
}
|
||||
|
||||
const seNames: string[] = []; //(await fs.readdir('./public/audio/se/battle_anims/')).map(se => se.toString());
|
||||
|
||||
const animsData: any[] = []; //battleAnimRawData.split('!ruby/array:PBAnimation').slice(1); // TODO: add a proper type
|
||||
for (let a = 0; a < animsData.length; a++) {
|
||||
const fields = animsData[a].split("@").slice(1);
|
||||
|
||||
const nameField = fields.find(f => f.startsWith("name: "));
|
||||
|
||||
let isOppMove: boolean | undefined;
|
||||
let commonAnimId: CommonAnim | undefined;
|
||||
let chargeAnimId: ChargeAnim | undefined;
|
||||
if (!nameField.startsWith("name: Move:") && !(isOppMove = nameField.startsWith("name: OppMove:"))) {
|
||||
const nameMatch = commonNamePattern.exec(nameField)!; // TODO: is this bang correct?
|
||||
const name = nameMatch[2].toLowerCase();
|
||||
if (commonAnimMatchNames.indexOf(name) > -1) {
|
||||
commonAnimId = commonAnimIds[commonAnimMatchNames.indexOf(name)];
|
||||
} else if (chargeAnimMatchNames.indexOf(name) > -1) {
|
||||
isOppMove = nameField.startsWith("name: Opp ");
|
||||
chargeAnimId = chargeAnimIds[chargeAnimMatchNames.indexOf(name)];
|
||||
}
|
||||
}
|
||||
const nameIndex = nameField.indexOf(":", 5) + 1;
|
||||
const animName = nameField.slice(nameIndex, nameField.indexOf("\n", nameIndex));
|
||||
if (!moveNameToId.hasOwnProperty(animName) && !commonAnimId && !chargeAnimId) {
|
||||
continue;
|
||||
}
|
||||
const anim = commonAnimId || chargeAnimId ? new AnimConfig() : new AnimConfig();
|
||||
if (anim instanceof AnimConfig) {
|
||||
(anim as AnimConfig).id = moveNameToId[animName];
|
||||
}
|
||||
if (commonAnimId) {
|
||||
commonAnims.set(commonAnimId, anim);
|
||||
} else if (chargeAnimId) {
|
||||
chargeAnims.set(chargeAnimId, !isOppMove ? anim : [chargeAnims.get(chargeAnimId) as AnimConfig, anim]);
|
||||
} else {
|
||||
moveAnims.set(
|
||||
moveNameToId[animName],
|
||||
!isOppMove ? (anim as AnimConfig) : [moveAnims.get(moveNameToId[animName]) as AnimConfig, anim as AnimConfig],
|
||||
);
|
||||
}
|
||||
for (let f = 0; f < fields.length; f++) {
|
||||
const field = fields[f];
|
||||
const fieldName = field.slice(0, field.indexOf(":"));
|
||||
const fieldData = field.slice(fieldName.length + 1, field.lastIndexOf("\n")).trim();
|
||||
switch (fieldName) {
|
||||
case "array": {
|
||||
const framesData = fieldData.split(" - - - ").slice(1);
|
||||
for (let fd = 0; fd < framesData.length; fd++) {
|
||||
anim.frames.push([]);
|
||||
const frameData = framesData[fd];
|
||||
const focusFramesData = frameData.split(" - - ");
|
||||
for (let tf = 0; tf < focusFramesData.length; tf++) {
|
||||
const values = focusFramesData[tf].replace(/ {6}- /g, "").split("\n");
|
||||
const targetFrame = new AnimFrame(
|
||||
Number.parseFloat(values[0]),
|
||||
Number.parseFloat(values[1]),
|
||||
Number.parseFloat(values[2]),
|
||||
Number.parseFloat(values[11]),
|
||||
Number.parseFloat(values[3]),
|
||||
Number.parseInt(values[4]) === 1,
|
||||
Number.parseInt(values[6]) === 1,
|
||||
Number.parseInt(values[5]),
|
||||
Number.parseInt(values[7]),
|
||||
Number.parseInt(values[8]),
|
||||
Number.parseInt(values[12]),
|
||||
Number.parseInt(values[13]),
|
||||
Number.parseInt(values[14]),
|
||||
Number.parseInt(values[15]),
|
||||
Number.parseInt(values[16]),
|
||||
Number.parseInt(values[17]),
|
||||
Number.parseInt(values[18]),
|
||||
Number.parseInt(values[19]),
|
||||
Number.parseInt(values[21]),
|
||||
Number.parseInt(values[22]),
|
||||
Number.parseInt(values[23]),
|
||||
Number.parseInt(values[24]),
|
||||
Number.parseInt(values[20]) === 1,
|
||||
Number.parseInt(values[25]),
|
||||
Number.parseInt(values[26]) as AnimFocus,
|
||||
);
|
||||
anim.frames[fd].push(targetFrame);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "graphic": {
|
||||
const graphic = fieldData !== "''" ? fieldData : "";
|
||||
anim.graphic = graphic.indexOf(".") > -1 ? graphic.slice(0, fieldData.indexOf(".")) : graphic;
|
||||
break;
|
||||
}
|
||||
case "timing": {
|
||||
const timingEntries = fieldData.split("- !ruby/object:PBAnimTiming ").slice(1);
|
||||
for (let t = 0; t < timingEntries.length; t++) {
|
||||
const timingData = timingEntries[t]
|
||||
.replace(/\n/g, " ")
|
||||
.replace(/[ ]{2,}/g, " ")
|
||||
.replace(/[a-z]+: ! '', /gi, "")
|
||||
.replace(/name: (.*?),/, 'name: "$1",')
|
||||
.replace(
|
||||
/flashColor: !ruby\/object:Color { alpha: ([\d.]+), blue: ([\d.]+), green: ([\d.]+), red: ([\d.]+)}/,
|
||||
"flashRed: $4, flashGreen: $3, flashBlue: $2, flashAlpha: $1",
|
||||
);
|
||||
const frameIndex = Number.parseInt(/frame: (\d+)/.exec(timingData)![1]); // TODO: is the bang correct?
|
||||
let resourceName = /name: "(.*?)"/.exec(timingData)![1].replace("''", ""); // TODO: is the bang correct?
|
||||
const timingType = Number.parseInt(/timingType: (\d)/.exec(timingData)![1]); // TODO: is the bang correct?
|
||||
let timedEvent: AnimTimedEvent | undefined;
|
||||
switch (timingType) {
|
||||
case 0:
|
||||
if (resourceName && resourceName.indexOf(".") === -1) {
|
||||
let ext: string | undefined;
|
||||
["wav", "mp3", "m4a"].every(e => {
|
||||
if (seNames.indexOf(`${resourceName}.${e}`) > -1) {
|
||||
ext = e;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (!ext) {
|
||||
ext = ".wav";
|
||||
}
|
||||
resourceName += `.${ext}`;
|
||||
}
|
||||
timedEvent = new AnimTimedSoundEvent(frameIndex, resourceName);
|
||||
break;
|
||||
case 1:
|
||||
timedEvent = new AnimTimedAddBgEvent(frameIndex, resourceName.slice(0, resourceName.indexOf(".")));
|
||||
break;
|
||||
case 2:
|
||||
timedEvent = new AnimTimedUpdateBgEvent(frameIndex, resourceName.slice(0, resourceName.indexOf(".")));
|
||||
break;
|
||||
}
|
||||
if (!timedEvent) {
|
||||
continue;
|
||||
}
|
||||
const propPattern = /([a-z]+): (.*?)(?:,|\})/gi;
|
||||
let propMatch: RegExpExecArray;
|
||||
while ((propMatch = propPattern.exec(timingData)!)) {
|
||||
// TODO: is this bang correct?
|
||||
const prop = propMatch[1];
|
||||
let value: any = propMatch[2];
|
||||
switch (prop) {
|
||||
case "bgX":
|
||||
case "bgY":
|
||||
value = Number.parseFloat(value);
|
||||
break;
|
||||
case "volume":
|
||||
case "pitch":
|
||||
case "opacity":
|
||||
case "colorRed":
|
||||
case "colorGreen":
|
||||
case "colorBlue":
|
||||
case "colorAlpha":
|
||||
case "duration":
|
||||
case "flashScope":
|
||||
case "flashRed":
|
||||
case "flashGreen":
|
||||
case "flashBlue":
|
||||
case "flashAlpha":
|
||||
case "flashDuration":
|
||||
value = Number.parseInt(value);
|
||||
break;
|
||||
}
|
||||
if (timedEvent.hasOwnProperty(prop)) {
|
||||
timedEvent[prop] = value;
|
||||
}
|
||||
}
|
||||
if (!anim.frameTimedEvents.has(frameIndex)) {
|
||||
anim.frameTimedEvents.set(frameIndex, []);
|
||||
}
|
||||
anim.frameTimedEvents.get(frameIndex)!.push(timedEvent); // TODO: is this bang correct?
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "position":
|
||||
anim.position = Number.parseInt(fieldData);
|
||||
break;
|
||||
case "hue":
|
||||
anim.hue = Number.parseInt(fieldData);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// biome-ignore lint/correctness/noUnusedVariables: used in commented code
|
||||
const animReplacer = (k, v) => {
|
||||
if (k === "id" && !v) {
|
||||
return undefined;
|
||||
}
|
||||
if (v instanceof Map) {
|
||||
return Object.fromEntries(v);
|
||||
}
|
||||
if (v instanceof AnimTimedEvent) {
|
||||
v["eventType"] = v.getEventType();
|
||||
}
|
||||
return v;
|
||||
};
|
||||
|
||||
const animConfigProps = ["id", "graphic", "frames", "frameTimedEvents", "position", "hue"];
|
||||
const animFrameProps = [
|
||||
"x",
|
||||
"y",
|
||||
"zoomX",
|
||||
"zoomY",
|
||||
"angle",
|
||||
"mirror",
|
||||
"visible",
|
||||
"blendType",
|
||||
"target",
|
||||
"graphicFrame",
|
||||
"opacity",
|
||||
"color",
|
||||
"tone",
|
||||
"flash",
|
||||
"locked",
|
||||
"priority",
|
||||
"focus",
|
||||
];
|
||||
const propSets = [animConfigProps, animFrameProps];
|
||||
|
||||
// biome-ignore lint/correctness/noUnusedVariables: used in commented code
|
||||
const animComparator = (a: Element, b: Element) => {
|
||||
let props: string[];
|
||||
for (let p = 0; p < propSets.length; p++) {
|
||||
props = propSets[p];
|
||||
// @ts-expect-error TODO
|
||||
const ai = props.indexOf(a.key);
|
||||
if (ai === -1) {
|
||||
continue;
|
||||
}
|
||||
// @ts-expect-error TODO
|
||||
const bi = props.indexOf(b.key);
|
||||
|
||||
return ai < bi ? -1 : ai > bi ? 1 : 0;
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
/*for (let ma of moveAnims.keys()) {
|
||||
const data = moveAnims.get(ma);
|
||||
(async () => {
|
||||
await fs.writeFile(`../public/battle-anims/${Moves[ma].toLowerCase().replace(/\_/g, '-')}.json`, stringify(data, { replacer: animReplacer, cmp: animComparator, space: ' ' }));
|
||||
})();
|
||||
}
|
||||
|
||||
for (let ca of chargeAnims.keys()) {
|
||||
const data = chargeAnims.get(ca);
|
||||
(async () => {
|
||||
await fs.writeFile(`../public/battle-anims/${chargeAnimNames[chargeAnimIds.indexOf(ca)].replace(/\_/g, '-')}.json`, stringify(data, { replacer: animReplacer, cmp: animComparator, space: ' ' }));
|
||||
})();
|
||||
}
|
||||
|
||||
for (let cma of commonAnims.keys()) {
|
||||
const data = commonAnims.get(cma);
|
||||
(async () => {
|
||||
await fs.writeFile(`../public/battle-anims/common-${commonAnimNames[commonAnimIds.indexOf(cma)].replace(/\_/g, '-')}.json`, stringify(data, { replacer: animReplacer, cmp: animComparator, space: ' ' }));
|
||||
})();
|
||||
}*/
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import { Trainer } from "#field/trainer";
|
||||
import type { ModifierTypeOption } from "#modifiers/modifier-type";
|
||||
import { PokemonMove } from "#moves/pokemon-move";
|
||||
import type { DexAttrProps, GameData } from "#system/game-data";
|
||||
import { RibbonData, type RibbonFlag } from "#system/ribbons/ribbon-data";
|
||||
import { type BooleanHolder, isBetween, type NumberHolder, randSeedItem } from "#utils/common";
|
||||
import { deepCopy } from "#utils/data";
|
||||
import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils";
|
||||
@ -42,6 +43,15 @@ export abstract class Challenge {
|
||||
|
||||
public conditions: ChallengeCondition[];
|
||||
|
||||
/**
|
||||
* The Ribbon awarded on challenge completion, or 0 if the challenge has no ribbon or is not enabled
|
||||
*
|
||||
* @defaultValue 0
|
||||
*/
|
||||
public get ribbonAwarded(): RibbonFlag {
|
||||
return 0 as RibbonFlag;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param id {@link Challenges} The enum value for the challenge
|
||||
*/
|
||||
@ -423,6 +433,12 @@ type ChallengeCondition = (data: GameData) => boolean;
|
||||
* Implements a mono generation challenge.
|
||||
*/
|
||||
export class SingleGenerationChallenge extends Challenge {
|
||||
public override get ribbonAwarded(): RibbonFlag {
|
||||
// NOTE: This logic will not work for the eventual mono gen 10 ribbon, as
|
||||
// as its flag will not be in sequence with the other mono gen ribbons.
|
||||
return this.value ? ((RibbonData.MONO_GEN_1 << (this.value - 1)) as RibbonFlag) : 0;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super(Challenges.SINGLE_GENERATION, 9);
|
||||
}
|
||||
@ -686,6 +702,12 @@ interface monotypeOverride {
|
||||
* Implements a mono type challenge.
|
||||
*/
|
||||
export class SingleTypeChallenge extends Challenge {
|
||||
public override get ribbonAwarded(): RibbonFlag {
|
||||
// `this.value` represents the 1-based index of pokemon type
|
||||
// `RibbonData.MONO_NORMAL` starts the flag position for the types,
|
||||
// and we shift it by 1 for the specific type.
|
||||
return this.value ? ((RibbonData.MONO_NORMAL << (this.value - 1)) as RibbonFlag) : 0;
|
||||
}
|
||||
private static TYPE_OVERRIDES: monotypeOverride[] = [
|
||||
{ species: SpeciesId.CASTFORM, type: PokemonType.NORMAL, fusion: false },
|
||||
];
|
||||
@ -755,6 +777,9 @@ export class SingleTypeChallenge extends Challenge {
|
||||
* Implements a fresh start challenge.
|
||||
*/
|
||||
export class FreshStartChallenge extends Challenge {
|
||||
public override get ribbonAwarded(): RibbonFlag {
|
||||
return this.value ? RibbonData.FRESH_START : 0;
|
||||
}
|
||||
constructor() {
|
||||
super(Challenges.FRESH_START, 2);
|
||||
}
|
||||
@ -828,6 +853,9 @@ export class FreshStartChallenge extends Challenge {
|
||||
* Implements an inverse battle challenge.
|
||||
*/
|
||||
export class InverseBattleChallenge extends Challenge {
|
||||
public override get ribbonAwarded(): RibbonFlag {
|
||||
return this.value ? RibbonData.INVERSE : 0;
|
||||
}
|
||||
constructor() {
|
||||
super(Challenges.INVERSE_BATTLE, 1);
|
||||
}
|
||||
@ -861,6 +889,9 @@ export class InverseBattleChallenge extends Challenge {
|
||||
* Implements a flip stat challenge.
|
||||
*/
|
||||
export class FlipStatChallenge extends Challenge {
|
||||
public override get ribbonAwarded(): RibbonFlag {
|
||||
return this.value ? RibbonData.FLIP_STATS : 0;
|
||||
}
|
||||
constructor() {
|
||||
super(Challenges.FLIP_STAT, 1);
|
||||
}
|
||||
@ -941,6 +972,9 @@ export class LowerStarterPointsChallenge extends Challenge {
|
||||
* Implements a No Support challenge
|
||||
*/
|
||||
export class LimitedSupportChallenge extends Challenge {
|
||||
public override get ribbonAwarded(): RibbonFlag {
|
||||
return this.value ? ((RibbonData.NO_HEAL << (this.value - 1)) as RibbonFlag) : 0;
|
||||
}
|
||||
constructor() {
|
||||
super(Challenges.LIMITED_SUPPORT, 3);
|
||||
}
|
||||
@ -973,6 +1007,9 @@ export class LimitedSupportChallenge extends Challenge {
|
||||
* Implements a Limited Catch challenge
|
||||
*/
|
||||
export class LimitedCatchChallenge extends Challenge {
|
||||
public override get ribbonAwarded(): RibbonFlag {
|
||||
return this.value ? RibbonData.LIMITED_CATCH : 0;
|
||||
}
|
||||
constructor() {
|
||||
super(Challenges.LIMITED_CATCH, 1);
|
||||
}
|
||||
@ -997,6 +1034,9 @@ export class LimitedCatchChallenge extends Challenge {
|
||||
* Implements a Permanent Faint challenge
|
||||
*/
|
||||
export class HardcoreChallenge extends Challenge {
|
||||
public override get ribbonAwarded(): RibbonFlag {
|
||||
return this.value ? RibbonData.HARDCORE : 0;
|
||||
}
|
||||
constructor() {
|
||||
super(Challenges.HARDCORE, 1);
|
||||
}
|
||||
|
@ -47,6 +47,7 @@ export class EggHatchData {
|
||||
caughtCount: currDexEntry.caughtCount,
|
||||
hatchedCount: currDexEntry.hatchedCount,
|
||||
ivs: [...currDexEntry.ivs],
|
||||
ribbons: currDexEntry.ribbons,
|
||||
};
|
||||
this.starterDataEntryBeforeUpdate = {
|
||||
moveset: currStarterDataEntry.moveset,
|
||||
|
@ -86,11 +86,11 @@ import { PokemonHealPhase } from "#phases/pokemon-heal-phase";
|
||||
import { SwitchSummonPhase } from "#phases/switch-summon-phase";
|
||||
import type { AttackMoveResult } from "#types/attack-move-result";
|
||||
import type { Localizable } from "#types/locales";
|
||||
import type { ChargingMove, MoveAttrMap, MoveAttrString, MoveClassMap, MoveKindString } from "#types/move-types";
|
||||
import type { ChargingMove, MoveAttrMap, MoveAttrString, MoveClassMap, MoveKindString, MoveMessageFunc } from "#types/move-types";
|
||||
import type { TurnMove } from "#types/turn-move";
|
||||
import { BooleanHolder, type Constructor, isNullOrUndefined, NumberHolder, randSeedFloat, randSeedInt, randSeedItem, toDmgValue } from "#utils/common";
|
||||
import { getEnumValues } from "#utils/enums";
|
||||
import { toTitleCase } from "#utils/strings";
|
||||
import { toCamelCase, toTitleCase } from "#utils/strings";
|
||||
import i18next from "i18next";
|
||||
import { applyChallenges } from "#utils/challenge-utils";
|
||||
|
||||
@ -162,10 +162,16 @@ export abstract class Move implements Localizable {
|
||||
}
|
||||
|
||||
localize(): void {
|
||||
const i18nKey = MoveId[this.id].split("_").filter(f => f).map((f, i) => i ? `${f[0]}${f.slice(1).toLowerCase()}` : f.toLowerCase()).join("") as unknown as string;
|
||||
const i18nKey = toCamelCase(MoveId[this.id])
|
||||
|
||||
this.name = this.id ? `${i18next.t(`move:${i18nKey}.name`)}${this.nameAppend}` : "";
|
||||
this.effect = this.id ? `${i18next.t(`move:${i18nKey}.effect`)}${this.nameAppend}` : "";
|
||||
if (this.id === MoveId.NONE) {
|
||||
this.name = "";
|
||||
this.effect = ""
|
||||
return;
|
||||
}
|
||||
|
||||
this.name = `${i18next.t(`move:${i18nKey}.name`)}${this.nameAppend}`;
|
||||
this.effect = `${i18next.t(`move:${i18nKey}.effect`)}${this.nameAppend}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1357,20 +1363,20 @@ export class MoveHeaderAttr extends MoveAttr {
|
||||
|
||||
/**
|
||||
* Header attribute to queue a message at the beginning of a turn.
|
||||
* @see {@link MoveHeaderAttr}
|
||||
*/
|
||||
export class MessageHeaderAttr extends MoveHeaderAttr {
|
||||
private message: string | ((user: Pokemon, move: Move) => string);
|
||||
/** The message to display, or a function producing one. */
|
||||
private message: string | MoveMessageFunc;
|
||||
|
||||
constructor(message: string | ((user: Pokemon, move: Move) => string)) {
|
||||
constructor(message: string | MoveMessageFunc) {
|
||||
super();
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
apply(user: Pokemon, target: Pokemon, move: Move): boolean {
|
||||
const message = typeof this.message === "string"
|
||||
? this.message
|
||||
: this.message(user, move);
|
||||
: this.message(user, target, move);
|
||||
|
||||
if (message) {
|
||||
globalScene.phaseManager.queueMessage(message);
|
||||
@ -1418,21 +1424,21 @@ export class BeakBlastHeaderAttr extends AddBattlerTagHeaderAttr {
|
||||
*/
|
||||
export class PreMoveMessageAttr extends MoveAttr {
|
||||
/** The message to display or a function returning one */
|
||||
private message: string | ((user: Pokemon, target: Pokemon, move: Move) => string | undefined);
|
||||
private message: string | MoveMessageFunc;
|
||||
|
||||
/**
|
||||
* Create a new {@linkcode PreMoveMessageAttr} to display a message before move execution.
|
||||
* @param message - The message to display before move use, either as a string or a function producing one.
|
||||
* @param message - The message to display before move use, either` a literal string or a function producing one.
|
||||
* @remarks
|
||||
* If {@linkcode message} evaluates to an empty string (`''`), no message will be displayed
|
||||
* If {@linkcode message} evaluates to an empty string (`""`), no message will be displayed
|
||||
* (though the move will still succeed).
|
||||
*/
|
||||
constructor(message: string | ((user: Pokemon, target: Pokemon, move: Move) => string)) {
|
||||
constructor(message: string | MoveMessageFunc) {
|
||||
super();
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, _args: any[]): boolean {
|
||||
apply(user: Pokemon, target: Pokemon, move: Move): boolean {
|
||||
const message = typeof this.message === "function"
|
||||
? this.message(user, target, move)
|
||||
: this.message;
|
||||
@ -1453,18 +1459,17 @@ export class PreMoveMessageAttr extends MoveAttr {
|
||||
* @extends MoveAttr
|
||||
*/
|
||||
export class PreUseInterruptAttr extends MoveAttr {
|
||||
protected message?: string | ((user: Pokemon, target: Pokemon, move: Move) => string);
|
||||
protected overridesFailedMessage: boolean;
|
||||
protected message: string | MoveMessageFunc;
|
||||
protected conditionFunc: MoveConditionFunc;
|
||||
|
||||
/**
|
||||
* Create a new MoveInterruptedMessageAttr.
|
||||
* @param message The message to display when the move is interrupted, or a function that formats the message based on the user, target, and move.
|
||||
*/
|
||||
constructor(message?: string | ((user: Pokemon, target: Pokemon, move: Move) => string), conditionFunc?: MoveConditionFunc) {
|
||||
constructor(message: string | MoveMessageFunc, conditionFunc: MoveConditionFunc) {
|
||||
super();
|
||||
this.message = message;
|
||||
this.conditionFunc = conditionFunc ?? (() => true);
|
||||
this.conditionFunc = conditionFunc;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1485,11 +1490,9 @@ export class PreUseInterruptAttr extends MoveAttr {
|
||||
*/
|
||||
override getFailedText(user: Pokemon, target: Pokemon, move: Move): string | undefined {
|
||||
if (this.message && this.conditionFunc(user, target, move)) {
|
||||
const message =
|
||||
typeof this.message === "string"
|
||||
? (this.message as string)
|
||||
return typeof this.message === "string"
|
||||
? this.message
|
||||
: this.message(user, target, move);
|
||||
return message;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1694,17 +1697,30 @@ export class SurviveDamageAttr extends ModifiedDamageAttr {
|
||||
}
|
||||
}
|
||||
|
||||
export class SplashAttr extends MoveEffectAttr {
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:splash"));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Move attribute to display arbitrary text during a move's execution.
|
||||
*/
|
||||
export class MessageAttr extends MoveEffectAttr {
|
||||
/** The message to display, either as a string or a function returning one. */
|
||||
private message: string | MoveMessageFunc;
|
||||
|
||||
export class CelebrateAttr extends MoveEffectAttr {
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:celebrate", { playerName: loggedInUser?.username }));
|
||||
return true;
|
||||
constructor(message: string | MoveMessageFunc, options?: MoveEffectAttrOptions) {
|
||||
// TODO: Do we need to respect `selfTarget` if we're just displaying text?
|
||||
super(false, options)
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
override apply(user: Pokemon, target: Pokemon, move: Move): boolean {
|
||||
const message = typeof this.message === "function"
|
||||
? this.message(user, target, move)
|
||||
: this.message;
|
||||
|
||||
// TODO: Consider changing if/when MoveAttr `apply` return values become significant
|
||||
if (message) {
|
||||
globalScene.phaseManager.queueMessage(message, 500);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -5916,8 +5932,8 @@ export class ProtectAttr extends AddBattlerTagAttr {
|
||||
for (const turnMove of user.getLastXMoves(-1).slice()) {
|
||||
if (
|
||||
// Quick & Wide guard increment the Protect counter without using it for fail chance
|
||||
!(allMoves[turnMove.move].hasAttr("ProtectAttr") ||
|
||||
[MoveId.QUICK_GUARD, MoveId.WIDE_GUARD].includes(turnMove.move)) ||
|
||||
!(allMoves[turnMove.move].hasAttr("ProtectAttr") ||
|
||||
[MoveId.QUICK_GUARD, MoveId.WIDE_GUARD].includes(turnMove.move)) ||
|
||||
turnMove.result !== MoveResult.SUCCESS
|
||||
) {
|
||||
break;
|
||||
@ -5931,38 +5947,6 @@ export class ProtectAttr extends AddBattlerTagAttr {
|
||||
}
|
||||
}
|
||||
|
||||
export class IgnoreAccuracyAttr extends AddBattlerTagAttr {
|
||||
constructor() {
|
||||
super(BattlerTagType.IGNORE_ACCURACY, true, false, 2);
|
||||
}
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
if (!super.apply(user, target, move, args)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:tookAimAtTarget", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target) }));
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export class FaintCountdownAttr extends AddBattlerTagAttr {
|
||||
constructor() {
|
||||
super(BattlerTagType.PERISH_SONG, false, true, 4);
|
||||
}
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
if (!super.apply(user, target, move, args)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:faintCountdown", { pokemonName: getPokemonNameWithAffix(target), turnCount: this.turnCountMin - 1 }));
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute to remove all Substitutes from the field.
|
||||
* @extends MoveEffectAttr
|
||||
@ -6603,8 +6587,10 @@ export class ChillyReceptionAttr extends ForceSwitchOutAttr {
|
||||
return (user, target, move) => globalScene.arena.weather?.weatherType !== WeatherType.SNOW || super.getSwitchOutCondition()(user, target, move);
|
||||
}
|
||||
}
|
||||
|
||||
export class RemoveTypeAttr extends MoveEffectAttr {
|
||||
|
||||
// TODO: Remove the message callback
|
||||
private removedType: PokemonType;
|
||||
private messageCallback: ((user: Pokemon) => void) | undefined;
|
||||
|
||||
@ -8299,8 +8285,6 @@ const MoveAttrs = Object.freeze({
|
||||
RandomLevelDamageAttr,
|
||||
ModifiedDamageAttr,
|
||||
SurviveDamageAttr,
|
||||
SplashAttr,
|
||||
CelebrateAttr,
|
||||
RecoilAttr,
|
||||
SacrificialAttr,
|
||||
SacrificialAttrOnHit,
|
||||
@ -8443,8 +8427,7 @@ const MoveAttrs = Object.freeze({
|
||||
RechargeAttr,
|
||||
TrapAttr,
|
||||
ProtectAttr,
|
||||
IgnoreAccuracyAttr,
|
||||
FaintCountdownAttr,
|
||||
MessageAttr,
|
||||
RemoveAllSubstitutesAttr,
|
||||
HitsTagAttr,
|
||||
HitsTagForDoubleDamageAttr,
|
||||
@ -8529,7 +8512,7 @@ export function initMoves() {
|
||||
.punchingMove(),
|
||||
new AttackMove(MoveId.SCRATCH, PokemonType.NORMAL, MoveCategory.PHYSICAL, 40, 100, 35, -1, 0, 1),
|
||||
new AttackMove(MoveId.VISE_GRIP, PokemonType.NORMAL, MoveCategory.PHYSICAL, 55, 100, 30, -1, 0, 1),
|
||||
new AttackMove(MoveId.GUILLOTINE, PokemonType.NORMAL, MoveCategory.PHYSICAL, 200, 30, 5, -1, 0, 1)
|
||||
new AttackMove(MoveId.GUILLOTINE, PokemonType.NORMAL, MoveCategory.PHYSICAL, 250, 30, 5, -1, 0, 1)
|
||||
.attr(OneHitKOAttr)
|
||||
.attr(OneHitKOAccuracyAttr),
|
||||
new ChargingAttackMove(MoveId.RAZOR_WIND, PokemonType.NORMAL, MoveCategory.SPECIAL, 80, 100, 10, -1, 0, 1)
|
||||
@ -8582,7 +8565,7 @@ export function initMoves() {
|
||||
new AttackMove(MoveId.HORN_ATTACK, PokemonType.NORMAL, MoveCategory.PHYSICAL, 65, 100, 25, -1, 0, 1),
|
||||
new AttackMove(MoveId.FURY_ATTACK, PokemonType.NORMAL, MoveCategory.PHYSICAL, 15, 85, 20, -1, 0, 1)
|
||||
.attr(MultiHitAttr),
|
||||
new AttackMove(MoveId.HORN_DRILL, PokemonType.NORMAL, MoveCategory.PHYSICAL, 200, 30, 5, -1, 0, 1)
|
||||
new AttackMove(MoveId.HORN_DRILL, PokemonType.NORMAL, MoveCategory.PHYSICAL, 250, 30, 5, -1, 0, 1)
|
||||
.attr(OneHitKOAttr)
|
||||
.attr(OneHitKOAccuracyAttr),
|
||||
new AttackMove(MoveId.TACKLE, PokemonType.NORMAL, MoveCategory.PHYSICAL, 40, 100, 35, -1, 0, 1),
|
||||
@ -8763,7 +8746,7 @@ export function initMoves() {
|
||||
.attr(MovePowerMultiplierAttr, (user, target, move) => globalScene.arena.getTerrainType() === TerrainType.GRASSY && target.isGrounded() ? 0.5 : 1)
|
||||
.makesContact(false)
|
||||
.target(MoveTarget.ALL_NEAR_OTHERS),
|
||||
new AttackMove(MoveId.FISSURE, PokemonType.GROUND, MoveCategory.PHYSICAL, 200, 30, 5, -1, 0, 1)
|
||||
new AttackMove(MoveId.FISSURE, PokemonType.GROUND, MoveCategory.PHYSICAL, 250, 30, 5, -1, 0, 1)
|
||||
.attr(OneHitKOAttr)
|
||||
.attr(OneHitKOAccuracyAttr)
|
||||
.attr(HitsTagAttr, BattlerTagType.UNDERGROUND)
|
||||
@ -8938,7 +8921,7 @@ export function initMoves() {
|
||||
new AttackMove(MoveId.PSYWAVE, PokemonType.PSYCHIC, MoveCategory.SPECIAL, -1, 100, 15, -1, 0, 1)
|
||||
.attr(RandomLevelDamageAttr),
|
||||
new SelfStatusMove(MoveId.SPLASH, PokemonType.NORMAL, -1, 40, -1, 0, 1)
|
||||
.attr(SplashAttr)
|
||||
.attr(MessageAttr, i18next.t("moveTriggers:splash"))
|
||||
.condition(failOnGravityCondition),
|
||||
new SelfStatusMove(MoveId.ACID_ARMOR, PokemonType.POISON, -1, 20, -1, 0, 1)
|
||||
.attr(StatStageChangeAttr, [ Stat.DEF ], 2, true),
|
||||
@ -9000,7 +8983,10 @@ export function initMoves() {
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1)
|
||||
.reflectable(),
|
||||
new StatusMove(MoveId.MIND_READER, PokemonType.NORMAL, -1, 5, -1, 0, 2)
|
||||
.attr(IgnoreAccuracyAttr),
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.IGNORE_ACCURACY, true, false, 2)
|
||||
.attr(MessageAttr, (user, target) =>
|
||||
i18next.t("moveTriggers:tookAimAtTarget", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target) })
|
||||
),
|
||||
new StatusMove(MoveId.NIGHTMARE, PokemonType.GHOST, 100, 15, -1, 0, 2)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.NIGHTMARE)
|
||||
.condition(targetSleptOrComatoseCondition),
|
||||
@ -9088,7 +9074,9 @@ export function initMoves() {
|
||||
return lastTurnMove.length === 0 || lastTurnMove[0].move !== move.id || lastTurnMove[0].result !== MoveResult.SUCCESS;
|
||||
}),
|
||||
new StatusMove(MoveId.PERISH_SONG, PokemonType.NORMAL, -1, 5, -1, 0, 2)
|
||||
.attr(FaintCountdownAttr)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.PERISH_SONG, false, true, 4)
|
||||
.attr(MessageAttr, (_user, target) =>
|
||||
i18next.t("moveTriggers:faintCountdown", { pokemonName: getPokemonNameWithAffix(target), turnCount: 3 }))
|
||||
.ignoresProtect()
|
||||
.soundBased()
|
||||
.condition(failOnBossCondition)
|
||||
@ -9104,7 +9092,10 @@ export function initMoves() {
|
||||
.attr(MultiHitAttr)
|
||||
.makesContact(false),
|
||||
new StatusMove(MoveId.LOCK_ON, PokemonType.NORMAL, -1, 5, -1, 0, 2)
|
||||
.attr(IgnoreAccuracyAttr),
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.IGNORE_ACCURACY, true, false, 2)
|
||||
.attr(MessageAttr, (user, target) =>
|
||||
i18next.t("moveTriggers:tookAimAtTarget", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target) })
|
||||
),
|
||||
new AttackMove(MoveId.OUTRAGE, PokemonType.DRAGON, MoveCategory.PHYSICAL, 120, 100, 10, -1, 0, 2)
|
||||
.attr(FrenzyAttr)
|
||||
.attr(MissEffectAttr, frenzyMissFunc)
|
||||
@ -9331,8 +9322,8 @@ export function initMoves() {
|
||||
&& (user.status.effect === StatusEffect.BURN || user.status.effect === StatusEffect.POISON || user.status.effect === StatusEffect.TOXIC || user.status.effect === StatusEffect.PARALYSIS) ? 2 : 1)
|
||||
.attr(BypassBurnDamageReductionAttr),
|
||||
new AttackMove(MoveId.FOCUS_PUNCH, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 150, 100, 20, -1, -3, 3)
|
||||
.attr(MessageHeaderAttr, (user, move) => i18next.t("moveTriggers:isTighteningFocus", { pokemonName: getPokemonNameWithAffix(user) }))
|
||||
.attr(PreUseInterruptAttr, (user, target, move) => i18next.t("moveTriggers:lostFocus", { pokemonName: getPokemonNameWithAffix(user) }), user => !!user.turnData.attacksReceived.find(r => r.damage))
|
||||
.attr(MessageHeaderAttr, (user) => i18next.t("moveTriggers:isTighteningFocus", { pokemonName: getPokemonNameWithAffix(user) }))
|
||||
.attr(PreUseInterruptAttr, (user) => i18next.t("moveTriggers:lostFocus", { pokemonName: getPokemonNameWithAffix(user) }), user => user.turnData.attacksReceived.some(r => r.damage > 0))
|
||||
.punchingMove(),
|
||||
new AttackMove(MoveId.SMELLING_SALTS, PokemonType.NORMAL, MoveCategory.PHYSICAL, 70, 100, 10, -1, 0, 3)
|
||||
.attr(MovePowerMultiplierAttr, (user, target, move) => target.status?.effect === StatusEffect.PARALYSIS ? 2 : 1)
|
||||
@ -9537,7 +9528,7 @@ export function initMoves() {
|
||||
new AttackMove(MoveId.SAND_TOMB, PokemonType.GROUND, MoveCategory.PHYSICAL, 35, 85, 15, -1, 0, 3)
|
||||
.attr(TrapAttr, BattlerTagType.SAND_TOMB)
|
||||
.makesContact(false),
|
||||
new AttackMove(MoveId.SHEER_COLD, PokemonType.ICE, MoveCategory.SPECIAL, 200, 30, 5, -1, 0, 3)
|
||||
new AttackMove(MoveId.SHEER_COLD, PokemonType.ICE, MoveCategory.SPECIAL, 250, 30, 5, -1, 0, 3)
|
||||
.attr(IceNoEffectTypeAttr)
|
||||
.attr(OneHitKOAttr)
|
||||
.attr(SheerColdAccuracyAttr),
|
||||
@ -10433,7 +10424,8 @@ export function initMoves() {
|
||||
new AttackMove(MoveId.DAZZLING_GLEAM, PokemonType.FAIRY, MoveCategory.SPECIAL, 80, 100, 10, -1, 0, 6)
|
||||
.target(MoveTarget.ALL_NEAR_ENEMIES),
|
||||
new SelfStatusMove(MoveId.CELEBRATE, PokemonType.NORMAL, -1, 40, -1, 0, 6)
|
||||
.attr(CelebrateAttr),
|
||||
// NB: This needs a lambda function as the user will not be logged in by the time the moves are initialized
|
||||
.attr(MessageAttr, () => i18next.t("moveTriggers:celebrate", { playerName: loggedInUser?.username })),
|
||||
new StatusMove(MoveId.HOLD_HANDS, PokemonType.NORMAL, -1, 40, -1, 0, 6)
|
||||
.ignoresSubstitute()
|
||||
.target(MoveTarget.NEAR_ALLY),
|
||||
@ -10608,7 +10600,12 @@ export function initMoves() {
|
||||
.attr(StatStageChangeAttr, [ Stat.SPD ], -1)
|
||||
.reflectable(),
|
||||
new SelfStatusMove(MoveId.LASER_FOCUS, PokemonType.NORMAL, -1, 30, -1, 0, 7)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.ALWAYS_CRIT, true, false),
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.ALWAYS_CRIT, true, false)
|
||||
.attr(MessageAttr, (user) =>
|
||||
i18next.t("battlerTags:laserFocusOnAdd", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(user),
|
||||
}),
|
||||
),
|
||||
new StatusMove(MoveId.GEAR_UP, PokemonType.STEEL, -1, 20, -1, 0, 7)
|
||||
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, { condition: (user, target, move) => !![ AbilityId.PLUS, AbilityId.MINUS ].find(a => target.hasAbility(a, false)) })
|
||||
.ignoresSubstitute()
|
||||
|
@ -11,7 +11,7 @@ import { BooleanHolder, toDmgValue } from "#utils/common";
|
||||
* These are the moves assigned to a {@linkcode Pokemon} object.
|
||||
* It links to {@linkcode Move} class via the move ID.
|
||||
* Compared to {@linkcode Move}, this class also tracks things like
|
||||
* PP Ups recieved, PP used, etc.
|
||||
* PP Ups received, PP used, etc.
|
||||
* @see {@linkcode isUsable} - checks if move is restricted, out of PP, or not implemented.
|
||||
* @see {@linkcode getMove} - returns {@linkcode Move} object by looking it up via ID.
|
||||
* @see {@linkcode usePp} - removes a point of PP from the move.
|
||||
|
@ -99,6 +99,7 @@ export const DarkDealEncounter: MysteryEncounter = MysteryEncounterBuilder.withE
|
||||
MysteryEncounterType.DARK_DEAL,
|
||||
)
|
||||
.withEncounterTier(MysteryEncounterTier.ROGUE)
|
||||
.withDisallowedChallenges(Challenges.HARDCORE)
|
||||
.withIntroSpriteConfigs([
|
||||
{
|
||||
spriteKey: "dark_deal_scientist",
|
||||
|
@ -673,6 +673,8 @@ export async function catchPokemon(
|
||||
globalScene.gameData.updateSpeciesDexIvs(pokemon.species.getRootSpeciesId(true), pokemon.ivs);
|
||||
|
||||
return new Promise(resolve => {
|
||||
const addStatus = new BooleanHolder(true);
|
||||
applyChallenges(ChallengeType.POKEMON_ADD_TO_PARTY, pokemon, addStatus);
|
||||
const doPokemonCatchMenu = () => {
|
||||
const end = () => {
|
||||
// Ensure the pokemon is in the enemy party in all situations
|
||||
@ -708,9 +710,7 @@ export async function catchPokemon(
|
||||
});
|
||||
};
|
||||
Promise.all([pokemon.hideInfo(), globalScene.gameData.setPokemonCaught(pokemon)]).then(() => {
|
||||
const addStatus = new BooleanHolder(true);
|
||||
applyChallenges(ChallengeType.POKEMON_ADD_TO_PARTY, pokemon, addStatus);
|
||||
if (!addStatus.value) {
|
||||
if (!(isObtain || addStatus.value)) {
|
||||
removePokemon();
|
||||
end();
|
||||
return;
|
||||
@ -807,10 +807,16 @@ export async function catchPokemon(
|
||||
};
|
||||
|
||||
if (showCatchObtainMessage) {
|
||||
let catchMessage: string;
|
||||
if (isObtain) {
|
||||
catchMessage = "battle:pokemonObtained";
|
||||
} else if (addStatus.value) {
|
||||
catchMessage = "battle:pokemonCaught";
|
||||
} else {
|
||||
catchMessage = "battle:pokemonCaughtButChallenge";
|
||||
}
|
||||
globalScene.ui.showText(
|
||||
i18next.t(isObtain ? "battle:pokemonObtained" : "battle:pokemonCaught", {
|
||||
pokemonName: pokemon.getNameToRender(),
|
||||
}),
|
||||
i18next.t(catchMessage, { pokemonName: pokemon.getNameToRender() }),
|
||||
null,
|
||||
doPokemonCatchMenu,
|
||||
0,
|
||||
|
@ -12,6 +12,7 @@ import { WeatherType } from "#enums/weather-type";
|
||||
import type { Pokemon } from "#field/pokemon";
|
||||
import type { PokemonFormChangeItemModifier } from "#modifiers/modifier";
|
||||
import { type Constructor, coerceArray } from "#utils/common";
|
||||
import { toCamelCase } from "#utils/strings";
|
||||
import i18next from "i18next";
|
||||
|
||||
export abstract class SpeciesFormChangeTrigger {
|
||||
@ -143,11 +144,7 @@ export class SpeciesFormChangeMoveLearnedTrigger extends SpeciesFormChangeTrigge
|
||||
super();
|
||||
this.move = move;
|
||||
this.known = known;
|
||||
const moveKey = MoveId[this.move]
|
||||
.split("_")
|
||||
.filter(f => f)
|
||||
.map((f, i) => (i ? `${f[0]}${f.slice(1).toLowerCase()}` : f.toLowerCase()))
|
||||
.join("") as unknown as string;
|
||||
const moveKey = toCamelCase(MoveId[this.move]);
|
||||
this.description = known
|
||||
? i18next.t("pokemonEvolutions:Forms.moveLearned", {
|
||||
move: i18next.t(`move:${moveKey}.name`),
|
||||
|
@ -1865,27 +1865,43 @@ export const trainerConfigs: TrainerConfigs = {
|
||||
.setPartyMemberFunc(
|
||||
0,
|
||||
getRandomPartyMemberFunc([
|
||||
SpeciesId.METAPOD,
|
||||
SpeciesId.LEDYBA,
|
||||
SpeciesId.CLEFFA,
|
||||
SpeciesId.WOOPER,
|
||||
SpeciesId.TEDDIURSA,
|
||||
SpeciesId.REMORAID,
|
||||
SpeciesId.HOUNDOUR,
|
||||
SpeciesId.SILCOON,
|
||||
SpeciesId.PLUSLE,
|
||||
SpeciesId.VOLBEAT,
|
||||
SpeciesId.PACHIRISU,
|
||||
SpeciesId.SILCOON,
|
||||
SpeciesId.METAPOD,
|
||||
SpeciesId.IGGLYBUFF,
|
||||
SpeciesId.SPINDA,
|
||||
SpeciesId.BONSLY,
|
||||
SpeciesId.PETILIL,
|
||||
SpeciesId.EEVEE,
|
||||
SpeciesId.SPRITZEE,
|
||||
SpeciesId.MILCERY,
|
||||
SpeciesId.PICHU,
|
||||
]),
|
||||
)
|
||||
.setPartyMemberFunc(
|
||||
1,
|
||||
getRandomPartyMemberFunc(
|
||||
[
|
||||
SpeciesId.KAKUNA,
|
||||
SpeciesId.SPINARAK,
|
||||
SpeciesId.IGGLYBUFF,
|
||||
SpeciesId.PALDEA_WOOPER,
|
||||
SpeciesId.PHANPY,
|
||||
SpeciesId.MANTYKE,
|
||||
SpeciesId.ELECTRIKE,
|
||||
SpeciesId.CASCOON,
|
||||
SpeciesId.MINUN,
|
||||
SpeciesId.ILLUMISE,
|
||||
SpeciesId.EMOLGA,
|
||||
SpeciesId.CASCOON,
|
||||
SpeciesId.KAKUNA,
|
||||
SpeciesId.CLEFFA,
|
||||
SpeciesId.SPINDA,
|
||||
SpeciesId.MIME_JR,
|
||||
SpeciesId.COTTONEE,
|
||||
SpeciesId.SWIRLIX,
|
||||
SpeciesId.FIDOUGH,
|
||||
SpeciesId.EEVEE,
|
||||
],
|
||||
TrainerSlot.TRAINER_PARTNER,
|
||||
|
@ -38,6 +38,7 @@ export enum UiMode {
|
||||
UNAVAILABLE,
|
||||
CHALLENGE_SELECT,
|
||||
RENAME_POKEMON,
|
||||
RENAME_RUN,
|
||||
RUN_HISTORY,
|
||||
RUN_INFO,
|
||||
TEST_DIALOGUE,
|
||||
|
@ -54,7 +54,7 @@ export class Arena {
|
||||
public bgm: string;
|
||||
public ignoreAbilities: boolean;
|
||||
public ignoringEffectSource: BattlerIndex | null;
|
||||
public playerTerasUsed: number;
|
||||
public playerTerasUsed = 0;
|
||||
/**
|
||||
* Saves the number of times a party pokemon faints during a arena encounter.
|
||||
* {@linkcode globalScene.currentBattle.enemyFaints} is the corresponding faint counter for the enemy (this resets every wave).
|
||||
@ -68,12 +68,11 @@ export class Arena {
|
||||
|
||||
public readonly eventTarget: EventTarget = new EventTarget();
|
||||
|
||||
constructor(biome: BiomeId, bgm: string, playerFaints = 0) {
|
||||
constructor(biome: BiomeId, playerFaints = 0) {
|
||||
this.biomeType = biome;
|
||||
this.bgm = bgm;
|
||||
this.bgm = BiomeId[biome].toLowerCase();
|
||||
this.trainerPool = biomeTrainerPools[biome];
|
||||
this.updatePoolsForTimeOfDay();
|
||||
this.playerTerasUsed = 0;
|
||||
this.playerFaints = playerFaints;
|
||||
}
|
||||
|
||||
@ -895,7 +894,7 @@ export class Arena {
|
||||
case BiomeId.CAVE:
|
||||
return 14.24;
|
||||
case BiomeId.DESERT:
|
||||
return 1.143;
|
||||
return 9.02;
|
||||
case BiomeId.ICE_CAVE:
|
||||
return 0.0;
|
||||
case BiomeId.MEADOW:
|
||||
@ -923,7 +922,7 @@ export class Arena {
|
||||
case BiomeId.JUNGLE:
|
||||
return 0.0;
|
||||
case BiomeId.FAIRY_CAVE:
|
||||
return 4.542;
|
||||
return 0.0;
|
||||
case BiomeId.TEMPLE:
|
||||
return 2.547;
|
||||
case BiomeId.ISLAND:
|
||||
|
@ -1,7 +1,7 @@
|
||||
import type { Ability, PreAttackModifyDamageAbAttrParams } from "#abilities/ability";
|
||||
import { applyAbAttrs, applyOnGainAbAttrs, applyOnLoseAbAttrs } from "#abilities/apply-ab-attrs";
|
||||
import type { AnySound, BattleScene } from "#app/battle-scene";
|
||||
import { PLAYER_PARTY_MAX_SIZE } from "#app/constants";
|
||||
import { PLAYER_PARTY_MAX_SIZE, RARE_CANDY_FRIENDSHIP_CAP } from "#app/constants";
|
||||
import { timedEventManager } from "#app/global-event-manager";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
@ -139,6 +139,8 @@ import { populateVariantColors, variantColorCache, variantData } from "#sprites/
|
||||
import { achvs } from "#system/achv";
|
||||
import type { StarterDataEntry, StarterMoveset } from "#system/game-data";
|
||||
import type { PokemonData } from "#system/pokemon-data";
|
||||
import { RibbonData } from "#system/ribbons/ribbon-data";
|
||||
import { awardRibbonsToSpeciesLine } from "#system/ribbons/ribbon-methods";
|
||||
import type { AbAttrMap, AbAttrString, TypeMultiplierAbAttrParams } from "#types/ability-types";
|
||||
import type { DamageCalculationResult, DamageResult } from "#types/damage-result";
|
||||
import type { IllusionData } from "#types/illusion-data";
|
||||
@ -1825,7 +1827,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
// Overrides moveset based on arrays specified in overrides.ts
|
||||
let overrideArray: MoveId | Array<MoveId> = this.isPlayer()
|
||||
? Overrides.MOVESET_OVERRIDE
|
||||
: Overrides.OPP_MOVESET_OVERRIDE;
|
||||
: Overrides.ENEMY_MOVESET_OVERRIDE;
|
||||
overrideArray = coerceArray(overrideArray);
|
||||
if (overrideArray.length > 0) {
|
||||
if (!this.isPlayer()) {
|
||||
@ -2030,8 +2032,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
if (Overrides.ABILITY_OVERRIDE && this.isPlayer()) {
|
||||
return allAbilities[Overrides.ABILITY_OVERRIDE];
|
||||
}
|
||||
if (Overrides.OPP_ABILITY_OVERRIDE && this.isEnemy()) {
|
||||
return allAbilities[Overrides.OPP_ABILITY_OVERRIDE];
|
||||
if (Overrides.ENEMY_ABILITY_OVERRIDE && this.isEnemy()) {
|
||||
return allAbilities[Overrides.ENEMY_ABILITY_OVERRIDE];
|
||||
}
|
||||
if (this.isFusion()) {
|
||||
if (!isNullOrUndefined(this.fusionCustomPokemonData?.ability) && this.fusionCustomPokemonData.ability !== -1) {
|
||||
@ -2060,8 +2062,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
if (Overrides.PASSIVE_ABILITY_OVERRIDE && this.isPlayer()) {
|
||||
return allAbilities[Overrides.PASSIVE_ABILITY_OVERRIDE];
|
||||
}
|
||||
if (Overrides.OPP_PASSIVE_ABILITY_OVERRIDE && this.isEnemy()) {
|
||||
return allAbilities[Overrides.OPP_PASSIVE_ABILITY_OVERRIDE];
|
||||
if (Overrides.ENEMY_PASSIVE_ABILITY_OVERRIDE && this.isEnemy()) {
|
||||
return allAbilities[Overrides.ENEMY_PASSIVE_ABILITY_OVERRIDE];
|
||||
}
|
||||
if (!isNullOrUndefined(this.customPokemonData.passive) && this.customPokemonData.passive !== -1) {
|
||||
return allAbilities[this.customPokemonData.passive];
|
||||
@ -2128,14 +2130,14 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
// returns override if valid for current case
|
||||
if (
|
||||
(Overrides.HAS_PASSIVE_ABILITY_OVERRIDE === false && this.isPlayer()) ||
|
||||
(Overrides.OPP_HAS_PASSIVE_ABILITY_OVERRIDE === false && this.isEnemy())
|
||||
(Overrides.ENEMY_HAS_PASSIVE_ABILITY_OVERRIDE === false && this.isEnemy())
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
((Overrides.PASSIVE_ABILITY_OVERRIDE !== AbilityId.NONE || Overrides.HAS_PASSIVE_ABILITY_OVERRIDE) &&
|
||||
this.isPlayer()) ||
|
||||
((Overrides.OPP_PASSIVE_ABILITY_OVERRIDE !== AbilityId.NONE || Overrides.OPP_HAS_PASSIVE_ABILITY_OVERRIDE) &&
|
||||
((Overrides.ENEMY_PASSIVE_ABILITY_OVERRIDE !== AbilityId.NONE || Overrides.ENEMY_HAS_PASSIVE_ABILITY_OVERRIDE) &&
|
||||
this.isEnemy())
|
||||
) {
|
||||
return true;
|
||||
@ -3001,8 +3003,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
|
||||
if (forStarter && this.isPlayer() && Overrides.STARTER_FUSION_SPECIES_OVERRIDE) {
|
||||
fusionOverride = getPokemonSpecies(Overrides.STARTER_FUSION_SPECIES_OVERRIDE);
|
||||
} else if (this.isEnemy() && Overrides.OPP_FUSION_SPECIES_OVERRIDE) {
|
||||
fusionOverride = getPokemonSpecies(Overrides.OPP_FUSION_SPECIES_OVERRIDE);
|
||||
} else if (this.isEnemy() && Overrides.ENEMY_FUSION_SPECIES_OVERRIDE) {
|
||||
fusionOverride = getPokemonSpecies(Overrides.ENEMY_FUSION_SPECIES_OVERRIDE);
|
||||
}
|
||||
|
||||
this.fusionSpecies =
|
||||
@ -5822,45 +5824,59 @@ export class PlayerPokemon extends Pokemon {
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
addFriendship(friendship: number): void {
|
||||
if (friendship > 0) {
|
||||
const starterSpeciesId = this.species.getRootSpeciesId();
|
||||
const fusionStarterSpeciesId = this.isFusion() && this.fusionSpecies ? this.fusionSpecies.getRootSpeciesId() : 0;
|
||||
const starterData = [
|
||||
globalScene.gameData.starterData[starterSpeciesId],
|
||||
fusionStarterSpeciesId ? globalScene.gameData.starterData[fusionStarterSpeciesId] : null,
|
||||
].filter(d => !!d);
|
||||
const amount = new NumberHolder(friendship);
|
||||
globalScene.applyModifier(PokemonFriendshipBoosterModifier, true, this, amount);
|
||||
const candyFriendshipMultiplier = globalScene.gameMode.isClassic
|
||||
? timedEventManager.getClassicFriendshipMultiplier()
|
||||
: 1;
|
||||
const fusionReduction = fusionStarterSpeciesId
|
||||
? timedEventManager.areFusionsBoosted()
|
||||
? 1.5 // Divide candy gain for fusions by 1.5 during events
|
||||
: 2 // 2 for fusions outside events
|
||||
: 1; // 1 for non-fused mons
|
||||
const starterAmount = new NumberHolder(Math.floor((amount.value * candyFriendshipMultiplier) / fusionReduction));
|
||||
|
||||
// Add friendship to this PlayerPokemon
|
||||
this.friendship = Math.min(this.friendship + amount.value, 255);
|
||||
if (this.friendship === 255) {
|
||||
globalScene.validateAchv(achvs.MAX_FRIENDSHIP);
|
||||
}
|
||||
// Add to candy progress for this mon's starter species and its fused species (if it has one)
|
||||
starterData.forEach((sd: StarterDataEntry, i: number) => {
|
||||
const speciesId = !i ? starterSpeciesId : (fusionStarterSpeciesId as SpeciesId);
|
||||
sd.friendship = (sd.friendship || 0) + starterAmount.value;
|
||||
if (sd.friendship >= getStarterValueFriendshipCap(speciesStarterCosts[speciesId])) {
|
||||
globalScene.gameData.addStarterCandy(getPokemonSpecies(speciesId), 1);
|
||||
sd.friendship = 0;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Lose friendship upon fainting
|
||||
/**
|
||||
* Add friendship to this Pokemon
|
||||
*
|
||||
* @remarks
|
||||
* This adds friendship to the pokemon's friendship stat (used for evolution, return, etc.) and candy progress.
|
||||
* For fusions, candy progress for each species in the fusion is halved.
|
||||
*
|
||||
* @param friendship - The amount of friendship to add. Negative values will reduce friendship, though not below 0.
|
||||
* @param capped - If true, don't allow the friendship gain to exceed 200. Used to cap friendship gains from rare candies.
|
||||
*/
|
||||
addFriendship(friendship: number, capped = false): void {
|
||||
// Short-circuit friendship loss, which doesn't impact candy friendship
|
||||
if (friendship <= 0) {
|
||||
this.friendship = Math.max(this.friendship + friendship, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
const starterSpeciesId = this.species.getRootSpeciesId();
|
||||
const fusionStarterSpeciesId = this.isFusion() && this.fusionSpecies ? this.fusionSpecies.getRootSpeciesId() : 0;
|
||||
const starterGameData = globalScene.gameData.starterData;
|
||||
const starterData: [StarterDataEntry, SpeciesId][] = [[starterGameData[starterSpeciesId], starterSpeciesId]];
|
||||
if (fusionStarterSpeciesId) {
|
||||
starterData.push([starterGameData[fusionStarterSpeciesId], fusionStarterSpeciesId]);
|
||||
}
|
||||
const amount = new NumberHolder(friendship);
|
||||
globalScene.applyModifier(PokemonFriendshipBoosterModifier, true, this, amount);
|
||||
friendship = amount.value;
|
||||
|
||||
const newFriendship = this.friendship + friendship;
|
||||
// If capped is true, only adjust friendship if the new friendship is less than or equal to 200.
|
||||
if (!capped || newFriendship <= RARE_CANDY_FRIENDSHIP_CAP) {
|
||||
this.friendship = Math.min(newFriendship, 255);
|
||||
if (newFriendship >= 255) {
|
||||
globalScene.validateAchv(achvs.MAX_FRIENDSHIP);
|
||||
awardRibbonsToSpeciesLine(this.species.speciesId, RibbonData.FRIENDSHIP);
|
||||
}
|
||||
}
|
||||
|
||||
let candyFriendshipMultiplier = globalScene.gameMode.isClassic
|
||||
? timedEventManager.getClassicFriendshipMultiplier()
|
||||
: 1;
|
||||
if (fusionStarterSpeciesId) {
|
||||
candyFriendshipMultiplier /= timedEventManager.areFusionsBoosted() ? 1.5 : 2;
|
||||
}
|
||||
const candyFriendshipAmount = Math.floor(friendship * candyFriendshipMultiplier);
|
||||
// Add to candy progress for this mon's starter species and its fused species (if it has one)
|
||||
starterData.forEach(([sd, id]: [StarterDataEntry, SpeciesId]) => {
|
||||
sd.friendship = (sd.friendship || 0) + candyFriendshipAmount;
|
||||
if (sd.friendship >= getStarterValueFriendshipCap(speciesStarterCosts[id])) {
|
||||
globalScene.gameData.addStarterCandy(getPokemonSpecies(id), 1);
|
||||
sd.friendship = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getPossibleEvolution(evolution: SpeciesFormEvolution | null): Promise<Pokemon> {
|
||||
@ -6241,22 +6257,22 @@ export class EnemyPokemon extends Pokemon {
|
||||
this.setBoss(boss, dataSource?.bossSegments);
|
||||
}
|
||||
|
||||
if (Overrides.OPP_STATUS_OVERRIDE) {
|
||||
this.status = new Status(Overrides.OPP_STATUS_OVERRIDE, 0, 4);
|
||||
if (Overrides.ENEMY_STATUS_OVERRIDE) {
|
||||
this.status = new Status(Overrides.ENEMY_STATUS_OVERRIDE, 0, 4);
|
||||
}
|
||||
|
||||
if (Overrides.OPP_GENDER_OVERRIDE !== null) {
|
||||
this.gender = Overrides.OPP_GENDER_OVERRIDE;
|
||||
if (Overrides.ENEMY_GENDER_OVERRIDE !== null) {
|
||||
this.gender = Overrides.ENEMY_GENDER_OVERRIDE;
|
||||
}
|
||||
|
||||
const speciesId = this.species.speciesId;
|
||||
|
||||
if (
|
||||
speciesId in Overrides.OPP_FORM_OVERRIDES &&
|
||||
!isNullOrUndefined(Overrides.OPP_FORM_OVERRIDES[speciesId]) &&
|
||||
this.species.forms[Overrides.OPP_FORM_OVERRIDES[speciesId]]
|
||||
speciesId in Overrides.ENEMY_FORM_OVERRIDES &&
|
||||
!isNullOrUndefined(Overrides.ENEMY_FORM_OVERRIDES[speciesId]) &&
|
||||
this.species.forms[Overrides.ENEMY_FORM_OVERRIDES[speciesId]]
|
||||
) {
|
||||
this.formIndex = Overrides.OPP_FORM_OVERRIDES[speciesId];
|
||||
this.formIndex = Overrides.ENEMY_FORM_OVERRIDES[speciesId];
|
||||
} else if (globalScene.gameMode.isDaily && globalScene.gameMode.isWaveFinal(globalScene.currentBattle.waveIndex)) {
|
||||
const eventBoss = getDailyEventSeedBoss(globalScene.seed);
|
||||
if (!isNullOrUndefined(eventBoss)) {
|
||||
@ -6266,21 +6282,21 @@ export class EnemyPokemon extends Pokemon {
|
||||
|
||||
if (!dataSource) {
|
||||
this.generateAndPopulateMoveset();
|
||||
if (shinyLock || Overrides.OPP_SHINY_OVERRIDE === false) {
|
||||
if (shinyLock || Overrides.ENEMY_SHINY_OVERRIDE === false) {
|
||||
this.shiny = false;
|
||||
} else {
|
||||
this.trySetShiny();
|
||||
}
|
||||
|
||||
if (!this.shiny && Overrides.OPP_SHINY_OVERRIDE) {
|
||||
if (!this.shiny && Overrides.ENEMY_SHINY_OVERRIDE) {
|
||||
this.shiny = true;
|
||||
this.initShinySparkle();
|
||||
}
|
||||
|
||||
if (this.shiny) {
|
||||
this.variant = this.generateShinyVariant();
|
||||
if (Overrides.OPP_VARIANT_OVERRIDE !== null) {
|
||||
this.variant = Overrides.OPP_VARIANT_OVERRIDE;
|
||||
if (Overrides.ENEMY_VARIANT_OVERRIDE !== null) {
|
||||
this.variant = Overrides.ENEMY_VARIANT_OVERRIDE;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -90,6 +90,7 @@ export class LoadingScene extends SceneBase {
|
||||
this.loadAtlas("shiny_icons", "ui");
|
||||
this.loadImage("ha_capsule", "ui", "ha_capsule.png");
|
||||
this.loadImage("champion_ribbon", "ui", "champion_ribbon.png");
|
||||
this.loadImage("champion_ribbon_emerald", "ui", "champion_ribbon_emerald.png");
|
||||
this.loadImage("icon_spliced", "ui");
|
||||
this.loadImage("icon_lock", "ui", "icon_lock.png");
|
||||
this.loadImage("icon_stop", "ui", "icon_stop.png");
|
||||
@ -122,6 +123,7 @@ export class LoadingScene extends SceneBase {
|
||||
this.loadImage("party_bg_double", "ui");
|
||||
this.loadImage("party_bg_double_manage", "ui");
|
||||
this.loadAtlas("party_slot_main", "ui");
|
||||
this.loadAtlas("party_slot_main_short", "ui");
|
||||
this.loadAtlas("party_slot", "ui");
|
||||
this.loadImage("party_slot_overlay_lv", "ui");
|
||||
this.loadImage("party_slot_hp_bar", "ui");
|
||||
@ -447,7 +449,9 @@ export class LoadingScene extends SceneBase {
|
||||
);
|
||||
|
||||
if (!mobile) {
|
||||
loadingGraphics.map(g => g.setVisible(false));
|
||||
loadingGraphics.forEach(g => {
|
||||
g.setVisible(false);
|
||||
});
|
||||
}
|
||||
|
||||
const intro = this.add.video(0, 0);
|
||||
|
@ -121,8 +121,8 @@ export class ModifierBar extends Phaser.GameObjects.Container {
|
||||
}
|
||||
|
||||
updateModifierOverflowVisibility(ignoreLimit: boolean) {
|
||||
const modifierIcons = this.getAll().reverse();
|
||||
for (const modifier of modifierIcons.map(m => m as Phaser.GameObjects.Container).slice(iconOverflowIndex)) {
|
||||
const modifierIcons = this.getAll().reverse() as Phaser.GameObjects.Container[];
|
||||
for (const modifier of modifierIcons.slice(iconOverflowIndex)) {
|
||||
modifier.setVisible(ignoreLimit);
|
||||
}
|
||||
}
|
||||
@ -2304,7 +2304,7 @@ export class PokemonLevelIncrementModifier extends ConsumablePokemonModifier {
|
||||
playerPokemon.levelExp = 0;
|
||||
}
|
||||
|
||||
playerPokemon.addFriendship(FRIENDSHIP_GAIN_FROM_RARE_CANDY);
|
||||
playerPokemon.addFriendship(FRIENDSHIP_GAIN_FROM_RARE_CANDY, true);
|
||||
|
||||
globalScene.phaseManager.unshiftNew(
|
||||
"LevelUpPhase",
|
||||
@ -3755,7 +3755,7 @@ export class EnemyFusionChanceModifier extends EnemyPersistentModifier {
|
||||
export function overrideModifiers(isPlayer = true): void {
|
||||
const modifiersOverride: ModifierOverride[] = isPlayer
|
||||
? Overrides.STARTING_MODIFIER_OVERRIDE
|
||||
: Overrides.OPP_MODIFIER_OVERRIDE;
|
||||
: Overrides.ENEMY_MODIFIER_OVERRIDE;
|
||||
if (!modifiersOverride || modifiersOverride.length === 0 || !globalScene) {
|
||||
return;
|
||||
}
|
||||
@ -3797,7 +3797,7 @@ export function overrideModifiers(isPlayer = true): void {
|
||||
export function overrideHeldItems(pokemon: Pokemon, isPlayer = true): void {
|
||||
const heldItemsOverride: ModifierOverride[] = isPlayer
|
||||
? Overrides.STARTING_HELD_ITEMS_OVERRIDE
|
||||
: Overrides.OPP_HELD_ITEMS_OVERRIDE;
|
||||
: Overrides.ENEMY_HELD_ITEMS_OVERRIDE;
|
||||
if (!heldItemsOverride || heldItemsOverride.length === 0 || !globalScene) {
|
||||
return;
|
||||
}
|
||||
|
@ -179,25 +179,24 @@ class DefaultOverrides {
|
||||
// --------------------------
|
||||
// OPPONENT / ENEMY OVERRIDES
|
||||
// --------------------------
|
||||
// TODO: rename `OPP_` to `ENEMY_`
|
||||
readonly OPP_SPECIES_OVERRIDE: SpeciesId | number = 0;
|
||||
readonly ENEMY_SPECIES_OVERRIDE: SpeciesId | number = 0;
|
||||
/**
|
||||
* This will make all opponents fused Pokemon
|
||||
*/
|
||||
readonly OPP_FUSION_OVERRIDE: boolean = false;
|
||||
readonly ENEMY_FUSION_OVERRIDE: boolean = false;
|
||||
/**
|
||||
* This will override the species of the fusion only when the opponent is already a fusion
|
||||
*/
|
||||
readonly OPP_FUSION_SPECIES_OVERRIDE: SpeciesId | number = 0;
|
||||
readonly OPP_LEVEL_OVERRIDE: number = 0;
|
||||
readonly OPP_ABILITY_OVERRIDE: AbilityId = AbilityId.NONE;
|
||||
readonly OPP_PASSIVE_ABILITY_OVERRIDE: AbilityId = AbilityId.NONE;
|
||||
readonly OPP_HAS_PASSIVE_ABILITY_OVERRIDE: boolean | null = null;
|
||||
readonly OPP_STATUS_OVERRIDE: StatusEffect = StatusEffect.NONE;
|
||||
readonly OPP_GENDER_OVERRIDE: Gender | null = null;
|
||||
readonly OPP_MOVESET_OVERRIDE: MoveId | Array<MoveId> = [];
|
||||
readonly OPP_SHINY_OVERRIDE: boolean | null = null;
|
||||
readonly OPP_VARIANT_OVERRIDE: Variant | null = null;
|
||||
readonly ENEMY_FUSION_SPECIES_OVERRIDE: SpeciesId | number = 0;
|
||||
readonly ENEMY_LEVEL_OVERRIDE: number = 0;
|
||||
readonly ENEMY_ABILITY_OVERRIDE: AbilityId = AbilityId.NONE;
|
||||
readonly ENEMY_PASSIVE_ABILITY_OVERRIDE: AbilityId = AbilityId.NONE;
|
||||
readonly ENEMY_HAS_PASSIVE_ABILITY_OVERRIDE: boolean | null = null;
|
||||
readonly ENEMY_STATUS_OVERRIDE: StatusEffect = StatusEffect.NONE;
|
||||
readonly ENEMY_GENDER_OVERRIDE: Gender | null = null;
|
||||
readonly ENEMY_MOVESET_OVERRIDE: MoveId | Array<MoveId> = [];
|
||||
readonly ENEMY_SHINY_OVERRIDE: boolean | null = null;
|
||||
readonly ENEMY_VARIANT_OVERRIDE: Variant | null = null;
|
||||
/**
|
||||
* Overrides the IVs of enemy pokemon. Values must never be outside the range `0` to `31`!
|
||||
* - If set to a number between `0` and `31`, set all IVs of all enemy pokemon to that number.
|
||||
@ -207,7 +206,7 @@ class DefaultOverrides {
|
||||
readonly ENEMY_IVS_OVERRIDE: number | number[] | null = null;
|
||||
/** Override the nature of all enemy pokemon to the specified nature. Disabled if `null`. */
|
||||
readonly ENEMY_NATURE_OVERRIDE: Nature | null = null;
|
||||
readonly OPP_FORM_OVERRIDES: Partial<Record<SpeciesId, number>> = {};
|
||||
readonly ENEMY_FORM_OVERRIDES: Partial<Record<SpeciesId, number>> = {};
|
||||
/**
|
||||
* Override to give the enemy Pokemon a given amount of health segments
|
||||
*
|
||||
@ -215,7 +214,7 @@ class DefaultOverrides {
|
||||
* 1: the Pokemon will have a single health segment and therefore will not be a boss
|
||||
* 2+: the Pokemon will be a boss with the given number of health segments
|
||||
*/
|
||||
readonly OPP_HEALTH_SEGMENTS_OVERRIDE: number = 0;
|
||||
readonly ENEMY_HEALTH_SEGMENTS_OVERRIDE: number = 0;
|
||||
|
||||
// -------------
|
||||
// EGG OVERRIDES
|
||||
@ -277,12 +276,12 @@ class DefaultOverrides {
|
||||
*
|
||||
* Note that any previous modifiers are cleared.
|
||||
*/
|
||||
readonly OPP_MODIFIER_OVERRIDE: ModifierOverride[] = [];
|
||||
readonly ENEMY_MODIFIER_OVERRIDE: ModifierOverride[] = [];
|
||||
|
||||
/** Override array of {@linkcode ModifierOverride}s used to provide held items to first party member when starting a new game. */
|
||||
readonly STARTING_HELD_ITEMS_OVERRIDE: ModifierOverride[] = [];
|
||||
/** Override array of {@linkcode ModifierOverride}s used to provide held items to enemies on spawn. */
|
||||
readonly OPP_HELD_ITEMS_OVERRIDE: ModifierOverride[] = [];
|
||||
readonly ENEMY_HELD_ITEMS_OVERRIDE: ModifierOverride[] = [];
|
||||
|
||||
/**
|
||||
* Override array of {@linkcode ModifierOverride}s used to replace the generated item rolls after a wave.
|
||||
|
@ -253,8 +253,11 @@ export class AttemptCapturePhase extends PokemonPhase {
|
||||
|
||||
globalScene.gameData.updateSpeciesDexIvs(pokemon.species.getRootSpeciesId(true), pokemon.ivs);
|
||||
|
||||
const addStatus = new BooleanHolder(true);
|
||||
applyChallenges(ChallengeType.POKEMON_ADD_TO_PARTY, pokemon, addStatus);
|
||||
|
||||
globalScene.ui.showText(
|
||||
i18next.t("battle:pokemonCaught", {
|
||||
i18next.t(addStatus.value ? "battle:pokemonCaught" : "battle:pokemonCaughtButChallenge", {
|
||||
pokemonName: getPokemonNameWithAffix(pokemon),
|
||||
}),
|
||||
null,
|
||||
@ -290,8 +293,6 @@ export class AttemptCapturePhase extends PokemonPhase {
|
||||
});
|
||||
};
|
||||
Promise.all([pokemon.hideInfo(), globalScene.gameData.setPokemonCaught(pokemon)]).then(() => {
|
||||
const addStatus = new BooleanHolder(true);
|
||||
applyChallenges(ChallengeType.POKEMON_ADD_TO_PARTY, pokemon, addStatus);
|
||||
if (!addStatus.value) {
|
||||
removePokemon();
|
||||
end();
|
||||
|
@ -229,7 +229,7 @@ export class EncounterPhase extends BattlePhase {
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
const overridedBossSegments = Overrides.OPP_HEALTH_SEGMENTS_OVERRIDE > 1;
|
||||
const overridedBossSegments = Overrides.ENEMY_HEALTH_SEGMENTS_OVERRIDE > 1;
|
||||
// for double battles, reduce the health segments for boss Pokemon unless there is an override
|
||||
if (!overridedBossSegments && battle.enemyParty.filter(p => p.isBoss()).length > 1) {
|
||||
for (const enemyPokemon of battle.enemyParty) {
|
||||
|
@ -19,8 +19,11 @@ import { ChallengeData } from "#system/challenge-data";
|
||||
import type { SessionSaveData } from "#system/game-data";
|
||||
import { ModifierData as PersistentModifierData } from "#system/modifier-data";
|
||||
import { PokemonData } from "#system/pokemon-data";
|
||||
import { RibbonData, type RibbonFlag } from "#system/ribbons/ribbon-data";
|
||||
import { awardRibbonsToSpeciesLine } from "#system/ribbons/ribbon-methods";
|
||||
import { TrainerData } from "#system/trainer-data";
|
||||
import { trainerConfigs } from "#trainers/trainer-config";
|
||||
import { checkSpeciesValidForChallenge, isNuzlockeChallenge } from "#utils/challenge-utils";
|
||||
import { isLocal, isLocalServerConnected } from "#utils/common";
|
||||
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
||||
import i18next from "i18next";
|
||||
@ -111,6 +114,40 @@ export class GameOverPhase extends BattlePhase {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Submethod of {@linkcode handleGameOver} that awards ribbons to Pokémon in the player's party based on the current
|
||||
* game mode and challenges.
|
||||
*/
|
||||
private awardRibbons(): void {
|
||||
let ribbonFlags = 0;
|
||||
if (globalScene.gameMode.isClassic) {
|
||||
ribbonFlags |= RibbonData.CLASSIC;
|
||||
}
|
||||
if (isNuzlockeChallenge()) {
|
||||
ribbonFlags |= RibbonData.NUZLOCKE;
|
||||
}
|
||||
for (const challenge of globalScene.gameMode.challenges) {
|
||||
const ribbon = challenge.ribbonAwarded;
|
||||
if (challenge.value && ribbon) {
|
||||
ribbonFlags |= ribbon;
|
||||
}
|
||||
}
|
||||
// Award ribbons to all Pokémon in the player's party that are considered valid
|
||||
// for the current game mode and challenges.
|
||||
for (const pokemon of globalScene.getPlayerParty()) {
|
||||
const species = pokemon.species;
|
||||
if (
|
||||
checkSpeciesValidForChallenge(
|
||||
species,
|
||||
globalScene.gameData.getSpeciesDexAttrProps(species, pokemon.getDexAttr()),
|
||||
false,
|
||||
)
|
||||
) {
|
||||
awardRibbonsToSpeciesLine(species.speciesId, ribbonFlags as RibbonFlag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleGameOver(): void {
|
||||
const doGameOver = (newClear: boolean) => {
|
||||
globalScene.disableMenu = true;
|
||||
@ -122,12 +159,12 @@ export class GameOverPhase extends BattlePhase {
|
||||
globalScene.validateAchv(achvs.UNEVOLVED_CLASSIC_VICTORY);
|
||||
globalScene.gameData.gameStats.sessionsWon++;
|
||||
for (const pokemon of globalScene.getPlayerParty()) {
|
||||
this.awardRibbon(pokemon);
|
||||
|
||||
this.awardFirstClassicCompletion(pokemon);
|
||||
if (pokemon.species.getRootSpeciesId() !== pokemon.species.getRootSpeciesId(true)) {
|
||||
this.awardRibbon(pokemon, true);
|
||||
this.awardFirstClassicCompletion(pokemon, true);
|
||||
}
|
||||
}
|
||||
this.awardRibbons();
|
||||
} else if (globalScene.gameMode.isDaily && newClear) {
|
||||
globalScene.gameData.gameStats.dailyRunSessionsWon++;
|
||||
}
|
||||
@ -263,7 +300,7 @@ export class GameOverPhase extends BattlePhase {
|
||||
}
|
||||
}
|
||||
|
||||
awardRibbon(pokemon: Pokemon, forStarter = false): void {
|
||||
awardFirstClassicCompletion(pokemon: Pokemon, forStarter = false): void {
|
||||
const speciesId = getPokemonSpecies(pokemon.species.speciesId);
|
||||
const speciesRibbonCount = globalScene.gameData.incrementRibbonCount(speciesId, forStarter);
|
||||
// first time classic win, award voucher
|
||||
|
@ -16,8 +16,10 @@ export class SelectBiomePhase extends BattlePhase {
|
||||
|
||||
globalScene.resetSeed();
|
||||
|
||||
const gameMode = globalScene.gameMode;
|
||||
const currentBiome = globalScene.arena.biomeType;
|
||||
const nextWaveIndex = globalScene.currentBattle.waveIndex + 1;
|
||||
const currentWaveIndex = globalScene.currentBattle.waveIndex;
|
||||
const nextWaveIndex = currentWaveIndex + 1;
|
||||
|
||||
const setNextBiome = (nextBiome: BiomeId) => {
|
||||
if (nextWaveIndex % 10 === 1) {
|
||||
@ -26,6 +28,15 @@ export class SelectBiomePhase extends BattlePhase {
|
||||
applyChallenges(ChallengeType.PARTY_HEAL, healStatus);
|
||||
if (healStatus.value) {
|
||||
globalScene.phaseManager.unshiftNew("PartyHealPhase", false);
|
||||
} else {
|
||||
globalScene.phaseManager.unshiftNew(
|
||||
"SelectModifierPhase",
|
||||
undefined,
|
||||
undefined,
|
||||
gameMode.isFixedBattle(currentWaveIndex)
|
||||
? gameMode.getFixedBattle(currentWaveIndex).customModifierRewardSettings
|
||||
: undefined,
|
||||
);
|
||||
}
|
||||
}
|
||||
globalScene.phaseManager.unshiftNew("SwitchBiomePhase", nextBiome);
|
||||
@ -33,12 +44,12 @@ export class SelectBiomePhase extends BattlePhase {
|
||||
};
|
||||
|
||||
if (
|
||||
(globalScene.gameMode.isClassic && globalScene.gameMode.isWaveFinal(nextWaveIndex + 9)) ||
|
||||
(globalScene.gameMode.isDaily && globalScene.gameMode.isWaveFinal(nextWaveIndex)) ||
|
||||
(globalScene.gameMode.hasShortBiomes && !(nextWaveIndex % 50))
|
||||
(gameMode.isClassic && gameMode.isWaveFinal(nextWaveIndex + 9)) ||
|
||||
(gameMode.isDaily && gameMode.isWaveFinal(nextWaveIndex)) ||
|
||||
(gameMode.hasShortBiomes && !(nextWaveIndex % 50))
|
||||
) {
|
||||
setNextBiome(BiomeId.END);
|
||||
} else if (globalScene.gameMode.hasRandomBiomes) {
|
||||
} else if (gameMode.hasRandomBiomes) {
|
||||
setNextBiome(this.generateNextBiome(nextWaveIndex));
|
||||
} else if (Array.isArray(biomeLinks[currentBiome])) {
|
||||
const biomes: BiomeId[] = (biomeLinks[currentBiome] as (BiomeId | [BiomeId, number])[])
|
||||
@ -73,9 +84,6 @@ export class SelectBiomePhase extends BattlePhase {
|
||||
}
|
||||
|
||||
generateNextBiome(waveIndex: number): BiomeId {
|
||||
if (!(waveIndex % 50)) {
|
||||
return BiomeId.END;
|
||||
}
|
||||
return globalScene.generateRandomBiome(waveIndex);
|
||||
return waveIndex % 50 === 0 ? BiomeId.END : globalScene.generateRandomBiome(waveIndex);
|
||||
}
|
||||
}
|
||||
|
@ -3,13 +3,9 @@ import { globalScene } from "#app/global-scene";
|
||||
import { modifierTypes } from "#data/data-lists";
|
||||
import { BattleType } from "#enums/battle-type";
|
||||
import type { BattlerIndex } from "#enums/battler-index";
|
||||
import { ChallengeType } from "#enums/challenge-type";
|
||||
import { ClassicFixedBossWaves } from "#enums/fixed-boss-waves";
|
||||
import type { CustomModifierSettings } from "#modifiers/modifier-type";
|
||||
import { handleMysteryEncounterVictory } from "#mystery-encounters/encounter-phase-utils";
|
||||
import { PokemonPhase } from "#phases/pokemon-phase";
|
||||
import { applyChallenges } from "#utils/challenge-utils";
|
||||
import { BooleanHolder } from "#utils/common";
|
||||
|
||||
export class VictoryPhase extends PokemonPhase {
|
||||
public readonly phaseName = "VictoryPhase";
|
||||
@ -49,15 +45,19 @@ export class VictoryPhase extends PokemonPhase {
|
||||
if (globalScene.currentBattle.battleType === BattleType.TRAINER) {
|
||||
globalScene.phaseManager.pushNew("TrainerVictoryPhase");
|
||||
}
|
||||
if (globalScene.gameMode.isEndless || !globalScene.gameMode.isWaveFinal(globalScene.currentBattle.waveIndex)) {
|
||||
|
||||
const gameMode = globalScene.gameMode;
|
||||
const currentWaveIndex = globalScene.currentBattle.waveIndex;
|
||||
|
||||
if (gameMode.isEndless || !gameMode.isWaveFinal(currentWaveIndex)) {
|
||||
globalScene.phaseManager.pushNew("EggLapsePhase");
|
||||
if (globalScene.gameMode.isClassic) {
|
||||
switch (globalScene.currentBattle.waveIndex) {
|
||||
if (gameMode.isClassic) {
|
||||
switch (currentWaveIndex) {
|
||||
case ClassicFixedBossWaves.RIVAL_1:
|
||||
case ClassicFixedBossWaves.RIVAL_2:
|
||||
// Get event modifiers for this wave
|
||||
timedEventManager
|
||||
.getFixedBattleEventRewards(globalScene.currentBattle.waveIndex)
|
||||
.getFixedBattleEventRewards(currentWaveIndex)
|
||||
.map(r => globalScene.phaseManager.pushNew("ModifierRewardPhase", modifierTypes[r]));
|
||||
break;
|
||||
case ClassicFixedBossWaves.EVIL_BOSS_2:
|
||||
@ -66,59 +66,53 @@ export class VictoryPhase extends PokemonPhase {
|
||||
break;
|
||||
}
|
||||
}
|
||||
const healStatus = new BooleanHolder(globalScene.currentBattle.waveIndex % 10 === 0);
|
||||
applyChallenges(ChallengeType.PARTY_HEAL, healStatus);
|
||||
if (!healStatus.value) {
|
||||
if (currentWaveIndex % 10) {
|
||||
globalScene.phaseManager.pushNew(
|
||||
"SelectModifierPhase",
|
||||
undefined,
|
||||
undefined,
|
||||
this.getFixedBattleCustomModifiers(),
|
||||
gameMode.isFixedBattle(currentWaveIndex)
|
||||
? gameMode.getFixedBattle(currentWaveIndex).customModifierRewardSettings
|
||||
: undefined,
|
||||
);
|
||||
} else if (globalScene.gameMode.isDaily) {
|
||||
} else if (gameMode.isDaily) {
|
||||
globalScene.phaseManager.pushNew("ModifierRewardPhase", modifierTypes.EXP_CHARM);
|
||||
if (
|
||||
globalScene.currentBattle.waveIndex > 10 &&
|
||||
!globalScene.gameMode.isWaveFinal(globalScene.currentBattle.waveIndex)
|
||||
) {
|
||||
if (currentWaveIndex > 10 && !gameMode.isWaveFinal(currentWaveIndex)) {
|
||||
globalScene.phaseManager.pushNew("ModifierRewardPhase", modifierTypes.GOLDEN_POKEBALL);
|
||||
}
|
||||
} else {
|
||||
const superExpWave = !globalScene.gameMode.isEndless ? (globalScene.offsetGym ? 0 : 20) : 10;
|
||||
if (globalScene.gameMode.isEndless && globalScene.currentBattle.waveIndex === 10) {
|
||||
const superExpWave = !gameMode.isEndless ? (globalScene.offsetGym ? 0 : 20) : 10;
|
||||
if (gameMode.isEndless && currentWaveIndex === 10) {
|
||||
globalScene.phaseManager.pushNew("ModifierRewardPhase", modifierTypes.EXP_SHARE);
|
||||
}
|
||||
if (
|
||||
globalScene.currentBattle.waveIndex <= 750 &&
|
||||
(globalScene.currentBattle.waveIndex <= 500 || globalScene.currentBattle.waveIndex % 30 === superExpWave)
|
||||
) {
|
||||
if (currentWaveIndex <= 750 && (currentWaveIndex <= 500 || currentWaveIndex % 30 === superExpWave)) {
|
||||
globalScene.phaseManager.pushNew(
|
||||
"ModifierRewardPhase",
|
||||
globalScene.currentBattle.waveIndex % 30 !== superExpWave || globalScene.currentBattle.waveIndex > 250
|
||||
currentWaveIndex % 30 !== superExpWave || currentWaveIndex > 250
|
||||
? modifierTypes.EXP_CHARM
|
||||
: modifierTypes.SUPER_EXP_CHARM,
|
||||
);
|
||||
}
|
||||
if (globalScene.currentBattle.waveIndex <= 150 && !(globalScene.currentBattle.waveIndex % 50)) {
|
||||
if (currentWaveIndex <= 150 && !(currentWaveIndex % 50)) {
|
||||
globalScene.phaseManager.pushNew("ModifierRewardPhase", modifierTypes.GOLDEN_POKEBALL);
|
||||
}
|
||||
if (globalScene.gameMode.isEndless && !(globalScene.currentBattle.waveIndex % 50)) {
|
||||
if (gameMode.isEndless && !(currentWaveIndex % 50)) {
|
||||
globalScene.phaseManager.pushNew(
|
||||
"ModifierRewardPhase",
|
||||
!(globalScene.currentBattle.waveIndex % 250) ? modifierTypes.VOUCHER_PREMIUM : modifierTypes.VOUCHER_PLUS,
|
||||
!(currentWaveIndex % 250) ? modifierTypes.VOUCHER_PREMIUM : modifierTypes.VOUCHER_PLUS,
|
||||
);
|
||||
globalScene.phaseManager.pushNew("AddEnemyBuffModifierPhase");
|
||||
}
|
||||
}
|
||||
|
||||
if (globalScene.gameMode.hasRandomBiomes || globalScene.isNewBiome()) {
|
||||
if (gameMode.hasRandomBiomes || globalScene.isNewBiome()) {
|
||||
globalScene.phaseManager.pushNew("SelectBiomePhase");
|
||||
}
|
||||
|
||||
globalScene.phaseManager.pushNew("NewBattlePhase");
|
||||
} else {
|
||||
globalScene.currentBattle.battleType = BattleType.CLEAR;
|
||||
globalScene.score += globalScene.gameMode.getClearScoreBonus();
|
||||
globalScene.score += gameMode.getClearScoreBonus();
|
||||
globalScene.updateScoreText();
|
||||
globalScene.phaseManager.pushNew("GameOverPhase", true);
|
||||
}
|
||||
@ -126,18 +120,4 @@ export class VictoryPhase extends PokemonPhase {
|
||||
|
||||
this.end();
|
||||
}
|
||||
|
||||
/**
|
||||
* If this wave is a fixed battle with special custom modifier rewards,
|
||||
* will pass those settings to the upcoming {@linkcode SelectModifierPhase}`.
|
||||
*/
|
||||
getFixedBattleCustomModifiers(): CustomModifierSettings | undefined {
|
||||
const gameMode = globalScene.gameMode;
|
||||
const waveIndex = globalScene.currentBattle.waveIndex;
|
||||
if (gameMode.isFixedBattle(waveIndex)) {
|
||||
return gameMode.getFixedBattle(waveIndex).customModifierRewardSettings;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
@ -56,15 +56,15 @@ export class PokerogueSessionSavedataApi extends ApiBase {
|
||||
|
||||
/**
|
||||
* Update a session savedata.
|
||||
* @param params The {@linkcode UpdateSessionSavedataRequest} to send
|
||||
* @param rawSavedata The raw savedata (as `string`)
|
||||
* @param params - The request to send
|
||||
* @param rawSavedata - The raw, unencrypted savedata
|
||||
* @returns An error message if something went wrong
|
||||
*/
|
||||
public async update(params: UpdateSessionSavedataRequest, rawSavedata: string) {
|
||||
public async update(params: UpdateSessionSavedataRequest, rawSavedata: string): Promise<string> {
|
||||
try {
|
||||
const urlSearchParams = this.toUrlSearchParams(params);
|
||||
const response = await this.doPost(`/savedata/session/update?${urlSearchParams}`, rawSavedata);
|
||||
|
||||
const response = await this.doPost(`/savedata/session/update?${urlSearchParams}`, rawSavedata);
|
||||
return await response.text();
|
||||
} catch (err) {
|
||||
console.warn("Could not update session savedata!", err);
|
||||
|
@ -5,7 +5,6 @@ import {
|
||||
FlipStatChallenge,
|
||||
FreshStartChallenge,
|
||||
InverseBattleChallenge,
|
||||
LimitedCatchChallenge,
|
||||
SingleGenerationChallenge,
|
||||
SingleTypeChallenge,
|
||||
} from "#data/challenge";
|
||||
@ -14,6 +13,7 @@ import { PlayerGender } from "#enums/player-gender";
|
||||
import { getShortenedStatKey, Stat } from "#enums/stat";
|
||||
import { TurnHeldItemTransferModifier } from "#modifiers/modifier";
|
||||
import type { ConditionFn } from "#types/common";
|
||||
import { isNuzlockeChallenge } from "#utils/challenge-utils";
|
||||
import { NumberHolder } from "#utils/common";
|
||||
import i18next from "i18next";
|
||||
import type { Modifier } from "typescript";
|
||||
@ -448,6 +448,8 @@ export function getAchievementDescription(localizationKey: string): string {
|
||||
return i18next.t("achv:FLIP_STATS.description", { context: genderStr });
|
||||
case "FLIP_INVERSE":
|
||||
return i18next.t("achv:FLIP_INVERSE.description", { context: genderStr });
|
||||
case "NUZLOCKE":
|
||||
return i18next.t("achv:NUZLOCKE.description", { context: genderStr });
|
||||
case "BREEDERS_IN_SPACE":
|
||||
return i18next.t("achv:BREEDERS_IN_SPACE.description", {
|
||||
context: genderStr,
|
||||
@ -924,18 +926,7 @@ export const achvs = {
|
||||
globalScene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0),
|
||||
).setSecret(),
|
||||
// TODO: Decide on icon
|
||||
NUZLOCKE: new ChallengeAchv(
|
||||
"NUZLOCKE",
|
||||
"",
|
||||
"NUZLOCKE.description",
|
||||
"leaf_stone",
|
||||
100,
|
||||
c =>
|
||||
c instanceof LimitedCatchChallenge &&
|
||||
c.value > 0 &&
|
||||
globalScene.gameMode.challenges.some(c => c.id === Challenges.HARDCORE && c.value > 0) &&
|
||||
globalScene.gameMode.challenges.some(c => c.id === Challenges.FRESH_START && c.value > 0),
|
||||
),
|
||||
NUZLOCKE: new ChallengeAchv("NUZLOCKE", "", "NUZLOCKE.description", "leaf_stone", 100, isNuzlockeChallenge),
|
||||
BREEDERS_IN_SPACE: new Achv("BREEDERS_IN_SPACE", "", "BREEDERS_IN_SPACE.description", "moon_stone", 50).setSecret(),
|
||||
};
|
||||
|
||||
|
@ -48,6 +48,7 @@ import { EggData } from "#system/egg-data";
|
||||
import { GameStats } from "#system/game-stats";
|
||||
import { ModifierData as PersistentModifierData } from "#system/modifier-data";
|
||||
import { PokemonData } from "#system/pokemon-data";
|
||||
import { RibbonData } from "#system/ribbons/ribbon-data";
|
||||
import { resetSettings, SettingKeys, setSetting } from "#system/settings";
|
||||
import { SettingGamepad, setSettingGamepad, settingGamepadDefaults } from "#system/settings-gamepad";
|
||||
import type { SettingKeyboard } from "#system/settings-keyboard";
|
||||
@ -127,6 +128,8 @@ export interface SessionSaveData {
|
||||
battleType: BattleType;
|
||||
trainer: TrainerData;
|
||||
gameVersion: string;
|
||||
/** The player-chosen name of the run */
|
||||
name: string;
|
||||
timestamp: number;
|
||||
challenges: ChallengeData[];
|
||||
mysteryEncounterType: MysteryEncounterType | -1; // Only defined when current wave is ME,
|
||||
@ -206,10 +209,12 @@ export interface StarterData {
|
||||
[key: number]: StarterDataEntry;
|
||||
}
|
||||
|
||||
export interface TutorialFlags {
|
||||
[key: string]: boolean;
|
||||
}
|
||||
// TODO: Rework into a bitmask
|
||||
export type TutorialFlags = {
|
||||
[key in Tutorial]: boolean;
|
||||
};
|
||||
|
||||
// TODO: Rework into a bitmask
|
||||
export interface SeenDialogues {
|
||||
[key: string]: boolean;
|
||||
}
|
||||
@ -399,121 +404,121 @@ export class GameData {
|
||||
}
|
||||
|
||||
public initSystem(systemDataStr: string, cachedSystemDataStr?: string): Promise<boolean> {
|
||||
return new Promise<boolean>(resolve => {
|
||||
try {
|
||||
let systemData = this.parseSystemData(systemDataStr);
|
||||
const { promise, resolve } = Promise.withResolvers<boolean>();
|
||||
try {
|
||||
let systemData = this.parseSystemData(systemDataStr);
|
||||
|
||||
if (cachedSystemDataStr) {
|
||||
const cachedSystemData = this.parseSystemData(cachedSystemDataStr);
|
||||
if (cachedSystemData.timestamp > systemData.timestamp) {
|
||||
console.debug("Use cached system");
|
||||
systemData = cachedSystemData;
|
||||
systemDataStr = cachedSystemDataStr;
|
||||
} else {
|
||||
this.clearLocalData();
|
||||
}
|
||||
}
|
||||
|
||||
console.debug(systemData);
|
||||
|
||||
localStorage.setItem(`data_${loggedInUser?.username}`, encrypt(systemDataStr, bypassLogin));
|
||||
|
||||
const lsItemKey = `runHistoryData_${loggedInUser?.username}`;
|
||||
const lsItem = localStorage.getItem(lsItemKey);
|
||||
if (!lsItem) {
|
||||
localStorage.setItem(lsItemKey, "");
|
||||
}
|
||||
|
||||
applySystemVersionMigration(systemData);
|
||||
|
||||
this.trainerId = systemData.trainerId;
|
||||
this.secretId = systemData.secretId;
|
||||
|
||||
this.gender = systemData.gender;
|
||||
|
||||
this.saveSetting(SettingKeys.Player_Gender, systemData.gender === PlayerGender.FEMALE ? 1 : 0);
|
||||
|
||||
if (!systemData.starterData) {
|
||||
this.initStarterData();
|
||||
|
||||
if (systemData["starterMoveData"]) {
|
||||
const starterMoveData = systemData["starterMoveData"];
|
||||
for (const s of Object.keys(starterMoveData)) {
|
||||
this.starterData[s].moveset = starterMoveData[s];
|
||||
}
|
||||
}
|
||||
|
||||
if (systemData["starterEggMoveData"]) {
|
||||
const starterEggMoveData = systemData["starterEggMoveData"];
|
||||
for (const s of Object.keys(starterEggMoveData)) {
|
||||
this.starterData[s].eggMoves = starterEggMoveData[s];
|
||||
}
|
||||
}
|
||||
|
||||
this.migrateStarterAbilities(systemData, this.starterData);
|
||||
|
||||
const starterIds = Object.keys(this.starterData).map(s => Number.parseInt(s) as SpeciesId);
|
||||
for (const s of starterIds) {
|
||||
this.starterData[s].candyCount += systemData.dexData[s].caughtCount;
|
||||
this.starterData[s].candyCount += systemData.dexData[s].hatchedCount * 2;
|
||||
if (systemData.dexData[s].caughtAttr & DexAttr.SHINY) {
|
||||
this.starterData[s].candyCount += 4;
|
||||
}
|
||||
}
|
||||
if (cachedSystemDataStr) {
|
||||
const cachedSystemData = this.parseSystemData(cachedSystemDataStr);
|
||||
if (cachedSystemData.timestamp > systemData.timestamp) {
|
||||
console.debug("Use cached system");
|
||||
systemData = cachedSystemData;
|
||||
systemDataStr = cachedSystemDataStr;
|
||||
} else {
|
||||
this.starterData = systemData.starterData;
|
||||
this.clearLocalData();
|
||||
}
|
||||
|
||||
if (systemData.gameStats) {
|
||||
this.gameStats = systemData.gameStats;
|
||||
}
|
||||
|
||||
if (systemData.unlocks) {
|
||||
for (const key of Object.keys(systemData.unlocks)) {
|
||||
if (this.unlocks.hasOwnProperty(key)) {
|
||||
this.unlocks[key] = systemData.unlocks[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (systemData.achvUnlocks) {
|
||||
for (const a of Object.keys(systemData.achvUnlocks)) {
|
||||
if (achvs.hasOwnProperty(a)) {
|
||||
this.achvUnlocks[a] = systemData.achvUnlocks[a];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (systemData.voucherUnlocks) {
|
||||
for (const v of Object.keys(systemData.voucherUnlocks)) {
|
||||
if (vouchers.hasOwnProperty(v)) {
|
||||
this.voucherUnlocks[v] = systemData.voucherUnlocks[v];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (systemData.voucherCounts) {
|
||||
getEnumKeys(VoucherType).forEach(key => {
|
||||
const index = VoucherType[key];
|
||||
this.voucherCounts[index] = systemData.voucherCounts[index] || 0;
|
||||
});
|
||||
}
|
||||
|
||||
this.eggs = systemData.eggs ? systemData.eggs.map(e => e.toEgg()) : [];
|
||||
|
||||
this.eggPity = systemData.eggPity ? systemData.eggPity.slice(0) : [0, 0, 0, 0];
|
||||
this.unlockPity = systemData.unlockPity ? systemData.unlockPity.slice(0) : [0, 0, 0, 0];
|
||||
|
||||
this.dexData = Object.assign(this.dexData, systemData.dexData);
|
||||
this.consolidateDexData(this.dexData);
|
||||
this.defaultDexData = null;
|
||||
|
||||
resolve(true);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
|
||||
console.debug(systemData);
|
||||
|
||||
localStorage.setItem(`data_${loggedInUser?.username}`, encrypt(systemDataStr, bypassLogin));
|
||||
|
||||
const lsItemKey = `runHistoryData_${loggedInUser?.username}`;
|
||||
const lsItem = localStorage.getItem(lsItemKey);
|
||||
if (!lsItem) {
|
||||
localStorage.setItem(lsItemKey, "");
|
||||
}
|
||||
|
||||
applySystemVersionMigration(systemData);
|
||||
|
||||
this.trainerId = systemData.trainerId;
|
||||
this.secretId = systemData.secretId;
|
||||
|
||||
this.gender = systemData.gender;
|
||||
|
||||
this.saveSetting(SettingKeys.Player_Gender, systemData.gender === PlayerGender.FEMALE ? 1 : 0);
|
||||
|
||||
if (!systemData.starterData) {
|
||||
this.initStarterData();
|
||||
|
||||
if (systemData["starterMoveData"]) {
|
||||
const starterMoveData = systemData["starterMoveData"];
|
||||
for (const s of Object.keys(starterMoveData)) {
|
||||
this.starterData[s].moveset = starterMoveData[s];
|
||||
}
|
||||
}
|
||||
|
||||
if (systemData["starterEggMoveData"]) {
|
||||
const starterEggMoveData = systemData["starterEggMoveData"];
|
||||
for (const s of Object.keys(starterEggMoveData)) {
|
||||
this.starterData[s].eggMoves = starterEggMoveData[s];
|
||||
}
|
||||
}
|
||||
|
||||
this.migrateStarterAbilities(systemData, this.starterData);
|
||||
|
||||
const starterIds = Object.keys(this.starterData).map(s => Number.parseInt(s) as SpeciesId);
|
||||
for (const s of starterIds) {
|
||||
this.starterData[s].candyCount += systemData.dexData[s].caughtCount;
|
||||
this.starterData[s].candyCount += systemData.dexData[s].hatchedCount * 2;
|
||||
if (systemData.dexData[s].caughtAttr & DexAttr.SHINY) {
|
||||
this.starterData[s].candyCount += 4;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.starterData = systemData.starterData;
|
||||
}
|
||||
|
||||
if (systemData.gameStats) {
|
||||
this.gameStats = systemData.gameStats;
|
||||
}
|
||||
|
||||
if (systemData.unlocks) {
|
||||
for (const key of Object.keys(systemData.unlocks)) {
|
||||
if (this.unlocks.hasOwnProperty(key)) {
|
||||
this.unlocks[key] = systemData.unlocks[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (systemData.achvUnlocks) {
|
||||
for (const a of Object.keys(systemData.achvUnlocks)) {
|
||||
if (achvs.hasOwnProperty(a)) {
|
||||
this.achvUnlocks[a] = systemData.achvUnlocks[a];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (systemData.voucherUnlocks) {
|
||||
for (const v of Object.keys(systemData.voucherUnlocks)) {
|
||||
if (vouchers.hasOwnProperty(v)) {
|
||||
this.voucherUnlocks[v] = systemData.voucherUnlocks[v];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (systemData.voucherCounts) {
|
||||
getEnumKeys(VoucherType).forEach(key => {
|
||||
const index = VoucherType[key];
|
||||
this.voucherCounts[index] = systemData.voucherCounts[index] || 0;
|
||||
});
|
||||
}
|
||||
|
||||
this.eggs = systemData.eggs ? systemData.eggs.map(e => e.toEgg()) : [];
|
||||
|
||||
this.eggPity = systemData.eggPity ? systemData.eggPity.slice(0) : [0, 0, 0, 0];
|
||||
this.unlockPity = systemData.unlockPity ? systemData.unlockPity.slice(0) : [0, 0, 0, 0];
|
||||
|
||||
this.dexData = Object.assign(this.dexData, systemData.dexData);
|
||||
this.consolidateDexData(this.dexData);
|
||||
this.defaultDexData = null;
|
||||
|
||||
resolve(true);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
resolve(false);
|
||||
}
|
||||
return promise;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -624,6 +629,9 @@ export class GameData {
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
if (k === "ribbons") {
|
||||
return RibbonData.fromJSON(v);
|
||||
}
|
||||
|
||||
return k.endsWith("Attr") && !["natureAttr", "abilityAttr", "passiveAttr"].includes(k) ? BigInt(v ?? 0) : v;
|
||||
}) as SystemSaveData;
|
||||
@ -822,52 +830,51 @@ export class GameData {
|
||||
return true; // TODO: is `true` the correct return value?
|
||||
}
|
||||
|
||||
private loadGamepadSettings(): boolean {
|
||||
Object.values(SettingGamepad)
|
||||
.map(setting => setting as SettingGamepad)
|
||||
.forEach(setting => setSettingGamepad(setting, settingGamepadDefaults[setting]));
|
||||
private loadGamepadSettings(): void {
|
||||
Object.values(SettingGamepad).forEach(setting => {
|
||||
setSettingGamepad(setting, settingGamepadDefaults[setting]);
|
||||
});
|
||||
|
||||
if (!localStorage.hasOwnProperty("settingsGamepad")) {
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
const settingsGamepad = JSON.parse(localStorage.getItem("settingsGamepad")!); // TODO: is this bang correct?
|
||||
|
||||
for (const setting of Object.keys(settingsGamepad)) {
|
||||
setSettingGamepad(setting as SettingGamepad, settingsGamepad[setting]);
|
||||
}
|
||||
|
||||
return true; // TODO: is `true` the correct return value?
|
||||
}
|
||||
|
||||
public saveTutorialFlag(tutorial: Tutorial, flag: boolean): boolean {
|
||||
const key = getDataTypeKey(GameDataType.TUTORIALS);
|
||||
let tutorials: object = {};
|
||||
if (localStorage.hasOwnProperty(key)) {
|
||||
tutorials = JSON.parse(localStorage.getItem(key)!); // TODO: is this bang correct?
|
||||
/**
|
||||
* Save the specified tutorial as having the specified completion status.
|
||||
* @param tutorial - The {@linkcode Tutorial} whose completion status is being saved
|
||||
* @param status - The completion status to set
|
||||
*/
|
||||
public saveTutorialFlag(tutorial: Tutorial, status: boolean): void {
|
||||
// Grab the prior save data tutorial
|
||||
const saveDataKey = getDataTypeKey(GameDataType.TUTORIALS);
|
||||
const tutorials: TutorialFlags = localStorage.hasOwnProperty(saveDataKey)
|
||||
? JSON.parse(localStorage.getItem(saveDataKey)!)
|
||||
: {};
|
||||
|
||||
// TODO: We shouldn't be storing this like that
|
||||
for (const key of Object.values(Tutorial)) {
|
||||
if (key === tutorial) {
|
||||
tutorials[key] = status;
|
||||
} else {
|
||||
tutorials[key] ??= false;
|
||||
}
|
||||
}
|
||||
|
||||
Object.keys(Tutorial)
|
||||
.map(t => t as Tutorial)
|
||||
.forEach(t => {
|
||||
const key = Tutorial[t];
|
||||
if (key === tutorial) {
|
||||
tutorials[key] = flag;
|
||||
} else {
|
||||
tutorials[key] ??= false;
|
||||
}
|
||||
});
|
||||
|
||||
localStorage.setItem(key, JSON.stringify(tutorials));
|
||||
|
||||
return true;
|
||||
localStorage.setItem(saveDataKey, JSON.stringify(tutorials));
|
||||
}
|
||||
|
||||
public getTutorialFlags(): TutorialFlags {
|
||||
const key = getDataTypeKey(GameDataType.TUTORIALS);
|
||||
const ret: TutorialFlags = {};
|
||||
Object.values(Tutorial)
|
||||
.map(tutorial => tutorial as Tutorial)
|
||||
.forEach(tutorial => (ret[Tutorial[tutorial]] = false));
|
||||
const ret: TutorialFlags = Object.values(Tutorial).reduce((acc, tutorial) => {
|
||||
acc[Tutorial[tutorial]] = false;
|
||||
return acc;
|
||||
}, {} as TutorialFlags);
|
||||
|
||||
if (!localStorage.hasOwnProperty(key)) {
|
||||
return ret;
|
||||
@ -979,6 +986,48 @@ export class GameData {
|
||||
});
|
||||
}
|
||||
|
||||
async renameSession(slotId: number, newName: string): Promise<boolean> {
|
||||
if (slotId < 0) {
|
||||
return false;
|
||||
}
|
||||
if (newName === "") {
|
||||
return true;
|
||||
}
|
||||
const sessionData: SessionSaveData | null = await this.getSession(slotId);
|
||||
|
||||
if (!sessionData) {
|
||||
return false;
|
||||
}
|
||||
|
||||
sessionData.name = newName;
|
||||
// update timestamp by 1 to ensure the session is saved
|
||||
sessionData.timestamp += 1;
|
||||
const updatedDataStr = JSON.stringify(sessionData);
|
||||
const encrypted = encrypt(updatedDataStr, bypassLogin);
|
||||
const secretId = this.secretId;
|
||||
const trainerId = this.trainerId;
|
||||
|
||||
if (bypassLogin) {
|
||||
localStorage.setItem(
|
||||
`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`,
|
||||
encrypt(updatedDataStr, bypassLogin),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
const response = await pokerogueApi.savedata.session.update(
|
||||
{ slot: slotId, trainerId, secretId, clientSessionId },
|
||||
updatedDataStr,
|
||||
);
|
||||
|
||||
if (response) {
|
||||
return false;
|
||||
}
|
||||
localStorage.setItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`, encrypted);
|
||||
const success = await updateUserInfo();
|
||||
return !(success !== null && !success);
|
||||
}
|
||||
|
||||
loadSession(slotId: number, sessionData?: SessionSaveData): Promise<boolean> {
|
||||
// biome-ignore lint/suspicious/noAsyncPromiseExecutor: TODO: fix this
|
||||
return new Promise(async (resolve, reject) => {
|
||||
@ -1584,6 +1633,7 @@ export class GameData {
|
||||
caughtCount: 0,
|
||||
hatchedCount: 0,
|
||||
ivs: [0, 0, 0, 0, 0, 0],
|
||||
ribbons: new RibbonData(0),
|
||||
};
|
||||
}
|
||||
|
||||
@ -1828,6 +1878,12 @@ export class GameData {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Increase the number of classic ribbons won with this species.
|
||||
* @param species - The species to increment the ribbon count for
|
||||
* @param forStarter - If true, will increment the ribbon count for the root species of the given species
|
||||
* @returns The number of classic wins after incrementing.
|
||||
*/
|
||||
incrementRibbonCount(species: PokemonSpecies, forStarter = false): number {
|
||||
const speciesIdToIncrement: SpeciesId = species.getRootSpeciesId(forStarter);
|
||||
|
||||
@ -2127,6 +2183,9 @@ export class GameData {
|
||||
if (!entry.hasOwnProperty("natureAttr") || (entry.caughtAttr && !entry.natureAttr)) {
|
||||
entry.natureAttr = this.defaultDexData?.[k].natureAttr || 1 << randInt(25, 1);
|
||||
}
|
||||
if (!entry.hasOwnProperty("ribbons")) {
|
||||
entry.ribbons = new RibbonData(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
148
src/system/ribbons/ribbon-data.ts
Normal file
148
src/system/ribbons/ribbon-data.ts
Normal file
@ -0,0 +1,148 @@
|
||||
import type { Brander } from "#types/type-helpers";
|
||||
|
||||
export type RibbonFlag = (number & Brander<"RibbonFlag">) | 0;
|
||||
|
||||
/**
|
||||
* Class for ribbon data management. Usually constructed via the {@linkcode fromJSON} method.
|
||||
*
|
||||
* @remarks
|
||||
* Stores information about the ribbons earned by a species using a bitfield.
|
||||
*/
|
||||
export class RibbonData {
|
||||
/** Internal bitfield storing the unlock state for each ribbon */
|
||||
private payload: number;
|
||||
|
||||
//#region Ribbons
|
||||
//#region Monotype challenge ribbons
|
||||
/** Ribbon for winning the normal monotype challenge */
|
||||
public static readonly MONO_NORMAL = 0x1 as RibbonFlag;
|
||||
/** Ribbon for winning the fighting monotype challenge */
|
||||
public static readonly MONO_FIGHTING = 0x2 as RibbonFlag;
|
||||
/** Ribbon for winning the flying monotype challenge */
|
||||
public static readonly MONO_FLYING = 0x4 as RibbonFlag;
|
||||
/** Ribbon for winning the poision monotype challenge */
|
||||
public static readonly MONO_POISON = 0x8 as RibbonFlag;
|
||||
/** Ribbon for winning the ground monotype challenge */
|
||||
public static readonly MONO_GROUND = 0x10 as RibbonFlag;
|
||||
/** Ribbon for winning the rock monotype challenge */
|
||||
public static readonly MONO_ROCK = 0x20 as RibbonFlag;
|
||||
/** Ribbon for winning the bug monotype challenge */
|
||||
public static readonly MONO_BUG = 0x40 as RibbonFlag;
|
||||
/** Ribbon for winning the ghost monotype challenge */
|
||||
public static readonly MONO_GHOST = 0x80 as RibbonFlag;
|
||||
/** Ribbon for winning the steel monotype challenge */
|
||||
public static readonly MONO_STEEL = 0x100 as RibbonFlag;
|
||||
/** Ribbon for winning the fire monotype challenge */
|
||||
public static readonly MONO_FIRE = 0x200 as RibbonFlag;
|
||||
/** Ribbon for winning the water monotype challenge */
|
||||
public static readonly MONO_WATER = 0x400 as RibbonFlag;
|
||||
/** Ribbon for winning the grass monotype challenge */
|
||||
public static readonly MONO_GRASS = 0x800 as RibbonFlag;
|
||||
/** Ribbon for winning the electric monotype challenge */
|
||||
public static readonly MONO_ELECTRIC = 0x1000 as RibbonFlag;
|
||||
/** Ribbon for winning the psychic monotype challenge */
|
||||
public static readonly MONO_PSYCHIC = 0x2000 as RibbonFlag;
|
||||
/** Ribbon for winning the ice monotype challenge */
|
||||
public static readonly MONO_ICE = 0x4000 as RibbonFlag;
|
||||
/** Ribbon for winning the dragon monotype challenge */
|
||||
public static readonly MONO_DRAGON = 0x8000 as RibbonFlag;
|
||||
/** Ribbon for winning the dark monotype challenge */
|
||||
public static readonly MONO_DARK = 0x10000 as RibbonFlag;
|
||||
/** Ribbon for winning the fairy monotype challenge */
|
||||
public static readonly MONO_FAIRY = 0x20000 as RibbonFlag;
|
||||
//#endregion Monotype ribbons
|
||||
|
||||
//#region Monogen ribbons
|
||||
/** Ribbon for winning the the mono gen 1 challenge */
|
||||
public static readonly MONO_GEN_1 = 0x40000 as RibbonFlag;
|
||||
/** Ribbon for winning the the mono gen 2 challenge */
|
||||
public static readonly MONO_GEN_2 = 0x80000 as RibbonFlag;
|
||||
/** Ribbon for winning the mono gen 3 challenge */
|
||||
public static readonly MONO_GEN_3 = 0x100000 as RibbonFlag;
|
||||
/** Ribbon for winning the mono gen 4 challenge */
|
||||
public static readonly MONO_GEN_4 = 0x200000 as RibbonFlag;
|
||||
/** Ribbon for winning the mono gen 5 challenge */
|
||||
public static readonly MONO_GEN_5 = 0x400000 as RibbonFlag;
|
||||
/** Ribbon for winning the mono gen 6 challenge */
|
||||
public static readonly MONO_GEN_6 = 0x800000 as RibbonFlag;
|
||||
/** Ribbon for winning the mono gen 7 challenge */
|
||||
public static readonly MONO_GEN_7 = 0x1000000 as RibbonFlag;
|
||||
/** Ribbon for winning the mono gen 8 challenge */
|
||||
public static readonly MONO_GEN_8 = 0x2000000 as RibbonFlag;
|
||||
/** Ribbon for winning the mono gen 9 challenge */
|
||||
public static readonly MONO_GEN_9 = 0x4000000 as RibbonFlag;
|
||||
//#endregion Monogen ribbons
|
||||
|
||||
/** Ribbon for winning classic */
|
||||
public static readonly CLASSIC = 0x8000000 as RibbonFlag;
|
||||
/** Ribbon for winning the nuzzlocke challenge */
|
||||
public static readonly NUZLOCKE = 0x10000000 as RibbonFlag;
|
||||
/** Ribbon for reaching max friendship */
|
||||
public static readonly FRIENDSHIP = 0x20000000 as RibbonFlag;
|
||||
/** Ribbon for winning the flip stats challenge */
|
||||
public static readonly FLIP_STATS = 0x40000000 as RibbonFlag;
|
||||
/** Ribbon for winning the inverse challenge */
|
||||
public static readonly INVERSE = 0x80000000 as RibbonFlag;
|
||||
/** Ribbon for winning the fresh start challenge */
|
||||
public static readonly FRESH_START = 0x100000000 as RibbonFlag;
|
||||
/** Ribbon for winning the hardcore challenge */
|
||||
public static readonly HARDCORE = 0x200000000 as RibbonFlag;
|
||||
/** Ribbon for winning the limited catch challenge */
|
||||
public static readonly LIMITED_CATCH = 0x400000000 as RibbonFlag;
|
||||
/** Ribbon for winning the limited support challenge set to no heal */
|
||||
public static readonly NO_HEAL = 0x800000000 as RibbonFlag;
|
||||
/** Ribbon for winning the limited uspport challenge set to no shop */
|
||||
public static readonly NO_SHOP = 0x1000000000 as RibbonFlag;
|
||||
/** Ribbon for winning the limited support challenge set to both*/
|
||||
public static readonly NO_SUPPORT = 0x2000000000 as RibbonFlag;
|
||||
|
||||
// NOTE: max possible ribbon flag is 0x20000000000000 (53 total ribbons)
|
||||
// Once this is exceeded, bitfield needs to be changed to a bigint or even a uint array
|
||||
// Note that this has no impact on serialization as it is stored in hex.
|
||||
|
||||
//#endregion Ribbons
|
||||
|
||||
/** Create a new instance of RibbonData. Generally, {@linkcode fromJSON} is used instead. */
|
||||
constructor(value: number) {
|
||||
this.payload = value;
|
||||
}
|
||||
|
||||
/** Serialize the bitfield payload as a hex encoded string */
|
||||
public toJSON(): string {
|
||||
return this.payload.toString(16);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a hexadecimal string representation of the bitfield into a `RibbonData` instance
|
||||
*
|
||||
* @param value - Hexadecimal string representation of the bitfield (without the leading 0x)
|
||||
* @returns A new instance of `RibbonData` initialized with the provided bitfield.
|
||||
*/
|
||||
public static fromJSON(value: string): RibbonData {
|
||||
try {
|
||||
return new RibbonData(Number.parseInt(value, 16));
|
||||
} catch {
|
||||
return new RibbonData(0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Award one or more ribbons to the ribbon data by setting the corresponding flags in the bitfield.
|
||||
*
|
||||
* @param flags - The flags to set. Can be a single flag or multiple flags.
|
||||
*/
|
||||
public award(...flags: [RibbonFlag, ...RibbonFlag[]]): void {
|
||||
for (const f of flags) {
|
||||
this.payload |= f;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a specific ribbon has been awarded
|
||||
* @param flag - The ribbon to check
|
||||
* @returns Whether the specified flag has been awarded
|
||||
*/
|
||||
public has(flag: RibbonFlag): boolean {
|
||||
return !!(this.payload & flag);
|
||||
}
|
||||
}
|
20
src/system/ribbons/ribbon-methods.ts
Normal file
20
src/system/ribbons/ribbon-methods.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import { pokemonPrevolutions } from "#balance/pokemon-evolutions";
|
||||
import type { SpeciesId } from "#enums/species-id";
|
||||
import type { RibbonFlag } from "#system/ribbons/ribbon-data";
|
||||
import { isNullOrUndefined } from "#utils/common";
|
||||
|
||||
/**
|
||||
* Award one or more ribbons to a species and its pre-evolutions
|
||||
*
|
||||
* @param id - The ID of the species to award ribbons to
|
||||
* @param ribbons - The ribbon(s) to award (use bitwise OR to combine multiple)
|
||||
*/
|
||||
export function awardRibbonsToSpeciesLine(id: SpeciesId, ribbons: RibbonFlag): void {
|
||||
const dexData = globalScene.gameData.dexData;
|
||||
dexData[id].ribbons.award(ribbons);
|
||||
// Mark all pre-evolutions of the Pokémon with the same ribbon flags.
|
||||
for (let prevoId = pokemonPrevolutions[id]; !isNullOrUndefined(prevoId); prevoId = pokemonPrevolutions[prevoId]) {
|
||||
dexData[id].ribbons.award(ribbons);
|
||||
}
|
||||
}
|
@ -31,6 +31,11 @@ import { toTitleCase } from "#utils/strings";
|
||||
import i18next from "i18next";
|
||||
import type BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext";
|
||||
|
||||
const DISCARD_BUTTON_X = 60;
|
||||
const DISCARD_BUTTON_X_DOUBLES = 64;
|
||||
const DISCARD_BUTTON_Y = -73;
|
||||
const DISCARD_BUTTON_Y_DOUBLES = -58;
|
||||
|
||||
const defaultMessage = i18next.t("partyUiHandler:choosePokemon");
|
||||
|
||||
/**
|
||||
@ -301,7 +306,7 @@ export class PartyUiHandler extends MessageUiHandler {
|
||||
const partyMessageText = addTextObject(10, 8, defaultMessage, TextStyle.WINDOW, { maxLines: 2 });
|
||||
partyMessageText.setName("text-party-msg");
|
||||
|
||||
partyMessageText.setOrigin(0, 0);
|
||||
partyMessageText.setOrigin(0);
|
||||
partyMessageBoxContainer.add(partyMessageText);
|
||||
|
||||
this.message = partyMessageText;
|
||||
@ -317,10 +322,8 @@ export class PartyUiHandler extends MessageUiHandler {
|
||||
this.iconAnimHandler = new PokemonIconAnimHandler();
|
||||
this.iconAnimHandler.setup();
|
||||
|
||||
const partyDiscardModeButton = new PartyDiscardModeButton(60, -globalScene.game.canvas.height / 15 - 1, this);
|
||||
|
||||
const partyDiscardModeButton = new PartyDiscardModeButton(DISCARD_BUTTON_X, DISCARD_BUTTON_Y, this);
|
||||
partyContainer.add(partyDiscardModeButton);
|
||||
|
||||
this.partyDiscardModeButton = partyDiscardModeButton;
|
||||
|
||||
// prepare move overlay
|
||||
@ -1233,7 +1236,7 @@ export class PartyUiHandler extends MessageUiHandler {
|
||||
}
|
||||
if (!this.optionsCursorObj) {
|
||||
this.optionsCursorObj = globalScene.add.image(0, 0, "cursor");
|
||||
this.optionsCursorObj.setOrigin(0, 0);
|
||||
this.optionsCursorObj.setOrigin(0);
|
||||
this.optionsContainer.add(this.optionsCursorObj);
|
||||
}
|
||||
this.optionsCursorObj.setPosition(
|
||||
@ -1605,7 +1608,7 @@ export class PartyUiHandler extends MessageUiHandler {
|
||||
optionText.setColor("#40c8f8");
|
||||
optionText.setShadowColor("#006090");
|
||||
}
|
||||
optionText.setOrigin(0, 0);
|
||||
optionText.setOrigin(0);
|
||||
|
||||
/** For every item that has stack bigger than 1, display the current quantity selection */
|
||||
const itemModifiers = this.getItemModifiers(pokemon);
|
||||
@ -1802,6 +1805,7 @@ class PartySlot extends Phaser.GameObjects.Container {
|
||||
private selected: boolean;
|
||||
private transfer: boolean;
|
||||
private slotIndex: number;
|
||||
private isBenched: boolean;
|
||||
private pokemon: PlayerPokemon;
|
||||
|
||||
private slotBg: Phaser.GameObjects.Image;
|
||||
@ -1812,6 +1816,7 @@ class PartySlot extends Phaser.GameObjects.Container {
|
||||
public slotHpText: Phaser.GameObjects.Text;
|
||||
public slotDescriptionLabel: Phaser.GameObjects.Text; // this is used to show text instead of the HP bar i.e. for showing "Able"/"Not Able" for TMs when you try to learn them
|
||||
|
||||
private slotBgKey: string;
|
||||
private pokemonIcon: Phaser.GameObjects.Container;
|
||||
private iconAnimHandler: PokemonIconAnimHandler;
|
||||
|
||||
@ -1822,19 +1827,34 @@ class PartySlot extends Phaser.GameObjects.Container {
|
||||
partyUiMode: PartyUiMode,
|
||||
tmMoveId: MoveId,
|
||||
) {
|
||||
super(
|
||||
globalScene,
|
||||
slotIndex >= globalScene.currentBattle.getBattlerCount() ? 230.5 : 64,
|
||||
slotIndex >= globalScene.currentBattle.getBattlerCount()
|
||||
? -184 +
|
||||
(globalScene.currentBattle.double ? -40 : 0) +
|
||||
(28 + (globalScene.currentBattle.double ? 8 : 0)) * slotIndex
|
||||
: partyUiMode === PartyUiMode.MODIFIER_TRANSFER
|
||||
? -124 + (globalScene.currentBattle.double ? -20 : 0) + slotIndex * 55
|
||||
: -124 + (globalScene.currentBattle.double ? -8 : 0) + slotIndex * 64,
|
||||
);
|
||||
const isBenched = slotIndex >= globalScene.currentBattle.getBattlerCount();
|
||||
const isDoubleBattle = globalScene.currentBattle.double;
|
||||
const isItemManageMode = partyUiMode === PartyUiMode.MODIFIER_TRANSFER || partyUiMode === PartyUiMode.DISCARD;
|
||||
|
||||
/*
|
||||
* Here we determine the position of the slot.
|
||||
* The x coordinate depends on whether the pokemon is on the field or in the bench.
|
||||
* The y coordinate depends on various factors, such as the number of pokémon on the field,
|
||||
* and whether the transfer/discard button is also on the screen.
|
||||
*/
|
||||
const slotPositionX = isBenched ? 143 : 9;
|
||||
|
||||
let slotPositionY: number;
|
||||
if (isBenched) {
|
||||
slotPositionY = -196 + (isDoubleBattle ? -40 : 0);
|
||||
slotPositionY += (28 + (isDoubleBattle ? 8 : 0)) * slotIndex;
|
||||
} else {
|
||||
slotPositionY = -148.5;
|
||||
if (isDoubleBattle) {
|
||||
slotPositionY += isItemManageMode ? -20 : -8;
|
||||
}
|
||||
slotPositionY += (isItemManageMode ? (isDoubleBattle ? 47 : 55) : 64) * slotIndex;
|
||||
}
|
||||
|
||||
super(globalScene, slotPositionX, slotPositionY);
|
||||
|
||||
this.slotIndex = slotIndex;
|
||||
this.isBenched = isBenched;
|
||||
this.pokemon = pokemon;
|
||||
this.iconAnimHandler = iconAnimHandler;
|
||||
|
||||
@ -1848,27 +1868,75 @@ class PartySlot extends Phaser.GameObjects.Container {
|
||||
setup(partyUiMode: PartyUiMode, tmMoveId: MoveId) {
|
||||
const currentLanguage = i18next.resolvedLanguage ?? "en";
|
||||
const offsetJa = currentLanguage === "ja";
|
||||
const isItemManageMode = partyUiMode === PartyUiMode.MODIFIER_TRANSFER || partyUiMode === PartyUiMode.DISCARD;
|
||||
|
||||
const battlerCount = globalScene.currentBattle.getBattlerCount();
|
||||
this.slotBgKey = this.isBenched
|
||||
? "party_slot"
|
||||
: isItemManageMode && globalScene.currentBattle.double
|
||||
? "party_slot_main_short"
|
||||
: "party_slot_main";
|
||||
const fullSlotBgKey = this.pokemon.hp ? this.slotBgKey : `${this.slotBgKey}${"_fnt"}`;
|
||||
this.slotBg = globalScene.add.sprite(0, 0, this.slotBgKey, fullSlotBgKey);
|
||||
this.slotBg.setOrigin(0);
|
||||
this.add(this.slotBg);
|
||||
|
||||
const slotKey = `party_slot${this.slotIndex >= battlerCount ? "" : "_main"}`;
|
||||
const genderSymbol = getGenderSymbol(this.pokemon.getGender(true));
|
||||
const isFusion = this.pokemon.isFusion();
|
||||
|
||||
const slotBg = globalScene.add.sprite(0, 0, slotKey, `${slotKey}${this.pokemon.hp ? "" : "_fnt"}`);
|
||||
this.slotBg = slotBg;
|
||||
// Here we define positions and offsets
|
||||
// Base values are for the active pokemon; they are changed for benched pokemon,
|
||||
// or for active pokemon if in a double battle in item management mode.
|
||||
|
||||
this.add(slotBg);
|
||||
// icon position relative to slot background
|
||||
let slotPb = { x: 4, y: 4 };
|
||||
// name position relative to slot background
|
||||
let namePosition = { x: 24, y: 10 + (offsetJa ? 2 : 0) };
|
||||
// maximum allowed length of name; must accomodate fusion symbol
|
||||
let maxNameTextWidth = 76 - (isFusion ? 8 : 0);
|
||||
// "Lv." label position relative to slot background
|
||||
let levelLabelPosition = { x: 24 + 8, y: 10 + 12 };
|
||||
// offset from "Lv." to the level number; should not be changed.
|
||||
const levelTextToLevelLabelOffset = { x: 9, y: offsetJa ? 1.5 : 0 };
|
||||
// offests from "Lv." to gender, spliced and status icons, these depend on the type of slot.
|
||||
let genderTextToLevelLabelOffset = { x: 68 - (isFusion ? 8 : 0), y: -9 };
|
||||
let splicedIconToLevelLabelOffset = { x: 68, y: 3.5 - 12 };
|
||||
let statusIconToLevelLabelOffset = { x: 55, y: 0 };
|
||||
// offset from the name to the shiny icon (on the left); should not be changed.
|
||||
const shinyIconToNameOffset = { x: -9, y: 3 };
|
||||
// hp bar position relative to slot background
|
||||
let hpBarPosition = { x: 8, y: 31 };
|
||||
// offsets of hp bar overlay (showing the remaining hp) and number; should not be changed.
|
||||
const hpOverlayToBarOffset = { x: 16, y: 2 };
|
||||
const hpTextToBarOffset = { x: -3, y: -2 + (offsetJa ? 2 : 0) };
|
||||
// description position relative to slot background
|
||||
let descriptionLabelPosition = { x: 32, y: 46 };
|
||||
|
||||
const slotPb = globalScene.add.sprite(
|
||||
this.slotIndex >= battlerCount ? -85.5 : -51,
|
||||
this.slotIndex >= battlerCount ? 0 : -20.5,
|
||||
"party_pb",
|
||||
);
|
||||
this.slotPb = slotPb;
|
||||
// If in item management mode, the active slots are shorter
|
||||
if (isItemManageMode && globalScene.currentBattle.double && !this.isBenched) {
|
||||
namePosition.y -= 8;
|
||||
levelLabelPosition.y -= 8;
|
||||
hpBarPosition.y -= 8;
|
||||
descriptionLabelPosition.y -= 8;
|
||||
}
|
||||
|
||||
this.add(slotPb);
|
||||
// Benched slots have significantly different parameters
|
||||
if (this.isBenched) {
|
||||
slotPb = { x: 2, y: 12 };
|
||||
namePosition = { x: 21, y: 2 + (offsetJa ? 2 : 0) };
|
||||
maxNameTextWidth = 52;
|
||||
levelLabelPosition = { x: 21 + 8, y: 2 + 12 };
|
||||
genderTextToLevelLabelOffset = { x: 36, y: 0 };
|
||||
splicedIconToLevelLabelOffset = { x: 36 + (genderSymbol ? 8 : 0), y: 0.5 };
|
||||
statusIconToLevelLabelOffset = { x: 43, y: 0 };
|
||||
hpBarPosition = { x: 72, y: 6 };
|
||||
descriptionLabelPosition = { x: 94, y: 16 };
|
||||
}
|
||||
|
||||
this.pokemonIcon = globalScene.addPokemonIcon(this.pokemon, slotPb.x, slotPb.y, 0.5, 0.5, true);
|
||||
this.slotPb = globalScene.add.sprite(0, 0, "party_pb");
|
||||
this.slotPb.setPosition(slotPb.x, slotPb.y);
|
||||
this.add(this.slotPb);
|
||||
|
||||
this.pokemonIcon = globalScene.addPokemonIcon(this.pokemon, this.slotPb.x, this.slotPb.y, 0.5, 0.5, true);
|
||||
this.add(this.pokemonIcon);
|
||||
|
||||
this.iconAnimHandler.addOrUpdate(this.pokemonIcon, PokemonIconAnimMode.PASSIVE);
|
||||
@ -1882,7 +1950,7 @@ class PartySlot extends Phaser.GameObjects.Container {
|
||||
const nameSizeTest = addTextObject(0, 0, displayName, TextStyle.PARTY);
|
||||
nameTextWidth = nameSizeTest.displayWidth;
|
||||
|
||||
while (nameTextWidth > (this.slotIndex >= battlerCount ? 52 : 76 - (this.pokemon.fusionSpecies ? 8 : 0))) {
|
||||
while (nameTextWidth > maxNameTextWidth) {
|
||||
displayName = `${displayName.slice(0, displayName.endsWith(".") ? -2 : -1).trimEnd()}.`;
|
||||
nameSizeTest.setText(displayName);
|
||||
nameTextWidth = nameSizeTest.displayWidth;
|
||||
@ -1891,78 +1959,59 @@ class PartySlot extends Phaser.GameObjects.Container {
|
||||
nameSizeTest.destroy();
|
||||
|
||||
this.slotName = addTextObject(0, 0, displayName, TextStyle.PARTY);
|
||||
this.slotName.setPositionRelative(
|
||||
slotBg,
|
||||
this.slotIndex >= battlerCount ? 21 : 24,
|
||||
(this.slotIndex >= battlerCount ? 2 : 10) + (offsetJa ? 2 : 0),
|
||||
);
|
||||
this.slotName.setOrigin(0, 0);
|
||||
this.slotName.setPositionRelative(this.slotBg, namePosition.x, namePosition.y);
|
||||
this.slotName.setOrigin(0);
|
||||
|
||||
const slotLevelLabel = globalScene.add.image(0, 0, "party_slot_overlay_lv");
|
||||
slotLevelLabel.setPositionRelative(
|
||||
slotBg,
|
||||
(this.slotIndex >= battlerCount ? 21 : 24) + 8,
|
||||
(this.slotIndex >= battlerCount ? 2 : 10) + 12,
|
||||
);
|
||||
slotLevelLabel.setOrigin(0, 0);
|
||||
const slotLevelLabel = globalScene.add
|
||||
.image(0, 0, "party_slot_overlay_lv")
|
||||
.setPositionRelative(this.slotBg, levelLabelPosition.x, levelLabelPosition.y)
|
||||
.setOrigin(0);
|
||||
|
||||
const slotLevelText = addTextObject(
|
||||
0,
|
||||
0,
|
||||
this.pokemon.level.toString(),
|
||||
this.pokemon.level < globalScene.getMaxExpLevel() ? TextStyle.PARTY : TextStyle.PARTY_RED,
|
||||
);
|
||||
slotLevelText.setPositionRelative(slotLevelLabel, 9, offsetJa ? 1.5 : 0);
|
||||
slotLevelText.setOrigin(0, 0.25);
|
||||
|
||||
)
|
||||
.setPositionRelative(slotLevelLabel, levelTextToLevelLabelOffset.x, levelTextToLevelLabelOffset.y)
|
||||
.setOrigin(0, 0.25);
|
||||
slotInfoContainer.add([this.slotName, slotLevelLabel, slotLevelText]);
|
||||
|
||||
const genderSymbol = getGenderSymbol(this.pokemon.getGender(true));
|
||||
|
||||
if (genderSymbol) {
|
||||
const slotGenderText = addTextObject(0, 0, genderSymbol, TextStyle.PARTY);
|
||||
slotGenderText.setColor(getGenderColor(this.pokemon.getGender(true)));
|
||||
slotGenderText.setShadowColor(getGenderColor(this.pokemon.getGender(true), true));
|
||||
if (this.slotIndex >= battlerCount) {
|
||||
slotGenderText.setPositionRelative(slotLevelLabel, 36, 0);
|
||||
} else {
|
||||
slotGenderText.setPositionRelative(this.slotName, 76 - (this.pokemon.fusionSpecies ? 8 : 0), 3);
|
||||
}
|
||||
slotGenderText.setOrigin(0, 0.25);
|
||||
|
||||
const slotGenderText = addTextObject(0, 0, genderSymbol, TextStyle.PARTY)
|
||||
.setColor(getGenderColor(this.pokemon.getGender(true)))
|
||||
.setShadowColor(getGenderColor(this.pokemon.getGender(true), true))
|
||||
.setPositionRelative(slotLevelLabel, genderTextToLevelLabelOffset.x, genderTextToLevelLabelOffset.y)
|
||||
.setOrigin(0, 0.25);
|
||||
slotInfoContainer.add(slotGenderText);
|
||||
}
|
||||
|
||||
if (this.pokemon.fusionSpecies) {
|
||||
const splicedIcon = globalScene.add.image(0, 0, "icon_spliced");
|
||||
splicedIcon.setScale(0.5);
|
||||
splicedIcon.setOrigin(0, 0);
|
||||
if (this.slotIndex >= battlerCount) {
|
||||
splicedIcon.setPositionRelative(slotLevelLabel, 36 + (genderSymbol ? 8 : 0), 0.5);
|
||||
} else {
|
||||
splicedIcon.setPositionRelative(this.slotName, 76, 3.5);
|
||||
}
|
||||
|
||||
if (isFusion) {
|
||||
const splicedIcon = globalScene.add
|
||||
.image(0, 0, "icon_spliced")
|
||||
.setScale(0.5)
|
||||
.setOrigin(0)
|
||||
.setPositionRelative(slotLevelLabel, splicedIconToLevelLabelOffset.x, splicedIconToLevelLabelOffset.y);
|
||||
slotInfoContainer.add(splicedIcon);
|
||||
}
|
||||
|
||||
if (this.pokemon.status) {
|
||||
const statusIndicator = globalScene.add.sprite(0, 0, getLocalizedSpriteKey("statuses"));
|
||||
statusIndicator.setFrame(StatusEffect[this.pokemon.status?.effect].toLowerCase());
|
||||
statusIndicator.setOrigin(0, 0);
|
||||
statusIndicator.setPositionRelative(slotLevelLabel, this.slotIndex >= battlerCount ? 43 : 55, 0);
|
||||
|
||||
const statusIndicator = globalScene.add
|
||||
.sprite(0, 0, getLocalizedSpriteKey("statuses"))
|
||||
.setFrame(StatusEffect[this.pokemon.status?.effect].toLowerCase())
|
||||
.setOrigin(0)
|
||||
.setPositionRelative(slotLevelLabel, statusIconToLevelLabelOffset.x, statusIconToLevelLabelOffset.y);
|
||||
slotInfoContainer.add(statusIndicator);
|
||||
}
|
||||
|
||||
if (this.pokemon.isShiny()) {
|
||||
const doubleShiny = this.pokemon.isDoubleShiny(false);
|
||||
|
||||
const shinyStar = globalScene.add.image(0, 0, `shiny_star_small${doubleShiny ? "_1" : ""}`);
|
||||
shinyStar.setOrigin(0, 0);
|
||||
shinyStar.setPositionRelative(this.slotName, -9, 3);
|
||||
shinyStar.setTint(getVariantTint(this.pokemon.getBaseVariant()));
|
||||
|
||||
const shinyStar = globalScene.add
|
||||
.image(0, 0, `shiny_star_small${doubleShiny ? "_1" : ""}`)
|
||||
.setOrigin(0)
|
||||
.setPositionRelative(this.slotName, shinyIconToNameOffset.x, shinyIconToNameOffset.y)
|
||||
.setTint(getVariantTint(this.pokemon.getBaseVariant()));
|
||||
slotInfoContainer.add(shinyStar);
|
||||
|
||||
if (doubleShiny) {
|
||||
@ -1971,50 +2020,38 @@ class PartySlot extends Phaser.GameObjects.Container {
|
||||
.setOrigin(0)
|
||||
.setPosition(shinyStar.x, shinyStar.y)
|
||||
.setTint(getVariantTint(this.pokemon.fusionVariant));
|
||||
|
||||
slotInfoContainer.add(fusionShinyStar);
|
||||
}
|
||||
}
|
||||
|
||||
this.slotHpBar = globalScene.add.image(0, 0, "party_slot_hp_bar");
|
||||
this.slotHpBar.setPositionRelative(
|
||||
slotBg,
|
||||
this.slotIndex >= battlerCount ? 72 : 8,
|
||||
this.slotIndex >= battlerCount ? 6 : 31,
|
||||
);
|
||||
this.slotHpBar.setOrigin(0, 0);
|
||||
this.slotHpBar.setVisible(false);
|
||||
this.slotHpBar = globalScene.add
|
||||
.image(0, 0, "party_slot_hp_bar")
|
||||
.setOrigin(0)
|
||||
.setVisible(false)
|
||||
.setPositionRelative(this.slotBg, hpBarPosition.x, hpBarPosition.y);
|
||||
|
||||
const hpRatio = this.pokemon.getHpRatio();
|
||||
|
||||
this.slotHpOverlay = globalScene.add.sprite(
|
||||
0,
|
||||
0,
|
||||
"party_slot_hp_overlay",
|
||||
hpRatio > 0.5 ? "high" : hpRatio > 0.25 ? "medium" : "low",
|
||||
);
|
||||
this.slotHpOverlay.setPositionRelative(this.slotHpBar, 16, 2);
|
||||
this.slotHpOverlay.setOrigin(0, 0);
|
||||
this.slotHpOverlay.setScale(hpRatio, 1);
|
||||
this.slotHpOverlay.setVisible(false);
|
||||
this.slotHpOverlay = globalScene.add
|
||||
.sprite(0, 0, "party_slot_hp_overlay", hpRatio > 0.5 ? "high" : hpRatio > 0.25 ? "medium" : "low")
|
||||
.setOrigin(0)
|
||||
.setPositionRelative(this.slotHpBar, hpOverlayToBarOffset.x, hpOverlayToBarOffset.y)
|
||||
.setScale(hpRatio, 1)
|
||||
.setVisible(false);
|
||||
|
||||
this.slotHpText = addTextObject(0, 0, `${this.pokemon.hp}/${this.pokemon.getMaxHp()}`, TextStyle.PARTY);
|
||||
this.slotHpText.setPositionRelative(
|
||||
this.slotHpBar,
|
||||
this.slotHpBar.width - 3,
|
||||
this.slotHpBar.height - 2 + (offsetJa ? 2 : 0),
|
||||
);
|
||||
this.slotHpText.setOrigin(1, 0);
|
||||
this.slotHpText.setVisible(false);
|
||||
this.slotHpText = addTextObject(0, 0, `${this.pokemon.hp}/${this.pokemon.getMaxHp()}`, TextStyle.PARTY)
|
||||
.setOrigin(1, 0)
|
||||
.setPositionRelative(
|
||||
this.slotHpBar,
|
||||
this.slotHpBar.width + hpTextToBarOffset.x,
|
||||
this.slotHpBar.height + hpTextToBarOffset.y,
|
||||
) // TODO: annoying because it contains the width
|
||||
.setVisible(false);
|
||||
|
||||
this.slotDescriptionLabel = addTextObject(0, 0, "", TextStyle.MESSAGE);
|
||||
this.slotDescriptionLabel.setPositionRelative(
|
||||
slotBg,
|
||||
this.slotIndex >= battlerCount ? 94 : 32,
|
||||
this.slotIndex >= battlerCount ? 16 : 46,
|
||||
);
|
||||
this.slotDescriptionLabel.setOrigin(0, 1);
|
||||
this.slotDescriptionLabel.setVisible(false);
|
||||
this.slotDescriptionLabel = addTextObject(0, 0, "", TextStyle.MESSAGE)
|
||||
.setOrigin(0, 1)
|
||||
.setVisible(false)
|
||||
.setPositionRelative(this.slotBg, descriptionLabelPosition.x, descriptionLabelPosition.y);
|
||||
|
||||
slotInfoContainer.add([this.slotHpBar, this.slotHpOverlay, this.slotHpText, this.slotDescriptionLabel]);
|
||||
|
||||
@ -2076,10 +2113,9 @@ class PartySlot extends Phaser.GameObjects.Container {
|
||||
}
|
||||
|
||||
private updateSlotTexture(): void {
|
||||
const battlerCount = globalScene.currentBattle.getBattlerCount();
|
||||
this.slotBg.setTexture(
|
||||
`party_slot${this.slotIndex >= battlerCount ? "" : "_main"}`,
|
||||
`party_slot${this.slotIndex >= battlerCount ? "" : "_main"}${this.transfer ? "_swap" : this.pokemon.hp ? "" : "_fnt"}${this.selected ? "_sel" : ""}`,
|
||||
this.slotBgKey,
|
||||
`${this.slotBgKey}${this.transfer ? "_swap" : this.pokemon.hp ? "" : "_fnt"}${this.selected ? "_sel" : ""}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -2106,7 +2142,12 @@ class PartyCancelButton extends Phaser.GameObjects.Container {
|
||||
|
||||
this.partyCancelPb = partyCancelPb;
|
||||
|
||||
const partyCancelText = addTextObject(-10, -7, i18next.t("partyUiHandler:cancel"), TextStyle.PARTY_CANCEL_BUTTON);
|
||||
const partyCancelText = addTextObject(
|
||||
-10,
|
||||
-7,
|
||||
i18next.t("partyUiHandler:cancelButton"),
|
||||
TextStyle.PARTY_CANCEL_BUTTON,
|
||||
);
|
||||
this.add(partyCancelText);
|
||||
}
|
||||
|
||||
@ -2198,10 +2239,6 @@ class PartyDiscardModeButton extends Phaser.GameObjects.Container {
|
||||
this.discardIcon.setVisible(false);
|
||||
this.textBox.setVisible(true);
|
||||
this.textBox.setText(i18next.t("partyUiHandler:TRANSFER"));
|
||||
this.setPosition(
|
||||
globalScene.currentBattle.double ? 64 : 60,
|
||||
globalScene.currentBattle.double ? -48 : -globalScene.game.canvas.height / 15 - 1,
|
||||
);
|
||||
this.transferIcon.displayWidth = this.textBox.text.length * 9 + 3;
|
||||
break;
|
||||
case PartyUiMode.DISCARD:
|
||||
@ -2209,13 +2246,13 @@ class PartyDiscardModeButton extends Phaser.GameObjects.Container {
|
||||
this.discardIcon.setVisible(true);
|
||||
this.textBox.setVisible(true);
|
||||
this.textBox.setText(i18next.t("partyUiHandler:DISCARD"));
|
||||
this.setPosition(
|
||||
globalScene.currentBattle.double ? 64 : 60,
|
||||
globalScene.currentBattle.double ? -48 : -globalScene.game.canvas.height / 15 - 1,
|
||||
);
|
||||
this.discardIcon.displayWidth = this.textBox.text.length * 9 + 3;
|
||||
break;
|
||||
}
|
||||
this.setPosition(
|
||||
globalScene.currentBattle.double ? DISCARD_BUTTON_X_DOUBLES : DISCARD_BUTTON_X,
|
||||
globalScene.currentBattle.double ? DISCARD_BUTTON_Y_DOUBLES : DISCARD_BUTTON_Y,
|
||||
);
|
||||
}
|
||||
|
||||
clear() {
|
||||
|
@ -208,6 +208,26 @@ export class PokedexMonContainer extends Phaser.GameObjects.Container {
|
||||
);
|
||||
this.checkIconId(defaultProps.female, defaultProps.formIndex, defaultProps.shiny, defaultProps.variant);
|
||||
this.add(this.icon);
|
||||
|
||||
[
|
||||
this.hiddenAbilityIcon,
|
||||
this.favoriteIcon,
|
||||
this.classicWinIcon,
|
||||
this.candyUpgradeIcon,
|
||||
this.candyUpgradeOverlayIcon,
|
||||
this.eggMove1Icon,
|
||||
this.tmMove1Icon,
|
||||
this.eggMove2Icon,
|
||||
this.tmMove2Icon,
|
||||
this.passive1Icon,
|
||||
this.passive2Icon,
|
||||
this.passive1OverlayIcon,
|
||||
this.passive2OverlayIcon,
|
||||
].forEach(icon => {
|
||||
if (icon) {
|
||||
this.bringToTop(icon);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
checkIconId(female, formIndex, shiny, variant) {
|
||||
|
@ -410,6 +410,11 @@ export class PokedexUiHandler extends MessageUiHandler {
|
||||
new DropDownLabel(i18next.t("filterBar:hasHiddenAbility"), undefined, DropDownState.ON),
|
||||
new DropDownLabel(i18next.t("filterBar:noHiddenAbility"), undefined, DropDownState.EXCLUDE),
|
||||
];
|
||||
const seenSpeciesLabels = [
|
||||
new DropDownLabel(i18next.t("filterBar:seenSpecies"), undefined, DropDownState.OFF),
|
||||
new DropDownLabel(i18next.t("filterBar:isSeen"), undefined, DropDownState.ON),
|
||||
new DropDownLabel(i18next.t("filterBar:isUnseen"), undefined, DropDownState.EXCLUDE),
|
||||
];
|
||||
const eggLabels = [
|
||||
new DropDownLabel(i18next.t("filterBar:egg"), undefined, DropDownState.OFF),
|
||||
new DropDownLabel(i18next.t("filterBar:eggPurchasable"), undefined, DropDownState.ON),
|
||||
@ -423,6 +428,7 @@ export class PokedexUiHandler extends MessageUiHandler {
|
||||
new DropDownOption("FAVORITE", favoriteLabels),
|
||||
new DropDownOption("WIN", winLabels),
|
||||
new DropDownOption("HIDDEN_ABILITY", hiddenAbilityLabels),
|
||||
new DropDownOption("SEEN_SPECIES", seenSpeciesLabels),
|
||||
new DropDownOption("EGG", eggLabels),
|
||||
new DropDownOption("POKERUS", pokerusLabels),
|
||||
];
|
||||
@ -792,13 +798,15 @@ export class PokedexUiHandler extends MessageUiHandler {
|
||||
this.starterSelectMessageBoxContainer.setVisible(!!text?.length);
|
||||
}
|
||||
|
||||
isSeen(species: PokemonSpecies, dexEntry: DexEntry): boolean {
|
||||
isSeen(species: PokemonSpecies, dexEntry: DexEntry, seenFilter?: boolean): boolean {
|
||||
if (dexEntry?.seenAttr) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const starterDexEntry = globalScene.gameData.dexData[this.getStarterSpeciesId(species.speciesId)];
|
||||
return !!starterDexEntry?.caughtAttr;
|
||||
if (!seenFilter) {
|
||||
const starterDexEntry = globalScene.gameData.dexData[this.getStarterSpeciesId(species.speciesId)];
|
||||
return !!starterDexEntry?.caughtAttr;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1617,6 +1625,21 @@ export class PokedexUiHandler extends MessageUiHandler {
|
||||
}
|
||||
});
|
||||
|
||||
// Seen Filter
|
||||
const dexEntry = globalScene.gameData.dexData[species.speciesId];
|
||||
const isItSeen = this.isSeen(species, dexEntry, true);
|
||||
const fitsSeen = this.filterBar.getVals(DropDownColumn.MISC).some(misc => {
|
||||
if (misc.val === "SEEN_SPECIES" && misc.state === DropDownState.ON) {
|
||||
return isItSeen;
|
||||
}
|
||||
if (misc.val === "SEEN_SPECIES" && misc.state === DropDownState.EXCLUDE) {
|
||||
return !isItSeen;
|
||||
}
|
||||
if (misc.val === "SEEN_SPECIES" && misc.state === DropDownState.OFF) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// Egg Purchasable Filter
|
||||
const isEggPurchasable = this.isSameSpeciesEggAvailable(species.speciesId);
|
||||
const fitsEgg = this.filterBar.getVals(DropDownColumn.MISC).some(misc => {
|
||||
@ -1658,6 +1681,7 @@ export class PokedexUiHandler extends MessageUiHandler {
|
||||
fitsFavorite &&
|
||||
fitsWin &&
|
||||
fitsHA &&
|
||||
fitsSeen &&
|
||||
fitsEgg &&
|
||||
fitsPokerus
|
||||
) {
|
||||
|
54
src/ui/rename-run-ui-handler.ts
Normal file
54
src/ui/rename-run-ui-handler.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import i18next from "i18next";
|
||||
import type { InputFieldConfig } from "./form-modal-ui-handler";
|
||||
import { FormModalUiHandler } from "./form-modal-ui-handler";
|
||||
import type { ModalConfig } from "./modal-ui-handler";
|
||||
|
||||
export class RenameRunFormUiHandler extends FormModalUiHandler {
|
||||
getModalTitle(_config?: ModalConfig): string {
|
||||
return i18next.t("menu:renamerun");
|
||||
}
|
||||
|
||||
getWidth(_config?: ModalConfig): number {
|
||||
return 160;
|
||||
}
|
||||
|
||||
getMargin(_config?: ModalConfig): [number, number, number, number] {
|
||||
return [0, 0, 48, 0];
|
||||
}
|
||||
|
||||
getButtonLabels(_config?: ModalConfig): string[] {
|
||||
return [i18next.t("menu:rename"), i18next.t("menu:cancel")];
|
||||
}
|
||||
|
||||
getReadableErrorMessage(error: string): string {
|
||||
const colonIndex = error?.indexOf(":");
|
||||
if (colonIndex > 0) {
|
||||
error = error.slice(0, colonIndex);
|
||||
}
|
||||
|
||||
return super.getReadableErrorMessage(error);
|
||||
}
|
||||
|
||||
override getInputFieldConfigs(): InputFieldConfig[] {
|
||||
return [{ label: i18next.t("menu:runName") }];
|
||||
}
|
||||
|
||||
show(args: any[]): boolean {
|
||||
if (!super.show(args)) {
|
||||
return false;
|
||||
}
|
||||
if (this.inputs?.length) {
|
||||
this.inputs.forEach(input => {
|
||||
input.text = "";
|
||||
});
|
||||
}
|
||||
const config = args[0] as ModalConfig;
|
||||
this.submitAction = _ => {
|
||||
this.sanitizeInputs();
|
||||
const sanitizedName = btoa(encodeURIComponent(this.inputs[0].text));
|
||||
config.buttonActions[0](sanitizedName);
|
||||
return true;
|
||||
};
|
||||
return true;
|
||||
}
|
||||
}
|
@ -26,6 +26,7 @@ import { addBBCodeTextObject, addTextObject, getTextColor } from "#ui/text";
|
||||
import { UiHandler } from "#ui/ui-handler";
|
||||
import { addWindow } from "#ui/ui-theme";
|
||||
import { formatFancyLargeNumber, formatLargeNumber, formatMoney, getPlayTimeString } from "#utils/common";
|
||||
import { toCamelCase } from "#utils/strings";
|
||||
import i18next from "i18next";
|
||||
import RoundRectangle from "phaser3-rex-plugins/plugins/roundrectangle";
|
||||
|
||||
@ -207,6 +208,11 @@ export class RunInfoUiHandler extends UiHandler {
|
||||
headerText.setOrigin(0, 0);
|
||||
headerText.setPositionRelative(headerBg, 8, 4);
|
||||
this.runContainer.add(headerText);
|
||||
const runName = addTextObject(0, 0, this.runInfo.name, TextStyle.WINDOW);
|
||||
runName.setOrigin(0, 0);
|
||||
const runNameX = headerText.width / 6 + headerText.x + 4;
|
||||
runName.setPositionRelative(headerBg, runNameX, 4);
|
||||
this.runContainer.add(runName);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -702,10 +708,7 @@ export class RunInfoUiHandler extends UiHandler {
|
||||
rules.push(i18next.t("challenges:inverseBattle.shortName"));
|
||||
break;
|
||||
default: {
|
||||
const localizationKey = Challenges[this.runInfo.challenges[i].id]
|
||||
.split("_")
|
||||
.map((f, i) => (i ? `${f[0]}${f.slice(1).toLowerCase()}` : f.toLowerCase()))
|
||||
.join("");
|
||||
const localizationKey = toCamelCase(Challenges[this.runInfo.challenges[i].id]);
|
||||
rules.push(i18next.t(`challenges:${localizationKey}.name`));
|
||||
break;
|
||||
}
|
||||
|
@ -1,12 +1,14 @@
|
||||
import { GameMode } from "#app/game-mode";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import { Button } from "#enums/buttons";
|
||||
import { GameModes } from "#enums/game-modes";
|
||||
import { TextStyle } from "#enums/text-style";
|
||||
import { UiMode } from "#enums/ui-mode";
|
||||
// biome-ignore lint/performance/noNamespaceImport: See `src/system/game-data.ts`
|
||||
import * as Modifier from "#modifiers/modifier";
|
||||
import type { SessionSaveData } from "#system/game-data";
|
||||
import type { PokemonData } from "#system/pokemon-data";
|
||||
import type { OptionSelectConfig } from "#ui/abstract-option-select-ui-handler";
|
||||
import { MessageUiHandler } from "#ui/message-ui-handler";
|
||||
import { RunDisplayMode } from "#ui/run-info-ui-handler";
|
||||
import { addTextObject } from "#ui/text";
|
||||
@ -15,7 +17,7 @@ import { fixedInt, formatLargeNumber, getPlayTimeString, isNullOrUndefined } fro
|
||||
import i18next from "i18next";
|
||||
|
||||
const SESSION_SLOTS_COUNT = 5;
|
||||
const SLOTS_ON_SCREEN = 3;
|
||||
const SLOTS_ON_SCREEN = 2;
|
||||
|
||||
export enum SaveSlotUiMode {
|
||||
LOAD,
|
||||
@ -33,6 +35,7 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
|
||||
|
||||
private uiMode: SaveSlotUiMode;
|
||||
private saveSlotSelectCallback: SaveSlotSelectCallback | null;
|
||||
protected manageDataConfig: OptionSelectConfig;
|
||||
|
||||
private scrollCursor = 0;
|
||||
|
||||
@ -101,6 +104,7 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
|
||||
|
||||
processInput(button: Button): boolean {
|
||||
const ui = this.getUi();
|
||||
const manageDataOptions: any[] = [];
|
||||
|
||||
let success = false;
|
||||
let error = false;
|
||||
@ -109,14 +113,115 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
|
||||
const originalCallback = this.saveSlotSelectCallback;
|
||||
if (button === Button.ACTION) {
|
||||
const cursor = this.cursor + this.scrollCursor;
|
||||
if (this.uiMode === SaveSlotUiMode.LOAD && !this.sessionSlots[cursor].hasData) {
|
||||
const sessionSlot = this.sessionSlots[cursor];
|
||||
if (this.uiMode === SaveSlotUiMode.LOAD && !sessionSlot.hasData) {
|
||||
error = true;
|
||||
} else {
|
||||
switch (this.uiMode) {
|
||||
case SaveSlotUiMode.LOAD:
|
||||
this.saveSlotSelectCallback = null;
|
||||
originalCallback?.(cursor);
|
||||
if (!sessionSlot.malformed) {
|
||||
manageDataOptions.push({
|
||||
label: i18next.t("menu:loadGame"),
|
||||
handler: () => {
|
||||
globalScene.ui.revertMode();
|
||||
originalCallback?.(cursor);
|
||||
return true;
|
||||
},
|
||||
keepOpen: false,
|
||||
});
|
||||
|
||||
manageDataOptions.push({
|
||||
label: i18next.t("saveSlotSelectUiHandler:renameRun"),
|
||||
handler: () => {
|
||||
globalScene.ui.revertMode();
|
||||
ui.setOverlayMode(
|
||||
UiMode.RENAME_RUN,
|
||||
{
|
||||
buttonActions: [
|
||||
(sanitizedName: string) => {
|
||||
const name = decodeURIComponent(atob(sanitizedName));
|
||||
globalScene.gameData.renameSession(cursor, name).then(response => {
|
||||
if (response[0] === false) {
|
||||
globalScene.reset(true);
|
||||
} else {
|
||||
this.clearSessionSlots();
|
||||
this.cursorObj = null;
|
||||
this.populateSessionSlots();
|
||||
this.setScrollCursor(0);
|
||||
this.setCursor(0);
|
||||
ui.revertMode();
|
||||
ui.showText("", 0);
|
||||
}
|
||||
});
|
||||
},
|
||||
() => {
|
||||
ui.revertMode();
|
||||
},
|
||||
],
|
||||
},
|
||||
"",
|
||||
);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
this.manageDataConfig = {
|
||||
xOffset: 0,
|
||||
yOffset: 48,
|
||||
options: manageDataOptions,
|
||||
maxOptions: 4,
|
||||
};
|
||||
|
||||
manageDataOptions.push({
|
||||
label: i18next.t("saveSlotSelectUiHandler:deleteRun"),
|
||||
handler: () => {
|
||||
globalScene.ui.revertMode();
|
||||
ui.showText(i18next.t("saveSlotSelectUiHandler:deleteData"), null, () => {
|
||||
ui.setOverlayMode(
|
||||
UiMode.CONFIRM,
|
||||
() => {
|
||||
globalScene.gameData.tryClearSession(cursor).then(response => {
|
||||
if (response[0] === false) {
|
||||
globalScene.reset(true);
|
||||
} else {
|
||||
this.clearSessionSlots();
|
||||
this.cursorObj = null;
|
||||
this.populateSessionSlots();
|
||||
this.setScrollCursor(0);
|
||||
this.setCursor(0);
|
||||
ui.revertMode();
|
||||
ui.showText("", 0);
|
||||
}
|
||||
});
|
||||
},
|
||||
() => {
|
||||
ui.revertMode();
|
||||
ui.showText("", 0);
|
||||
},
|
||||
false,
|
||||
0,
|
||||
19,
|
||||
import.meta.env.DEV ? 300 : 2000,
|
||||
);
|
||||
});
|
||||
return true;
|
||||
},
|
||||
keepOpen: false,
|
||||
});
|
||||
|
||||
manageDataOptions.push({
|
||||
label: i18next.t("menuUiHandler:cancel"),
|
||||
handler: () => {
|
||||
globalScene.ui.revertMode();
|
||||
return true;
|
||||
},
|
||||
keepOpen: true,
|
||||
});
|
||||
|
||||
ui.setOverlayMode(UiMode.MENU_OPTION_SELECT, this.manageDataConfig);
|
||||
break;
|
||||
|
||||
case SaveSlotUiMode.SAVE: {
|
||||
const saveAndCallback = () => {
|
||||
const originalCallback = this.saveSlotSelectCallback;
|
||||
@ -161,6 +266,7 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
|
||||
}
|
||||
} else {
|
||||
this.saveSlotSelectCallback = null;
|
||||
ui.showText("", 0);
|
||||
originalCallback?.(-1);
|
||||
success = true;
|
||||
}
|
||||
@ -267,33 +373,34 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
|
||||
this.cursorObj = globalScene.add.container(0, 0);
|
||||
const cursorBox = globalScene.add.nineslice(
|
||||
0,
|
||||
0,
|
||||
15,
|
||||
"select_cursor_highlight_thick",
|
||||
undefined,
|
||||
296,
|
||||
44,
|
||||
294,
|
||||
this.sessionSlots[prevSlotIndex ?? 0]?.saveData?.name ? 50 : 60,
|
||||
6,
|
||||
6,
|
||||
6,
|
||||
6,
|
||||
);
|
||||
const rightArrow = globalScene.add.image(0, 0, "cursor");
|
||||
rightArrow.setPosition(160, 0);
|
||||
rightArrow.setPosition(160, 15);
|
||||
rightArrow.setName("rightArrow");
|
||||
this.cursorObj.add([cursorBox, rightArrow]);
|
||||
this.sessionSlotsContainer.add(this.cursorObj);
|
||||
}
|
||||
const cursorPosition = cursor + this.scrollCursor;
|
||||
const cursorIncrement = cursorPosition * 56;
|
||||
const cursorIncrement = cursorPosition * 76;
|
||||
if (this.sessionSlots[cursorPosition] && this.cursorObj) {
|
||||
const hasData = this.sessionSlots[cursorPosition].hasData;
|
||||
const session = this.sessionSlots[cursorPosition];
|
||||
const hasData = session.hasData && !session.malformed;
|
||||
// If the session slot lacks session data, it does not move from its default, central position.
|
||||
// Only session slots with session data will move leftwards and have a visible arrow.
|
||||
if (!hasData) {
|
||||
this.cursorObj.setPosition(151, 26 + cursorIncrement);
|
||||
this.cursorObj.setPosition(151, 20 + cursorIncrement);
|
||||
this.sessionSlots[cursorPosition].setPosition(0, cursorIncrement);
|
||||
} else {
|
||||
this.cursorObj.setPosition(145, 26 + cursorIncrement);
|
||||
this.cursorObj.setPosition(145, 20 + cursorIncrement);
|
||||
this.sessionSlots[cursorPosition].setPosition(-6, cursorIncrement);
|
||||
}
|
||||
this.setArrowVisibility(hasData);
|
||||
@ -311,7 +418,8 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
|
||||
revertSessionSlot(slotIndex: number): void {
|
||||
const sessionSlot = this.sessionSlots[slotIndex];
|
||||
if (sessionSlot) {
|
||||
sessionSlot.setPosition(0, slotIndex * 56);
|
||||
const valueHeight = 76;
|
||||
sessionSlot.setPosition(0, slotIndex * valueHeight);
|
||||
}
|
||||
}
|
||||
|
||||
@ -340,7 +448,7 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
|
||||
this.setCursor(this.cursor, prevSlotIndex);
|
||||
globalScene.tweens.add({
|
||||
targets: this.sessionSlotsContainer,
|
||||
y: this.sessionSlotsContainerInitialY - 56 * scrollCursor,
|
||||
y: this.sessionSlotsContainerInitialY - 76 * scrollCursor,
|
||||
duration: fixedInt(325),
|
||||
ease: "Sine.easeInOut",
|
||||
});
|
||||
@ -374,12 +482,14 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
|
||||
class SessionSlot extends Phaser.GameObjects.Container {
|
||||
public slotId: number;
|
||||
public hasData: boolean;
|
||||
/** Indicates the save slot ran into an error while being loaded */
|
||||
public malformed: boolean;
|
||||
private slotWindow: Phaser.GameObjects.NineSlice;
|
||||
private loadingLabel: Phaser.GameObjects.Text;
|
||||
|
||||
public saveData: SessionSaveData;
|
||||
|
||||
constructor(slotId: number) {
|
||||
super(globalScene, 0, slotId * 56);
|
||||
super(globalScene, 0, slotId * 76);
|
||||
|
||||
this.slotId = slotId;
|
||||
|
||||
@ -387,32 +497,89 @@ class SessionSlot extends Phaser.GameObjects.Container {
|
||||
}
|
||||
|
||||
setup() {
|
||||
const slotWindow = addWindow(0, 0, 304, 52);
|
||||
this.add(slotWindow);
|
||||
this.slotWindow = addWindow(0, 0, 304, 70);
|
||||
this.add(this.slotWindow);
|
||||
|
||||
this.loadingLabel = addTextObject(152, 26, i18next.t("saveSlotSelectUiHandler:loading"), TextStyle.WINDOW);
|
||||
this.loadingLabel = addTextObject(152, 33, i18next.t("saveSlotSelectUiHandler:loading"), TextStyle.WINDOW);
|
||||
this.loadingLabel.setOrigin(0.5, 0.5);
|
||||
this.add(this.loadingLabel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a name for sessions that don't have a name yet.
|
||||
* @param data - The {@linkcode SessionSaveData} being checked
|
||||
* @returns The default name for the given data.
|
||||
*/
|
||||
decideFallback(data: SessionSaveData): string {
|
||||
let fallbackName = `${GameMode.getModeName(data.gameMode)}`;
|
||||
switch (data.gameMode) {
|
||||
case GameModes.CLASSIC:
|
||||
fallbackName += ` (${globalScene.gameData.gameStats.classicSessionsPlayed + 1})`;
|
||||
break;
|
||||
case GameModes.ENDLESS:
|
||||
case GameModes.SPLICED_ENDLESS:
|
||||
fallbackName += ` (${globalScene.gameData.gameStats.endlessSessionsPlayed + 1})`;
|
||||
break;
|
||||
case GameModes.DAILY: {
|
||||
const runDay = new Date(data.timestamp).toLocaleDateString();
|
||||
fallbackName += ` (${runDay})`;
|
||||
break;
|
||||
}
|
||||
case GameModes.CHALLENGE: {
|
||||
const activeChallenges = data.challenges.filter(c => c.value !== 0);
|
||||
if (activeChallenges.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
fallbackName = "";
|
||||
for (const challenge of activeChallenges.slice(0, 3)) {
|
||||
if (fallbackName !== "") {
|
||||
fallbackName += ", ";
|
||||
}
|
||||
fallbackName += challenge.toChallenge().getName();
|
||||
}
|
||||
|
||||
if (activeChallenges.length > 3) {
|
||||
fallbackName += ", ...";
|
||||
} else if (fallbackName === "") {
|
||||
// Something went wrong when retrieving the names of the active challenges,
|
||||
// so fall back to just naming the run "Challenge"
|
||||
fallbackName = `${GameMode.getModeName(data.gameMode)}`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return fallbackName;
|
||||
}
|
||||
|
||||
async setupWithData(data: SessionSaveData) {
|
||||
const hasName = data?.name;
|
||||
this.remove(this.loadingLabel, true);
|
||||
if (hasName) {
|
||||
const nameLabel = addTextObject(8, 5, data.name, TextStyle.WINDOW);
|
||||
this.add(nameLabel);
|
||||
} else {
|
||||
const fallbackName = this.decideFallback(data);
|
||||
await globalScene.gameData.renameSession(this.slotId, fallbackName);
|
||||
const nameLabel = addTextObject(8, 5, fallbackName, TextStyle.WINDOW);
|
||||
this.add(nameLabel);
|
||||
}
|
||||
|
||||
const gameModeLabel = addTextObject(
|
||||
8,
|
||||
5,
|
||||
19,
|
||||
`${GameMode.getModeName(data.gameMode) || i18next.t("gameMode:unknown")} - ${i18next.t("saveSlotSelectUiHandler:wave")} ${data.waveIndex}`,
|
||||
TextStyle.WINDOW,
|
||||
);
|
||||
this.add(gameModeLabel);
|
||||
|
||||
const timestampLabel = addTextObject(8, 19, new Date(data.timestamp).toLocaleString(), TextStyle.WINDOW);
|
||||
const timestampLabel = addTextObject(8, 33, new Date(data.timestamp).toLocaleString(), TextStyle.WINDOW);
|
||||
this.add(timestampLabel);
|
||||
|
||||
const playTimeLabel = addTextObject(8, 33, getPlayTimeString(data.playTime), TextStyle.WINDOW);
|
||||
const playTimeLabel = addTextObject(8, 47, getPlayTimeString(data.playTime), TextStyle.WINDOW);
|
||||
this.add(playTimeLabel);
|
||||
|
||||
const pokemonIconsContainer = globalScene.add.container(144, 4);
|
||||
const pokemonIconsContainer = globalScene.add.container(144, 16);
|
||||
data.party.forEach((p: PokemonData, i: number) => {
|
||||
const iconContainer = globalScene.add.container(26 * i, 0);
|
||||
iconContainer.setScale(0.75);
|
||||
@ -427,13 +594,9 @@ class SessionSlot extends Phaser.GameObjects.Container {
|
||||
TextStyle.PARTY,
|
||||
{ fontSize: "54px", color: "#f8f8f8" },
|
||||
);
|
||||
text.setShadow(0, 0, undefined);
|
||||
text.setStroke("#424242", 14);
|
||||
text.setOrigin(1, 0);
|
||||
|
||||
iconContainer.add(icon);
|
||||
iconContainer.add(text);
|
||||
text.setShadow(0, 0, undefined).setStroke("#424242", 14).setOrigin(1, 0);
|
||||
|
||||
iconContainer.add([icon, text]);
|
||||
pokemonIconsContainer.add(iconContainer);
|
||||
|
||||
pokemon.destroy();
|
||||
@ -441,7 +604,7 @@ class SessionSlot extends Phaser.GameObjects.Container {
|
||||
|
||||
this.add(pokemonIconsContainer);
|
||||
|
||||
const modifierIconsContainer = globalScene.add.container(148, 30);
|
||||
const modifierIconsContainer = globalScene.add.container(148, 38);
|
||||
modifierIconsContainer.setScale(0.5);
|
||||
let visibleModifierIndex = 0;
|
||||
for (const m of data.modifiers) {
|
||||
@ -464,22 +627,33 @@ class SessionSlot extends Phaser.GameObjects.Container {
|
||||
|
||||
load(): Promise<boolean> {
|
||||
return new Promise<boolean>(resolve => {
|
||||
globalScene.gameData.getSession(this.slotId).then(async sessionData => {
|
||||
// Ignore the results if the view was exited
|
||||
if (!this.active) {
|
||||
return;
|
||||
}
|
||||
if (!sessionData) {
|
||||
this.hasData = false;
|
||||
this.loadingLabel.setText(i18next.t("saveSlotSelectUiHandler:empty"));
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
this.hasData = true;
|
||||
this.saveData = sessionData;
|
||||
await this.setupWithData(sessionData);
|
||||
resolve(true);
|
||||
});
|
||||
globalScene.gameData
|
||||
.getSession(this.slotId)
|
||||
.then(async sessionData => {
|
||||
// Ignore the results if the view was exited
|
||||
if (!this.active) {
|
||||
return;
|
||||
}
|
||||
this.hasData = !!sessionData;
|
||||
if (!sessionData) {
|
||||
this.loadingLabel.setText(i18next.t("saveSlotSelectUiHandler:empty"));
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
this.saveData = sessionData;
|
||||
this.setupWithData(sessionData);
|
||||
resolve(true);
|
||||
})
|
||||
.catch(e => {
|
||||
if (!this.active) {
|
||||
return;
|
||||
}
|
||||
console.warn(`Failed to load session slot #${this.slotId}:`, e);
|
||||
this.loadingLabel.setText(i18next.t("menu:failedToLoadSession"));
|
||||
this.hasData = true;
|
||||
this.malformed = true;
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -45,6 +45,7 @@ import type { Variant } from "#sprites/variant";
|
||||
import { getVariantIcon, getVariantTint } from "#sprites/variant";
|
||||
import { achvs } from "#system/achv";
|
||||
import type { DexAttrProps, StarterAttributes, StarterMoveset } from "#system/game-data";
|
||||
import { RibbonData } from "#system/ribbons/ribbon-data";
|
||||
import { SettingKeyboard } from "#system/settings-keyboard";
|
||||
import type { DexEntry } from "#types/dex-data";
|
||||
import type { OptionSelectItem } from "#ui/abstract-option-select-ui-handler";
|
||||
@ -3226,6 +3227,8 @@ export class StarterSelectUiHandler extends MessageUiHandler {
|
||||
onScreenFirstIndex + maxRows * maxColumns - 1,
|
||||
);
|
||||
|
||||
const gameData = globalScene.gameData;
|
||||
|
||||
this.starterSelectScrollBar.setScrollCursor(this.scrollCursor);
|
||||
|
||||
let pokerusCursorIndex = 0;
|
||||
@ -3265,9 +3268,9 @@ export class StarterSelectUiHandler extends MessageUiHandler {
|
||||
|
||||
container.label.setVisible(true);
|
||||
const speciesVariants =
|
||||
speciesId && globalScene.gameData.dexData[speciesId].caughtAttr & DexAttr.SHINY
|
||||
speciesId && gameData.dexData[speciesId].caughtAttr & DexAttr.SHINY
|
||||
? [DexAttr.DEFAULT_VARIANT, DexAttr.VARIANT_2, DexAttr.VARIANT_3].filter(
|
||||
v => !!(globalScene.gameData.dexData[speciesId].caughtAttr & v),
|
||||
v => !!(gameData.dexData[speciesId].caughtAttr & v),
|
||||
)
|
||||
: [];
|
||||
for (let v = 0; v < 3; v++) {
|
||||
@ -3282,12 +3285,15 @@ export class StarterSelectUiHandler extends MessageUiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
container.starterPassiveBgs.setVisible(!!globalScene.gameData.starterData[speciesId].passiveAttr);
|
||||
container.starterPassiveBgs.setVisible(!!gameData.starterData[speciesId].passiveAttr);
|
||||
container.hiddenAbilityIcon.setVisible(
|
||||
!!globalScene.gameData.dexData[speciesId].caughtAttr &&
|
||||
!!(globalScene.gameData.starterData[speciesId].abilityAttr & 4),
|
||||
!!gameData.dexData[speciesId].caughtAttr && !!(gameData.starterData[speciesId].abilityAttr & 4),
|
||||
);
|
||||
container.classicWinIcon.setVisible(globalScene.gameData.starterData[speciesId].classicWinCount > 0);
|
||||
container.classicWinIcon
|
||||
.setVisible(gameData.starterData[speciesId].classicWinCount > 0)
|
||||
.setTexture(
|
||||
gameData.dexData[speciesId].ribbons.has(RibbonData.NUZLOCKE) ? "champion_ribbon_emerald" : "champion_ribbon",
|
||||
);
|
||||
container.favoriteIcon.setVisible(this.starterPreferences[speciesId]?.favorite ?? false);
|
||||
|
||||
// 'Candy Icon' mode
|
||||
|
@ -31,7 +31,7 @@ export class TestDialogueUiHandler extends FormModalUiHandler {
|
||||
// we check for null or undefined here as per above - the typeof is still an object but the value is null so we need to exit out of this and pass the null key
|
||||
|
||||
// Return in the format expected by i18next
|
||||
return middleKey ? `${topKey}:${middleKey.map(m => m).join(".")}.${t}` : `${topKey}:${t}`;
|
||||
return middleKey ? `${topKey}:${middleKey.join(".")}.${t}` : `${topKey}:${t}`;
|
||||
}
|
||||
})
|
||||
.filter(t => t);
|
||||
|
@ -60,6 +60,7 @@ import { addWindow } from "#ui/ui-theme";
|
||||
import { UnavailableModalUiHandler } from "#ui/unavailable-modal-ui-handler";
|
||||
import { executeIf } from "#utils/common";
|
||||
import i18next from "i18next";
|
||||
import { RenameRunFormUiHandler } from "./rename-run-ui-handler";
|
||||
|
||||
const transitionModes = [
|
||||
UiMode.SAVE_SLOT,
|
||||
@ -98,6 +99,7 @@ const noTransitionModes = [
|
||||
UiMode.SESSION_RELOAD,
|
||||
UiMode.UNAVAILABLE,
|
||||
UiMode.RENAME_POKEMON,
|
||||
UiMode.RENAME_RUN,
|
||||
UiMode.TEST_DIALOGUE,
|
||||
UiMode.AUTO_COMPLETE,
|
||||
UiMode.ADMIN,
|
||||
@ -168,6 +170,7 @@ export class UI extends Phaser.GameObjects.Container {
|
||||
new UnavailableModalUiHandler(),
|
||||
new GameChallengesUiHandler(),
|
||||
new RenameFormUiHandler(),
|
||||
new RenameRunFormUiHandler(),
|
||||
new RunHistoryUiHandler(),
|
||||
new RunInfoUiHandler(),
|
||||
new TestDialogueUiHandler(UiMode.TEST_DIALOGUE),
|
||||
|
@ -4,6 +4,7 @@ import { pokemonEvolutions } from "#balance/pokemon-evolutions";
|
||||
import { pokemonFormChanges } from "#data/pokemon-forms";
|
||||
import type { PokemonSpecies } from "#data/pokemon-species";
|
||||
import { ChallengeType } from "#enums/challenge-type";
|
||||
import { Challenges } from "#enums/challenges";
|
||||
import type { MoveId } from "#enums/move-id";
|
||||
import type { MoveSourceType } from "#enums/move-source-type";
|
||||
import type { SpeciesId } from "#enums/species-id";
|
||||
@ -378,7 +379,7 @@ export function checkStarterValidForChallenge(species: PokemonSpecies, props: De
|
||||
* @param soft - If `true`, allow it if it could become valid through a form change.
|
||||
* @returns `true` if the species is considered valid.
|
||||
*/
|
||||
function checkSpeciesValidForChallenge(species: PokemonSpecies, props: DexAttrProps, soft: boolean) {
|
||||
export function checkSpeciesValidForChallenge(species: PokemonSpecies, props: DexAttrProps, soft: boolean) {
|
||||
const isValidForChallenge = new BooleanHolder(true);
|
||||
applyChallenges(ChallengeType.STARTER_CHOICE, species, isValidForChallenge, props);
|
||||
if (!soft || !pokemonFormChanges.hasOwnProperty(species.speciesId)) {
|
||||
@ -407,3 +408,28 @@ function checkSpeciesValidForChallenge(species: PokemonSpecies, props: DexAttrPr
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/** @returns Whether the current game mode meets the criteria to be considered a Nuzlocke challenge */
|
||||
export function isNuzlockeChallenge(): boolean {
|
||||
let isFreshStart = false;
|
||||
let isLimitedCatch = false;
|
||||
let isHardcore = false;
|
||||
for (const challenge of globalScene.gameMode.challenges) {
|
||||
// value is 0 if challenge is not active
|
||||
if (!challenge.value) {
|
||||
continue;
|
||||
}
|
||||
switch (challenge.id) {
|
||||
case Challenges.FRESH_START:
|
||||
isFreshStart = true;
|
||||
break;
|
||||
case Challenges.LIMITED_CATCH:
|
||||
isLimitedCatch = true;
|
||||
break;
|
||||
case Challenges.HARDCORE:
|
||||
isHardcore = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return isFreshStart && isLimitedCatch && isHardcore;
|
||||
}
|
||||
|
@ -45,17 +45,17 @@ export function deepMergeSpriteData(dest: object, source: object) {
|
||||
}
|
||||
|
||||
export function encrypt(data: string, bypassLogin: boolean): string {
|
||||
return (bypassLogin
|
||||
? (data: string) => btoa(encodeURIComponent(data))
|
||||
: (data: string) => AES.encrypt(data, saveKey))(data) as unknown as string; // TODO: is this correct?
|
||||
if (bypassLogin) {
|
||||
return btoa(encodeURIComponent(data));
|
||||
}
|
||||
return AES.encrypt(data, saveKey).toString();
|
||||
}
|
||||
|
||||
export function decrypt(data: string, bypassLogin: boolean): string {
|
||||
return (
|
||||
bypassLogin
|
||||
? (data: string) => decodeURIComponent(atob(data))
|
||||
: (data: string) => AES.decrypt(data, saveKey).toString(enc.Utf8)
|
||||
)(data);
|
||||
if (bypassLogin) {
|
||||
return decodeURIComponent(atob(data));
|
||||
}
|
||||
return AES.decrypt(data, saveKey).toString(enc.Utf8);
|
||||
}
|
||||
|
||||
// the latest data saved/loaded for the Starter Preferences. Required to reduce read/writes. Initialize as "{}", since this is the default value and no data needs to be stored if present.
|
||||
|
27
test/@types/test-helpers.ts
Normal file
27
test/@types/test-helpers.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import type { AtLeastOne, NonFunctionPropertiesRecursive as nonFunc } from "#types/type-helpers";
|
||||
|
||||
/**
|
||||
* Helper type to admit an object containing the given properties
|
||||
* _and_ at least 1 other non-function property.
|
||||
* @example
|
||||
* ```ts
|
||||
* type foo = {
|
||||
* qux: 1 | 2 | 3,
|
||||
* bar: number,
|
||||
* baz: string
|
||||
* quux: () => void; // ignored!
|
||||
* }
|
||||
*
|
||||
* type quxAndSomethingElse = OneOther<foo, "qux">
|
||||
*
|
||||
* const good1: quxAndSomethingElse = {qux: 1, bar: 3} // OK!
|
||||
* const good2: quxAndSomethingElse = {qux: 2, baz: "4", bar: 12} // OK!
|
||||
* const bad1: quxAndSomethingElse = {baz: "4", bar: 12} // Errors because `qux` is required
|
||||
* const bad2: quxAndSomethingElse = {qux: 1} // Errors because at least 1 thing _other_ than `qux` is required
|
||||
* ```
|
||||
* @typeParam O - The object to source keys from
|
||||
* @typeParam K - One or more of O's keys to render mandatory
|
||||
*/
|
||||
export type OneOther<O extends object, K extends keyof O> = AtLeastOne<Omit<nonFunc<O>, K>> & {
|
||||
[key in K]: O[K];
|
||||
};
|
140
test/@types/vitest.d.ts
vendored
140
test/@types/vitest.d.ts
vendored
@ -1,23 +1,32 @@
|
||||
import type { TerrainType } from "#app/data/terrain";
|
||||
import type Overrides from "#app/overrides";
|
||||
import type { ArenaTag } from "#data/arena-tag";
|
||||
import type { PositionalTag } from "#data/positional-tags/positional-tag";
|
||||
import type { AbilityId } from "#enums/ability-id";
|
||||
import type { ArenaTagSide } from "#enums/arena-tag-side";
|
||||
import type { ArenaTagType } from "#enums/arena-tag-type";
|
||||
import type { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import type { MoveId } from "#enums/move-id";
|
||||
import type { PokemonType } from "#enums/pokemon-type";
|
||||
import type { PositionalTagType } from "#enums/positional-tag-type";
|
||||
import type { BattleStat, EffectiveStat, Stat } from "#enums/stat";
|
||||
import type { StatusEffect } from "#enums/status-effect";
|
||||
import type { WeatherType } from "#enums/weather-type";
|
||||
import type { Arena } from "#field/arena";
|
||||
import type { Pokemon } from "#field/pokemon";
|
||||
import type { ToHaveEffectiveStatMatcherOptions } from "#test/test-utils/matchers/to-have-effective-stat";
|
||||
import type { PokemonMove } from "#moves/pokemon-move";
|
||||
import type { toHaveArenaTagOptions } from "#test/test-utils/matchers/to-have-arena-tag";
|
||||
import type { toHaveEffectiveStatOptions } from "#test/test-utils/matchers/to-have-effective-stat";
|
||||
import type { toHavePositionalTagOptions } from "#test/test-utils/matchers/to-have-positional-tag";
|
||||
import type { expectedStatusType } from "#test/test-utils/matchers/to-have-status-effect";
|
||||
import type { toHaveTypesOptions } from "#test/test-utils/matchers/to-have-types";
|
||||
import type { TurnMove } from "#types/turn-move";
|
||||
import type { AtLeastOne } from "#types/type-helpers";
|
||||
import type { toDmgValue } from "utils/common";
|
||||
import type { expect } from "vitest";
|
||||
import type Overrides from "#app/overrides";
|
||||
import type { PokemonMove } from "#moves/pokemon-move";
|
||||
|
||||
declare module "vitest" {
|
||||
interface Assertion {
|
||||
interface Assertion<T> {
|
||||
/**
|
||||
* Check whether an array contains EXACTLY the given items (in any order).
|
||||
*
|
||||
@ -27,45 +36,9 @@ declare module "vitest" {
|
||||
* @param expected - The expected contents of the array, in any order
|
||||
* @see {@linkcode expect.arrayContaining}
|
||||
*/
|
||||
toEqualArrayUnsorted<E>(expected: E[]): void;
|
||||
toEqualArrayUnsorted(expected: T[]): void;
|
||||
|
||||
/**
|
||||
* Check whether a {@linkcode Pokemon}'s current typing includes the given types.
|
||||
*
|
||||
* @param expected - The expected types (in any order)
|
||||
* @param options - The options passed to the matcher
|
||||
*/
|
||||
toHaveTypes(expected: [PokemonType, ...PokemonType[]], options?: toHaveTypesOptions): void;
|
||||
|
||||
/**
|
||||
* Matcher to check the contents of a {@linkcode Pokemon}'s move history.
|
||||
*
|
||||
* @param expectedValue - The expected value; can be a {@linkcode MoveId} or a partially filled {@linkcode TurnMove}
|
||||
* containing the desired properties to check
|
||||
* @param index - The index of the move history entry to check, in order from most recent to least recent.
|
||||
* Default `0` (last used move)
|
||||
* @see {@linkcode Pokemon.getLastXMoves}
|
||||
*/
|
||||
toHaveUsedMove(expected: MoveId | AtLeastOne<TurnMove>, index?: number): void;
|
||||
|
||||
/**
|
||||
* Check whether a {@linkcode Pokemon}'s effective stat is as expected
|
||||
* (checked after all stat value modifications).
|
||||
*
|
||||
* @param stat - The {@linkcode EffectiveStat} to check
|
||||
* @param expectedValue - The expected value of {@linkcode stat}
|
||||
* @param options - (Optional) The {@linkcode ToHaveEffectiveStatMatcherOptions}
|
||||
* @remarks
|
||||
* If you want to check the stat **before** modifiers are applied, use {@linkcode Pokemon.getStat} instead.
|
||||
*/
|
||||
toHaveEffectiveStat(stat: EffectiveStat, expectedValue: number, options?: ToHaveEffectiveStatMatcherOptions): void;
|
||||
|
||||
/**
|
||||
* Check whether a {@linkcode Pokemon} has taken a specific amount of damage.
|
||||
* @param expectedDamageTaken - The expected amount of damage taken
|
||||
* @param roundDown - Whether to round down {@linkcode expectedDamageTaken} with {@linkcode toDmgValue}; default `true`
|
||||
*/
|
||||
toHaveTakenDamage(expectedDamageTaken: number, roundDown?: boolean): void;
|
||||
// #region Arena Matchers
|
||||
|
||||
/**
|
||||
* Check whether the current {@linkcode WeatherType} is as expected.
|
||||
@ -80,9 +53,60 @@ declare module "vitest" {
|
||||
toHaveTerrain(expectedTerrainType: TerrainType): void;
|
||||
|
||||
/**
|
||||
* Check whether a {@linkcode Pokemon} is at full HP.
|
||||
* Check whether the current {@linkcode Arena} contains the given {@linkcode ArenaTag}.
|
||||
* @param expectedTag - A partially-filled {@linkcode ArenaTag} containing the desired properties
|
||||
*/
|
||||
toHaveFullHp(): void;
|
||||
toHaveArenaTag<A extends ArenaTagType>(expectedTag: toHaveArenaTagOptions<A>): void;
|
||||
/**
|
||||
* Check whether the current {@linkcode Arena} contains the given {@linkcode ArenaTag}.
|
||||
* @param expectedType - The {@linkcode ArenaTagType} of the desired tag
|
||||
* @param side - The {@linkcode ArenaTagSide | side(s) of the field} the tag should affect; default {@linkcode ArenaTagSide.BOTH}
|
||||
*/
|
||||
toHaveArenaTag(expectedType: ArenaTagType, side?: ArenaTagSide): void;
|
||||
|
||||
/**
|
||||
* Check whether the current {@linkcode Arena} contains the given {@linkcode PositionalTag}.
|
||||
* @param expectedTag - A partially-filled `PositionalTag` containing the desired properties
|
||||
*/
|
||||
toHavePositionalTag<P extends PositionalTagType>(expectedTag: toHavePositionalTagOptions<P>): void;
|
||||
/**
|
||||
* Check whether the current {@linkcode Arena} contains the given number of {@linkcode PositionalTag}s.
|
||||
* @param expectedType - The {@linkcode PositionalTagType} of the desired tag
|
||||
* @param count - The number of instances of {@linkcode expectedType} that should be active;
|
||||
* defaults to `1` and must be within the range `[0, 4]`
|
||||
*/
|
||||
toHavePositionalTag(expectedType: PositionalTagType, count?: number): void;
|
||||
|
||||
// #endregion Arena Matchers
|
||||
|
||||
// #region Pokemon Matchers
|
||||
|
||||
/**
|
||||
* Check whether a {@linkcode Pokemon}'s current typing includes the given types.
|
||||
* @param expectedTypes - The expected {@linkcode PokemonType}s to check against; must have length `>0`
|
||||
* @param options - The {@linkcode toHaveTypesOptions | options} passed to the matcher
|
||||
*/
|
||||
toHaveTypes(expectedTypes: PokemonType[], options?: toHaveTypesOptions): void;
|
||||
|
||||
/**
|
||||
* Check whether a {@linkcode Pokemon} has used a move matching the given criteria.
|
||||
* @param expectedMove - The {@linkcode MoveId} the Pokemon is expected to have used,
|
||||
* or a partially filled {@linkcode TurnMove} containing the desired properties to check
|
||||
* @param index - The index of the move history entry to check, in order from most recent to least recent; default `0`
|
||||
* @see {@linkcode Pokemon.getLastXMoves}
|
||||
*/
|
||||
toHaveUsedMove(expectedMove: MoveId | AtLeastOne<TurnMove>, index?: number): void;
|
||||
|
||||
/**
|
||||
* Check whether a {@linkcode Pokemon}'s effective stat is as expected
|
||||
* (checked after all stat value modifications).
|
||||
* @param stat - The {@linkcode EffectiveStat} to check
|
||||
* @param expectedValue - The expected value of {@linkcode stat}
|
||||
* @param options - The {@linkcode toHaveEffectiveStatOptions | options} passed to the matcher
|
||||
* @remarks
|
||||
* If you want to check the stat **before** modifiers are applied, use {@linkcode Pokemon.getStat} instead.
|
||||
*/
|
||||
toHaveEffectiveStat(stat: EffectiveStat, expectedValue: number, options?: toHaveEffectiveStatOptions): void;
|
||||
|
||||
/**
|
||||
* Check whether a {@linkcode Pokemon} has a specific {@linkcode StatusEffect | non-volatile status effect}.
|
||||
@ -106,7 +130,7 @@ declare module "vitest" {
|
||||
|
||||
/**
|
||||
* Check whether a {@linkcode Pokemon} has applied a specific {@linkcode AbilityId}.
|
||||
* @param expectedAbilityId - The expected {@linkcode AbilityId}
|
||||
* @param expectedAbilityId - The `AbilityId` to check for
|
||||
*/
|
||||
toHaveAbilityApplied(expectedAbilityId: AbilityId): void;
|
||||
|
||||
@ -116,24 +140,36 @@ declare module "vitest" {
|
||||
*/
|
||||
toHaveHp(expectedHp: number): void;
|
||||
|
||||
/**
|
||||
* Check whether a {@linkcode Pokemon} has taken a specific amount of damage.
|
||||
* @param expectedDamageTaken - The expected amount of damage taken
|
||||
* @param roundDown - Whether to round down `expectedDamageTaken` with {@linkcode toDmgValue}; default `true`
|
||||
*/
|
||||
toHaveTakenDamage(expectedDamageTaken: number, roundDown?: boolean): void;
|
||||
|
||||
/**
|
||||
* Check whether a {@linkcode Pokemon} is currently fainted (as determined by {@linkcode Pokemon.isFainted}).
|
||||
* @remarks
|
||||
* When checking whether an enemy wild Pokemon is fainted, one must reference it in a variable _before_ the fainting effect occurs
|
||||
* as otherwise the Pokemon will be GC'ed and rendered `undefined`.
|
||||
* When checking whether an enemy wild Pokemon is fainted, one must store a reference to it in a variable _before_ the fainting effect occurs.
|
||||
* Otherwise, the Pokemon will be removed from the field and garbage collected.
|
||||
*/
|
||||
toHaveFainted(): void;
|
||||
|
||||
/**
|
||||
* Check whether a {@linkcode Pokemon} is at full HP.
|
||||
*/
|
||||
toHaveFullHp(): void;
|
||||
/**
|
||||
* Check whether a {@linkcode Pokemon} has consumed the given amount of PP for one of its moves.
|
||||
* @param expectedValue - The {@linkcode MoveId} of the {@linkcode PokemonMove} that should have consumed PP
|
||||
* @param moveId - The {@linkcode MoveId} corresponding to the {@linkcode PokemonMove} that should have consumed PP
|
||||
* @param ppUsed - The numerical amount of PP that should have been consumed,
|
||||
* or `all` to indicate the move should be _out_ of PP
|
||||
* @remarks
|
||||
* If the Pokemon's moveset has been set via {@linkcode Overrides.MOVESET_OVERRIDE}/{@linkcode Overrides.OPP_MOVESET_OVERRIDE},
|
||||
* does not contain {@linkcode expectedMove}
|
||||
* or contains the desired move more than once, this will fail the test.
|
||||
* If the Pokemon's moveset has been set via {@linkcode Overrides.MOVESET_OVERRIDE}/{@linkcode Overrides.ENEMY_MOVESET_OVERRIDE}
|
||||
* or does not contain exactly one copy of `moveId`, this will fail the test.
|
||||
*/
|
||||
toHaveUsedPP(expectedMove: MoveId, ppUsed: number | "all"): void;
|
||||
toHaveUsedPP(moveId: MoveId, ppUsed: number | "all"): void;
|
||||
|
||||
// #endregion Pokemon Matchers
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,10 @@
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { Challenges } from "#enums/challenges";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
|
||||
import { PokeballType } from "#enums/pokeball";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { runMysteryEncounterToEnd } from "#test/mystery-encounter/encounter-test-utils";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
@ -52,4 +54,18 @@ describe("Challenges - Limited Catch", () => {
|
||||
|
||||
expect(game.scene.getPlayerParty()).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("should allow gift Pokémon from Mystery Encounters to be added to party", async () => {
|
||||
game.override
|
||||
.mysteryEncounterChance(100)
|
||||
.mysteryEncounter(MysteryEncounterType.THE_POKEMON_SALESMAN)
|
||||
.startingWave(12);
|
||||
game.scene.money = 20000;
|
||||
|
||||
await game.challengeMode.runToSummon([SpeciesId.NUZLEAF]);
|
||||
|
||||
await runMysteryEncounterToEnd(game, 1);
|
||||
|
||||
expect(game.scene.getPlayerParty()).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
@ -3,6 +3,7 @@ import { Challenges } from "#enums/challenges";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { UiMode } from "#enums/ui-mode";
|
||||
import { ExpBoosterModifier } from "#modifiers/modifier";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler";
|
||||
import Phaser from "phaser";
|
||||
@ -75,6 +76,7 @@ describe("Challenges - Limited Support", () => {
|
||||
await game.doKillOpponents();
|
||||
await game.toNextWave();
|
||||
|
||||
expect(game.scene.getModifiers(ExpBoosterModifier)).toHaveLength(1);
|
||||
expect(playerPokemon).not.toHaveFullHp();
|
||||
|
||||
game.move.use(MoveId.SPLASH);
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { toEqualArrayUnsorted } from "#test/test-utils/matchers/to-equal-array-unsorted";
|
||||
import { toHaveAbilityApplied } from "#test/test-utils/matchers/to-have-ability-applied";
|
||||
import { toHaveArenaTag } from "#test/test-utils/matchers/to-have-arena-tag";
|
||||
import { toHaveBattlerTag } from "#test/test-utils/matchers/to-have-battler-tag";
|
||||
import { toHaveEffectiveStat } from "#test/test-utils/matchers/to-have-effective-stat";
|
||||
import { toHaveFainted } from "#test/test-utils/matchers/to-have-fainted";
|
||||
import { toHaveFullHp } from "#test/test-utils/matchers/to-have-full-hp";
|
||||
import { toHaveHp } from "#test/test-utils/matchers/to-have-hp";
|
||||
import { toHavePositionalTag } from "#test/test-utils/matchers/to-have-positional-tag";
|
||||
import { toHaveStatStage } from "#test/test-utils/matchers/to-have-stat-stage";
|
||||
import { toHaveStatusEffect } from "#test/test-utils/matchers/to-have-status-effect";
|
||||
import { toHaveTakenDamage } from "#test/test-utils/matchers/to-have-taken-damage";
|
||||
@ -22,18 +24,20 @@ import { expect } from "vitest";
|
||||
|
||||
expect.extend({
|
||||
toEqualArrayUnsorted,
|
||||
toHaveWeather,
|
||||
toHaveTerrain,
|
||||
toHaveArenaTag,
|
||||
toHavePositionalTag,
|
||||
toHaveTypes,
|
||||
toHaveUsedMove,
|
||||
toHaveEffectiveStat,
|
||||
toHaveTakenDamage,
|
||||
toHaveWeather,
|
||||
toHaveTerrain,
|
||||
toHaveFullHp,
|
||||
toHaveStatusEffect,
|
||||
toHaveStatStage,
|
||||
toHaveBattlerTag,
|
||||
toHaveAbilityApplied,
|
||||
toHaveHp,
|
||||
toHaveTakenDamage,
|
||||
toHaveFullHp,
|
||||
toHaveFainted,
|
||||
toHaveUsedPP,
|
||||
});
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import { Status } from "#data/status-effect";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { BattleType } from "#enums/battle-type";
|
||||
@ -179,18 +178,13 @@ describe("Moves - Whirlwind", () => {
|
||||
const eligibleEnemy = enemyParty.filter(p => p.hp > 0 && p.isAllowedInBattle());
|
||||
expect(eligibleEnemy.length).toBe(1);
|
||||
|
||||
// Spy on the queueMessage function
|
||||
const queueSpy = vi.spyOn(globalScene.phaseManager, "queueMessage");
|
||||
|
||||
// Player uses Whirlwind; opponent uses Splash
|
||||
game.move.select(MoveId.WHIRLWIND);
|
||||
await game.move.selectEnemyMove(MoveId.SPLASH);
|
||||
await game.toNextTurn();
|
||||
|
||||
// Verify that the failure message is displayed for Whirlwind
|
||||
expect(queueSpy).toHaveBeenCalledWith(expect.stringContaining("But it failed"));
|
||||
// Verify the opponent's Splash message
|
||||
expect(queueSpy).toHaveBeenCalledWith(expect.stringContaining("But nothing happened!"));
|
||||
const player = game.field.getPlayerPokemon();
|
||||
expect(player).toHaveUsedMove({ move: MoveId.WHIRLWIND, result: MoveResult.FAIL });
|
||||
});
|
||||
|
||||
it("should not pull in the other trainer's pokemon in a partner trainer battle", async () => {
|
||||
|
@ -39,15 +39,6 @@ describe("Move - Wish", () => {
|
||||
.enemyLevel(100);
|
||||
});
|
||||
|
||||
/**
|
||||
* Expect that wish is active with the specified number of attacks.
|
||||
* @param numAttacks - The number of wish instances that should be queued; default `1`
|
||||
*/
|
||||
function expectWishActive(numAttacks = 1) {
|
||||
const wishes = game.scene.arena.positionalTagManager["tags"].filter(t => t.tagType === PositionalTagType.WISH);
|
||||
expect(wishes).toHaveLength(numAttacks);
|
||||
}
|
||||
|
||||
it("should heal the Pokemon in the current slot for 50% of the user's maximum HP", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.ALOMOMOLA, SpeciesId.BLISSEY]);
|
||||
|
||||
@ -58,19 +49,19 @@ describe("Move - Wish", () => {
|
||||
game.move.use(MoveId.WISH);
|
||||
await game.toNextTurn();
|
||||
|
||||
expectWishActive();
|
||||
expect(game).toHavePositionalTag(PositionalTagType.WISH);
|
||||
|
||||
game.doSwitchPokemon(1);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expectWishActive(0);
|
||||
expect(game).toHavePositionalTag(PositionalTagType.WISH, 0);
|
||||
expect(game.textInterceptor.logs).toContain(
|
||||
i18next.t("arenaTag:wishTagOnAdd", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(alomomola),
|
||||
}),
|
||||
);
|
||||
expect(alomomola.hp).toBe(1);
|
||||
expect(blissey.hp).toBe(toDmgValue(alomomola.getMaxHp() / 2) + 1);
|
||||
expect(alomomola).toHaveHp(1);
|
||||
expect(blissey).toHaveHp(toDmgValue(alomomola.getMaxHp() / 2) + 1);
|
||||
});
|
||||
|
||||
it("should work if the user has full HP, but not if it already has an active Wish", async () => {
|
||||
@ -82,13 +73,13 @@ describe("Move - Wish", () => {
|
||||
game.move.use(MoveId.WISH);
|
||||
await game.toNextTurn();
|
||||
|
||||
expectWishActive();
|
||||
expect(game).toHavePositionalTag(PositionalTagType.WISH);
|
||||
|
||||
game.move.use(MoveId.WISH);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(alomomola.hp).toBe(toDmgValue(alomomola.getMaxHp() / 2) + 1);
|
||||
expect(alomomola.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
expect(alomomola).toHaveUsedMove({ result: MoveResult.FAIL });
|
||||
});
|
||||
|
||||
it("should function independently of Future Sight", async () => {
|
||||
@ -103,7 +94,8 @@ describe("Move - Wish", () => {
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.toNextTurn();
|
||||
|
||||
expectWishActive(1);
|
||||
expect(game).toHavePositionalTag(PositionalTagType.WISH);
|
||||
expect(game).toHavePositionalTag(PositionalTagType.DELAYED_ATTACK);
|
||||
});
|
||||
|
||||
it("should work in double battles and trigger in order of creation", async () => {
|
||||
@ -127,7 +119,7 @@ describe("Move - Wish", () => {
|
||||
await game.setTurnOrder(oldOrder.map(p => p.getBattlerIndex()));
|
||||
await game.toNextTurn();
|
||||
|
||||
expectWishActive(4);
|
||||
expect(game).toHavePositionalTag(PositionalTagType.WISH, 4);
|
||||
|
||||
// Lower speed to change turn order
|
||||
alomomola.setStatStage(Stat.SPD, 6);
|
||||
@ -141,7 +133,7 @@ describe("Move - Wish", () => {
|
||||
await game.phaseInterceptor.to("PositionalTagPhase");
|
||||
|
||||
// all wishes have activated and added healing phases
|
||||
expectWishActive(0);
|
||||
expect(game).toHavePositionalTag(PositionalTagType.WISH, 0);
|
||||
|
||||
const healPhases = game.scene.phaseManager.phaseQueue.filter(p => p.is("PokemonHealPhase"));
|
||||
expect(healPhases).toHaveLength(4);
|
||||
@ -165,14 +157,14 @@ describe("Move - Wish", () => {
|
||||
game.move.use(MoveId.WISH, BattlerIndex.PLAYER_2);
|
||||
await game.toNextTurn();
|
||||
|
||||
expectWishActive();
|
||||
expect(game).toHavePositionalTag(PositionalTagType.WISH);
|
||||
|
||||
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER);
|
||||
game.move.use(MoveId.MEMENTO, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY_2);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
// Wish went away without doing anything
|
||||
expectWishActive(0);
|
||||
expect(game).toHavePositionalTag(PositionalTagType.WISH, 0);
|
||||
expect(game.textInterceptor.logs).not.toContain(
|
||||
i18next.t("arenaTag:wishTagOnAdd", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(blissey),
|
||||
|
@ -112,7 +112,7 @@ describe("Weird Dream - Mystery Encounter", () => {
|
||||
it("should transform the new party into new species, 2 at +90/+110, the rest at +40/50 BST", async () => {
|
||||
await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty);
|
||||
|
||||
const pokemonPrior = scene.getPlayerParty().map(pokemon => pokemon);
|
||||
const pokemonPrior = scene.getPlayerParty().slice();
|
||||
const bstsPrior = pokemonPrior.map(species => species.getSpeciesForm().getBaseStatTotal());
|
||||
|
||||
await runMysteryEncounterToEnd(game, 1);
|
||||
|
82
test/system/rename-run.test.ts
Normal file
82
test/system/rename-run.test.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import * as account from "#app/account";
|
||||
import * as bypassLoginModule from "#app/global-vars/bypass-login";
|
||||
import { pokerogueApi } from "#app/plugins/api/pokerogue-api";
|
||||
import type { SessionSaveData } from "#app/system/game-data";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
describe("System - Rename Run", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.moveset([MoveId.SPLASH])
|
||||
.battleStyle("single")
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
describe("renameSession", () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(bypassLoginModule, "bypassLogin", "get").mockReturnValue(false);
|
||||
vi.spyOn(account, "updateUserInfo").mockImplementation(async () => [true, 1]);
|
||||
});
|
||||
|
||||
it("should return false if slotId < 0", async () => {
|
||||
const result = await game.scene.gameData.renameSession(-1, "Named Run");
|
||||
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
|
||||
it("should return false if getSession returns null", async () => {
|
||||
vi.spyOn(game.scene.gameData, "getSession").mockResolvedValue(null as unknown as SessionSaveData);
|
||||
|
||||
const result = await game.scene.gameData.renameSession(-1, "Named Run");
|
||||
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
|
||||
it("should return true if bypassLogin is true", async () => {
|
||||
vi.spyOn(bypassLoginModule, "bypassLogin", "get").mockReturnValue(true);
|
||||
vi.spyOn(game.scene.gameData, "getSession").mockResolvedValue({} as SessionSaveData);
|
||||
|
||||
const result = await game.scene.gameData.renameSession(0, "Named Run");
|
||||
|
||||
expect(result).toEqual(true);
|
||||
});
|
||||
|
||||
it("should return false if api returns error", async () => {
|
||||
vi.spyOn(game.scene.gameData, "getSession").mockResolvedValue({} as SessionSaveData);
|
||||
vi.spyOn(pokerogueApi.savedata.session, "update").mockResolvedValue("Unknown Error!");
|
||||
|
||||
const result = await game.scene.gameData.renameSession(0, "Named Run");
|
||||
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
|
||||
it("should return true if api is succesfull", async () => {
|
||||
vi.spyOn(game.scene.gameData, "getSession").mockResolvedValue({} as SessionSaveData);
|
||||
vi.spyOn(pokerogueApi.savedata.session, "update").mockResolvedValue("");
|
||||
|
||||
const result = await game.scene.gameData.renameSession(0, "Named Run");
|
||||
|
||||
expect(result).toEqual(true);
|
||||
expect(account.updateUserInfo).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
@ -224,7 +224,7 @@ export class GameManager {
|
||||
// This will consider all battle entry dialog as seens and skip them
|
||||
vi.spyOn(this.scene.ui, "shouldSkipDialogue").mockReturnValue(true);
|
||||
|
||||
if (overrides.OPP_HELD_ITEMS_OVERRIDE.length === 0) {
|
||||
if (overrides.ENEMY_HELD_ITEMS_OVERRIDE.length === 0) {
|
||||
this.removeEnemyHeldItems();
|
||||
}
|
||||
|
||||
|
@ -50,7 +50,7 @@ export class ChallengeModeHelper extends GameManagerHelper {
|
||||
});
|
||||
|
||||
await this.game.phaseInterceptor.to("EncounterPhase");
|
||||
if (overrides.OPP_HELD_ITEMS_OVERRIDE.length === 0 && this.game.override.removeEnemyStartingItems) {
|
||||
if (overrides.ENEMY_HELD_ITEMS_OVERRIDE.length === 0 && this.game.override.removeEnemyStartingItems) {
|
||||
this.game.removeEnemyHeldItems();
|
||||
}
|
||||
}
|
||||
|
@ -53,7 +53,7 @@ export class ClassicModeHelper extends GameManagerHelper {
|
||||
});
|
||||
|
||||
await this.game.phaseInterceptor.to(EncounterPhase);
|
||||
if (overrides.OPP_HELD_ITEMS_OVERRIDE.length === 0 && this.game.override.removeEnemyStartingItems) {
|
||||
if (overrides.ENEMY_HELD_ITEMS_OVERRIDE.length === 0 && this.game.override.removeEnemyStartingItems) {
|
||||
this.game.removeEnemyHeldItems();
|
||||
}
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ export class DailyModeHelper extends GameManagerHelper {
|
||||
|
||||
await this.game.phaseInterceptor.to(EncounterPhase);
|
||||
|
||||
if (overrides.OPP_HELD_ITEMS_OVERRIDE.length === 0 && this.game.override.removeEnemyStartingItems) {
|
||||
if (overrides.ENEMY_HELD_ITEMS_OVERRIDE.length === 0 && this.game.override.removeEnemyStartingItems) {
|
||||
this.game.removeEnemyHeldItems();
|
||||
}
|
||||
}
|
||||
|
@ -228,8 +228,8 @@ export class MoveHelper extends GameManagerHelper {
|
||||
console.warn("Player moveset override disabled due to use of `game.move.changeMoveset`!");
|
||||
}
|
||||
} else {
|
||||
if (coerceArray(Overrides.OPP_MOVESET_OVERRIDE).length > 0) {
|
||||
vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([]);
|
||||
if (coerceArray(Overrides.ENEMY_MOVESET_OVERRIDE).length > 0) {
|
||||
vi.spyOn(Overrides, "ENEMY_MOVESET_OVERRIDE", "get").mockReturnValue([]);
|
||||
console.warn("Enemy moveset override disabled due to use of `game.move.changeMoveset`!");
|
||||
}
|
||||
}
|
||||
@ -302,8 +302,8 @@ export class MoveHelper extends GameManagerHelper {
|
||||
(this.game.scene.phaseManager.getCurrentPhase() as EnemyCommandPhase).getFieldIndex()
|
||||
];
|
||||
|
||||
if ([Overrides.OPP_MOVESET_OVERRIDE].flat().length > 0) {
|
||||
vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([]);
|
||||
if ([Overrides.ENEMY_MOVESET_OVERRIDE].flat().length > 0) {
|
||||
vi.spyOn(Overrides, "ENEMY_MOVESET_OVERRIDE", "get").mockReturnValue([]);
|
||||
console.warn(
|
||||
"Warning: `forceEnemyMove` overwrites the Pokemon's moveset and disables the enemy moveset override!",
|
||||
);
|
||||
|
@ -406,7 +406,7 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
* @returns `this`
|
||||
*/
|
||||
public enemySpecies(species: SpeciesId | number): this {
|
||||
vi.spyOn(Overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(species);
|
||||
vi.spyOn(Overrides, "ENEMY_SPECIES_OVERRIDE", "get").mockReturnValue(species);
|
||||
this.log(`Enemy Pokemon species set to ${SpeciesId[species]} (=${species})!`);
|
||||
return this;
|
||||
}
|
||||
@ -416,7 +416,7 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
* @returns `this`
|
||||
*/
|
||||
public enableEnemyFusion(): this {
|
||||
vi.spyOn(Overrides, "OPP_FUSION_OVERRIDE", "get").mockReturnValue(true);
|
||||
vi.spyOn(Overrides, "ENEMY_FUSION_OVERRIDE", "get").mockReturnValue(true);
|
||||
this.log("Enemy Pokemon is a random fusion!");
|
||||
return this;
|
||||
}
|
||||
@ -427,7 +427,7 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
* @returns `this`
|
||||
*/
|
||||
public enemyFusionSpecies(species: SpeciesId | number): this {
|
||||
vi.spyOn(Overrides, "OPP_FUSION_SPECIES_OVERRIDE", "get").mockReturnValue(species);
|
||||
vi.spyOn(Overrides, "ENEMY_FUSION_SPECIES_OVERRIDE", "get").mockReturnValue(species);
|
||||
this.log(`Enemy Pokemon fusion species set to ${SpeciesId[species]} (=${species})!`);
|
||||
return this;
|
||||
}
|
||||
@ -438,7 +438,7 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
* @returns `this`
|
||||
*/
|
||||
public enemyAbility(ability: AbilityId): this {
|
||||
vi.spyOn(Overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(ability);
|
||||
vi.spyOn(Overrides, "ENEMY_ABILITY_OVERRIDE", "get").mockReturnValue(ability);
|
||||
this.log(`Enemy Pokemon ability set to ${AbilityId[ability]} (=${ability})!`);
|
||||
return this;
|
||||
}
|
||||
@ -449,7 +449,7 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
* @returns `this`
|
||||
*/
|
||||
public enemyPassiveAbility(passiveAbility: AbilityId): this {
|
||||
vi.spyOn(Overrides, "OPP_PASSIVE_ABILITY_OVERRIDE", "get").mockReturnValue(passiveAbility);
|
||||
vi.spyOn(Overrides, "ENEMY_PASSIVE_ABILITY_OVERRIDE", "get").mockReturnValue(passiveAbility);
|
||||
this.log(`Enemy Pokemon PASSIVE ability set to ${AbilityId[passiveAbility]} (=${passiveAbility})!`);
|
||||
return this;
|
||||
}
|
||||
@ -460,7 +460,7 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
* @returns `this`
|
||||
*/
|
||||
public enemyHasPassiveAbility(hasPassiveAbility: boolean | null): this {
|
||||
vi.spyOn(Overrides, "OPP_HAS_PASSIVE_ABILITY_OVERRIDE", "get").mockReturnValue(hasPassiveAbility);
|
||||
vi.spyOn(Overrides, "ENEMY_HAS_PASSIVE_ABILITY_OVERRIDE", "get").mockReturnValue(hasPassiveAbility);
|
||||
if (hasPassiveAbility === null) {
|
||||
this.log("Enemy Pokemon PASSIVE ability no longer force enabled or disabled!");
|
||||
} else {
|
||||
@ -475,7 +475,7 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
* @returns `this`
|
||||
*/
|
||||
public enemyMoveset(moveset: MoveId | MoveId[]): this {
|
||||
vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue(moveset);
|
||||
vi.spyOn(Overrides, "ENEMY_MOVESET_OVERRIDE", "get").mockReturnValue(moveset);
|
||||
moveset = coerceArray(moveset);
|
||||
const movesetStr = moveset.map(moveId => MoveId[moveId]).join(", ");
|
||||
this.log(`Enemy Pokemon moveset set to ${movesetStr} (=[${moveset.join(", ")}])!`);
|
||||
@ -488,7 +488,7 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
* @returns `this`
|
||||
*/
|
||||
public enemyLevel(level: number): this {
|
||||
vi.spyOn(Overrides, "OPP_LEVEL_OVERRIDE", "get").mockReturnValue(level);
|
||||
vi.spyOn(Overrides, "ENEMY_LEVEL_OVERRIDE", "get").mockReturnValue(level);
|
||||
this.log(`Enemy Pokemon level set to ${level}!`);
|
||||
return this;
|
||||
}
|
||||
@ -499,7 +499,7 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
* @returns `this`
|
||||
*/
|
||||
public enemyStatusEffect(statusEffect: StatusEffect): this {
|
||||
vi.spyOn(Overrides, "OPP_STATUS_OVERRIDE", "get").mockReturnValue(statusEffect);
|
||||
vi.spyOn(Overrides, "ENEMY_STATUS_OVERRIDE", "get").mockReturnValue(statusEffect);
|
||||
this.log(`Enemy Pokemon status-effect set to ${StatusEffect[statusEffect]} (=${statusEffect})!`);
|
||||
return this;
|
||||
}
|
||||
@ -510,7 +510,7 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
* @returns `this`
|
||||
*/
|
||||
public enemyHeldItems(items: ModifierOverride[]): this {
|
||||
vi.spyOn(Overrides, "OPP_HELD_ITEMS_OVERRIDE", "get").mockReturnValue(items);
|
||||
vi.spyOn(Overrides, "ENEMY_HELD_ITEMS_OVERRIDE", "get").mockReturnValue(items);
|
||||
this.log("Enemy Pokemon held items set to:", items);
|
||||
return this;
|
||||
}
|
||||
@ -571,7 +571,7 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
* @param variant - (Optional) The enemy's shiny {@linkcode Variant}.
|
||||
*/
|
||||
enemyShiny(shininess: boolean | null, variant?: Variant): this {
|
||||
vi.spyOn(Overrides, "OPP_SHINY_OVERRIDE", "get").mockReturnValue(shininess);
|
||||
vi.spyOn(Overrides, "ENEMY_SHINY_OVERRIDE", "get").mockReturnValue(shininess);
|
||||
if (shininess === null) {
|
||||
this.log("Disabled enemy Pokemon shiny override!");
|
||||
} else {
|
||||
@ -579,7 +579,7 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
}
|
||||
|
||||
if (variant !== undefined) {
|
||||
vi.spyOn(Overrides, "OPP_VARIANT_OVERRIDE", "get").mockReturnValue(variant);
|
||||
vi.spyOn(Overrides, "ENEMY_VARIANT_OVERRIDE", "get").mockReturnValue(variant);
|
||||
this.log(`Set enemy shiny variant to be ${variant}!`);
|
||||
}
|
||||
return this;
|
||||
@ -594,7 +594,7 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
* @returns `this`
|
||||
*/
|
||||
public enemyHealthSegments(healthSegments: number): this {
|
||||
vi.spyOn(Overrides, "OPP_HEALTH_SEGMENTS_OVERRIDE", "get").mockReturnValue(healthSegments);
|
||||
vi.spyOn(Overrides, "ENEMY_HEALTH_SEGMENTS_OVERRIDE", "get").mockReturnValue(healthSegments);
|
||||
this.log("Enemy Pokemon health segments set to:", healthSegments);
|
||||
return this;
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { getOnelineDiffStr } from "#test/test-utils/string-utils";
|
||||
import { receivedStr } from "#test/test-utils/test-utils";
|
||||
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||
|
||||
/**
|
||||
@ -14,22 +15,22 @@ export function toEqualArrayUnsorted(
|
||||
): SyncExpectationResult {
|
||||
if (!Array.isArray(received)) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected an array, but got ${this.utils.stringify(received)}!`,
|
||||
pass: this.isNot,
|
||||
message: () => `Expected to receive an array, but got ${receivedStr(received)}!`,
|
||||
};
|
||||
}
|
||||
|
||||
if (received.length !== expected.length) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected to receive array of length ${received.length}, but got ${expected.length} instead!`,
|
||||
actual: received,
|
||||
message: () => `Expected to receive an array of length ${received.length}, but got ${expected.length} instead!`,
|
||||
expected,
|
||||
actual: received,
|
||||
};
|
||||
}
|
||||
|
||||
const actualSorted = received.slice().sort();
|
||||
const expectedSorted = expected.slice().sort();
|
||||
const actualSorted = received.toSorted();
|
||||
const expectedSorted = expected.toSorted();
|
||||
const pass = this.equals(actualSorted, expectedSorted, [...this.customTesters, this.utils.iterableEquality]);
|
||||
|
||||
const actualStr = getOnelineDiffStr.call(this, actualSorted);
|
||||
|
@ -21,8 +21,8 @@ export function toHaveAbilityApplied(
|
||||
): SyncExpectationResult {
|
||||
if (!isPokemonInstance(received)) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected to recieve a Pokemon, but got ${receivedStr(received)}!`,
|
||||
pass: this.isNot,
|
||||
message: () => `Expected to receive a Pokemon, but got ${receivedStr(received)}!`,
|
||||
};
|
||||
}
|
||||
|
||||
|
77
test/test-utils/matchers/to-have-arena-tag.ts
Normal file
77
test/test-utils/matchers/to-have-arena-tag.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import type { ArenaTag, ArenaTagTypeMap } from "#data/arena-tag";
|
||||
import type { ArenaTagSide } from "#enums/arena-tag-side";
|
||||
import type { ArenaTagType } from "#enums/arena-tag-type";
|
||||
import type { OneOther } from "#test/@types/test-helpers";
|
||||
// biome-ignore lint/correctness/noUnusedImports: TSDoc
|
||||
import type { GameManager } from "#test/test-utils/game-manager";
|
||||
import { getOnelineDiffStr } from "#test/test-utils/string-utils";
|
||||
import { isGameManagerInstance, receivedStr } from "#test/test-utils/test-utils";
|
||||
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||
|
||||
// intersection required to preserve T for inferences
|
||||
export type toHaveArenaTagOptions<T extends ArenaTagType> = OneOther<ArenaTagTypeMap[T], "tagType" | "side"> & {
|
||||
tagType: T;
|
||||
};
|
||||
|
||||
/**
|
||||
* Matcher to check if the {@linkcode Arena} has a given {@linkcode ArenaTag} active.
|
||||
* @param received - The object to check. Should be the current {@linkcode GameManager}.
|
||||
* @param expectedTag - The `ArenaTagType` of the desired tag, or a partially-filled object
|
||||
* containing the desired properties
|
||||
* @param side - The {@linkcode ArenaTagSide | side of the field} the tag should affect, or
|
||||
* {@linkcode ArenaTagSide.BOTH} to check both sides
|
||||
* @returns The result of the matching
|
||||
*/
|
||||
export function toHaveArenaTag<T extends ArenaTagType>(
|
||||
this: MatcherState,
|
||||
received: unknown,
|
||||
expectedTag: T | toHaveArenaTagOptions<T>,
|
||||
side?: ArenaTagSide,
|
||||
): SyncExpectationResult {
|
||||
if (!isGameManagerInstance(received)) {
|
||||
return {
|
||||
pass: this.isNot,
|
||||
message: () => `Expected to receive a GameManager, but got ${receivedStr(received)}!`,
|
||||
};
|
||||
}
|
||||
|
||||
if (!received.scene?.arena) {
|
||||
return {
|
||||
pass: this.isNot,
|
||||
message: () => `Expected GameManager.${received.scene ? "scene.arena" : "scene"} to be defined!`,
|
||||
};
|
||||
}
|
||||
|
||||
// Coerce lone `tagType`s into objects
|
||||
// Bangs are ok as we enforce safety via overloads
|
||||
// @ts-expect-error - Typescript is being stupid as tag type and side will always exist
|
||||
const etag: Partial<ArenaTag> & { tagType: T; side: ArenaTagSide } =
|
||||
typeof expectedTag === "object" ? expectedTag : { tagType: expectedTag, side: side! };
|
||||
|
||||
// We need to get all tags for the case of checking properties of a tag present on both sides of the arena
|
||||
const tags = received.scene.arena.findTagsOnSide(t => t.tagType === etag.tagType, etag.side);
|
||||
if (tags.length === 0) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected the Arena to have a tag of type ${etag.tagType}, but it didn't!`,
|
||||
expected: etag.tagType,
|
||||
actual: received.scene.arena.tags.map(t => t.tagType),
|
||||
};
|
||||
}
|
||||
|
||||
// Pass if any of the matching tags meet our criteria
|
||||
const pass = tags.some(tag =>
|
||||
this.equals(tag, expectedTag, [...this.customTesters, this.utils.subsetEquality, this.utils.iterableEquality]),
|
||||
);
|
||||
|
||||
const expectedStr = getOnelineDiffStr.call(this, expectedTag);
|
||||
return {
|
||||
pass,
|
||||
message: () =>
|
||||
pass
|
||||
? `Expected the Arena to NOT have a tag matching ${expectedStr}, but it did!`
|
||||
: `Expected the Arena to have a tag matching ${expectedStr}, but it didn't!`,
|
||||
expected: expectedTag,
|
||||
actual: tags,
|
||||
};
|
||||
}
|
@ -6,7 +6,7 @@ import { getStatName } from "#test/test-utils/string-utils";
|
||||
import { isPokemonInstance, receivedStr } from "#test/test-utils/test-utils";
|
||||
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||
|
||||
export interface ToHaveEffectiveStatMatcherOptions {
|
||||
export interface toHaveEffectiveStatOptions {
|
||||
/**
|
||||
* The target {@linkcode Pokemon}
|
||||
* @see {@linkcode Pokemon.getEffectiveStat}
|
||||
@ -30,7 +30,7 @@ export interface ToHaveEffectiveStatMatcherOptions {
|
||||
* @param received - The object to check. Should be a {@linkcode Pokemon}
|
||||
* @param stat - The {@linkcode EffectiveStat} to check
|
||||
* @param expectedValue - The expected value of the {@linkcode stat}
|
||||
* @param options - The {@linkcode ToHaveEffectiveStatMatcherOptions}
|
||||
* @param options - The {@linkcode toHaveEffectiveStatOptions}
|
||||
* @returns Whether the matcher passed
|
||||
*/
|
||||
export function toHaveEffectiveStat(
|
||||
@ -38,11 +38,11 @@ export function toHaveEffectiveStat(
|
||||
received: unknown,
|
||||
stat: EffectiveStat,
|
||||
expectedValue: number,
|
||||
{ enemy, move, isCritical = false }: ToHaveEffectiveStatMatcherOptions = {},
|
||||
{ enemy, move, isCritical = false }: toHaveEffectiveStatOptions = {},
|
||||
): SyncExpectationResult {
|
||||
if (!isPokemonInstance(received)) {
|
||||
return {
|
||||
pass: false,
|
||||
pass: this.isNot,
|
||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||
};
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||
export function toHaveFainted(this: MatcherState, received: unknown): SyncExpectationResult {
|
||||
if (!isPokemonInstance(received)) {
|
||||
return {
|
||||
pass: false,
|
||||
pass: this.isNot,
|
||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||
};
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||
export function toHaveFullHp(this: MatcherState, received: unknown): SyncExpectationResult {
|
||||
if (!isPokemonInstance(received)) {
|
||||
return {
|
||||
pass: false,
|
||||
pass: this.isNot,
|
||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||
};
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||
export function toHaveHp(this: MatcherState, received: unknown, expectedHp: number): SyncExpectationResult {
|
||||
if (!isPokemonInstance(received)) {
|
||||
return {
|
||||
pass: false,
|
||||
pass: this.isNot,
|
||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||
};
|
||||
}
|
||||
|
107
test/test-utils/matchers/to-have-positional-tag.ts
Normal file
107
test/test-utils/matchers/to-have-positional-tag.ts
Normal file
@ -0,0 +1,107 @@
|
||||
// biome-ignore-start lint/correctness/noUnusedImports: TSDoc
|
||||
import type { GameManager } from "#test/test-utils/game-manager";
|
||||
// biome-ignore-end lint/correctness/noUnusedImports: TSDoc
|
||||
|
||||
import type { serializedPosTagMap } from "#data/positional-tags/load-positional-tag";
|
||||
import type { PositionalTagType } from "#enums/positional-tag-type";
|
||||
import type { OneOther } from "#test/@types/test-helpers";
|
||||
import { getOnelineDiffStr } from "#test/test-utils/string-utils";
|
||||
import { isGameManagerInstance, receivedStr } from "#test/test-utils/test-utils";
|
||||
import { toTitleCase } from "#utils/strings";
|
||||
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||
|
||||
export type toHavePositionalTagOptions<P extends PositionalTagType> = OneOther<serializedPosTagMap[P], "tagType"> & {
|
||||
tagType: P;
|
||||
};
|
||||
|
||||
/**
|
||||
* Matcher to check if the {@linkcode Arena} has a certain number of {@linkcode PositionalTag}s active.
|
||||
* @param received - The object to check. Should be the current {@linkcode GameManager}
|
||||
* @param expectedTag - The {@linkcode PositionalTagType} of the desired tag, or a partially-filled {@linkcode PositionalTag}
|
||||
* containing the desired properties
|
||||
* @param count - The number of tags that should be active; defaults to `1` and must be within the range `[0, 4]`
|
||||
* @returns The result of the matching
|
||||
*/
|
||||
export function toHavePositionalTag<P extends PositionalTagType>(
|
||||
this: MatcherState,
|
||||
received: unknown,
|
||||
expectedTag: P | toHavePositionalTagOptions<P>,
|
||||
count = 1,
|
||||
): SyncExpectationResult {
|
||||
if (!isGameManagerInstance(received)) {
|
||||
return {
|
||||
pass: this.isNot,
|
||||
message: () => `Expected to receive a GameManager, but got ${receivedStr(received)}!`,
|
||||
};
|
||||
}
|
||||
|
||||
if (!received.scene?.arena?.positionalTagManager) {
|
||||
return {
|
||||
pass: this.isNot,
|
||||
message: () =>
|
||||
`Expected GameManager.${received.scene?.arena ? "scene.arena.positionalTagManager" : received.scene ? "scene.arena" : "scene"} to be defined!`,
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: Increase limit if triple battles are added
|
||||
if (count < 0 || count > 4) {
|
||||
return {
|
||||
pass: this.isNot,
|
||||
message: () => `Expected count to be between 0 and 4, but got ${count} instead!`,
|
||||
};
|
||||
}
|
||||
|
||||
const allTags = received.scene.arena.positionalTagManager.tags;
|
||||
const tagType = typeof expectedTag === "string" ? expectedTag : expectedTag.tagType;
|
||||
const matchingTags = allTags.filter(t => t.tagType === tagType);
|
||||
|
||||
// If checking exclusively tag type, check solely the number of matching tags on field
|
||||
if (typeof expectedTag === "string") {
|
||||
const pass = matchingTags.length === count;
|
||||
const expectedStr = getPosTagStr(expectedTag);
|
||||
|
||||
return {
|
||||
pass,
|
||||
message: () =>
|
||||
pass
|
||||
? `Expected the Arena to NOT have ${count} ${expectedStr} active, but it did!`
|
||||
: `Expected the Arena to have ${count} ${expectedStr} active, but got ${matchingTags.length} instead!`,
|
||||
expected: expectedTag,
|
||||
actual: allTags,
|
||||
};
|
||||
}
|
||||
|
||||
// Check for equality with the provided object
|
||||
if (matchingTags.length === 0) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected the Arena to have a tag of type ${expectedTag.tagType}, but it didn't!`,
|
||||
expected: expectedTag.tagType,
|
||||
actual: received.scene.arena.tags.map(t => t.tagType),
|
||||
};
|
||||
}
|
||||
|
||||
// Pass if any of the matching tags meet the criteria
|
||||
const pass = matchingTags.some(tag =>
|
||||
this.equals(tag, expectedTag, [...this.customTesters, this.utils.subsetEquality, this.utils.iterableEquality]),
|
||||
);
|
||||
|
||||
const expectedStr = getOnelineDiffStr.call(this, expectedTag);
|
||||
return {
|
||||
pass,
|
||||
message: () =>
|
||||
pass
|
||||
? `Expected the Arena to NOT have a tag matching ${expectedStr}, but it did!`
|
||||
: `Expected the Arena to have a tag matching ${expectedStr}, but it didn't!`,
|
||||
expected: expectedTag,
|
||||
actual: matchingTags,
|
||||
};
|
||||
}
|
||||
|
||||
function getPosTagStr(pType: PositionalTagType, count = 1): string {
|
||||
let ret = toTitleCase(pType) + "Tag";
|
||||
if (count > 1) {
|
||||
ret += "s";
|
||||
}
|
||||
return ret;
|
||||
}
|
@ -23,14 +23,14 @@ export function toHaveStatStage(
|
||||
): SyncExpectationResult {
|
||||
if (!isPokemonInstance(received)) {
|
||||
return {
|
||||
pass: false,
|
||||
pass: this.isNot,
|
||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||
};
|
||||
}
|
||||
|
||||
if (expectedStage < -6 || expectedStage > 6) {
|
||||
return {
|
||||
pass: false,
|
||||
pass: this.isNot,
|
||||
message: () => `Expected ${expectedStage} to be within the range [-6, 6]!`,
|
||||
};
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ export function toHaveStatusEffect(
|
||||
): SyncExpectationResult {
|
||||
if (!isPokemonInstance(received)) {
|
||||
return {
|
||||
pass: false,
|
||||
pass: this.isNot,
|
||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||
};
|
||||
}
|
||||
@ -37,10 +37,8 @@ export function toHaveStatusEffect(
|
||||
const actualEffect = received.status?.effect ?? StatusEffect.NONE;
|
||||
|
||||
// Check exclusively effect equality first, coercing non-matching status effects to numbers.
|
||||
if (actualEffect !== (expectedStatus as Exclude<typeof expectedStatus, StatusEffect>)?.effect) {
|
||||
// This is actually 100% safe as `expectedStatus?.effect` will evaluate to `undefined` if a StatusEffect was passed,
|
||||
// which will never match actualEffect by definition
|
||||
expectedStatus = (expectedStatus as Exclude<typeof expectedStatus, StatusEffect>).effect;
|
||||
if (typeof expectedStatus === "object" && actualEffect !== expectedStatus.effect) {
|
||||
expectedStatus = expectedStatus.effect;
|
||||
}
|
||||
|
||||
if (typeof expectedStatus === "number") {
|
||||
|
@ -24,7 +24,7 @@ export function toHaveTakenDamage(
|
||||
): SyncExpectationResult {
|
||||
if (!isPokemonInstance(received)) {
|
||||
return {
|
||||
pass: false,
|
||||
pass: this.isNot,
|
||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||
};
|
||||
}
|
||||
|
@ -20,15 +20,15 @@ export function toHaveTerrain(
|
||||
): SyncExpectationResult {
|
||||
if (!isGameManagerInstance(received)) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected GameManager, but got ${receivedStr(received)}!`,
|
||||
pass: this.isNot,
|
||||
message: () => `Expected to receive a GameManager, but got ${receivedStr(received)}!`,
|
||||
};
|
||||
}
|
||||
|
||||
if (!received.scene?.arena) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected GameManager.${received.scene ? "scene" : "scene.arena"} to be defined!`,
|
||||
pass: this.isNot,
|
||||
message: () => `Expected GameManager.${received.scene ? "scene.arena" : "scene"} to be defined!`,
|
||||
};
|
||||
}
|
||||
|
||||
@ -41,8 +41,8 @@ export function toHaveTerrain(
|
||||
pass,
|
||||
message: () =>
|
||||
pass
|
||||
? `Expected Arena to NOT have ${expectedStr} active, but it did!`
|
||||
: `Expected Arena to have ${expectedStr} active, but got ${actualStr} instead!`,
|
||||
? `Expected the Arena to NOT have ${expectedStr} active, but it did!`
|
||||
: `Expected the Arena to have ${expectedStr} active, but got ${actualStr} instead!`,
|
||||
expected: expectedTerrainType,
|
||||
actual,
|
||||
};
|
||||
|
@ -7,10 +7,16 @@ import { isPokemonInstance, receivedStr } from "../test-utils";
|
||||
|
||||
export interface toHaveTypesOptions {
|
||||
/**
|
||||
* Whether to enforce exact matches (`true`) or superset matches (`false`).
|
||||
* @defaultValue `true`
|
||||
* Value dictating the strength of the enforced typing match.
|
||||
*
|
||||
* Possible values (in ascending order of strength) are:
|
||||
* - `"ordered"`: Enforce that the {@linkcode Pokemon}'s types are identical **and in the same order**
|
||||
* - `"unordered"`: Enforce that the {@linkcode Pokemon}'s types are identical **without checking order**
|
||||
* - `"superset"`: Enforce that the {@linkcode Pokemon}'s types are **a superset of** the expected types
|
||||
* (all must be present, but extras can be there)
|
||||
* @defaultValue `"unordered"`
|
||||
*/
|
||||
exact?: boolean;
|
||||
mode?: "ordered" | "unordered" | "superset";
|
||||
/**
|
||||
* Optional arguments to pass to {@linkcode Pokemon.getTypes}.
|
||||
*/
|
||||
@ -18,35 +24,54 @@ export interface toHaveTypesOptions {
|
||||
}
|
||||
|
||||
/**
|
||||
* Matcher that checks if an array contains exactly the given items, disregarding order.
|
||||
* @param received - The object to check. Should be an array of one or more {@linkcode PokemonType}s.
|
||||
* @param options - The {@linkcode toHaveTypesOptions | options} for this matcher
|
||||
* Matcher that checks if a Pokemon's typing is as expected.
|
||||
* @param received - The object to check. Should be a {@linkcode Pokemon}
|
||||
* @param expectedTypes - An array of one or more {@linkcode PokemonType}s to compare against.
|
||||
* @param mode - The mode to perform the matching in.
|
||||
* Possible values (in ascending order of strength) are:
|
||||
* - `"ordered"`: Enforce that the {@linkcode Pokemon}'s types are identical **and in the same order**
|
||||
* - `"unordered"`: Enforce that the {@linkcode Pokemon}'s types are identical **without checking order**
|
||||
* - `"superset"`: Enforce that the {@linkcode Pokemon}'s types are **a superset of** the expected types
|
||||
* (all must be present, but extras can be there)
|
||||
*
|
||||
* Default `unordered`
|
||||
* @param args - Extra arguments passed to {@linkcode Pokemon.getTypes}
|
||||
* @returns The result of the matching
|
||||
*/
|
||||
export function toHaveTypes(
|
||||
this: MatcherState,
|
||||
received: unknown,
|
||||
expected: [PokemonType, ...PokemonType[]],
|
||||
options: toHaveTypesOptions = {},
|
||||
expectedTypes: [PokemonType, ...PokemonType[]],
|
||||
{ mode = "unordered", args = [] }: toHaveTypesOptions = {},
|
||||
): SyncExpectationResult {
|
||||
if (!isPokemonInstance(received)) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected to recieve a Pokémon, but got ${receivedStr(received)}!`,
|
||||
pass: this.isNot,
|
||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||
};
|
||||
}
|
||||
|
||||
const actualTypes = received.getTypes(...(options.args ?? [])).sort();
|
||||
const expectedTypes = expected.slice().sort();
|
||||
// Return early if no types were passed in
|
||||
if (expectedTypes.length === 0) {
|
||||
return {
|
||||
pass: this.isNot,
|
||||
message: () => "Expected to receive a non-empty array of PokemonTypes!",
|
||||
};
|
||||
}
|
||||
|
||||
// Avoid sorting the types if strict ordering is desired
|
||||
const actualSorted = mode === "ordered" ? received.getTypes(...args) : received.getTypes(...args).toSorted();
|
||||
const expectedSorted = mode === "ordered" ? expectedTypes : expectedTypes.toSorted();
|
||||
|
||||
// Exact matches do not care about subset equality
|
||||
const matchers = options.exact
|
||||
? [...this.customTesters, this.utils.iterableEquality]
|
||||
: [...this.customTesters, this.utils.subsetEquality, this.utils.iterableEquality];
|
||||
const pass = this.equals(actualTypes, expectedTypes, matchers);
|
||||
const matchers =
|
||||
mode === "superset"
|
||||
? [...this.customTesters, this.utils.iterableEquality]
|
||||
: [...this.customTesters, this.utils.subsetEquality, this.utils.iterableEquality];
|
||||
const pass = this.equals(actualSorted, expectedSorted, matchers);
|
||||
|
||||
const actualStr = stringifyEnumArray(PokemonType, actualTypes);
|
||||
const expectedStr = stringifyEnumArray(PokemonType, expectedTypes);
|
||||
const actualStr = stringifyEnumArray(PokemonType, actualSorted);
|
||||
const expectedStr = stringifyEnumArray(PokemonType, expectedSorted);
|
||||
const pkmName = getPokemonNameWithAffix(received);
|
||||
|
||||
return {
|
||||
@ -55,7 +80,7 @@ export function toHaveTypes(
|
||||
pass
|
||||
? `Expected ${pkmName} to NOT have types ${expectedStr}, but it did!`
|
||||
: `Expected ${pkmName} to have types ${expectedStr}, but got ${actualStr} instead!`,
|
||||
expected: expectedTypes,
|
||||
actual: actualTypes,
|
||||
expected: expectedSorted,
|
||||
actual: actualSorted,
|
||||
};
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||
/**
|
||||
* Matcher to check the contents of a {@linkcode Pokemon}'s move history.
|
||||
* @param received - The actual value received. Should be a {@linkcode Pokemon}
|
||||
* @param expectedValue - The {@linkcode MoveId} the Pokemon is expected to have used,
|
||||
* @param expectedMove - The {@linkcode MoveId} the Pokemon is expected to have used,
|
||||
* or a partially filled {@linkcode TurnMove} containing the desired properties to check
|
||||
* @param index - The index of the move history entry to check, in order from most recent to least recent.
|
||||
* Default `0` (last used move)
|
||||
@ -22,12 +22,12 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||
export function toHaveUsedMove(
|
||||
this: MatcherState,
|
||||
received: unknown,
|
||||
expectedResult: MoveId | AtLeastOne<TurnMove>,
|
||||
expectedMove: MoveId | AtLeastOne<TurnMove>,
|
||||
index = 0,
|
||||
): SyncExpectationResult {
|
||||
if (!isPokemonInstance(received)) {
|
||||
return {
|
||||
pass: false,
|
||||
pass: this.isNot,
|
||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||
};
|
||||
}
|
||||
@ -37,34 +37,33 @@ export function toHaveUsedMove(
|
||||
|
||||
if (move === undefined) {
|
||||
return {
|
||||
pass: false,
|
||||
pass: this.isNot,
|
||||
message: () => `Expected ${pkmName} to have used ${index + 1} moves, but it didn't!`,
|
||||
actual: received.getLastXMoves(-1),
|
||||
};
|
||||
}
|
||||
|
||||
// Coerce to a `TurnMove`
|
||||
if (typeof expectedResult === "number") {
|
||||
expectedResult = { move: expectedResult };
|
||||
if (typeof expectedMove === "number") {
|
||||
expectedMove = { move: expectedMove };
|
||||
}
|
||||
|
||||
const moveIndexStr = index === 0 ? "last move" : `${getOrdinal(index)} most recent move`;
|
||||
|
||||
const pass = this.equals(move, expectedResult, [
|
||||
const pass = this.equals(move, expectedMove, [
|
||||
...this.customTesters,
|
||||
this.utils.subsetEquality,
|
||||
this.utils.iterableEquality,
|
||||
]);
|
||||
|
||||
const expectedStr = getOnelineDiffStr.call(this, expectedResult);
|
||||
const expectedStr = getOnelineDiffStr.call(this, expectedMove);
|
||||
return {
|
||||
pass,
|
||||
message: () =>
|
||||
pass
|
||||
? `Expected ${pkmName}'s ${moveIndexStr} to NOT match ${expectedStr}, but it did!`
|
||||
: // Replace newlines with spaces to preserve one-line ness
|
||||
`Expected ${pkmName}'s ${moveIndexStr} to match ${expectedStr}, but it didn't!`,
|
||||
expected: expectedResult,
|
||||
: `Expected ${pkmName}'s ${moveIndexStr} to match ${expectedStr}, but it didn't!`,
|
||||
expected: expectedMove,
|
||||
actual: move,
|
||||
};
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||
/**
|
||||
* Matcher to check the amount of PP consumed by a {@linkcode Pokemon}.
|
||||
* @param received - The actual value received. Should be a {@linkcode Pokemon}
|
||||
* @param expectedValue - The {@linkcode MoveId} that should have consumed PP
|
||||
* @param moveId - The {@linkcode MoveId} that should have consumed PP
|
||||
* @param ppUsed - The numerical amount of PP that should have been consumed,
|
||||
* or `all` to indicate the move should be _out_ of PP
|
||||
* @returns Whether the matcher passed
|
||||
@ -23,35 +23,35 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||
export function toHaveUsedPP(
|
||||
this: MatcherState,
|
||||
received: unknown,
|
||||
expectedMove: MoveId,
|
||||
moveId: MoveId,
|
||||
ppUsed: number | "all",
|
||||
): SyncExpectationResult {
|
||||
if (!isPokemonInstance(received)) {
|
||||
return {
|
||||
pass: false,
|
||||
pass: this.isNot,
|
||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||
};
|
||||
}
|
||||
|
||||
const override = received.isPlayer() ? Overrides.MOVESET_OVERRIDE : Overrides.OPP_MOVESET_OVERRIDE;
|
||||
const override = received.isPlayer() ? Overrides.MOVESET_OVERRIDE : Overrides.ENEMY_MOVESET_OVERRIDE;
|
||||
if (coerceArray(override).length > 0) {
|
||||
return {
|
||||
pass: false,
|
||||
pass: this.isNot,
|
||||
message: () =>
|
||||
`Cannot test for PP consumption with ${received.isPlayer() ? "player" : "enemy"} moveset overrides active!`,
|
||||
};
|
||||
}
|
||||
|
||||
const pkmName = getPokemonNameWithAffix(received);
|
||||
const moveStr = getEnumStr(MoveId, expectedMove);
|
||||
const moveStr = getEnumStr(MoveId, moveId);
|
||||
|
||||
const movesetMoves = received.getMoveset().filter(pm => pm.moveId === expectedMove);
|
||||
const movesetMoves = received.getMoveset().filter(pm => pm.moveId === moveId);
|
||||
if (movesetMoves.length !== 1) {
|
||||
return {
|
||||
pass: false,
|
||||
pass: this.isNot,
|
||||
message: () =>
|
||||
`Expected MoveId.${moveStr} to appear in ${pkmName}'s moveset exactly once, but got ${movesetMoves.length} times!`,
|
||||
expected: expectedMove,
|
||||
expected: moveId,
|
||||
actual: received.getMoveset(),
|
||||
};
|
||||
}
|
||||
|
@ -20,15 +20,15 @@ export function toHaveWeather(
|
||||
): SyncExpectationResult {
|
||||
if (!isGameManagerInstance(received)) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected GameManager, but got ${receivedStr(received)}!`,
|
||||
pass: this.isNot,
|
||||
message: () => `Expected to receive a GameManager, but got ${receivedStr(received)}!`,
|
||||
};
|
||||
}
|
||||
|
||||
if (!received.scene?.arena) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected GameManager.${received.scene ? "scene" : "scene.arena"} to be defined!`,
|
||||
pass: this.isNot,
|
||||
message: () => `Expected GameManager.${received.scene ? "scene.arena" : "scene"} to be defined!`,
|
||||
};
|
||||
}
|
||||
|
||||
@ -41,8 +41,8 @@ export function toHaveWeather(
|
||||
pass,
|
||||
message: () =>
|
||||
pass
|
||||
? `Expected Arena to NOT have ${expectedStr} weather active, but it did!`
|
||||
: `Expected Arena to have ${expectedStr} weather active, but got ${actualStr} instead!`,
|
||||
? `Expected the Arena to NOT have ${expectedStr} weather active, but it did!`
|
||||
: `Expected the Arena to have ${expectedStr} weather active, but got ${actualStr} instead!`,
|
||||
expected: expectedWeatherType,
|
||||
actual,
|
||||
};
|
||||
|
@ -34,10 +34,10 @@ interface getEnumStrOptions {
|
||||
* @returns The stringified representation of `val` as dictated by the options.
|
||||
* @example
|
||||
* ```ts
|
||||
* enum fakeEnum {
|
||||
* ONE: 1,
|
||||
* TWO: 2,
|
||||
* THREE: 3,
|
||||
* enum testEnum {
|
||||
* ONE = 1,
|
||||
* TWO = 2,
|
||||
* THREE = 3,
|
||||
* }
|
||||
* getEnumStr(fakeEnum, fakeEnum.ONE); // Output: "ONE (=1)"
|
||||
* getEnumStr(fakeEnum, fakeEnum.TWO, {casing: "Title", prefix: "fakeEnum.", suffix: "!!!"}); // Output: "fakeEnum.TWO!!! (=2)"
|
||||
@ -174,10 +174,14 @@ export function getStatName(s: Stat): string {
|
||||
* Convert an object into a oneline diff to be shown in an error message.
|
||||
* @param obj - The object to return the oneline diff of
|
||||
* @returns The updated diff
|
||||
* @example
|
||||
* ```ts
|
||||
* const diff = getOnelineDiffStr.call(this, obj)
|
||||
* ```
|
||||
*/
|
||||
export function getOnelineDiffStr(this: MatcherState, obj: unknown): string {
|
||||
return this.utils
|
||||
.stringify(obj, undefined, { maxLength: 35, indent: 0, printBasicPrototype: false })
|
||||
.replace(/\n/g, " ") // Replace newlines with spaces
|
||||
.replace(/,(\s*)}$/g, "$1}");
|
||||
.replace(/,(\s*)}$/g, "$1}"); // Trim trailing commas
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user