From ce189c01da5cdb768db1c3ccb5da361caf790fe5 Mon Sep 17 00:00:00 2001 From: Lugiad Date: Thu, 19 Sep 2024 16:21:21 +0200 Subject: [PATCH 1/6] [Localization] French adjustments (+German) (#4323) * Update settings.json * Update move.json * Update move.json * Update starter-select-ui-handler.ts * Update party-ui-handler.json * Update move.json --- src/locales/de/move.json | 4 ++-- src/locales/fr/move.json | 4 ++-- src/locales/fr/party-ui-handler.json | 2 +- src/ui/starter-select-ui-handler.ts | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) 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/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/ui/starter-select-ui-handler.ts b/src/ui/starter-select-ui-handler.ts index 19527c9777f..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", From 714630c9de1decb1993c414ef599ee216dc3b846 Mon Sep 17 00:00:00 2001 From: Tempoanon <163687446+Tempo-anon@users.noreply.github.com> Date: Thu, 19 Sep 2024 10:25:00 -0400 Subject: [PATCH 2/6] [Refactor] Renamed the HitsTagAttr back to how it was (#4324) --- src/data/move.ts | 63 ++++++++++++++++++++------------- src/field/pokemon.ts | 8 ++--- src/phases/move-effect-phase.ts | 4 +-- 3 files changed, 44 insertions(+), 31 deletions(-) diff --git a/src/data/move.ts b/src/data/move.ts index 740bde75508..ef96ff19b07 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -4859,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; @@ -4880,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; @@ -6759,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) @@ -6777,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), @@ -6802,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), @@ -6870,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), @@ -6953,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) @@ -7353,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) @@ -7409,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), @@ -7441,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) @@ -7664,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) @@ -7896,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) @@ -8093,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), @@ -8108,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) @@ -8261,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), @@ -8279,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) @@ -8335,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) @@ -8506,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) @@ -8765,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) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 1019bcf86ab..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; } @@ -2515,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; } 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; } From c4d5c923fc549750fef9af893cc07d3c5f856270 Mon Sep 17 00:00:00 2001 From: Mumble <171087428+frutescens@users.noreply.github.com> Date: Thu, 19 Sep 2024 07:42:29 -0700 Subject: [PATCH 3/6] [Bug] Run History Not Saving Correctly (#4248) * Fix - need to test though * New Line for Personal Best * Run History basic Interpretation * Stray console log * Added personal best message fix * argh * removed a stray log --------- Co-authored-by: frutescens Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com> --- src/phases/game-over-phase.ts | 2 +- src/ui/run-history-ui-handler.ts | 4 ++-- src/ui/run-info-ui-handler.ts | 19 ++++++++----------- 3 files changed, 11 insertions(+), 14 deletions(-) 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/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. From 528e231794a30d77d0af61dc5e43f90ea964c5b5 Mon Sep 17 00:00:00 2001 From: Lugiad Date: Thu, 19 Sep 2024 21:13:37 +0200 Subject: [PATCH 4/6] [Localization] Update pkmnems font (#4329) --- public/fonts/pkmnems.ttf | Bin 18932 -> 26132 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/public/fonts/pkmnems.ttf b/public/fonts/pkmnems.ttf index 0aa3a19b417a45c24421be1d769d2286dbfdabd8..b0b50d0f10ff8a64753acb663781be30d9b0213d 100644 GIT binary patch delta 5529 zcmbtYeQ;b=6+dV9ZFWgQn$2c6Nt0&xTTv8CMBo(^M3fM}u@s6RTR=cWhe0Q1#(_U{rX!4#=`aq?AY${e{?2`OH%kF^ zba&pn_q})TIluEeUvKwqmk+(dd14|uoeq$nF6!_9#D)9v*#yy!cX79E{n~Z?G{vpB zeht^{AG_qN?1Fcl+lhD!t~YPrGdR5cx_vc7RkcLE@v8>+4Ht`$L^~$%JapCW8+QJ3 zQ_TvZ>f=NgzcI99@XCK!N6*5uKjK(AG_)f>ShFp(1OgjzBRRBZWdENwSD%UNBSglf zyRY3oIP+@w0A%6X*RyAE|1fvZay;+F`o!MBJv;i^4nIM(?L{L0Cx@@yH*#$9g&5Ib z8}>i0jDvv>zxhhvWeZmRooYhZMs(z!jVEP%y}{S|_FcR*#4%aQO-EPpjehqcU&y`O zXH@yRsbFk%5Bp>8abK-Fqs+D#ieG%e9R1%s+g+Z|eQqN@Cu>y;797Pf&>r-<12TVdn1x-{QkIGU)Rd7g9a$RAXM`IA{10 zzRX}FWPIG%l4wbFxBxlmJyxkV14COsSz- zglHWtpnA7w!$WzZFVk1(t8^Q=^i6tz9-=WCr|;AE=m+#8`U(A*9;TntBXpRK(4%ye zenyYe6ZCU>lAfkx^b9={Kw1ck3Y(G6n!*mU1pP^ki-%9u4yblM_RrFakdZ*oV z6DB{*Q-Quf+X<%Y&mQ_5W>?V7bPIihUZP)@9)OERdJ0$1&)?Cr8}#@k8l^AN*XSU9 zo$jDJ>2|uC?!mJ|bU%HYzD2)M>#ozQZ^U>sQ+1H@lkL^*>GoXvmi7bfPq$CS>*Kxg z{`eK~o8#m0m*Q_FD3MI8N{l3qBqmHUyUl)cz}#;>U_NF}m@^%99ode7j?s?sj^{dF z>v+eCS<9_Utx@Y)>o3XbWG;DWa#!-!7D8Q zXQb~>|0exTXIh0Rqb${1mUBBykb1^N>F24Tgi@_x; z@{)Oo+u$6cGYFS1pa?b5A~L~|V@FNPvBQoT4VakPVTj9t98&>CCvGz@>K`5;uy73;#8$!%P}1@Y&t>c z4o9HUsnCgOS+N2Um?-I+8Sr4p^V#M?0lJ_M@PcchZvjyR#=)pQD-0uHqa{EEy%#1*8Yd>Gg!IPlEaDh9c4t-f~y7%cYtrUDZXd>RrWOi^m_7&N1n!)DZu z+Blc_gWAOmIVo@nsyycAM%};GF3ceol-P!I9DQz^2sxUe*aq&eN$d2z&n$|x-uD33W3;cvl#Cl3vb>jbqB!- zF<@Ys70y5oVhnq+4nD>6V!yF9@OuO;L9PnSu#BQ35Vlo>z*66oa%`Y!W}DFM>GpLS z+jzE_-0w6j-4zg3{xPVM9B70G+f|T25_p9)k*%PcB3sRbLlyE8VM>KJD$d0&IRvdY zn{AdExsTM|xOkqRkPyTcbU;(s^adeFWY12z#~Ygl6u62cjUZRvlb%g_KkMzV+e=Il zqNasHMKXec=0NP^2_E4QIDNV`QDeeNm1QmLb~kpMgKa8zBzR<^^T!Kwyjn!CJfa}m zk`MSuz#3bp$Ngi|;|PUY8#C@;Bd1{MWjkNBUa*&^%79BMf$6|Cc4TEH*9h5 zjrF(>#=EWwOw2a>rl!Y*S~ zu)#+%TXI`+Smp6>)3&?>8)U1PC8q=v&UTb(vUhr1&_b3ht#sk(a0hI%GKg}&#D~PR zDtzL8#PlSzEAL|=_Iibhz{>7cBm~< zqE4OwR>3Go8Ke$t>|W+oQ%OeK3t?Zkcz14eEg>`J*`)i#oCjQw6h z%atnZ%_3wzo244S=yF0=uvM+fZ&^ad!@>FY?62G_F^cysp-t6-5P&qGNwtBR3|N3o z5ws}N30P6v3Cl^^l>>i+@-uy&fx?l8F%X*>fRPff@{#1l2d^)9M-(N>+8U4@#akQv zYWam}@sVmk0pVWixwImFO7?nky+jdh{xRt#Cr=2us?>UcHXp;DA;JV+utiqbT#Az& z4nR?h%3_HZEmu;xR3assR6@>9devlS%lUZ)TX9or;3RF$^O0joBM*Qf$hS0BI7T^H zNDzb)t2&Ml3!|Vx_XY%@lyD@dc&zjd2c%p`&VdAkPNnlp-Nln9f?=ZYsv6k)3OTf@ z=eoh6HG=~wwlDx^E1ZHKMT$Tb#t|pDBh>nqh+IFpw=LY5*99DS|K9@c=};t#GfNpF z!7bNN87!M4eX~xAk=vM!W`aUUlblpJZ316XJOsT!*B|9hxr-k$ch*Ppx{v^QxfdY6 zOTECE?*+17oHaWsg+hfefRr+${3i8*-9aRJ-NCVx=RHCpF?lbL8cP%>G^7eHr!n}qebSZLX21Eyz_Acsk|JJhQl(l$cl9-arRa8ocDY%E? z7hWQjKlZD{+lV(zB+`ch{BUfB6Toy9*UK;*@zva=}r{!M2=21ub% zsi4AA@!}uz^n6%nDe02msAR2)#Fd_h)qU(#}OI-y8Me3*skoYKrP<2Ih6^S9QL_wHf zP}}qR9a9CalsRz}cF$KhucN4LA~q~Ttu!4GRqi2DYvu4ZOUQe_Ds;mDgOv>PPxC&9I*h4saC*ImD-N zHP>)0pU!o>fa^KT4IJS{j&h8fxS3mcA-8fHFT(#AaZa$w9c*!uQ=H~b?&8IK27iRl z&}Mui}sLIead!<~6*Q*Kt3u=M8)wZ{+j& z0(z1!Q47FY54} zfr0%91B1)yfrY^hNb@joFfakxbqxO*gB2vzoPhoXI)q^XV-S$% z@c;k+$NvxgFLE-R?>*pU37!({58WtWA85JE98yBCD$k5i_**$s2yv2)_EL*yK z)YLlt&|$F5k(~?&!3-c#3B)Zu&Anay6DIaeoyIU_&fM8Rp+X?8+Ps#di;;7e>?7H4 zavE|On;&rLT2J8-B@|#o@mPgfTO8^hB!FB)u From 7490699bef72672874ca009addf363b27ebad8a3 Mon Sep 17 00:00:00 2001 From: ImperialSympathizer <110984302+ben-lear@users.noreply.github.com> Date: Thu, 19 Sep 2024 16:46:27 -0400 Subject: [PATCH 5/6] [Feature] Adds Expert Pokemon Breeder Mystery Encounter to the game (#4328) * Adds Expert Breeder Mystery Encounter to the game * add achievement for Breeders in Space and remove redundant tests * rename to Expert Pokemon Breeder * remove unintentional test code * remove unintentional test code * test fix with breeder rename --------- Co-authored-by: ImperialSympathizer Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> --- .../trainer/expert_pokemon_breeder.json | 41 ++ .../images/trainer/expert_pokemon_breeder.png | Bin 0 -> 1006 bytes .../encounters/bug-type-superfan-encounter.ts | 15 +- .../encounters/dancing-lessons-encounter.ts | 2 +- .../the-expert-pokemon-breeder-encounter.ts | 549 ++++++++++++++++++ .../mystery-encounters/mystery-encounters.ts | 10 +- .../utils/encounter-phase-utils.ts | 17 + src/data/trainer-config.ts | 16 +- src/enums/mystery-encounter-type.ts | 3 +- src/enums/trainer-type.ts | 1 + src/field/arena.ts | 8 +- src/locales/en/achv.json | 4 + src/locales/en/config.ts | 4 +- .../part-timer-dialogue.json | 2 +- .../the-expert-pokemon-breeder-dialogue.json | 30 + src/locales/en/trainer-names.json | 3 +- src/system/achv.ts | 3 + .../a-trainers-test-encounter.test.ts | 16 - .../absolute-avarice-encounter.test.ts | 16 - ...an-offer-you-cant-refuse-encounter.test.ts | 16 - .../berries-abound-encounter.test.ts | 16 - .../bug-type-superfan-encounter.test.ts | 16 - .../clowning-around-encounter.test.ts | 8 - .../dancing-lessons-encounter.test.ts | 16 - .../encounters/delibirdy-encounter.test.ts | 16 - .../department-store-sale-encounter.test.ts | 16 - .../encounters/field-trip-encounter.test.ts | 16 - .../fiery-fallout-encounter.test.ts | 8 - .../fight-or-flight-encounter.test.ts | 16 - .../fun-and-games-encounter.test.ts | 16 - .../global-trade-system-encounter.test.ts | 16 - .../encounters/lost-at-sea-encounter.test.ts | 16 - .../mysterious-challengers-encounter.test.ts | 16 - .../encounters/part-timer-encounter.test.ts | 16 - .../teleporting-hijinks-encounter.test.ts | 16 - .../the-expert-breeder-encounter.test.ts | 283 +++++++++ .../the-pokemon-salesman-encounter.test.ts | 16 - .../the-strong-stuff-encounter.test.ts | 16 - .../the-winstrate-challenge-encounter.test.ts | 16 - .../trash-to-treasure-encounter.test.ts | 16 - .../uncommon-breed-encounter.test.ts | 16 - .../encounters/weird-dream-encounter.test.ts | 16 - .../mystery-encounter.test.ts | 24 +- 43 files changed, 975 insertions(+), 408 deletions(-) create mode 100644 public/images/trainer/expert_pokemon_breeder.json create mode 100644 public/images/trainer/expert_pokemon_breeder.png create mode 100644 src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts create mode 100644 src/locales/en/mystery-encounters/the-expert-pokemon-breeder-dialogue.json create mode 100644 src/test/mystery-encounter/encounters/the-expert-breeder-encounter.test.ts 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 0000000000000000000000000000000000000000..0625f5255c3f49fad5727546414a04cc53a306dd GIT binary patch literal 1006 zcmV7SSd-a4d|jEMp%@=p00J1IFjKi-ecI!@ ztc=2jB1WOI(i=!ccTaHk`1}eIC#i0NgS28;u57euot;1-h@ZzW<|*BnL_=7Y1P<0_VaY48D;D4}cD#17pJnatv{T-QN#>5QobU*CzNH3`}tcQ510< zuVId{C?f7H%tc=C)={v>a_Y-zE{^Fbn98}&Xp`g596Liz^Y7-IwaF=tFoTdJiK2;7 zgdL9@J2BK+u@Y~!JF>~{I%PGwmYM4x7(quwy%z=;*gTzT#z7?9Wgdff4K!yOG z8~a)FV8Tz7fZjc*MW7-#LCA0w>v`{UhR4hK)ix$z&;MtLqzA1T+viE${PaCs`^w%P;G2sUjuloaEK zg|fDdYlxd7F1IVsg8j>E8Kn{sxe&cvN<{d?kz{efD(R;mW 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/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/field/arena.ts b/src/field/arena.ts index 9ce19c85283..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"; 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/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/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/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/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(); }); }); From 48430c8feb62bd276f9725baba41833d05f06c71 Mon Sep 17 00:00:00 2001 From: flx-sta <50131232+flx-sta@users.noreply.github.com> Date: Thu, 19 Sep 2024 15:59:37 -0700 Subject: [PATCH 6/6] [Feature] Seasonal splash messages logic + scaffolding (#4318) * add: seasonsl splash messages logic + scaffolding * refactor: settin up and displaying splash messages. They are now stored with their i18next keys and only get translated as soon as they are displayed. This also allows for better display of the `battlesWon` parameter which now supports better number formatting and the count is an interpolation * fix: updateTitleStats not checking the namespace of battlesWon * add tests for splash_messages * test: always use UTC time * fix: time-pattern to MM-DD * fix splash_messages test * add: const to control usage of seasonal splash messages * fix tests (splashj) * Update src/locales/ja/splash-messages.json Co-authored-by: Chapybara-jp * Update src/locales/es/splash-messages.json Add missing `number` format for battlesWon message --------- Co-authored-by: Chapybara-jp --- src/constants.ts | 6 +- src/data/splash-messages.ts | 170 +++++++++++++++++++------ src/locales/de/splash-messages.json | 2 +- src/locales/en/splash-messages.json | 16 ++- src/locales/es/splash-messages.json | 2 +- src/locales/fr/splash-messages.json | 2 +- src/locales/it/splash-messages.json | 2 +- src/locales/ja/splash-messages.json | 2 +- src/locales/ko/splash-messages.json | 2 +- src/locales/pt_BR/splash-messages.json | 2 +- src/locales/zh_CN/splash-messages.json | 2 +- src/locales/zh_TW/splash-messages.json | 2 +- src/test/data/splash_messages.test.ts | 66 ++++++++++ src/test/vitest.setup.ts | 2 + src/ui/title-ui-handler.ts | 11 +- 15 files changed, 233 insertions(+), 56 deletions(-) create mode 100644 src/test/data/splash_messages.test.ts 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/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/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/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/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/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/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/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/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();