Compare commits

...

18 Commits

Author SHA1 Message Date
fabske0
4886bb0655
Merge dea6442c65 into ee4950633e 2025-08-14 22:17:16 +02:00
Bertie690
ee4950633e
[Test] Added toHaveArenaTagMatcher + fixed prior matchers (#6205)
* [Test] Added `toHaveArenaTagMatcher` + fixed prior matchers

* Fixed imports and stuff

* Removed accidental test file addition

* More improvements and minor fixes

* More semantic changes

* Shuffled a few funcs around

* More fixups to strings

* Added `toHavePositionalTag` matcher

* Applied reviews and fixed my godawful penmanship

* Fix vitest.d.ts

* Fix imports in `vitest.d.ts`

---------

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
Co-authored-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com>
2025-08-14 13:16:23 -07:00
fabske0
dea6442c65 Merge branch 'beta' into locales-keys-camel-case-part-2 2025-08-14 22:12:09 +02:00
fabske0
662bcbe038 change ME texts locales uses 2025-08-14 21:29:46 +02:00
Sirz Benjie
30058ed70e
[Feature] Add per-species tracking for ribbons, show nuzlocke ribbon (#6246)
* Add tracking for nuzlocke completion

* Add ribbon to legacy ui folder

* Add tracking for friendship ribbon

* fix overlapping flag set

* Replace mass getters with a single method

* Add tracking for each generational ribbon

* Add ribbons for each challenge

* Apply Kev's suggestions from code review

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

---------

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
2025-08-14 13:20:48 -05:00
Wlowscha
140e4ab142
[UI/UX] Party slots refactor (#6199)
* constants for position of discard button

* Moved transfer/discard button up in doubles

* Fixed the various `.setOrigin(0,0)`

* Small clean up

* Added `isBenched` property to slots; x origin of `slotBg` is now 0

* Also set y origin to 0

* Offsets are relevant to the same thing

* Introducing const object to store ui magic numbers

* More magic numbers in const

* Laid out numbers for slot positions

* Added smaller main slots for transfer mode in doubles

* Changed background to fit new slot disposition

* Apply suggestions from code review

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com>

* Optimized PNGs

* Updated comment

* Removed "magicNumbers" container, added multiple comments

* Update src/ui/party-ui-handler.ts

Co-authored-by: Amani H. <109637146+xsn34kzx@users.noreply.github.com>

* Fainted pkmn slots displaying correctly

---------

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com>
Co-authored-by: Adri1 <adrien.grivel@hotmail.fr>
Co-authored-by: Amani H. <109637146+xsn34kzx@users.noreply.github.com>
Co-authored-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com>
2025-08-14 13:10:15 -05:00
fabske0
76d8357d0b
[Dev] Rename OPP_ overrides to ENEMY_ (#6255)
rename `OPP_` to `ENEMY_`
2025-08-14 18:06:24 +00:00
Bertie690
f42237d415
[Refactor] Removed map(x => x) (#6256)
* Enforced a few usages of `toCamelCase`

* Removed `map(x => x)`

* Removed more maps and sufff

* Update test/mystery-encounter/encounters/weird-dream-encounter.test.ts

* Update game-data.ts types to work
2025-08-14 10:25:44 -07:00
fabske0
b44f0a4176
[Refactor] Remove bgm param from arena constructor (#6254) 2025-08-14 16:52:56 +00:00
fabske0
a97fcab678 update menu-ui-handler locales use 2025-08-13 18:46:30 +02:00
fabske0
81a4e98a11 change growth locales use 2025-08-13 14:58:17 +02:00
fabske0
b2454234f6 change dialogue locales use 2025-08-13 12:27:35 +02:00
fabske0
111f661170 change misc dialogue locales use 2025-08-12 23:08:46 +02:00
fabske0
9d7b9f742e change double battle dialogue use 2025-08-12 18:24:35 +02:00
fabske0
35cf80705e change biome locales uses 2025-08-12 13:56:06 +02:00
fabske0
7dfe1ce216 change bgm name locales use 2025-08-11 17:49:56 +02:00
fabske0
1accc20b37 Change berry locales use 2025-08-11 11:47:20 +02:00
fabske0
64b9427f11 Change Achv locales 2025-08-10 22:08:52 +02:00
73 changed files with 1974 additions and 1212 deletions

View File

@ -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>>;
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 B

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

View 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$"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -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;
}

View File

@ -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;
}

View File

@ -104,6 +104,7 @@ import {
getLuckString,
getLuckTextTint,
getPartyLuckValue,
type ModifierType,
PokemonHeldItemModifierType,
} from "#modifiers/modifier-type";
import { MysteryEncounter } from "#mystery-encounters/mystery-encounter";
@ -943,17 +944,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 +1204,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 +1240,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 +1515,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 +1766,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 +2713,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)) {

View File

@ -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;

View File

@ -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) : "";
}

View File

@ -7,6 +7,7 @@ import { TimeOfDay } from "#enums/time-of-day";
import { TrainerType } from "#enums/trainer-type";
import { randSeedInt } from "#utils/common";
import { getEnumValues } from "#utils/enums";
import { toCamelCase } from "#utils/strings";
import i18next from "i18next";
export function getBiomeName(biome: BiomeId | -1) {
@ -15,13 +16,13 @@ export function getBiomeName(biome: BiomeId | -1) {
}
switch (biome) {
case BiomeId.GRASS:
return i18next.t("biome:GRASS");
return i18next.t("biome:grass");
case BiomeId.RUINS:
return i18next.t("biome:RUINS");
return i18next.t("biome:ruins");
case BiomeId.END:
return i18next.t("biome:END");
return i18next.t("biome:end");
default:
return i18next.t(`biome:${BiomeId[biome].toUpperCase()}`);
return i18next.t(`biome:${toCamelCase(BiomeId[biome])}`);
}
}

View File

@ -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;
}
});
}
}

View File

@ -11,11 +11,11 @@ import { NumberHolder, randSeedInt, toDmgValue } from "#utils/common";
import i18next from "i18next";
export function getBerryName(berryType: BerryType): string {
return i18next.t(`berry:${BerryType[berryType]}.name`);
return i18next.t(`berry:${BerryType[berryType].toLowerCase()}.name`);
}
export function getBerryEffectDescription(berryType: BerryType): string {
return i18next.t(`berry:${BerryType[berryType]}.effect`);
return i18next.t(`berry:${BerryType[berryType].toLowerCase()}.effect`);
}
export type BerryPredicate = (pokemon: Pokemon) => boolean;

View File

@ -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);
}

View File

