Merge branch 'beta' of https://github.com/ben-lear/pokerogue into mystery-encounter-fixes

This commit is contained in:
ImperialSympathizer 2024-09-19 22:19:07 -04:00
commit f877e842e8
91 changed files with 1631 additions and 650 deletions

View File

@ -56,7 +56,7 @@ Check out [Github Issues](https://github.com/pagefaultgames/pokerogue/issues) to
- Pokémon Legends: Arceus - Pokémon Legends: Arceus
- Pokémon Scarlet/Violet - Pokémon Scarlet/Violet
- Firel (Custom Ice Cave, Laboratory, Metropolis, Plains, Power Plant, Seabed, Space, and Volcano biome music) - Firel (Custom Ice Cave, Laboratory, Metropolis, Plains, Power Plant, Seabed, Space, and Volcano biome music)
- Lmz (Custom Jungle biome music) - Lmz (Custom Ancient Ruins, Jungle, and Lake biome music)
- Andr06 (Custom Slum and Sea biome music) - Andr06 (Custom Slum and Sea biome music)
### 🎵 Sound Effects ### 🎵 Sound Effects

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,41 @@
{
"textures": [
{
"image": "expert_pokemon_breeder.png",
"format": "RGBA8888",
"size": {
"w": 39,
"h": 75
},
"scale": 1,
"frames": [
{
"filename": "0001.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 21,
"y": 3,
"w": 39,
"h": 75
},
"frame": {
"x": 0,
"y": 0,
"w": 39,
"h": 75
}
}
]
}
],
"meta": {
"app": "https://www.codeandweb.com/texturepacker",
"version": "3.0",
"smartupdate": "$TexturePacker:SmartUpdate:cb681265d8dca038a518ab14076fd140:18ff41b1ef6967682643a11695926e58:c59ea3971195f5a395b75223a77d9068$"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1006 B

View File

@ -16,6 +16,14 @@ import { TrainerType } from "#enums/trainer-type";
import i18next from "#app/plugins/i18n"; import i18next from "#app/plugins/i18n";
import MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; import MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter";
import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; import { MysteryEncounterMode } from "#enums/mystery-encounter-mode";
import { CustomModifierSettings } from "#app/modifier/modifier-type";
import { ModifierTier } from "#app/modifier/modifier-tier";
export enum ClassicFixedBossWaves {
// TODO: other fixed wave battles should be added here
EVIL_BOSS_1 = 115,
EVIL_BOSS_2 = 165,
}
export enum BattleType { export enum BattleType {
WILD, WILD,
@ -419,6 +427,7 @@ export class FixedBattleConfig {
public getTrainer: GetTrainerFunc; public getTrainer: GetTrainerFunc;
public getEnemyParty: GetEnemyPartyFunc; public getEnemyParty: GetEnemyPartyFunc;
public seedOffsetWaveIndex: number; public seedOffsetWaveIndex: number;
public customModifierRewardSettings?: CustomModifierSettings;
setBattleType(battleType: BattleType): FixedBattleConfig { setBattleType(battleType: BattleType): FixedBattleConfig {
this.battleType = battleType; this.battleType = battleType;
@ -444,6 +453,11 @@ export class FixedBattleConfig {
this.seedOffsetWaveIndex = seedOffsetWaveIndex; this.seedOffsetWaveIndex = seedOffsetWaveIndex;
return this; return this;
} }
setCustomModifierRewards(customModifierRewardSettings: CustomModifierSettings) {
this.customModifierRewardSettings = customModifierRewardSettings;
return this;
}
} }
@ -503,11 +517,13 @@ export const classicFixedBattles: FixedBattleConfigs = {
[8]: new FixedBattleConfig().setBattleType(BattleType.TRAINER) [8]: new FixedBattleConfig().setBattleType(BattleType.TRAINER)
.setGetTrainerFunc(scene => new Trainer(scene, TrainerType.RIVAL, scene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT)), .setGetTrainerFunc(scene => new Trainer(scene, TrainerType.RIVAL, scene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT)),
[25]: new FixedBattleConfig().setBattleType(BattleType.TRAINER) [25]: new FixedBattleConfig().setBattleType(BattleType.TRAINER)
.setGetTrainerFunc(scene => new Trainer(scene, TrainerType.RIVAL_2, scene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT)), .setGetTrainerFunc(scene => new Trainer(scene, TrainerType.RIVAL_2, scene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT))
.setCustomModifierRewards({ guaranteedModifierTiers: [ModifierTier.ULTRA, ModifierTier.GREAT, ModifierTier.GREAT], allowLuckUpgrades: false }),
[35]: new FixedBattleConfig().setBattleType(BattleType.TRAINER) [35]: new FixedBattleConfig().setBattleType(BattleType.TRAINER)
.setGetTrainerFunc(getRandomTrainerFunc([ TrainerType.ROCKET_GRUNT, TrainerType.MAGMA_GRUNT, TrainerType.AQUA_GRUNT, TrainerType.GALACTIC_GRUNT, TrainerType.PLASMA_GRUNT, TrainerType.FLARE_GRUNT, TrainerType.AETHER_GRUNT, TrainerType.SKULL_GRUNT, TrainerType.MACRO_GRUNT ], true)), .setGetTrainerFunc(getRandomTrainerFunc([ TrainerType.ROCKET_GRUNT, TrainerType.MAGMA_GRUNT, TrainerType.AQUA_GRUNT, TrainerType.GALACTIC_GRUNT, TrainerType.PLASMA_GRUNT, TrainerType.FLARE_GRUNT, TrainerType.AETHER_GRUNT, TrainerType.SKULL_GRUNT, TrainerType.MACRO_GRUNT ], true)),
[55]: new FixedBattleConfig().setBattleType(BattleType.TRAINER) [55]: new FixedBattleConfig().setBattleType(BattleType.TRAINER)
.setGetTrainerFunc(scene => new Trainer(scene, TrainerType.RIVAL_3, scene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT)), .setGetTrainerFunc(scene => new Trainer(scene, TrainerType.RIVAL_3, scene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT))
.setCustomModifierRewards({ guaranteedModifierTiers: [ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.GREAT, ModifierTier.GREAT], allowLuckUpgrades: false }),
[62]: new FixedBattleConfig().setBattleType(BattleType.TRAINER).setSeedOffsetWave(35) [62]: new FixedBattleConfig().setBattleType(BattleType.TRAINER).setSeedOffsetWave(35)
.setGetTrainerFunc(getRandomTrainerFunc([ TrainerType.ROCKET_GRUNT, TrainerType.MAGMA_GRUNT, TrainerType.AQUA_GRUNT, TrainerType.GALACTIC_GRUNT, TrainerType.PLASMA_GRUNT, TrainerType.FLARE_GRUNT, TrainerType.AETHER_GRUNT, TrainerType.SKULL_GRUNT, TrainerType.MACRO_GRUNT ], true)), .setGetTrainerFunc(getRandomTrainerFunc([ TrainerType.ROCKET_GRUNT, TrainerType.MAGMA_GRUNT, TrainerType.AQUA_GRUNT, TrainerType.GALACTIC_GRUNT, TrainerType.PLASMA_GRUNT, TrainerType.FLARE_GRUNT, TrainerType.AETHER_GRUNT, TrainerType.SKULL_GRUNT, TrainerType.MACRO_GRUNT ], true)),
[64]: new FixedBattleConfig().setBattleType(BattleType.TRAINER).setSeedOffsetWave(35) [64]: new FixedBattleConfig().setBattleType(BattleType.TRAINER).setSeedOffsetWave(35)
@ -515,17 +531,21 @@ export const classicFixedBattles: FixedBattleConfigs = {
[66]: new FixedBattleConfig().setBattleType(BattleType.TRAINER).setSeedOffsetWave(35) [66]: new FixedBattleConfig().setBattleType(BattleType.TRAINER).setSeedOffsetWave(35)
.setGetTrainerFunc(getRandomTrainerFunc([[ TrainerType.ARCHER, TrainerType.ARIANA, TrainerType.PROTON, TrainerType.PETREL ], [ TrainerType.TABITHA, TrainerType.COURTNEY ], [ TrainerType.MATT, TrainerType.SHELLY ], [ TrainerType.JUPITER, TrainerType.MARS, TrainerType.SATURN ], [ TrainerType.ZINZOLIN, TrainerType.ROOD ], [ TrainerType.XEROSIC, TrainerType.BRYONY ], TrainerType.FABA, TrainerType.PLUMERIA, TrainerType.OLEANA ], true)), .setGetTrainerFunc(getRandomTrainerFunc([[ TrainerType.ARCHER, TrainerType.ARIANA, TrainerType.PROTON, TrainerType.PETREL ], [ TrainerType.TABITHA, TrainerType.COURTNEY ], [ TrainerType.MATT, TrainerType.SHELLY ], [ TrainerType.JUPITER, TrainerType.MARS, TrainerType.SATURN ], [ TrainerType.ZINZOLIN, TrainerType.ROOD ], [ TrainerType.XEROSIC, TrainerType.BRYONY ], TrainerType.FABA, TrainerType.PLUMERIA, TrainerType.OLEANA ], true)),
[95]: new FixedBattleConfig().setBattleType(BattleType.TRAINER) [95]: new FixedBattleConfig().setBattleType(BattleType.TRAINER)
.setGetTrainerFunc(scene => new Trainer(scene, TrainerType.RIVAL_4, scene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT)), .setGetTrainerFunc(scene => new Trainer(scene, TrainerType.RIVAL_4, scene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT))
.setCustomModifierRewards({ guaranteedModifierTiers: [ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.ULTRA], allowLuckUpgrades: false }),
[112]: new FixedBattleConfig().setBattleType(BattleType.TRAINER).setSeedOffsetWave(35) [112]: new FixedBattleConfig().setBattleType(BattleType.TRAINER).setSeedOffsetWave(35)
.setGetTrainerFunc(getRandomTrainerFunc([ TrainerType.ROCKET_GRUNT, TrainerType.MAGMA_GRUNT, TrainerType.AQUA_GRUNT, TrainerType.GALACTIC_GRUNT, TrainerType.PLASMA_GRUNT, TrainerType.FLARE_GRUNT, TrainerType.AETHER_GRUNT, TrainerType.SKULL_GRUNT, TrainerType.MACRO_GRUNT ], true)), .setGetTrainerFunc(getRandomTrainerFunc([ TrainerType.ROCKET_GRUNT, TrainerType.MAGMA_GRUNT, TrainerType.AQUA_GRUNT, TrainerType.GALACTIC_GRUNT, TrainerType.PLASMA_GRUNT, TrainerType.FLARE_GRUNT, TrainerType.AETHER_GRUNT, TrainerType.SKULL_GRUNT, TrainerType.MACRO_GRUNT ], true)),
[114]: new FixedBattleConfig().setBattleType(BattleType.TRAINER).setSeedOffsetWave(35) [114]: new FixedBattleConfig().setBattleType(BattleType.TRAINER).setSeedOffsetWave(35)
.setGetTrainerFunc(getRandomTrainerFunc([[ TrainerType.ARCHER, TrainerType.ARIANA, TrainerType.PROTON, TrainerType.PETREL ], [ TrainerType.TABITHA, TrainerType.COURTNEY ], [ TrainerType.MATT, TrainerType.SHELLY ], [ TrainerType.JUPITER, TrainerType.MARS, TrainerType.SATURN ], [ TrainerType.ZINZOLIN, TrainerType.ROOD ], [ TrainerType.XEROSIC, TrainerType.BRYONY ], TrainerType.FABA, TrainerType.PLUMERIA, TrainerType.OLEANA ], true, 1)), .setGetTrainerFunc(getRandomTrainerFunc([[ TrainerType.ARCHER, TrainerType.ARIANA, TrainerType.PROTON, TrainerType.PETREL ], [ TrainerType.TABITHA, TrainerType.COURTNEY ], [ TrainerType.MATT, TrainerType.SHELLY ], [ TrainerType.JUPITER, TrainerType.MARS, TrainerType.SATURN ], [ TrainerType.ZINZOLIN, TrainerType.ROOD ], [ TrainerType.XEROSIC, TrainerType.BRYONY ], TrainerType.FABA, TrainerType.PLUMERIA, TrainerType.OLEANA ], true, 1)),
[115]: new FixedBattleConfig().setBattleType(BattleType.TRAINER).setSeedOffsetWave(35) [ClassicFixedBossWaves.EVIL_BOSS_1]: new FixedBattleConfig().setBattleType(BattleType.TRAINER).setSeedOffsetWave(35)
.setGetTrainerFunc(getRandomTrainerFunc([ TrainerType.ROCKET_BOSS_GIOVANNI_1, TrainerType.MAXIE, TrainerType.ARCHIE, TrainerType.CYRUS, TrainerType.GHETSIS, TrainerType.LYSANDRE, TrainerType.LUSAMINE, TrainerType.GUZMA, TrainerType.ROSE ])), .setGetTrainerFunc(getRandomTrainerFunc([ TrainerType.ROCKET_BOSS_GIOVANNI_1, TrainerType.MAXIE, TrainerType.ARCHIE, TrainerType.CYRUS, TrainerType.GHETSIS, TrainerType.LYSANDRE, TrainerType.LUSAMINE, TrainerType.GUZMA, TrainerType.ROSE ]))
.setCustomModifierRewards({ guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ROGUE, ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.ULTRA], allowLuckUpgrades: false }),
[145]: new FixedBattleConfig().setBattleType(BattleType.TRAINER) [145]: new FixedBattleConfig().setBattleType(BattleType.TRAINER)
.setGetTrainerFunc(scene => new Trainer(scene, TrainerType.RIVAL_5, scene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT)), .setGetTrainerFunc(scene => new Trainer(scene, TrainerType.RIVAL_5, scene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT))
[165]: new FixedBattleConfig().setBattleType(BattleType.TRAINER).setSeedOffsetWave(35) .setCustomModifierRewards({ guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ROGUE, ModifierTier.ROGUE, ModifierTier.ULTRA, ModifierTier.ULTRA], allowLuckUpgrades: false }),
.setGetTrainerFunc(getRandomTrainerFunc([ TrainerType.ROCKET_BOSS_GIOVANNI_2, TrainerType.MAXIE_2, TrainerType.ARCHIE_2, TrainerType.CYRUS_2, TrainerType.GHETSIS_2, TrainerType.LYSANDRE_2, TrainerType.LUSAMINE_2, TrainerType.GUZMA_2, TrainerType.ROSE_2 ])), [ClassicFixedBossWaves.EVIL_BOSS_2]: new FixedBattleConfig().setBattleType(BattleType.TRAINER).setSeedOffsetWave(35)
.setGetTrainerFunc(getRandomTrainerFunc([ TrainerType.ROCKET_BOSS_GIOVANNI_2, TrainerType.MAXIE_2, TrainerType.ARCHIE_2, TrainerType.CYRUS_2, TrainerType.GHETSIS_2, TrainerType.LYSANDRE_2, TrainerType.LUSAMINE_2, TrainerType.GUZMA_2, TrainerType.ROSE_2 ]))
.setCustomModifierRewards({ guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ROGUE, ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.ULTRA], allowLuckUpgrades: false }),
[182]: new FixedBattleConfig().setBattleType(BattleType.TRAINER) [182]: new FixedBattleConfig().setBattleType(BattleType.TRAINER)
.setGetTrainerFunc(getRandomTrainerFunc([ TrainerType.LORELEI, TrainerType.WILL, TrainerType.SIDNEY, TrainerType.AARON, TrainerType.SHAUNTAL, TrainerType.MALVA, [ TrainerType.HALA, TrainerType.MOLAYNE ], TrainerType.MARNIE_ELITE, TrainerType.RIKA, TrainerType.CRISPIN ])), .setGetTrainerFunc(getRandomTrainerFunc([ TrainerType.LORELEI, TrainerType.WILL, TrainerType.SIDNEY, TrainerType.AARON, TrainerType.SHAUNTAL, TrainerType.MALVA, [ TrainerType.HALA, TrainerType.MOLAYNE ], TrainerType.MARNIE_ELITE, TrainerType.RIKA, TrainerType.CRISPIN ])),
[184]: new FixedBattleConfig().setBattleType(BattleType.TRAINER).setSeedOffsetWave(182) [184]: new FixedBattleConfig().setBattleType(BattleType.TRAINER).setSeedOffsetWave(182)
@ -538,4 +558,5 @@ export const classicFixedBattles: FixedBattleConfigs = {
.setGetTrainerFunc(getRandomTrainerFunc([ TrainerType.BLUE, [ TrainerType.RED, TrainerType.LANCE_CHAMPION ], [ TrainerType.STEVEN, TrainerType.WALLACE ], TrainerType.CYNTHIA, [ TrainerType.ALDER, TrainerType.IRIS ], TrainerType.DIANTHA, TrainerType.HAU, TrainerType.LEON, [ TrainerType.GEETA, TrainerType.NEMONA ], TrainerType.KIERAN ])), .setGetTrainerFunc(getRandomTrainerFunc([ TrainerType.BLUE, [ TrainerType.RED, TrainerType.LANCE_CHAMPION ], [ TrainerType.STEVEN, TrainerType.WALLACE ], TrainerType.CYNTHIA, [ TrainerType.ALDER, TrainerType.IRIS ], TrainerType.DIANTHA, TrainerType.HAU, TrainerType.LEON, [ TrainerType.GEETA, TrainerType.NEMONA ], TrainerType.KIERAN ])),
[195]: new FixedBattleConfig().setBattleType(BattleType.TRAINER) [195]: new FixedBattleConfig().setBattleType(BattleType.TRAINER)
.setGetTrainerFunc(scene => new Trainer(scene, TrainerType.RIVAL_6, scene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT)) .setGetTrainerFunc(scene => new Trainer(scene, TrainerType.RIVAL_6, scene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT))
.setCustomModifierRewards({ guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ROGUE, ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.GREAT, ModifierTier.GREAT], allowLuckUpgrades: false })
}; };

View File

@ -1 +1,5 @@
export const PLAYER_PARTY_MAX_SIZE = 6; /** The maximum size of the player's party */
export const PLAYER_PARTY_MAX_SIZE: number = 6;
/** Whether to use seasonal splash messages in general */
export const USE_SEASONAL_SPLASH_MESSAGES: boolean = false;

View File

@ -1,6 +1,6 @@
import BattleScene from "../battle-scene"; import BattleScene from "../battle-scene";
import PokemonSpecies, { getPokemonSpecies, speciesStarters } from "./pokemon-species"; import PokemonSpecies, { getPokemonSpecies, speciesStarters } from "./pokemon-species";
import { VariantTier } from "../enums/variant-tiers"; import { VariantTier } from "../enums/variant-tier";
import * as Utils from "../utils"; import * as Utils from "../utils";
import Overrides from "#app/overrides"; import Overrides from "#app/overrides";
import { pokemonPrevolutions } from "./pokemon-evolutions"; import { pokemonPrevolutions } from "./pokemon-evolutions";
@ -178,7 +178,7 @@ export class Egg {
// be done because species with no variants get filtered at rollSpecies but if the // be done because species with no variants get filtered at rollSpecies but if the
// species is set via options or the legendary gacha pokemon gets choosen the check never happens // species is set via options or the legendary gacha pokemon gets choosen the check never happens
if (this._species && !getPokemonSpecies(this._species).hasVariants()) { if (this._species && !getPokemonSpecies(this._species).hasVariants()) {
this._variantTier = VariantTier.COMMON; this._variantTier = VariantTier.STANDARD;
} }
// Needs this._tier so it needs to be generated afer the tier override if bought from same species // Needs this._tier so it needs to be generated afer the tier override if bought from same species
this._eggMoveIndex = eggOptions?.eggMoveIndex ?? this.rollEggMoveIndex(); this._eggMoveIndex = eggOptions?.eggMoveIndex ?? this.rollEggMoveIndex();
@ -494,12 +494,12 @@ export class Egg {
// place but I don't want to touch the pokemon class. // place but I don't want to touch the pokemon class.
private rollVariant(): VariantTier { private rollVariant(): VariantTier {
if (!this.isShiny) { if (!this.isShiny) {
return VariantTier.COMMON; return VariantTier.STANDARD;
} }
const rand = Utils.randSeedInt(10); const rand = Utils.randSeedInt(10);
if (rand >= 4) { if (rand >= 4) {
return VariantTier.COMMON; // 6/10 return VariantTier.STANDARD; // 6/10
} else if (rand >= 1) { } else if (rand >= 1) {
return VariantTier.RARE; // 3/10 return VariantTier.RARE; // 3/10
} else { } else {

View File

@ -3982,18 +3982,17 @@ export class StatusCategoryOnAllyAttr extends VariableMoveCategoryAttr {
export class ShellSideArmCategoryAttr extends VariableMoveCategoryAttr { export class ShellSideArmCategoryAttr extends VariableMoveCategoryAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const category = (args[0] as Utils.NumberHolder); const category = (args[0] as Utils.NumberHolder);
const atkRatio = user.getEffectiveStat(Stat.ATK, target, move) / target.getEffectiveStat(Stat.DEF, user, move);
const specialRatio = user.getEffectiveStat(Stat.SPATK, target, move) / target.getEffectiveStat(Stat.SPDEF, user, move);
// Shell Side Arm is much more complicated than it looks, this is a partial implementation to try to achieve something similar to the games const predictedPhysDmg = target.getBaseDamage(user, move, MoveCategory.PHYSICAL, true, true);
if (atkRatio > specialRatio) { const predictedSpecDmg = target.getBaseDamage(user, move, MoveCategory.SPECIAL, true, true);
if (predictedPhysDmg > predictedSpecDmg) {
category.value = MoveCategory.PHYSICAL; category.value = MoveCategory.PHYSICAL;
return true; return true;
} else if (atkRatio === specialRatio && user.randSeedInt(2) === 0) { } else if (predictedPhysDmg === predictedSpecDmg && user.randSeedInt(2) === 0) {
category.value = MoveCategory.PHYSICAL; category.value = MoveCategory.PHYSICAL;
return true; return true;
} }
return false; return false;
} }
} }
@ -4860,16 +4859,18 @@ export class RemoveAllSubstitutesAttr extends MoveEffectAttr {
} }
/** /**
* Attribute used when a move hits a {@linkcode BattlerTagType} for double damage * Attribute used when a move can deal damage to {@linkcode BattlerTagType}
* Moves that always hit but do not deal double damage: Thunder, Fissure, Sky Uppercut,
* Smack Down, Hurricane, Thousand Arrows
* @extends MoveAttr * @extends MoveAttr
*/ */
export class DealsDoubleDamageToTagAttr extends MoveAttr { export class HitsTagAttr extends MoveAttr {
/** The {@linkcode BattlerTagType} this move hits */ /** The {@linkcode BattlerTagType} this move hits */
public tagType: BattlerTagType; public tagType: BattlerTagType;
/** Should this move deal double damage against {@linkcode DealsDoubleDamageToTagAttr.tagType}? */ /** Should this move deal double damage against {@linkcode HitsTagAttr.tagType}? */
public doubleDamage: boolean; public doubleDamage: boolean;
constructor(tagType: BattlerTagType, doubleDamage?: boolean) { constructor(tagType: BattlerTagType, doubleDamage: boolean = false) {
super(); super();
this.tagType = tagType; this.tagType = tagType;
@ -4881,6 +4882,17 @@ export class DealsDoubleDamageToTagAttr extends MoveAttr {
} }
} }
/**
* Used for moves that will always hit for a given tag but also doubles damage.
* Moves include: Gust, Stomp, Body Slam, Surf, Earthquake, Magnitude, Twister,
* Whirlpool, Dragon Rush, Heat Crash, Steam Roller, Flying Press
*/
export class HitsTagForDoubleDamageAttr extends HitsTagAttr {
constructor(tagType: BattlerTagType) {
super(tagType, true);
}
}
export class AddArenaTagAttr extends MoveEffectAttr { export class AddArenaTagAttr extends MoveEffectAttr {
public tagType: ArenaTagType; public tagType: ArenaTagType;
public turnCount: integer; public turnCount: integer;
@ -6760,7 +6772,7 @@ export function initMoves() {
new AttackMove(Moves.CUT, Type.NORMAL, MoveCategory.PHYSICAL, 50, 95, 30, -1, 0, 1) new AttackMove(Moves.CUT, Type.NORMAL, MoveCategory.PHYSICAL, 50, 95, 30, -1, 0, 1)
.slicingMove(), .slicingMove(),
new AttackMove(Moves.GUST, Type.FLYING, MoveCategory.SPECIAL, 40, 100, 35, -1, 0, 1) new AttackMove(Moves.GUST, Type.FLYING, MoveCategory.SPECIAL, 40, 100, 35, -1, 0, 1)
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.FLYING, true) .attr(HitsTagForDoubleDamageAttr, BattlerTagType.FLYING)
.windMove(), .windMove(),
new AttackMove(Moves.WING_ATTACK, Type.FLYING, MoveCategory.PHYSICAL, 60, 100, 35, -1, 0, 1), new AttackMove(Moves.WING_ATTACK, Type.FLYING, MoveCategory.PHYSICAL, 60, 100, 35, -1, 0, 1),
new StatusMove(Moves.WHIRLWIND, Type.NORMAL, -1, 20, -1, -6, 1) new StatusMove(Moves.WHIRLWIND, Type.NORMAL, -1, 20, -1, -6, 1)
@ -6778,7 +6790,7 @@ export function initMoves() {
new AttackMove(Moves.VINE_WHIP, Type.GRASS, MoveCategory.PHYSICAL, 45, 100, 25, -1, 0, 1), new AttackMove(Moves.VINE_WHIP, Type.GRASS, MoveCategory.PHYSICAL, 45, 100, 25, -1, 0, 1),
new AttackMove(Moves.STOMP, Type.NORMAL, MoveCategory.PHYSICAL, 65, 100, 20, 30, 0, 1) new AttackMove(Moves.STOMP, Type.NORMAL, MoveCategory.PHYSICAL, 65, 100, 20, 30, 0, 1)
.attr(AlwaysHitMinimizeAttr) .attr(AlwaysHitMinimizeAttr)
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true) .attr(HitsTagForDoubleDamageAttr, BattlerTagType.MINIMIZED)
.attr(FlinchAttr), .attr(FlinchAttr),
new AttackMove(Moves.DOUBLE_KICK, Type.FIGHTING, MoveCategory.PHYSICAL, 30, 100, 30, -1, 0, 1) new AttackMove(Moves.DOUBLE_KICK, Type.FIGHTING, MoveCategory.PHYSICAL, 30, 100, 30, -1, 0, 1)
.attr(MultiHitAttr, MultiHitType._2), .attr(MultiHitAttr, MultiHitType._2),
@ -6803,7 +6815,7 @@ export function initMoves() {
new AttackMove(Moves.TACKLE, Type.NORMAL, MoveCategory.PHYSICAL, 40, 100, 35, -1, 0, 1), new AttackMove(Moves.TACKLE, Type.NORMAL, MoveCategory.PHYSICAL, 40, 100, 35, -1, 0, 1),
new AttackMove(Moves.BODY_SLAM, Type.NORMAL, MoveCategory.PHYSICAL, 85, 100, 15, 30, 0, 1) new AttackMove(Moves.BODY_SLAM, Type.NORMAL, MoveCategory.PHYSICAL, 85, 100, 15, 30, 0, 1)
.attr(AlwaysHitMinimizeAttr) .attr(AlwaysHitMinimizeAttr)
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true) .attr(HitsTagForDoubleDamageAttr, BattlerTagType.MINIMIZED)
.attr(StatusEffectAttr, StatusEffect.PARALYSIS), .attr(StatusEffectAttr, StatusEffect.PARALYSIS),
new AttackMove(Moves.WRAP, Type.NORMAL, MoveCategory.PHYSICAL, 15, 90, 20, -1, 0, 1) new AttackMove(Moves.WRAP, Type.NORMAL, MoveCategory.PHYSICAL, 15, 90, 20, -1, 0, 1)
.attr(TrapAttr, BattlerTagType.WRAP), .attr(TrapAttr, BattlerTagType.WRAP),
@ -6871,7 +6883,7 @@ export function initMoves() {
new AttackMove(Moves.HYDRO_PUMP, Type.WATER, MoveCategory.SPECIAL, 110, 80, 5, -1, 0, 1), new AttackMove(Moves.HYDRO_PUMP, Type.WATER, MoveCategory.SPECIAL, 110, 80, 5, -1, 0, 1),
new AttackMove(Moves.SURF, Type.WATER, MoveCategory.SPECIAL, 90, 100, 15, -1, 0, 1) new AttackMove(Moves.SURF, Type.WATER, MoveCategory.SPECIAL, 90, 100, 15, -1, 0, 1)
.target(MoveTarget.ALL_NEAR_OTHERS) .target(MoveTarget.ALL_NEAR_OTHERS)
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.UNDERWATER, true) .attr(HitsTagForDoubleDamageAttr, BattlerTagType.UNDERWATER)
.attr(GulpMissileTagAttr), .attr(GulpMissileTagAttr),
new AttackMove(Moves.ICE_BEAM, Type.ICE, MoveCategory.SPECIAL, 90, 100, 10, 10, 0, 1) new AttackMove(Moves.ICE_BEAM, Type.ICE, MoveCategory.SPECIAL, 90, 100, 10, 10, 0, 1)
.attr(StatusEffectAttr, StatusEffect.FREEZE), .attr(StatusEffectAttr, StatusEffect.FREEZE),
@ -6954,18 +6966,18 @@ export function initMoves() {
new AttackMove(Moves.THUNDER, Type.ELECTRIC, MoveCategory.SPECIAL, 110, 70, 10, 30, 0, 1) new AttackMove(Moves.THUNDER, Type.ELECTRIC, MoveCategory.SPECIAL, 110, 70, 10, 30, 0, 1)
.attr(StatusEffectAttr, StatusEffect.PARALYSIS) .attr(StatusEffectAttr, StatusEffect.PARALYSIS)
.attr(ThunderAccuracyAttr) .attr(ThunderAccuracyAttr)
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.FLYING, false), .attr(HitsTagAttr, BattlerTagType.FLYING),
new AttackMove(Moves.ROCK_THROW, Type.ROCK, MoveCategory.PHYSICAL, 50, 90, 15, -1, 0, 1) new AttackMove(Moves.ROCK_THROW, Type.ROCK, MoveCategory.PHYSICAL, 50, 90, 15, -1, 0, 1)
.makesContact(false), .makesContact(false),
new AttackMove(Moves.EARTHQUAKE, Type.GROUND, MoveCategory.PHYSICAL, 100, 100, 10, -1, 0, 1) new AttackMove(Moves.EARTHQUAKE, Type.GROUND, MoveCategory.PHYSICAL, 100, 100, 10, -1, 0, 1)
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.UNDERGROUND, true) .attr(HitsTagForDoubleDamageAttr, BattlerTagType.UNDERGROUND)
.attr(MovePowerMultiplierAttr, (user, target, move) => user.scene.arena.getTerrainType() === TerrainType.GRASSY && target.isGrounded() ? 0.5 : 1) .attr(MovePowerMultiplierAttr, (user, target, move) => user.scene.arena.getTerrainType() === TerrainType.GRASSY && target.isGrounded() ? 0.5 : 1)
.makesContact(false) .makesContact(false)
.target(MoveTarget.ALL_NEAR_OTHERS), .target(MoveTarget.ALL_NEAR_OTHERS),
new AttackMove(Moves.FISSURE, Type.GROUND, MoveCategory.PHYSICAL, 200, 30, 5, -1, 0, 1) new AttackMove(Moves.FISSURE, Type.GROUND, MoveCategory.PHYSICAL, 200, 30, 5, -1, 0, 1)
.attr(OneHitKOAttr) .attr(OneHitKOAttr)
.attr(OneHitKOAccuracyAttr) .attr(OneHitKOAccuracyAttr)
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.UNDERGROUND, false) .attr(HitsTagAttr, BattlerTagType.UNDERGROUND)
.makesContact(false), .makesContact(false),
new AttackMove(Moves.DIG, Type.GROUND, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 1) new AttackMove(Moves.DIG, Type.GROUND, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 1)
.attr(ChargeAttr, ChargeAnim.DIG_CHARGING, i18next.t("moveTriggers:dugAHole", {pokemonName: "{USER}"}), BattlerTagType.UNDERGROUND) .attr(ChargeAttr, ChargeAnim.DIG_CHARGING, i18next.t("moveTriggers:dugAHole", {pokemonName: "{USER}"}), BattlerTagType.UNDERGROUND)
@ -7354,7 +7366,7 @@ export function initMoves() {
.attr(PreMoveMessageAttr, magnitudeMessageFunc) .attr(PreMoveMessageAttr, magnitudeMessageFunc)
.attr(MagnitudePowerAttr) .attr(MagnitudePowerAttr)
.attr(MovePowerMultiplierAttr, (user, target, move) => user.scene.arena.getTerrainType() === TerrainType.GRASSY && target.isGrounded() ? 0.5 : 1) .attr(MovePowerMultiplierAttr, (user, target, move) => user.scene.arena.getTerrainType() === TerrainType.GRASSY && target.isGrounded() ? 0.5 : 1)
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.UNDERGROUND, true) .attr(HitsTagForDoubleDamageAttr, BattlerTagType.UNDERGROUND)
.makesContact(false) .makesContact(false)
.target(MoveTarget.ALL_NEAR_OTHERS), .target(MoveTarget.ALL_NEAR_OTHERS),
new AttackMove(Moves.DYNAMIC_PUNCH, Type.FIGHTING, MoveCategory.PHYSICAL, 100, 50, 5, 100, 0, 2) new AttackMove(Moves.DYNAMIC_PUNCH, Type.FIGHTING, MoveCategory.PHYSICAL, 100, 50, 5, 100, 0, 2)
@ -7410,7 +7422,7 @@ export function initMoves() {
new AttackMove(Moves.CROSS_CHOP, Type.FIGHTING, MoveCategory.PHYSICAL, 100, 80, 5, -1, 0, 2) new AttackMove(Moves.CROSS_CHOP, Type.FIGHTING, MoveCategory.PHYSICAL, 100, 80, 5, -1, 0, 2)
.attr(HighCritAttr), .attr(HighCritAttr),
new AttackMove(Moves.TWISTER, Type.DRAGON, MoveCategory.SPECIAL, 40, 100, 20, 20, 0, 2) new AttackMove(Moves.TWISTER, Type.DRAGON, MoveCategory.SPECIAL, 40, 100, 20, 20, 0, 2)
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.FLYING, true) .attr(HitsTagForDoubleDamageAttr, BattlerTagType.FLYING)
.attr(FlinchAttr) .attr(FlinchAttr)
.windMove() .windMove()
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES),
@ -7442,7 +7454,7 @@ export function initMoves() {
.attr(StatStageChangeAttr, [ Stat.DEF ], -1), .attr(StatStageChangeAttr, [ Stat.DEF ], -1),
new AttackMove(Moves.WHIRLPOOL, Type.WATER, MoveCategory.SPECIAL, 35, 85, 15, -1, 0, 2) new AttackMove(Moves.WHIRLPOOL, Type.WATER, MoveCategory.SPECIAL, 35, 85, 15, -1, 0, 2)
.attr(TrapAttr, BattlerTagType.WHIRLPOOL) .attr(TrapAttr, BattlerTagType.WHIRLPOOL)
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.UNDERWATER, true), .attr(HitsTagForDoubleDamageAttr, BattlerTagType.UNDERWATER),
new AttackMove(Moves.BEAT_UP, Type.DARK, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 2) new AttackMove(Moves.BEAT_UP, Type.DARK, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 2)
.attr(MultiHitAttr, MultiHitType.BEAT_UP) .attr(MultiHitAttr, MultiHitType.BEAT_UP)
.attr(BeatUpAttr) .attr(BeatUpAttr)
@ -7665,7 +7677,7 @@ export function initMoves() {
new AttackMove(Moves.EXTRASENSORY, Type.PSYCHIC, MoveCategory.SPECIAL, 80, 100, 20, 10, 0, 3) new AttackMove(Moves.EXTRASENSORY, Type.PSYCHIC, MoveCategory.SPECIAL, 80, 100, 20, 10, 0, 3)
.attr(FlinchAttr), .attr(FlinchAttr),
new AttackMove(Moves.SKY_UPPERCUT, Type.FIGHTING, MoveCategory.PHYSICAL, 85, 90, 15, -1, 0, 3) new AttackMove(Moves.SKY_UPPERCUT, Type.FIGHTING, MoveCategory.PHYSICAL, 85, 90, 15, -1, 0, 3)
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.FLYING) .attr(HitsTagAttr, BattlerTagType.FLYING)
.punchingMove(), .punchingMove(),
new AttackMove(Moves.SAND_TOMB, Type.GROUND, MoveCategory.PHYSICAL, 35, 85, 15, -1, 0, 3) new AttackMove(Moves.SAND_TOMB, Type.GROUND, MoveCategory.PHYSICAL, 35, 85, 15, -1, 0, 3)
.attr(TrapAttr, BattlerTagType.SAND_TOMB) .attr(TrapAttr, BattlerTagType.SAND_TOMB)
@ -7897,7 +7909,7 @@ export function initMoves() {
.pulseMove(), .pulseMove(),
new AttackMove(Moves.DRAGON_RUSH, Type.DRAGON, MoveCategory.PHYSICAL, 100, 75, 10, 20, 0, 4) new AttackMove(Moves.DRAGON_RUSH, Type.DRAGON, MoveCategory.PHYSICAL, 100, 75, 10, 20, 0, 4)
.attr(AlwaysHitMinimizeAttr) .attr(AlwaysHitMinimizeAttr)
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true) .attr(HitsTagForDoubleDamageAttr, BattlerTagType.MINIMIZED)
.attr(FlinchAttr), .attr(FlinchAttr),
new AttackMove(Moves.POWER_GEM, Type.ROCK, MoveCategory.SPECIAL, 80, 100, 20, -1, 0, 4), new AttackMove(Moves.POWER_GEM, Type.ROCK, MoveCategory.SPECIAL, 80, 100, 20, -1, 0, 4),
new AttackMove(Moves.DRAIN_PUNCH, Type.FIGHTING, MoveCategory.PHYSICAL, 75, 100, 10, -1, 0, 4) new AttackMove(Moves.DRAIN_PUNCH, Type.FIGHTING, MoveCategory.PHYSICAL, 75, 100, 10, -1, 0, 4)
@ -8094,7 +8106,7 @@ export function initMoves() {
.attr(AddBattlerTagAttr, BattlerTagType.IGNORE_FLYING, false, false, 1, 1, true) .attr(AddBattlerTagAttr, BattlerTagType.IGNORE_FLYING, false, false, 1, 1, true)
.attr(AddBattlerTagAttr, BattlerTagType.INTERRUPTED) .attr(AddBattlerTagAttr, BattlerTagType.INTERRUPTED)
.attr(RemoveBattlerTagAttr, [BattlerTagType.FLYING, BattlerTagType.MAGNET_RISEN]) .attr(RemoveBattlerTagAttr, [BattlerTagType.FLYING, BattlerTagType.MAGNET_RISEN])
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.FLYING, false) .attr(HitsTagAttr, BattlerTagType.FLYING)
.makesContact(false), .makesContact(false),
new AttackMove(Moves.STORM_THROW, Type.FIGHTING, MoveCategory.PHYSICAL, 60, 100, 10, -1, 0, 5) new AttackMove(Moves.STORM_THROW, Type.FIGHTING, MoveCategory.PHYSICAL, 60, 100, 10, -1, 0, 5)
.attr(CritOnlyAttr), .attr(CritOnlyAttr),
@ -8109,7 +8121,7 @@ export function initMoves() {
new AttackMove(Moves.HEAVY_SLAM, Type.STEEL, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 5) new AttackMove(Moves.HEAVY_SLAM, Type.STEEL, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 5)
.attr(AlwaysHitMinimizeAttr) .attr(AlwaysHitMinimizeAttr)
.attr(CompareWeightPowerAttr) .attr(CompareWeightPowerAttr)
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true), .attr(HitsTagForDoubleDamageAttr, BattlerTagType.MINIMIZED),
new AttackMove(Moves.SYNCHRONOISE, Type.PSYCHIC, MoveCategory.SPECIAL, 120, 100, 10, -1, 0, 5) new AttackMove(Moves.SYNCHRONOISE, Type.PSYCHIC, MoveCategory.SPECIAL, 120, 100, 10, -1, 0, 5)
.target(MoveTarget.ALL_NEAR_OTHERS) .target(MoveTarget.ALL_NEAR_OTHERS)
.condition(unknownTypeCondition) .condition(unknownTypeCondition)
@ -8262,12 +8274,12 @@ export function initMoves() {
new AttackMove(Moves.HEAT_CRASH, Type.FIRE, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 5) new AttackMove(Moves.HEAT_CRASH, Type.FIRE, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 5)
.attr(AlwaysHitMinimizeAttr) .attr(AlwaysHitMinimizeAttr)
.attr(CompareWeightPowerAttr) .attr(CompareWeightPowerAttr)
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true), .attr(HitsTagForDoubleDamageAttr, BattlerTagType.MINIMIZED),
new AttackMove(Moves.LEAF_TORNADO, Type.GRASS, MoveCategory.SPECIAL, 65, 90, 10, 50, 0, 5) new AttackMove(Moves.LEAF_TORNADO, Type.GRASS, MoveCategory.SPECIAL, 65, 90, 10, 50, 0, 5)
.attr(StatStageChangeAttr, [ Stat.ACC ], -1), .attr(StatStageChangeAttr, [ Stat.ACC ], -1),
new AttackMove(Moves.STEAMROLLER, Type.BUG, MoveCategory.PHYSICAL, 65, 100, 20, 30, 0, 5) new AttackMove(Moves.STEAMROLLER, Type.BUG, MoveCategory.PHYSICAL, 65, 100, 20, 30, 0, 5)
.attr(AlwaysHitMinimizeAttr) .attr(AlwaysHitMinimizeAttr)
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true) .attr(HitsTagForDoubleDamageAttr, BattlerTagType.MINIMIZED)
.attr(FlinchAttr), .attr(FlinchAttr),
new SelfStatusMove(Moves.COTTON_GUARD, Type.GRASS, -1, 10, -1, 0, 5) new SelfStatusMove(Moves.COTTON_GUARD, Type.GRASS, -1, 10, -1, 0, 5)
.attr(StatStageChangeAttr, [ Stat.DEF ], 3, true), .attr(StatStageChangeAttr, [ Stat.DEF ], 3, true),
@ -8280,7 +8292,7 @@ export function initMoves() {
new AttackMove(Moves.HURRICANE, Type.FLYING, MoveCategory.SPECIAL, 110, 70, 10, 30, 0, 5) new AttackMove(Moves.HURRICANE, Type.FLYING, MoveCategory.SPECIAL, 110, 70, 10, 30, 0, 5)
.attr(ThunderAccuracyAttr) .attr(ThunderAccuracyAttr)
.attr(ConfuseAttr) .attr(ConfuseAttr)
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.FLYING, false) .attr(HitsTagAttr, BattlerTagType.FLYING)
.windMove(), .windMove(),
new AttackMove(Moves.HEAD_CHARGE, Type.NORMAL, MoveCategory.PHYSICAL, 120, 100, 15, -1, 0, 5) new AttackMove(Moves.HEAD_CHARGE, Type.NORMAL, MoveCategory.PHYSICAL, 120, 100, 15, -1, 0, 5)
.attr(RecoilAttr) .attr(RecoilAttr)
@ -8336,7 +8348,7 @@ export function initMoves() {
new AttackMove(Moves.FLYING_PRESS, Type.FIGHTING, MoveCategory.PHYSICAL, 100, 95, 10, -1, 0, 6) new AttackMove(Moves.FLYING_PRESS, Type.FIGHTING, MoveCategory.PHYSICAL, 100, 95, 10, -1, 0, 6)
.attr(AlwaysHitMinimizeAttr) .attr(AlwaysHitMinimizeAttr)
.attr(FlyingTypeMultiplierAttr) .attr(FlyingTypeMultiplierAttr)
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true) .attr(HitsTagForDoubleDamageAttr, BattlerTagType.MINIMIZED)
.condition(failOnGravityCondition), .condition(failOnGravityCondition),
new StatusMove(Moves.MAT_BLOCK, Type.FIGHTING, -1, 10, -1, 0, 6) new StatusMove(Moves.MAT_BLOCK, Type.FIGHTING, -1, 10, -1, 0, 6)
.target(MoveTarget.USER_SIDE) .target(MoveTarget.USER_SIDE)
@ -8507,8 +8519,8 @@ export function initMoves() {
new AttackMove(Moves.THOUSAND_ARROWS, Type.GROUND, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6) new AttackMove(Moves.THOUSAND_ARROWS, Type.GROUND, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6)
.attr(NeutralDamageAgainstFlyingTypeMultiplierAttr) .attr(NeutralDamageAgainstFlyingTypeMultiplierAttr)
.attr(AddBattlerTagAttr, BattlerTagType.IGNORE_FLYING, false, false, 1, 1, true) .attr(AddBattlerTagAttr, BattlerTagType.IGNORE_FLYING, false, false, 1, 1, true)
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.FLYING, false) .attr(HitsTagAttr, BattlerTagType.FLYING)
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.MAGNET_RISEN, false) .attr(HitsTagAttr, BattlerTagType.MAGNET_RISEN)
.attr(AddBattlerTagAttr, BattlerTagType.INTERRUPTED) .attr(AddBattlerTagAttr, BattlerTagType.INTERRUPTED)
.attr(RemoveBattlerTagAttr, [BattlerTagType.FLYING, BattlerTagType.MAGNET_RISEN]) .attr(RemoveBattlerTagAttr, [BattlerTagType.FLYING, BattlerTagType.MAGNET_RISEN])
.makesContact(false) .makesContact(false)
@ -8766,7 +8778,7 @@ export function initMoves() {
.ignoresVirtual(), .ignoresVirtual(),
new AttackMove(Moves.MALICIOUS_MOONSAULT, Type.DARK, MoveCategory.PHYSICAL, 180, -1, 1, -1, 0, 7) new AttackMove(Moves.MALICIOUS_MOONSAULT, Type.DARK, MoveCategory.PHYSICAL, 180, -1, 1, -1, 0, 7)
.attr(AlwaysHitMinimizeAttr) .attr(AlwaysHitMinimizeAttr)
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true) .attr(HitsTagAttr, BattlerTagType.MINIMIZED, true)
.partial() .partial()
.ignoresVirtual(), .ignoresVirtual(),
new AttackMove(Moves.OCEANIC_OPERETTA, Type.WATER, MoveCategory.SPECIAL, 195, -1, 1, -1, 0, 7) new AttackMove(Moves.OCEANIC_OPERETTA, Type.WATER, MoveCategory.SPECIAL, 195, -1, 1, -1, 0, 7)
@ -9114,7 +9126,7 @@ export function initMoves() {
new AttackMove(Moves.SHELL_SIDE_ARM, Type.POISON, MoveCategory.SPECIAL, 90, 100, 10, 20, 0, 8) new AttackMove(Moves.SHELL_SIDE_ARM, Type.POISON, MoveCategory.SPECIAL, 90, 100, 10, 20, 0, 8)
.attr(ShellSideArmCategoryAttr) .attr(ShellSideArmCategoryAttr)
.attr(StatusEffectAttr, StatusEffect.POISON) .attr(StatusEffectAttr, StatusEffect.POISON)
.partial(), .partial(), // Physical version of the move does not make contact
new AttackMove(Moves.MISTY_EXPLOSION, Type.FAIRY, MoveCategory.SPECIAL, 100, 100, 5, -1, 0, 8) new AttackMove(Moves.MISTY_EXPLOSION, Type.FAIRY, MoveCategory.SPECIAL, 100, 100, 5, -1, 0, 8)
.attr(SacrificialAttr) .attr(SacrificialAttr)
.target(MoveTarget.ALL_NEAR_OTHERS) .target(MoveTarget.ALL_NEAR_OTHERS)

View File

@ -9,6 +9,7 @@ import {
transitionMysteryEncounterIntroVisuals, transitionMysteryEncounterIntroVisuals,
} from "#app/data/mystery-encounters/utils/encounter-phase-utils"; } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { import {
getRandomPartyMemberFunc,
trainerConfigs, trainerConfigs,
TrainerPartyCompoundTemplate, TrainerPartyCompoundTemplate,
TrainerPartyTemplate, TrainerPartyTemplate,
@ -17,14 +18,12 @@ import {
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { PartyMemberStrength } from "#enums/party-member-strength"; import { PartyMemberStrength } from "#enums/party-member-strength";
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import * as Utils from "#app/utils";
import { isNullOrUndefined, randSeedInt, randSeedShuffle } from "#app/utils"; import { isNullOrUndefined, randSeedInt, randSeedShuffle } from "#app/utils";
import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { TrainerType } from "#enums/trainer-type"; import { TrainerType } from "#enums/trainer-type";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import Pokemon, { EnemyPokemon, PlayerPokemon, PokemonMove } from "#app/field/pokemon"; import Pokemon, { PlayerPokemon, PokemonMove } from "#app/field/pokemon";
import { getPokemonSpecies } from "#app/data/pokemon-species";
import { getEncounterText, showEncounterDialogue } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { getEncounterText, showEncounterDialogue } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { LearnMovePhase } from "#app/phases/learn-move-phase"; import { LearnMovePhase } from "#app/phases/learn-move-phase";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
@ -584,16 +583,6 @@ function getTrainerConfigForWave(waveIndex: number) {
return config; return config;
} }
function getRandomPartyMemberFunc(speciesPool: Species[], trainerSlot: TrainerSlot = TrainerSlot.TRAINER, ignoreEvolution: boolean = false, postProcess?: (enemyPokemon: EnemyPokemon) => void) {
return (scene: BattleScene, level: number, strength: PartyMemberStrength) => {
let species = Utils.randSeedItem(speciesPool);
if (!ignoreEvolution) {
species = getPokemonSpecies(species).getTrainerSpeciesForLevel(level, true, strength);
}
return scene.addEnemyPokemon(getPokemonSpecies(species), level, trainerSlot, undefined, undefined, postProcess);
};
}
function doBugTypeMoveTutor(scene: BattleScene): Promise<void> { function doBugTypeMoveTutor(scene: BattleScene): Promise<void> {
return new Promise<void>(async resolve => { return new Promise<void>(async resolve => {
const moveOptions = scene.currentBattle.mysteryEncounter!.misc.moveTutorOptions; const moveOptions = scene.currentBattle.mysteryEncounter!.misc.moveTutorOptions;

View File

@ -133,7 +133,7 @@ export const DancingLessonsEncounter: MysteryEncounter =
} }
const oricorioData = new PokemonData(enemyPokemon); const oricorioData = new PokemonData(enemyPokemon);
const oricorio = scene.addEnemyPokemon(species, scene.currentBattle.enemyLevels![0], TrainerSlot.NONE, false, oricorioData); const oricorio = scene.addEnemyPokemon(species, level, TrainerSlot.NONE, false, oricorioData);
// Adds a real Pokemon sprite to the field (required for the animation) // Adds a real Pokemon sprite to the field (required for the animation)
scene.getEnemyParty().forEach(enemyPokemon => { scene.getEnemyParty().forEach(enemyPokemon => {

View File

@ -0,0 +1,549 @@
import { EnemyPartyConfig, generateModifierType, initBattleWithEnemyConfig, setEncounterRewards, } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { trainerConfigs } from "#app/data/trainer-config";
import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type";
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import BattleScene from "#app/battle-scene";
import { randSeedShuffle } from "#app/utils";
import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { Biome } from "#enums/biome";
import { TrainerType } from "#enums/trainer-type";
import i18next from "i18next";
import { Species } from "#enums/species";
import { getPokemonSpecies, speciesStarters } from "#app/data/pokemon-species";
import { Nature } from "#enums/nature";
import { Moves } from "#enums/moves";
import { Type } from "#app/data/type";
import { Stat } from "#enums/stat";
import { PlayerPokemon } from "#app/field/pokemon";
import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { IEggOptions } from "#app/data/egg";
import { EggSourceType } from "#enums/egg-source-types";
import { EggTier } from "#enums/egg-type";
import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { achvs } from "#app/system/achv";
/** the i18n namespace for the encounter */
const namespace = "mysteryEncounter:expertPokemonBreeder";
const trainerNameKey = "trainerNames:expert_pokemon_breeder";
const FIRST_STAGE_EVOLUTION_WAVE = 30;
const SECOND_STAGE_EVOLUTION_WAVE = 45;
const FINAL_STAGE_EVOLUTION_WAVE = 60;
const FRIENDSHIP_ADDED = 20;
class BreederSpeciesEvolution {
species: Species;
evolution: number;
constructor(species: Species, evolution: number) {
this.species = species;
this.evolution = evolution;
}
}
const POOL_1_POKEMON: (Species | BreederSpeciesEvolution)[][] = [
[Species.MUNCHLAX, new BreederSpeciesEvolution(Species.SNORLAX, SECOND_STAGE_EVOLUTION_WAVE)],
[Species.HAPPINY, new BreederSpeciesEvolution(Species.CHANSEY, FIRST_STAGE_EVOLUTION_WAVE), new BreederSpeciesEvolution(Species.BLISSEY, FINAL_STAGE_EVOLUTION_WAVE)],
[Species.MAGBY, new BreederSpeciesEvolution(Species.MAGMAR, FIRST_STAGE_EVOLUTION_WAVE), new BreederSpeciesEvolution(Species.MAGMORTAR, FINAL_STAGE_EVOLUTION_WAVE)],
[Species.ELEKID, new BreederSpeciesEvolution(Species.ELECTABUZZ, FIRST_STAGE_EVOLUTION_WAVE), new BreederSpeciesEvolution(Species.ELECTIVIRE, FINAL_STAGE_EVOLUTION_WAVE)],
[Species.RIOLU, new BreederSpeciesEvolution(Species.LUCARIO, SECOND_STAGE_EVOLUTION_WAVE)],
[Species.BUDEW, new BreederSpeciesEvolution(Species.ROSELIA, FIRST_STAGE_EVOLUTION_WAVE), new BreederSpeciesEvolution(Species.ROSERADE, FINAL_STAGE_EVOLUTION_WAVE)],
[Species.TOXEL, new BreederSpeciesEvolution(Species.TOXTRICITY, SECOND_STAGE_EVOLUTION_WAVE)],
[Species.MIME_JR, new BreederSpeciesEvolution(Species.GALAR_MR_MIME, FIRST_STAGE_EVOLUTION_WAVE), new BreederSpeciesEvolution(Species.MR_RIME, FINAL_STAGE_EVOLUTION_WAVE)]
];
const POOL_2_POKEMON: (Species | BreederSpeciesEvolution)[][] = [
[Species.PICHU, new BreederSpeciesEvolution(Species.PIKACHU, FIRST_STAGE_EVOLUTION_WAVE), new BreederSpeciesEvolution(Species.RAICHU, FINAL_STAGE_EVOLUTION_WAVE)],
[Species.PICHU, new BreederSpeciesEvolution(Species.PIKACHU, FIRST_STAGE_EVOLUTION_WAVE), new BreederSpeciesEvolution(Species.ALOLA_RAICHU, FINAL_STAGE_EVOLUTION_WAVE)],
[Species.JYNX],
[Species.TYROGUE, new BreederSpeciesEvolution(Species.HITMONLEE, SECOND_STAGE_EVOLUTION_WAVE)],
[Species.TYROGUE, new BreederSpeciesEvolution(Species.HITMONCHAN, SECOND_STAGE_EVOLUTION_WAVE)],
[Species.TYROGUE, new BreederSpeciesEvolution(Species.HITMONTOP, SECOND_STAGE_EVOLUTION_WAVE)],
[Species.IGGLYBUFF, new BreederSpeciesEvolution(Species.JIGGLYPUFF, FIRST_STAGE_EVOLUTION_WAVE), new BreederSpeciesEvolution(Species.WIGGLYTUFF, FINAL_STAGE_EVOLUTION_WAVE)],
[Species.AZURILL, new BreederSpeciesEvolution(Species.MARILL, FIRST_STAGE_EVOLUTION_WAVE), new BreederSpeciesEvolution(Species.AZUMARILL, FINAL_STAGE_EVOLUTION_WAVE)],
[Species.WYNAUT, new BreederSpeciesEvolution(Species.WOBBUFFET, SECOND_STAGE_EVOLUTION_WAVE)],
[Species.CHINGLING, new BreederSpeciesEvolution(Species.CHIMECHO, SECOND_STAGE_EVOLUTION_WAVE)],
[Species.BONSLY, new BreederSpeciesEvolution(Species.SUDOWOODO, SECOND_STAGE_EVOLUTION_WAVE)],
[Species.MANTYKE, new BreederSpeciesEvolution(Species.MANTINE, SECOND_STAGE_EVOLUTION_WAVE)]
];
/**
* The Expert Pokémon Breeder encounter.
* @see {@link https://github.com/pagefaultgames/pokerogue/issues/3818 | GitHub Issue #3818}
* @see For biome requirements check {@linkcode mysteryEncountersByBiome}
*/
export const TheExpertPokemonBreederEncounter: MysteryEncounter =
MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER)
.withEncounterTier(MysteryEncounterTier.ULTRA)
.withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES)
.withScenePartySizeRequirement(4, 6, true) // Must have at least 4 legal pokemon in party
.withIntroSpriteConfigs([]) // These are set in onInit()
.withIntroDialogue([
{
text: `${namespace}.intro`,
},
{
speaker: trainerNameKey,
text: `${namespace}.intro_dialogue`,
},
])
.withOnInit((scene: BattleScene) => {
const encounter = scene.currentBattle.mysteryEncounter!;
const waveIndex = scene.currentBattle.waveIndex;
// Calculates what trainers are available for battle in the encounter
// If player is in space biome, uses special "Space" version of the trainer
encounter.enemyPartyConfigs = [
getPartyConfig(scene)
];
const cleffaSpecies = waveIndex < FIRST_STAGE_EVOLUTION_WAVE ? Species.CLEFFA : waveIndex < FINAL_STAGE_EVOLUTION_WAVE ? Species.CLEFAIRY : Species.CLEFABLE;
encounter.spriteConfigs = [
{
spriteKey: cleffaSpecies.toString(),
fileRoot: "pokemon",
hasShadow: true,
repeat: true,
x: 14,
y: -2,
yShadow: -2
},
{
spriteKey: "expert_pokemon_breeder",
fileRoot: "trainer",
hasShadow: true,
x: -14,
y: 4,
yShadow: 2
},
];
// Determine the 3 pokemon the player can battle with
let partyCopy = scene.getParty().slice(0);
partyCopy = partyCopy
.filter(p => p.isAllowedInBattle())
.sort((a, b) => a.friendship - b.friendship);
const pokemon1 = partyCopy[0];
const pokemon2 = partyCopy[1];
const pokemon3 = partyCopy[2];
encounter.setDialogueToken("pokemon1Name", pokemon1.getNameToRender());
encounter.setDialogueToken("pokemon2Name", pokemon2.getNameToRender());
encounter.setDialogueToken("pokemon3Name", pokemon3.getNameToRender());
// Dialogue and egg calcs for Pokemon 1
const [pokemon1CommonEggs, pokemon1RareEggs] = calculateEggRewardsForPokemon(pokemon1);
let pokemon1Tooltip = getEncounterText(scene, `${namespace}.option.1.tooltip_base`)!;
if (pokemon1RareEggs > 0) {
const eggsText = i18next.t(`${namespace}.numEggs`, { count: pokemon1RareEggs, rarity: i18next.t("egg:greatTier") });
pokemon1Tooltip += i18next.t(`${namespace}.eggs_tooltip`, { eggs: eggsText });
encounter.setDialogueToken("pokemon1RareEggs", eggsText);
}
if (pokemon1CommonEggs > 0) {
const eggsText = i18next.t(`${namespace}.numEggs`, { count: pokemon1CommonEggs, rarity: i18next.t("egg:defaultTier") });
pokemon1Tooltip += i18next.t(`${namespace}.eggs_tooltip`, { eggs: eggsText });
encounter.setDialogueToken("pokemon1CommonEggs", eggsText);
}
encounter.options[0].dialogue!.buttonTooltip = pokemon1Tooltip;
// Dialogue and egg calcs for Pokemon 2
const [pokemon2CommonEggs, pokemon2RareEggs] = calculateEggRewardsForPokemon(pokemon2);
let pokemon2Tooltip = getEncounterText(scene, `${namespace}.option.2.tooltip_base`)!;
if (pokemon2RareEggs > 0) {
const eggsText = i18next.t(`${namespace}.numEggs`, { count: pokemon2RareEggs, rarity: i18next.t("egg:greatTier") });
pokemon2Tooltip += i18next.t(`${namespace}.eggs_tooltip`, { eggs: eggsText });
encounter.setDialogueToken("pokemon2RareEggs", eggsText);
}
if (pokemon2CommonEggs > 0) {
const eggsText = i18next.t(`${namespace}.numEggs`, { count: pokemon2CommonEggs, rarity: i18next.t("egg:defaultTier") });
pokemon2Tooltip += i18next.t(`${namespace}.eggs_tooltip`, { eggs: eggsText });
encounter.setDialogueToken("pokemon1CommonEggs", eggsText);
}
encounter.options[1].dialogue!.buttonTooltip = pokemon2Tooltip;
// Dialogue and egg calcs for Pokemon 3
const [pokemon3CommonEggs, pokemon3RareEggs] = calculateEggRewardsForPokemon(pokemon3);
let pokemon3Tooltip = getEncounterText(scene, `${namespace}.option.3.tooltip_base`)!;
if (pokemon3RareEggs > 0) {
const eggsText = i18next.t(`${namespace}.numEggs`, { count: pokemon3RareEggs, rarity: i18next.t("egg:greatTier") });
pokemon3Tooltip += i18next.t(`${namespace}.eggs_tooltip`, { eggs: eggsText });
encounter.setDialogueToken("pokemon3RareEggs", eggsText);
}
if (pokemon3CommonEggs > 0) {
const eggsText = i18next.t(`${namespace}.numEggs`, { count: pokemon3CommonEggs, rarity: i18next.t("egg:defaultTier") });
pokemon3Tooltip += i18next.t(`${namespace}.eggs_tooltip`, { eggs: eggsText });
encounter.setDialogueToken("pokemon3CommonEggs", eggsText);
}
encounter.options[2].dialogue!.buttonTooltip = pokemon3Tooltip;
encounter.misc = {
pokemon1,
pokemon1CommonEggs,
pokemon1RareEggs,
pokemon2,
pokemon2CommonEggs,
pokemon2RareEggs,
pokemon3,
pokemon3CommonEggs,
pokemon3RareEggs
};
return true;
})
.withTitle(`${namespace}.title`)
.withDescription(`${namespace}.description`)
.withQuery(`${namespace}.query`)
.withOption(
MysteryEncounterOptionBuilder
.newOptionWithMode(MysteryEncounterOptionMode.DEFAULT)
.withDialogue({
buttonLabel: `${namespace}.option.1.label`,
selected: [
{
speaker: trainerNameKey,
text: `${namespace}.option.selected`,
},
],
})
.withOptionPhase(async (scene: BattleScene) => {
const encounter = scene.currentBattle.mysteryEncounter!;
// Spawn battle with first pokemon
const config: EnemyPartyConfig = encounter.enemyPartyConfigs[0];
const { pokemon1, pokemon1CommonEggs, pokemon1RareEggs } = encounter.misc;
encounter.setDialogueToken("chosenPokemon", pokemon1.getNameToRender());
const eggOptions = getEggOptions(scene, pokemon1CommonEggs, pokemon1RareEggs);
setEncounterRewards(scene, { fillRemaining: true }, eggOptions);
// Remove all Pokemon from the party except the chosen Pokemon
removePokemonFromPartyAndStoreHeldItems(scene, encounter, pokemon1);
// Configure outro dialogue for egg rewards
encounter.dialogue.outro = [
{
speaker: trainerNameKey,
text: `${namespace}.outro`,
},
];
if (encounter.dialogueTokens.hasOwnProperty("pokemon1CommonEggs")) {
encounter.dialogue.outro.push({
text: i18next.t(`${namespace}.gained_eggs`, { numEggs: encounter.dialogueTokens["pokemon1CommonEggs"] }),
});
}
if (encounter.dialogueTokens.hasOwnProperty("pokemon1RareEggs")) {
encounter.dialogue.outro.push({
text: i18next.t(`${namespace}.gained_eggs`, { numEggs: encounter.dialogueTokens["pokemon1RareEggs"] }),
});
}
initBattleWithEnemyConfig(scene, config);
})
.withPostOptionPhase(async (scene: BattleScene) => {
// Give achievement if in Space biome
checkAchievement(scene);
// Give 20 friendship to the chosen pokemon
scene.currentBattle.mysteryEncounter!.misc.pokemon1.addFriendship(FRIENDSHIP_ADDED);
await restorePartyAndHeldItems(scene);
})
.build()
)
.withOption(
MysteryEncounterOptionBuilder
.newOptionWithMode(MysteryEncounterOptionMode.DEFAULT)
.withDialogue({
buttonLabel: `${namespace}.option.2.label`,
selected: [
{
speaker: trainerNameKey,
text: `${namespace}.option.selected`,
},
],
})
.withOptionPhase(async (scene: BattleScene) => {
const encounter = scene.currentBattle.mysteryEncounter!;
// Spawn battle with second pokemon
const config: EnemyPartyConfig = encounter.enemyPartyConfigs[0];
const { pokemon2, pokemon2CommonEggs, pokemon2RareEggs } = encounter.misc;
encounter.setDialogueToken("chosenPokemon", pokemon2.getNameToRender());
const eggOptions = getEggOptions(scene, pokemon2CommonEggs, pokemon2RareEggs);
setEncounterRewards(scene, { fillRemaining: true }, eggOptions);
// Remove all Pokemon from the party except the chosen Pokemon
removePokemonFromPartyAndStoreHeldItems(scene, encounter, pokemon2);
// Configure outro dialogue for egg rewards
encounter.dialogue.outro = [
{
speaker: trainerNameKey,
text: `${namespace}.outro`,
},
];
if (encounter.dialogueTokens.hasOwnProperty("pokemon2CommonEggs")) {
encounter.dialogue.outro.push({
text: i18next.t(`${namespace}.gained_eggs`, { numEggs: encounter.dialogueTokens["pokemon2CommonEggs"] }),
});
}
if (encounter.dialogueTokens.hasOwnProperty("pokemon2RareEggs")) {
encounter.dialogue.outro.push({
text: i18next.t(`${namespace}.gained_eggs`, { numEggs: encounter.dialogueTokens["pokemon2RareEggs"] }),
});
}
initBattleWithEnemyConfig(scene, config);
})
.withPostOptionPhase(async (scene: BattleScene) => {
// Give achievement if in Space biome
checkAchievement(scene);
// Give 20 friendship to the chosen pokemon
scene.currentBattle.mysteryEncounter!.misc.pokemon2.addFriendship(FRIENDSHIP_ADDED);
await restorePartyAndHeldItems(scene);
})
.build()
)
.withOption(
MysteryEncounterOptionBuilder
.newOptionWithMode(MysteryEncounterOptionMode.DEFAULT)
.withDialogue({
buttonLabel: `${namespace}.option.3.label`,
selected: [
{
speaker: trainerNameKey,
text: `${namespace}.option.selected`,
},
],
})
.withOptionPhase(async (scene: BattleScene) => {
const encounter = scene.currentBattle.mysteryEncounter!;
// Spawn battle with third pokemon
const config: EnemyPartyConfig = encounter.enemyPartyConfigs[0];
const { pokemon3, pokemon3CommonEggs, pokemon3RareEggs } = encounter.misc;
encounter.setDialogueToken("chosenPokemon", pokemon3.getNameToRender());
const eggOptions = getEggOptions(scene, pokemon3CommonEggs, pokemon3RareEggs);
setEncounterRewards(scene, { fillRemaining: true }, eggOptions);
// Remove all Pokemon from the party except the chosen Pokemon
removePokemonFromPartyAndStoreHeldItems(scene, encounter, pokemon3);
// Configure outro dialogue for egg rewards
encounter.dialogue.outro = [
{
speaker: trainerNameKey,
text: `${namespace}.outro`,
},
];
if (encounter.dialogueTokens.hasOwnProperty("pokemon3CommonEggs")) {
encounter.dialogue.outro.push({
text: i18next.t(`${namespace}.gained_eggs`, { numEggs: encounter.dialogueTokens["pokemon3CommonEggs"] }),
});
}
if (encounter.dialogueTokens.hasOwnProperty("pokemon3RareEggs")) {
encounter.dialogue.outro.push({
text: i18next.t(`${namespace}.gained_eggs`, { numEggs: encounter.dialogueTokens["pokemon3RareEggs"] }),
});
}
initBattleWithEnemyConfig(scene, config);
})
.withPostOptionPhase(async (scene: BattleScene) => {
// Give achievement if in Space biome
checkAchievement(scene);
// Give 20 friendship to the chosen pokemon
scene.currentBattle.mysteryEncounter!.misc.pokemon3.addFriendship(FRIENDSHIP_ADDED);
await restorePartyAndHeldItems(scene);
})
.build()
)
.withOutroDialogue([
{
text: `${namespace}.outro`,
},
])
.build();
function getPartyConfig(scene: BattleScene): EnemyPartyConfig {
// Bug type superfan trainer config
const waveIndex = scene.currentBattle.waveIndex;
const breederConfig = trainerConfigs[TrainerType.EXPERT_POKEMON_BREEDER].clone();
breederConfig.name = i18next.t(trainerNameKey);
// First mon is *always* this special cleffa
const cleffaSpecies = waveIndex < FIRST_STAGE_EVOLUTION_WAVE ? Species.CLEFFA : waveIndex < FINAL_STAGE_EVOLUTION_WAVE ? Species.CLEFAIRY : Species.CLEFABLE;
const baseConfig: EnemyPartyConfig = {
trainerType: TrainerType.EXPERT_POKEMON_BREEDER,
pokemonConfigs: [
{
nickname: i18next.t(`${namespace}.cleffa_1_nickname`),
species: getPokemonSpecies(cleffaSpecies),
isBoss: false,
abilityIndex: 1, // Magic Guard
shiny: false,
nature: Nature.ADAMANT,
moveSet: [Moves.METEOR_MASH, Moves.FIRE_PUNCH, Moves.ICE_PUNCH, Moves.THUNDER_PUNCH],
ivs: [31, 31, 31, 31, 31, 31],
modifierConfigs: [
{
modifier: generateModifierType(scene, modifierTypes.TERA_SHARD, [Type.STEEL]) as PokemonHeldItemModifierType,
},
{
modifier: generateModifierType(scene, modifierTypes.BASE_STAT_BOOSTER, [Stat.ATK]) as PokemonHeldItemModifierType,
stackCount: 1 + Math.floor(waveIndex / 20), // +1 Protein every 20 waves
},
{
modifier: generateModifierType(scene, modifierTypes.BASE_STAT_BOOSTER, [Stat.SPD]) as PokemonHeldItemModifierType,
stackCount: 1 + Math.floor(waveIndex / 40), // +1 Carbos every 40 waves
},
]
}
]
};
if (scene.arena.biomeType === Biome.SPACE) {
// All 3 members always Cleffa line, but different configs
baseConfig.pokemonConfigs!.push({
nickname: i18next.t(`${namespace}.cleffa_2_nickname`),
species: getPokemonSpecies(cleffaSpecies),
isBoss: false,
abilityIndex: 1, // Magic Guard
shiny: true,
variant: 1,
nature: Nature.MODEST,
moveSet: [Moves.MOONBLAST, Moves.MYSTICAL_FIRE, Moves.ICE_BEAM, Moves.THUNDERBOLT],
ivs: [31, 31, 31, 31, 31, 31]
},
{
nickname: i18next.t(`${namespace}.cleffa_3_nickname`, { speciesName: getPokemonSpecies(cleffaSpecies).getName() }),
species: getPokemonSpecies(cleffaSpecies),
isBoss: false,
abilityIndex: 2, // Friend Guard / Unaware
shiny: true,
variant: 2,
nature: Nature.BOLD,
moveSet: [Moves.TRI_ATTACK, Moves.STORED_POWER, Moves.TAKE_HEART, Moves.MOONLIGHT],
ivs: [31, 31, 31, 31, 31, 31]
});
} else {
// Second member from pool 1
const pool1Species = getSpeciesFromPool(POOL_1_POKEMON, waveIndex);
// Third member from pool 2
const pool2Species = getSpeciesFromPool(POOL_2_POKEMON, waveIndex);
baseConfig.pokemonConfigs!.push({
species: getPokemonSpecies(pool1Species),
isBoss: false,
ivs: [31, 31, 31, 31, 31, 31]
},
{
species: getPokemonSpecies(pool2Species),
isBoss: false,
ivs: [31, 31, 31, 31, 31, 31]
});
}
return baseConfig;
}
function getSpeciesFromPool(speciesPool: (Species | BreederSpeciesEvolution)[][], waveIndex: number): Species {
const poolCopy = speciesPool.slice(0);
randSeedShuffle(poolCopy);
const speciesEvolutions = poolCopy.pop()!.slice(0);
let speciesObject = speciesEvolutions.pop()!;
while (speciesObject instanceof BreederSpeciesEvolution && speciesObject.evolution > waveIndex) {
speciesObject = speciesEvolutions.pop()!;
}
return speciesObject instanceof BreederSpeciesEvolution ? speciesObject.species : speciesObject;
}
function calculateEggRewardsForPokemon(pokemon: PlayerPokemon): [number, number] {
const bst = pokemon.calculateBaseStats().reduce((a, b) => a + b, 0);
// 1 point for every 20 points below 680 BST the pokemon is, (max 18, min 1)
const pointsFromBst = Math.min(Math.max(Math.floor((680 - bst) / 20), 1), 18);
const rootSpecies = pokemon.species.getRootSpeciesId(true);
let pointsFromStarterTier = 0;
// 2 points for every 1 below 7 that the pokemon's starter tier is (max 12, min 0)
if (speciesStarters.hasOwnProperty(rootSpecies)) {
const starterTier = speciesStarters[rootSpecies];
pointsFromStarterTier = Math.min(Math.max(Math.floor(7 - starterTier) * 2, 0), 12);
}
// Maximum of 30 points
const totalPoints = Math.min(pointsFromStarterTier + pointsFromBst, 30);
// 1 Rare egg for every 6 points
const numRares = Math.floor(totalPoints / 6);
// 1 Common egg for every point leftover
const numCommons = totalPoints % 6;
return [numCommons, numRares];
}
function getEggOptions(scene: BattleScene, commonEggs: number, rareEggs: number) {
const eggDescription = i18next.t(`${namespace}.title`) + ":\n" + i18next.t(trainerNameKey);
const eggOptions: IEggOptions[] = [];
if (commonEggs > 0) {
for (let i = 0; i < commonEggs; i++) {
eggOptions.push({
scene,
pulled: false,
sourceType: EggSourceType.EVENT,
eggDescriptor: eggDescription,
tier: EggTier.COMMON
});
}
}
if (rareEggs > 0) {
for (let i = 0; i < rareEggs; i++) {
eggOptions.push({
scene,
pulled: false,
sourceType: EggSourceType.EVENT,
eggDescriptor: eggDescription,
tier: EggTier.GREAT
});
}
}
return eggOptions;
}
function removePokemonFromPartyAndStoreHeldItems(scene: BattleScene, encounter: MysteryEncounter, chosenPokemon: PlayerPokemon) {
const party = scene.getParty();
const chosenIndex = party.indexOf(chosenPokemon);
party[chosenIndex] = party[0];
party[0] = chosenPokemon;
encounter.misc.originalParty = scene.getParty().slice(1);
encounter.misc.originalPartyHeldItems = encounter.misc.originalParty
.map(p => p.getHeldItems());
scene["party"] = [
chosenPokemon
];
}
function checkAchievement(scene: BattleScene) {
if (scene.arena.biomeType === Biome.SPACE) {
scene.validateAchv(achvs.BREEDERS_IN_SPACE);
}
}
async function restorePartyAndHeldItems(scene: BattleScene) {
const encounter = scene.currentBattle.mysteryEncounter!;
// Restore original party
scene.getParty().push(...encounter.misc.originalParty);
// Restore held items
const originalHeldItems = encounter.misc.originalPartyHeldItems;
originalHeldItems.forEach(pokemonHeldItemsList => {
pokemonHeldItemsList.forEach(heldItem => {
scene.addModifier(heldItem, true, false, false, true);
});
});
await scene.updateModifiers(true);
}

View File

@ -31,6 +31,7 @@ import { BugTypeSuperfanEncounter } from "#app/data/mystery-encounters/encounter
import { FunAndGamesEncounter } from "#app/data/mystery-encounters/encounters/fun-and-games-encounter"; import { FunAndGamesEncounter } from "#app/data/mystery-encounters/encounters/fun-and-games-encounter";
import { UncommonBreedEncounter } from "#app/data/mystery-encounters/encounters/uncommon-breed-encounter"; import { UncommonBreedEncounter } from "#app/data/mystery-encounters/encounters/uncommon-breed-encounter";
import { GlobalTradeSystemEncounter } from "#app/data/mystery-encounters/encounters/global-trade-system-encounter"; import { GlobalTradeSystemEncounter } from "#app/data/mystery-encounters/encounters/global-trade-system-encounter";
import { TheExpertPokemonBreederEncounter } from "#app/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter";
/** /**
* Spawn chance: (BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT + WIGHT_INCREMENT_ON_SPAWN_MISS * <number of missed spawns>) / MYSTERY_ENCOUNTER_SPAWN_MAX_WEIGHT * Spawn chance: (BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT + WIGHT_INCREMENT_ON_SPAWN_MISS * <number of missed spawns>) / MYSTERY_ENCOUNTER_SPAWN_MAX_WEIGHT
@ -184,7 +185,8 @@ const humanTransitableBiomeEncounters: MysteryEncounterType[] = [
MysteryEncounterType.SHADY_VITAMIN_DEALER, MysteryEncounterType.SHADY_VITAMIN_DEALER,
MysteryEncounterType.THE_POKEMON_SALESMAN, MysteryEncounterType.THE_POKEMON_SALESMAN,
MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE, MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE,
MysteryEncounterType.THE_WINSTRATE_CHALLENGE MysteryEncounterType.THE_WINSTRATE_CHALLENGE,
MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER
]; ];
const civilizationBiomeEncounters: MysteryEncounterType[] = [ const civilizationBiomeEncounters: MysteryEncounterType[] = [
@ -238,7 +240,6 @@ export const mysteryEncountersByBiome = new Map<Biome, MysteryEncounterType[]>([
MysteryEncounterType.SAFARI_ZONE, MysteryEncounterType.SAFARI_ZONE,
MysteryEncounterType.ABSOLUTE_AVARICE MysteryEncounterType.ABSOLUTE_AVARICE
]], ]],
[Biome.SEA, [ [Biome.SEA, [
MysteryEncounterType.LOST_AT_SEA MysteryEncounterType.LOST_AT_SEA
]], ]],
@ -275,7 +276,9 @@ export const mysteryEncountersByBiome = new Map<Biome, MysteryEncounterType[]>([
[Biome.ABYSS, [ [Biome.ABYSS, [
MysteryEncounterType.DANCING_LESSONS MysteryEncounterType.DANCING_LESSONS
]], ]],
[Biome.SPACE, []], [Biome.SPACE, [
MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER
]],
[Biome.CONSTRUCTION_SITE, []], [Biome.CONSTRUCTION_SITE, []],
[Biome.JUNGLE, [ [Biome.JUNGLE, [
MysteryEncounterType.SAFARI_ZONE MysteryEncounterType.SAFARI_ZONE
@ -319,6 +322,7 @@ export function initMysteryEncounters() {
allMysteryEncounters[MysteryEncounterType.FUN_AND_GAMES] = FunAndGamesEncounter; allMysteryEncounters[MysteryEncounterType.FUN_AND_GAMES] = FunAndGamesEncounter;
allMysteryEncounters[MysteryEncounterType.UNCOMMON_BREED] = UncommonBreedEncounter; allMysteryEncounters[MysteryEncounterType.UNCOMMON_BREED] = UncommonBreedEncounter;
allMysteryEncounters[MysteryEncounterType.GLOBAL_TRADE_SYSTEM] = GlobalTradeSystemEncounter; allMysteryEncounters[MysteryEncounterType.GLOBAL_TRADE_SYSTEM] = GlobalTradeSystemEncounter;
allMysteryEncounters[MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER] = TheExpertPokemonBreederEncounter;
// Add extreme encounters to biome map // Add extreme encounters to biome map
extremeBiomeEncounters.forEach(encounter => { extremeBiomeEncounters.forEach(encounter => {

View File

@ -36,6 +36,7 @@ import { BattleEndPhase } from "#app/phases/battle-end-phase";
import { GameOverPhase } from "#app/phases/game-over-phase"; import { GameOverPhase } from "#app/phases/game-over-phase";
import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; import { SelectModifierPhase } from "#app/phases/select-modifier-phase";
import { PartyExpPhase } from "#app/phases/party-exp-phase"; import { PartyExpPhase } from "#app/phases/party-exp-phase";
import { Variant } from "#app/data/variant";
/** /**
* Animates exclamation sprite over trainer's head at start of encounter * Animates exclamation sprite over trainer's head at start of encounter
@ -67,6 +68,7 @@ export function doTrainerExclamation(scene: BattleScene) {
export interface EnemyPokemonConfig { export interface EnemyPokemonConfig {
species: PokemonSpecies; species: PokemonSpecies;
isBoss: boolean; isBoss: boolean;
nickname?: string;
bossSegments?: number; bossSegments?: number;
bossSegmentModifier?: number; // Additive to the determined segment number bossSegmentModifier?: number; // Additive to the determined segment number
mysteryEncounterPokemonData?: MysteryEncounterPokemonData; mysteryEncounterPokemonData?: MysteryEncounterPokemonData;
@ -79,6 +81,8 @@ export interface EnemyPokemonConfig {
nature?: Nature; nature?: Nature;
ivs?: [number, number, number, number, number, number]; ivs?: [number, number, number, number, number, number];
shiny?: boolean; shiny?: boolean;
/** Is only checked if Pokemon is shiny */
variant?: Variant;
/** Can set just the status, or pass a timer on the status turns */ /** Can set just the status, or pass a timer on the status turns */
status?: StatusEffect | [StatusEffect, number]; status?: StatusEffect | [StatusEffect, number];
mysteryEncounterBattleEffects?: (pokemon: Pokemon) => void; mysteryEncounterBattleEffects?: (pokemon: Pokemon) => void;
@ -220,6 +224,11 @@ export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig:
if (partyConfig?.pokemonConfigs && e < partyConfig.pokemonConfigs.length) { if (partyConfig?.pokemonConfigs && e < partyConfig.pokemonConfigs.length) {
const config = partyConfig.pokemonConfigs[e]; const config = partyConfig.pokemonConfigs[e];
// Set form
if (!isNullOrUndefined(config.nickname)) {
enemyPokemon.nickname = btoa(unescape(encodeURIComponent(config.nickname!)));
}
// Generate new id, reset status and HP in case using data source // Generate new id, reset status and HP in case using data source
if (config.dataSource) { if (config.dataSource) {
enemyPokemon.id = Utils.randSeedInt(4294967296); enemyPokemon.id = Utils.randSeedInt(4294967296);
@ -235,6 +244,11 @@ export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig:
enemyPokemon.shiny = config.shiny!; enemyPokemon.shiny = config.shiny!;
} }
// Set Variant
if (enemyPokemon.shiny && !isNullOrUndefined(config.variant)) {
enemyPokemon.variant = config.variant!;
}
// Set custom mystery encounter data fields (such as sprite scale, custom abilities, types, etc.) // Set custom mystery encounter data fields (such as sprite scale, custom abilities, types, etc.)
if (!isNullOrUndefined(config.mysteryEncounterPokemonData)) { if (!isNullOrUndefined(config.mysteryEncounterPokemonData)) {
enemyPokemon.mysteryEncounterPokemonData = config.mysteryEncounterPokemonData!; enemyPokemon.mysteryEncounterPokemonData = config.mysteryEncounterPokemonData!;
@ -315,6 +329,9 @@ export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig:
// Requires re-priming summon data to update everything properly // Requires re-priming summon data to update everything properly
enemyPokemon.primeSummonData(enemyPokemon.summonData); enemyPokemon.primeSummonData(enemyPokemon.summonData);
if (enemyPokemon.isShiny() && !enemyPokemon["shinySparkle"]) {
enemyPokemon.initShinySparkle();
}
enemyPokemon.initBattleInfo(); enemyPokemon.initBattleInfo();
enemyPokemon.getBattleInfo().initInfo(enemyPokemon); enemyPokemon.getBattleInfo().initInfo(enemyPokemon);
enemyPokemon.generateName(); enemyPokemon.generateName();

View File

@ -1,46 +1,136 @@
import i18next from "i18next"; import { USE_SEASONAL_SPLASH_MESSAGES } from "#app/constants";
export function getBattleCountSplashMessage(): string { //#region Interfaces/Types
return `{COUNT} ${i18next.t("splashMessages:battlesWon")}`;
type Month = "01" | "02" | "03" | "04" | "05" | "06" | "07" | "08" | "09" | "10" | "11" | "12";
type Day =
| Month
| "13"
| "14"
| "15"
| "16"
| "17"
| "18"
| "19"
| "20"
| "21"
| "22"
| "23"
| "24"
| "25"
| "26"
| "27"
| "28"
| "29"
| "30"
| "31";
/**
* Represents a season with its {@linkcode name},
* {@linkcode start} day+month, {@linkcode end} day+month
* and {@linkcode messages}.
*/
interface Season {
/** The name of the season (internal use only) */
name: string;
/** The start day and month of the season. Format `MM-DD` */
start: `${Month}-${Day}`;
/** The end day and month of the season. Format `MM-DD` */
end: `${Month}-${Day}`;
/** Collection of the messages to display (without the `i18next.t()` call!) */
messages: string[];
} }
//#region Constants
/** The weight multiplier for the battles-won splash message */
const BATTLES_WON_WEIGHT_MULTIPLIER = 10;
/** The weight multiplier for the seasonal splash messages */
const SEASONAL_WEIGHT_MULTIPLIER = 10;
//#region Common Messages
const commonSplashMessages = [
...Array(BATTLES_WON_WEIGHT_MULTIPLIER).fill("battlesWon"),
"joinTheDiscord",
"infiniteLevels",
"everythingStacks",
"optionalSaveScumming",
"biomes",
"openSource",
"playWithSpeed",
"liveBugTesting",
"heavyInfluence",
"pokemonRiskAndPokemonRain",
"nowWithMoreSalt",
"infiniteFusionAtHome",
"brokenEggMoves",
"magnificent",
"mubstitute",
"thatsCrazy",
"oranceJuice",
"questionableBalancing",
"coolShaders",
"aiFree",
"suddenDifficultySpikes",
"basedOnAnUnfinishedFlashGame",
"moreAddictiveThanIntended",
"mostlyConsistentSeeds",
"achievementPointsDontDoAnything",
"youDoNotStartAtLevel",
"dontTalkAboutTheManaphyEggIncident",
"alsoTryPokengine",
"alsoTryEmeraldRogue",
"alsoTryRadicalRed",
"eeveeExpo",
"ynoproject",
"breedersInSpace",
];
//#region Seasonal Messages
const seasonalSplashMessages: Season[] = [
{
name: "Halloween",
start: "09-15",
end: "10-31",
messages: ["halloween.pumpkaboosAbout", "halloween.mayContainSpiders", "halloween.spookyScaryDuskulls"],
},
{
name: "XMAS",
start: "12-01",
end: "12-26",
messages: ["xmas.happyHolidays", "xmas.delibirdSeason"],
},
{
name: "New Year's",
start: "01-01",
end: "01-31",
messages: ["newYears.happyNewYear"],
},
];
//#endregion
export function getSplashMessages(): string[] { export function getSplashMessages(): string[] {
const splashMessages = Array(10).fill(getBattleCountSplashMessage()); const splashMessages: string[] = [...commonSplashMessages];
splashMessages.push( console.log("use seasonal splash messages", USE_SEASONAL_SPLASH_MESSAGES);
i18next.t("splashMessages:joinTheDiscord"), if (USE_SEASONAL_SPLASH_MESSAGES) {
i18next.t("splashMessages:infiniteLevels"), // add seasonal splash messages if the season is active
i18next.t("splashMessages:everythingStacks"), for (const { name, start, end, messages } of seasonalSplashMessages) {
i18next.t("splashMessages:optionalSaveScumming"), const now = new Date();
i18next.t("splashMessages:biomes"), const startDate = new Date(`${start}-${now.getFullYear()}`);
i18next.t("splashMessages:openSource"), const endDate = new Date(`${end}-${now.getFullYear()}`);
i18next.t("splashMessages:playWithSpeed"),
i18next.t("splashMessages:liveBugTesting"),
i18next.t("splashMessages:heavyInfluence"),
i18next.t("splashMessages:pokemonRiskAndPokemonRain"),
i18next.t("splashMessages:nowWithMoreSalt"),
i18next.t("splashMessages:infiniteFusionAtHome"),
i18next.t("splashMessages:brokenEggMoves"),
i18next.t("splashMessages:magnificent"),
i18next.t("splashMessages:mubstitute"),
i18next.t("splashMessages:thatsCrazy"),
i18next.t("splashMessages:oranceJuice"),
i18next.t("splashMessages:questionableBalancing"),
i18next.t("splashMessages:coolShaders"),
i18next.t("splashMessages:aiFree"),
i18next.t("splashMessages:suddenDifficultySpikes"),
i18next.t("splashMessages:basedOnAnUnfinishedFlashGame"),
i18next.t("splashMessages:moreAddictiveThanIntended"),
i18next.t("splashMessages:mostlyConsistentSeeds"),
i18next.t("splashMessages:achievementPointsDontDoAnything"),
i18next.t("splashMessages:youDoNotStartAtLevel"),
i18next.t("splashMessages:dontTalkAboutTheManaphyEggIncident"),
i18next.t("splashMessages:alsoTryPokengine"),
i18next.t("splashMessages:alsoTryEmeraldRogue"),
i18next.t("splashMessages:alsoTryRadicalRed"),
i18next.t("splashMessages:eeveeExpo"),
i18next.t("splashMessages:ynoproject"),
i18next.t("splashMessages:breedersInSpace"),
);
return splashMessages; if (now >= startDate && now <= endDate) {
console.log(`Adding ${messages.length} ${name} splash messages (weight: x${SEASONAL_WEIGHT_MULTIPLIER})`);
messages.forEach((message) => {
const weightedMessage = Array(SEASONAL_WEIGHT_MULTIPLIER).fill(message);
splashMessages.push(...weightedMessage);
});
}
}
}
return splashMessages.map((message) => `splashMessages:${message}`);
} }

View File

@ -1105,8 +1105,16 @@ function getGymLeaderPartyTemplate(scene: BattleScene) {
return getWavePartyTemplate(scene, trainerPartyTemplates.GYM_LEADER_1, trainerPartyTemplates.GYM_LEADER_2, trainerPartyTemplates.GYM_LEADER_3, trainerPartyTemplates.GYM_LEADER_4, trainerPartyTemplates.GYM_LEADER_5); return getWavePartyTemplate(scene, trainerPartyTemplates.GYM_LEADER_1, trainerPartyTemplates.GYM_LEADER_2, trainerPartyTemplates.GYM_LEADER_3, trainerPartyTemplates.GYM_LEADER_4, trainerPartyTemplates.GYM_LEADER_5);
} }
function getRandomPartyMemberFunc(speciesPool: Species[], trainerSlot: TrainerSlot = TrainerSlot.TRAINER, ignoreEvolution: boolean = false, postProcess?: (enemyPokemon: EnemyPokemon) => void): PartyMemberFunc { /**
return (scene: BattleScene, level: integer, strength: PartyMemberStrength) => { * Randomly selects one of the `Species` from `speciesPool`, determines its evolution, level, and strength.
* Then adds Pokemon to scene.
* @param speciesPool
* @param trainerSlot
* @param ignoreEvolution
* @param postProcess
*/
export function getRandomPartyMemberFunc(speciesPool: Species[], trainerSlot: TrainerSlot = TrainerSlot.TRAINER, ignoreEvolution: boolean = false, postProcess?: (enemyPokemon: EnemyPokemon) => void) {
return (scene: BattleScene, level: number, strength: PartyMemberStrength) => {
let species = Utils.randSeedItem(speciesPool); let species = Utils.randSeedItem(speciesPool);
if (!ignoreEvolution) { if (!ignoreEvolution) {
species = getPokemonSpecies(species).getTrainerSpeciesForLevel(level, true, strength, scene.currentBattle.waveIndex); species = getPokemonSpecies(species).getTrainerSpeciesForLevel(level, true, strength, scene.currentBattle.waveIndex);
@ -2302,6 +2310,8 @@ export const trainerConfigs: TrainerConfigs = {
.setMoneyMultiplier(2) .setMoneyMultiplier(2)
.setPartyTemplates(new TrainerPartyCompoundTemplate(new TrainerPartyTemplate(3, PartyMemberStrength.AVERAGE), new TrainerPartyTemplate(2, PartyMemberStrength.STRONG))), .setPartyTemplates(new TrainerPartyCompoundTemplate(new TrainerPartyTemplate(3, PartyMemberStrength.AVERAGE), new TrainerPartyTemplate(2, PartyMemberStrength.STRONG))),
[TrainerType.BUG_TYPE_SUPERFAN]: new TrainerConfig(++t).setMoneyMultiplier(2.25).setEncounterBgm(TrainerType.ACE_TRAINER) [TrainerType.BUG_TYPE_SUPERFAN]: new TrainerConfig(++t).setMoneyMultiplier(2.25).setEncounterBgm(TrainerType.ACE_TRAINER)
.setPartyTemplates(new TrainerPartyTemplate(2, PartyMemberStrength.AVERAGE)) .setPartyTemplates(new TrainerPartyTemplate(2, PartyMemberStrength.AVERAGE)),
[TrainerType.EXPERT_POKEMON_BREEDER]: new TrainerConfig(++t).setMoneyMultiplier(3).setEncounterBgm(TrainerType.ACE_TRAINER)
.setPartyTemplates(new TrainerPartyTemplate(3, PartyMemberStrength.STRONG))
}; };

View File

@ -28,5 +28,6 @@ export enum MysteryEncounterType {
BUG_TYPE_SUPERFAN, BUG_TYPE_SUPERFAN,
FUN_AND_GAMES, FUN_AND_GAMES,
UNCOMMON_BREED, UNCOMMON_BREED,
GLOBAL_TRADE_SYSTEM GLOBAL_TRADE_SYSTEM,
THE_EXPERT_POKEMON_BREEDER
} }

View File

@ -107,6 +107,7 @@ export enum TrainerType {
VICKY, VICKY,
VITO, VITO,
BUG_TYPE_SUPERFAN, BUG_TYPE_SUPERFAN,
EXPERT_POKEMON_BREEDER,
BROCK = 200, BROCK = 200,
MISTY, MISTY,

View File

@ -1,5 +0,0 @@
export enum VariantTier {
COMMON,
RARE,
EPIC
}

View File

@ -1,19 +1,19 @@
import BattleScene from "../battle-scene"; import BattleScene from "../battle-scene";
import { BiomePoolTier, PokemonPools, BiomeTierTrainerPools, biomePokemonPools, biomeTrainerPools } from "../data/biomes"; import { biomePokemonPools, BiomePoolTier, BiomeTierTrainerPools, biomeTrainerPools, PokemonPools } from "../data/biomes";
import { Constructor } from "#app/utils"; import { Constructor } from "#app/utils";
import * as Utils from "../utils"; import * as Utils from "../utils";
import PokemonSpecies, { getPokemonSpecies } from "../data/pokemon-species"; import PokemonSpecies, { getPokemonSpecies } from "../data/pokemon-species";
import { Weather, WeatherType, getTerrainClearMessage, getTerrainStartMessage, getWeatherClearMessage, getWeatherStartMessage } from "../data/weather"; import { getTerrainClearMessage, getTerrainStartMessage, getWeatherClearMessage, getWeatherStartMessage, Weather, WeatherType } from "../data/weather";
import { CommonAnim } from "../data/battle-anims"; import { CommonAnim } from "../data/battle-anims";
import { Type } from "../data/type"; import { Type } from "../data/type";
import Move from "../data/move"; import Move from "../data/move";
import { ArenaTag, ArenaTagSide, ArenaTrapTag, getArenaTag } from "../data/arena-tag"; import { ArenaTag, ArenaTagSide, ArenaTrapTag, getArenaTag } from "../data/arena-tag";
import { BattlerIndex } from "../battle"; import { BattlerIndex } from "../battle";
import { Terrain, TerrainType } from "../data/terrain"; import { Terrain, TerrainType } from "../data/terrain";
import { PostTerrainChangeAbAttr, PostWeatherChangeAbAttr, applyPostTerrainChangeAbAttrs, applyPostWeatherChangeAbAttrs } from "../data/ability"; import { applyPostTerrainChangeAbAttrs, applyPostWeatherChangeAbAttrs, PostTerrainChangeAbAttr, PostWeatherChangeAbAttr } from "../data/ability";
import Pokemon from "./pokemon"; import Pokemon from "./pokemon";
import Overrides from "#app/overrides"; import Overrides from "#app/overrides";
import { WeatherChangedEvent, TerrainChangedEvent, TagAddedEvent, TagRemovedEvent } from "../events/arena"; import { TagAddedEvent, TagRemovedEvent, TerrainChangedEvent, WeatherChangedEvent } from "../events/arena";
import { ArenaTagType } from "#enums/arena-tag-type"; import { ArenaTagType } from "#enums/arena-tag-type";
import { Biome } from "#enums/biome"; import { Biome } from "#enums/biome";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
@ -762,7 +762,7 @@ export class Arena {
case Biome.BEACH: case Biome.BEACH:
return 3.462; return 3.462;
case Biome.LAKE: case Biome.LAKE:
return 5.350; return 7.215;
case Biome.SEABED: case Biome.SEABED:
return 2.600; return 2.600;
case Biome.MOUNTAIN: case Biome.MOUNTAIN:
@ -788,7 +788,7 @@ export class Arena {
case Biome.FACTORY: case Biome.FACTORY:
return 4.985; return 4.985;
case Biome.RUINS: case Biome.RUINS:
return 2.270; return 0.000;
case Biome.WASTELAND: case Biome.WASTELAND:
return 6.336; return 6.336;
case Biome.ABYSS: case Biome.ABYSS:

View File

@ -3,7 +3,7 @@ import BattleScene, { AnySound } from "../battle-scene";
import { Variant, VariantSet, variantColorCache } from "#app/data/variant"; import { Variant, VariantSet, variantColorCache } from "#app/data/variant";
import { variantData } from "#app/data/variant"; import { variantData } from "#app/data/variant";
import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from "../ui/battle-info"; import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from "../ui/battle-info";
import Move, { HighCritAttr, DealsDoubleDamageToTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, VariableMoveTypeAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatStagesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatStageChangeAttr, RechargeAttr, ChargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, OneHitKOAccuracyAttr, RespectAttackTypeImmunityAttr, MoveTarget } from "../data/move"; import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, VariableMoveTypeAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatStagesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatStageChangeAttr, RechargeAttr, ChargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, OneHitKOAccuracyAttr, RespectAttackTypeImmunityAttr, MoveTarget } from "../data/move";
import { default as PokemonSpecies, PokemonSpeciesForm, SpeciesFormKey, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm, getStarterValueFriendshipCap, speciesStarters, starterPassiveAbilities } from "../data/pokemon-species"; import { default as PokemonSpecies, PokemonSpeciesForm, SpeciesFormKey, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm, getStarterValueFriendshipCap, speciesStarters, starterPassiveAbilities } from "../data/pokemon-species";
import { Constructor, isNullOrUndefined, randSeedInt } from "#app/utils"; import { Constructor, isNullOrUndefined, randSeedInt } from "#app/utils";
import * as Utils from "../utils"; import * as Utils from "../utils";
@ -1513,7 +1513,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
const immuneTags = this.findTags(tag => tag instanceof TypeImmuneTag && tag.immuneType === moveType); const immuneTags = this.findTags(tag => tag instanceof TypeImmuneTag && tag.immuneType === moveType);
for (const tag of immuneTags) { for (const tag of immuneTags) {
if (move && !move.getAttrs(DealsDoubleDamageToTagAttr).some(attr => attr.tagType === tag.tagType)) { if (move && !move.getAttrs(HitsTagAttr).some(attr => attr.tagType === tag.tagType)) {
typeMultiplier.value = 0; typeMultiplier.value = 0;
break; break;
} }
@ -2322,11 +2322,61 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
return accuracyMultiplier.value / evasionMultiplier.value; return accuracyMultiplier.value / evasionMultiplier.value;
} }
/**
* Calculates the base damage of the given move against this Pokemon when attacked by the given source.
* Used during damage calculation and for Shell Side Arm's forecasting effect.
* @param source the attacking {@linkcode Pokemon}.
* @param move the {@linkcode Move} used in the attack.
* @param moveCategory the move's {@linkcode MoveCategory} after variable-category effects are applied.
* @param ignoreAbility if `true`, ignores this Pokemon's defensive ability effects (defaults to `false`).
* @param ignoreSourceAbility if `true`, ignore's the attacking Pokemon's ability effects (defaults to `false`).
* @param isCritical if `true`, calculates effective stats as if the hit were critical (defaults to `false`).
* @param simulated if `true`, suppresses changes to game state during calculation (defaults to `true`).
* @returns The move's base damage against this Pokemon when used by the source Pokemon.
*/
getBaseDamage(source: Pokemon, move: Move, moveCategory: MoveCategory, ignoreAbility: boolean = false, ignoreSourceAbility: boolean = false, isCritical: boolean = false, simulated: boolean = true): number {
const isPhysical = moveCategory === MoveCategory.PHYSICAL;
/** A base damage multiplier based on the source's level */
const levelMultiplier = (2 * source.level / 5 + 2);
/** The power of the move after power boosts from abilities, etc. have applied */
const power = move.calculateBattlePower(source, this, simulated);
/**
* The attacker's offensive stat for the given move's category.
* Critical hits cause negative stat stages to be ignored.
*/
const sourceAtk = new Utils.NumberHolder(source.getEffectiveStat(isPhysical ? Stat.ATK : Stat.SPATK, this, undefined, ignoreSourceAbility, ignoreAbility, isCritical, simulated));
applyMoveAttrs(VariableAtkAttr, source, this, move, sourceAtk);
/**
* This Pokemon's defensive stat for the given move's category.
* Critical hits cause positive stat stages to be ignored.
*/
const targetDef = new Utils.NumberHolder(this.getEffectiveStat(isPhysical ? Stat.DEF : Stat.SPDEF, source, move, ignoreAbility, ignoreSourceAbility, isCritical, simulated));
applyMoveAttrs(VariableDefAttr, source, this, move, targetDef);
/**
* The attack's base damage, as determined by the source's level, move power
* and Attack stat as well as this Pokemon's Defense stat
*/
const baseDamage = ((levelMultiplier * power * sourceAtk.value / targetDef.value) / 50) + 2;
/** Debug message for non-simulated calls (i.e. when damage is actually dealt) */
if (!simulated) {
console.log("base damage", baseDamage, move.name, power, sourceAtk.value, targetDef.value);
}
return baseDamage;
}
/** /**
* Calculates the damage of an attack made by another Pokemon against this Pokemon * Calculates the damage of an attack made by another Pokemon against this Pokemon
* @param source {@linkcode Pokemon} the attacking Pokemon * @param source {@linkcode Pokemon} the attacking Pokemon
* @param move {@linkcode Pokemon} the move used in the attack * @param move {@linkcode Pokemon} the move used in the attack
* @param ignoreAbility If `true`, ignores this Pokemon's defensive ability effects * @param ignoreAbility If `true`, ignores this Pokemon's defensive ability effects
* @param ignoreSourceAbility If `true`, ignores the attacking Pokemon's ability effects
* @param isCritical If `true`, calculates damage for a critical hit. * @param isCritical If `true`, calculates damage for a critical hit.
* @param simulated If `true`, suppresses changes to game state during the calculation. * @param simulated If `true`, suppresses changes to game state during the calculation.
* @returns a {@linkcode DamageCalculationResult} object with three fields: * @returns a {@linkcode DamageCalculationResult} object with three fields:
@ -2395,35 +2445,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}; };
} }
// ----- BEGIN BASE DAMAGE MULTIPLIERS -----
/** A base damage multiplier based on the source's level */
const levelMultiplier = (2 * source.level / 5 + 2);
/** The power of the move after power boosts from abilities, etc. have applied */
const power = move.calculateBattlePower(source, this, simulated);
/**
* The attacker's offensive stat for the given move's category.
* Critical hits ignore negative stat stages.
*/
const sourceAtk = new Utils.NumberHolder(source.getEffectiveStat(isPhysical ? Stat.ATK : Stat.SPATK, this, undefined, ignoreSourceAbility, ignoreAbility, isCritical, simulated));
applyMoveAttrs(VariableAtkAttr, source, this, move, sourceAtk);
/**
* This Pokemon's defensive stat for the given move's category.
* Critical hits ignore positive stat stages.
*/
const targetDef = new Utils.NumberHolder(this.getEffectiveStat(isPhysical ? Stat.DEF : Stat.SPDEF, source, move, ignoreAbility, ignoreSourceAbility, isCritical, simulated));
applyMoveAttrs(VariableDefAttr, source, this, move, targetDef);
/** /**
* The attack's base damage, as determined by the source's level, move power * The attack's base damage, as determined by the source's level, move power
* and Attack stat as well as this Pokemon's Defense stat * and Attack stat as well as this Pokemon's Defense stat
*/ */
const baseDamage = ((levelMultiplier * power * sourceAtk.value / targetDef.value) / 50) + 2; const baseDamage = this.getBaseDamage(source, move, moveCategory, ignoreAbility, ignoreSourceAbility, isCritical, simulated);
// ------ END BASE DAMAGE MULTIPLIERS ------
/** 25% damage debuff on moves hitting more than one non-fainted target (regardless of immunities) */ /** 25% damage debuff on moves hitting more than one non-fainted target (regardless of immunities) */
const { targets, multiple } = getMoveTargets(source, move.id); const { targets, multiple } = getMoveTargets(source, move.id);
@ -2489,13 +2515,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.scene.arena.applyTagsForSide(WeakenMoveScreenTag, defendingSide, move.category, this.scene.currentBattle.double, screenMultiplier); this.scene.arena.applyTagsForSide(WeakenMoveScreenTag, defendingSide, move.category, this.scene.currentBattle.double, screenMultiplier);
/** /**
* For each {@linkcode DealsDoubleDamageToTagAttr} the move has, doubles the damage of the move if: * For each {@linkcode HitsTagAttr} the move has, doubles the damage of the move if:
* The target has a {@linkcode BattlerTagType} that this move interacts with * The target has a {@linkcode BattlerTagType} that this move interacts with
* AND * AND
* The move doubles damage when used against that tag * The move doubles damage when used against that tag
*/ */
const hitsTagMultiplier = new Utils.NumberHolder(1); const hitsTagMultiplier = new Utils.NumberHolder(1);
move.getAttrs(DealsDoubleDamageToTagAttr).filter(hta => hta.doubleDamage).forEach(hta => { move.getAttrs(HitsTagAttr).filter(hta => hta.doubleDamage).forEach(hta => {
if (this.getTag(hta.tagType)) { if (this.getTag(hta.tagType)) {
hitsTagMultiplier.value *= 2; hitsTagMultiplier.value *= 2;
} }
@ -2549,7 +2575,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
// debug message for when damage is applied (i.e. not simulated) // debug message for when damage is applied (i.e. not simulated)
if (!simulated) { if (!simulated) {
console.log("damage", damage.value, move.name, power, sourceAtk, targetDef); console.log("damage", damage.value, move.name);
} }
let hitResult: HitResult; let hitResult: HitResult;

View File

@ -268,7 +268,6 @@ export class GameMode implements GameModeConfig {
isFixedBattle(waveIndex: integer): boolean { isFixedBattle(waveIndex: integer): boolean {
const dummyConfig = new FixedBattleConfig(); const dummyConfig = new FixedBattleConfig();
return this.battleConfig.hasOwnProperty(waveIndex) || applyChallenges(this, ChallengeType.FIXED_BATTLES, waveIndex, dummyConfig); return this.battleConfig.hasOwnProperty(waveIndex) || applyChallenges(this, ChallengeType.FIXED_BATTLES, waveIndex, dummyConfig);
} }
/** /**

View File

@ -3121,11 +3121,11 @@
}, },
"behemothBlade": { "behemothBlade": {
"name": "Gigantenhieb", "name": "Gigantenhieb",
"effect": "Der Anwender wird zu einem riesigen Schwert und greift das Ziel an. Dynamaximierte Ziele erleiden doppelten Schaden." "effect": "Der Anwender wird zu einem riesigen Schwert und greift das Ziel an."
}, },
"behemothBash": { "behemothBash": {
"name": "Gigantenstoß", "name": "Gigantenstoß",
"effect": "Der Anwender wird zu einem riesigen Schild und greift das Ziel an. Dynamaximierte Ziele erleiden doppelten Schaden." "effect": "Der Anwender wird zu einem riesigen Schild und greift das Ziel an."
}, },
"auraWheel": { "auraWheel": {
"name": "Aura-Rad", "name": "Aura-Rad",

View File

@ -1,5 +1,5 @@
{ {
"battlesWon": "Kämpfe gewonnen!", "battlesWon": "{{count, number}} Kämpfe gewonnen!",
"joinTheDiscord": "Tritt dem Discord bei!", "joinTheDiscord": "Tritt dem Discord bei!",
"infiniteLevels": "Unendliche Level!", "infiniteLevels": "Unendliche Level!",
"everythingStacks": "Alles stapelt sich!", "everythingStacks": "Alles stapelt sich!",

View File

@ -283,5 +283,9 @@
"INVERSE_BATTLE": { "INVERSE_BATTLE": {
"name": "Mirror rorriM", "name": "Mirror rorriM",
"description": "Complete the Inverse Battle challenge.\n.egnellahc elttaB esrevnI eht etelpmoC" "description": "Complete the Inverse Battle challenge.\n.egnellahc elttaB esrevnI eht etelpmoC"
},
"BREEDERS_IN_SPACE": {
"name": "Breeders in Space!",
"description": "Beat the Expert Pokémon Breeder in the Space Biome."
} }
} }

View File

@ -112,13 +112,13 @@
"island": "PMD EoS Craggy Coast", "island": "PMD EoS Craggy Coast",
"jungle": "Lmz - Jungle", "jungle": "Lmz - Jungle",
"laboratory": "Firel - Laboratory", "laboratory": "Firel - Laboratory",
"lake": "PMD EoS Crystal Cave", "lake": "Lmz - Lake",
"meadow": "PMD EoS Sky Peak Forest", "meadow": "PMD EoS Sky Peak Forest",
"metropolis": "Firel - Metropolis", "metropolis": "Firel - Metropolis",
"mountain": "PMD EoS Mt. Horn", "mountain": "PMD EoS Mt. Horn",
"plains": "Firel - Route 888", "plains": "Firel - Route 888",
"power_plant": "Firel - The Klink", "power_plant": "Firel - The Klink",
"ruins": "PMD EoS Deep Sealed Ruin", "ruins": "Lmz - Ancient Ruins",
"sea": "Andr06 - Marine Mystique", "sea": "Andr06 - Marine Mystique",
"seabed": "Firel - Seabed", "seabed": "Firel - Seabed",
"slum": "Andr06 - Sneaky Snom", "slum": "Andr06 - Sneaky Snom",

View File

@ -84,6 +84,7 @@ import bugTypeSuperfan from "#app/locales/en/mystery-encounters/bug-type-superfa
import funAndGames from "#app/locales/en/mystery-encounters/fun-and-games-dialogue.json"; import funAndGames from "#app/locales/en/mystery-encounters/fun-and-games-dialogue.json";
import uncommonBreed from "#app/locales/en/mystery-encounters/uncommon-breed-dialogue.json"; import uncommonBreed from "#app/locales/en/mystery-encounters/uncommon-breed-dialogue.json";
import globalTradeSystem from "#app/locales/en/mystery-encounters/global-trade-system-dialogue.json"; import globalTradeSystem from "#app/locales/en/mystery-encounters/global-trade-system-dialogue.json";
import expertPokemonBreeder from "#app/locales/en/mystery-encounters/the-expert-pokemon-breeder-dialogue.json";
/** /**
* Dialogue/Text token injection patterns that can be used: * Dialogue/Text token injection patterns that can be used:
@ -183,7 +184,8 @@ export const enConfig = {
bugTypeSuperfan, bugTypeSuperfan,
funAndGames, funAndGames,
uncommonBreed, uncommonBreed,
globalTradeSystem globalTradeSystem,
expertPokemonBreeder
}, },
mysteryEncounterMessages mysteryEncounterMessages
}; };

View File

@ -21,7 +21,7 @@
"label": "Sales Assistant", "label": "Sales Assistant",
"tooltip": "(-) Your {{option3PrimaryName}} uses {{option3PrimaryMove}}\n(+) Earn @[MONEY]{Money}", "tooltip": "(-) Your {{option3PrimaryName}} uses {{option3PrimaryMove}}\n(+) Earn @[MONEY]{Money}",
"disabled_tooltip": "Your Pokémon need to know certain moves for this job", "disabled_tooltip": "Your Pokémon need to know certain moves for this job",
"selected": "Your {{option3PrimaryName}} spends the day using {{option3PrimaryMove}} to attract customers to the business!" "selected": "Your {{option3PrimaryName}} spends the day using {{option3PrimaryMove}} to draw customers to the business!"
} }
}, },
"job_complete_good": "Thanks for the assistance!\nYour {{selectedPokemon}} was incredibly helpful!$Here's your check for the day.", "job_complete_good": "Thanks for the assistance!\nYour {{selectedPokemon}} was incredibly helpful!$Here's your check for the day.",

View File

@ -0,0 +1,30 @@
{
"intro": "It's a trainer carrying tons of Pokémon Eggs!",
"intro_dialogue": "Hey there, trainer!$It looks like some of your\npartner Pokémon are feeling a little down.$Why not have a battle with me to cheer them up?",
"title": "The Expert Pokémon Breeder",
"description": "You've been challenged to a battle where @[TOOLTIP_TITLE]{you can only use a single Pokémon}. It might be tough, but it would surely deepen the bond you have with the Pokémon you choose!\nThe breeder will also give you some @[TOOLTIP_TITLE]{Pokémon Eggs} if you win!",
"query": "Who will you battle with?",
"cleffa_1_nickname": "Ace",
"cleffa_2_nickname": "Clefablest",
"cleffa_3_nickname": "{{speciesName}} the Great",
"option": {
"1": {
"label": "{{pokemon1Name}}",
"tooltip_base": "(-) Tough Battle\n(+) Gain Friendship with {{pokemon1Name}}"
},
"2": {
"label": "{{pokemon2Name}}",
"tooltip_base": "(-) Tough Battle\n(+) Gain Friendship with {{pokemon2Name}}"
},
"3": {
"label": "{{pokemon3Name}}",
"tooltip_base": "(-) Tough Battle\n(+) Gain Friendship with {{pokemon3Name}}"
},
"selected": "Let's do this!"
},
"outro": "Look how happy your {{chosenPokemon}} is now!$Here, you can have these as well.",
"gained_eggs": "@s{item_fanfare}You received {{numEggs}}!",
"eggs_tooltip": "\n(+) Earn {{eggs}}",
"numEggs_one": "{{count}} {{rarity}} Egg",
"numEggs_other": "{{count}} {{rarity}} Eggs"
}

View File

@ -1,5 +1,5 @@
{ {
"battlesWon": "Battles Won!", "battlesWon": "{{count, number}} Battles Won!",
"joinTheDiscord": "Join the Discord!", "joinTheDiscord": "Join the Discord!",
"infiniteLevels": "Infinite Levels!", "infiniteLevels": "Infinite Levels!",
"everythingStacks": "Everything Stacks!", "everythingStacks": "Everything Stacks!",
@ -32,5 +32,17 @@
"alsoTryRadicalRed": "Also Try Radical Red!", "alsoTryRadicalRed": "Also Try Radical Red!",
"eeveeExpo": "Eevee Expo!", "eeveeExpo": "Eevee Expo!",
"ynoproject": "YNOproject!", "ynoproject": "YNOproject!",
"breedersInSpace": "Breeders in space!" "breedersInSpace": "Breeders in space!",
"halloween": {
"pumpkaboosAbout": "Pumpkaboos about!",
"mayContainSpiders": "May contain spiders!",
"spookyScaryDuskulls": "Spooky, Scary Duskulls!"
},
"xmas": {
"happyHolidays": "Happy Holidays!",
"delibirdSeason": "Delibird Season!"
},
"newYears": {
"happyNewYear": "Happy New Year!"
}
} }

View File

@ -172,5 +172,6 @@
"vivi": "Vivi", "vivi": "Vivi",
"vicky": "Vicky", "vicky": "Vicky",
"vito": "Vito", "vito": "Vito",
"bug_type_superfan": "Bug-Type Superfan" "bug_type_superfan": "Bug-Type Superfan",
"expert_pokemon_breeder": "Expert Pokémon Breeder"
} }

View File

@ -1,5 +1,5 @@
{ {
"battlesWon": Batallas ganadas!", "battlesWon": {{count, number}} Batallas ganadas!",
"joinTheDiscord": "¡Únete al Discord!", "joinTheDiscord": "¡Únete al Discord!",
"infiniteLevels": "¡Niveles infinitos!", "infiniteLevels": "¡Niveles infinitos!",
"everythingStacks": "¡Todo se acumula!", "everythingStacks": "¡Todo se acumula!",

View File

@ -3121,11 +3121,11 @@
}, },
"behemothBlade": { "behemothBlade": {
"name": "Gladius Maximus", "name": "Gladius Maximus",
"effect": "Le lanceur se transforme en une immense épée et pourfend sa cible. Cette capacité inflige le double de dégâts aux Pokémon Dynamax." "effect": "Le lanceur se transforme en une immense épée et pourfend sa cible."
}, },
"behemothBash": { "behemothBash": {
"name": "Aegis Maxima", "name": "Aegis Maxima",
"effect": "Le lanceur se transforme en un immense bouclier et charge sa cible. Cette capacité inflige le double de dégâts aux Pokémon Dynamax." "effect": "Le lanceur se transforme en un immense bouclier et charge sa cible."
}, },
"auraWheel": { "auraWheel": {
"name": "Roue Libre", "name": "Roue Libre",

View File

@ -13,7 +13,7 @@
"ALL": "Tout", "ALL": "Tout",
"PASS_BATON": "Relais", "PASS_BATON": "Relais",
"UNPAUSE_EVOLUTION": "Réactiver Évolution", "UNPAUSE_EVOLUTION": "Réactiver Évolution",
"PAUSE_EVOLUTION": "Interrompre Évolution", "PAUSE_EVOLUTION": "Empêcher Évolution",
"REVIVE": "Ranimer", "REVIVE": "Ranimer",
"RENAME": "Renommer", "RENAME": "Renommer",
"choosePokemon": "Sélectionnez un Pokémon.", "choosePokemon": "Sélectionnez un Pokémon.",

View File

@ -1,5 +1,5 @@
{ {
"battlesWon": "combats gagnés !", "battlesWon": "{{count, number}} combats gagnés !",
"joinTheDiscord": "Rejoins le Discord !", "joinTheDiscord": "Rejoins le Discord !",
"infiniteLevels": "Niveaux infinis !", "infiniteLevels": "Niveaux infinis !",
"everythingStacks": "Tout se cumule !", "everythingStacks": "Tout se cumule !",

View File

@ -1,5 +1,5 @@
{ {
"battlesWon": "Battaglie Vinte!", "battlesWon": "{{count, number}} Battaglie Vinte!",
"joinTheDiscord": "Entra nel Discord!", "joinTheDiscord": "Entra nel Discord!",
"infiniteLevels": "Livelli Infiniti!", "infiniteLevels": "Livelli Infiniti!",
"everythingStacks": "Tutto si impila!", "everythingStacks": "Tutto si impila!",

View File

@ -1,5 +1,5 @@
{ {
"battlesWon": "Battles Won!", "battlesWon": "勝ったバトル:{{count, number}}回!",
"joinTheDiscord": "Join the Discord!", "joinTheDiscord": "Join the Discord!",
"infiniteLevels": "Infinite Levels!", "infiniteLevels": "Infinite Levels!",
"everythingStacks": "Everything Stacks!", "everythingStacks": "Everything Stacks!",

View File

@ -1,5 +1,5 @@
{ {
"battlesWon": "전투에서 승리하세요!", "battlesWon": "{{count, number}} 전투에서 승리하세요!",
"joinTheDiscord": "디스코드에 가입하세요!", "joinTheDiscord": "디스코드에 가입하세요!",
"infiniteLevels": "무한한 레벨!", "infiniteLevels": "무한한 레벨!",
"everythingStacks": "모든 것이 누적됩니다!", "everythingStacks": "모든 것이 누적됩니다!",

View File

@ -1,5 +1,5 @@
{ {
"battlesWon": "Batalhas Ganhas!", "battlesWon": "{{count, number}} Batalhas Ganhas!",
"joinTheDiscord": "Junte-se ao Discord!", "joinTheDiscord": "Junte-se ao Discord!",
"infiniteLevels": "Níveis Infinitos!", "infiniteLevels": "Níveis Infinitos!",
"everythingStacks": "Tudo Acumula!", "everythingStacks": "Tudo Acumula!",

View File

@ -1,5 +1,5 @@
{ {
"battlesWon": "场胜利!", "battlesWon": "{{count, number}} 场胜利!",
"joinTheDiscord": "加入Discord", "joinTheDiscord": "加入Discord",
"infiniteLevels": "等级无限!", "infiniteLevels": "等级无限!",
"everythingStacks": "道具全部叠加!", "everythingStacks": "道具全部叠加!",

View File

@ -1,5 +1,5 @@
{ {
"battlesWon": "勝利場數!", "battlesWon": "{{count, number}} 勝利場數!",
"joinTheDiscord": "加入Discord", "joinTheDiscord": "加入Discord",
"infiniteLevels": "無限等級!", "infiniteLevels": "無限等級!",
"everythingStacks": "所有效果都能疊加!", "everythingStacks": "所有效果都能疊加!",

View File

@ -1761,7 +1761,7 @@ const modifierPool: ModifierPool = {
new WeightedModifierType(modifierTypes.ABILITY_CHARM, skipInClassicAfterWave(189, 6)), new WeightedModifierType(modifierTypes.ABILITY_CHARM, skipInClassicAfterWave(189, 6)),
new WeightedModifierType(modifierTypes.FOCUS_BAND, 5), new WeightedModifierType(modifierTypes.FOCUS_BAND, 5),
new WeightedModifierType(modifierTypes.KINGS_ROCK, 3), new WeightedModifierType(modifierTypes.KINGS_ROCK, 3),
new WeightedModifierType(modifierTypes.LOCK_CAPSULE, skipInLastClassicWaveOrDefault(3)), new WeightedModifierType(modifierTypes.LOCK_CAPSULE, (party: Pokemon[]) => party[0].scene.gameMode.isClassic ? 0 : 3),
new WeightedModifierType(modifierTypes.SUPER_EXP_CHARM, skipInLastClassicWaveOrDefault(8)), new WeightedModifierType(modifierTypes.SUPER_EXP_CHARM, skipInLastClassicWaveOrDefault(8)),
new WeightedModifierType(modifierTypes.RARE_FORM_CHANGE_ITEM, (party: Pokemon[]) => Math.min(Math.ceil(party[0].scene.currentBattle.waveIndex / 50), 4) * 6, 24), new WeightedModifierType(modifierTypes.RARE_FORM_CHANGE_ITEM, (party: Pokemon[]) => Math.min(Math.ceil(party[0].scene.currentBattle.waveIndex / 50), 4) * 6, 24),
new WeightedModifierType(modifierTypes.MEGA_BRACELET, (party: Pokemon[]) => Math.min(Math.ceil(party[0].scene.currentBattle.waveIndex / 50), 4) * 9, 36), new WeightedModifierType(modifierTypes.MEGA_BRACELET, (party: Pokemon[]) => Math.min(Math.ceil(party[0].scene.currentBattle.waveIndex / 50), 4) * 9, 36),

View File

@ -6,7 +6,7 @@ import { PokeballType } from "#enums/pokeball";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import { StatusEffect } from "#enums/status-effect"; import { StatusEffect } from "#enums/status-effect";
import { TimeOfDay } from "#enums/time-of-day"; import { TimeOfDay } from "#enums/time-of-day";
import { VariantTier } from "#enums/variant-tiers"; import { VariantTier } from "#enums/variant-tier";
import { WeatherType } from "#enums/weather-type"; import { WeatherType } from "#enums/weather-type";
import { type PokeballCounts } from "./battle-scene"; import { type PokeballCounts } from "./battle-scene";
import { Gender } from "./data/gender"; import { Gender } from "./data/gender";

View File

@ -243,7 +243,7 @@ export class GameOverPhase extends BattlePhase {
gameVersion: this.scene.game.config.gameVersion, gameVersion: this.scene.game.config.gameVersion,
timestamp: new Date().getTime(), timestamp: new Date().getTime(),
challenges: this.scene.gameMode.challenges.map(c => new ChallengeData(c)), challenges: this.scene.gameMode.challenges.map(c => new ChallengeData(c)),
mysteryEncounterType: this.scene.currentBattle.mysteryEncounter?.encounterType, mysteryEncounterType: this.scene.currentBattle.mysteryEncounter?.encounterType ?? -1,
mysteryEncounterSaveData: this.scene.mysteryEncounterSaveData mysteryEncounterSaveData: this.scene.mysteryEncounterSaveData
} as SessionSaveData; } as SessionSaveData;
} }

View File

@ -4,7 +4,7 @@ import { applyPreAttackAbAttrs, AddSecondStrikeAbAttr, IgnoreMoveEffectsAbAttr,
import { ArenaTagSide, ConditionalProtectTag } from "#app/data/arena-tag"; import { ArenaTagSide, ConditionalProtectTag } from "#app/data/arena-tag";
import { MoveAnim } from "#app/data/battle-anims"; import { MoveAnim } from "#app/data/battle-anims";
import { BattlerTagLapseType, DamageProtectedTag, ProtectedTag, SemiInvulnerableTag, SubstituteTag } from "#app/data/battler-tags"; import { BattlerTagLapseType, DamageProtectedTag, ProtectedTag, SemiInvulnerableTag, SubstituteTag } from "#app/data/battler-tags";
import { MoveTarget, applyMoveAttrs, OverrideMoveEffectAttr, MultiHitAttr, AttackMove, FixedDamageAttr, VariableTargetAttr, MissEffectAttr, MoveFlags, applyFilteredMoveAttrs, MoveAttr, MoveEffectAttr, MoveEffectTrigger, ChargeAttr, MoveCategory, NoEffectAttr, DealsDoubleDamageToTagAttr } from "#app/data/move"; import { MoveTarget, applyMoveAttrs, OverrideMoveEffectAttr, MultiHitAttr, AttackMove, FixedDamageAttr, VariableTargetAttr, MissEffectAttr, MoveFlags, applyFilteredMoveAttrs, MoveAttr, MoveEffectAttr, MoveEffectTrigger, ChargeAttr, MoveCategory, NoEffectAttr, HitsTagAttr } from "#app/data/move";
import { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms"; import { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms";
import { BattlerTagType } from "#app/enums/battler-tag-type"; import { BattlerTagType } from "#app/enums/battler-tag-type";
import { Moves } from "#app/enums/moves"; import { Moves } from "#app/enums/moves";
@ -394,7 +394,7 @@ export class MoveEffectPhase extends PokemonPhase {
} }
const semiInvulnerableTag = target.getTag(SemiInvulnerableTag); const semiInvulnerableTag = target.getTag(SemiInvulnerableTag);
if (semiInvulnerableTag && !this.move.getMove().getAttrs(DealsDoubleDamageToTagAttr).some(hta => hta.tagType === semiInvulnerableTag.tagType)) { if (semiInvulnerableTag && !this.move.getMove().getAttrs(HitsTagAttr).some(hta => hta.tagType === semiInvulnerableTag.tagType)) {
return false; return false;
} }

View File

@ -1,6 +1,6 @@
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import { BattlerIndex, BattleType } from "#app/battle"; import { BattlerIndex, BattleType, ClassicFixedBossWaves } from "#app/battle";
import { modifierTypes } from "#app/modifier/modifier-type"; import { CustomModifierSettings, modifierTypes } from "#app/modifier/modifier-type";
import { BattleEndPhase } from "./battle-end-phase"; import { BattleEndPhase } from "./battle-end-phase";
import { NewBattlePhase } from "./new-battle-phase"; import { NewBattlePhase } from "./new-battle-phase";
import { PokemonPhase } from "./pokemon-phase"; import { PokemonPhase } from "./pokemon-phase";
@ -42,8 +42,12 @@ export class VictoryPhase extends PokemonPhase {
} }
if (this.scene.gameMode.isEndless || !this.scene.gameMode.isWaveFinal(this.scene.currentBattle.waveIndex)) { if (this.scene.gameMode.isEndless || !this.scene.gameMode.isWaveFinal(this.scene.currentBattle.waveIndex)) {
this.scene.pushPhase(new EggLapsePhase(this.scene)); this.scene.pushPhase(new EggLapsePhase(this.scene));
if (this.scene.gameMode.isClassic && this.scene.currentBattle.waveIndex === ClassicFixedBossWaves.EVIL_BOSS_2) {
// Should get Lock Capsule on 165 before shop phase so it can be used in the rewards shop
this.scene.pushPhase(new ModifierRewardPhase(this.scene, modifierTypes.LOCK_CAPSULE));
}
if (this.scene.currentBattle.waveIndex % 10) { if (this.scene.currentBattle.waveIndex % 10) {
this.scene.pushPhase(new SelectModifierPhase(this.scene)); this.scene.pushPhase(new SelectModifierPhase(this.scene, undefined, undefined, this.getFixedBattleCustomModifiers()));
} else if (this.scene.gameMode.isDaily) { } else if (this.scene.gameMode.isDaily) {
this.scene.pushPhase(new ModifierRewardPhase(this.scene, modifierTypes.EXP_CHARM)); this.scene.pushPhase(new ModifierRewardPhase(this.scene, modifierTypes.EXP_CHARM));
if (this.scene.currentBattle.waveIndex > 10 && !this.scene.gameMode.isWaveFinal(this.scene.currentBattle.waveIndex)) { if (this.scene.currentBattle.waveIndex > 10 && !this.scene.gameMode.isWaveFinal(this.scene.currentBattle.waveIndex)) {
@ -76,4 +80,18 @@ export class VictoryPhase extends PokemonPhase {
this.end(); this.end();
} }
/**
* If this wave is a fixed battle with special custom modifier rewards,
* will pass those settings to the upcoming {@linkcode SelectModifierPhase}`.
*/
getFixedBattleCustomModifiers(): CustomModifierSettings | undefined {
const gameMode = this.scene.gameMode;
const waveIndex = this.scene.currentBattle.waveIndex;
if (gameMode.isFixedBattle(waveIndex)) {
return gameMode.getFixedBattle(waveIndex).customModifierRewardSettings;
}
return undefined;
}
} }

View File

@ -279,6 +279,8 @@ export function getAchievementDescription(localizationKey: string): string {
return i18next.t("achv:FRESH_START.description", { context: genderStr }); return i18next.t("achv:FRESH_START.description", { context: genderStr });
case "INVERSE_BATTLE": case "INVERSE_BATTLE":
return i18next.t("achv:INVERSE_BATTLE.description", { context: genderStr }); return i18next.t("achv:INVERSE_BATTLE.description", { context: genderStr });
case "BREEDERS_IN_SPACE":
return i18next.t("achv:BREEDERS_IN_SPACE.description", { context: genderStr });
default: default:
return ""; return "";
} }
@ -356,6 +358,7 @@ export const achvs = {
MONO_FAIRY: new ChallengeAchv("MONO_FAIRY", "", "MONO_FAIRY.description", "fairy_feather", 100, (c, scene) => c instanceof SingleTypeChallenge && c.value === 18 && !scene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)), MONO_FAIRY: new ChallengeAchv("MONO_FAIRY", "", "MONO_FAIRY.description", "fairy_feather", 100, (c, scene) => c instanceof SingleTypeChallenge && c.value === 18 && !scene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)),
FRESH_START: new ChallengeAchv("FRESH_START", "", "FRESH_START.description", "reviver_seed", 100, (c, scene) => c instanceof FreshStartChallenge && c.value > 0 && !scene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)), FRESH_START: new ChallengeAchv("FRESH_START", "", "FRESH_START.description", "reviver_seed", 100, (c, scene) => c instanceof FreshStartChallenge && c.value > 0 && !scene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)),
INVERSE_BATTLE: new ChallengeAchv("INVERSE_BATTLE", "", "INVERSE_BATTLE.description", "inverse", 100, c => c instanceof InverseBattleChallenge && c.value > 0), INVERSE_BATTLE: new ChallengeAchv("INVERSE_BATTLE", "", "INVERSE_BATTLE.description", "inverse", 100, c => c instanceof InverseBattleChallenge && c.value > 0),
BREEDERS_IN_SPACE: new Achv("BREEDERS_IN_SPACE", "", "BREEDERS_IN_SPACE.description", "moon_stone", 100).setSecret(),
}; };
export function initAchievements() { export function initAchievements() {

View File

@ -1,6 +1,6 @@
import { EggTier } from "#enums/egg-type"; import { EggTier } from "#enums/egg-type";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import { VariantTier } from "#enums/variant-tiers"; import { VariantTier } from "#enums/variant-tier";
import { EGG_SEED, Egg } from "../data/egg"; import { EGG_SEED, Egg } from "../data/egg";
import { EggSourceType } from "#app/enums/egg-source-types"; import { EggSourceType } from "#app/enums/egg-source-types";

View File

@ -0,0 +1,66 @@
import { getSplashMessages } from "#app/data/splash-messages";
import { describe, expect, it, vi, afterEach, beforeEach } from "vitest";
import * as Constants from "#app/constants";
describe("Data - Splash Messages", () => {
it("should contain at least 15 splash messages", () => {
expect(getSplashMessages().length).toBeGreaterThanOrEqual(15);
});
// make sure to adjust this test if the weight it changed!
it("should add contain 10 `battlesWon` splash messages", () => {
const battlesWonMessages = getSplashMessages().filter((message) => message === "splashMessages:battlesWon");
expect(battlesWonMessages).toHaveLength(10);
});
describe("Seasonal", () => {
beforeEach(() => {
vi.spyOn(Constants, "USE_SEASONAL_SPLASH_MESSAGES", "get").mockReturnValue(true);
});
afterEach(() => {
vi.useRealTimers(); // reset system time
});
it("should contain halloween messages from Sep 15 to Oct 31", () => {
testSeason(new Date("2024-09-15"), new Date("2024-10-31"), "halloween");
});
it("should contain xmas messages from Dec 1 to Dec 26", () => {
testSeason(new Date("2024-12-01"), new Date("2024-12-26"), "xmas");
});
it("should contain new years messages frm Jan 1 to Jan 31", () => {
testSeason(new Date("2024-01-01"), new Date("2024-01-31"), "newYears");
});
});
});
/**
* Helpoer method to test seasonal messages
* @param startDate The seasons start date
* @param endDate The seasons end date
* @param prefix the splash message prefix (e.g. `newYears` or `xmas`)
*/
function testSeason(startDate: Date, endDate: Date, prefix: string) {
const filterFn = (message: string) => message.startsWith(`splashMessages:${prefix}.`);
const beforeDate = new Date(startDate);
beforeDate.setDate(startDate.getDate() - 1);
const afterDate = new Date(endDate);
afterDate.setDate(endDate.getDate() + 1);
const dates: Date[] = [beforeDate, startDate, endDate, afterDate];
const [before, start, end, after] = dates.map((date) => {
vi.setSystemTime(date);
console.log("System time set to", date);
const count = getSplashMessages().filter(filterFn).length;
return count;
});
expect(before).toBe(0);
expect(start).toBeGreaterThanOrEqual(10); // make sure to adjust if weight is changed!
expect(end).toBeGreaterThanOrEqual(10); // make sure to adjust if weight is changed!
expect(after).toBe(0);
}

View File

@ -1,7 +1,7 @@
import { Egg, getLegendaryGachaSpeciesForTimestamp } from "#app/data/egg"; import { Egg, getLegendaryGachaSpeciesForTimestamp } from "#app/data/egg";
import { EggSourceType } from "#app/enums/egg-source-types"; import { EggSourceType } from "#app/enums/egg-source-types";
import { EggTier } from "#app/enums/egg-type"; import { EggTier } from "#app/enums/egg-type";
import { VariantTier } from "#app/enums/variant-tiers"; import { VariantTier } from "#app/enums/variant-tier";
import EggData from "#app/system/egg-data"; import EggData from "#app/system/egg-data";
import * as Utils from "#app/utils"; import * as Utils from "#app/utils";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
@ -136,9 +136,9 @@ describe("Egg Generation Tests", () => {
expect(result).toBe(expectedResult); expect(result).toBe(expectedResult);
}); });
it("should return a shiny common variant", () => { it("should return a shiny standard variant", () => {
const scene = game.scene; const scene = game.scene;
const expectedVariantTier = VariantTier.COMMON; const expectedVariantTier = VariantTier.STANDARD;
const result = new Egg({ scene, isShiny: true, variantTier: expectedVariantTier, species: Species.BULBASAUR }).generatePlayerPokemon(scene).variant; const result = new Egg({ scene, isShiny: true, variantTier: expectedVariantTier, species: Species.BULBASAUR }).generatePlayerPokemon(scene).variant;

View File

@ -0,0 +1,87 @@
import { BattlerIndex } from "#app/battle";
import { allMoves, ShellSideArmCategoryAttr } from "#app/data/move";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, it, expect, vi } from "vitest";
describe("Moves - Shell Side Arm", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
const TIMEOUT = 20 * 1000;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.moveset([Moves.SHELL_SIDE_ARM])
.battleType("single")
.startingLevel(100)
.enemyLevel(100)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);
});
it("becomes a physical attack if forecasted to deal more damage as physical", async () => {
game.override.enemySpecies(Species.SNORLAX);
await game.classicMode.startBattle([Species.MANAPHY]);
const shellSideArm = allMoves[Moves.SHELL_SIDE_ARM];
const shellSideArmAttr = shellSideArm.getAttrs(ShellSideArmCategoryAttr)[0];
vi.spyOn(shellSideArmAttr, "apply");
game.move.select(Moves.SHELL_SIDE_ARM);
await game.phaseInterceptor.to("MoveEffectPhase");
expect(shellSideArmAttr.apply).toHaveLastReturnedWith(true);
}, TIMEOUT);
it("remains a special attack if forecasted to deal more damage as special", async () => {
game.override.enemySpecies(Species.SLOWBRO);
await game.classicMode.startBattle([Species.MANAPHY]);
const shellSideArm = allMoves[Moves.SHELL_SIDE_ARM];
const shellSideArmAttr = shellSideArm.getAttrs(ShellSideArmCategoryAttr)[0];
vi.spyOn(shellSideArmAttr, "apply");
game.move.select(Moves.SHELL_SIDE_ARM);
await game.phaseInterceptor.to("MoveEffectPhase");
expect(shellSideArmAttr.apply).toHaveLastReturnedWith(false);
}, TIMEOUT);
it("respects stat stage changes when forecasting base damage", async () => {
game.override
.enemySpecies(Species.SNORLAX)
.enemyMoveset(Moves.COTTON_GUARD);
await game.classicMode.startBattle([Species.MANAPHY]);
const shellSideArm = allMoves[Moves.SHELL_SIDE_ARM];
const shellSideArmAttr = shellSideArm.getAttrs(ShellSideArmCategoryAttr)[0];
vi.spyOn(shellSideArmAttr, "apply");
game.move.select(Moves.SHELL_SIDE_ARM);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.phaseInterceptor.to("BerryPhase", false);
expect(shellSideArmAttr.apply).toHaveLastReturnedWith(false);
}, TIMEOUT);
});

View File

@ -69,22 +69,6 @@ describe("A Trainer's Test - Mystery Encounter", () => {
expect(ATrainersTestEncounter.options.length).toBe(2); expect(ATrainersTestEncounter.options.length).toBe(2);
}); });
it("should not run below wave 10", async () => {
game.override.startingWave(9);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.A_TRAINERS_TEST);
});
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
it("should initialize fully ", async () => { it("should initialize fully ", async () => {
initSceneWithoutEncounterPhase(scene, defaultParty); initSceneWithoutEncounterPhase(scene, defaultParty);
scene.currentBattle.mysteryEncounter = ATrainersTestEncounter; scene.currentBattle.mysteryEncounter = ATrainersTestEncounter;

View File

@ -66,22 +66,6 @@ describe("Absolute Avarice - Mystery Encounter", () => {
expect(AbsoluteAvariceEncounter.options.length).toBe(3); expect(AbsoluteAvariceEncounter.options.length).toBe(3);
}); });
it("should not run below wave 10", async () => {
game.override.startingWave(9);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.ABSOLUTE_AVARICE);
});
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
it("should not spawn outside of proper biomes", async () => { it("should not spawn outside of proper biomes", async () => {
game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT); game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT);
game.override.startingBiome(Biome.VOLCANO); game.override.startingBiome(Biome.VOLCANO);

View File

@ -80,22 +80,6 @@ describe("An Offer You Can't Refuse - Mystery Encounter", () => {
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE); expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE);
}); });
it("should not run below wave 10", async () => {
game.override.startingWave(9);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE);
});
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
it("should initialize fully ", async () => { it("should initialize fully ", async () => {
initSceneWithoutEncounterPhase(scene, defaultParty); initSceneWithoutEncounterPhase(scene, defaultParty);
scene.currentBattle.mysteryEncounter = AnOfferYouCantRefuseEncounter; scene.currentBattle.mysteryEncounter = AnOfferYouCantRefuseEncounter;

View File

@ -69,22 +69,6 @@ describe("Berries Abound - Mystery Encounter", () => {
expect(BerriesAboundEncounter.options.length).toBe(3); expect(BerriesAboundEncounter.options.length).toBe(3);
}); });
it("should not run below wave 10", async () => {
game.override.startingWave(9);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.BERRIES_ABOUND);
});
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
it("should initialize fully", async () => { it("should initialize fully", async () => {
initSceneWithoutEncounterPhase(scene, defaultParty); initSceneWithoutEncounterPhase(scene, defaultParty);
scene.currentBattle.mysteryEncounter = BerriesAboundEncounter; scene.currentBattle.mysteryEncounter = BerriesAboundEncounter;

View File

@ -201,22 +201,6 @@ describe("Bug-Type Superfan - Mystery Encounter", () => {
expect(BugTypeSuperfanEncounter.options.length).toBe(3); expect(BugTypeSuperfanEncounter.options.length).toBe(3);
}); });
it("should not run below wave 10", async () => {
game.override.startingWave(9);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.BUG_TYPE_SUPERFAN);
});
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
it("should initialize fully", async () => { it("should initialize fully", async () => {
initSceneWithoutEncounterPhase(scene, defaultParty); initSceneWithoutEncounterPhase(scene, defaultParty);
scene.currentBattle.mysteryEncounter = BugTypeSuperfanEncounter; scene.currentBattle.mysteryEncounter = BugTypeSuperfanEncounter;

View File

@ -95,14 +95,6 @@ describe("Clowning Around - Mystery Encounter", () => {
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.CLOWNING_AROUND); expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.CLOWNING_AROUND);
}); });
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
it("should initialize fully", async () => { it("should initialize fully", async () => {
initSceneWithoutEncounterPhase(scene, defaultParty); initSceneWithoutEncounterPhase(scene, defaultParty);
scene.currentBattle.mysteryEncounter = ClowningAroundEncounter; scene.currentBattle.mysteryEncounter = ClowningAroundEncounter;

View File

@ -69,22 +69,6 @@ describe("Dancing Lessons - Mystery Encounter", () => {
expect(DancingLessonsEncounter.options.length).toBe(3); expect(DancingLessonsEncounter.options.length).toBe(3);
}); });
it("should not run below wave 10", async () => {
game.override.startingWave(9);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.DANCING_LESSONS);
});
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
it("should not spawn outside of proper biomes", async () => { it("should not spawn outside of proper biomes", async () => {
game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT); game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT);
game.override.startingBiome(Biome.SPACE); game.override.startingBiome(Biome.SPACE);

View File

@ -66,22 +66,6 @@ describe("Delibird-y - Mystery Encounter", () => {
expect(DelibirdyEncounter.options.length).toBe(3); expect(DelibirdyEncounter.options.length).toBe(3);
}); });
it("should not run below wave 10", async () => {
game.override.startingWave(9);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.DELIBIRDY);
});
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
it("should not spawn if player does not have enough money", async () => { it("should not spawn if player does not have enough money", async () => {
scene.money = 0; scene.money = 0;

View File

@ -79,22 +79,6 @@ describe("Department Store Sale - Mystery Encounter", () => {
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.DEPARTMENT_STORE_SALE); expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.DEPARTMENT_STORE_SALE);
}); });
it("should not run below wave 10", async () => {
game.override.startingWave(9);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.DEPARTMENT_STORE_SALE);
});
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
describe("Option 1 - TM Shop", () => { describe("Option 1 - TM Shop", () => {
it("should have the correct properties", () => { it("should have the correct properties", () => {
const option = DepartmentStoreSaleEncounter.options[0]; const option = DepartmentStoreSaleEncounter.options[0];

View File

@ -72,22 +72,6 @@ describe("Field Trip - Mystery Encounter", () => {
expect(FieldTripEncounter.options.length).toBe(3); expect(FieldTripEncounter.options.length).toBe(3);
}); });
it("should not run below wave 10", async () => {
game.override.startingWave(9);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.FIELD_TRIP);
});
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
describe("Option 1 - Show off a physical move", () => { describe("Option 1 - Show off a physical move", () => {
it("should have the correct properties", () => { it("should have the correct properties", () => {
const option = FieldTripEncounter.options[0]; const option = FieldTripEncounter.options[0];

View File

@ -88,14 +88,6 @@ describe("Fiery Fallout - Mystery Encounter", () => {
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.FIERY_FALLOUT); expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.FIERY_FALLOUT);
}); });
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
it("should initialize fully ", async () => { it("should initialize fully ", async () => {
initSceneWithoutEncounterPhase(scene, defaultParty); initSceneWithoutEncounterPhase(scene, defaultParty);
scene.currentBattle.mysteryEncounter = FieryFalloutEncounter; scene.currentBattle.mysteryEncounter = FieryFalloutEncounter;

View File

@ -67,22 +67,6 @@ describe("Fight or Flight - Mystery Encounter", () => {
expect(FightOrFlightEncounter.options.length).toBe(3); expect(FightOrFlightEncounter.options.length).toBe(3);
}); });
it("should not run below wave 10", async () => {
game.override.startingWave(9);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.FIGHT_OR_FLIGHT);
});
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
it("should initialize fully", async () => { it("should initialize fully", async () => {
initSceneWithoutEncounterPhase(scene, defaultParty); initSceneWithoutEncounterPhase(scene, defaultParty);
scene.currentBattle.mysteryEncounter = FightOrFlightEncounter; scene.currentBattle.mysteryEncounter = FightOrFlightEncounter;

View File

@ -85,22 +85,6 @@ describe("Fun And Games! - Mystery Encounter", () => {
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.FUN_AND_GAMES); expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.FUN_AND_GAMES);
}); });
it("should not run below wave 10", async () => {
game.override.startingWave(9);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.FUN_AND_GAMES);
});
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
it("should initialize fully", async () => { it("should initialize fully", async () => {
initSceneWithoutEncounterPhase(scene, defaultParty); initSceneWithoutEncounterPhase(scene, defaultParty);
scene.currentBattle.mysteryEncounter = new MysteryEncounter(FunAndGamesEncounter); scene.currentBattle.mysteryEncounter = new MysteryEncounter(FunAndGamesEncounter);

View File

@ -69,22 +69,6 @@ describe("Global Trade System - Mystery Encounter", () => {
expect(GlobalTradeSystemEncounter.options.length).toBe(4); expect(GlobalTradeSystemEncounter.options.length).toBe(4);
}); });
it("should not run below wave 10", async () => {
game.override.startingWave(9);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.GLOBAL_TRADE_SYSTEM);
});
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
it("should not spawn outside of CIVILIZATION_ENCOUNTER_BIOMES", async () => { it("should not spawn outside of CIVILIZATION_ENCOUNTER_BIOMES", async () => {
game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON); game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON);
game.override.startingBiome(Biome.VOLCANO); game.override.startingBiome(Biome.VOLCANO);

View File

@ -74,22 +74,6 @@ describe("Lost at Sea - Mystery Encounter", () => {
expect(game.scene.currentBattle.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.LOST_AT_SEA); expect(game.scene.currentBattle.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.LOST_AT_SEA);
}); });
it("should not run below wave 11", async () => {
game.override.startingWave(9);
await game.runToMysteryEncounter();
expect(game.scene.currentBattle.mysteryEncounter).toBeUndefined();
});
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(game.scene.currentBattle.mysteryEncounter).toBeUndefined();
});
it("should initialize fully", () => { it("should initialize fully", () => {
initSceneWithoutEncounterPhase(scene, defaultParty); initSceneWithoutEncounterPhase(scene, defaultParty);
scene.currentBattle.mysteryEncounter = LostAtSeaEncounter; scene.currentBattle.mysteryEncounter = LostAtSeaEncounter;

View File

@ -79,22 +79,6 @@ describe("Mysterious Challengers - Mystery Encounter", () => {
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.MYSTERIOUS_CHALLENGERS); expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.MYSTERIOUS_CHALLENGERS);
}); });
it("should not run below wave 10", async () => {
game.override.startingWave(9);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.MYSTERIOUS_CHALLENGERS);
});
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
it("should initialize fully", async () => { it("should initialize fully", async () => {
initSceneWithoutEncounterPhase(scene, defaultParty); initSceneWithoutEncounterPhase(scene, defaultParty);
scene.currentBattle.mysteryEncounter = new MysteryEncounter(MysteriousChallengersEncounter); scene.currentBattle.mysteryEncounter = new MysteryEncounter(MysteriousChallengersEncounter);

View File

@ -80,22 +80,6 @@ describe("Part-Timer - Mystery Encounter", () => {
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.PART_TIMER); expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.PART_TIMER);
}); });
it("should not run below wave 10", async () => {
game.override.startingWave(9);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.PART_TIMER);
});
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
describe("Option 1 - Make Deliveries", () => { describe("Option 1 - Make Deliveries", () => {
it("should have the correct properties", () => { it("should have the correct properties", () => {
const option = PartTimerEncounter.options[0]; const option = PartTimerEncounter.options[0];

View File

@ -67,22 +67,6 @@ describe("Teleporting Hijinks - Mystery Encounter", () => {
expect(TeleportingHijinksEncounter.options.length).toBe(3); expect(TeleportingHijinksEncounter.options.length).toBe(3);
}); });
it("should not run below wave 10", async () => {
game.override.startingWave(9);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.TELEPORTING_HIJINKS);
});
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
it("should run in waves that are X1", async () => { it("should run in waves that are X1", async () => {
game.override.startingWave(11); game.override.startingWave(11);
game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON); game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON);

View File

@ -0,0 +1,283 @@
import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters";
import { HUMAN_TRANSITABLE_BIOMES } from "#app/data/mystery-encounters/mystery-encounters";
import { Biome } from "#app/enums/biome";
import { MysteryEncounterType } from "#app/enums/mystery-encounter-type";
import { Species } from "#app/enums/species";
import GameManager from "#app/test/utils/gameManager";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { runMysteryEncounterToEnd, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounter-test-utils";
import BattleScene from "#app/battle-scene";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils";
import { MysteryEncounterMode } from "#enums/mystery-encounter-mode";
import MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter";
import { CommandPhase } from "#app/phases/command-phase";
import { SelectModifierPhase } from "#app/phases/select-modifier-phase";
import { TheExpertPokemonBreederEncounter } from "#app/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter";
import { TrainerType } from "#enums/trainer-type";
import { EggTier } from "#enums/egg-type";
import { PostMysteryEncounterPhase } from "#app/phases/mystery-encounter-phases";
const namespace = "mysteryEncounter:expertPokemonBreeder";
const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA];
const defaultBiome = Biome.CAVE;
const defaultWave = 45;
describe("The Expert Pokémon Breeder - Mystery Encounter", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
let scene: BattleScene;
beforeAll(() => {
phaserGame = new Phaser.Game({ type: Phaser.HEADLESS });
});
beforeEach(async () => {
game = new GameManager(phaserGame);
scene = game.scene;
game.override.mysteryEncounterChance(100);
game.override.startingWave(defaultWave);
game.override.startingBiome(defaultBiome);
game.override.disableTrainerWaves();
const biomeMap = new Map<Biome, MysteryEncounterType[]>([
[Biome.VOLCANO, [MysteryEncounterType.FIGHT_OR_FLIGHT]],
]);
HUMAN_TRANSITABLE_BIOMES.forEach(biome => {
biomeMap.set(biome, [MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER]);
});
vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue(biomeMap);
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
vi.clearAllMocks();
vi.resetAllMocks();
});
it("should have the correct properties", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER, defaultParty);
expect(TheExpertPokemonBreederEncounter.encounterType).toBe(MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER);
expect(TheExpertPokemonBreederEncounter.encounterTier).toBe(MysteryEncounterTier.ULTRA);
expect(TheExpertPokemonBreederEncounter.dialogue).toBeDefined();
expect(TheExpertPokemonBreederEncounter.dialogue.intro).toStrictEqual([
{
text: `${namespace}.intro`
},
{
speaker: "trainerNames:expert_pokemon_breeder",
text: `${namespace}.intro_dialogue`
},
]);
expect(TheExpertPokemonBreederEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`);
expect(TheExpertPokemonBreederEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`);
expect(TheExpertPokemonBreederEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`);
expect(TheExpertPokemonBreederEncounter.options.length).toBe(3);
});
it("should not spawn outside of HUMAN_TRANSITABLE_BIOMES", async () => {
game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT);
game.override.startingBiome(Biome.VOLCANO);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER);
});
it("should initialize fully", async () => {
initSceneWithoutEncounterPhase(scene, defaultParty);
scene.currentBattle.mysteryEncounter = new MysteryEncounter(TheExpertPokemonBreederEncounter);
const encounter = scene.currentBattle.mysteryEncounter!;
scene.currentBattle.waveIndex = defaultWave;
const { onInit } = encounter;
expect(encounter.onInit).toBeDefined();
encounter.populateDialogueTokensFromRequirements(scene);
const onInitResult = onInit!(scene);
expect(encounter.enemyPartyConfigs).toBeDefined();
expect(encounter.enemyPartyConfigs.length).toBe(1);
expect(encounter.enemyPartyConfigs[0].trainerType).toBe(TrainerType.EXPERT_POKEMON_BREEDER);
expect(encounter.enemyPartyConfigs[0].pokemonConfigs?.length).toBe(3);
expect(encounter.spriteConfigs).toBeDefined();
expect(encounter.spriteConfigs.length).toBe(2);
expect(onInitResult).toBe(true);
});
describe("Option 1 - Battle with Pokemon 1", () => {
it("should have the correct properties", () => {
const option = TheExpertPokemonBreederEncounter.options[0];
expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT);
expect(option.dialogue).toBeDefined();
expect(option.dialogue).toStrictEqual({
buttonLabel: `${namespace}.option.1.label`,
buttonTooltip: expect.any(String), // Varies based on pokemon
selected: [
{
speaker: "trainerNames:expert_pokemon_breeder",
text: `${namespace}.option.selected`,
},
],
});
});
it("should start battle against the trainer", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER, defaultParty);
await runMysteryEncounterToEnd(game, 1, undefined, true);
expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name);
expect(scene.currentBattle.trainer).toBeDefined();
expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE);
expect(scene.getParty().length).toBe(1);
});
it("Should reward the player with friendship and eggs based on pokemon selected", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER, defaultParty);
const friendshipBefore = scene.currentBattle.mysteryEncounter!.misc.pokemon1.friendship;
scene.gameData.eggs = [];
const eggsBefore = scene.gameData.eggs;
expect(eggsBefore).toBeDefined();
const eggsBeforeLength = eggsBefore.length;
await runMysteryEncounterToEnd(game, 1, undefined, true);
await skipBattleRunMysteryEncounterRewardsPhase(game);
await game.phaseInterceptor.to(SelectModifierPhase, false);
expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name);
const eggsAfter = scene.gameData.eggs;
const commonEggs = scene.currentBattle.mysteryEncounter!.misc.pokemon1CommonEggs;
const rareEggs = scene.currentBattle.mysteryEncounter!.misc.pokemon1RareEggs;
expect(eggsAfter).toBeDefined();
expect(eggsBeforeLength + commonEggs + rareEggs).toBe(eggsAfter.length);
expect(eggsAfter.filter(egg => egg.tier === EggTier.COMMON).length).toBe(commonEggs);
expect(eggsAfter.filter(egg => egg.tier === EggTier.GREAT).length).toBe(rareEggs);
game.phaseInterceptor.superEndPhase();
await game.phaseInterceptor.to(PostMysteryEncounterPhase);
const friendshipAfter = scene.currentBattle.mysteryEncounter!.misc.pokemon1.friendship;
expect(friendshipAfter).toBe(friendshipBefore + 20 + 2); // +2 extra for friendship gained from winning battle
});
});
describe("Option 2 - Battle with Pokemon 2", () => {
it("should have the correct properties", () => {
const option = TheExpertPokemonBreederEncounter.options[1];
expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT);
expect(option.dialogue).toBeDefined();
expect(option.dialogue).toStrictEqual({
buttonLabel: `${namespace}.option.2.label`,
buttonTooltip: expect.any(String), // Varies based on pokemon
selected: [
{
speaker: "trainerNames:expert_pokemon_breeder",
text: `${namespace}.option.selected`,
},
],
});
});
it("should start battle against the trainer", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER, defaultParty);
await runMysteryEncounterToEnd(game, 2, undefined, true);
expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name);
expect(scene.currentBattle.trainer).toBeDefined();
expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE);
expect(scene.getParty().length).toBe(1);
});
it("Should reward the player with friendship and eggs based on pokemon selected", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER, defaultParty);
const friendshipBefore = scene.currentBattle.mysteryEncounter!.misc.pokemon2.friendship;
scene.gameData.eggs = [];
const eggsBefore = scene.gameData.eggs;
expect(eggsBefore).toBeDefined();
const eggsBeforeLength = eggsBefore.length;
await runMysteryEncounterToEnd(game, 2, undefined, true);
await skipBattleRunMysteryEncounterRewardsPhase(game);
await game.phaseInterceptor.to(SelectModifierPhase, false);
expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name);
const eggsAfter = scene.gameData.eggs;
const commonEggs = scene.currentBattle.mysteryEncounter!.misc.pokemon2CommonEggs;
const rareEggs = scene.currentBattle.mysteryEncounter!.misc.pokemon2RareEggs;
expect(eggsAfter).toBeDefined();
expect(eggsBeforeLength + commonEggs + rareEggs).toBe(eggsAfter.length);
expect(eggsAfter.filter(egg => egg.tier === EggTier.COMMON).length).toBe(commonEggs);
expect(eggsAfter.filter(egg => egg.tier === EggTier.GREAT).length).toBe(rareEggs);
game.phaseInterceptor.superEndPhase();
await game.phaseInterceptor.to(PostMysteryEncounterPhase);
const friendshipAfter = scene.currentBattle.mysteryEncounter!.misc.pokemon2.friendship;
expect(friendshipAfter).toBe(friendshipBefore + 20 + 2); // +2 extra for friendship gained from winning battle
});
});
describe("Option 3 - Battle with Pokemon 3", () => {
it("should have the correct properties", () => {
const option = TheExpertPokemonBreederEncounter.options[2];
expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT);
expect(option.dialogue).toBeDefined();
expect(option.dialogue).toStrictEqual({
buttonLabel: `${namespace}.option.3.label`,
buttonTooltip: expect.any(String), // Varies based on pokemon
selected: [
{
speaker: "trainerNames:expert_pokemon_breeder",
text: `${namespace}.option.selected`,
},
],
});
});
it("should start battle against the trainer", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER, defaultParty);
await runMysteryEncounterToEnd(game, 3, undefined, true);
expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name);
expect(scene.currentBattle.trainer).toBeDefined();
expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE);
expect(scene.getParty().length).toBe(1);
});
it("Should reward the player with friendship and eggs based on pokemon selected", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER, defaultParty);
const friendshipBefore = scene.currentBattle.mysteryEncounter!.misc.pokemon3.friendship;
scene.gameData.eggs = [];
const eggsBefore = scene.gameData.eggs;
expect(eggsBefore).toBeDefined();
const eggsBeforeLength = eggsBefore.length;
await runMysteryEncounterToEnd(game, 3, undefined, true);
await skipBattleRunMysteryEncounterRewardsPhase(game);
await game.phaseInterceptor.to(SelectModifierPhase, false);
expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name);
const eggsAfter = scene.gameData.eggs;
const commonEggs = scene.currentBattle.mysteryEncounter!.misc.pokemon3CommonEggs;
const rareEggs = scene.currentBattle.mysteryEncounter!.misc.pokemon3RareEggs;
expect(eggsAfter).toBeDefined();
expect(eggsBeforeLength + commonEggs + rareEggs).toBe(eggsAfter.length);
expect(eggsAfter.filter(egg => egg.tier === EggTier.COMMON).length).toBe(commonEggs);
expect(eggsAfter.filter(egg => egg.tier === EggTier.GREAT).length).toBe(rareEggs);
game.phaseInterceptor.superEndPhase();
await game.phaseInterceptor.to(PostMysteryEncounterPhase);
const friendshipAfter = scene.currentBattle.mysteryEncounter!.misc.pokemon3.friendship;
expect(friendshipAfter).toBe(friendshipBefore + 20 + 2); // +2 extra for friendship gained from winning battle
});
});
});

View File

@ -76,22 +76,6 @@ describe("The Pokemon Salesman - Mystery Encounter", () => {
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.THE_POKEMON_SALESMAN); expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.THE_POKEMON_SALESMAN);
}); });
it("should not run below wave 10", async () => {
game.override.startingWave(9);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.THE_POKEMON_SALESMAN);
});
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
it("should initialize fully ", async () => { it("should initialize fully ", async () => {
initSceneWithoutEncounterPhase(scene, defaultParty); initSceneWithoutEncounterPhase(scene, defaultParty);
scene.currentBattle.mysteryEncounter = ThePokemonSalesmanEncounter; scene.currentBattle.mysteryEncounter = ThePokemonSalesmanEncounter;

View File

@ -83,22 +83,6 @@ describe("The Strong Stuff - Mystery Encounter", () => {
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.THE_STRONG_STUFF); expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.THE_STRONG_STUFF);
}); });
it("should not run below wave 10", async () => {
game.override.startingWave(9);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.THE_STRONG_STUFF);
});
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
it("should initialize fully ", async () => { it("should initialize fully ", async () => {
initSceneWithoutEncounterPhase(scene, defaultParty); initSceneWithoutEncounterPhase(scene, defaultParty);
scene.currentBattle.mysteryEncounter = TheStrongStuffEncounter; scene.currentBattle.mysteryEncounter = TheStrongStuffEncounter;

View File

@ -90,22 +90,6 @@ describe("The Winstrate Challenge - Mystery Encounter", () => {
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.THE_WINSTRATE_CHALLENGE); expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.THE_WINSTRATE_CHALLENGE);
}); });
it("should not run below wave 10", async () => {
game.override.startingWave(9);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.THE_WINSTRATE_CHALLENGE);
});
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
it("should initialize fully", async () => { it("should initialize fully", async () => {
initSceneWithoutEncounterPhase(scene, defaultParty); initSceneWithoutEncounterPhase(scene, defaultParty);
scene.currentBattle.mysteryEncounter = new MysteryEncounter(TheWinstrateChallengeEncounter); scene.currentBattle.mysteryEncounter = new MysteryEncounter(TheWinstrateChallengeEncounter);

View File

@ -71,22 +71,6 @@ describe("Trash to Treasure - Mystery Encounter", () => {
expect(TrashToTreasureEncounter.options.length).toBe(2); expect(TrashToTreasureEncounter.options.length).toBe(2);
}); });
it("should not run below wave 10", async () => {
game.override.startingWave(9);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.TRASH_TO_TREASURE);
});
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
it("should initialize fully", async () => { it("should initialize fully", async () => {
initSceneWithoutEncounterPhase(scene, defaultParty); initSceneWithoutEncounterPhase(scene, defaultParty);
scene.currentBattle.mysteryEncounter = TrashToTreasureEncounter; scene.currentBattle.mysteryEncounter = TrashToTreasureEncounter;

View File

@ -74,22 +74,6 @@ describe("Uncommon Breed - Mystery Encounter", () => {
expect(UncommonBreedEncounter.options.length).toBe(3); expect(UncommonBreedEncounter.options.length).toBe(3);
}); });
it("should not run below wave 10", async () => {
game.override.startingWave(9);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.UNCOMMON_BREED);
});
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
it("should initialize fully", async () => { it("should initialize fully", async () => {
initSceneWithoutEncounterPhase(scene, defaultParty); initSceneWithoutEncounterPhase(scene, defaultParty);
scene.currentBattle.mysteryEncounter = UncommonBreedEncounter; scene.currentBattle.mysteryEncounter = UncommonBreedEncounter;

View File

@ -73,22 +73,6 @@ describe("Weird Dream - Mystery Encounter", () => {
expect(WeirdDreamEncounter.options.length).toBe(2); expect(WeirdDreamEncounter.options.length).toBe(2);
}); });
it("should not run below wave 10", async () => {
game.override.startingWave(9);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.WEIRD_DREAM);
});
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
it("should initialize fully", async () => { it("should initialize fully", async () => {
initSceneWithoutEncounterPhase(scene, defaultParty); initSceneWithoutEncounterPhase(scene, defaultParty);
scene.currentBattle.mysteryEncounter = WeirdDreamEncounter; scene.currentBattle.mysteryEncounter = WeirdDreamEncounter;

View File

@ -4,10 +4,12 @@ import Phaser from "phaser";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import BattleScene from "#app/battle-scene";
describe("Mystery Encounters", () => { describe("Mystery Encounters", () => {
let phaserGame: Phaser.Game; let phaserGame: Phaser.Game;
let game: GameManager; let game: GameManager;
let scene: BattleScene;
beforeAll(() => { beforeAll(() => {
phaserGame = new Phaser.Game({ phaserGame = new Phaser.Game({
@ -21,6 +23,7 @@ describe("Mystery Encounters", () => {
beforeEach(() => { beforeEach(() => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
scene = game.scene;
game.override.startingWave(11); game.override.startingWave(11);
game.override.mysteryEncounterChance(100); game.override.mysteryEncounterChance(100);
}); });
@ -32,23 +35,20 @@ describe("Mystery Encounters", () => {
expect(game.scene.getCurrentPhase()!.constructor.name).toBe(MysteryEncounterPhase.name); expect(game.scene.getCurrentPhase()!.constructor.name).toBe(MysteryEncounterPhase.name);
}); });
it("", async () => { it("Encounters should not run below wave 10", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS, [Species.CHARIZARD, Species.VOLCARONA]); game.override.startingWave(9);
await game.phaseInterceptor.to(MysteryEncounterPhase, false); await game.runToMysteryEncounter();
expect(game.scene.getCurrentPhase()!.constructor.name).toBe(MysteryEncounterPhase.name);
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.MYSTERIOUS_CHALLENGERS);
}); });
it("spawns mysterious challengers encounter", async () => { it("Encounters should not run above wave 180", async () => {
}); game.override.startingWave(181);
it("spawns mysterious chest encounter", async () => { await game.runToMysteryEncounter();
});
it("spawns dark deal encounter", async () => { expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
it("spawns fight or flight encounter", async () => {
}); });
}); });

View File

@ -14,6 +14,8 @@ import { initStatsKeys } from "#app/ui/game-stats-ui-handler";
import { initMysteryEncounters } from "#app/data/mystery-encounters/mystery-encounters"; import { initMysteryEncounters } from "#app/data/mystery-encounters/mystery-encounters";
import { beforeAll, vi } from "vitest"; import { beforeAll, vi } from "vitest";
process.env.TZ = "UTC";
/** Mock the override import to always return default values, ignoring any custom overrides. */ /** Mock the override import to always return default values, ignoring any custom overrides. */
vi.mock("#app/overrides", async (importOriginal) => { vi.mock("#app/overrides", async (importOriginal) => {
const { defaultOverrides } = await importOriginal<typeof import("#app/overrides")>(); const { defaultOverrides } = await importOriginal<typeof import("#app/overrides")>();

View File

@ -344,6 +344,7 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler {
super.clear(); super.clear();
this.config = null; this.config = null;
this.optionSelectContainer.setVisible(false); this.optionSelectContainer.setVisible(false);
this.scrollCursor = 0;
this.eraseCursor(); this.eraseCursor();
} }

View File

@ -1,12 +1,13 @@
import BattleScene from "../battle-scene"; import BattleScene from "#app/battle-scene";
import { Button } from "#enums/buttons"; import { Button } from "#enums/buttons";
import i18next from "i18next"; import i18next from "i18next";
import { Achv, achvs, getAchievementDescription } from "../system/achv"; import { Achv, achvs, getAchievementDescription } from "#app/system/achv";
import { Voucher, getVoucherTypeIcon, getVoucherTypeName, vouchers } from "../system/voucher"; import { Voucher, getVoucherTypeIcon, getVoucherTypeName, vouchers } from "#app/system/voucher";
import MessageUiHandler from "./message-ui-handler"; import MessageUiHandler from "#app/ui/message-ui-handler";
import { addTextObject, TextStyle } from "./text"; import { addTextObject, TextStyle } from "#app/ui/text";
import { Mode } from "./ui"; import { Mode } from "#app/ui/ui";
import { addWindow } from "./ui-theme"; import { addWindow } from "#app/ui/ui-theme";
import { ScrollBar } from "#app/ui/scroll-bar";
import { PlayerGender } from "#enums/player-gender"; import { PlayerGender } from "#enums/player-gender";
enum Page { enum Page {
@ -49,6 +50,7 @@ export default class AchvsUiHandler extends MessageUiHandler {
private vouchersTotal: number; private vouchersTotal: number;
private currentTotal: number; private currentTotal: number;
private scrollBar: ScrollBar;
private scrollCursor: number; private scrollCursor: number;
private cursorObj: Phaser.GameObjects.NineSlice | null; private cursorObj: Phaser.GameObjects.NineSlice | null;
private currentPage: Page; private currentPage: Page;
@ -91,7 +93,10 @@ export default class AchvsUiHandler extends MessageUiHandler {
this.iconsBg = addWindow(this.scene, 0, this.headerBg.height, (this.scene.game.canvas.width / 6) - 2, (this.scene.game.canvas.height / 6) - this.headerBg.height - 68); this.iconsBg = addWindow(this.scene, 0, this.headerBg.height, (this.scene.game.canvas.width / 6) - 2, (this.scene.game.canvas.height / 6) - this.headerBg.height - 68);
this.iconsBg.setOrigin(0, 0); this.iconsBg.setOrigin(0, 0);
this.iconsContainer = this.scene.add.container(6, this.headerBg.height + 6); const yOffset = 6;
this.scrollBar = new ScrollBar(this.scene, this.iconsBg.width - 9, this.iconsBg.y + yOffset, 4, this.iconsBg.height - yOffset * 2, this.ROWS);
this.iconsContainer = this.scene.add.container(5, this.headerBg.height + 8);
this.icons = []; this.icons = [];
@ -148,6 +153,7 @@ export default class AchvsUiHandler extends MessageUiHandler {
this.mainContainer.add(this.headerText); this.mainContainer.add(this.headerText);
this.mainContainer.add(this.headerActionText); this.mainContainer.add(this.headerActionText);
this.mainContainer.add(this.iconsBg); this.mainContainer.add(this.iconsBg);
this.mainContainer.add(this.scrollBar);
this.mainContainer.add(this.iconsContainer); this.mainContainer.add(this.iconsContainer);
this.mainContainer.add(titleBg); this.mainContainer.add(titleBg);
this.mainContainer.add(this.titleText); this.mainContainer.add(this.titleText);
@ -162,6 +168,7 @@ export default class AchvsUiHandler extends MessageUiHandler {
this.currentPage = Page.ACHIEVEMENTS; this.currentPage = Page.ACHIEVEMENTS;
this.setCursor(0); this.setCursor(0);
this.setScrollCursor(0);
this.mainContainer.setVisible(false); this.mainContainer.setVisible(false);
} }
@ -175,6 +182,8 @@ export default class AchvsUiHandler extends MessageUiHandler {
this.mainContainer.setVisible(true); this.mainContainer.setVisible(true);
this.setCursor(0); this.setCursor(0);
this.setScrollCursor(0); this.setScrollCursor(0);
this.scrollBar.setTotalRows(Math.ceil(this.currentTotal / this.COLS));
this.scrollBar.setScrollCursor(0);
this.getUi().moveTo(this.mainContainer, this.getUi().length - 1); this.getUi().moveTo(this.mainContainer, this.getUi().length - 1);
@ -224,6 +233,8 @@ export default class AchvsUiHandler extends MessageUiHandler {
this.updateAchvIcons(); this.updateAchvIcons();
} }
this.setCursor(0, true); this.setCursor(0, true);
this.scrollBar.setTotalRows(Math.ceil(this.currentTotal / this.COLS));
this.scrollBar.setScrollCursor(0);
this.mainContainer.update(); this.mainContainer.update();
} }
if (button === Button.CANCEL) { if (button === Button.CANCEL) {
@ -237,32 +248,44 @@ export default class AchvsUiHandler extends MessageUiHandler {
if (this.cursor < this.COLS) { if (this.cursor < this.COLS) {
if (this.scrollCursor) { if (this.scrollCursor) {
success = this.setScrollCursor(this.scrollCursor - 1); success = this.setScrollCursor(this.scrollCursor - 1);
} else {
// Wrap around to the last row
success = this.setScrollCursor(Math.ceil(this.currentTotal / this.COLS) - this.ROWS);
let newCursorIndex = this.cursor + (this.ROWS - 1) * this.COLS;
if (newCursorIndex > this.currentTotal - this.scrollCursor * this.COLS -1) {
newCursorIndex -= this.COLS;
}
success = success && this.setCursor(newCursorIndex);
} }
} else { } else {
success = this.setCursor(this.cursor - this.COLS); success = this.setCursor(this.cursor - this.COLS);
} }
break; break;
case Button.DOWN: case Button.DOWN:
const canMoveDown = (this.cursor + itemOffset) + this.COLS < this.currentTotal; const canMoveDown = itemOffset + 1 < this.currentTotal;
if (rowIndex >= this.ROWS - 1) { if (rowIndex >= this.ROWS - 1) {
if (this.scrollCursor < Math.ceil(this.currentTotal / this.COLS) - this.ROWS && canMoveDown) { if (this.scrollCursor < Math.ceil(this.currentTotal / this.COLS) - this.ROWS && canMoveDown) {
// scroll down one row
success = this.setScrollCursor(this.scrollCursor + 1); success = this.setScrollCursor(this.scrollCursor + 1);
} else {
// wrap back to the first row
success = this.setScrollCursor(0) && this.setCursor(this.cursor % this.COLS);
} }
} else if (canMoveDown) { } else if (canMoveDown) {
success = this.setCursor(this.cursor + this.COLS); success = this.setCursor(Math.min(this.cursor + this.COLS, this.currentTotal - itemOffset - 1));
} }
break; break;
case Button.LEFT: case Button.LEFT:
if (!this.cursor && this.scrollCursor) { if (this.cursor % this.COLS === 0) {
success = this.setScrollCursor(this.scrollCursor - 1) && this.setCursor(this.cursor + (this.COLS - 1)); success = this.setCursor(Math.min(this.cursor + this.COLS - 1, this.currentTotal - itemOffset - 1));
} else if (this.cursor) { } else {
success = this.setCursor(this.cursor - 1); success = this.setCursor(this.cursor - 1);
} }
break; break;
case Button.RIGHT: case Button.RIGHT:
if (this.cursor + 1 === this.ROWS * this.COLS && this.scrollCursor < Math.ceil(this.currentTotal / this.COLS) - this.ROWS) { if ((this.cursor + 1) % this.COLS === 0 || (this.cursor + itemOffset) === (this.currentTotal - 1)) {
success = this.setScrollCursor(this.scrollCursor + 1) && this.setCursor(this.cursor - (this.COLS - 1)); success = this.setCursor(this.cursor - this.cursor % this.COLS);
} else if (this.cursor + itemOffset < this.currentTotal - 1) { } else {
success = this.setCursor(this.cursor + 1); success = this.setCursor(this.cursor + 1);
} }
break; break;
@ -315,15 +338,22 @@ export default class AchvsUiHandler extends MessageUiHandler {
} }
this.scrollCursor = scrollCursor; this.scrollCursor = scrollCursor;
this.scrollBar.setScrollCursor(this.scrollCursor);
// Cursor cannot go farther than the last element in the list
const maxCursor = Math.min(this.cursor, this.currentTotal - this.scrollCursor * this.COLS - 1);
if (maxCursor !== this.cursor) {
this.setCursor(maxCursor);
}
switch (this.currentPage) { switch (this.currentPage) {
case Page.ACHIEVEMENTS: case Page.ACHIEVEMENTS:
this.updateAchvIcons(); this.updateAchvIcons();
this.showAchv(achvs[Object.keys(achvs)[Math.min(this.cursor + this.scrollCursor * this.COLS, Object.values(achvs).length - 1)]]); this.showAchv(achvs[Object.keys(achvs)[this.cursor + this.scrollCursor * this.COLS]]);
break; break;
case Page.VOUCHERS: case Page.VOUCHERS:
this.updateVoucherIcons(); this.updateVoucherIcons();
this.showVoucher(vouchers[Object.keys(vouchers)[Math.min(this.cursor + this.scrollCursor * this.COLS, Object.values(vouchers).length - 1)]]); this.showVoucher(vouchers[Object.keys(vouchers)[this.cursor + this.scrollCursor * this.COLS]]);
break; break;
} }
return true; return true;
@ -411,6 +441,7 @@ export default class AchvsUiHandler extends MessageUiHandler {
super.clear(); super.clear();
this.currentPage = Page.ACHIEVEMENTS; this.currentPage = Page.ACHIEVEMENTS;
this.mainContainer.setVisible(false); this.mainContainer.setVisible(false);
this.setScrollCursor(0);
this.eraseCursor(); this.eraseCursor();
} }

View File

@ -281,7 +281,7 @@ class RunEntryContainer extends Phaser.GameObjects.Container {
const genderIndex = this.scene.gameData.gender ?? PlayerGender.UNSET; const genderIndex = this.scene.gameData.gender ?? PlayerGender.UNSET;
const genderStr = PlayerGender[genderIndex].toLowerCase(); const genderStr = PlayerGender[genderIndex].toLowerCase();
// Defeats from wild Pokemon battles will show the Pokemon responsible by the text of the run result. // Defeats from wild Pokemon battles will show the Pokemon responsible by the text of the run result.
if (data.battleType === BattleType.WILD) { if (data.battleType === BattleType.WILD || (data.battleType === BattleType.MYSTERY_ENCOUNTER && !data.trainer)) {
const enemyContainer = this.scene.add.container(8, 5); const enemyContainer = this.scene.add.container(8, 5);
const gameOutcomeLabel = addTextObject(this.scene, 0, 0, `${i18next.t("runHistory:defeatedWild", { context: genderStr })}`, TextStyle.WINDOW); const gameOutcomeLabel = addTextObject(this.scene, 0, 0, `${i18next.t("runHistory:defeatedWild", { context: genderStr })}`, TextStyle.WINDOW);
enemyContainer.add(gameOutcomeLabel); enemyContainer.add(gameOutcomeLabel);
@ -302,7 +302,7 @@ class RunEntryContainer extends Phaser.GameObjects.Container {
enemy.destroy(); enemy.destroy();
}); });
this.add(enemyContainer); this.add(enemyContainer);
} else if (data.battleType === BattleType.TRAINER) { // Defeats from Trainers show the trainer's title and name } else if (data.battleType === BattleType.TRAINER || (data.battleType === BattleType.MYSTERY_ENCOUNTER && data.trainer)) { // Defeats from Trainers show the trainer's title and name
const tObj = data.trainer.toTrainer(this.scene); const tObj = data.trainer.toTrainer(this.scene);
// Because of the interesting mechanics behind rival names, the rival name and title have to be retrieved differently // Because of the interesting mechanics behind rival names, the rival name and title have to be retrieved differently
const RIVAL_TRAINER_ID_THRESHOLD = 375; const RIVAL_TRAINER_ID_THRESHOLD = 375;

View File

@ -211,7 +211,7 @@ export default class RunInfoUiHandler extends UiHandler {
if (!this.isVictory) { if (!this.isVictory) {
const enemyContainer = this.scene.add.container(0, 0); const enemyContainer = this.scene.add.container(0, 0);
// Wild - Single and Doubles // Wild - Single and Doubles
if (this.runInfo.battleType === BattleType.WILD) { if (this.runInfo.battleType === BattleType.WILD || (this.runInfo.battleType === BattleType.MYSTERY_ENCOUNTER && !this.runInfo.trainer)) {
switch (this.runInfo.enemyParty.length) { switch (this.runInfo.enemyParty.length) {
case 1: case 1:
// Wild - Singles // Wild - Singles
@ -222,7 +222,7 @@ export default class RunInfoUiHandler extends UiHandler {
this.parseWildDoubleDefeat(enemyContainer); this.parseWildDoubleDefeat(enemyContainer);
break; break;
} }
} else if (this.runInfo.battleType === BattleType.TRAINER) { } else if (this.runInfo.battleType === BattleType.TRAINER || (this.runInfo.battleType === BattleType.MYSTERY_ENCOUNTER && this.runInfo.trainer)) {
this.parseTrainerDefeat(enemyContainer); this.parseTrainerDefeat(enemyContainer);
} }
this.runResultContainer.add(enemyContainer); this.runResultContainer.add(enemyContainer);
@ -381,10 +381,6 @@ export default class RunInfoUiHandler extends UiHandler {
break; break;
case GameModes.SPLICED_ENDLESS: case GameModes.SPLICED_ENDLESS:
modeText.appendText(`${i18next.t("gameMode:endlessSpliced")}`, false); modeText.appendText(`${i18next.t("gameMode:endlessSpliced")}`, false);
if (this.runInfo.waveIndex === this.scene.gameData.gameStats.highestEndlessWave) {
modeText.appendText(` [${i18next.t("runHistory:personalBest")}]`, false);
modeText.setTint(0xffef5c, 0x47ff69, 0x6b6bff, 0xff6969);
}
break; break;
case GameModes.CHALLENGE: case GameModes.CHALLENGE:
modeText.appendText(`${i18next.t("gameMode:challenge")}`, false); modeText.appendText(`${i18next.t("gameMode:challenge")}`, false);
@ -403,17 +399,18 @@ export default class RunInfoUiHandler extends UiHandler {
break; break;
case GameModes.ENDLESS: case GameModes.ENDLESS:
modeText.appendText(`${i18next.t("gameMode:endless")}`, false); modeText.appendText(`${i18next.t("gameMode:endless")}`, false);
// If the player achieves a personal best in Endless, the mode text will be tinted similarly to SSS luck to celebrate their achievement.
if (this.runInfo.waveIndex === this.scene.gameData.gameStats.highestEndlessWave) {
modeText.appendText(` [${i18next.t("runHistory:personalBest")}]`, false);
modeText.setTint(0xffef5c, 0x47ff69, 0x6b6bff, 0xff6969);
}
break; break;
case GameModes.CLASSIC: case GameModes.CLASSIC:
modeText.appendText(`${i18next.t("gameMode:classic")}`, false); modeText.appendText(`${i18next.t("gameMode:classic")}`, false);
break; break;
} }
// If the player achieves a personal best in Endless, the mode text will be tinted similarly to SSS luck to celebrate their achievement.
if ((this.runInfo.gameMode === GameModes.ENDLESS || this.runInfo.gameMode === GameModes.SPLICED_ENDLESS) && this.runInfo.waveIndex === this.scene.gameData.gameStats.highestEndlessWave) {
modeText.appendText(` [${i18next.t("runHistory:personalBest")}]`);
modeText.setTint(0xffef5c, 0x47ff69, 0x6b6bff, 0xff6969);
}
// Duration + Money // Duration + Money
const runInfoTextContainer = this.scene.add.container(0, 0); const runInfoTextContainer = this.scene.add.container(0, 0);
// Japanese is set to a greater line spacing of 35px in addBBCodeTextObject() if lineSpacing < 12. // Japanese is set to a greater line spacing of 35px in addBBCodeTextObject() if lineSpacing < 12.

View File

@ -1,36 +1,65 @@
/**
* A vertical scrollbar element that resizes dynamically based on the current scrolling
* and number of elements that can be shown on screen
*/
export class ScrollBar extends Phaser.GameObjects.Container { export class ScrollBar extends Phaser.GameObjects.Container {
private bg: Phaser.GameObjects.Image; private bg: Phaser.GameObjects.NineSlice;
private handleBody: Phaser.GameObjects.Rectangle; private handleBody: Phaser.GameObjects.Rectangle;
private handleBottom: Phaser.GameObjects.Image; private handleBottom: Phaser.GameObjects.NineSlice;
private pages: number; private currentRow: number;
private page: number; private totalRows: number;
private maxRows: number;
constructor(scene: Phaser.Scene, x: number, y: number, pages: number) { /**
* @param scene the current scene
* @param x the scrollbar's x position (origin: top left)
* @param y the scrollbar's y position (origin: top left)
* @param width the scrollbar's width
* @param height the scrollbar's height
* @param maxRows the maximum number of rows that can be shown at once
*/
constructor(scene: Phaser.Scene, x: number, y: number, width: number, height: number, maxRows: number) {
super(scene, x, y); super(scene, x, y);
this.bg = scene.add.image(0, 0, "scroll_bar"); this.maxRows = maxRows;
const borderSize = 2;
width = Math.max(width, 4);
this.bg = scene.add.nineslice(0, 0, "scroll_bar", undefined, width, height, borderSize, borderSize, borderSize, borderSize);
this.bg.setOrigin(0, 0); this.bg.setOrigin(0, 0);
this.add(this.bg); this.add(this.bg);
this.handleBody = scene.add.rectangle(1, 1, 3, 4, 0xaaaaaa); this.handleBody = scene.add.rectangle(1, 1, width - 2, 4, 0xaaaaaa);
this.handleBody.setOrigin(0, 0); this.handleBody.setOrigin(0, 0);
this.add(this.handleBody); this.add(this.handleBody);
this.handleBottom = scene.add.image(1, 1, "scroll_bar_handle"); this.handleBottom = scene.add.nineslice(1, 1, "scroll_bar_handle", undefined, width - 2, 2, 2, 0, 0, 0);
this.handleBottom.setOrigin(0, 0); this.handleBottom.setOrigin(0, 0);
this.add(this.handleBottom); this.add(this.handleBottom);
} }
setPage(page: number): void { /**
this.page = page; * Set the current row that is displayed
this.handleBody.y = 1 + (this.bg.displayHeight - 1 - this.handleBottom.displayHeight) / this.pages * page; * Moves the bar handle up or down accordingly
* @param scrollCursor how many times the view was scrolled down
*/
setScrollCursor(scrollCursor: number): void {
this.currentRow = scrollCursor;
this.handleBody.y = 1 + (this.bg.displayHeight - 1 - this.handleBottom.displayHeight) / this.totalRows * this.currentRow;
this.handleBottom.y = this.handleBody.y + this.handleBody.displayHeight; this.handleBottom.y = this.handleBody.y + this.handleBody.displayHeight;
} }
setPages(pages: number): void { /**
this.pages = pages; * Set the total number of rows to display
this.handleBody.height = (this.bg.displayHeight - 1 - this.handleBottom.displayHeight) * 9 / this.pages; * If it's smaller than the maximum number of rows on screen the bar will get hidden
* Otherwise the scrollbar handle gets resized based on the ratio to the maximum number of rows
* @param rows how many rows of data there are in total
*/
setTotalRows(rows: number): void {
this.totalRows = rows;
this.handleBody.height = (this.bg.displayHeight - 1 - this.handleBottom.displayHeight) * this.maxRows / this.totalRows;
this.setVisible(this.pages > 9); this.setVisible(this.totalRows > this.maxRows);
} }
} }

View File

@ -1,9 +1,10 @@
import UiHandler from "../ui-handler"; import UiHandler from "#app/ui/ui-handler";
import BattleScene from "../../battle-scene"; import BattleScene from "#app/battle-scene";
import {Mode} from "../ui"; import { Mode } from "#app/ui/ui";
import {InterfaceConfig} from "../../inputs-controller"; import { InterfaceConfig } from "#app/inputs-controller";
import {addWindow} from "../ui-theme"; import { addWindow } from "#app/ui/ui-theme";
import {addTextObject, TextStyle} from "../text"; import { addTextObject, TextStyle } from "#app/ui/text";
import { ScrollBar } from "#app/ui/scroll-bar";
import { getIconWithSettingName } from "#app/configs/inputs/configHandler"; import { getIconWithSettingName } from "#app/configs/inputs/configHandler";
import NavigationMenu, { NavigationManager } from "#app/ui/settings/navigationMenu"; import NavigationMenu, { NavigationManager } from "#app/ui/settings/navigationMenu";
import { Device } from "#enums/devices"; import { Device } from "#enums/devices";
@ -19,7 +20,7 @@ export interface LayoutConfig {
inputsIcons: InputsIcons; inputsIcons: InputsIcons;
settingLabels: Phaser.GameObjects.Text[]; settingLabels: Phaser.GameObjects.Text[];
optionValueLabels: Phaser.GameObjects.Text[][]; optionValueLabels: Phaser.GameObjects.Text[][];
optionCursors: integer[]; optionCursors: number[];
keys: string[]; keys: string[];
bindingSettings: Array<String>; bindingSettings: Array<String>;
} }
@ -31,8 +32,9 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler
protected optionsContainer: Phaser.GameObjects.Container; protected optionsContainer: Phaser.GameObjects.Container;
protected navigationContainer: NavigationMenu; protected navigationContainer: NavigationMenu;
protected scrollCursor: integer; protected scrollBar: ScrollBar;
protected optionCursors: integer[]; protected scrollCursor: number;
protected optionCursors: number[];
protected cursorObj: Phaser.GameObjects.NineSlice | null; protected cursorObj: Phaser.GameObjects.NineSlice | null;
protected optionsBg: Phaser.GameObjects.NineSlice; protected optionsBg: Phaser.GameObjects.NineSlice;
@ -65,7 +67,7 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler
protected device: Device; protected device: Device;
abstract saveSettingToLocalStorage(setting, cursor): void; abstract saveSettingToLocalStorage(setting, cursor): void;
abstract setSetting(scene: BattleScene, setting, value: integer): boolean; abstract setSetting(scene: BattleScene, setting, value: number): boolean;
/** /**
* Constructor for the AbstractSettingsUiHandler. * Constructor for the AbstractSettingsUiHandler.
@ -241,7 +243,7 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler
// Calculate the total available space for placing option labels next to their setting label // Calculate the total available space for placing option labels next to their setting label
// We reserve space for the setting label and then distribute the remaining space evenly // We reserve space for the setting label and then distribute the remaining space evenly
const totalSpace = (300 - labelWidth) - totalWidth / 6; const totalSpace = (297 - labelWidth) - totalWidth / 6;
// Calculate the spacing between options based on the available space divided by the number of gaps between labels // Calculate the spacing between options based on the available space divided by the number of gaps between labels
const optionSpacing = Math.floor(totalSpace / (optionValueLabels[s].length - 1)); const optionSpacing = Math.floor(totalSpace / (optionValueLabels[s].length - 1));
@ -269,6 +271,11 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler
// Add the options container to the overall settings container to be displayed in the UI. // Add the options container to the overall settings container to be displayed in the UI.
this.settingsContainer.add(optionsContainer); this.settingsContainer.add(optionsContainer);
} }
// Add vertical scrollbar
this.scrollBar = new ScrollBar(this.scene, this.optionsBg.width - 9, this.optionsBg.y + 5, 4, this.optionsBg.height - 11, this.rowsToDisplay);
this.settingsContainer.add(this.scrollBar);
// Add the settings container to the UI. // Add the settings container to the UI.
ui.add(this.settingsContainer); ui.add(this.settingsContainer);
@ -413,6 +420,8 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler
this.optionCursors = layout.optionCursors; this.optionCursors = layout.optionCursors;
this.inputsIcons = layout.inputsIcons; this.inputsIcons = layout.inputsIcons;
this.bindingSettings = layout.bindingSettings; this.bindingSettings = layout.bindingSettings;
this.scrollBar.setTotalRows(layout.settingLabels.length);
this.scrollBar.setScrollCursor(0);
// Return true indicating the layout was successfully applied. // Return true indicating the layout was successfully applied.
return true; return true;
@ -538,7 +547,7 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler
* @param cursor - The cursor position to set. * @param cursor - The cursor position to set.
* @returns `true` if the cursor was set successfully. * @returns `true` if the cursor was set successfully.
*/ */
setCursor(cursor: integer): boolean { setCursor(cursor: number): boolean {
const ret = super.setCursor(cursor); const ret = super.setCursor(cursor);
// If the optionsContainer is not initialized, return the result from the parent class directly. // If the optionsContainer is not initialized, return the result from the parent class directly.
if (!this.optionsContainer) { if (!this.optionsContainer) {
@ -547,7 +556,8 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler
// Check if the cursor object exists, if not, create it. // Check if the cursor object exists, if not, create it.
if (!this.cursorObj) { if (!this.cursorObj) {
this.cursorObj = this.scene.add.nineslice(0, 0, "summary_moves_cursor", undefined, (this.scene.game.canvas.width / 6) - 10, 16, 1, 1, 1, 1); const cursorWidth = (this.scene.game.canvas.width / 6) - (this.scrollBar.visible? 16 : 10);
this.cursorObj = this.scene.add.nineslice(0, 0, "summary_moves_cursor", undefined, cursorWidth, 16, 1, 1, 1, 1);
this.cursorObj.setOrigin(0, 0); // Set the origin to the top-left corner. this.cursorObj.setOrigin(0, 0); // Set the origin to the top-left corner.
this.optionsContainer.add(this.cursorObj); // Add the cursor to the options container. this.optionsContainer.add(this.cursorObj); // Add the cursor to the options container.
} }
@ -564,7 +574,7 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler
* @param scrollCursor - The scroll cursor position to set. * @param scrollCursor - The scroll cursor position to set.
* @returns `true` if the scroll cursor was set successfully. * @returns `true` if the scroll cursor was set successfully.
*/ */
setScrollCursor(scrollCursor: integer): boolean { setScrollCursor(scrollCursor: number): boolean {
// Check if the new scroll position is the same as the current one; if so, do not update. // Check if the new scroll position is the same as the current one; if so, do not update.
if (scrollCursor === this.scrollCursor) { if (scrollCursor === this.scrollCursor) {
return false; return false;
@ -572,6 +582,7 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler
// Update the internal scroll cursor state // Update the internal scroll cursor state
this.scrollCursor = scrollCursor; this.scrollCursor = scrollCursor;
this.scrollBar.setScrollCursor(this.scrollCursor);
// Apply the new scroll position to the settings UI. // Apply the new scroll position to the settings UI.
this.updateSettingsScroll(); this.updateSettingsScroll();
@ -590,7 +601,7 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler
* @param save - Whether to save the setting to local storage. * @param save - Whether to save the setting to local storage.
* @returns `true` if the option cursor was set successfully. * @returns `true` if the option cursor was set successfully.
*/ */
setOptionCursor(settingIndex: integer, cursor: integer, save?: boolean): boolean { setOptionCursor(settingIndex: number, cursor: number, save?: boolean): boolean {
// Retrieve the specific setting using the settingIndex from the settingDevice enumeration. // Retrieve the specific setting using the settingIndex from the settingDevice enumeration.
const setting = this.setting[Object.keys(this.setting)[settingIndex]]; const setting = this.setting[Object.keys(this.setting)[settingIndex]];

View File

@ -1,9 +1,10 @@
import BattleScene from "../../battle-scene"; import BattleScene from "#app/battle-scene";
import { hasTouchscreen, isMobile } from "../../touch-controls"; import { hasTouchscreen, isMobile } from "#app/touch-controls";
import { TextStyle, addTextObject } from "../text"; import { TextStyle, addTextObject } from "#app/ui/text";
import { Mode } from "../ui"; import { Mode } from "#app/ui/ui";
import UiHandler from "../ui-handler"; import UiHandler from "#app/ui/ui-handler";
import { addWindow } from "../ui-theme"; import { addWindow } from "#app/ui/ui-theme";
import { ScrollBar } from "#app/ui/scroll-bar";
import { Button } from "#enums/buttons"; import { Button } from "#enums/buttons";
import { InputsIcons } from "#app/ui/settings/abstract-control-settings-ui-handler"; import { InputsIcons } from "#app/ui/settings/abstract-control-settings-ui-handler";
import NavigationMenu, { NavigationManager } from "#app/ui/settings/navigationMenu"; import NavigationMenu, { NavigationManager } from "#app/ui/settings/navigationMenu";
@ -19,11 +20,12 @@ export default class AbstractSettingsUiHandler extends UiHandler {
private optionsContainer: Phaser.GameObjects.Container; private optionsContainer: Phaser.GameObjects.Container;
private navigationContainer: NavigationMenu; private navigationContainer: NavigationMenu;
private scrollCursor: integer; private scrollCursor: number;
private scrollBar: ScrollBar;
private optionsBg: Phaser.GameObjects.NineSlice; private optionsBg: Phaser.GameObjects.NineSlice;
private optionCursors: integer[]; private optionCursors: number[];
private settingLabels: Phaser.GameObjects.Text[]; private settingLabels: Phaser.GameObjects.Text[];
private optionValueLabels: Phaser.GameObjects.Text[][]; private optionValueLabels: Phaser.GameObjects.Text[][];
@ -117,7 +119,7 @@ export default class AbstractSettingsUiHandler extends UiHandler {
const labelWidth = Math.max(78, this.settingLabels[s].displayWidth + 8); const labelWidth = Math.max(78, this.settingLabels[s].displayWidth + 8);
const totalSpace = (300 - labelWidth) - totalWidth / 6; const totalSpace = (297 - labelWidth) - totalWidth / 6;
const optionSpacing = Math.floor(totalSpace / (this.optionValueLabels[s].length - 1)); const optionSpacing = Math.floor(totalSpace / (this.optionValueLabels[s].length - 1));
let xOffset = 0; let xOffset = 0;
@ -130,7 +132,11 @@ export default class AbstractSettingsUiHandler extends UiHandler {
this.optionCursors = this.settings.map(setting => setting.default); this.optionCursors = this.settings.map(setting => setting.default);
this.scrollBar = new ScrollBar(this.scene, this.optionsBg.width - 9, this.optionsBg.y + 5, 4, this.optionsBg.height - 11, this.rowsToDisplay);
this.scrollBar.setTotalRows(this.settings.length);
this.settingsContainer.add(this.optionsBg); this.settingsContainer.add(this.optionsBg);
this.settingsContainer.add(this.scrollBar);
this.settingsContainer.add(this.navigationContainer); this.settingsContainer.add(this.navigationContainer);
this.settingsContainer.add(actionsBg); this.settingsContainer.add(actionsBg);
this.settingsContainer.add(this.optionsContainer); this.settingsContainer.add(this.optionsContainer);
@ -186,6 +192,7 @@ export default class AbstractSettingsUiHandler extends UiHandler {
this.settingsContainer.setVisible(true); this.settingsContainer.setVisible(true);
this.setCursor(0); this.setCursor(0);
this.setScrollCursor(0);
this.getUi().moveTo(this.settingsContainer, this.getUi().length - 1); this.getUi().moveTo(this.settingsContainer, this.getUi().length - 1);
@ -301,11 +308,12 @@ export default class AbstractSettingsUiHandler extends UiHandler {
* @param cursor - The cursor position to set. * @param cursor - The cursor position to set.
* @returns `true` if the cursor was set successfully. * @returns `true` if the cursor was set successfully.
*/ */
setCursor(cursor: integer): boolean { setCursor(cursor: number): boolean {
const ret = super.setCursor(cursor); const ret = super.setCursor(cursor);
if (!this.cursorObj) { if (!this.cursorObj) {
this.cursorObj = this.scene.add.nineslice(0, 0, "summary_moves_cursor", undefined, (this.scene.game.canvas.width / 6) - 10, 16, 1, 1, 1, 1); const cursorWidth = (this.scene.game.canvas.width / 6) - (this.scrollBar.visible? 16 : 10);
this.cursorObj = this.scene.add.nineslice(0, 0, "summary_moves_cursor", undefined, cursorWidth, 16, 1, 1, 1, 1);
this.cursorObj.setOrigin(0, 0); this.cursorObj.setOrigin(0, 0);
this.optionsContainer.add(this.cursorObj); this.optionsContainer.add(this.cursorObj);
} }
@ -323,7 +331,7 @@ export default class AbstractSettingsUiHandler extends UiHandler {
* @param save - Whether to save the setting to local storage. * @param save - Whether to save the setting to local storage.
* @returns `true` if the option cursor was set successfully. * @returns `true` if the option cursor was set successfully.
*/ */
setOptionCursor(settingIndex: integer, cursor: integer, save?: boolean): boolean { setOptionCursor(settingIndex: number, cursor: number, save?: boolean): boolean {
const setting = this.settings[settingIndex]; const setting = this.settings[settingIndex];
if (setting.key === SettingKeys.Touch_Controls && cursor && hasTouchscreen() && isMobile()) { if (setting.key === SettingKeys.Touch_Controls && cursor && hasTouchscreen() && isMobile()) {
@ -359,12 +367,13 @@ export default class AbstractSettingsUiHandler extends UiHandler {
* @param scrollCursor - The scroll cursor position to set. * @param scrollCursor - The scroll cursor position to set.
* @returns `true` if the scroll cursor was set successfully. * @returns `true` if the scroll cursor was set successfully.
*/ */
setScrollCursor(scrollCursor: integer): boolean { setScrollCursor(scrollCursor: number): boolean {
if (scrollCursor === this.scrollCursor) { if (scrollCursor === this.scrollCursor) {
return false; return false;
} }
this.scrollCursor = scrollCursor; this.scrollCursor = scrollCursor;
this.scrollBar.setScrollCursor(this.scrollCursor);
this.updateSettingsScroll(); this.updateSettingsScroll();
@ -394,6 +403,7 @@ export default class AbstractSettingsUiHandler extends UiHandler {
clear() { clear() {
super.clear(); super.clear();
this.settingsContainer.setVisible(false); this.settingsContainer.setVisible(false);
this.setScrollCursor(0);
this.eraseCursor(); this.eraseCursor();
this.getUi().bgmBar.toggleBgmBar(this.scene.showBgmBar); this.getUi().bgmBar.toggleBgmBar(this.scene.showBgmBar);
if (this.reloadRequired) { if (this.reloadRequired) {

View File

@ -83,7 +83,7 @@ const languageSettings: { [key: string]: LanguageSetting } = {
}, },
"fr":{ "fr":{
starterInfoTextSize: "54px", starterInfoTextSize: "54px",
instructionTextSize: "35px", instructionTextSize: "38px",
}, },
"it":{ "it":{
starterInfoTextSize: "56px", starterInfoTextSize: "56px",
@ -627,7 +627,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
const starterBoxContainer = this.scene.add.container(speciesContainerX + 6, 9); //115 const starterBoxContainer = this.scene.add.container(speciesContainerX + 6, 9); //115
this.starterSelectScrollBar = new ScrollBar(this.scene, 161, 12, 0); this.starterSelectScrollBar = new ScrollBar(this.scene, 161, 12, 5, starterContainerWindow.height - 6, 9);
starterBoxContainer.add(this.starterSelectScrollBar); starterBoxContainer.add(this.starterSelectScrollBar);
@ -2540,8 +2540,8 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
} }
}); });
this.starterSelectScrollBar.setPages(Math.max(Math.ceil(this.filteredStarterContainers.length / 9), 1)); this.starterSelectScrollBar.setTotalRows(Math.max(Math.ceil(this.filteredStarterContainers.length / 9), 1));
this.starterSelectScrollBar.setPage(0); this.starterSelectScrollBar.setScrollCursor(0);
// sort // sort
const sort = this.filterBar.getVals(DropDownColumn.SORT)[0]; const sort = this.filterBar.getVals(DropDownColumn.SORT)[0];
@ -2576,7 +2576,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
const onScreenFirstIndex = this.scrollCursor * maxColumns; const onScreenFirstIndex = this.scrollCursor * maxColumns;
const onScreenLastIndex = Math.min(this.filteredStarterContainers.length - 1, onScreenFirstIndex + maxRows * maxColumns -1); const onScreenLastIndex = Math.min(this.filteredStarterContainers.length - 1, onScreenFirstIndex + maxRows * maxColumns -1);
this.starterSelectScrollBar.setPage(this.scrollCursor); this.starterSelectScrollBar.setScrollCursor(this.scrollCursor);
let pokerusCursorIndex = 0; let pokerusCursorIndex = 0;
this.filteredStarterContainers.forEach((container, i) => { this.filteredStarterContainers.forEach((container, i) => {

View File

@ -3,11 +3,14 @@ import OptionSelectUiHandler from "./settings/option-select-ui-handler";
import { Mode } from "./ui"; import { Mode } from "./ui";
import * as Utils from "../utils"; import * as Utils from "../utils";
import { TextStyle, addTextObject, getTextStyleOptions } from "./text"; import { TextStyle, addTextObject, getTextStyleOptions } from "./text";
import { getBattleCountSplashMessage, getSplashMessages } from "../data/splash-messages"; import { getSplashMessages } from "../data/splash-messages";
import i18next from "i18next"; import i18next from "i18next";
import { TimedEventDisplay } from "#app/timed-event-manager"; import { TimedEventDisplay } from "#app/timed-event-manager";
export default class TitleUiHandler extends OptionSelectUiHandler { export default class TitleUiHandler extends OptionSelectUiHandler {
/** If the stats can not be retrieved, use this fallback value */
private static readonly BATTLES_WON_FALLBACK: number = -99999999;
private titleContainer: Phaser.GameObjects.Container; private titleContainer: Phaser.GameObjects.Container;
private playerCountLabel: Phaser.GameObjects.Text; private playerCountLabel: Phaser.GameObjects.Text;
private splashMessage: string; private splashMessage: string;
@ -72,8 +75,8 @@ export default class TitleUiHandler extends OptionSelectUiHandler {
.then(request => request.json()) .then(request => request.json())
.then(stats => { .then(stats => {
this.playerCountLabel.setText(`${stats.playerCount} ${i18next.t("menu:playersOnline")}`); this.playerCountLabel.setText(`${stats.playerCount} ${i18next.t("menu:playersOnline")}`);
if (this.splashMessage === getBattleCountSplashMessage()) { if (this.splashMessage === "splashMessages:battlesWon") {
this.splashMessageText.setText(getBattleCountSplashMessage().replace("{COUNT}", stats.battleCount.toLocaleString("en-US"))); this.splashMessageText.setText(i18next.t(this.splashMessage, { count: stats.battlesWon }));
} }
}) })
.catch(err => { .catch(err => {
@ -86,7 +89,7 @@ export default class TitleUiHandler extends OptionSelectUiHandler {
if (ret) { if (ret) {
this.splashMessage = Utils.randItem(getSplashMessages()); this.splashMessage = Utils.randItem(getSplashMessages());
this.splashMessageText.setText(this.splashMessage.replace("{COUNT}", "?")); this.splashMessageText.setText(i18next.t(this.splashMessage, { count: TitleUiHandler.BATTLES_WON_FALLBACK }));
const ui = this.getUi(); const ui = this.getUi();