diff --git a/README.md b/README.md index f3ec1c793bd..b26fd56a7b1 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ Check out [Github Issues](https://github.com/pagefaultgames/pokerogue/issues) to - Pokémon Legends: Arceus - Pokémon Scarlet/Violet - 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) ### 🎵 Sound Effects diff --git a/public/audio/bgm/jungle.mp3 b/public/audio/bgm/jungle.mp3 index 3a21c9bdb41..fbb770b0ad3 100644 Binary files a/public/audio/bgm/jungle.mp3 and b/public/audio/bgm/jungle.mp3 differ diff --git a/public/audio/bgm/laboratory.mp3 b/public/audio/bgm/laboratory.mp3 index e2b617e590a..1eba66ef56f 100644 Binary files a/public/audio/bgm/laboratory.mp3 and b/public/audio/bgm/laboratory.mp3 differ diff --git a/public/audio/bgm/lake.mp3 b/public/audio/bgm/lake.mp3 index c61fef15e42..e6762935770 100644 Binary files a/public/audio/bgm/lake.mp3 and b/public/audio/bgm/lake.mp3 differ diff --git a/public/audio/bgm/metropolis.mp3 b/public/audio/bgm/metropolis.mp3 index 98c2eb396b6..514c9ae15b1 100644 Binary files a/public/audio/bgm/metropolis.mp3 and b/public/audio/bgm/metropolis.mp3 differ diff --git a/public/audio/bgm/ruins.mp3 b/public/audio/bgm/ruins.mp3 index 62f31893423..3692c71562f 100644 Binary files a/public/audio/bgm/ruins.mp3 and b/public/audio/bgm/ruins.mp3 differ diff --git a/public/fonts/pkmnems.ttf b/public/fonts/pkmnems.ttf index 0aa3a19b417..b0b50d0f10f 100644 Binary files a/public/fonts/pkmnems.ttf and b/public/fonts/pkmnems.ttf differ diff --git a/public/images/trainer/expert_pokemon_breeder.json b/public/images/trainer/expert_pokemon_breeder.json new file mode 100644 index 00000000000..cd6ecffb267 --- /dev/null +++ b/public/images/trainer/expert_pokemon_breeder.json @@ -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$" + } +} diff --git a/public/images/trainer/expert_pokemon_breeder.png b/public/images/trainer/expert_pokemon_breeder.png new file mode 100644 index 00000000000..0625f5255c3 Binary files /dev/null and b/public/images/trainer/expert_pokemon_breeder.png differ diff --git a/src/battle.ts b/src/battle.ts index b857314b4bf..973104108b3 100644 --- a/src/battle.ts +++ b/src/battle.ts @@ -16,6 +16,14 @@ import { TrainerType } from "#enums/trainer-type"; import i18next from "#app/plugins/i18n"; import MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; 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 { WILD, @@ -419,6 +427,7 @@ export class FixedBattleConfig { public getTrainer: GetTrainerFunc; public getEnemyParty: GetEnemyPartyFunc; public seedOffsetWaveIndex: number; + public customModifierRewardSettings?: CustomModifierSettings; setBattleType(battleType: BattleType): FixedBattleConfig { this.battleType = battleType; @@ -444,6 +453,11 @@ export class FixedBattleConfig { this.seedOffsetWaveIndex = seedOffsetWaveIndex; return this; } + + setCustomModifierRewards(customModifierRewardSettings: CustomModifierSettings) { + this.customModifierRewardSettings = customModifierRewardSettings; + return this; + } } @@ -503,11 +517,13 @@ export const classicFixedBattles: FixedBattleConfigs = { [8]: new FixedBattleConfig().setBattleType(BattleType.TRAINER) .setGetTrainerFunc(scene => new Trainer(scene, TrainerType.RIVAL, scene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT)), [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) .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) - .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) .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) @@ -515,17 +531,21 @@ export const classicFixedBattles: FixedBattleConfigs = { [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)), [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) .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) .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) - .setGetTrainerFunc(getRandomTrainerFunc([ TrainerType.ROCKET_BOSS_GIOVANNI_1, TrainerType.MAXIE, TrainerType.ARCHIE, TrainerType.CYRUS, TrainerType.GHETSIS, TrainerType.LYSANDRE, TrainerType.LUSAMINE, TrainerType.GUZMA, TrainerType.ROSE ])), + [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 ])) + .setCustomModifierRewards({ guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ROGUE, ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.ULTRA], allowLuckUpgrades: false }), [145]: new FixedBattleConfig().setBattleType(BattleType.TRAINER) - .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) - .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 ])), + .setGetTrainerFunc(scene => new Trainer(scene, TrainerType.RIVAL_5, scene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT)) + .setCustomModifierRewards({ guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ROGUE, ModifierTier.ROGUE, ModifierTier.ULTRA, ModifierTier.ULTRA], allowLuckUpgrades: false }), + [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) .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) @@ -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 ])), [195]: new FixedBattleConfig().setBattleType(BattleType.TRAINER) .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 }) }; diff --git a/src/constants.ts b/src/constants.ts index a2f7e47b996..0b1261ad814 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -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; diff --git a/src/data/egg.ts b/src/data/egg.ts index 0219f4f5b47..b37240a2028 100644 --- a/src/data/egg.ts +++ b/src/data/egg.ts @@ -1,6 +1,6 @@ import BattleScene from "../battle-scene"; 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 Overrides from "#app/overrides"; 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 // species is set via options or the legendary gacha pokemon gets choosen the check never happens 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 this._eggMoveIndex = eggOptions?.eggMoveIndex ?? this.rollEggMoveIndex(); @@ -494,12 +494,12 @@ export class Egg { // place but I don't want to touch the pokemon class. private rollVariant(): VariantTier { if (!this.isShiny) { - return VariantTier.COMMON; + return VariantTier.STANDARD; } const rand = Utils.randSeedInt(10); if (rand >= 4) { - return VariantTier.COMMON; // 6/10 + return VariantTier.STANDARD; // 6/10 } else if (rand >= 1) { return VariantTier.RARE; // 3/10 } else { diff --git a/src/data/move.ts b/src/data/move.ts index 1b7108448bc..ef96ff19b07 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -3982,18 +3982,17 @@ export class StatusCategoryOnAllyAttr extends VariableMoveCategoryAttr { export class ShellSideArmCategoryAttr extends VariableMoveCategoryAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { 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 - if (atkRatio > specialRatio) { + const predictedPhysDmg = target.getBaseDamage(user, move, MoveCategory.PHYSICAL, true, true); + const predictedSpecDmg = target.getBaseDamage(user, move, MoveCategory.SPECIAL, true, true); + + if (predictedPhysDmg > predictedSpecDmg) { category.value = MoveCategory.PHYSICAL; return true; - } else if (atkRatio === specialRatio && user.randSeedInt(2) === 0) { + } else if (predictedPhysDmg === predictedSpecDmg && user.randSeedInt(2) === 0) { category.value = MoveCategory.PHYSICAL; return true; } - 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 */ -export class DealsDoubleDamageToTagAttr extends MoveAttr { +export class HitsTagAttr extends MoveAttr { /** The {@linkcode BattlerTagType} this move hits */ 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; - constructor(tagType: BattlerTagType, doubleDamage?: boolean) { + constructor(tagType: BattlerTagType, doubleDamage: boolean = false) { super(); 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 { public tagType: ArenaTagType; public turnCount: integer; @@ -6760,7 +6772,7 @@ export function initMoves() { new AttackMove(Moves.CUT, Type.NORMAL, MoveCategory.PHYSICAL, 50, 95, 30, -1, 0, 1) .slicingMove(), new AttackMove(Moves.GUST, Type.FLYING, MoveCategory.SPECIAL, 40, 100, 35, -1, 0, 1) - .attr(DealsDoubleDamageToTagAttr, BattlerTagType.FLYING, true) + .attr(HitsTagForDoubleDamageAttr, BattlerTagType.FLYING) .windMove(), 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) @@ -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.STOMP, Type.NORMAL, MoveCategory.PHYSICAL, 65, 100, 20, 30, 0, 1) .attr(AlwaysHitMinimizeAttr) - .attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true) + .attr(HitsTagForDoubleDamageAttr, BattlerTagType.MINIMIZED) .attr(FlinchAttr), new AttackMove(Moves.DOUBLE_KICK, Type.FIGHTING, MoveCategory.PHYSICAL, 30, 100, 30, -1, 0, 1) .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.BODY_SLAM, Type.NORMAL, MoveCategory.PHYSICAL, 85, 100, 15, 30, 0, 1) .attr(AlwaysHitMinimizeAttr) - .attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true) + .attr(HitsTagForDoubleDamageAttr, BattlerTagType.MINIMIZED) .attr(StatusEffectAttr, StatusEffect.PARALYSIS), new AttackMove(Moves.WRAP, Type.NORMAL, MoveCategory.PHYSICAL, 15, 90, 20, -1, 0, 1) .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.SURF, Type.WATER, MoveCategory.SPECIAL, 90, 100, 15, -1, 0, 1) .target(MoveTarget.ALL_NEAR_OTHERS) - .attr(DealsDoubleDamageToTagAttr, BattlerTagType.UNDERWATER, true) + .attr(HitsTagForDoubleDamageAttr, BattlerTagType.UNDERWATER) .attr(GulpMissileTagAttr), new AttackMove(Moves.ICE_BEAM, Type.ICE, MoveCategory.SPECIAL, 90, 100, 10, 10, 0, 1) .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) .attr(StatusEffectAttr, StatusEffect.PARALYSIS) .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) .makesContact(false), 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) .makesContact(false) .target(MoveTarget.ALL_NEAR_OTHERS), new AttackMove(Moves.FISSURE, Type.GROUND, MoveCategory.PHYSICAL, 200, 30, 5, -1, 0, 1) .attr(OneHitKOAttr) .attr(OneHitKOAccuracyAttr) - .attr(DealsDoubleDamageToTagAttr, BattlerTagType.UNDERGROUND, false) + .attr(HitsTagAttr, BattlerTagType.UNDERGROUND) .makesContact(false), 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) @@ -7354,7 +7366,7 @@ export function initMoves() { .attr(PreMoveMessageAttr, magnitudeMessageFunc) .attr(MagnitudePowerAttr) .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) .target(MoveTarget.ALL_NEAR_OTHERS), 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) .attr(HighCritAttr), 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) .windMove() .target(MoveTarget.ALL_NEAR_ENEMIES), @@ -7442,7 +7454,7 @@ export function initMoves() { .attr(StatStageChangeAttr, [ Stat.DEF ], -1), new AttackMove(Moves.WHIRLPOOL, Type.WATER, MoveCategory.SPECIAL, 35, 85, 15, -1, 0, 2) .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) .attr(MultiHitAttr, MultiHitType.BEAT_UP) .attr(BeatUpAttr) @@ -7665,7 +7677,7 @@ export function initMoves() { new AttackMove(Moves.EXTRASENSORY, Type.PSYCHIC, MoveCategory.SPECIAL, 80, 100, 20, 10, 0, 3) .attr(FlinchAttr), new AttackMove(Moves.SKY_UPPERCUT, Type.FIGHTING, MoveCategory.PHYSICAL, 85, 90, 15, -1, 0, 3) - .attr(DealsDoubleDamageToTagAttr, BattlerTagType.FLYING) + .attr(HitsTagAttr, BattlerTagType.FLYING) .punchingMove(), new AttackMove(Moves.SAND_TOMB, Type.GROUND, MoveCategory.PHYSICAL, 35, 85, 15, -1, 0, 3) .attr(TrapAttr, BattlerTagType.SAND_TOMB) @@ -7897,7 +7909,7 @@ export function initMoves() { .pulseMove(), new AttackMove(Moves.DRAGON_RUSH, Type.DRAGON, MoveCategory.PHYSICAL, 100, 75, 10, 20, 0, 4) .attr(AlwaysHitMinimizeAttr) - .attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true) + .attr(HitsTagForDoubleDamageAttr, BattlerTagType.MINIMIZED) .attr(FlinchAttr), 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) @@ -8094,7 +8106,7 @@ export function initMoves() { .attr(AddBattlerTagAttr, BattlerTagType.IGNORE_FLYING, false, false, 1, 1, true) .attr(AddBattlerTagAttr, BattlerTagType.INTERRUPTED) .attr(RemoveBattlerTagAttr, [BattlerTagType.FLYING, BattlerTagType.MAGNET_RISEN]) - .attr(DealsDoubleDamageToTagAttr, BattlerTagType.FLYING, false) + .attr(HitsTagAttr, BattlerTagType.FLYING) .makesContact(false), new AttackMove(Moves.STORM_THROW, Type.FIGHTING, MoveCategory.PHYSICAL, 60, 100, 10, -1, 0, 5) .attr(CritOnlyAttr), @@ -8109,7 +8121,7 @@ export function initMoves() { new AttackMove(Moves.HEAVY_SLAM, Type.STEEL, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 5) .attr(AlwaysHitMinimizeAttr) .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) .target(MoveTarget.ALL_NEAR_OTHERS) .condition(unknownTypeCondition) @@ -8262,12 +8274,12 @@ export function initMoves() { new AttackMove(Moves.HEAT_CRASH, Type.FIRE, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 5) .attr(AlwaysHitMinimizeAttr) .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) .attr(StatStageChangeAttr, [ Stat.ACC ], -1), new AttackMove(Moves.STEAMROLLER, Type.BUG, MoveCategory.PHYSICAL, 65, 100, 20, 30, 0, 5) .attr(AlwaysHitMinimizeAttr) - .attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true) + .attr(HitsTagForDoubleDamageAttr, BattlerTagType.MINIMIZED) .attr(FlinchAttr), new SelfStatusMove(Moves.COTTON_GUARD, Type.GRASS, -1, 10, -1, 0, 5) .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) .attr(ThunderAccuracyAttr) .attr(ConfuseAttr) - .attr(DealsDoubleDamageToTagAttr, BattlerTagType.FLYING, false) + .attr(HitsTagAttr, BattlerTagType.FLYING) .windMove(), new AttackMove(Moves.HEAD_CHARGE, Type.NORMAL, MoveCategory.PHYSICAL, 120, 100, 15, -1, 0, 5) .attr(RecoilAttr) @@ -8336,7 +8348,7 @@ export function initMoves() { new AttackMove(Moves.FLYING_PRESS, Type.FIGHTING, MoveCategory.PHYSICAL, 100, 95, 10, -1, 0, 6) .attr(AlwaysHitMinimizeAttr) .attr(FlyingTypeMultiplierAttr) - .attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true) + .attr(HitsTagForDoubleDamageAttr, BattlerTagType.MINIMIZED) .condition(failOnGravityCondition), new StatusMove(Moves.MAT_BLOCK, Type.FIGHTING, -1, 10, -1, 0, 6) .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) .attr(NeutralDamageAgainstFlyingTypeMultiplierAttr) .attr(AddBattlerTagAttr, BattlerTagType.IGNORE_FLYING, false, false, 1, 1, true) - .attr(DealsDoubleDamageToTagAttr, BattlerTagType.FLYING, false) - .attr(DealsDoubleDamageToTagAttr, BattlerTagType.MAGNET_RISEN, false) + .attr(HitsTagAttr, BattlerTagType.FLYING) + .attr(HitsTagAttr, BattlerTagType.MAGNET_RISEN) .attr(AddBattlerTagAttr, BattlerTagType.INTERRUPTED) .attr(RemoveBattlerTagAttr, [BattlerTagType.FLYING, BattlerTagType.MAGNET_RISEN]) .makesContact(false) @@ -8766,7 +8778,7 @@ export function initMoves() { .ignoresVirtual(), new AttackMove(Moves.MALICIOUS_MOONSAULT, Type.DARK, MoveCategory.PHYSICAL, 180, -1, 1, -1, 0, 7) .attr(AlwaysHitMinimizeAttr) - .attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true) + .attr(HitsTagAttr, BattlerTagType.MINIMIZED, true) .partial() .ignoresVirtual(), 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) .attr(ShellSideArmCategoryAttr) .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) .attr(SacrificialAttr) .target(MoveTarget.ALL_NEAR_OTHERS) diff --git a/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts b/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts index d79ac087725..202488030ee 100644 --- a/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts +++ b/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts @@ -9,6 +9,7 @@ import { transitionMysteryEncounterIntroVisuals, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { + getRandomPartyMemberFunc, trainerConfigs, TrainerPartyCompoundTemplate, TrainerPartyTemplate, @@ -17,14 +18,12 @@ import { import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { PartyMemberStrength } from "#enums/party-member-strength"; import BattleScene from "#app/battle-scene"; -import * as Utils from "#app/utils"; import { isNullOrUndefined, randSeedInt, randSeedShuffle } from "#app/utils"; import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { TrainerType } from "#enums/trainer-type"; import { Species } from "#enums/species"; -import Pokemon, { EnemyPokemon, PlayerPokemon, PokemonMove } from "#app/field/pokemon"; -import { getPokemonSpecies } from "#app/data/pokemon-species"; +import Pokemon, { PlayerPokemon, PokemonMove } from "#app/field/pokemon"; import { getEncounterText, showEncounterDialogue } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { LearnMovePhase } from "#app/phases/learn-move-phase"; import { Moves } from "#enums/moves"; @@ -584,16 +583,6 @@ function getTrainerConfigForWave(waveIndex: number) { 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 { return new Promise(async resolve => { const moveOptions = scene.currentBattle.mysteryEncounter!.misc.moveTutorOptions; diff --git a/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts b/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts index bd308a5919b..8a0a18d48ea 100644 --- a/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts +++ b/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts @@ -133,7 +133,7 @@ export const DancingLessonsEncounter: MysteryEncounter = } 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) scene.getEnemyParty().forEach(enemyPokemon => { diff --git a/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts b/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts new file mode 100644 index 00000000000..104ddd3d663 --- /dev/null +++ b/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts @@ -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); +} diff --git a/src/data/mystery-encounters/mystery-encounters.ts b/src/data/mystery-encounters/mystery-encounters.ts index d235ff86861..cc2eaf234c4 100644 --- a/src/data/mystery-encounters/mystery-encounters.ts +++ b/src/data/mystery-encounters/mystery-encounters.ts @@ -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 { UncommonBreedEncounter } from "#app/data/mystery-encounters/encounters/uncommon-breed-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 * ) / MYSTERY_ENCOUNTER_SPAWN_MAX_WEIGHT @@ -184,7 +185,8 @@ const humanTransitableBiomeEncounters: MysteryEncounterType[] = [ MysteryEncounterType.SHADY_VITAMIN_DEALER, MysteryEncounterType.THE_POKEMON_SALESMAN, MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE, - MysteryEncounterType.THE_WINSTRATE_CHALLENGE + MysteryEncounterType.THE_WINSTRATE_CHALLENGE, + MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER ]; const civilizationBiomeEncounters: MysteryEncounterType[] = [ @@ -238,7 +240,6 @@ export const mysteryEncountersByBiome = new Map([ MysteryEncounterType.SAFARI_ZONE, MysteryEncounterType.ABSOLUTE_AVARICE ]], - [Biome.SEA, [ MysteryEncounterType.LOST_AT_SEA ]], @@ -275,7 +276,9 @@ export const mysteryEncountersByBiome = new Map([ [Biome.ABYSS, [ MysteryEncounterType.DANCING_LESSONS ]], - [Biome.SPACE, []], + [Biome.SPACE, [ + MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER + ]], [Biome.CONSTRUCTION_SITE, []], [Biome.JUNGLE, [ MysteryEncounterType.SAFARI_ZONE @@ -319,6 +322,7 @@ export function initMysteryEncounters() { allMysteryEncounters[MysteryEncounterType.FUN_AND_GAMES] = FunAndGamesEncounter; allMysteryEncounters[MysteryEncounterType.UNCOMMON_BREED] = UncommonBreedEncounter; allMysteryEncounters[MysteryEncounterType.GLOBAL_TRADE_SYSTEM] = GlobalTradeSystemEncounter; + allMysteryEncounters[MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER] = TheExpertPokemonBreederEncounter; // Add extreme encounters to biome map extremeBiomeEncounters.forEach(encounter => { diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts index 85d583fc898..187de3c93c4 100644 --- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -36,6 +36,7 @@ import { BattleEndPhase } from "#app/phases/battle-end-phase"; import { GameOverPhase } from "#app/phases/game-over-phase"; import { SelectModifierPhase } from "#app/phases/select-modifier-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 @@ -67,6 +68,7 @@ export function doTrainerExclamation(scene: BattleScene) { export interface EnemyPokemonConfig { species: PokemonSpecies; isBoss: boolean; + nickname?: string; bossSegments?: number; bossSegmentModifier?: number; // Additive to the determined segment number mysteryEncounterPokemonData?: MysteryEncounterPokemonData; @@ -79,6 +81,8 @@ export interface EnemyPokemonConfig { nature?: Nature; ivs?: [number, number, number, number, number, number]; shiny?: boolean; + /** Is only checked if Pokemon is shiny */ + variant?: Variant; /** Can set just the status, or pass a timer on the status turns */ status?: StatusEffect | [StatusEffect, number]; mysteryEncounterBattleEffects?: (pokemon: Pokemon) => void; @@ -220,6 +224,11 @@ export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig: if (partyConfig?.pokemonConfigs && e < partyConfig.pokemonConfigs.length) { 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 if (config.dataSource) { enemyPokemon.id = Utils.randSeedInt(4294967296); @@ -235,6 +244,11 @@ export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig: 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.) if (!isNullOrUndefined(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 enemyPokemon.primeSummonData(enemyPokemon.summonData); + if (enemyPokemon.isShiny() && !enemyPokemon["shinySparkle"]) { + enemyPokemon.initShinySparkle(); + } enemyPokemon.initBattleInfo(); enemyPokemon.getBattleInfo().initInfo(enemyPokemon); enemyPokemon.generateName(); diff --git a/src/data/splash-messages.ts b/src/data/splash-messages.ts index 8e95bba0591..b8069f77737 100644 --- a/src/data/splash-messages.ts +++ b/src/data/splash-messages.ts @@ -1,46 +1,136 @@ -import i18next from "i18next"; +import { USE_SEASONAL_SPLASH_MESSAGES } from "#app/constants"; -export function getBattleCountSplashMessage(): string { - return `{COUNT} ${i18next.t("splashMessages:battlesWon")}`; +//#region Interfaces/Types + +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[] { - const splashMessages = Array(10).fill(getBattleCountSplashMessage()); - splashMessages.push( - i18next.t("splashMessages:joinTheDiscord"), - i18next.t("splashMessages:infiniteLevels"), - i18next.t("splashMessages:everythingStacks"), - i18next.t("splashMessages:optionalSaveScumming"), - i18next.t("splashMessages:biomes"), - i18next.t("splashMessages:openSource"), - 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"), - ); + const splashMessages: string[] = [...commonSplashMessages]; + console.log("use seasonal splash messages", USE_SEASONAL_SPLASH_MESSAGES); + if (USE_SEASONAL_SPLASH_MESSAGES) { + // add seasonal splash messages if the season is active + for (const { name, start, end, messages } of seasonalSplashMessages) { + const now = new Date(); + const startDate = new Date(`${start}-${now.getFullYear()}`); + const endDate = new Date(`${end}-${now.getFullYear()}`); - 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}`); } diff --git a/src/data/trainer-config.ts b/src/data/trainer-config.ts index 8b96e3cefb8..dca7b31a862 100644 --- a/src/data/trainer-config.ts +++ b/src/data/trainer-config.ts @@ -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); } -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); if (!ignoreEvolution) { species = getPokemonSpecies(species).getTrainerSpeciesForLevel(level, true, strength, scene.currentBattle.waveIndex); @@ -2302,6 +2310,8 @@ export const trainerConfigs: TrainerConfigs = { .setMoneyMultiplier(2) .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) - .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)) }; diff --git a/src/enums/mystery-encounter-type.ts b/src/enums/mystery-encounter-type.ts index 39a8087599c..b973652b113 100644 --- a/src/enums/mystery-encounter-type.ts +++ b/src/enums/mystery-encounter-type.ts @@ -28,5 +28,6 @@ export enum MysteryEncounterType { BUG_TYPE_SUPERFAN, FUN_AND_GAMES, UNCOMMON_BREED, - GLOBAL_TRADE_SYSTEM + GLOBAL_TRADE_SYSTEM, + THE_EXPERT_POKEMON_BREEDER } diff --git a/src/enums/trainer-type.ts b/src/enums/trainer-type.ts index cfc52b70eb0..fa52e376a07 100644 --- a/src/enums/trainer-type.ts +++ b/src/enums/trainer-type.ts @@ -107,6 +107,7 @@ export enum TrainerType { VICKY, VITO, BUG_TYPE_SUPERFAN, + EXPERT_POKEMON_BREEDER, BROCK = 200, MISTY, diff --git a/src/enums/variant-tiers.ts b/src/enums/variant-tiers.ts deleted file mode 100644 index 20a0e8ec4e4..00000000000 --- a/src/enums/variant-tiers.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum VariantTier { - COMMON, - RARE, - EPIC -} diff --git a/src/field/arena.ts b/src/field/arena.ts index bf4075e5c1c..9897da7cfd7 100644 --- a/src/field/arena.ts +++ b/src/field/arena.ts @@ -1,19 +1,19 @@ 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 * as Utils from "../utils"; 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 { Type } from "../data/type"; import Move from "../data/move"; import { ArenaTag, ArenaTagSide, ArenaTrapTag, getArenaTag } from "../data/arena-tag"; import { BattlerIndex } from "../battle"; 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 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 { Biome } from "#enums/biome"; import { Moves } from "#enums/moves"; @@ -762,7 +762,7 @@ export class Arena { case Biome.BEACH: return 3.462; case Biome.LAKE: - return 5.350; + return 7.215; case Biome.SEABED: return 2.600; case Biome.MOUNTAIN: @@ -788,7 +788,7 @@ export class Arena { case Biome.FACTORY: return 4.985; case Biome.RUINS: - return 2.270; + return 0.000; case Biome.WASTELAND: return 6.336; case Biome.ABYSS: diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index cdafc960382..5cfdb333db2 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -3,7 +3,7 @@ import BattleScene, { AnySound } from "../battle-scene"; import { Variant, VariantSet, variantColorCache } from "#app/data/variant"; import { variantData } from "#app/data/variant"; 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 { Constructor, isNullOrUndefined, randSeedInt } from "#app/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); 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; break; } @@ -2322,11 +2322,61 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { 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 * @param source {@linkcode Pokemon} the attacking Pokemon * @param move {@linkcode Pokemon} the move used in the attack * @param ignoreAbility If `true`, ignores this Pokemon's defensive ability effects + * @param ignoreSourceAbility If `true`, ignores the attacking Pokemon's ability effects * @param isCritical If `true`, calculates damage for a critical hit. * @param simulated If `true`, suppresses changes to game state during the calculation. * @returns a {@linkcode DamageCalculationResult} object with three fields: @@ -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 * and Attack stat as well as this Pokemon's Defense stat */ - const baseDamage = ((levelMultiplier * power * sourceAtk.value / targetDef.value) / 50) + 2; - - // ------ END BASE DAMAGE MULTIPLIERS ------ + const baseDamage = this.getBaseDamage(source, move, moveCategory, ignoreAbility, ignoreSourceAbility, isCritical, simulated); /** 25% damage debuff on moves hitting more than one non-fainted target (regardless of immunities) */ 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); /** - * 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 * AND * The move doubles damage when used against that tag */ 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)) { 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) if (!simulated) { - console.log("damage", damage.value, move.name, power, sourceAtk, targetDef); + console.log("damage", damage.value, move.name); } let hitResult: HitResult; diff --git a/src/game-mode.ts b/src/game-mode.ts index a2d61d7cfff..525c975a19b 100644 --- a/src/game-mode.ts +++ b/src/game-mode.ts @@ -268,7 +268,6 @@ export class GameMode implements GameModeConfig { isFixedBattle(waveIndex: integer): boolean { const dummyConfig = new FixedBattleConfig(); return this.battleConfig.hasOwnProperty(waveIndex) || applyChallenges(this, ChallengeType.FIXED_BATTLES, waveIndex, dummyConfig); - } /** diff --git a/src/locales/de/move.json b/src/locales/de/move.json index 3c81ccfd7df..b7a42cb1787 100644 --- a/src/locales/de/move.json +++ b/src/locales/de/move.json @@ -3121,11 +3121,11 @@ }, "behemothBlade": { "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": { "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": { "name": "Aura-Rad", diff --git a/src/locales/de/splash-messages.json b/src/locales/de/splash-messages.json index ac3fd345f3f..ba126393ccb 100644 --- a/src/locales/de/splash-messages.json +++ b/src/locales/de/splash-messages.json @@ -1,5 +1,5 @@ { - "battlesWon": "Kämpfe gewonnen!", + "battlesWon": "{{count, number}} Kämpfe gewonnen!", "joinTheDiscord": "Tritt dem Discord bei!", "infiniteLevels": "Unendliche Level!", "everythingStacks": "Alles stapelt sich!", diff --git a/src/locales/en/achv.json b/src/locales/en/achv.json index 32d519fbf78..b04f23d4209 100644 --- a/src/locales/en/achv.json +++ b/src/locales/en/achv.json @@ -283,5 +283,9 @@ "INVERSE_BATTLE": { "name": "Mirror rorriM", "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." } } diff --git a/src/locales/en/bgm-name.json b/src/locales/en/bgm-name.json index 7a8ec738c98..f38b62d14c6 100644 --- a/src/locales/en/bgm-name.json +++ b/src/locales/en/bgm-name.json @@ -112,13 +112,13 @@ "island": "PMD EoS Craggy Coast", "jungle": "Lmz - Jungle", "laboratory": "Firel - Laboratory", - "lake": "PMD EoS Crystal Cave", + "lake": "Lmz - Lake", "meadow": "PMD EoS Sky Peak Forest", "metropolis": "Firel - Metropolis", "mountain": "PMD EoS Mt. Horn", "plains": "Firel - Route 888", "power_plant": "Firel - The Klink", - "ruins": "PMD EoS Deep Sealed Ruin", + "ruins": "Lmz - Ancient Ruins", "sea": "Andr06 - Marine Mystique", "seabed": "Firel - Seabed", "slum": "Andr06 - Sneaky Snom", diff --git a/src/locales/en/config.ts b/src/locales/en/config.ts index f83fec5be26..35eef91e2ad 100644 --- a/src/locales/en/config.ts +++ b/src/locales/en/config.ts @@ -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 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 expertPokemonBreeder from "#app/locales/en/mystery-encounters/the-expert-pokemon-breeder-dialogue.json"; /** * Dialogue/Text token injection patterns that can be used: @@ -183,7 +184,8 @@ export const enConfig = { bugTypeSuperfan, funAndGames, uncommonBreed, - globalTradeSystem + globalTradeSystem, + expertPokemonBreeder }, mysteryEncounterMessages }; diff --git a/src/locales/en/mystery-encounters/part-timer-dialogue.json b/src/locales/en/mystery-encounters/part-timer-dialogue.json index 614f1818e3f..801a409ee84 100644 --- a/src/locales/en/mystery-encounters/part-timer-dialogue.json +++ b/src/locales/en/mystery-encounters/part-timer-dialogue.json @@ -21,7 +21,7 @@ "label": "Sales Assistant", "tooltip": "(-) Your {{option3PrimaryName}} uses {{option3PrimaryMove}}\n(+) Earn @[MONEY]{Money}", "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.", diff --git a/src/locales/en/mystery-encounters/the-expert-pokemon-breeder-dialogue.json b/src/locales/en/mystery-encounters/the-expert-pokemon-breeder-dialogue.json new file mode 100644 index 00000000000..ebe3af38add --- /dev/null +++ b/src/locales/en/mystery-encounters/the-expert-pokemon-breeder-dialogue.json @@ -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" +} \ No newline at end of file diff --git a/src/locales/en/splash-messages.json b/src/locales/en/splash-messages.json index c0686e6ad75..168974525f8 100644 --- a/src/locales/en/splash-messages.json +++ b/src/locales/en/splash-messages.json @@ -1,5 +1,5 @@ { - "battlesWon": "Battles Won!", + "battlesWon": "{{count, number}} Battles Won!", "joinTheDiscord": "Join the Discord!", "infiniteLevels": "Infinite Levels!", "everythingStacks": "Everything Stacks!", @@ -32,5 +32,17 @@ "alsoTryRadicalRed": "Also Try Radical Red!", "eeveeExpo": "Eevee Expo!", "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!" + } } \ No newline at end of file diff --git a/src/locales/en/trainer-names.json b/src/locales/en/trainer-names.json index 467ed03e044..3c709f6a138 100644 --- a/src/locales/en/trainer-names.json +++ b/src/locales/en/trainer-names.json @@ -172,5 +172,6 @@ "vivi": "Vivi", "vicky": "Vicky", "vito": "Vito", - "bug_type_superfan": "Bug-Type Superfan" + "bug_type_superfan": "Bug-Type Superfan", + "expert_pokemon_breeder": "Expert Pokémon Breeder" } diff --git a/src/locales/es/splash-messages.json b/src/locales/es/splash-messages.json index b1d4820b06e..da31d394c0f 100644 --- a/src/locales/es/splash-messages.json +++ b/src/locales/es/splash-messages.json @@ -1,5 +1,5 @@ { - "battlesWon": "¡Batallas ganadas!", + "battlesWon": "¡{{count, number}} Batallas ganadas!", "joinTheDiscord": "¡Únete al Discord!", "infiniteLevels": "¡Niveles infinitos!", "everythingStacks": "¡Todo se acumula!", diff --git a/src/locales/fr/move.json b/src/locales/fr/move.json index 957895b5db9..da42f188a80 100644 --- a/src/locales/fr/move.json +++ b/src/locales/fr/move.json @@ -3121,11 +3121,11 @@ }, "behemothBlade": { "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": { "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": { "name": "Roue Libre", diff --git a/src/locales/fr/party-ui-handler.json b/src/locales/fr/party-ui-handler.json index a11640c80b3..4eef55da790 100644 --- a/src/locales/fr/party-ui-handler.json +++ b/src/locales/fr/party-ui-handler.json @@ -13,7 +13,7 @@ "ALL": "Tout", "PASS_BATON": "Relais", "UNPAUSE_EVOLUTION": "Réactiver Évolution", - "PAUSE_EVOLUTION": "Interrompre Évolution", + "PAUSE_EVOLUTION": "Empêcher Évolution", "REVIVE": "Ranimer", "RENAME": "Renommer", "choosePokemon": "Sélectionnez un Pokémon.", diff --git a/src/locales/fr/splash-messages.json b/src/locales/fr/splash-messages.json index 9dd3e86fb32..2ac85680e58 100644 --- a/src/locales/fr/splash-messages.json +++ b/src/locales/fr/splash-messages.json @@ -1,5 +1,5 @@ { - "battlesWon": "combats gagnés !", + "battlesWon": "{{count, number}} combats gagnés !", "joinTheDiscord": "Rejoins le Discord !", "infiniteLevels": "Niveaux infinis !", "everythingStacks": "Tout se cumule !", diff --git a/src/locales/it/splash-messages.json b/src/locales/it/splash-messages.json index 55018d0ada0..d4b411241b6 100644 --- a/src/locales/it/splash-messages.json +++ b/src/locales/it/splash-messages.json @@ -1,5 +1,5 @@ { - "battlesWon": "Battaglie Vinte!", + "battlesWon": "{{count, number}} Battaglie Vinte!", "joinTheDiscord": "Entra nel Discord!", "infiniteLevels": "Livelli Infiniti!", "everythingStacks": "Tutto si impila!", diff --git a/src/locales/ja/splash-messages.json b/src/locales/ja/splash-messages.json index b7378e7a916..db3948fa2f1 100644 --- a/src/locales/ja/splash-messages.json +++ b/src/locales/ja/splash-messages.json @@ -1,5 +1,5 @@ { - "battlesWon": "Battles Won!", + "battlesWon": "勝ったバトル:{{count, number}}回!", "joinTheDiscord": "Join the Discord!", "infiniteLevels": "Infinite Levels!", "everythingStacks": "Everything Stacks!", diff --git a/src/locales/ko/splash-messages.json b/src/locales/ko/splash-messages.json index 6cf7ce050b7..1e89713ccde 100644 --- a/src/locales/ko/splash-messages.json +++ b/src/locales/ko/splash-messages.json @@ -1,5 +1,5 @@ { - "battlesWon": "전투에서 승리하세요!", + "battlesWon": "{{count, number}} 전투에서 승리하세요!", "joinTheDiscord": "디스코드에 가입하세요!", "infiniteLevels": "무한한 레벨!", "everythingStacks": "모든 것이 누적됩니다!", diff --git a/src/locales/pt_BR/splash-messages.json b/src/locales/pt_BR/splash-messages.json index 55c0b1b9e74..237b0f21202 100644 --- a/src/locales/pt_BR/splash-messages.json +++ b/src/locales/pt_BR/splash-messages.json @@ -1,5 +1,5 @@ { - "battlesWon": "Batalhas Ganhas!", + "battlesWon": "{{count, number}} Batalhas Ganhas!", "joinTheDiscord": "Junte-se ao Discord!", "infiniteLevels": "Níveis Infinitos!", "everythingStacks": "Tudo Acumula!", diff --git a/src/locales/zh_CN/splash-messages.json b/src/locales/zh_CN/splash-messages.json index 4d2d208edfd..24981513afe 100644 --- a/src/locales/zh_CN/splash-messages.json +++ b/src/locales/zh_CN/splash-messages.json @@ -1,5 +1,5 @@ { - "battlesWon": "场胜利!", + "battlesWon": "{{count, number}} 场胜利!", "joinTheDiscord": "加入Discord!", "infiniteLevels": "等级无限!", "everythingStacks": "道具全部叠加!", diff --git a/src/locales/zh_TW/splash-messages.json b/src/locales/zh_TW/splash-messages.json index a25e7dab97b..60b03549c2f 100644 --- a/src/locales/zh_TW/splash-messages.json +++ b/src/locales/zh_TW/splash-messages.json @@ -1,5 +1,5 @@ { - "battlesWon": "勝利場數!", + "battlesWon": "{{count, number}} 勝利場數!", "joinTheDiscord": "加入Discord!", "infiniteLevels": "無限等級!", "everythingStacks": "所有效果都能疊加!", diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index a23a9c5ece2..ce2ffc6a462 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -1761,7 +1761,7 @@ const modifierPool: ModifierPool = { new WeightedModifierType(modifierTypes.ABILITY_CHARM, skipInClassicAfterWave(189, 6)), new WeightedModifierType(modifierTypes.FOCUS_BAND, 5), 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.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), diff --git a/src/overrides.ts b/src/overrides.ts index ec0577ceb3d..35ca299721b 100644 --- a/src/overrides.ts +++ b/src/overrides.ts @@ -6,7 +6,7 @@ import { PokeballType } from "#enums/pokeball"; import { Species } from "#enums/species"; import { StatusEffect } from "#enums/status-effect"; 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 { type PokeballCounts } from "./battle-scene"; import { Gender } from "./data/gender"; diff --git a/src/phases/game-over-phase.ts b/src/phases/game-over-phase.ts index 438ff1aecb0..e6ccca6c95a 100644 --- a/src/phases/game-over-phase.ts +++ b/src/phases/game-over-phase.ts @@ -243,7 +243,7 @@ export class GameOverPhase extends BattlePhase { gameVersion: this.scene.game.config.gameVersion, timestamp: new Date().getTime(), 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 } as SessionSaveData; } diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index e2fca951b2f..c3199166e84 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -4,7 +4,7 @@ import { applyPreAttackAbAttrs, AddSecondStrikeAbAttr, IgnoreMoveEffectsAbAttr, import { ArenaTagSide, ConditionalProtectTag } from "#app/data/arena-tag"; import { MoveAnim } from "#app/data/battle-anims"; 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 { BattlerTagType } from "#app/enums/battler-tag-type"; import { Moves } from "#app/enums/moves"; @@ -394,7 +394,7 @@ export class MoveEffectPhase extends PokemonPhase { } 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; } diff --git a/src/phases/victory-phase.ts b/src/phases/victory-phase.ts index c11dd80b3aa..c10adc5683d 100644 --- a/src/phases/victory-phase.ts +++ b/src/phases/victory-phase.ts @@ -1,6 +1,6 @@ import BattleScene from "#app/battle-scene"; -import { BattlerIndex, BattleType } from "#app/battle"; -import { modifierTypes } from "#app/modifier/modifier-type"; +import { BattlerIndex, BattleType, ClassicFixedBossWaves } from "#app/battle"; +import { CustomModifierSettings, modifierTypes } from "#app/modifier/modifier-type"; import { BattleEndPhase } from "./battle-end-phase"; import { NewBattlePhase } from "./new-battle-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)) { 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) { - this.scene.pushPhase(new SelectModifierPhase(this.scene)); + this.scene.pushPhase(new SelectModifierPhase(this.scene, undefined, undefined, this.getFixedBattleCustomModifiers())); } else if (this.scene.gameMode.isDaily) { this.scene.pushPhase(new ModifierRewardPhase(this.scene, modifierTypes.EXP_CHARM)); 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(); } + + /** + * 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; + } } diff --git a/src/system/achv.ts b/src/system/achv.ts index 6170fe23e1d..09ec74de50c 100644 --- a/src/system/achv.ts +++ b/src/system/achv.ts @@ -279,6 +279,8 @@ export function getAchievementDescription(localizationKey: string): string { return i18next.t("achv:FRESH_START.description", { context: genderStr }); case "INVERSE_BATTLE": return i18next.t("achv:INVERSE_BATTLE.description", { context: genderStr }); + case "BREEDERS_IN_SPACE": + return i18next.t("achv:BREEDERS_IN_SPACE.description", { context: genderStr }); default: 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)), 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), + BREEDERS_IN_SPACE: new Achv("BREEDERS_IN_SPACE", "", "BREEDERS_IN_SPACE.description", "moon_stone", 100).setSecret(), }; export function initAchievements() { diff --git a/src/system/egg-data.ts b/src/system/egg-data.ts index 785ae364efe..1c9c903688a 100644 --- a/src/system/egg-data.ts +++ b/src/system/egg-data.ts @@ -1,6 +1,6 @@ import { EggTier } from "#enums/egg-type"; 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 { EggSourceType } from "#app/enums/egg-source-types"; diff --git a/src/test/data/splash_messages.test.ts b/src/test/data/splash_messages.test.ts new file mode 100644 index 00000000000..7e07b9a6e77 --- /dev/null +++ b/src/test/data/splash_messages.test.ts @@ -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); +} diff --git a/src/test/eggs/egg.test.ts b/src/test/eggs/egg.test.ts index 4f00e843b47..053ff8f1112 100644 --- a/src/test/eggs/egg.test.ts +++ b/src/test/eggs/egg.test.ts @@ -1,7 +1,7 @@ import { Egg, getLegendaryGachaSpeciesForTimestamp } from "#app/data/egg"; import { EggSourceType } from "#app/enums/egg-source-types"; 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 * as Utils from "#app/utils"; import { Species } from "#enums/species"; @@ -136,9 +136,9 @@ describe("Egg Generation Tests", () => { expect(result).toBe(expectedResult); }); - it("should return a shiny common variant", () => { + it("should return a shiny standard variant", () => { 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; diff --git a/src/test/moves/shell_side_arm.test.ts b/src/test/moves/shell_side_arm.test.ts new file mode 100644 index 00000000000..38d2556a1a2 --- /dev/null +++ b/src/test/moves/shell_side_arm.test.ts @@ -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); +}); diff --git a/src/test/mystery-encounter/encounters/a-trainers-test-encounter.test.ts b/src/test/mystery-encounter/encounters/a-trainers-test-encounter.test.ts index b4cc186864c..3dc90427eb2 100644 --- a/src/test/mystery-encounter/encounters/a-trainers-test-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/a-trainers-test-encounter.test.ts @@ -69,22 +69,6 @@ describe("A Trainer's Test - Mystery Encounter", () => { 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 () => { initSceneWithoutEncounterPhase(scene, defaultParty); scene.currentBattle.mysteryEncounter = ATrainersTestEncounter; diff --git a/src/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts b/src/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts index 7cca7abba27..58c8e1fbc30 100644 --- a/src/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts @@ -66,22 +66,6 @@ describe("Absolute Avarice - Mystery Encounter", () => { 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 () => { game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT); game.override.startingBiome(Biome.VOLCANO); diff --git a/src/test/mystery-encounter/encounters/an-offer-you-cant-refuse-encounter.test.ts b/src/test/mystery-encounter/encounters/an-offer-you-cant-refuse-encounter.test.ts index 1c68852a63d..88704746a3c 100644 --- a/src/test/mystery-encounter/encounters/an-offer-you-cant-refuse-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/an-offer-you-cant-refuse-encounter.test.ts @@ -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); }); - 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 () => { initSceneWithoutEncounterPhase(scene, defaultParty); scene.currentBattle.mysteryEncounter = AnOfferYouCantRefuseEncounter; diff --git a/src/test/mystery-encounter/encounters/berries-abound-encounter.test.ts b/src/test/mystery-encounter/encounters/berries-abound-encounter.test.ts index 5d30bc91de9..af5c0f0b48b 100644 --- a/src/test/mystery-encounter/encounters/berries-abound-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/berries-abound-encounter.test.ts @@ -69,22 +69,6 @@ describe("Berries Abound - Mystery Encounter", () => { 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 () => { initSceneWithoutEncounterPhase(scene, defaultParty); scene.currentBattle.mysteryEncounter = BerriesAboundEncounter; diff --git a/src/test/mystery-encounter/encounters/bug-type-superfan-encounter.test.ts b/src/test/mystery-encounter/encounters/bug-type-superfan-encounter.test.ts index 70adf93d502..247acc9e5b6 100644 --- a/src/test/mystery-encounter/encounters/bug-type-superfan-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/bug-type-superfan-encounter.test.ts @@ -201,22 +201,6 @@ describe("Bug-Type Superfan - Mystery Encounter", () => { 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 () => { initSceneWithoutEncounterPhase(scene, defaultParty); scene.currentBattle.mysteryEncounter = BugTypeSuperfanEncounter; diff --git a/src/test/mystery-encounter/encounters/clowning-around-encounter.test.ts b/src/test/mystery-encounter/encounters/clowning-around-encounter.test.ts index 383e3bd3564..5ed5a9487de 100644 --- a/src/test/mystery-encounter/encounters/clowning-around-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/clowning-around-encounter.test.ts @@ -95,14 +95,6 @@ describe("Clowning Around - Mystery Encounter", () => { 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 () => { initSceneWithoutEncounterPhase(scene, defaultParty); scene.currentBattle.mysteryEncounter = ClowningAroundEncounter; diff --git a/src/test/mystery-encounter/encounters/dancing-lessons-encounter.test.ts b/src/test/mystery-encounter/encounters/dancing-lessons-encounter.test.ts index 5a2512ddaf6..cbf8251f2e7 100644 --- a/src/test/mystery-encounter/encounters/dancing-lessons-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/dancing-lessons-encounter.test.ts @@ -69,22 +69,6 @@ describe("Dancing Lessons - Mystery Encounter", () => { 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 () => { game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT); game.override.startingBiome(Biome.SPACE); diff --git a/src/test/mystery-encounter/encounters/delibirdy-encounter.test.ts b/src/test/mystery-encounter/encounters/delibirdy-encounter.test.ts index 969188dca06..7e452fd90c7 100644 --- a/src/test/mystery-encounter/encounters/delibirdy-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/delibirdy-encounter.test.ts @@ -66,22 +66,6 @@ describe("Delibird-y - Mystery Encounter", () => { 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 () => { scene.money = 0; diff --git a/src/test/mystery-encounter/encounters/department-store-sale-encounter.test.ts b/src/test/mystery-encounter/encounters/department-store-sale-encounter.test.ts index f22bd832964..0b2d66db20b 100644 --- a/src/test/mystery-encounter/encounters/department-store-sale-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/department-store-sale-encounter.test.ts @@ -79,22 +79,6 @@ describe("Department Store Sale - Mystery Encounter", () => { 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", () => { it("should have the correct properties", () => { const option = DepartmentStoreSaleEncounter.options[0]; diff --git a/src/test/mystery-encounter/encounters/field-trip-encounter.test.ts b/src/test/mystery-encounter/encounters/field-trip-encounter.test.ts index 7a8d951c5da..13550abb97c 100644 --- a/src/test/mystery-encounter/encounters/field-trip-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/field-trip-encounter.test.ts @@ -72,22 +72,6 @@ describe("Field Trip - Mystery Encounter", () => { 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", () => { it("should have the correct properties", () => { const option = FieldTripEncounter.options[0]; diff --git a/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts b/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts index 445ab4491a4..cd11aa2628b 100644 --- a/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts @@ -88,14 +88,6 @@ describe("Fiery Fallout - Mystery Encounter", () => { 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 () => { initSceneWithoutEncounterPhase(scene, defaultParty); scene.currentBattle.mysteryEncounter = FieryFalloutEncounter; diff --git a/src/test/mystery-encounter/encounters/fight-or-flight-encounter.test.ts b/src/test/mystery-encounter/encounters/fight-or-flight-encounter.test.ts index 86d79b57f80..df2f32231ba 100644 --- a/src/test/mystery-encounter/encounters/fight-or-flight-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/fight-or-flight-encounter.test.ts @@ -67,22 +67,6 @@ describe("Fight or Flight - Mystery Encounter", () => { 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 () => { initSceneWithoutEncounterPhase(scene, defaultParty); scene.currentBattle.mysteryEncounter = FightOrFlightEncounter; diff --git a/src/test/mystery-encounter/encounters/fun-and-games-encounter.test.ts b/src/test/mystery-encounter/encounters/fun-and-games-encounter.test.ts index 70250350af4..c337556728b 100644 --- a/src/test/mystery-encounter/encounters/fun-and-games-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/fun-and-games-encounter.test.ts @@ -85,22 +85,6 @@ describe("Fun And Games! - Mystery Encounter", () => { 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 () => { initSceneWithoutEncounterPhase(scene, defaultParty); scene.currentBattle.mysteryEncounter = new MysteryEncounter(FunAndGamesEncounter); diff --git a/src/test/mystery-encounter/encounters/global-trade-system-encounter.test.ts b/src/test/mystery-encounter/encounters/global-trade-system-encounter.test.ts index e91b936cb9d..5a99b0450ca 100644 --- a/src/test/mystery-encounter/encounters/global-trade-system-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/global-trade-system-encounter.test.ts @@ -69,22 +69,6 @@ describe("Global Trade System - Mystery Encounter", () => { 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 () => { game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON); game.override.startingBiome(Biome.VOLCANO); diff --git a/src/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts b/src/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts index 82670e32daa..02872334fac 100644 --- a/src/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts @@ -74,22 +74,6 @@ describe("Lost at Sea - Mystery Encounter", () => { 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", () => { initSceneWithoutEncounterPhase(scene, defaultParty); scene.currentBattle.mysteryEncounter = LostAtSeaEncounter; diff --git a/src/test/mystery-encounter/encounters/mysterious-challengers-encounter.test.ts b/src/test/mystery-encounter/encounters/mysterious-challengers-encounter.test.ts index 2c0d48e59c6..15cd3338fff 100644 --- a/src/test/mystery-encounter/encounters/mysterious-challengers-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/mysterious-challengers-encounter.test.ts @@ -79,22 +79,6 @@ describe("Mysterious Challengers - Mystery Encounter", () => { 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 () => { initSceneWithoutEncounterPhase(scene, defaultParty); scene.currentBattle.mysteryEncounter = new MysteryEncounter(MysteriousChallengersEncounter); diff --git a/src/test/mystery-encounter/encounters/part-timer-encounter.test.ts b/src/test/mystery-encounter/encounters/part-timer-encounter.test.ts index f73c1f437d0..061b6a61461 100644 --- a/src/test/mystery-encounter/encounters/part-timer-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/part-timer-encounter.test.ts @@ -80,22 +80,6 @@ describe("Part-Timer - Mystery Encounter", () => { 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", () => { it("should have the correct properties", () => { const option = PartTimerEncounter.options[0]; diff --git a/src/test/mystery-encounter/encounters/teleporting-hijinks-encounter.test.ts b/src/test/mystery-encounter/encounters/teleporting-hijinks-encounter.test.ts index 2505f28aa43..f7f96de3af3 100644 --- a/src/test/mystery-encounter/encounters/teleporting-hijinks-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/teleporting-hijinks-encounter.test.ts @@ -67,22 +67,6 @@ describe("Teleporting Hijinks - Mystery Encounter", () => { 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 () => { game.override.startingWave(11); game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON); diff --git a/src/test/mystery-encounter/encounters/the-expert-breeder-encounter.test.ts b/src/test/mystery-encounter/encounters/the-expert-breeder-encounter.test.ts new file mode 100644 index 00000000000..59765148ead --- /dev/null +++ b/src/test/mystery-encounter/encounters/the-expert-breeder-encounter.test.ts @@ -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.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 + }); + }); +}); diff --git a/src/test/mystery-encounter/encounters/the-pokemon-salesman-encounter.test.ts b/src/test/mystery-encounter/encounters/the-pokemon-salesman-encounter.test.ts index c43577337da..d37a9d8fb7b 100644 --- a/src/test/mystery-encounter/encounters/the-pokemon-salesman-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/the-pokemon-salesman-encounter.test.ts @@ -76,22 +76,6 @@ describe("The Pokemon Salesman - Mystery Encounter", () => { 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 () => { initSceneWithoutEncounterPhase(scene, defaultParty); scene.currentBattle.mysteryEncounter = ThePokemonSalesmanEncounter; diff --git a/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts b/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts index ef80271c423..5c1353ee337 100644 --- a/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts @@ -83,22 +83,6 @@ describe("The Strong Stuff - Mystery Encounter", () => { 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 () => { initSceneWithoutEncounterPhase(scene, defaultParty); scene.currentBattle.mysteryEncounter = TheStrongStuffEncounter; diff --git a/src/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts b/src/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts index 0c642225031..1efe6dbd7f8 100644 --- a/src/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts @@ -90,22 +90,6 @@ describe("The Winstrate Challenge - Mystery Encounter", () => { 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 () => { initSceneWithoutEncounterPhase(scene, defaultParty); scene.currentBattle.mysteryEncounter = new MysteryEncounter(TheWinstrateChallengeEncounter); diff --git a/src/test/mystery-encounter/encounters/trash-to-treasure-encounter.test.ts b/src/test/mystery-encounter/encounters/trash-to-treasure-encounter.test.ts index 0f89e90031c..bfeb249543f 100644 --- a/src/test/mystery-encounter/encounters/trash-to-treasure-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/trash-to-treasure-encounter.test.ts @@ -71,22 +71,6 @@ describe("Trash to Treasure - Mystery Encounter", () => { 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 () => { initSceneWithoutEncounterPhase(scene, defaultParty); scene.currentBattle.mysteryEncounter = TrashToTreasureEncounter; diff --git a/src/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts b/src/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts index 5b17ff3a2f7..2f8c4e5111a 100644 --- a/src/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts @@ -74,22 +74,6 @@ describe("Uncommon Breed - Mystery Encounter", () => { 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 () => { initSceneWithoutEncounterPhase(scene, defaultParty); scene.currentBattle.mysteryEncounter = UncommonBreedEncounter; diff --git a/src/test/mystery-encounter/encounters/weird-dream-encounter.test.ts b/src/test/mystery-encounter/encounters/weird-dream-encounter.test.ts index ef014c6949b..e532891810c 100644 --- a/src/test/mystery-encounter/encounters/weird-dream-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/weird-dream-encounter.test.ts @@ -73,22 +73,6 @@ describe("Weird Dream - Mystery Encounter", () => { 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 () => { initSceneWithoutEncounterPhase(scene, defaultParty); scene.currentBattle.mysteryEncounter = WeirdDreamEncounter; diff --git a/src/test/mystery-encounter/mystery-encounter.test.ts b/src/test/mystery-encounter/mystery-encounter.test.ts index d2a2e7f9d92..38c999f8aac 100644 --- a/src/test/mystery-encounter/mystery-encounter.test.ts +++ b/src/test/mystery-encounter/mystery-encounter.test.ts @@ -4,10 +4,12 @@ import Phaser from "phaser"; import { Species } from "#enums/species"; import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import BattleScene from "#app/battle-scene"; describe("Mystery Encounters", () => { let phaserGame: Phaser.Game; let game: GameManager; + let scene: BattleScene; beforeAll(() => { phaserGame = new Phaser.Game({ @@ -21,6 +23,7 @@ describe("Mystery Encounters", () => { beforeEach(() => { game = new GameManager(phaserGame); + scene = game.scene; game.override.startingWave(11); game.override.mysteryEncounterChance(100); }); @@ -32,23 +35,20 @@ describe("Mystery Encounters", () => { expect(game.scene.getCurrentPhase()!.constructor.name).toBe(MysteryEncounterPhase.name); }); - it("", async () => { - await game.runToMysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS, [Species.CHARIZARD, Species.VOLCARONA]); + it("Encounters should not run below wave 10", async () => { + game.override.startingWave(9); - await game.phaseInterceptor.to(MysteryEncounterPhase, false); - expect(game.scene.getCurrentPhase()!.constructor.name).toBe(MysteryEncounterPhase.name); + await game.runToMysteryEncounter(); + + 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 () => { - }); - - it("spawns fight or flight encounter", async () => { + expect(scene.currentBattle.mysteryEncounter).toBeUndefined(); }); }); diff --git a/src/test/vitest.setup.ts b/src/test/vitest.setup.ts index 3bb5c240d94..74129f20d26 100644 --- a/src/test/vitest.setup.ts +++ b/src/test/vitest.setup.ts @@ -14,6 +14,8 @@ import { initStatsKeys } from "#app/ui/game-stats-ui-handler"; import { initMysteryEncounters } from "#app/data/mystery-encounters/mystery-encounters"; import { beforeAll, vi } from "vitest"; +process.env.TZ = "UTC"; + /** Mock the override import to always return default values, ignoring any custom overrides. */ vi.mock("#app/overrides", async (importOriginal) => { const { defaultOverrides } = await importOriginal(); diff --git a/src/ui/abstact-option-select-ui-handler.ts b/src/ui/abstact-option-select-ui-handler.ts index c6abecda4c0..740830595ed 100644 --- a/src/ui/abstact-option-select-ui-handler.ts +++ b/src/ui/abstact-option-select-ui-handler.ts @@ -344,6 +344,7 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler { super.clear(); this.config = null; this.optionSelectContainer.setVisible(false); + this.scrollCursor = 0; this.eraseCursor(); } diff --git a/src/ui/achvs-ui-handler.ts b/src/ui/achvs-ui-handler.ts index 605b8c538a9..491c4acb523 100644 --- a/src/ui/achvs-ui-handler.ts +++ b/src/ui/achvs-ui-handler.ts @@ -1,12 +1,13 @@ -import BattleScene from "../battle-scene"; +import BattleScene from "#app/battle-scene"; import { Button } from "#enums/buttons"; import i18next from "i18next"; -import { Achv, achvs, getAchievementDescription } from "../system/achv"; -import { Voucher, getVoucherTypeIcon, getVoucherTypeName, vouchers } from "../system/voucher"; -import MessageUiHandler from "./message-ui-handler"; -import { addTextObject, TextStyle } from "./text"; -import { Mode } from "./ui"; -import { addWindow } from "./ui-theme"; +import { Achv, achvs, getAchievementDescription } from "#app/system/achv"; +import { Voucher, getVoucherTypeIcon, getVoucherTypeName, vouchers } from "#app/system/voucher"; +import MessageUiHandler from "#app/ui/message-ui-handler"; +import { addTextObject, TextStyle } from "#app/ui/text"; +import { Mode } from "#app/ui/ui"; +import { addWindow } from "#app/ui/ui-theme"; +import { ScrollBar } from "#app/ui/scroll-bar"; import { PlayerGender } from "#enums/player-gender"; enum Page { @@ -49,6 +50,7 @@ export default class AchvsUiHandler extends MessageUiHandler { private vouchersTotal: number; private currentTotal: number; + private scrollBar: ScrollBar; private scrollCursor: number; private cursorObj: Phaser.GameObjects.NineSlice | null; 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.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 = []; @@ -148,6 +153,7 @@ export default class AchvsUiHandler extends MessageUiHandler { this.mainContainer.add(this.headerText); this.mainContainer.add(this.headerActionText); this.mainContainer.add(this.iconsBg); + this.mainContainer.add(this.scrollBar); this.mainContainer.add(this.iconsContainer); this.mainContainer.add(titleBg); this.mainContainer.add(this.titleText); @@ -162,6 +168,7 @@ export default class AchvsUiHandler extends MessageUiHandler { this.currentPage = Page.ACHIEVEMENTS; this.setCursor(0); + this.setScrollCursor(0); this.mainContainer.setVisible(false); } @@ -175,6 +182,8 @@ export default class AchvsUiHandler extends MessageUiHandler { this.mainContainer.setVisible(true); this.setCursor(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); @@ -224,6 +233,8 @@ export default class AchvsUiHandler extends MessageUiHandler { this.updateAchvIcons(); } this.setCursor(0, true); + this.scrollBar.setTotalRows(Math.ceil(this.currentTotal / this.COLS)); + this.scrollBar.setScrollCursor(0); this.mainContainer.update(); } if (button === Button.CANCEL) { @@ -237,32 +248,44 @@ export default class AchvsUiHandler extends MessageUiHandler { if (this.cursor < this.COLS) { if (this.scrollCursor) { 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 { success = this.setCursor(this.cursor - this.COLS); } break; case Button.DOWN: - const canMoveDown = (this.cursor + itemOffset) + this.COLS < this.currentTotal; + const canMoveDown = itemOffset + 1 < this.currentTotal; if (rowIndex >= this.ROWS - 1) { if (this.scrollCursor < Math.ceil(this.currentTotal / this.COLS) - this.ROWS && canMoveDown) { + // scroll down one row 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) { - success = this.setCursor(this.cursor + this.COLS); + success = this.setCursor(Math.min(this.cursor + this.COLS, this.currentTotal - itemOffset - 1)); } break; case Button.LEFT: - if (!this.cursor && this.scrollCursor) { - success = this.setScrollCursor(this.scrollCursor - 1) && this.setCursor(this.cursor + (this.COLS - 1)); - } else if (this.cursor) { + if (this.cursor % this.COLS === 0) { + success = this.setCursor(Math.min(this.cursor + this.COLS - 1, this.currentTotal - itemOffset - 1)); + } else { success = this.setCursor(this.cursor - 1); } break; case Button.RIGHT: - if (this.cursor + 1 === this.ROWS * this.COLS && this.scrollCursor < Math.ceil(this.currentTotal / this.COLS) - this.ROWS) { - success = this.setScrollCursor(this.scrollCursor + 1) && this.setCursor(this.cursor - (this.COLS - 1)); - } else if (this.cursor + itemOffset < this.currentTotal - 1) { + if ((this.cursor + 1) % this.COLS === 0 || (this.cursor + itemOffset) === (this.currentTotal - 1)) { + success = this.setCursor(this.cursor - this.cursor % this.COLS); + } else { success = this.setCursor(this.cursor + 1); } break; @@ -315,15 +338,22 @@ export default class AchvsUiHandler extends MessageUiHandler { } 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) { case Page.ACHIEVEMENTS: 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; case Page.VOUCHERS: 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; } return true; @@ -411,6 +441,7 @@ export default class AchvsUiHandler extends MessageUiHandler { super.clear(); this.currentPage = Page.ACHIEVEMENTS; this.mainContainer.setVisible(false); + this.setScrollCursor(0); this.eraseCursor(); } diff --git a/src/ui/run-history-ui-handler.ts b/src/ui/run-history-ui-handler.ts index 8f132a1ab1c..220e386c1b4 100644 --- a/src/ui/run-history-ui-handler.ts +++ b/src/ui/run-history-ui-handler.ts @@ -281,7 +281,7 @@ class RunEntryContainer extends Phaser.GameObjects.Container { const genderIndex = this.scene.gameData.gender ?? PlayerGender.UNSET; const genderStr = PlayerGender[genderIndex].toLowerCase(); // 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 gameOutcomeLabel = addTextObject(this.scene, 0, 0, `${i18next.t("runHistory:defeatedWild", { context: genderStr })}`, TextStyle.WINDOW); enemyContainer.add(gameOutcomeLabel); @@ -302,7 +302,7 @@ class RunEntryContainer extends Phaser.GameObjects.Container { enemy.destroy(); }); 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); // Because of the interesting mechanics behind rival names, the rival name and title have to be retrieved differently const RIVAL_TRAINER_ID_THRESHOLD = 375; diff --git a/src/ui/run-info-ui-handler.ts b/src/ui/run-info-ui-handler.ts index 8f0437002d4..8b06e68b112 100644 --- a/src/ui/run-info-ui-handler.ts +++ b/src/ui/run-info-ui-handler.ts @@ -211,7 +211,7 @@ export default class RunInfoUiHandler extends UiHandler { if (!this.isVictory) { const enemyContainer = this.scene.add.container(0, 0); // 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) { case 1: // Wild - Singles @@ -222,7 +222,7 @@ export default class RunInfoUiHandler extends UiHandler { this.parseWildDoubleDefeat(enemyContainer); 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.runResultContainer.add(enemyContainer); @@ -381,10 +381,6 @@ export default class RunInfoUiHandler extends UiHandler { break; case GameModes.SPLICED_ENDLESS: 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; case GameModes.CHALLENGE: modeText.appendText(`${i18next.t("gameMode:challenge")}`, false); @@ -403,17 +399,18 @@ export default class RunInfoUiHandler extends UiHandler { break; case GameModes.ENDLESS: 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; case GameModes.CLASSIC: modeText.appendText(`${i18next.t("gameMode:classic")}`, false); 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 const runInfoTextContainer = this.scene.add.container(0, 0); // Japanese is set to a greater line spacing of 35px in addBBCodeTextObject() if lineSpacing < 12. diff --git a/src/ui/scroll-bar.ts b/src/ui/scroll-bar.ts index e756393ae1a..5ed79d0cdad 100644 --- a/src/ui/scroll-bar.ts +++ b/src/ui/scroll-bar.ts @@ -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 { - private bg: Phaser.GameObjects.Image; + private bg: Phaser.GameObjects.NineSlice; private handleBody: Phaser.GameObjects.Rectangle; - private handleBottom: Phaser.GameObjects.Image; - private pages: number; - private page: number; + private handleBottom: Phaser.GameObjects.NineSlice; + private currentRow: 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); - 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.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.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.add(this.handleBottom); } - setPage(page: number): void { - this.page = page; - this.handleBody.y = 1 + (this.bg.displayHeight - 1 - this.handleBottom.displayHeight) / this.pages * page; + /** + * Set the current row that is displayed + * 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; } - setPages(pages: number): void { - this.pages = pages; - this.handleBody.height = (this.bg.displayHeight - 1 - this.handleBottom.displayHeight) * 9 / this.pages; + /** + * Set the total number of rows to display + * 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); } } diff --git a/src/ui/settings/abstract-control-settings-ui-handler.ts b/src/ui/settings/abstract-control-settings-ui-handler.ts index f8dab1bf7cc..efa262bb2e9 100644 --- a/src/ui/settings/abstract-control-settings-ui-handler.ts +++ b/src/ui/settings/abstract-control-settings-ui-handler.ts @@ -1,11 +1,12 @@ -import UiHandler from "../ui-handler"; -import BattleScene from "../../battle-scene"; -import {Mode} from "../ui"; -import {InterfaceConfig} from "../../inputs-controller"; -import {addWindow} from "../ui-theme"; -import {addTextObject, TextStyle} from "../text"; -import {getIconWithSettingName} from "#app/configs/inputs/configHandler"; -import NavigationMenu, {NavigationManager} from "#app/ui/settings/navigationMenu"; +import UiHandler from "#app/ui/ui-handler"; +import BattleScene from "#app/battle-scene"; +import { Mode } from "#app/ui/ui"; +import { InterfaceConfig } from "#app/inputs-controller"; +import { addWindow } from "#app/ui/ui-theme"; +import { addTextObject, TextStyle } from "#app/ui/text"; +import { ScrollBar } from "#app/ui/scroll-bar"; +import { getIconWithSettingName } from "#app/configs/inputs/configHandler"; +import NavigationMenu, { NavigationManager } from "#app/ui/settings/navigationMenu"; import { Device } from "#enums/devices"; import { Button } from "#enums/buttons"; import i18next from "i18next"; @@ -19,7 +20,7 @@ export interface LayoutConfig { inputsIcons: InputsIcons; settingLabels: Phaser.GameObjects.Text[]; optionValueLabels: Phaser.GameObjects.Text[][]; - optionCursors: integer[]; + optionCursors: number[]; keys: string[]; bindingSettings: Array; } @@ -31,8 +32,9 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler protected optionsContainer: Phaser.GameObjects.Container; protected navigationContainer: NavigationMenu; - protected scrollCursor: integer; - protected optionCursors: integer[]; + protected scrollBar: ScrollBar; + protected scrollCursor: number; + protected optionCursors: number[]; protected cursorObj: Phaser.GameObjects.NineSlice | null; protected optionsBg: Phaser.GameObjects.NineSlice; @@ -65,7 +67,7 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler protected device: Device; 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. @@ -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 // 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 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. 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. ui.add(this.settingsContainer); @@ -413,6 +420,8 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler this.optionCursors = layout.optionCursors; this.inputsIcons = layout.inputsIcons; this.bindingSettings = layout.bindingSettings; + this.scrollBar.setTotalRows(layout.settingLabels.length); + this.scrollBar.setScrollCursor(0); // Return true indicating the layout was successfully applied. return true; @@ -538,7 +547,7 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler * @param cursor - The cursor position to set. * @returns `true` if the cursor was set successfully. */ - setCursor(cursor: integer): boolean { + setCursor(cursor: number): boolean { const ret = super.setCursor(cursor); // If the optionsContainer is not initialized, return the result from the parent class directly. if (!this.optionsContainer) { @@ -547,7 +556,8 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler // Check if the cursor object exists, if not, create it. 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.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. * @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. if (scrollCursor === this.scrollCursor) { return false; @@ -572,6 +582,7 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler // Update the internal scroll cursor state this.scrollCursor = scrollCursor; + this.scrollBar.setScrollCursor(this.scrollCursor); // Apply the new scroll position to the settings UI. this.updateSettingsScroll(); @@ -590,7 +601,7 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler * @param save - Whether to save the setting to local storage. * @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. const setting = this.setting[Object.keys(this.setting)[settingIndex]]; diff --git a/src/ui/settings/abstract-settings-ui-handler.ts b/src/ui/settings/abstract-settings-ui-handler.ts index 570377eab43..975a32127ff 100644 --- a/src/ui/settings/abstract-settings-ui-handler.ts +++ b/src/ui/settings/abstract-settings-ui-handler.ts @@ -1,12 +1,13 @@ -import BattleScene from "../../battle-scene"; -import { hasTouchscreen, isMobile } from "../../touch-controls"; -import { TextStyle, addTextObject } from "../text"; -import { Mode } from "../ui"; -import UiHandler from "../ui-handler"; -import { addWindow } from "../ui-theme"; -import {Button} from "#enums/buttons"; -import {InputsIcons} from "#app/ui/settings/abstract-control-settings-ui-handler"; -import NavigationMenu, {NavigationManager} from "#app/ui/settings/navigationMenu"; +import BattleScene from "#app/battle-scene"; +import { hasTouchscreen, isMobile } from "#app/touch-controls"; +import { TextStyle, addTextObject } from "#app/ui/text"; +import { Mode } from "#app/ui/ui"; +import UiHandler from "#app/ui/ui-handler"; +import { addWindow } from "#app/ui/ui-theme"; +import { ScrollBar } from "#app/ui/scroll-bar"; +import { Button } from "#enums/buttons"; +import { InputsIcons } from "#app/ui/settings/abstract-control-settings-ui-handler"; +import NavigationMenu, { NavigationManager } from "#app/ui/settings/navigationMenu"; import { Setting, SettingKeys, SettingType } from "#app/system/settings/settings"; import i18next from "i18next"; @@ -19,11 +20,12 @@ export default class AbstractSettingsUiHandler extends UiHandler { private optionsContainer: Phaser.GameObjects.Container; private navigationContainer: NavigationMenu; - private scrollCursor: integer; + private scrollCursor: number; + private scrollBar: ScrollBar; private optionsBg: Phaser.GameObjects.NineSlice; - private optionCursors: integer[]; + private optionCursors: number[]; private settingLabels: 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 totalSpace = (300 - labelWidth) - totalWidth / 6; + const totalSpace = (297 - labelWidth) - totalWidth / 6; const optionSpacing = Math.floor(totalSpace / (this.optionValueLabels[s].length - 1)); let xOffset = 0; @@ -130,7 +132,11 @@ export default class AbstractSettingsUiHandler extends UiHandler { 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.scrollBar); this.settingsContainer.add(this.navigationContainer); this.settingsContainer.add(actionsBg); this.settingsContainer.add(this.optionsContainer); @@ -186,6 +192,7 @@ export default class AbstractSettingsUiHandler extends UiHandler { this.settingsContainer.setVisible(true); this.setCursor(0); + this.setScrollCursor(0); 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. * @returns `true` if the cursor was set successfully. */ - setCursor(cursor: integer): boolean { + setCursor(cursor: number): boolean { const ret = super.setCursor(cursor); 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.optionsContainer.add(this.cursorObj); } @@ -323,7 +331,7 @@ export default class AbstractSettingsUiHandler extends UiHandler { * @param save - Whether to save the setting to local storage. * @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]; 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. * @returns `true` if the scroll cursor was set successfully. */ - setScrollCursor(scrollCursor: integer): boolean { + setScrollCursor(scrollCursor: number): boolean { if (scrollCursor === this.scrollCursor) { return false; } this.scrollCursor = scrollCursor; + this.scrollBar.setScrollCursor(this.scrollCursor); this.updateSettingsScroll(); @@ -394,6 +403,7 @@ export default class AbstractSettingsUiHandler extends UiHandler { clear() { super.clear(); this.settingsContainer.setVisible(false); + this.setScrollCursor(0); this.eraseCursor(); this.getUi().bgmBar.toggleBgmBar(this.scene.showBgmBar); if (this.reloadRequired) { diff --git a/src/ui/starter-select-ui-handler.ts b/src/ui/starter-select-ui-handler.ts index 0e101d30ce7..5ef26d1ba88 100644 --- a/src/ui/starter-select-ui-handler.ts +++ b/src/ui/starter-select-ui-handler.ts @@ -83,7 +83,7 @@ const languageSettings: { [key: string]: LanguageSetting } = { }, "fr":{ starterInfoTextSize: "54px", - instructionTextSize: "35px", + instructionTextSize: "38px", }, "it":{ starterInfoTextSize: "56px", @@ -627,7 +627,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { 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); @@ -2540,8 +2540,8 @@ export default class StarterSelectUiHandler extends MessageUiHandler { } }); - this.starterSelectScrollBar.setPages(Math.max(Math.ceil(this.filteredStarterContainers.length / 9), 1)); - this.starterSelectScrollBar.setPage(0); + this.starterSelectScrollBar.setTotalRows(Math.max(Math.ceil(this.filteredStarterContainers.length / 9), 1)); + this.starterSelectScrollBar.setScrollCursor(0); // sort const sort = this.filterBar.getVals(DropDownColumn.SORT)[0]; @@ -2576,7 +2576,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { const onScreenFirstIndex = this.scrollCursor * maxColumns; 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; this.filteredStarterContainers.forEach((container, i) => { diff --git a/src/ui/title-ui-handler.ts b/src/ui/title-ui-handler.ts index 67a4f7260e6..a85e670eaba 100644 --- a/src/ui/title-ui-handler.ts +++ b/src/ui/title-ui-handler.ts @@ -3,11 +3,14 @@ import OptionSelectUiHandler from "./settings/option-select-ui-handler"; import { Mode } from "./ui"; import * as Utils from "../utils"; import { TextStyle, addTextObject, getTextStyleOptions } from "./text"; -import { getBattleCountSplashMessage, getSplashMessages } from "../data/splash-messages"; +import { getSplashMessages } from "../data/splash-messages"; import i18next from "i18next"; import { TimedEventDisplay } from "#app/timed-event-manager"; 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 playerCountLabel: Phaser.GameObjects.Text; private splashMessage: string; @@ -72,8 +75,8 @@ export default class TitleUiHandler extends OptionSelectUiHandler { .then(request => request.json()) .then(stats => { this.playerCountLabel.setText(`${stats.playerCount} ${i18next.t("menu:playersOnline")}`); - if (this.splashMessage === getBattleCountSplashMessage()) { - this.splashMessageText.setText(getBattleCountSplashMessage().replace("{COUNT}", stats.battleCount.toLocaleString("en-US"))); + if (this.splashMessage === "splashMessages:battlesWon") { + this.splashMessageText.setText(i18next.t(this.splashMessage, { count: stats.battlesWon })); } }) .catch(err => { @@ -86,7 +89,7 @@ export default class TitleUiHandler extends OptionSelectUiHandler { if (ret) { 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();