@ -85,20 +85,16 @@ export const trainerTypeDialogue: TrainerTypeDialogue = {
},
{
encounter: [
"dialogue:breeder_female.encounter.1",
"dialogue:breeder_female.encounter.2",
"dialogue:breeder_female.encounter.3",
"dialogue:breederFemale.encounter.1",
"dialogue:breederFemale.encounter.2",
"dialogue:breederFemale.encounter.3",
],
victory: [
"dialogue:breeder_female.victory.1",
"dialogue:breeder_female.victory.2",
"dialogue:breeder_female.victory.3",
],
defeat: [
"dialogue:breeder_female.defeat.1",
"dialogue:breeder_female.defeat.2",
"dialogue:breeder_female.defeat.3",
"dialogue:breederFemale.victory.1",
"dialogue:breederFemale.victory.2",
"dialogue:breederFemale.victory.3",
],
defeat: ["dialogue:breederFemale.defeat.1", "dialogue:breederFemale.defeat.2", "dialogue:breederFemale.defeat.3"],
},
],
[TrainerType.FISHERMAN]: [
@ -108,14 +104,14 @@ export const trainerTypeDialogue: TrainerTypeDialogue = {
},
{
encounter: [
"dialogue:fisherman_female.encounter.1",
"dialogue:fisherman_female.encounter.2",
"dialogue:fisherman_female.encounter.3",
"dialogue:fishermanFemale.encounter.1",
"dialogue:fishermanFemale.encounter.2",
"dialogue:fishermanFemale.encounter.3",
],
victory: [
"dialogue:fisherman_female.victory.1",
"dialogue:fisherman_female.victory.2",
"dialogue:fisherman_female.victory.3",
"dialogue:fishermanFemale.victory.1",
"dialogue:fishermanFemale.victory.2",
"dialogue:fishermanFemale.victory.3",
],
},
],
@ -144,29 +140,29 @@ export const trainerTypeDialogue: TrainerTypeDialogue = {
[TrainerType.ACE_TRAINER]: [
{
encounter: [
"dialogue:ace_trainer.encounter.1",
"dialogue:ace_trainer.encounter.2",
"dialogue:ace_trainer.encounter.3",
"dialogue:ace_trainer.encounter.4",
"dialogue:aceTrainer.encounter.1",
"dialogue:aceTrainer.encounter.2",
"dialogue:aceTrainer.encounter.3",
"dialogue:aceTrainer.encounter.4",
],
victory: [
"dialogue:ace_trainer.victory.1",
"dialogue:ace_trainer.victory.2",
"dialogue:ace_trainer.victory.3",
"dialogue:ace_trainer.victory.4",
"dialogue:aceTrainer.victory.1",
"dialogue:aceTrainer.victory.2",
"dialogue:aceTrainer.victory.3",
"dialogue:aceTrainer.victory.4",
],
defeat: [
"dialogue:ace_trainer.defeat.1",
"dialogue:ace_trainer.defeat.2",
"dialogue:ace_trainer.defeat.3",
"dialogue:ace_trainer.defeat.4",
"dialogue:aceTrainer.defeat.1",
"dialogue:aceTrainer.defeat.2",
"dialogue:aceTrainer.defeat.3",
"dialogue:aceTrainer.defeat.4",
],
},
],
[TrainerType.PARASOL_LADY]: [
{
encounter: ["dialogue:parasol_lady.encounter.1"],
victory: ["dialogue:parasol_lady.victory.1"],
encounter: ["dialogue:parasolLady.encounter.1"],
victory: ["dialogue:parasolLady.victory.1"],
},
],
[TrainerType.TWINS]: [
@ -184,13 +180,13 @@ export const trainerTypeDialogue: TrainerTypeDialogue = {
],
[TrainerType.BLACK_BELT]: [
{
encounter: ["dialogue:black_belt.encounter.1", "dialogue:black_belt.encounter.2"],
victory: ["dialogue:black_belt.victory.1", "dialogue:black_belt.victory.2"],
encounter: ["dialogue:blackBelt.encounter.1", "dialogue:blackBelt.encounter.2"],
victory: ["dialogue:blackBelt.victory.1", "dialogue:blackBelt.victory.2"],
},
//BATTLE GIRL
{
encounter: ["dialogue:battle_girl.encounter.1"],
victory: ["dialogue:battle_girl.victory.1"],
encounter: ["dialogue:battleGirl.encounter.1"],
victory: ["dialogue:battleGirl.victory.1"],
},
],
[TrainerType.HIKER]: [
@ -214,8 +210,8 @@ export const trainerTypeDialogue: TrainerTypeDialogue = {
],
[TrainerType.SCHOOL_KID]: [
{
encounter: ["dialogue:school_kid.encounter.1", "dialogue:school_kid.encounter.2"],
victory: ["dialogue:school_kid.victory.1", "dialogue:school_kid.victory.2"],
encounter: ["dialogue:schoolKid.encounter.1", "dialogue:schoolKid.encounter.2"],
victory: ["dialogue:schoolKid.victory.1", "dialogue:schoolKid.victory.2"],
},
],
[TrainerType.ARTIST]: [
@ -236,31 +232,31 @@ export const trainerTypeDialogue: TrainerTypeDialogue = {
victory: ["dialogue:worker.victory.1"],
},
{
encounter: ["dialogue:worker_female.encounter.1"],
victory: ["dialogue:worker_female.victory.1"],
defeat: ["dialogue:worker_female.defeat.1"],
encounter: ["dialogue:workerFemale.encounter.1"],
victory: ["dialogue:workerFemale.victory.1"],
defeat: ["dialogue:workerFemale.defeat.1"],
},
{
encounter: ["dialogue:worker_double.encounter.1"],
victory: ["dialogue:worker_double.victory.1"],
encounter: ["dialogue:workerDouble.encounter.1"],
victory: ["dialogue:workerDouble.victory.1"],
},
],
// Defeat dialogue in the language .JSONS exist as translated or placeholders; (en, fr, it, es, de, ja, ko, zh_cn, zh_tw, pt_br)
[TrainerType.SNOW_WORKER]: [
{
encounter: ["dialogue:snow_worker.encounter.1"],
victory: ["dialogue:snow_worker.victory.1"],
encounter: ["dialogue:snowWorker.encounter.1"],
victory: ["dialogue:snowWorker.victory.1"],
},
{
encounter: ["dialogue:snow_worker_double.encounter.1"],
victory: ["dialogue:snow_worker_double.victory.1"],
encounter: ["dialogue:snowWorkerDouble.encounter.1"],
victory: ["dialogue:snowWorkerDouble.victory.1"],
},
],
[TrainerType.HEX_MANIAC]: [
{
encounter: ["dialogue:hex_maniac.encounter.1", "dialogue:hex_maniac.encounter.2"],
victory: ["dialogue:hex_maniac.victory.1", "dialogue:hex_maniac.victory.2"],
defeat: ["dialogue:hex_maniac.defeat.1", "dialogue:hex_maniac.defeat.2"],
encounter: ["dialogue:hexManiac.encounter.1", "dialogue:hexManiac.encounter.2"],
victory: ["dialogue:hexManiac.victory.1", "dialogue:hexManiac.victory.2"],
defeat: ["dialogue:hexManiac.defeat.1", "dialogue:hexManiac.defeat.2"],
},
],
[TrainerType.PSYCHIC]: [
@ -320,15 +316,11 @@ export const trainerTypeDialogue: TrainerTypeDialogue = {
},
{
encounter: [
"dialogue:clerk_female.encounter.1",
"dialogue:clerk_female.encounter.2",
"dialogue:clerk_female.encounter.3",
],
victory: [
"dialogue:clerk_female.victory.1",
"dialogue:clerk_female.victory.2",
"dialogue:clerk_female.victory.3",
"dialogue:clerkFemale.encounter.1",
"dialogue:clerkFemale.encounter.2",
"dialogue:clerkFemale.encounter.3",
],
victory: ["dialogue:clerkFemale.victory.1", "dialogue:clerkFemale.victory.2", "dialogue:clerkFemale.victory.3"],
},
],
[TrainerType.HOOLIGANS]: [
@ -371,14 +363,14 @@ export const trainerTypeDialogue: TrainerTypeDialogue = {
},
{
encounter: [
"dialogue:pokefan_female.encounter.1",
"dialogue:pokefan_female.encounter.2",
"dialogue:pokefan_female.encounter.3",
"dialogue:pokefanFemale.encounter.1",
"dialogue:pokefanFemale.encounter.2",
"dialogue:pokefanFemale.encounter.3",
],
victory: [
"dialogue:pokefan_female.victory.1",
"dialogue:pokefan_female.victory.2",
"dialogue:pokefan_female.victory.3",
"dialogue:pokefanFemale.victory.1",
"dialogue:pokefanFemale.victory.2",
"dialogue:pokefanFemale.victory.3",
],
},
],
@ -389,52 +381,52 @@ export const trainerTypeDialogue: TrainerTypeDialogue = {
},
{
encounter: [
"dialogue:rich_female.encounter.1",
"dialogue:rich_female.encounter.2",
"dialogue:rich_female.encounter.3",
"dialogue:richFemale.encounter.1",
"dialogue:richFemale.encounter.2",
"dialogue:richFemale.encounter.3",
],
victory: ["dialogue:rich_female.victory.1", "dialogue:rich_female.victory.2", "dialogue:rich_female.victory.3"],
victory: ["dialogue:richFemale.victory.1", "dialogue:richFemale.victory.2", "dialogue:richFemale.victory.3"],
},
],
[TrainerType.RICH_KID]: [
{
encounter: ["dialogue:rich_kid.encounter.1", "dialogue:rich_kid.encounter.2", "dialogue:rich_kid.encounter.3"],
encounter: ["dialogue:richKid.encounter.1", "dialogue:richKid.encounter.2", "dialogue:richKid.encounter.3"],
victory: [
"dialogue:rich_kid.victory.1",
"dialogue:rich_kid.victory.2",
"dialogue:rich_kid.victory.3",
"dialogue:rich_kid.victory.4",
"dialogue:richKid.victory.1",
"dialogue:richKid.victory.2",
"dialogue:richKid.victory.3",
"dialogue:richKid.victory.4",
],
},
{
encounter: [
"dialogue:rich_kid_female.encounter.1",
"dialogue:rich_kid_female.encounter.2",
"dialogue:rich_kid_female.encounter.3",
"dialogue:richKidFemale.encounter.1",
"dialogue:richKidFemale.encounter.2",
"dialogue:richKidFemale.encounter.3",
],
victory: [
"dialogue:rich_kid_female.victory.1",
"dialogue:rich_kid_female.victory.2",
"dialogue:rich_kid_female.victory.3",
"dialogue:rich_kid_female.victory.4",
"dialogue:richKidFemale.victory.1",
"dialogue:richKidFemale.victory.2",
"dialogue:richKidFemale.victory.3",
"dialogue:richKidFemale.victory.4",
],
},
],
[TrainerType.ROCKET_GRUNT]: [
{
encounter: [
"dialogue:rocket_grunt.encounter.1",
"dialogue:rocket_grunt.encounter.2",
"dialogue:rocket_grunt.encounter.3",
"dialogue:rocket_grunt.encounter.4",
"dialogue:rocket_grunt.encounter.5",
"dialogue:rocketGrunt.encounter.1",
"dialogue:rocketGrunt.encounter.2",
"dialogue:rocketGrunt.encounter.3",
"dialogue:rocketGrunt.encounter.4",
"dialogue:rocketGrunt.encounter.5",
],
victory: [
"dialogue:rocket_grunt.victory.1",
"dialogue:rocket_grunt.victory.2",
"dialogue:rocket_grunt.victory.3",
"dialogue:rocket_grunt.victory.4",
"dialogue:rocket_grunt.victory.5",
"dialogue:rocketGrunt.victory.1",
"dialogue:rocketGrunt.victory.2",
"dialogue:rocketGrunt.victory.3",
"dialogue:rocketGrunt.victory.4",
"dialogue:rocketGrunt.victory.5",
],
},
],
@ -465,18 +457,18 @@ export const trainerTypeDialogue: TrainerTypeDialogue = {
[TrainerType.MAGMA_GRUNT]: [
{
encounter: [
"dialogue:magma_grunt.encounter.1",
"dialogue:magma_grunt.encounter.2",
"dialogue:magma_grunt.encounter.3",
"dialogue:magma_grunt.encounter.4",
"dialogue:magma_grunt.encounter.5",
"dialogue:magmaGrunt.encounter.1",
"dialogue:magmaGrunt.encounter.2",
"dialogue:magmaGrunt.encounter.3",
"dialogue:magmaGrunt.encounter.4",
"dialogue:magmaGrunt.encounter.5",
],
victory: [
"dialogue:magma_grunt.victory.1",
"dialogue:magma_grunt.victory.2",
"dialogue:magma_grunt.victory.3",
"dialogue:magma_grunt.victory.4",
"dialogue:magma_grunt.victory.5",
"dialogue:magmaGrunt.victory.1",
"dialogue:magmaGrunt.victory.2",
"dialogue:magmaGrunt.victory.3",
"dialogue:magmaGrunt.victory.4",
"dialogue:magmaGrunt.victory.5",
],
},
],
@ -495,18 +487,18 @@ export const trainerTypeDialogue: TrainerTypeDialogue = {
[TrainerType.AQUA_GRUNT]: [
{
encounter: [
"dialogue:aqua_grunt.encounter.1",
"dialogue:aqua_grunt.encounter.2",
"dialogue:aqua_grunt.encounter.3",
"dialogue:aqua_grunt.encounter.4",
"dialogue:aqua_grunt.encounter.5",
"dialogue:aquaGrunt.encounter.1",
"dialogue:aquaGrunt.encounter.2",
"dialogue:aquaGrunt.encounter.3",
"dialogue:aquaGrunt.encounter.4",
"dialogue:aquaGrunt.encounter.5",
],
victory: [
"dialogue:aqua_grunt.victory.1",
"dialogue:aqua_grunt.victory.2",
"dialogue:aqua_grunt.victory.3",
"dialogue:aqua_grunt.victory.4",
"dialogue:aqua_grunt.victory.5",
"dialogue:aquaGrunt.victory.1",
"dialogue:aquaGrunt.victory.2",
"dialogue:aquaGrunt.victory.3",
"dialogue:aquaGrunt.victory.4",
"dialogue:aquaGrunt.victory.5",
],
},
],
@ -525,18 +517,18 @@ export const trainerTypeDialogue: TrainerTypeDialogue = {
[TrainerType.GALACTIC_GRUNT]: [
{
encounter: [
"dialogue:galactic_grunt.encounter.1",
"dialogue:galactic_grunt.encounter.2",
"dialogue:galactic_grunt.encounter.3",
"dialogue:galactic_grunt.encounter.4",
"dialogue:galactic_grunt.encounter.5",
"dialogue:galacticGrunt.encounter.1",
"dialogue:galacticGrunt.encounter.2",
"dialogue:galacticGrunt.encounter.3",
"dialogue:galacticGrunt.encounter.4",
"dialogue:galacticGrunt.encounter.5",
],
victory: [
"dialogue:galactic_grunt.victory.1",
"dialogue:galactic_grunt.victory.2",
"dialogue:galactic_grunt.victory.3",
"dialogue:galactic_grunt.victory.4",
"dialogue:galactic_grunt.victory.5",
"dialogue:galacticGrunt.victory.1",
"dialogue:galacticGrunt.victory.2",
"dialogue:galacticGrunt.victory.3",
"dialogue:galacticGrunt.victory.4",
"dialogue:galacticGrunt.victory.5",
],
},
],
@ -561,18 +553,18 @@ export const trainerTypeDialogue: TrainerTypeDialogue = {
[TrainerType.PLASMA_GRUNT]: [
{
encounter: [
"dialogue:plasma_grunt.encounter.1",
"dialogue:plasma_grunt.encounter.2",
"dialogue:plasma_grunt.encounter.3",
"dialogue:plasma_grunt.encounter.4",
"dialogue:plasma_grunt.encounter.5",
"dialogue:plasmaGrunt.encounter.1",
"dialogue:plasmaGrunt.encounter.2",
"dialogue:plasmaGrunt.encounter.3",
"dialogue:plasmaGrunt.encounter.4",
"dialogue:plasmaGrunt.encounter.5",
],
victory: [
"dialogue:plasma_grunt.victory.1",
"dialogue:plasma_grunt.victory.2",
"dialogue:plasma_grunt.victory.3",
"dialogue:plasma_grunt.victory.4",
"dialogue:plasma_grunt.victory.5",
"dialogue:plasmaGrunt.victory.1",
"dialogue:plasmaGrunt.victory.2",
"dialogue:plasmaGrunt.victory.3",
"dialogue:plasmaGrunt.victory.4",
"dialogue:plasmaGrunt.victory.5",
],
},
],
@ -591,18 +583,18 @@ export const trainerTypeDialogue: TrainerTypeDialogue = {
[TrainerType.FLARE_GRUNT]: [
{
encounter: [
"dialogue:flare_grunt.encounter.1",
"dialogue:flare_grunt.encounter.2",
"dialogue:flare_grunt.encounter.3",
"dialogue:flare_grunt.encounter.4",
"dialogue:flare_grunt.encounter.5",
"dialogue:flareGrunt.encounter.1",
"dialogue:flareGrunt.encounter.2",
"dialogue:flareGrunt.encounter.3",
"dialogue:flareGrunt.encounter.4",
"dialogue:flareGrunt.encounter.5",
],
victory: [
"dialogue:flare_grunt.victory.1",
"dialogue:flare_grunt.victory.2",
"dialogue:flare_grunt.victory.3",
"dialogue:flare_grunt.victory.4",
"dialogue:flare_grunt.victory.5",
"dialogue:flareGrunt.victory.1",
"dialogue:flareGrunt.victory.2",
"dialogue:flareGrunt.victory.3",
"dialogue:flareGrunt.victory.4",
"dialogue:flareGrunt.victory.5",
],
},
],
@ -621,18 +613,18 @@ export const trainerTypeDialogue: TrainerTypeDialogue = {
[TrainerType.AETHER_GRUNT]: [
{
encounter: [
"dialogue:aether_grunt.encounter.1",
"dialogue:aether_grunt.encounter.2",
"dialogue:aether_grunt.encounter.3",
"dialogue:aether_grunt.encounter.4",
"dialogue:aether_grunt.encounter.5",
"dialogue:aetherGrunt.encounter.1",
"dialogue:aetherGrunt.encounter.2",
"dialogue:aetherGrunt.encounter.3",
"dialogue:aetherGrunt.encounter.4",
"dialogue:aetherGrunt.encounter.5",
],
victory: [
"dialogue:aether_grunt.victory.1",
"dialogue:aether_grunt.victory.2",
"dialogue:aether_grunt.victory.3",
"dialogue:aether_grunt.victory.4",
"dialogue:aether_grunt.victory.5",
"dialogue:aetherGrunt.victory.1",
"dialogue:aetherGrunt.victory.2",
"dialogue:aetherGrunt.victory.3",
"dialogue:aetherGrunt.victory.4",
"dialogue:aetherGrunt.victory.5",
],
},
],
@ -645,18 +637,18 @@ export const trainerTypeDialogue: TrainerTypeDialogue = {
[TrainerType.SKULL_GRUNT]: [
{
encounter: [
"dialogue:skull_grunt.encounter.1",
"dialogue:skull_grunt.encounter.2",
"dialogue:skull_grunt.encounter.3",
"dialogue:skull_grunt.encounter.4",
"dialogue:skull_grunt.encounter.5",
"dialogue:skullGrunt.encounter.1",
"dialogue:skullGrunt.encounter.2",
"dialogue:skullGrunt.encounter.3",
"dialogue:skullGrunt.encounter.4",
"dialogue:skullGrunt.encounter.5",
],
victory: [
"dialogue:skull_grunt.victory.1",
"dialogue:skull_grunt.victory.2",
"dialogue:skull_grunt.victory.3",
"dialogue:skull_grunt.victory.4",
"dialogue:skull_grunt.victory.5",
"dialogue:skullGrunt.victory.1",
"dialogue:skullGrunt.victory.2",
"dialogue:skullGrunt.victory.3",
"dialogue:skullGrunt.victory.4",
"dialogue:skullGrunt.victory.5",
],
},
],
@ -669,18 +661,18 @@ export const trainerTypeDialogue: TrainerTypeDialogue = {
[TrainerType.MACRO_GRUNT]: [
{
encounter: [
"dialogue:macro_grunt.encounter.1",
"dialogue:macro_grunt.encounter.2",
"dialogue:macro_grunt.encounter.3",
"dialogue:macro_grunt.encounter.4",
"dialogue:macro_grunt.encounter.5",
"dialogue:macroGrunt.encounter.1",
"dialogue:macroGrunt.encounter.2",
"dialogue:macroGrunt.encounter.3",
"dialogue:macroGrunt.encounter.4",
"dialogue:macroGrunt.encounter.5",
],
victory: [
"dialogue:macro_grunt.victory.1",
"dialogue:macro_grunt.victory.2",
"dialogue:macro_grunt.victory.3",
"dialogue:macro_grunt.victory.4",
"dialogue:macro_grunt.victory.5",
"dialogue:macroGrunt.victory.1",
"dialogue:macroGrunt.victory.2",
"dialogue:macroGrunt.victory.3",
"dialogue:macroGrunt.victory.4",
"dialogue:macroGrunt.victory.5",
],
},
],
@ -693,18 +685,18 @@ export const trainerTypeDialogue: TrainerTypeDialogue = {
[TrainerType.STAR_GRUNT]: [
{
encounter: [
"dialogue:star_grunt.encounter.1",
"dialogue:star_grunt.encounter.2",
"dialogue:star_grunt.encounter.3",
"dialogue:star_grunt.encounter.4",
"dialogue:star_grunt.encounter.5",
"dialogue:starGrunt.encounter.1",
"dialogue:starGrunt.encounter.2",
"dialogue:starGrunt.encounter.3",
"dialogue:starGrunt.encounter.4",
"dialogue:starGrunt.encounter.5",
],
victory: [
"dialogue:star_grunt.victory.1",
"dialogue:star_grunt.victory.2",
"dialogue:star_grunt.victory.3",
"dialogue:star_grunt.victory.4",
"dialogue:star_grunt.victory.5",
"dialogue:starGrunt.victory.1",
"dialogue:starGrunt.victory.2",
"dialogue:starGrunt.victory.3",
"dialogue:starGrunt.victory.4",
"dialogue:starGrunt.victory.5",
],
},
],
@ -740,207 +732,207 @@ export const trainerTypeDialogue: TrainerTypeDialogue = {
],
[TrainerType.ROCKET_BOSS_GIOVANNI_1]: [
{
encounter: ["dialogue:rocket_boss_giovanni_1.encounter.1"],
victory: ["dialogue:rocket_boss_giovanni_1.victory.1"],
defeat: ["dialogue:rocket_boss_giovanni_1.defeat.1"],
encounter: ["dialogue:rocketBossGiovanni1.encounter.1"],
victory: ["dialogue:rocketBossGiovanni1.victory.1"],
defeat: ["dialogue:rocketBossGiovanni1.defeat.1"],
},
],
[TrainerType.ROCKET_BOSS_GIOVANNI_2]: [
{
encounter: ["dialogue:rocket_boss_giovanni_2.encounter.1"],
victory: ["dialogue:rocket_boss_giovanni_2.victory.1"],
defeat: ["dialogue:rocket_boss_giovanni_2.defeat.1"],
encounter: ["dialogue:rocketBossGiovanni2.encounter.1"],
victory: ["dialogue:rocketBossGiovanni2.victory.1"],
defeat: ["dialogue:rocketBossGiovanni2.defeat.1"],
},
],
[TrainerType.MAXIE]: [
{
encounter: ["dialogue:magma_boss_maxie_1.encounter.1"],
victory: ["dialogue:magma_boss_maxie_1.victory.1"],
defeat: ["dialogue:magma_boss_maxie_1.defeat.1"],
encounter: ["dialogue:magmaBossMaxie1.encounter.1"],
victory: ["dialogue:magmaBossMaxie1.victory.1"],
defeat: ["dialogue:magmaBossMaxie1.defeat.1"],
},
],
[TrainerType.MAXIE_2]: [
{
encounter: ["dialogue:magma_boss_maxie_2.encounter.1"],
victory: ["dialogue:magma_boss_maxie_2.victory.1"],
defeat: ["dialogue:magma_boss_maxie_2.defeat.1"],
encounter: ["dialogue:magmaBossMaxie2.encounter.1"],
victory: ["dialogue:magmaBossMaxie2.victory.1"],
defeat: ["dialogue:magmaBossMaxie2.defeat.1"],
},
],
[TrainerType.ARCHIE]: [
{
encounter: ["dialogue:aqua_boss_archie_1.encounter.1"],
victory: ["dialogue:aqua_boss_archie_1.victory.1"],
defeat: ["dialogue:aqua_boss_archie_1.defeat.1"],
encounter: ["dialogue:aquaBossArchie1.encounter.1"],
victory: ["dialogue:aquaBossArchie1.victory.1"],
defeat: ["dialogue:aquaBossArchie1.defeat.1"],
},
],
[TrainerType.ARCHIE_2]: [
{
encounter: ["dialogue:aqua_boss_archie_2.encounter.1"],
victory: ["dialogue:aqua_boss_archie_2.victory.1"],
defeat: ["dialogue:aqua_boss_archie_2.defeat.1"],
encounter: ["dialogue:aquaBossArchie2.encounter.1"],
victory: ["dialogue:aquaBossArchie2.victory.1"],
defeat: ["dialogue:aquaBossArchie2.defeat.1"],
},
],
[TrainerType.CYRUS]: [
{
encounter: ["dialogue:galactic_boss_cyrus_1.encounter.1"],
victory: ["dialogue:galactic_boss_cyrus_1.victory.1"],
defeat: ["dialogue:galactic_boss_cyrus_1.defeat.1"],
encounter: ["dialogue:galacticBossCyrus1.encounter.1"],
victory: ["dialogue:galacticBossCyrus1.victory.1"],
defeat: ["dialogue:galacticBossCyrus1.defeat.1"],
},
],
[TrainerType.CYRUS_2]: [
{
encounter: ["dialogue:galactic_boss_cyrus_2.encounter.1"],
victory: ["dialogue:galactic_boss_cyrus_2.victory.1"],
defeat: ["dialogue:galactic_boss_cyrus_2.defeat.1"],
encounter: ["dialogue:galacticBossCyrus2.encounter.1"],
victory: ["dialogue:galacticBossCyrus2.victory.1"],
defeat: ["dialogue:galacticBossCyrus2.defeat.1"],
},
],
[TrainerType.GHETSIS]: [
{
encounter: ["dialogue:plasma_boss_ghetsis_1.encounter.1"],
victory: ["dialogue:plasma_boss_ghetsis_1.victory.1"],
defeat: ["dialogue:plasma_boss_ghetsis_1.defeat.1"],
encounter: ["dialogue:plasmaBossGhetsis1.encounter.1"],
victory: ["dialogue:plasmaBossGhetsis1.victory.1"],
defeat: ["dialogue:plasmaBossGhetsis1.defeat.1"],
},
],
[TrainerType.GHETSIS_2]: [
{
encounter: ["dialogue:plasma_boss_ghetsis_2.encounter.1"],
victory: ["dialogue:plasma_boss_ghetsis_2.victory.1"],
defeat: ["dialogue:plasma_boss_ghetsis_2.defeat.1"],
encounter: ["dialogue:plasmaBossGhetsis2.encounter.1"],
victory: ["dialogue:plasmaBossGhetsis2.victory.1"],
defeat: ["dialogue:plasmaBossGhetsis2.defeat.1"],
},
],
[TrainerType.LYSANDRE]: [
{
encounter: ["dialogue:flare_boss_lysandre_1.encounter.1"],
victory: ["dialogue:flare_boss_lysandre_1.victory.1"],
defeat: ["dialogue:flare_boss_lysandre_1.defeat.1"],
encounter: ["dialogue:flareBossLysandre1.encounter.1"],
victory: ["dialogue:flareBossLysandre1.victory.1"],
defeat: ["dialogue:flareBossLysandre1.defeat.1"],
},
],
[TrainerType.LYSANDRE_2]: [
{
encounter: ["dialogue:flare_boss_lysandre_2.encounter.1"],
victory: ["dialogue:flare_boss_lysandre_2.victory.1"],
defeat: ["dialogue:flare_boss_lysandre_2.defeat.1"],
encounter: ["dialogue:flareBossLysandre2.encounter.1"],
victory: ["dialogue:flareBossLysandre2.victory.1"],
defeat: ["dialogue:flareBossLysandre2.defeat.1"],
},
],
[TrainerType.LUSAMINE]: [
{
encounter: ["dialogue:aether_boss_lusamine_1.encounter.1"],
victory: ["dialogue:aether_boss_lusamine_1.victory.1"],
defeat: ["dialogue:aether_boss_lusamine_1.defeat.1"],
encounter: ["dialogue:aetherBossLusamine1.encounter.1"],
victory: ["dialogue:aetherBossLusamine1.victory.1"],
defeat: ["dialogue:aetherBossLusamine1.defeat.1"],
},
],
[TrainerType.LUSAMINE_2]: [
{
encounter: ["dialogue:aether_boss_lusamine_2.encounter.1"],
victory: ["dialogue:aether_boss_lusamine_2.victory.1"],
defeat: ["dialogue:aether_boss_lusamine_2.defeat.1"],
encounter: ["dialogue:aetherBossLusamine2.encounter.1"],
victory: ["dialogue:aetherBossLusamine2.victory.1"],
defeat: ["dialogue:aetherBossLusamine2.defeat.1"],
},
],
[TrainerType.GUZMA]: [
{
encounter: ["dialogue:skull_boss_guzma_1.encounter.1"],
victory: ["dialogue:skull_boss_guzma_1.victory.1"],
defeat: ["dialogue:skull_boss_guzma_1.defeat.1"],
encounter: ["dialogue:skullBossGuzma1.encounter.1"],
victory: ["dialogue:skullBossGuzma1.victory.1"],
defeat: ["dialogue:skullBossGuzma1.defeat.1"],
},
],
[TrainerType.GUZMA_2]: [
{
encounter: ["dialogue:skull_boss_guzma_2.encounter.1"],
victory: ["dialogue:skull_boss_guzma_2.victory.1"],
defeat: ["dialogue:skull_boss_guzma_2.defeat.1"],
encounter: ["dialogue:skullBossGuzma2.encounter.1"],
victory: ["dialogue:skullBossGuzma2.victory.1"],
defeat: ["dialogue:skullBossGuzma2.defeat.1"],
},
],
[TrainerType.ROSE]: [
{
encounter: ["dialogue:macro_boss_rose_1.encounter.1"],
victory: ["dialogue:macro_boss_rose_1.victory.1"],
defeat: ["dialogue:macro_boss_rose_1.defeat.1"],
encounter: ["dialogue:macroBossRose1.encounter.1"],
victory: ["dialogue:macroBossRose1.victory.1"],
defeat: ["dialogue:macroBossRose1.defeat.1"],
},
],
[TrainerType.ROSE_2]: [
{
encounter: ["dialogue:macro_boss_rose_2.encounter.1"],
victory: ["dialogue:macro_boss_rose_2.victory.1"],
defeat: ["dialogue:macro_boss_rose_2.defeat.1"],
encounter: ["dialogue:macroBossRose2.encounter.1"],
victory: ["dialogue:macroBossRose2.victory.1"],
defeat: ["dialogue:macroBossRose2.defeat.1"],
},
],
[TrainerType.PENNY]: [
{
encounter: ["dialogue:star_boss_penny_1.encounter.1"],
victory: ["dialogue:star_boss_penny_1.victory.1"],
defeat: ["dialogue:star_boss_penny_1.defeat.1"],
encounter: ["dialogue:starBossPenny1.encounter.1"],
victory: ["dialogue:starBossPenny1.victory.1"],
defeat: ["dialogue:starBossPenny1.defeat.1"],
},
],
[TrainerType.PENNY_2]: [
{
encounter: ["dialogue:star_boss_penny_2.encounter.1"],
victory: ["dialogue:star_boss_penny_2.victory.1"],
defeat: ["dialogue:star_boss_penny_2.defeat.1"],
encounter: ["dialogue:starBossPenny2.encounter.1"],
victory: ["dialogue:starBossPenny2.victory.1"],
defeat: ["dialogue:starBossPenny2.defeat.1"],
},
],
[TrainerType.BUCK]: [
{
encounter: ["dialogue:stat_trainer_buck.encounter.1", "dialogue:stat_trainer_buck.encounter.2"],
victory: ["dialogue:stat_trainer_buck.victory.1", "dialogue:stat_trainer_buck.victory.2"],
defeat: ["dialogue:stat_trainer_buck.defeat.1", "dialogue:stat_trainer_buck.defeat.2"],
encounter: ["dialogue:statTrainerBuck.encounter.1", "dialogue:statTrainerBuck.encounter.2"],
victory: ["dialogue:statTrainerBuck.victory.1", "dialogue:statTrainerBuck.victory.2"],
defeat: ["dialogue:statTrainerBuck.defeat.1", "dialogue:statTrainerBuck.defeat.2"],
},
],
[TrainerType.CHERYL]: [
{
encounter: ["dialogue:stat_trainer_cheryl.encounter.1", "dialogue:stat_trainer_cheryl.encounter.2"],
victory: ["dialogue:stat_trainer_cheryl.victory.1", "dialogue:stat_trainer_cheryl.victory.2"],
defeat: ["dialogue:stat_trainer_cheryl.defeat.1", "dialogue:stat_trainer_cheryl.defeat.2"],
encounter: ["dialogue:statTrainerCheryl.encounter.1", "dialogue:statTrainerCheryl.encounter.2"],
victory: ["dialogue:statTrainerCheryl.victory.1", "dialogue:statTrainerCheryl.victory.2"],
defeat: ["dialogue:statTrainerCheryl.defeat.1", "dialogue:statTrainerCheryl.defeat.2"],
},
],
[TrainerType.MARLEY]: [
{
encounter: ["dialogue:stat_trainer_marley.encounter.1", "dialogue:stat_trainer_marley.encounter.2"],
victory: ["dialogue:stat_trainer_marley.victory.1", "dialogue:stat_trainer_marley.victory.2"],
defeat: ["dialogue:stat_trainer_marley.defeat.1", "dialogue:stat_trainer_marley.defeat.2"],
encounter: ["dialogue:statTrainerMarley.encounter.1", "dialogue:statTrainerMarley.encounter.2"],
victory: ["dialogue:statTrainerMarley.victory.1", "dialogue:statTrainerMarley.victory.2"],
defeat: ["dialogue:statTrainerMarley.defeat.1", "dialogue:statTrainerMarley.defeat.2"],
},
],
[TrainerType.MIRA]: [
{
encounter: ["dialogue:stat_trainer_mira.encounter.1", "dialogue:stat_trainer_mira.encounter.2"],
victory: ["dialogue:stat_trainer_mira.victory.1", "dialogue:stat_trainer_mira.victory.2"],
defeat: ["dialogue:stat_trainer_mira.defeat.1", "dialogue:stat_trainer_mira.defeat.2"],
encounter: ["dialogue:statTrainerMira.encounter.1", "dialogue:statTrainerMira.encounter.2"],
victory: ["dialogue:statTrainerMira.victory.1", "dialogue:statTrainerMira.victory.2"],
defeat: ["dialogue:statTrainerMira.defeat.1", "dialogue:statTrainerMira.defeat.2"],
},
],
[TrainerType.RILEY]: [
{
encounter: ["dialogue:stat_trainer_riley.encounter.1", "dialogue:stat_trainer_riley.encounter.2"],
victory: ["dialogue:stat_trainer_riley.victory.1", "dialogue:stat_trainer_riley.victory.2"],
defeat: ["dialogue:stat_trainer_riley.defeat.1", "dialogue:stat_trainer_riley.defeat.2"],
encounter: ["dialogue:statTrainerRiley.encounter.1", "dialogue:statTrainerRiley.encounter.2"],
victory: ["dialogue:statTrainerRiley.victory.1", "dialogue:statTrainerRiley.victory.2"],
defeat: ["dialogue:statTrainerRiley.defeat.1", "dialogue:statTrainerRiley.defeat.2"],
},
],
[TrainerType.VICTOR]: [
{
encounter: ["dialogue:winstrates_victor.encounter.1"],
victory: ["dialogue:winstrates_victor.victory.1"],
encounter: ["dialogue:winstratesVictor.encounter.1"],
victory: ["dialogue:winstratesVictor.victory.1"],
},
],
[TrainerType.VICTORIA]: [
{
encounter: ["dialogue:winstrates_victoria.encounter.1"],
victory: ["dialogue:winstrates_victoria.victory.1"],
encounter: ["dialogue:winstratesVictoria.encounter.1"],
victory: ["dialogue:winstratesVictoria.victory.1"],
},
],
[TrainerType.VIVI]: [
{
encounter: ["dialogue:winstrates_vivi.encounter.1"],
victory: ["dialogue:winstrates_vivi.victory.1"],
encounter: ["dialogue:winstratesVivi.encounter.1"],
victory: ["dialogue:winstratesVivi.victory.1"],
},
],
[TrainerType.VICKY]: [
{
encounter: ["dialogue:winstrates_vicky.encounter.1"],
victory: ["dialogue:winstrates_vicky.victory.1"],
encounter: ["dialogue:winstratesVicky.encounter.1"],
victory: ["dialogue:winstratesVicky.victory.1"],
},
],
[TrainerType.VITO]: [
{
encounter: ["dialogue:winstrates_vito.encounter.1"],
victory: ["dialogue:winstrates_vito.victory.1"],
encounter: ["dialogue:winstratesVito.encounter.1"],
victory: ["dialogue:winstratesVito.victory.1"],
},
],
[TrainerType.BROCK]: {
@ -954,9 +946,9 @@ export const trainerTypeDialogue: TrainerTypeDialogue = {
defeat: ["dialogue:misty.defeat.1", "dialogue:misty.defeat.2", "dialogue:misty.defeat.3"],
},
[TrainerType.LT_SURGE]: {
encounter: ["dialogue:lt_surge.encounter.1", "dialogue:lt_surge.encounter.2", "dialogue:lt_surge.encounter.3"],
victory: ["dialogue:lt_surge.victory.1", "dialogue:lt_surge.victory.2", "dialogue:lt_surge.victory.3"],
defeat: ["dialogue:lt_surge.defeat.1", "dialogue:lt_surge.defeat.2", "dialogue:lt_surge.defeat.3"],
encounter: ["dialogue:ltSurge.encounter.1", "dialogue:ltSurge.encounter.2", "dialogue:ltSurge.encounter.3"],
victory: ["dialogue:ltSurge.victory.1", "dialogue:ltSurge.victory.2", "dialogue:ltSurge.victory.3"],
defeat: ["dialogue:ltSurge.defeat.1", "dialogue:ltSurge.defeat.2", "dialogue:ltSurge.defeat.3"],
},
[TrainerType.ERIKA]: {
encounter: [
@ -1055,12 +1047,12 @@ export const trainerTypeDialogue: TrainerTypeDialogue = {
},
[TrainerType.CRASHER_WAKE]: {
encounter: [
"dialogue:crasher_wake.encounter.1",
"dialogue:crasher_wake.encounter.2",
"dialogue:crasher_wake.encounter.3",
"dialogue:crasherWake.encounter.1",
"dialogue:crasherWake.encounter.2",
"dialogue:crasherWake.encounter.3",
],
victory: ["dialogue:crasher_wake.victory.1", "dialogue:crasher_wake.victory.2", "dialogue:crasher_wake.victory.3"],
defeat: ["dialogue:crasher_wake.defeat.1", "dialogue:crasher_wake.defeat.2", "dialogue:crasher_wake.defeat.3"],
victory: ["dialogue:crasherWake.victory.1", "dialogue:crasherWake.victory.2", "dialogue:crasherWake.victory.3"],
defeat: ["dialogue:crasherWake.defeat.1", "dialogue:crasherWake.defeat.2", "dialogue:crasherWake.defeat.3"],
},
[TrainerType.FALKNER]: {
encounter: ["dialogue:falkner.encounter.1", "dialogue:falkner.encounter.2", "dialogue:falkner.encounter.3"],
@ -1354,9 +1346,9 @@ export const trainerTypeDialogue: TrainerTypeDialogue = {
defeat: ["dialogue:acerola.defeat.1"],
},
[TrainerType.LARRY_ELITE]: {
encounter: ["dialogue:larry_elite.encounter.1"],
victory: ["dialogue:larry_elite.victory.1"],
defeat: ["dialogue:larry_elite.defeat.1"],
encounter: ["dialogue:larryElite.encounter.1"],
victory: ["dialogue:larryElite.victory.1"],
defeat: ["dialogue:larryElite.defeat.1"],
},
[TrainerType.LANCE]: {
encounter: ["dialogue:lance.encounter.1", "dialogue:lance.encounter.2"],
@ -1414,9 +1406,9 @@ export const trainerTypeDialogue: TrainerTypeDialogue = {
defeat: ["dialogue:jasmine.defeat.1"],
},
[TrainerType.LANCE_CHAMPION]: {
encounter: ["dialogue:lance_champion.encounter.1"],
victory: ["dialogue:lance_champion.victory.1"],
defeat: ["dialogue:lance_champion.defeat.1"],
encounter: ["dialogue:lanceChampion.encounter.1"],
victory: ["dialogue:lanceChampion.victory.1"],
defeat: ["dialogue:lanceChampion.defeat.1"],
},
[TrainerType.STEVEN]: {
encounter: ["dialogue:steven.encounter.1"],
@ -1624,29 +1616,29 @@ export const trainerTypeDialogue: TrainerTypeDialogue = {
defeat: ["dialogue:grusha.defeat.1"],
},
[TrainerType.MARNIE_ELITE]: {
encounter: ["dialogue:marnie_elite.encounter.1", "dialogue:marnie_elite.encounter.2"],
victory: ["dialogue:marnie_elite.victory.1", "dialogue:marnie_elite.victory.2"],
defeat: ["dialogue:marnie_elite.defeat.1", "dialogue:marnie_elite.defeat.2"],
encounter: ["dialogue:marnieElite.encounter.1", "dialogue:marnieElite.encounter.2"],
victory: ["dialogue:marnieElite.victory.1", "dialogue:marnieElite.victory.2"],
defeat: ["dialogue:marnieElite.defeat.1", "dialogue:marnieElite.defeat.2"],
},
[TrainerType.NESSA_ELITE]: {
encounter: ["dialogue:nessa_elite.encounter.1", "dialogue:nessa_elite.encounter.2"],
victory: ["dialogue:nessa_elite.victory.1", "dialogue:nessa_elite.victory.2"],
defeat: ["dialogue:nessa_elite.defeat.1", "dialogue:nessa_elite.defeat.2"],
encounter: ["dialogue:nessaElite.encounter.1", "dialogue:nessaElite.encounter.2"],
victory: ["dialogue:nessaElite.victory.1", "dialogue:nessaElite.victory.2"],
defeat: ["dialogue:nessaElite.defeat.1", "dialogue:nessaElite.defeat.2"],
},
[TrainerType.BEA_ELITE]: {
encounter: ["dialogue:bea_elite.encounter.1", "dialogue:bea_elite.encounter.2"],
victory: ["dialogue:bea_elite.victory.1", "dialogue:bea_elite.victory.2"],
defeat: ["dialogue:bea_elite.defeat.1", "dialogue:bea_elite.defeat.2"],
encounter: ["dialogue:beaElite.encounter.1", "dialogue:beaElite.encounter.2"],
victory: ["dialogue:beaElite.victory.1", "dialogue:beaElite.victory.2"],
defeat: ["dialogue:beaElite.defeat.1", "dialogue:beaElite.defeat.2"],
},
[TrainerType.ALLISTER_ELITE]: {
encounter: ["dialogue:allister_elite.encounter.1", "dialogue:allister_elite.encounter.2"],
victory: ["dialogue:allister_elite.victory.1", "dialogue:allister_elite.victory.2"],
defeat: ["dialogue:allister_elite.defeat.1", "dialogue:allister_elite.defeat.2"],
encounter: ["dialogue:allisterElite.encounter.1", "dialogue:allisterElite.encounter.2"],
victory: ["dialogue:allisterElite.victory.1", "dialogue:allisterElite.victory.2"],
defeat: ["dialogue:allisterElite.defeat.1", "dialogue:allisterElite.defeat.2"],
},
[TrainerType.RAIHAN_ELITE]: {
encounter: ["dialogue:raihan_elite.encounter.1", "dialogue:raihan_elite.encounter.2"],
victory: ["dialogue:raihan_elite.victory.1", "dialogue:raihan_elite.victory.2"],
defeat: ["dialogue:raihan_elite.defeat.1", "dialogue:raihan_elite.defeat.2"],
encounter: ["dialogue:raihanElite.encounter.1", "dialogue:raihanElite.encounter.2"],
victory: ["dialogue:raihanElite.victory.1", "dialogue:raihanElite.victory.2"],
defeat: ["dialogue:raihanElite.defeat.1", "dialogue:raihanElite.defeat.2"],
},
[TrainerType.ALDER]: {
encounter: ["dialogue:alder.encounter.1"],
@ -1670,56 +1662,56 @@ export const trainerTypeDialogue: TrainerTypeDialogue = {
],
[TrainerType.RIVAL_2]: [
{
encounter: ["dialogue:rival_2.encounter.1"],
victory: ["dialogue:rival_2.victory.1"],
encounter: ["dialogue:rival2.encounter.1"],
victory: ["dialogue:rival2.victory.1"],
},
{
encounter: ["dialogue:rival_2_female.encounter.1"],
victory: ["dialogue:rival_2_female.victory.1"],
defeat: ["dialogue:rival_2_female.defeat.1"],
encounter: ["dialogue:rival2Female.encounter.1"],
victory: ["dialogue:rival2Female.victory.1"],
defeat: ["dialogue:rival2Female.defeat.1"],
},
],
[TrainerType.RIVAL_3]: [
{
encounter: ["dialogue:rival_3.encounter.1"],
victory: ["dialogue:rival_3.victory.1"],
encounter: ["dialogue:rival3.encounter.1"],
victory: ["dialogue:rival3.victory.1"],
},
{
encounter: ["dialogue:rival_3_female.encounter.1"],
victory: ["dialogue:rival_3_female.victory.1"],
defeat: ["dialogue:rival_3_female.defeat.1"],
encounter: ["dialogue:rival3Female.encounter.1"],
victory: ["dialogue:rival3Female.victory.1"],
defeat: ["dialogue:rival3Female.defeat.1"],
},
],
[TrainerType.RIVAL_4]: [
{
encounter: ["dialogue:rival_4.encounter.1"],
victory: ["dialogue:rival_4.victory.1"],
encounter: ["dialogue:rival4.encounter.1"],
victory: ["dialogue:rival4.victory.1"],
},
{
encounter: ["dialogue:rival_4_female.encounter.1"],
victory: ["dialogue:rival_4_female.victory.1"],
defeat: ["dialogue:rival_4_female.defeat.1"],
encounter: ["dialogue:rival4Female.encounter.1"],
victory: ["dialogue:rival4Female.victory.1"],
defeat: ["dialogue:rival4Female.defeat.1"],
},
],
[TrainerType.RIVAL_5]: [
{
encounter: ["dialogue:rival_5.encounter.1"],
victory: ["dialogue:rival_5.victory.1"],
encounter: ["dialogue:rival5.encounter.1"],
victory: ["dialogue:rival5.victory.1"],
},
{
encounter: ["dialogue:rival_5_female.encounter.1"],
victory: ["dialogue:rival_5_female.victory.1"],
defeat: ["dialogue:rival_5_female.defeat.1"],
encounter: ["dialogue:rival5Female.encounter.1"],
victory: ["dialogue:rival5Female.victory.1"],
defeat: ["dialogue:rival5Female.defeat.1"],
},
],
[TrainerType.RIVAL_6]: [
{
encounter: ["dialogue:rival_6.encounter.1"],
victory: ["dialogue:rival_6.victory.1"],
encounter: ["dialogue:rival6.encounter.1"],
victory: ["dialogue:rival6.victory.1"],
},
{
encounter: ["dialogue:rival_6_female.encounter.1"],
victory: ["dialogue:rival_6_female.victory.1"],
encounter: ["dialogue:rival6Female.encounter.1"],
victory: ["dialogue:rival6Female.victory.1"],
},
],
};

View File

@ -2,43 +2,43 @@
// that caused this to be moved out in the first place
export const doubleBattleDialogue = {
blue_red_double: {
encounter: ["doubleBattleDialogue:blue_red_double.encounter.1"],
victory: ["doubleBattleDialogue:blue_red_double.victory.1"],
encounter: ["doubleBattleDialogue:blueRedDouble.encounter.1"],
victory: ["doubleBattleDialogue:blueRedDouble.victory.1"],
},
red_blue_double: {
encounter: ["doubleBattleDialogue:red_blue_double.encounter.1"],
victory: ["doubleBattleDialogue:red_blue_double.victory.1"],
encounter: ["doubleBattleDialogue:redBlueDouble.encounter.1"],
victory: ["doubleBattleDialogue:redBlueDouble.victory.1"],
},
tate_liza_double: {
encounter: ["doubleBattleDialogue:tate_liza_double.encounter.1"],
victory: ["doubleBattleDialogue:tate_liza_double.victory.1"],
encounter: ["doubleBattleDialogue:tateLizaDouble.encounter.1"],
victory: ["doubleBattleDialogue:tateLizaDouble.victory.1"],
},
liza_tate_double: {
encounter: ["doubleBattleDialogue:liza_tate_double.encounter.1"],
victory: ["doubleBattleDialogue:liza_tate_double.victory.1"],
encounter: ["doubleBattleDialogue:lizaTateDouble.encounter.1"],
victory: ["doubleBattleDialogue:lizaTateDouble.victory.1"],
},
wallace_steven_double: {
encounter: ["doubleBattleDialogue:wallace_steven_double.encounter.1"],
victory: ["doubleBattleDialogue:wallace_steven_double.victory.1"],
encounter: ["doubleBattleDialogue:wallaceStevenDouble.encounter.1"],
victory: ["doubleBattleDialogue:wallaceStevenDouble.victory.1"],
},
steven_wallace_double: {
encounter: ["doubleBattleDialogue:steven_wallace_double.encounter.1"],
victory: ["doubleBattleDialogue:steven_wallace_double.victory.1"],
encounter: ["doubleBattleDialogue:stevenWallaceDouble.encounter.1"],
victory: ["doubleBattleDialogue:stevenWallaceDouble.victory.1"],
},
alder_iris_double: {
encounter: ["doubleBattleDialogue:alder_iris_double.encounter.1"],
victory: ["doubleBattleDialogue:alder_iris_double.victory.1"],
encounter: ["doubleBattleDialogue:alderIrisDouble.encounter.1"],
victory: ["doubleBattleDialogue:alderIrisDouble.victory.1"],
},
iris_alder_double: {
encounter: ["doubleBattleDialogue:iris_alder_double.encounter.1"],
victory: ["doubleBattleDialogue:iris_alder_double.victory.1"],
encounter: ["doubleBattleDialogue:irisAlderDouble.encounter.1"],
victory: ["doubleBattleDialogue:irisAlderDouble.victory.1"],
},
marnie_piers_double: {
encounter: ["doubleBattleDialogue:marnie_piers_double.encounter.1"],
victory: ["doubleBattleDialogue:marnie_piers_double.victory.1"],
encounter: ["doubleBattleDialogue:marniePiersDouble.encounter.1"],
victory: ["doubleBattleDialogue:marniePiersDouble.victory.1"],
},
piers_marnie_double: {
encounter: ["doubleBattleDialogue:piers_marnie_double.encounter.1"],
victory: ["doubleBattleDialogue:piers_marnie_double.victory.1"],
encounter: ["doubleBattleDialogue:piersMarnieDouble.encounter.1"],
victory: ["doubleBattleDialogue:piersMarnieDouble.victory.1"],
},
};

View File

@ -47,6 +47,7 @@ export class EggHatchData {
caughtCount: currDexEntry.caughtCount,
hatchedCount: currDexEntry.hatchedCount,
ivs: [...currDexEntry.ivs],
ribbons: currDexEntry.ribbons,
};
this.starterDataEntryBeforeUpdate = {
moveset: currStarterDataEntry.moveset,

View File

@ -90,7 +90,7 @@ import type { ChargingMove, MoveAttrMap, MoveAttrString, MoveClassMap, MoveKindS
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}`;
}
/**

View File

@ -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.

View File

@ -132,7 +132,7 @@ export const FunAndGamesEncounter: MysteryEncounter = MysteryEncounterBuilder.wi
const moneyCost = (encounter.options[0].requirements[0] as MoneyRequirement).requiredMoney;
updatePlayerMoney(-moneyCost, true, false);
await showEncounterText(
i18next.t("mysteryEncounterMessages:paid_money", {
i18next.t("mysteryEncounterMessages:paidMoney", {
amount: moneyCost,
}),
);

View File

@ -149,7 +149,7 @@ export const PartTimerEncounter: MysteryEncounter = MysteryEncounterBuilder.with
const moneyChange = globalScene.getWaveMoneyAmount(moneyMultiplier);
updatePlayerMoney(moneyChange, true, false);
await showEncounterText(
i18next.t("mysteryEncounterMessages:receive_money", {
i18next.t("mysteryEncounterMessages:receiveMoney", {
amount: moneyChange,
}),
);
@ -231,7 +231,7 @@ export const PartTimerEncounter: MysteryEncounter = MysteryEncounterBuilder.with
const moneyChange = globalScene.getWaveMoneyAmount(moneyMultiplier);
updatePlayerMoney(moneyChange, true, false);
await showEncounterText(
i18next.t("mysteryEncounterMessages:receive_money", {
i18next.t("mysteryEncounterMessages:receiveMoney", {
amount: moneyChange,
}),
);
@ -286,7 +286,7 @@ export const PartTimerEncounter: MysteryEncounter = MysteryEncounterBuilder.with
const moneyChange = globalScene.getWaveMoneyAmount(2.5);
updatePlayerMoney(moneyChange, true, false);
await showEncounterText(
i18next.t("mysteryEncounterMessages:receive_money", {
i18next.t("mysteryEncounterMessages:receiveMoney", {
amount: moneyChange,
}),
);

View File

@ -467,7 +467,7 @@ export function updatePlayerMoney(changeValue: number, playSound = true, showMes
if (showMessage) {
if (changeValue < 0) {
globalScene.phaseManager.queueMessage(
i18next.t("mysteryEncounterMessages:paid_money", {
i18next.t("mysteryEncounterMessages:paidMoney", {
amount: -changeValue,
}),
null,
@ -475,7 +475,7 @@ export function updatePlayerMoney(changeValue: number, playSound = true, showMes
);
} else {
globalScene.phaseManager.queueMessage(
i18next.t("mysteryEncounterMessages:receive_money", {
i18next.t("mysteryEncounterMessages:receiveMoney", {
amount: changeValue,
}),
null,
@ -587,7 +587,7 @@ export function selectPokemonForOption(
return true;
},
onHover: () => {
showEncounterText(i18next.t("mysteryEncounterMessages:cancel_option"), 0, 0, false);
showEncounterText(i18next.t("mysteryEncounterMessages:cancelOption"), 0, 0, false);
},
});
@ -720,7 +720,7 @@ export function selectOptionThenPokemon(
if (onHoverOverCancelOption) {
onHoverOverCancelOption();
}
showEncounterText(i18next.t("mysteryEncounterMessages:cancel_option"), 0, 0, false);
showEncounterText(i18next.t("mysteryEncounterMessages:cancelOption"), 0, 0, false);
},
});

View File

@ -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`),

View File

@ -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;
}

View File

@ -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 {
);
});
}
/**
* 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;
}
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 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);
const candyFriendshipMultiplier = globalScene.gameMode.isClassic
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;
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);
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: 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);
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;
}
});
} else {
// Lose friendship upon fainting
this.friendship = Math.max(this.friendship + 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;
}
}

View File

@ -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);

View File

@ -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;
}

View File

@ -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.

View File

@ -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) {

View File

@ -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";
@ -62,8 +65,8 @@ export class GameOverPhase extends BattlePhase {
const genderIndex = globalScene.gameData.gender ?? PlayerGender.UNSET;
const genderStr = PlayerGender[genderIndex].toLowerCase();
globalScene.ui.showDialogue(
i18next.t("miscDialogue:ending_endless", { context: genderStr }),
i18next.t("miscDialogue:ending_name"),
i18next.t("miscDialogue:endingEndless", { context: genderStr }),
i18next.t("miscDialogue:endingName"),
0,
() => this.handleGameOver(),
);
@ -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

View File

@ -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";
@ -214,242 +214,243 @@ export function getAchievementDescription(localizationKey: string): string {
const genderStr = PlayerGender[genderIndex].toLowerCase();
switch (localizationKey) {
case "10K_MONEY":
return i18next.t("achv:MoneyAchv.description", {
case "10KMoney":
return i18next.t("achv:moneyAchv.description", {
context: genderStr,
moneyAmount: achvs._10K_MONEY.moneyAmount.toLocaleString("en-US"),
});
case "100K_MONEY":
return i18next.t("achv:MoneyAchv.description", {
case "100KMoney":
return i18next.t("achv:moneyAchv.description", {
context: genderStr,
moneyAmount: achvs._100K_MONEY.moneyAmount.toLocaleString("en-US"),
});
case "1M_MONEY":
return i18next.t("achv:MoneyAchv.description", {
case "1MMoney":
return i18next.t("achv:moneyAchv.description", {
context: genderStr,
moneyAmount: achvs._1M_MONEY.moneyAmount.toLocaleString("en-US"),
});
case "10M_MONEY":
return i18next.t("achv:MoneyAchv.description", {
case "10MMoney":
return i18next.t("achv:moneyAchv.description", {
context: genderStr,
moneyAmount: achvs._10M_MONEY.moneyAmount.toLocaleString("en-US"),
});
case "250_DMG":
return i18next.t("achv:DamageAchv.description", {
case "250Dmg":
return i18next.t("achv:damageAchv.description", {
context: genderStr,
damageAmount: achvs._250_DMG.damageAmount.toLocaleString("en-US"),
});
case "1000_DMG":
return i18next.t("achv:DamageAchv.description", {
case "1000Dmg":
return i18next.t("achv:damageAchv.description", {
context: genderStr,
damageAmount: achvs._1000_DMG.damageAmount.toLocaleString("en-US"),
});
case "2500_DMG":
return i18next.t("achv:DamageAchv.description", {
case "2500Dmg":
return i18next.t("achv:damageAchv.description", {
context: genderStr,
damageAmount: achvs._2500_DMG.damageAmount.toLocaleString("en-US"),
});
case "10000_DMG":
return i18next.t("achv:DamageAchv.description", {
case "10000Dmg":
return i18next.t("achv:damageAchv.description", {
context: genderStr,
damageAmount: achvs._10000_DMG.damageAmount.toLocaleString("en-US"),
});
case "250_HEAL":
return i18next.t("achv:HealAchv.description", {
case "250Heal":
return i18next.t("achv:healAchv.description", {
context: genderStr,
healAmount: achvs._250_HEAL.healAmount.toLocaleString("en-US"),
HP: i18next.t(getShortenedStatKey(Stat.HP)),
});
case "1000_HEAL":
return i18next.t("achv:HealAchv.description", {
case "1000Heal":
return i18next.t("achv:healAchv.description", {
context: genderStr,
healAmount: achvs._1000_HEAL.healAmount.toLocaleString("en-US"),
HP: i18next.t(getShortenedStatKey(Stat.HP)),
});
case "2500_HEAL":
return i18next.t("achv:HealAchv.description", {
case "2500Heal":
return i18next.t("achv:healAchv.description", {
context: genderStr,
healAmount: achvs._2500_HEAL.healAmount.toLocaleString("en-US"),
HP: i18next.t(getShortenedStatKey(Stat.HP)),
});
case "10000_HEAL":
return i18next.t("achv:HealAchv.description", {
case "10000Heal":
return i18next.t("achv:healAchv.description", {
context: genderStr,
healAmount: achvs._10000_HEAL.healAmount.toLocaleString("en-US"),
HP: i18next.t(getShortenedStatKey(Stat.HP)),
});
case "LV_100":
return i18next.t("achv:LevelAchv.description", {
case "lv100":
return i18next.t("achv:levelAchv.description", {
context: genderStr,
level: achvs.LV_100.level,
});
case "LV_250":
return i18next.t("achv:LevelAchv.description", {
case "lv250":
return i18next.t("achv:levelAchv.description", {
context: genderStr,
level: achvs.LV_250.level,
});
case "LV_1000":
return i18next.t("achv:LevelAchv.description", {
case "lv1000":
return i18next.t("achv:levelAchv.description", {
context: genderStr,
level: achvs.LV_1000.level,
});
case "10_RIBBONS":
return i18next.t("achv:RibbonAchv.description", {
case "10Ribbons":
return i18next.t("achv:ribbonAchv.description", {
context: genderStr,
ribbonAmount: achvs._10_RIBBONS.ribbonAmount.toLocaleString("en-US"),
});
case "25_RIBBONS":
return i18next.t("achv:RibbonAchv.description", {
case "25Ribbons":
return i18next.t("achv:ribbonAchv.description", {
context: genderStr,
ribbonAmount: achvs._25_RIBBONS.ribbonAmount.toLocaleString("en-US"),
});
case "50_RIBBONS":
return i18next.t("achv:RibbonAchv.description", {
case "50Ribbons":
return i18next.t("achv:ribbonAchv.description", {
context: genderStr,
ribbonAmount: achvs._50_RIBBONS.ribbonAmount.toLocaleString("en-US"),
});
case "75_RIBBONS":
return i18next.t("achv:RibbonAchv.description", {
case "75Ribbons":
return i18next.t("achv:ribbonAchv.description", {
context: genderStr,
ribbonAmount: achvs._75_RIBBONS.ribbonAmount.toLocaleString("en-US"),
});
case "100_RIBBONS":
return i18next.t("achv:RibbonAchv.description", {
case "100Ribbons":
return i18next.t("achv:ribbonAchv.description", {
context: genderStr,
ribbonAmount: achvs._100_RIBBONS.ribbonAmount.toLocaleString("en-US"),
});
case "TRANSFER_MAX_STAT_STAGE":
return i18next.t("achv:TRANSFER_MAX_STAT_STAGE.description", {
case "transferMaxStatStage":
return i18next.t("achv:transferMaxStatStage.description", {
context: genderStr,
});
case "MAX_FRIENDSHIP":
return i18next.t("achv:MAX_FRIENDSHIP.description", {
case "maxFriendship":
return i18next.t("achv:maxFriendship.description", {
context: genderStr,
});
case "MEGA_EVOLVE":
return i18next.t("achv:MEGA_EVOLVE.description", { context: genderStr });
case "GIGANTAMAX":
return i18next.t("achv:GIGANTAMAX.description", { context: genderStr });
case "TERASTALLIZE":
return i18next.t("achv:TERASTALLIZE.description", { context: genderStr });
case "STELLAR_TERASTALLIZE":
return i18next.t("achv:STELLAR_TERASTALLIZE.description", {
case "megaEvolve":
return i18next.t("achv:megaEvolve.description", { context: genderStr });
case "gigantamax":
return i18next.t("achv:gigantamax.description", { context: genderStr });
case "terastallize":
return i18next.t("achv:terastallize.description", { context: genderStr });
case "stellarTerastallize":
return i18next.t("achv:stellarTerastallize.description", {
context: genderStr,
});
case "SPLICE":
return i18next.t("achv:SPLICE.description", { context: genderStr });
case "MINI_BLACK_HOLE":
return i18next.t("achv:MINI_BLACK_HOLE.description", {
case "Splice":
return i18next.t("achv:Splice.description", { context: genderStr });
case "miniBlackHole":
return i18next.t("achv:miniBlackHole.description", {
context: genderStr,
});
case "CATCH_MYTHICAL":
return i18next.t("achv:CATCH_MYTHICAL.description", {
case "catchMythical":
return i18next.t("achv:catchMythical.description", {
context: genderStr,
});
case "CATCH_SUB_LEGENDARY":
return i18next.t("achv:CATCH_SUB_LEGENDARY.description", {
case "catchSubLegendary":
return i18next.t("achv:catchSubLegendary.description", {
context: genderStr,
});
case "CATCH_LEGENDARY":
return i18next.t("achv:CATCH_LEGENDARY.description", {
case "catchLegendary":
return i18next.t("achv:catchLegendary.description", {
context: genderStr,
});
case "SEE_SHINY":
return i18next.t("achv:SEE_SHINY.description", { context: genderStr });
case "SHINY_PARTY":
return i18next.t("achv:SHINY_PARTY.description", { context: genderStr });
case "HATCH_MYTHICAL":
return i18next.t("achv:HATCH_MYTHICAL.description", {
case "seeShiny":
return i18next.t("achv:seeShiny.description", { context: genderStr });
case "shinyParty":
return i18next.t("achv:shinyParty.description", { context: genderStr });
case "hatchMythical":
return i18next.t("achv:hatchMythical.description", {
context: genderStr,
});
case "HATCH_SUB_LEGENDARY":
return i18next.t("achv:HATCH_SUB_LEGENDARY.description", {
case "hatchSubLegendary":
return i18next.t("achv:hatchSubLegendary.description", {
context: genderStr,
});
case "HATCH_LEGENDARY":
return i18next.t("achv:HATCH_LEGENDARY.description", {
case "hatchLegendary":
return i18next.t("achv:hatchLegendary.description", {
context: genderStr,
});
case "HATCH_SHINY":
return i18next.t("achv:HATCH_SHINY.description", { context: genderStr });
case "HIDDEN_ABILITY":
return i18next.t("achv:HIDDEN_ABILITY.description", {
case "hatchShiny":
return i18next.t("achv:hatchShiny.description", { context: genderStr });
case "hiddenAbility":
return i18next.t("achv:hiddenAbility.description", {
context: genderStr,
});
case "PERFECT_IVS":
return i18next.t("achv:PERFECT_IVS.description", { context: genderStr });
case "CLASSIC_VICTORY":
return i18next.t("achv:CLASSIC_VICTORY.description", {
case "perfectIvs":
return i18next.t("achv:perfectIvs.description", { context: genderStr });
case "classicVictory":
return i18next.t("achv:classicVictory.description", {
context: genderStr,
});
case "UNEVOLVED_CLASSIC_VICTORY":
return i18next.t("achv:UNEVOLVED_CLASSIC_VICTORY.description", {
case "unevolvedClassicVictory":
return i18next.t("achv:unevolvedClassicVictory.description", {
context: genderStr,
});
case "MONO_GEN_ONE":
return i18next.t("achv:MONO_GEN_ONE.description", { context: genderStr });
case "MONO_GEN_TWO":
return i18next.t("achv:MONO_GEN_TWO.description", { context: genderStr });
case "MONO_GEN_THREE":
return i18next.t("achv:MONO_GEN_THREE.description", {
case "monoGenOne":
return i18next.t("achv:monoGenOne.description", { context: genderStr });
case "monoGenTwo":
return i18next.t("achv:monoGenTwo.description", { context: genderStr });
case "monoGenThree":
return i18next.t("achv:monoGenThree.description", {
context: genderStr,
});
case "MONO_GEN_FOUR":
return i18next.t("achv:MONO_GEN_FOUR.description", {
case "monoGenFour":
return i18next.t("achv:monoGenFour.description", {
context: genderStr,
});
case "MONO_GEN_FIVE":
return i18next.t("achv:MONO_GEN_FIVE.description", {
case "monoGenFive":
return i18next.t("achv:monoGenFive.description", {
context: genderStr,
});
case "MONO_GEN_SIX":
return i18next.t("achv:MONO_GEN_SIX.description", { context: genderStr });
case "MONO_GEN_SEVEN":
return i18next.t("achv:MONO_GEN_SEVEN.description", {
case "monoGenSix":
return i18next.t("achv:monoGenSix.description", { context: genderStr });
case "monoGenSeven":
return i18next.t("achv:monoGenSeven.description", {
context: genderStr,
});
case "MONO_GEN_EIGHT":
return i18next.t("achv:MONO_GEN_EIGHT.description", {
case "monoGenEight":
return i18next.t("achv:monoGenEight.description", {
context: genderStr,
});
case "MONO_GEN_NINE":
return i18next.t("achv:MONO_GEN_NINE.description", {
case "monoGenNine":
return i18next.t("achv:monoGenNine.description", {
context: genderStr,
});
case "MONO_NORMAL":
case "MONO_FIGHTING":
case "MONO_FLYING":
case "MONO_POISON":
case "MONO_GROUND":
case "MONO_ROCK":
case "MONO_BUG":
case "MONO_GHOST":
case "MONO_STEEL":
case "MONO_FIRE":
case "MONO_WATER":
case "MONO_GRASS":
case "MONO_ELECTRIC":
case "MONO_PSYCHIC":
case "MONO_ICE":
case "MONO_DRAGON":
case "MONO_DARK":
case "MONO_FAIRY":
return i18next.t("achv:MonoType.description", {
case "monoNormal":
case "monoFighting":
case "monoFlying":
case "monoPoison":
case "monoGround":
case "monoRock":
case "monoBug":
case "monoGhost":
case "monoSteel":
case "monoFire":
case "monoWater":
case "monoGrass":
case "monoElectric":
case "monoPsychic":
case "monoIce":
case "monoDragon":
case "monoDark":
case "monoFairy":
return i18next.t("achv:monoType.description", {
context: genderStr,
type: i18next.t(`pokemonInfo:Type.${localizationKey.slice(5)}`),
// Todo: Remove the `toUpperCase()` again after changing the `pokemonInfo.json` locales
type: i18next.t(`pokemonInfo:Type.${localizationKey.slice(4).toUpperCase()}`),
});
case "FRESH_START":
return i18next.t("achv:FRESH_START.description", { context: genderStr });
case "INVERSE_BATTLE":
return i18next.t("achv:INVERSE_BATTLE.description", {
case "freshStart":
return i18next.t("achv:freshStart.description", { context: genderStr });
case "inverseBattle":
return i18next.t("achv:inverseBattle.description", {
context: genderStr,
});
case "FLIP_STATS":
return i18next.t("achv:FLIP_STATS.description", { context: genderStr });
case "FLIP_INVERSE":
return i18next.t("achv:FLIP_INVERSE.description", { context: genderStr });
case "BREEDERS_IN_SPACE":
return i18next.t("achv:BREEDERS_IN_SPACE.description", {
case "flipStats":
return i18next.t("achv:flipStats.description", { context: genderStr });
case "flipInverse":
return i18next.t("achv:flipInverse.description", { context: genderStr });
case "breedersInSpace":
return i18next.t("achv:breedersInSpace.description", {
context: genderStr,
});
default:
@ -458,84 +459,84 @@ export function getAchievementDescription(localizationKey: string): string {
}
export const achvs = {
_10K_MONEY: new MoneyAchv("10K_MONEY", "", 10000, "nugget", 10),
_100K_MONEY: new MoneyAchv("100K_MONEY", "", 100000, "big_nugget", 25).setSecret(true),
_1M_MONEY: new MoneyAchv("1M_MONEY", "", 1000000, "relic_gold", 50).setSecret(true),
_10M_MONEY: new MoneyAchv("10M_MONEY", "", 10000000, "coin_case", 100).setSecret(true),
_250_DMG: new DamageAchv("250_DMG", "", 250, "lucky_punch", 10),
_1000_DMG: new DamageAchv("1000_DMG", "", 1000, "lucky_punch_great", 25).setSecret(true),
_2500_DMG: new DamageAchv("2500_DMG", "", 2500, "lucky_punch_ultra", 50).setSecret(true),
_10000_DMG: new DamageAchv("10000_DMG", "", 10000, "lucky_punch_master", 100).setSecret(true),
_250_HEAL: new HealAchv("250_HEAL", "", 250, "potion", 10),
_1000_HEAL: new HealAchv("1000_HEAL", "", 1000, "super_potion", 25).setSecret(true),
_2500_HEAL: new HealAchv("2500_HEAL", "", 2500, "hyper_potion", 50).setSecret(true),
_10000_HEAL: new HealAchv("10000_HEAL", "", 10000, "max_potion", 100).setSecret(true),
LV_100: new LevelAchv("LV_100", "", 100, "rare_candy", 25).setSecret(),
LV_250: new LevelAchv("LV_250", "", 250, "rarer_candy", 50).setSecret(true),
LV_1000: new LevelAchv("LV_1000", "", 1000, "candy_jar", 100).setSecret(true),
_10_RIBBONS: new RibbonAchv("10_RIBBONS", "", 10, "bronze_ribbon", 10),
_25_RIBBONS: new RibbonAchv("25_RIBBONS", "", 25, "great_ribbon", 25).setSecret(true),
_50_RIBBONS: new RibbonAchv("50_RIBBONS", "", 50, "ultra_ribbon", 50).setSecret(true),
_75_RIBBONS: new RibbonAchv("75_RIBBONS", "", 75, "rogue_ribbon", 75).setSecret(true),
_100_RIBBONS: new RibbonAchv("100_RIBBONS", "", 100, "master_ribbon", 100).setSecret(true),
TRANSFER_MAX_STAT_STAGE: new Achv("TRANSFER_MAX_STAT_STAGE", "", "TRANSFER_MAX_STAT_STAGE.description", "baton", 20),
MAX_FRIENDSHIP: new Achv("MAX_FRIENDSHIP", "", "MAX_FRIENDSHIP.description", "soothe_bell", 25),
MEGA_EVOLVE: new Achv("MEGA_EVOLVE", "", "MEGA_EVOLVE.description", "mega_bracelet", 50),
GIGANTAMAX: new Achv("GIGANTAMAX", "", "GIGANTAMAX.description", "dynamax_band", 50),
TERASTALLIZE: new Achv("TERASTALLIZE", "", "TERASTALLIZE.description", "tera_orb", 25),
_10K_MONEY: new MoneyAchv("10KMoney", "", 10000, "nugget", 10),
_100K_MONEY: new MoneyAchv("100KMoney", "", 100000, "big_nugget", 25).setSecret(true),
_1M_MONEY: new MoneyAchv("1MMoney", "", 1000000, "relic_gold", 50).setSecret(true),
_10M_MONEY: new MoneyAchv("10MMoney", "", 10000000, "coin_case", 100).setSecret(true),
_250_DMG: new DamageAchv("250Dmg", "", 250, "lucky_punch", 10),
_1000_DMG: new DamageAchv("1000Dmg", "", 1000, "lucky_punch_great", 25).setSecret(true),
_2500_DMG: new DamageAchv("2500Dmg", "", 2500, "lucky_punch_ultra", 50).setSecret(true),
_10000_DMG: new DamageAchv("10000Dmg", "", 10000, "lucky_punch_master", 100).setSecret(true),
_250_HEAL: new HealAchv("250Heal", "", 250, "potion", 10),
_1000_HEAL: new HealAchv("1000Heal", "", 1000, "super_potion", 25).setSecret(true),
_2500_HEAL: new HealAchv("2500Heal", "", 2500, "hyper_potion", 50).setSecret(true),
_10000_HEAL: new HealAchv("10000Heal", "", 10000, "max_potion", 100).setSecret(true),
LV_100: new LevelAchv("lv100", "", 100, "rare_candy", 25).setSecret(),
LV_250: new LevelAchv("lv250", "", 250, "rarer_candy", 50).setSecret(true),
LV_1000: new LevelAchv("lv1000", "", 1000, "candy_jar", 100).setSecret(true),
_10_RIBBONS: new RibbonAchv("10Ribbons", "", 10, "bronze_ribbon", 10),
_25_RIBBONS: new RibbonAchv("25Ribbons", "", 25, "great_ribbon", 25).setSecret(true),
_50_RIBBONS: new RibbonAchv("50Ribbons", "", 50, "ultra_ribbon", 50).setSecret(true),
_75_RIBBONS: new RibbonAchv("75Ribbons", "", 75, "rogue_ribbon", 75).setSecret(true),
_100_RIBBONS: new RibbonAchv("100Ribbons", "", 100, "master_ribbon", 100).setSecret(true),
TRANSFER_MAX_STAT_STAGE: new Achv("transferMaxStatStage", "", "transferMaxStatStage.description", "baton", 20),
MAX_FRIENDSHIP: new Achv("maxFriendship", "", "maxFriendship.description", "soothe_bell", 25),
MEGA_EVOLVE: new Achv("megaEvolve", "", "megaEvolve.description", "mega_bracelet", 50),
GIGANTAMAX: new Achv("gigantamax", "", "gigantamax.description", "dynamax_band", 50),
TERASTALLIZE: new Achv("terastallize", "", "terastallize.description", "tera_orb", 25),
STELLAR_TERASTALLIZE: new Achv(
"STELLAR_TERASTALLIZE",
"stellarTerastallize",
"",
"STELLAR_TERASTALLIZE.description",
"stellarTerastallize.description",
"stellar_tera_shard",
25,
).setSecret(true),
SPLICE: new Achv("SPLICE", "", "SPLICE.description", "dna_splicers", 10),
SPLICE: new Achv("Splice", "", "Splice.description", "dna_splicers", 10),
MINI_BLACK_HOLE: new ModifierAchv(
"MINI_BLACK_HOLE",
"miniBlackHole",
"",
"MINI_BLACK_HOLE.description",
"miniBlackHole.description",
"mini_black_hole",
25,
modifier => modifier instanceof TurnHeldItemTransferModifier,
).setSecret(),
CATCH_MYTHICAL: new Achv("CATCH_MYTHICAL", "", "CATCH_MYTHICAL.description", "strange_ball", 50).setSecret(),
CATCH_SUB_LEGENDARY: new Achv("CATCH_SUB_LEGENDARY", "", "CATCH_SUB_LEGENDARY.description", "rb", 75).setSecret(),
CATCH_LEGENDARY: new Achv("CATCH_LEGENDARY", "", "CATCH_LEGENDARY.description", "mb", 100).setSecret(),
SEE_SHINY: new Achv("SEE_SHINY", "", "SEE_SHINY.description", "pb_gold", 75),
SHINY_PARTY: new Achv("SHINY_PARTY", "", "SHINY_PARTY.description", "shiny_charm", 100).setSecret(true),
HATCH_MYTHICAL: new Achv("HATCH_MYTHICAL", "", "HATCH_MYTHICAL.description", "mystery_egg", 75).setSecret(),
CATCH_MYTHICAL: new Achv("catchMythical", "", "catchMythical.description", "strange_ball", 50).setSecret(),
CATCH_SUB_LEGENDARY: new Achv("catchSubLegendary", "", "catchSubLegendary.description", "rb", 75).setSecret(),
CATCH_LEGENDARY: new Achv("catchLegendary", "", "catchLegendary.description", "mb", 100).setSecret(),
SEE_SHINY: new Achv("seeShiny", "", "seeShiny.description", "pb_gold", 75),
SHINY_PARTY: new Achv("shinyParty", "", "shinyParty.description", "shiny_charm", 100).setSecret(true),
HATCH_MYTHICAL: new Achv("hatchMythical", "", "hatchMythical.description", "mystery_egg", 75).setSecret(),
HATCH_SUB_LEGENDARY: new Achv(
"HATCH_SUB_LEGENDARY",
"hatchSubLegendary",
"",
"HATCH_SUB_LEGENDARY.description",
"hatchSubLegendary.description",
"oval_stone",
100,
).setSecret(),
HATCH_LEGENDARY: new Achv("HATCH_LEGENDARY", "", "HATCH_LEGENDARY.description", "lucky_egg", 125).setSecret(),
HATCH_SHINY: new Achv("HATCH_SHINY", "", "HATCH_SHINY.description", "golden_egg", 100).setSecret(),
HIDDEN_ABILITY: new Achv("HIDDEN_ABILITY", "", "HIDDEN_ABILITY.description", "ability_charm", 75),
PERFECT_IVS: new Achv("PERFECT_IVS", "", "PERFECT_IVS.description", "blunder_policy", 100),
HATCH_LEGENDARY: new Achv("hatchLegendary", "", "hatchLegendary.description", "lucky_egg", 125).setSecret(),
HATCH_SHINY: new Achv("hatchShiny", "", "hatchShiny.description", "golden_egg", 100).setSecret(),
HIDDEN_ABILITY: new Achv("hiddenAbility", "", "hiddenAbility.description", "ability_charm", 75),
PERFECT_IVS: new Achv("perfectIvs", "", "perfectIvs.description", "blunder_policy", 100),
CLASSIC_VICTORY: new Achv(
"CLASSIC_VICTORY",
"classicVictory",
"",
"CLASSIC_VICTORY.description",
"classicVictory.description",
"relic_crown",
150,
_ => globalScene.gameData.gameStats.sessionsWon === 0,
),
UNEVOLVED_CLASSIC_VICTORY: new Achv(
"UNEVOLVED_CLASSIC_VICTORY",
"unevolvedClassicVictory",
"",
"UNEVOLVED_CLASSIC_VICTORY.description",
"unevolvedClassicVictory.description",
"eviolite",
175,
_ => globalScene.getPlayerParty().some(p => p.getSpeciesForm(true).speciesId in pokemonEvolutions),
),
MONO_GEN_ONE_VICTORY: new ChallengeAchv(
"MONO_GEN_ONE",
"monoGenOne",
"",
"MONO_GEN_ONE.description",
"monoGenOne.description",
"ribbon_gen1",
100,
c =>
@ -546,9 +547,9 @@ export const achvs = {
),
),
MONO_GEN_TWO_VICTORY: new ChallengeAchv(
"MONO_GEN_TWO",
"monoGenTwo",
"",
"MONO_GEN_TWO.description",
"monoGenTwo.description",
"ribbon_gen2",
100,
c =>
@ -559,9 +560,9 @@ export const achvs = {
),
),
MONO_GEN_THREE_VICTORY: new ChallengeAchv(
"MONO_GEN_THREE",
"monoGenThree",
"",
"MONO_GEN_THREE.description",
"monoGenThree.description",
"ribbon_gen3",
100,
c =>
@ -572,9 +573,9 @@ export const achvs = {
),
),
MONO_GEN_FOUR_VICTORY: new ChallengeAchv(
"MONO_GEN_FOUR",
"monoGenFour",
"",
"MONO_GEN_FOUR.description",
"monoGenFour.description",
"ribbon_gen4",
100,
c =>
@ -585,9 +586,9 @@ export const achvs = {
),
),
MONO_GEN_FIVE_VICTORY: new ChallengeAchv(
"MONO_GEN_FIVE",
"monoGenFive",
"",
"MONO_GEN_FIVE.description",
"monoGenFive.description",
"ribbon_gen5",
100,
c =>
@ -598,9 +599,9 @@ export const achvs = {
),
),
MONO_GEN_SIX_VICTORY: new ChallengeAchv(
"MONO_GEN_SIX",
"monoGenSix",
"",
"MONO_GEN_SIX.description",
"monoGenSix.description",
"ribbon_gen6",
100,
c =>
@ -611,9 +612,9 @@ export const achvs = {
),
),
MONO_GEN_SEVEN_VICTORY: new ChallengeAchv(
"MONO_GEN_SEVEN",
"monoGenSeven",
"",
"MONO_GEN_SEVEN.description",
"monoGenSeven.description",
"ribbon_gen7",
100,
c =>
@ -624,9 +625,9 @@ export const achvs = {
),
),
MONO_GEN_EIGHT_VICTORY: new ChallengeAchv(
"MONO_GEN_EIGHT",
"monoGenEight",
"",
"MONO_GEN_EIGHT.description",
"monoGenEight.description",
"ribbon_gen8",
100,
c =>
@ -637,9 +638,9 @@ export const achvs = {
),
),
MONO_GEN_NINE_VICTORY: new ChallengeAchv(
"MONO_GEN_NINE",
"monoGenNine",
"",
"MONO_GEN_NINE.description",
"monoGenNine.description",
"ribbon_gen9",
100,
c =>
@ -650,9 +651,9 @@ export const achvs = {
),
),
MONO_NORMAL: new ChallengeAchv(
"MONO_NORMAL",
"monoNormal",
"",
"MONO_NORMAL.description",
"monoNormal.description",
"silk_scarf",
100,
c =>
@ -663,9 +664,9 @@ export const achvs = {
),
),
MONO_FIGHTING: new ChallengeAchv(
"MONO_FIGHTING",
"monoFighting",
"",
"MONO_FIGHTING.description",
"monoFighting.description",
"black_belt",
100,
c =>
@ -676,9 +677,9 @@ export const achvs = {
),
),
MONO_FLYING: new ChallengeAchv(
"MONO_FLYING",
"monoFlying",
"",
"MONO_FLYING.description",
"monoFlying.description",
"sharp_beak",
100,
c =>
@ -689,9 +690,9 @@ export const achvs = {
),
),
MONO_POISON: new ChallengeAchv(
"MONO_POISON",
"monoPoison",
"",
"MONO_POISON.description",
"monoPoison.description",
"poison_barb",
100,
c =>
@ -702,9 +703,9 @@ export const achvs = {
),
),
MONO_GROUND: new ChallengeAchv(
"MONO_GROUND",
"monoGround",
"",
"MONO_GROUND.description",
"monoGround.description",
"soft_sand",
100,
c =>
@ -715,9 +716,9 @@ export const achvs = {
),
),
MONO_ROCK: new ChallengeAchv(
"MONO_ROCK",
"monoRock",
"",
"MONO_ROCK.description",
"monoRock.description",
"hard_stone",
100,
c =>
@ -728,9 +729,9 @@ export const achvs = {
),
),
MONO_BUG: new ChallengeAchv(
"MONO_BUG",
"monoBug",
"",
"MONO_BUG.description",
"monoBug.description",
"silver_powder",
100,
c =>
@ -741,9 +742,9 @@ export const achvs = {
),
),
MONO_GHOST: new ChallengeAchv(
"MONO_GHOST",
"monoGhost",
"",
"MONO_GHOST.description",
"monoGhost.description",
"spell_tag",
100,
c =>
@ -754,9 +755,9 @@ export const achvs = {
),
),
MONO_STEEL: new ChallengeAchv(
"MONO_STEEL",
"monoSteel",
"",
"MONO_STEEL.description",
"monoSteel.description",
"metal_coat",
100,
c =>
@ -767,9 +768,9 @@ export const achvs = {
),
),
MONO_FIRE: new ChallengeAchv(
"MONO_FIRE",
"monoFire",
"",
"MONO_FIRE.description",
"monoFire.description",
"charcoal",
100,
c =>
@ -780,9 +781,9 @@ export const achvs = {
),
),
MONO_WATER: new ChallengeAchv(
"MONO_WATER",
"monoWater",
"",
"MONO_WATER.description",
"monoWater.description",
"mystic_water",
100,
c =>
@ -793,9 +794,9 @@ export const achvs = {
),
),
MONO_GRASS: new ChallengeAchv(
"MONO_GRASS",
"monoGrass",
"",
"MONO_GRASS.description",
"monoGrass.description",
"miracle_seed",
100,
c =>
@ -806,9 +807,9 @@ export const achvs = {
),
),
MONO_ELECTRIC: new ChallengeAchv(
"MONO_ELECTRIC",
"monoElectric",
"",
"MONO_ELECTRIC.description",
"monoElectric.description",
"magnet",
100,
c =>
@ -819,9 +820,9 @@ export const achvs = {
),
),
MONO_PSYCHIC: new ChallengeAchv(
"MONO_PSYCHIC",
"monoPsychic",
"",
"MONO_PSYCHIC.description",
"monoPsychic.description",
"twisted_spoon",
100,
c =>
@ -832,9 +833,9 @@ export const achvs = {
),
),
MONO_ICE: new ChallengeAchv(
"MONO_ICE",
"monoIce",
"",
"MONO_ICE.description",
"monoIce.description",
"never_melt_ice",
100,
c =>
@ -845,9 +846,9 @@ export const achvs = {
),
),
MONO_DRAGON: new ChallengeAchv(
"MONO_DRAGON",
"monoDragon",
"",
"MONO_DRAGON.description",
"monoDragon.description",
"dragon_fang",
100,
c =>
@ -858,9 +859,9 @@ export const achvs = {
),
),
MONO_DARK: new ChallengeAchv(
"MONO_DARK",
"monoDark",
"",
"MONO_DARK.description",
"monoDark.description",
"black_glasses",
100,
c =>
@ -871,9 +872,9 @@ export const achvs = {
),
),
MONO_FAIRY: new ChallengeAchv(
"MONO_FAIRY",
"monoFairy",
"",
"MONO_FAIRY.description",
"monoFairy.description",
"fairy_feather",
100,
c =>
@ -884,9 +885,9 @@ export const achvs = {
),
),
FRESH_START: new ChallengeAchv(
"FRESH_START",
"freshStart",
"",
"FRESH_START.description",
"freshStart.description",
"reviver_seed",
100,
c =>
@ -897,25 +898,25 @@ export const achvs = {
),
),
INVERSE_BATTLE: new ChallengeAchv(
"INVERSE_BATTLE",
"inverseBattle",
"",
"INVERSE_BATTLE.description",
"inverseBattle.description",
"inverse",
100,
c => c instanceof InverseBattleChallenge && c.value > 0,
),
FLIP_STATS: new ChallengeAchv(
"FLIP_STATS",
"flipStats",
"",
"FLIP_STATS.description",
"flipStats.description",
"dubious_disc",
100,
c => c instanceof FlipStatChallenge && c.value > 0,
),
FLIP_INVERSE: new ChallengeAchv(
"FLIP_INVERSE",
"flipInverse",
"",
"FLIP_INVERSE.description",
"flipInverse.description",
"cracked_pot",
100,
c =>
@ -924,19 +925,8 @@ 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),
),
BREEDERS_IN_SPACE: new Achv("BREEDERS_IN_SPACE", "", "BREEDERS_IN_SPACE.description", "moon_stone", 50).setSecret(),
NUZLOCKE: new ChallengeAchv("nuzlocke", "", "nuzlocke.description", "leaf_stone", 100, isNuzlockeChallenge),
BREEDERS_IN_SPACE: new Achv("breedersInSpace", "", "breedersInSpace.description", "moon_stone", 50).setSecret(),
};
export function initAchievements() {

View File

@ -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";
@ -207,10 +208,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;
}
@ -400,7 +403,7 @@ export class GameData {
}
public initSystem(systemDataStr: string, cachedSystemDataStr?: string): Promise<boolean> {
return new Promise<boolean>(resolve => {
const { promise, resolve } = Promise.withResolvers<boolean>();
try {
let systemData = this.parseSystemData(systemDataStr);
@ -514,7 +517,7 @@ export class GameData {
console.error(err);
resolve(false);
}
});
return promise;
}
/**
@ -625,6 +628,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;
@ -823,52 +829,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)!)
: {};
Object.keys(Tutorial)
.map(t => t as Tutorial)
.forEach(t => {
const key = Tutorial[t];
// TODO: We shouldn't be storing this like that
for (const key of Object.values(Tutorial)) {
if (key === tutorial) {
tutorials[key] = flag;
tutorials[key] = status;
} 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;
@ -1633,6 +1638,7 @@ export class GameData {
caughtCount: 0,
hatchedCount: 0,
ivs: [0, 0, 0, 0, 0, 0],
ribbons: new RibbonData(0),
};
}
@ -1877,6 +1883,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);
@ -2176,6 +2188,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);
}
}
}

View 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);
}
}

View 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);
}
}

View File

@ -96,7 +96,7 @@ export class AchvsUiHandler extends MessageUiHandler {
const genderIndex = globalScene.gameData.gender ?? PlayerGender.MALE;
const genderStr = PlayerGender[genderIndex].toLowerCase();
this.achvsName = i18next.t("achv:Achievements.name", { context: genderStr });
this.achvsName = i18next.t("achv:achievements.name", { context: genderStr });
this.vouchersName = i18next.t("voucher:vouchers");
this.iconsBg = addWindow(0, this.headerBg.height, WIDTH - 2, HEIGHT - this.headerBg.height - 68).setOrigin(0);
@ -214,7 +214,7 @@ export class AchvsUiHandler extends MessageUiHandler {
this.showText(!hidden ? achv.description : "");
this.scoreText.setText(`${achv.score}pt`);
this.unlockText.setText(
unlocked ? new Date(achvUnlocks[achv.id]).toLocaleDateString() : i18next.t("achv:Locked.name"),
unlocked ? new Date(achvUnlocks[achv.id]).toLocaleDateString() : i18next.t("achv:locked.name"),
);
}

View File

@ -1,7 +1,7 @@
import { globalScene } from "#app/global-scene";
import { TextStyle } from "#enums/text-style";
import { addTextObject } from "#ui/text";
import { toTitleCase } from "#utils/strings";
import { toCamelCase, toTitleCase } from "#utils/strings";
import i18next from "i18next";
const hiddenX = -150;
@ -100,7 +100,7 @@ export class BgmBar extends Phaser.GameObjects.Container {
}
getRealBgmName(bgmName: string): string {
return i18next.t([`bgmName:${bgmName}`, "bgmName:missing_entries"], {
return i18next.t([`bgmName:${toCamelCase(bgmName)}`, "bgmName:missingEntries"], {
name: toTitleCase(bgmName),
});
}

View File

@ -17,6 +17,7 @@ import { addWindow, WindowVariant } from "#ui/ui-theme";
import { fixedInt, isLocal, sessionIdKey } from "#utils/common";
import { getCookie } from "#utils/cookies";
import { getEnumValues } from "#utils/enums";
import { toCamelCase } from "#utils/strings";
import { isBeta } from "#utils/utility-vars";
import i18next from "i18next";
@ -138,7 +139,7 @@ export class MenuUiHandler extends MessageUiHandler {
this.optionSelectText = addTextObject(
0,
0,
this.menuOptions.map(o => `${i18next.t(`menuUiHandler:${MenuOptions[o]}`)}`).join("\n"),
this.menuOptions.map(o => `${i18next.t(`menuUiHandler:${toCamelCase(MenuOptions[o])}`)}`).join("\n"),
TextStyle.WINDOW,
{ maxLines: this.menuOptions.length },
);

View File

@ -473,7 +473,7 @@ export class MysteryEncounterUiHandler extends UiHandler {
const viewPartyText = addBBCodeTextObject(
globalScene.scaledCanvas.width,
-24,
getBBCodeFrag(i18next.t("mysteryEncounterMessages:view_party_button"), TextStyle.PARTY),
getBBCodeFrag(i18next.t("mysteryEncounterMessages:viewPartyButton"), TextStyle.PARTY),
TextStyle.PARTY,
);
this.optionsContainer.add(viewPartyText);
@ -694,7 +694,7 @@ export class MysteryEncounterUiHandler extends UiHandler {
duration: 750,
onComplete: () => {
this.dexProgressContainer.on("pointerover", () => {
globalScene.ui.showTooltip("", i18next.t("mysteryEncounterMessages:affects_pokedex"), true);
globalScene.ui.showTooltip("", i18next.t("mysteryEncounterMessages:affectsPokedex"), true);
});
this.dexProgressContainer.on("pointerout", () => {
globalScene.ui.hideTooltip();

View File

@ -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.slotHpText = addTextObject(0, 0, `${this.pokemon.hp}/${this.pokemon.getMaxHp()}`, TextStyle.PARTY)
.setOrigin(1, 0)
.setPositionRelative(
this.slotHpBar,
this.slotHpBar.width - 3,
this.slotHpBar.height - 2 + (offsetJa ? 2 : 0),
);
this.slotHpText.setOrigin(1, 0);
this.slotHpText.setVisible(false);
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" : ""}`,
);
}
}
@ -2198,10 +2234,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 +2241,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() {

View File

@ -57,7 +57,7 @@ import { addWindow } from "#ui/ui-theme";
import { BooleanHolder, getLocalizedSpriteKey, isNullOrUndefined, padInt, rgbHexToRgba } from "#utils/common";
import { getEnumValues } from "#utils/enums";
import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils";
import { toTitleCase } from "#utils/strings";
import { toCamelCase, toTitleCase } from "#utils/strings";
import { argbFromRgba } from "@material/material-color-utilities";
import i18next from "i18next";
import type BBCodeText from "phaser3-rex-plugins/plugins/gameobjects/tagtext/bbcodetext/BBCodeText";
@ -1517,13 +1517,13 @@ export class PokedexPageUiHandler extends MessageUiHandler {
this.biomes.map(b => {
options.push({
label:
i18next.t(`biome:${BiomeId[b.biome].toUpperCase()}`) +
i18next.t(`biome:${toCamelCase(BiomeId[b.biome])}`) +
" - " +
i18next.t(`biome:${BiomePoolTier[b.tier].toUpperCase()}`) +
i18next.t(`biome:${toCamelCase(BiomePoolTier[b.tier])}`) +
(b.tod.length === 1 && b.tod[0] === -1
? ""
: " (" +
b.tod.map(tod => i18next.t(`biome:${TimeOfDay[tod].toUpperCase()}`)).join(", ") +
b.tod.map(tod => i18next.t(`biome:${toCamelCase(TimeOfDay[tod])}`)).join(", ") +
")"),
handler: () => false,
});
@ -1538,13 +1538,13 @@ export class PokedexPageUiHandler extends MessageUiHandler {
this.preBiomes.map(b => {
options.push({
label:
i18next.t(`biome:${BiomeId[b.biome].toUpperCase()}`) +
i18next.t(`biome:${toCamelCase(BiomeId[b.biome])}`) +
" - " +
i18next.t(`biome:${BiomePoolTier[b.tier].toUpperCase()}`) +
i18next.t(`biome:${toCamelCase(BiomePoolTier[b.tier])}`) +
(b.tod.length === 1 && b.tod[0] === -1
? ""
: " (" +
b.tod.map(tod => i18next.t(`biome:${TimeOfDay[tod].toUpperCase()}`)).join(", ") +
b.tod.map(tod => i18next.t(`biome:${toCamelCase(TimeOfDay[tod])}`)).join(", ") +
")"),
handler: () => false,
});
@ -2612,7 +2612,7 @@ export class PokedexPageUiHandler extends MessageUiHandler {
// Setting growth rate text
if (isFormCaught) {
let growthReadable = toTitleCase(GrowthRate[species.growthRate]);
const growthAux = growthReadable.replace(" ", "_");
const growthAux = toCamelCase(growthReadable);
if (i18next.exists("growth:" + growthAux)) {
growthReadable = i18next.t(("growth:" + growthAux) as any);
}

View File

@ -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";
@ -706,10 +707,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;
}

View File

@ -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";
@ -72,7 +73,7 @@ import {
import type { StarterPreferences } from "#utils/data";
import { loadStarterPreferences, saveStarterPreferences } from "#utils/data";
import { getPokemonSpeciesForm, getPokerusStarters } from "#utils/pokemon-utils";
import { toTitleCase } from "#utils/strings";
import { toCamelCase, toTitleCase } from "#utils/strings";
import { argbFromRgba } from "@material/material-color-utilities";
import i18next from "i18next";
import type { GameObjects } from "phaser";
@ -2263,7 +2264,7 @@ export class StarterSelectUiHandler extends MessageUiHandler {
});
};
options.push({
label: i18next.t("menuUiHandler:POKEDEX"),
label: i18next.t("menuUiHandler:pokedex"),
handler: () => {
ui.setMode(UiMode.STARTER_SELECT).then(() => {
const attributes = {
@ -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(gameData.starterData[speciesId].classicWinCount > 0)
.setTexture(
gameData.dexData[speciesId].ribbons.has(RibbonData.NUZLOCKE) ? "champion_ribbon_emerald" : "champion_ribbon",
);
container.classicWinIcon.setVisible(globalScene.gameData.starterData[speciesId].classicWinCount > 0);
container.favoriteIcon.setVisible(this.starterPreferences[speciesId]?.favorite ?? false);
// 'Candy Icon' mode
@ -3464,7 +3470,7 @@ export class StarterSelectUiHandler extends MessageUiHandler {
//Growth translate
let growthReadable = toTitleCase(GrowthRate[species.growthRate]);
const growthAux = growthReadable.replace(" ", "_");
const growthAux = toCamelCase(growthReadable);
if (i18next.exists("growth:" + growthAux)) {
growthReadable = i18next.t(("growth:" + growthAux) as any);
}

View File

@ -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);

View File

@ -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;
}

View 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];
};

View File

@ -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
}
}

View File

@ -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,
});

View File

@ -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),

View File

@ -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);

View File

@ -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();
}

View File

@ -50,7 +50,7 @@ export class ChallengeModeHelper extends GameManagerHelper {
});
await this.game.phaseInterceptor.run(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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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!",
);

View File

@ -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;
}

View File

@ -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);

View File

@ -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)}!`,
};
}

View 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,
};
}

View File

@ -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)}!`,
};
}

View File

@ -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)}!`,
};
}

View File

@ -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)}!`,
};
}

View File

@ -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)}!`,
};
}

View 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;
}

View File

@ -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]!`,
};
}

View File

@ -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") {

View File

@ -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)}!`,
};
}

View File

@ -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,
};

View File

@ -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
const matchers =
mode === "superset"
? [...this.customTesters, this.utils.iterableEquality]
: [...this.customTesters, this.utils.subsetEquality, this.utils.iterableEquality];
const pass = this.equals(actualTypes, expectedTypes, matchers);
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,
};
}

View File

@ -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,
};
}

View File

@ -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(),
};
}

View File

@ -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,
};

View File

@ -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
}