diff --git a/README.md b/README.md index f3ec1c793bd..b26fd56a7b1 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ Check out [Github Issues](https://github.com/pagefaultgames/pokerogue/issues) to - Pokémon Legends: Arceus - Pokémon Scarlet/Violet - Firel (Custom Ice Cave, Laboratory, Metropolis, Plains, Power Plant, Seabed, Space, and Volcano biome music) - - Lmz (Custom Jungle biome music) + - Lmz (Custom Ancient Ruins, Jungle, and Lake biome music) - Andr06 (Custom Slum and Sea biome music) ### 🎵 Sound Effects diff --git a/public/audio/bgm/jungle.mp3 b/public/audio/bgm/jungle.mp3 index 3a21c9bdb41..fbb770b0ad3 100644 Binary files a/public/audio/bgm/jungle.mp3 and b/public/audio/bgm/jungle.mp3 differ diff --git a/public/audio/bgm/laboratory.mp3 b/public/audio/bgm/laboratory.mp3 index e2b617e590a..1eba66ef56f 100644 Binary files a/public/audio/bgm/laboratory.mp3 and b/public/audio/bgm/laboratory.mp3 differ diff --git a/public/audio/bgm/lake.mp3 b/public/audio/bgm/lake.mp3 index c61fef15e42..e6762935770 100644 Binary files a/public/audio/bgm/lake.mp3 and b/public/audio/bgm/lake.mp3 differ diff --git a/public/audio/bgm/metropolis.mp3 b/public/audio/bgm/metropolis.mp3 index 98c2eb396b6..514c9ae15b1 100644 Binary files a/public/audio/bgm/metropolis.mp3 and b/public/audio/bgm/metropolis.mp3 differ diff --git a/public/audio/bgm/mystery_encounter_delibirdy.mp3 b/public/audio/bgm/mystery_encounter_delibirdy.mp3 new file mode 100644 index 00000000000..515a429aaba Binary files /dev/null and b/public/audio/bgm/mystery_encounter_delibirdy.mp3 differ diff --git a/public/audio/bgm/mystery_encounter_fun_and_games.mp3 b/public/audio/bgm/mystery_encounter_fun_and_games.mp3 index a9660d75e90..7864bf7a73d 100644 Binary files a/public/audio/bgm/mystery_encounter_fun_and_games.mp3 and b/public/audio/bgm/mystery_encounter_fun_and_games.mp3 differ diff --git a/public/audio/bgm/mystery_encounter_gen_5_gts.mp3 b/public/audio/bgm/mystery_encounter_gen_5_gts.mp3 index 989a7f9c598..d03c8f8d4d5 100644 Binary files a/public/audio/bgm/mystery_encounter_gen_5_gts.mp3 and b/public/audio/bgm/mystery_encounter_gen_5_gts.mp3 differ diff --git a/public/audio/bgm/mystery_encounter_gen_6_gts.mp3 b/public/audio/bgm/mystery_encounter_gen_6_gts.mp3 index 2c574da66ae..c921a01c204 100644 Binary files a/public/audio/bgm/mystery_encounter_gen_6_gts.mp3 and b/public/audio/bgm/mystery_encounter_gen_6_gts.mp3 differ diff --git a/public/audio/bgm/mystery_encounter_weird_dream.mp3 b/public/audio/bgm/mystery_encounter_weird_dream.mp3 index a630fe549db..433e07bab08 100644 Binary files a/public/audio/bgm/mystery_encounter_weird_dream.mp3 and b/public/audio/bgm/mystery_encounter_weird_dream.mp3 differ diff --git a/public/audio/bgm/ruins.mp3 b/public/audio/bgm/ruins.mp3 index 62f31893423..3692c71562f 100644 Binary files a/public/audio/bgm/ruins.mp3 and b/public/audio/bgm/ruins.mp3 differ diff --git a/public/images/mystery-encounters/weird_dream_woman.json b/public/images/mystery-encounters/weird_dream_woman.json index 66a9b8d68db..49ebc001d18 100644 --- a/public/images/mystery-encounters/weird_dream_woman.json +++ b/public/images/mystery-encounters/weird_dream_woman.json @@ -5,29 +5,29 @@ "format": "RGBA8888", "size": { "w": 78, - "h": 87 + "h": 86 }, "scale": 1, "frames": [ { "filename": "0001.png", "rotated": false, - "trimmed": true, + "trimmed": false, "sourceSize": { - "w": 80, - "h": 87 + "w": 78, + "h": 86 }, "spriteSourceSize": { - "x": 1, + "x": 0, "y": 0, "w": 78, - "h": 87 + "h": 86 }, "frame": { "x": 0, "y": 0, "w": 78, - "h": 87 + "h": 86 } } ] @@ -36,6 +36,6 @@ "meta": { "app": "https://www.codeandweb.com/texturepacker", "version": "3.0", - "smartupdate": "$TexturePacker:SmartUpdate:d3cce87ee0e3a880d840bffe9373d5d4:7c776d33b75abad1fe36b14a5e5734af:56468b7a2883e66dadcd2af13ebd8010$" + "smartupdate": "$TexturePacker:SmartUpdate:65266da62e9d2953511c0d68ae431345:c1ca63690bed8dd5af71bb443910c830:56468b7a2883e66dadcd2af13ebd8010$" } } diff --git a/public/images/mystery-encounters/weird_dream_woman.png b/public/images/mystery-encounters/weird_dream_woman.png index 1b8d142ed5b..50d04667152 100644 Binary files a/public/images/mystery-encounters/weird_dream_woman.png and b/public/images/mystery-encounters/weird_dream_woman.png differ diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 516662617f1..753efdaf62c 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -1911,6 +1911,19 @@ export default class BattleScene extends SceneBase { return false; } + /** + * Fades out current track for `delay` ms, then fades in new track. + * @param newBgmKey + * @param destroy + * @param delay + */ + fadeAndSwitchBgm(newBgmKey: string, destroy: boolean = false, delay: number = 2000) { + this.fadeOutBgm(delay, destroy); + this.time.delayedCall(delay, () => { + this.playBgm(newBgmKey); + }); + } + playSound(sound: string | AnySound, config?: object): AnySound { const key = typeof sound === "string" ? sound : sound.key; config = config ?? {}; @@ -2157,6 +2170,16 @@ export default class BattleScene extends SceneBase { return 13.13; case "battle_macro_boss": //SWSH Rose Battle return 11.42; + case "mystery_encounter_gen_5_gts": // BW GTS + return 8.52; + case "mystery_encounter_gen_6_gts": // XY GTS + return 9.24; + case "mystery_encounter_fun_and_games": // EoS Guildmaster Wigglytuff + return 4.78; + case "mystery_encounter_weird_dream": // EoS Temporal Spire + return 41.42; + case "mystery_encounter_delibirdy": // Firel Delibirdy + return 82.28; } return 0; @@ -2603,7 +2626,7 @@ export default class BattleScene extends SceneBase { } party.forEach((enemyPokemon: EnemyPokemon, i: integer) => { - if (heldModifiersConfigs && i < heldModifiersConfigs.length && heldModifiersConfigs[i] && heldModifiersConfigs[i].length > 0) { + if (heldModifiersConfigs && i < heldModifiersConfigs.length && heldModifiersConfigs[i]) { heldModifiersConfigs[i].forEach(mt => { let modifier: PokemonHeldItemModifier; if (mt.modifier instanceof PokemonHeldItemModifierType) { @@ -2614,8 +2637,7 @@ export default class BattleScene extends SceneBase { } const stackCount = mt.stackCount ?? 1; modifier.stackCount = stackCount; - // TODO: set isTransferable - // modifier.isTransferrable = mt.isTransferable ?? true; + modifier.isTransferable = mt.isTransferable ?? modifier.isTransferable; this.addEnemyModifier(modifier, true); }); } else { diff --git a/src/battle.ts b/src/battle.ts index a886a0eb771..b857314b4bf 100644 --- a/src/battle.ts +++ b/src/battle.ts @@ -14,7 +14,7 @@ import { PlayerGender } from "#enums/player-gender"; import { Species } from "#enums/species"; import { TrainerType } from "#enums/trainer-type"; import i18next from "#app/plugins/i18n"; -import MysteryEncounter from "./data/mystery-encounters/mystery-encounter"; +import MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; export enum BattleType { @@ -157,7 +157,7 @@ export default class Battle { } addPostBattleLoot(enemyPokemon: EnemyPokemon): void { - this.postBattleLoot.push(...enemyPokemon.scene.findModifiers(m => m instanceof PokemonHeldItemModifier && m.pokemonId === enemyPokemon.id && m.isTransferrable, false).map(i => { + this.postBattleLoot.push(...enemyPokemon.scene.findModifiers(m => m instanceof PokemonHeldItemModifier && m.pokemonId === enemyPokemon.id && m.isTransferable, false).map(i => { const ret = i as PokemonHeldItemModifier; //@ts-ignore - this is awful to fix/change ret.pokemonId = null; diff --git a/src/data/ability.ts b/src/data/ability.ts index becdad32f34..520710e9527 100755 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -1670,7 +1670,7 @@ export class PostAttackStealHeldItemAbAttr extends PostAttackAbAttr { applyPostAttackAfterMoveTypeCheck(pokemon: Pokemon, passive: boolean, simulated: boolean, defender: Pokemon, move: Move, hitResult: HitResult, args: any[]): Promise { return new Promise(resolve => { if (!simulated && hitResult < HitResult.NO_EFFECT && (!this.stealCondition || this.stealCondition(pokemon, defender, move))) { - const heldItems = this.getTargetHeldItems(defender).filter(i => i.isTransferrable); + const heldItems = this.getTargetHeldItems(defender).filter(i => i.isTransferable); if (heldItems.length) { const stolenItem = heldItems[pokemon.randSeedInt(heldItems.length)]; pokemon.scene.tryTransferHeldItemModifier(stolenItem, pokemon, false).then(success => { @@ -1763,7 +1763,7 @@ export class PostDefendStealHeldItemAbAttr extends PostDefendAbAttr { applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): Promise { return new Promise(resolve => { if (!simulated && hitResult < HitResult.NO_EFFECT && (!this.condition || this.condition(pokemon, attacker, move))) { - const heldItems = this.getTargetHeldItems(attacker).filter(i => i.isTransferrable); + const heldItems = this.getTargetHeldItems(attacker).filter(i => i.isTransferable); if (heldItems.length) { const stolenItem = heldItems[pokemon.randSeedInt(heldItems.length)]; pokemon.scene.tryTransferHeldItemModifier(stolenItem, pokemon, false).then(success => { diff --git a/src/data/egg.ts b/src/data/egg.ts index 0219f4f5b47..b37240a2028 100644 --- a/src/data/egg.ts +++ b/src/data/egg.ts @@ -1,6 +1,6 @@ import BattleScene from "../battle-scene"; import PokemonSpecies, { getPokemonSpecies, speciesStarters } from "./pokemon-species"; -import { VariantTier } from "../enums/variant-tiers"; +import { VariantTier } from "../enums/variant-tier"; import * as Utils from "../utils"; import Overrides from "#app/overrides"; import { pokemonPrevolutions } from "./pokemon-evolutions"; @@ -178,7 +178,7 @@ export class Egg { // be done because species with no variants get filtered at rollSpecies but if the // species is set via options or the legendary gacha pokemon gets choosen the check never happens if (this._species && !getPokemonSpecies(this._species).hasVariants()) { - this._variantTier = VariantTier.COMMON; + this._variantTier = VariantTier.STANDARD; } // Needs this._tier so it needs to be generated afer the tier override if bought from same species this._eggMoveIndex = eggOptions?.eggMoveIndex ?? this.rollEggMoveIndex(); @@ -494,12 +494,12 @@ export class Egg { // place but I don't want to touch the pokemon class. private rollVariant(): VariantTier { if (!this.isShiny) { - return VariantTier.COMMON; + return VariantTier.STANDARD; } const rand = Utils.randSeedInt(10); if (rand >= 4) { - return VariantTier.COMMON; // 6/10 + return VariantTier.STANDARD; // 6/10 } else if (rand >= 1) { return VariantTier.RARE; // 3/10 } else { diff --git a/src/data/move.ts b/src/data/move.ts index a4d552cbac6..951c3300515 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -1,16 +1,15 @@ -import { ChargeAnim, MoveChargeAnim, initMoveAnim, loadMoveAnimAssets } from "./battle-anims"; -import { EncoreTag, GulpMissileTag, HelpingHandTag, SemiInvulnerableTag, ShellTrapTag, StockpilingTag, TrappedTag, SubstituteTag, TypeBoostTag } from "./battler-tags"; +import { ChargeAnim, initMoveAnim, loadMoveAnimAssets, MoveChargeAnim } from "./battle-anims"; +import { EncoreTag, GulpMissileTag, HelpingHandTag, SemiInvulnerableTag, ShellTrapTag, StockpilingTag, SubstituteTag, TrappedTag, TypeBoostTag } from "./battler-tags"; import { getPokemonNameWithAffix } from "../messages"; import Pokemon, { AttackMoveResult, EnemyPokemon, HitResult, MoveResult, PlayerPokemon, PokemonMove, TurnMove } from "../field/pokemon"; -import { StatusEffect, getStatusEffectHealText, isNonVolatileStatusEffect, getNonVolatileStatusEffects } from "./status-effect"; +import { getNonVolatileStatusEffects, getStatusEffectHealText, isNonVolatileStatusEffect, StatusEffect } from "./status-effect"; import { getTypeDamageMultiplier, Type } from "./type"; -import { Constructor } from "#app/utils"; +import { Constructor, NumberHolder } from "#app/utils"; import * as Utils from "../utils"; import { WeatherType } from "./weather"; import { ArenaTagSide, ArenaTrapTag, WeakenMoveTypeTag } from "./arena-tag"; -import { UnswappableAbilityAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, BlockRecoilDamageAttr, BlockOneHitKOAbAttr, IgnoreContactAbAttr, MaxMultiHitAbAttr, applyAbAttrs, BlockNonDirectDamageAbAttr, MoveAbilityBypassAbAttr, ReverseDrainAbAttr, FieldPreventExplosiveMovesAbAttr, ForceSwitchOutImmunityAbAttr, BlockItemTheftAbAttr, applyPostAttackAbAttrs, ConfusionOnStatusEffectAbAttr, HealFromBerryUseAbAttr, IgnoreProtectOnContactAbAttr, IgnoreMoveEffectsAbAttr, applyPreDefendAbAttrs, MoveEffectChanceMultiplierAbAttr, WonderSkinAbAttr, applyPreAttackAbAttrs, MoveTypeChangeAbAttr, UserFieldMoveTypePowerBoostAbAttr, FieldMoveTypePowerBoostAbAttr, AllyMoveCategoryPowerBoostAbAttr, VariableMovePowerAbAttr } from "./ability"; -import { allAbilities } from "./ability"; -import { PokemonHeldItemModifier, BerryModifier, PreserveBerryModifier, PokemonMoveAccuracyBoosterModifier, AttackTypeBoosterModifier, PokemonMultiHitModifier } from "../modifier/modifier"; +import { allAbilities, AllyMoveCategoryPowerBoostAbAttr, applyAbAttrs, applyPostAttackAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, BlockItemTheftAbAttr, BlockNonDirectDamageAbAttr, BlockOneHitKOAbAttr, BlockRecoilDamageAttr, ConfusionOnStatusEffectAbAttr, FieldMoveTypePowerBoostAbAttr, FieldPreventExplosiveMovesAbAttr, ForceSwitchOutImmunityAbAttr, HealFromBerryUseAbAttr, IgnoreContactAbAttr, IgnoreMoveEffectsAbAttr, IgnoreProtectOnContactAbAttr, MaxMultiHitAbAttr, MoveAbilityBypassAbAttr, MoveEffectChanceMultiplierAbAttr, MoveTypeChangeAbAttr, ReverseDrainAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, UnswappableAbilityAbAttr, UserFieldMoveTypePowerBoostAbAttr, VariableMovePowerAbAttr, WonderSkinAbAttr } from "./ability"; +import { AttackTypeBoosterModifier, BerryModifier, PokemonHeldItemModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, PreserveBerryModifier } from "../modifier/modifier"; import { BattlerIndex, BattleType } from "../battle"; import { TerrainType } from "./terrain"; import { ModifierPoolType } from "#app/modifier/modifier-type"; @@ -25,7 +24,7 @@ import { Biome } from "#enums/biome"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import { MoveUsedEvent } from "#app/events/battle-scene"; -import { Stat, type BattleStat, type EffectiveStat, BATTLE_STATS, EFFECTIVE_STATS, getStatKey } from "#app/enums/stat"; +import { BATTLE_STATS, type BattleStat, EFFECTIVE_STATS, type EffectiveStat, getStatKey, Stat } from "#app/enums/stat"; import { PartyStatusCurePhase } from "#app/phases/party-status-cure-phase"; import { BattleEndPhase } from "#app/phases/battle-end-phase"; import { MoveEndPhase } from "#app/phases/move-end-phase"; @@ -36,7 +35,6 @@ import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; import { SwitchPhase } from "#app/phases/switch-phase"; import { SwitchSummonPhase } from "#app/phases/switch-summon-phase"; import { SpeciesFormChangeRevertWeatherFormTrigger } from "./pokemon-forms"; -import { NumberHolder } from "#app/utils"; import { GameMode } from "#app/game-mode"; import { applyChallenges, ChallengeType } from "./challenge"; @@ -2136,7 +2134,7 @@ export class StealHeldItemChanceAttr extends MoveEffectAttr { if (rand >= this.chance) { return resolve(false); } - const heldItems = this.getTargetHeldItems(target).filter(i => i.isTransferrable); + const heldItems = this.getTargetHeldItems(target).filter(i => i.isTransferable); if (heldItems.length) { const poolType = target.isPlayer() ? ModifierPoolType.PLAYER : target.hasTrainer() ? ModifierPoolType.TRAINER : ModifierPoolType.WILD; const highestItemTier = heldItems.map(m => m.type.getOrInferTier(poolType)).reduce((highestTier, tier) => Math.max(tier!, highestTier), 0); // TODO: is the bang after tier correct? @@ -2213,7 +2211,7 @@ export class RemoveHeldItemAttr extends MoveEffectAttr { } // Considers entire transferrable item pool by default (Knock Off). Otherwise berries only if specified (Incinerate). - let heldItems = this.getTargetHeldItems(target).filter(i => i.isTransferrable); + let heldItems = this.getTargetHeldItems(target).filter(i => i.isTransferable); if (this.berriesOnly) { heldItems = heldItems.filter(m => m instanceof BerryModifier && m.pokemonId === target.id, target.isPlayer()); @@ -2417,6 +2415,16 @@ export class BypassSleepAttr extends MoveAttr { return false; } + + /** + * Returns arbitrarily high score when Pokemon is asleep, otherwise shouldn't be used + * @param user + * @param target + * @param move + */ + getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer { + return user.status && user.status.effect === StatusEffect.SLEEP ? 200 : -10; + } } /** @@ -3839,7 +3847,7 @@ export class StormAccuracyAttr extends VariableAccuracyAttr { * @extends VariableAccuracyAttr * @see {@linkcode apply} */ -export class MinimizeAccuracyAttr extends VariableAccuracyAttr { +export class AlwaysHitMinimizeAttr extends VariableAccuracyAttr { /** * @see {@linkcode apply} * @param user N/A @@ -3974,18 +3982,17 @@ export class StatusCategoryOnAllyAttr extends VariableMoveCategoryAttr { export class ShellSideArmCategoryAttr extends VariableMoveCategoryAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { const category = (args[0] as Utils.NumberHolder); - const atkRatio = user.getEffectiveStat(Stat.ATK, target, move) / target.getEffectiveStat(Stat.DEF, user, move); - const specialRatio = user.getEffectiveStat(Stat.SPATK, target, move) / target.getEffectiveStat(Stat.SPDEF, user, move); - // Shell Side Arm is much more complicated than it looks, this is a partial implementation to try to achieve something similar to the games - if (atkRatio > specialRatio) { + const predictedPhysDmg = target.getBaseDamage(user, move, MoveCategory.PHYSICAL, true, true); + const predictedSpecDmg = target.getBaseDamage(user, move, MoveCategory.SPECIAL, true, true); + + if (predictedPhysDmg > predictedSpecDmg) { category.value = MoveCategory.PHYSICAL; return true; - } else if (atkRatio === specialRatio && user.randSeedInt(2) === 0) { + } else if (predictedPhysDmg === predictedSpecDmg && user.randSeedInt(2) === 0) { category.value = MoveCategory.PHYSICAL; return true; } - return false; } } @@ -4852,7 +4859,9 @@ 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 HitsTagAttr extends MoveAttr { @@ -4861,7 +4870,7 @@ export class HitsTagAttr extends MoveAttr { /** 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; @@ -4873,6 +4882,17 @@ export class HitsTagAttr 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; @@ -6393,7 +6413,7 @@ export class AttackedByItemAttr extends MoveAttr { */ getCondition(): MoveConditionFunc { return (user: Pokemon, target: Pokemon, move: Move) => { - const heldItems = target.getHeldItems().filter(i => i.isTransferrable); + const heldItems = target.getHeldItems().filter(i => i.isTransferable); if (heldItems.length === 0) { return false; } @@ -6752,12 +6772,11 @@ 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(HitsTagAttr, 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) .attr(ForceSwitchOutAttr) - .attr(HitsTagAttr, BattlerTagType.FLYING, false) .ignoresSubstitute() .hidesTarget() .windMove(), @@ -6770,8 +6789,8 @@ export function initMoves() { new AttackMove(Moves.SLAM, Type.NORMAL, MoveCategory.PHYSICAL, 80, 75, 20, -1, 0, 1), new AttackMove(Moves.VINE_WHIP, Type.GRASS, MoveCategory.PHYSICAL, 45, 100, 25, -1, 0, 1), new AttackMove(Moves.STOMP, Type.NORMAL, MoveCategory.PHYSICAL, 65, 100, 20, 30, 0, 1) - .attr(MinimizeAccuracyAttr) - .attr(HitsTagAttr, BattlerTagType.MINIMIZED, true) + .attr(AlwaysHitMinimizeAttr) + .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), @@ -6795,8 +6814,8 @@ export function initMoves() { .attr(OneHitKOAccuracyAttr), 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(MinimizeAccuracyAttr) - .attr(HitsTagAttr, BattlerTagType.MINIMIZED, true) + .attr(AlwaysHitMinimizeAttr) + .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), @@ -6864,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(HitsTagAttr, 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), @@ -6947,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(HitsTagAttr, 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(HitsTagAttr, 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(HitsTagAttr, 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) @@ -7347,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(HitsTagAttr, 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) @@ -7403,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(HitsTagAttr, BattlerTagType.FLYING, true) + .attr(HitsTagForDoubleDamageAttr, BattlerTagType.FLYING) .attr(FlinchAttr) .windMove() .target(MoveTarget.ALL_NEAR_ENEMIES), @@ -7435,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(HitsTagAttr, 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) @@ -7533,7 +7552,7 @@ export function initMoves() { .attr(AddBattlerTagAttr, BattlerTagType.DROWSY, false, true) .condition((user, target, move) => !target.status && !target.scene.arena.getTagOnSide(ArenaTagType.SAFEGUARD, target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY)), new AttackMove(Moves.KNOCK_OFF, Type.DARK, MoveCategory.PHYSICAL, 65, 100, 20, -1, 0, 3) - .attr(MovePowerMultiplierAttr, (user, target, move) => target.getHeldItems().filter(i => i.isTransferrable).length > 0 ? 1.5 : 1) + .attr(MovePowerMultiplierAttr, (user, target, move) => target.getHeldItems().filter(i => i.isTransferable).length > 0 ? 1.5 : 1) .attr(RemoveHeldItemAttr, false), new AttackMove(Moves.ENDEAVOR, Type.NORMAL, MoveCategory.PHYSICAL, -1, 100, 5, -1, 0, 3) .attr(MatchHpAttr) @@ -7889,8 +7908,8 @@ export function initMoves() { new AttackMove(Moves.DRAGON_PULSE, Type.DRAGON, MoveCategory.SPECIAL, 85, 100, 10, -1, 0, 4) .pulseMove(), new AttackMove(Moves.DRAGON_RUSH, Type.DRAGON, MoveCategory.PHYSICAL, 100, 75, 10, 20, 0, 4) - .attr(MinimizeAccuracyAttr) - .attr(HitsTagAttr, BattlerTagType.MINIMIZED, true) + .attr(AlwaysHitMinimizeAttr) + .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) @@ -8087,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(HitsTagAttr, 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), @@ -8100,9 +8119,9 @@ export function initMoves() { .attr(StatStageChangeAttr, [ Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true) .danceMove(), new AttackMove(Moves.HEAVY_SLAM, Type.STEEL, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 5) - .attr(MinimizeAccuracyAttr) + .attr(AlwaysHitMinimizeAttr) .attr(CompareWeightPowerAttr) - .attr(HitsTagAttr, 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) @@ -8185,7 +8204,7 @@ export function initMoves() { new StatusMove(Moves.QUASH, Type.DARK, 100, 15, -1, 0, 5) .unimplemented(), new AttackMove(Moves.ACROBATICS, Type.FLYING, MoveCategory.PHYSICAL, 55, 100, 15, -1, 0, 5) - .attr(MovePowerMultiplierAttr, (user, target, move) => Math.max(1, 2 - 0.2 * user.getHeldItems().filter(i => i.isTransferrable).reduce((v, m) => v + m.stackCount, 0))), + .attr(MovePowerMultiplierAttr, (user, target, move) => Math.max(1, 2 - 0.2 * user.getHeldItems().filter(i => i.isTransferable).reduce((v, m) => v + m.stackCount, 0))), new StatusMove(Moves.REFLECT_TYPE, Type.NORMAL, -1, 15, -1, 0, 5) .ignoresSubstitute() .attr(CopyTypeAttr), @@ -8253,12 +8272,14 @@ export function initMoves() { .attr(StatStageChangeAttr, [ Stat.DEF ], -1) .slicingMove(), new AttackMove(Moves.HEAT_CRASH, Type.FIRE, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 5) - .attr(MinimizeAccuracyAttr) + .attr(AlwaysHitMinimizeAttr) .attr(CompareWeightPowerAttr) - .attr(HitsTagAttr, 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(HitsTagForDoubleDamageAttr, BattlerTagType.MINIMIZED) .attr(FlinchAttr), new SelfStatusMove(Moves.COTTON_GUARD, Type.GRASS, -1, 10, -1, 0, 5) .attr(StatStageChangeAttr, [ Stat.DEF ], 3, true), @@ -8271,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(HitsTagAttr, 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) @@ -8325,9 +8346,9 @@ export function initMoves() { .attr(LastMoveDoublePowerAttr, Moves.FUSION_FLARE) .makesContact(false), new AttackMove(Moves.FLYING_PRESS, Type.FIGHTING, MoveCategory.PHYSICAL, 100, 95, 10, -1, 0, 6) - .attr(MinimizeAccuracyAttr) + .attr(AlwaysHitMinimizeAttr) .attr(FlyingTypeMultiplierAttr) - .attr(HitsTagAttr, 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) @@ -8498,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(HitsTagAttr, BattlerTagType.FLYING, false) - .attr(HitsTagAttr, 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) @@ -8756,6 +8777,8 @@ export function initMoves() { .partial() .ignoresVirtual(), new AttackMove(Moves.MALICIOUS_MOONSAULT, Type.DARK, MoveCategory.PHYSICAL, 180, -1, 1, -1, 0, 7) + .attr(AlwaysHitMinimizeAttr) + .attr(HitsTagAttr, BattlerTagType.MINIMIZED, true) .partial() .ignoresVirtual(), new AttackMove(Moves.OCEANIC_OPERETTA, Type.WATER, MoveCategory.SPECIAL, 195, -1, 1, -1, 0, 7) @@ -9103,7 +9126,7 @@ export function initMoves() { new AttackMove(Moves.SHELL_SIDE_ARM, Type.POISON, MoveCategory.SPECIAL, 90, 100, 10, 20, 0, 8) .attr(ShellSideArmCategoryAttr) .attr(StatusEffectAttr, StatusEffect.POISON) - .partial(), + .partial(), // Physical version of the move does not make contact new AttackMove(Moves.MISTY_EXPLOSION, Type.FAIRY, MoveCategory.SPECIAL, 100, 100, 5, -1, 0, 8) .attr(SacrificialAttr) .target(MoveTarget.ALL_NEAR_OTHERS) diff --git a/src/data/mystery-encounters/encounters/a-trainers-test-encounter.ts b/src/data/mystery-encounters/encounters/a-trainers-test-encounter.ts index b66ca10c9f5..f7666fa1b37 100644 --- a/src/data/mystery-encounters/encounters/a-trainers-test-encounter.ts +++ b/src/data/mystery-encounters/encounters/a-trainers-test-encounter.ts @@ -2,7 +2,7 @@ import { EnemyPartyConfig, initBattleWithEnemyConfig, leaveEncounterWithoutBattl import { trainerConfigs, } from "#app/data/trainer-config"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "#app/battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { TrainerType } from "#enums/trainer-type"; import { Species } from "#enums/species"; @@ -99,7 +99,7 @@ export const ATrainersTestEncounter: MysteryEncounter = const trainerConfig = trainerConfigs[trainerType].clone(); const trainerSpriteKey = trainerConfig.getSpriteKey(); encounter.enemyPartyConfigs.push({ - levelAdditiveMultiplier: 1, + levelAdditiveModifier: 1, trainerConfig: trainerConfig }); @@ -152,7 +152,6 @@ export const ATrainersTestEncounter: MysteryEncounter = }; encounter.setDialogueToken("eggType", i18next.t(`${namespace}.eggTypes.epic`)); setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.SACRED_ASH], guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ULTRA], fillRemaining: true }, [eggOptions]); - return initBattleWithEnemyConfig(scene, config); } ) @@ -180,7 +179,7 @@ export const ATrainersTestEncounter: MysteryEncounter = ) .withOutroDialogue([ { - text: `${namespace}.outro`, - }, + text: `${namespace}.outro` + } ]) .build(); diff --git a/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts b/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts index a9a273c6ec4..18f998192ce 100644 --- a/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts +++ b/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts @@ -4,9 +4,9 @@ import { BerryModifierType, modifierTypes, PokemonHeldItemModifierType } from "# import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { Species } from "#enums/species"; import BattleScene from "#app/battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; -import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; -import { PersistentModifierRequirement } from "../mystery-encounter-requirements"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { PersistentModifierRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; @@ -208,7 +208,7 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = // Calculate boss mon const config: EnemyPartyConfig = { - levelAdditiveMultiplier: 1, + levelAdditiveModifier: 1, pokemonConfigs: [ { species: getPokemonSpecies(Species.GREEDENT), @@ -291,7 +291,7 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = const encounter = scene.currentBattle.mysteryEncounter!; const berryMap = encounter.misc.berryItemsMap; - // Returns 2/5 of the berries stolen from each Pokemon + // Returns 2/5 of the berries stolen to each Pokemon const party = scene.getParty(); party.forEach(pokemon => { const stolenBerries: BerryModifier[] = berryMap.get(pokemon.id); @@ -310,6 +310,7 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = } } }); + await scene.updateModifiers(true); transitionMysteryEncounterIntroVisuals(scene, true, true, 500); leaveEncounterWithoutBattle(scene, true); diff --git a/src/data/mystery-encounters/encounters/an-offer-you-cant-refuse-encounter.ts b/src/data/mystery-encounters/encounters/an-offer-you-cant-refuse-encounter.ts index 9f38b5a4dea..eb43424a8ff 100644 --- a/src/data/mystery-encounters/encounters/an-offer-you-cant-refuse-encounter.ts +++ b/src/data/mystery-encounters/encounters/an-offer-you-cant-refuse-encounter.ts @@ -3,9 +3,9 @@ import { modifierTypes } from "#app/modifier/modifier-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { Species } from "#enums/species"; import BattleScene from "#app/battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; -import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; -import { AbilityRequirement, CombinationPokemonRequirement, MoveRequirement } from "../mystery-encounter-requirements"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { AbilityRequirement, CombinationPokemonRequirement, MoveRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import { getHighestStatTotalPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { EXTORTION_ABILITIES, EXTORTION_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups"; import { getPokemonSpecies } from "#app/data/pokemon-species"; diff --git a/src/data/mystery-encounters/encounters/berries-abound-encounter.ts b/src/data/mystery-encounters/encounters/berries-abound-encounter.ts index 7e6914cabdd..d1497c54527 100644 --- a/src/data/mystery-encounters/encounters/berries-abound-encounter.ts +++ b/src/data/mystery-encounters/encounters/berries-abound-encounter.ts @@ -17,13 +17,13 @@ import { randSeedInt } from "#app/utils"; import { BattlerTagType } from "#enums/battler-tag-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "#app/battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { getPokemonNameWithAffix } from "#app/messages"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { TrainerSlot } from "#app/data/trainer-config"; -import { applyModifierTypeToPlayerPokemon, getHighestStatPlayerPokemon, getSpriteKeysFromPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { applyModifierTypeToPlayerPokemon, getEncounterPokemonLevelForWave, getHighestStatPlayerPokemon, getSpriteKeysFromPokemon, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import PokemonData from "#app/system/pokemon-data"; import { BerryModifier } from "#app/modifier/modifier"; import i18next from "#app/plugins/i18n"; @@ -56,12 +56,11 @@ export const BerriesAboundEncounter: MysteryEncounter = const encounter = scene.currentBattle.mysteryEncounter!; // Calculate boss mon - const level = (scene.currentBattle.enemyLevels?.[0] ?? scene.currentBattle.waveIndex) + Math.max(Math.round((scene.currentBattle.waveIndex / 10)), 0); + const level = getEncounterPokemonLevelForWave(scene, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER); const bossSpecies = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getParty()), true); const bossPokemon = new EnemyPokemon(scene, bossSpecies, level, TrainerSlot.NONE, true); encounter.setDialogueToken("enemyPokemon", getPokemonNameWithAffix(bossPokemon)); const config: EnemyPartyConfig = { - levelAdditiveMultiplier: 1, pokemonConfigs: [{ level: level, species: bossSpecies, diff --git a/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts b/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts index 7fdaec35dc3..d79ac087725 100644 --- a/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts +++ b/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts @@ -19,7 +19,7 @@ import { PartyMemberStrength } from "#enums/party-member-strength"; import BattleScene from "#app/battle-scene"; import * as Utils from "#app/utils"; import { isNullOrUndefined, randSeedInt, randSeedShuffle } from "#app/utils"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { TrainerType } from "#enums/trainer-type"; import { Species } from "#enums/species"; @@ -192,6 +192,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = new AttackTypeBoosterHeldItemTypeRequirement(Type.BUG, 1), new TypeRequirement(Type.BUG, false, 1) )) + .withMaxAllowedEncounters(1) .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) .withIntroSpriteConfigs([]) // These are set in onInit() .withAutoHideIntroVisuals(false) @@ -286,7 +287,8 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = // Player gets different rewards depending on the number of bug types they have const numBugTypes = scene.getParty().filter(p => p.isOfType(Type.BUG, true)).length; - encounter.setDialogueToken("numBugTypes", numBugTypes.toString()); + const numBugTypesText = i18next.t(`${namespace}.numBugTypes`, { count: numBugTypes }); + encounter.setDialogueToken("numBugTypes", numBugTypesText); if (numBugTypes < 2) { setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.SUPER_LURE, modifierTypes.GREAT_BALL], fillRemaining: false }); diff --git a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts index 061d2a33e8a..d930e43c45f 100644 --- a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts +++ b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts @@ -5,7 +5,7 @@ import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifi import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { PartyMemberStrength } from "#enums/party-member-strength"; import BattleScene from "#app/battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { Species } from "#enums/species"; import { TrainerType } from "#enums/trainer-type"; @@ -246,12 +246,12 @@ export const ClowningAroundEncounter: MysteryEncounter = const party = scene.getParty(); let mostHeldItemsPokemon = party[0]; let count = mostHeldItemsPokemon.getHeldItems() - .filter(m => m.isTransferrable && !(m instanceof BerryModifier)) + .filter(m => m.isTransferable && !(m instanceof BerryModifier)) .reduce((v, m) => v + m.stackCount, 0); party.forEach(pokemon => { const nextCount = pokemon.getHeldItems() - .filter(m => m.isTransferrable && !(m instanceof BerryModifier)) + .filter(m => m.isTransferable && !(m instanceof BerryModifier)) .reduce((v, m) => v + m.stackCount, 0); if (nextCount > count) { mostHeldItemsPokemon = pokemon; @@ -276,7 +276,7 @@ export const ClowningAroundEncounter: MysteryEncounter = // Shuffle Transferable held items in the same tier (only shuffles Ultra and Rogue atm) let numUltra = 0; let numRogue = 0; - items.filter(m => m.isTransferrable && !(m instanceof BerryModifier)) + items.filter(m => m.isTransferable && !(m instanceof BerryModifier)) .forEach(m => { const type = m.type.withTierFromPool(); const tier = type.tier ?? ModifierTier.ULTRA; diff --git a/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts b/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts index 046e2b2f876..bd308a5919b 100644 --- a/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts +++ b/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts @@ -3,8 +3,8 @@ import Pokemon, { EnemyPokemon, PlayerPokemon, PokemonMove } from "#app/field/po import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { Species } from "#enums/species"; import BattleScene from "#app/battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; -import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { getPokemonSpecies } from "#app/data/pokemon-species"; @@ -19,7 +19,7 @@ import { MoveRequirement } from "#app/data/mystery-encounters/mystery-encounter- import { DANCING_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups"; import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; import { BattlerIndex } from "#app/battle"; -import { catchPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { catchPokemon, getEncounterPokemonLevelForWave, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { PokeballType } from "#enums/pokeball"; import { modifierTypes } from "#app/modifier/modifier-type"; import { LearnMovePhase } from "#app/phases/learn-move-phase"; @@ -107,7 +107,7 @@ export const DancingLessonsEncounter: MysteryEncounter = const encounter = scene.currentBattle.mysteryEncounter!; const species = getPokemonSpecies(Species.ORICORIO); - const level = (scene.currentBattle.enemyLevels?.[0] ?? scene.currentBattle.waveIndex) + Math.max(Math.round((scene.currentBattle.waveIndex / 10)), 0); + const level = getEncounterPokemonLevelForWave(scene, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER); const enemyPokemon = new EnemyPokemon(scene, species, level, TrainerSlot.NONE, false); if (!enemyPokemon.moveset.some(m => m && m.getMove().id === Moves.REVELATION_DANCE)) { if (enemyPokemon.moveset.length < 4) { @@ -146,7 +146,6 @@ export const DancingLessonsEncounter: MysteryEncounter = encounter.loadAssets.push(oricorio.loadAssets()); const config: EnemyPartyConfig = { - levelAdditiveMultiplier: 1, pokemonConfigs: [{ species: species, dataSource: oricorioData, diff --git a/src/data/mystery-encounters/encounters/dark-deal-encounter.ts b/src/data/mystery-encounters/encounters/dark-deal-encounter.ts index 212ff6ed1bb..09b058ab7c9 100644 --- a/src/data/mystery-encounters/encounters/dark-deal-encounter.ts +++ b/src/data/mystery-encounters/encounters/dark-deal-encounter.ts @@ -5,8 +5,8 @@ import { Species } from "#enums/species"; import BattleScene from "#app/battle-scene"; import { modifierTypes } from "#app/modifier/modifier-type"; import { getPokemonSpecies } from "#app/data/pokemon-species"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; -import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { EnemyPartyConfig, EnemyPokemonConfig, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, } from "../utils/encounter-phase-utils"; import { getRandomPlayerPokemon, getRandomSpeciesByStarterTier } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; @@ -18,7 +18,7 @@ import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; /** i18n namespace for encounter */ const namespace = "mysteryEncounter:darkDeal"; -/** Exclude Ultra Beasts (inludes Cosmog/Solgaleo/Lunala/Necrozma), Paradox (includes Miraidon/Koraidon), Eternatus, and egg-locked mythicals */ +/** Exclude Ultra Beasts (inludes Cosmog/Solgaleo/Lunala/Necrozma), Paradox (includes Miraidon/Koraidon), Eternatus, and Mythicals */ const excludedBosses = [ Species.NECROZMA, Species.COSMOG, @@ -63,11 +63,24 @@ const excludedBosses = [ Species.CELEBI, Species.DEOXYS, Species.JIRACHI, + Species.DARKRAI, Species.PHIONE, Species.MANAPHY, Species.ARCEUS, + Species.SHAYMIN, Species.VICTINI, + Species.MELOETTA, + Species.KELDEO, + Species.GENESECT, + Species.DIANCIE, + Species.HOOPA, + Species.VOLCANION, + Species.MAGEARNA, + Species.MARSHADOW, + Species.ZERAORA, + Species.ZARUDE, Species.MELTAN, + Species.MELMETAL, Species.PECHARUNT, ]; @@ -151,7 +164,7 @@ export const DarkDealEncounter: MysteryEncounter = // Starter egg tier, 35/50/10/5 %odds for tiers 6/7/8/9+ const roll = randSeedInt(100); const starterTier: number | [number, number] = - roll > 65 ? 6 : roll > 15 ? 7 : roll > 5 ? 8 : [9, 10]; + roll >= 65 ? 6 : roll >= 15 ? 7 : roll >= 5 ? 8 : [9, 10]; const bossSpecies = getPokemonSpecies(getRandomSpeciesByStarterTier(starterTier, excludedBosses, bossTypes)); const pokemonConfig: EnemyPokemonConfig = { species: bossSpecies, diff --git a/src/data/mystery-encounters/encounters/delibirdy-encounter.ts b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts index ed9344d3c95..25959abe19e 100644 --- a/src/data/mystery-encounters/encounters/delibirdy-encounter.ts +++ b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts @@ -4,9 +4,9 @@ import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifi import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { Species } from "#enums/species"; import BattleScene from "#app/battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; -import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; -import { CombinationPokemonRequirement, HeldItemRequirement, MoneyRequirement } from "../mystery-encounter-requirements"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { CombinationPokemonRequirement, HeldItemRequirement, MoneyRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import { getEncounterText, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; @@ -33,6 +33,8 @@ const OPTION_3_DISALLOWED_MODIFIERS = [ "PokemonBaseStatTotalModifier" ]; +const DELIBIRDY_MONEY_PRICE_MULTIPLIER = 1.5; + /** * Delibird-y encounter. * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3804 | GitHub Issue #3804} @@ -42,7 +44,7 @@ export const DelibirdyEncounter: MysteryEncounter = MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.DELIBIRDY) .withEncounterTier(MysteryEncounterTier.GREAT) .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) - .withSceneRequirement(new MoneyRequirement(0, 2)) // Must have enough money for it to spawn at the very least + .withSceneRequirement(new MoneyRequirement(0, DELIBIRDY_MONEY_PRICE_MULTIPLIER)) // Must have enough money for it to spawn at the very least .withPrimaryPokemonRequirement(new CombinationPokemonRequirement( // Must also have either option 2 or 3 available to spawn new HeldItemRequirement(OPTION_2_ALLOWED_MODIFIERS), new HeldItemRequirement(OPTION_3_DISALLOWED_MODIFIERS, 1, true) @@ -93,12 +95,18 @@ export const DelibirdyEncounter: MysteryEncounter = .withOnInit((scene: BattleScene) => { const encounter = scene.currentBattle.mysteryEncounter!; encounter.setDialogueToken("delibirdName", getPokemonSpecies(Species.DELIBIRD).getName()); + + scene.loadBgm("mystery_encounter_delibirdy", "mystery_encounter_delibirdy.mp3"); + return true; + }) + .withOnVisualsStart((scene: BattleScene) => { + scene.fadeAndSwitchBgm("mystery_encounter_delibirdy"); return true; }) .withOption( MysteryEncounterOptionBuilder .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) - .withSceneMoneyRequirement(0, 2) // Must have money to spawn + .withSceneMoneyRequirement(0, DELIBIRDY_MONEY_PRICE_MULTIPLIER) // Must have money to spawn .withDialogue({ buttonLabel: `${namespace}.option.1.label`, buttonTooltip: `${namespace}.option.1.tooltip`, diff --git a/src/data/mystery-encounters/encounters/department-store-sale-encounter.ts b/src/data/mystery-encounters/encounters/department-store-sale-encounter.ts index e35ca08b6a0..104b46bce8a 100644 --- a/src/data/mystery-encounters/encounters/department-store-sale-encounter.ts +++ b/src/data/mystery-encounters/encounters/department-store-sale-encounter.ts @@ -9,7 +9,7 @@ import { Species } from "#enums/species"; import BattleScene from "#app/battle-scene"; import MysteryEncounter, { MysteryEncounterBuilder, -} from "../mystery-encounter"; +} from "#app/data/mystery-encounters/mystery-encounter"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; diff --git a/src/data/mystery-encounters/encounters/field-trip-encounter.ts b/src/data/mystery-encounters/encounters/field-trip-encounter.ts index e0101d60a2a..d03a3c1fcca 100644 --- a/src/data/mystery-encounters/encounters/field-trip-encounter.ts +++ b/src/data/mystery-encounters/encounters/field-trip-encounter.ts @@ -6,7 +6,7 @@ import { modifierTypes } from "#app/modifier/modifier-type"; import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "#app/battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { Stat } from "#enums/stat"; diff --git a/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts index 1861abcc7a4..470c4b96c82 100644 --- a/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts +++ b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts @@ -3,8 +3,8 @@ import { EnemyPartyConfig, initBattleWithEnemyConfig, loadCustomMovesForEncounte import { AttackTypeBoosterModifierType, modifierTypes, } from "#app/modifier/modifier-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "#app/battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; -import { TypeRequirement } from "../mystery-encounter-requirements"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { TypeRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import { Species } from "#enums/species"; import { getPokemonSpecies } from "#app/data/pokemon-species"; import { Gender } from "#app/data/gender"; diff --git a/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts b/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts index c163a2fc194..aa11a07f218 100644 --- a/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts +++ b/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts @@ -17,12 +17,12 @@ import { } from "#app/modifier/modifier-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "#app/battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; -import { MoveRequirement } from "../mystery-encounter-requirements"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MoveRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { TrainerSlot } from "#app/data/trainer-config"; -import { getSpriteKeysFromPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { getEncounterPokemonLevelForWave, getSpriteKeysFromPokemon, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import PokemonData from "#app/system/pokemon-data"; import { BattlerTagType } from "#enums/battler-tag-type"; import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; @@ -54,12 +54,11 @@ export const FightOrFlightEncounter: MysteryEncounter = const encounter = scene.currentBattle.mysteryEncounter!; // Calculate boss mon - const level = (scene.currentBattle.enemyLevels?.[0] ?? scene.currentBattle.waveIndex) + Math.max(Math.round((scene.currentBattle.waveIndex / 10)), 0); + const level = getEncounterPokemonLevelForWave(scene, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER); const bossSpecies = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getParty()), true); const bossPokemon = new EnemyPokemon(scene, bossSpecies, level, TrainerSlot.NONE, true); encounter.setDialogueToken("enemyPokemon", bossPokemon.getNameToRender()); const config: EnemyPartyConfig = { - levelAdditiveMultiplier: 1, pokemonConfigs: [{ level: level, species: bossSpecies, @@ -69,7 +68,7 @@ export const FightOrFlightEncounter: MysteryEncounter = mysteryEncounterBattleEffects: (pokemon: Pokemon) => { queueEncounterMessage(pokemon.scene, `${namespace}.option.1.stat_boost`); // Randomly boost 1 stat 2 stages - pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [randSeedInt(5)], 2)); + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [randSeedInt(4, 1)], 2)); } }], }; diff --git a/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts b/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts index a544657e47c..9ca7c7c2865 100644 --- a/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts +++ b/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts @@ -1,7 +1,7 @@ import { leaveEncounterWithoutBattle, selectPokemonForOption, setEncounterRewards, transitionMysteryEncounterIntroVisuals, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "#app/battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { TrainerSlot } from "#app/data/trainer-config"; import Pokemon, { FieldPosition, PlayerPokemon } from "#app/field/pokemon"; @@ -84,12 +84,7 @@ export const FunAndGamesEncounter: MysteryEncounter = return true; }) .withOnVisualsStart((scene: BattleScene) => { - // Change the bgm - scene.fadeOutBgm(2000, false); - scene.time.delayedCall(2000, () => { - scene.playBgm("mystery_encounter_fun_and_games"); - }); - + scene.fadeAndSwitchBgm("mystery_encounter_fun_and_games"); return true; }) .withOption(MysteryEncounterOptionBuilder @@ -175,7 +170,9 @@ async function summonPlayerPokemon(scene: BattleScene) { const party = scene.getParty(); const chosenIndex = party.indexOf(playerPokemon); if (chosenIndex !== 0) { - [party[chosenIndex], party[0]] = [party[chosenIndex], party[chosenIndex]]; + const leadPokemon = party[0]; + party[0] = playerPokemon; + party[chosenIndex] = leadPokemon; } // Do trainer summon animation diff --git a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts index 1c5a1f009d9..55d4953d438 100644 --- a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts +++ b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts @@ -4,7 +4,7 @@ import { ModifierTier } from "#app/modifier/modifier-tier"; import { getPlayerModifierTypeOptions, ModifierPoolType, ModifierTypeOption, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "#app/battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { Species } from "#enums/species"; import PokemonSpecies, { allSpecies, getPokemonSpecies } from "#app/data/pokemon-species"; @@ -23,6 +23,7 @@ import { getPokeballAtlasKey, getPokeballTintColor, PokeballType } from "#app/da import { getEncounterText, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { trainerNamePools } from "#app/data/trainer-names"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; +import { addPokemonDataToDexAndValidateAchievements } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounter:globalTradeSystem"; @@ -118,17 +119,13 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = return true; }) .withOnVisualsStart((scene: BattleScene) => { - // Change the bgm - scene.fadeOutBgm(1500, false); - scene.time.delayedCall(1500, () => { - scene.playBgm(scene.currentBattle.mysteryEncounter!.misc.bgmKey); - }); - + scene.fadeAndSwitchBgm(scene.currentBattle.mysteryEncounter!.misc.bgmKey); return true; }) .withOption( MysteryEncounterOptionBuilder .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withHasDexProgress(true) .withDialogue({ buttonLabel: `${namespace}.option.1.label`, buttonTooltip: `${namespace}.option.1.tooltip`, @@ -200,6 +197,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = await doPokemonTradeSequence(scene, tradedPokemon, newPlayerPokemon); await showEncounterText(scene, `${namespace}.trade_received`, null, 0, true, 4000); scene.playBgm(encounter.misc.bgmKey); + await addPokemonDataToDexAndValidateAchievements(scene, newPlayerPokemon); await hideTradeBackground(scene); tradedPokemon.destroy(); @@ -210,6 +208,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = .withOption( MysteryEncounterOptionBuilder .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withHasDexProgress(true) .withDialogue({ buttonLabel: `${namespace}.option.2.label`, buttonTooltip: `${namespace}.option.2.tooltip`, @@ -218,8 +217,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = const encounter = scene.currentBattle.mysteryEncounter!; const onPokemonSelected = (pokemon: PlayerPokemon) => { // Randomly generate a Wonder Trade pokemon - // const randomTradeOption = generateTradeOption(scene.getParty().map(p => p.species)); - const randomTradeOption = getPokemonSpecies(Species.BURMY); + const randomTradeOption = generateTradeOption(scene.getParty().map(p => p.species)); const tradePokemon = new EnemyPokemon(scene, randomTradeOption, pokemon.level, TrainerSlot.NONE, false); // Extra shiny roll at 1/128 odds (boosted by events and charms) if (!tradePokemon.shiny) { @@ -280,7 +278,8 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = await showTradeBackground(scene); await doPokemonTradeSequence(scene, tradedPokemon, newPlayerPokemon); await showEncounterText(scene, `${namespace}.trade_received`, null, 0, true, 4000); - scene.playBgm(scene.currentBattle.mysteryEncounter!.misc.bgmKey); + scene.playBgm(encounter.misc.bgmKey); + await addPokemonDataToDexAndValidateAchievements(scene, newPlayerPokemon); await hideTradeBackground(scene); tradedPokemon.destroy(); @@ -301,7 +300,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = const onPokemonSelected = (pokemon: PlayerPokemon) => { // Get Pokemon held items and filter for valid ones const validItems = pokemon.getHeldItems().filter((it) => { - return it.isTransferrable; + return it.isTransferable; }); return validItems.map((modifier: PokemonHeldItemModifier) => { @@ -322,7 +321,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = const selectableFilter = (pokemon: Pokemon) => { // If pokemon has items to trade const meetsReqs = pokemon.getHeldItems().filter((it) => { - return it.isTransferrable; + return it.isTransferable; }).length > 0; if (!meetsReqs) { return getEncounterText(scene, `${namespace}.option.3.invalid_selection`) ?? null; diff --git a/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts b/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts index 16568d8cb7d..509ffb11b26 100644 --- a/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts +++ b/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts @@ -3,8 +3,8 @@ import { Moves } from "#app/enums/moves"; import { Species } from "#app/enums/species"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "#app/battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; -import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { leaveEncounterWithoutBattle, setEncounterExp } from "../utils/encounter-phase-utils"; import { applyDamageToPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; diff --git a/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts b/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts index 71a44bd6852..ac257a8975f 100644 --- a/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts +++ b/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts @@ -15,7 +15,7 @@ import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { PartyMemberStrength } from "#enums/party-member-strength"; import BattleScene from "#app/battle-scene"; import * as Utils from "#app/utils"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; @@ -75,7 +75,7 @@ export const MysteriousChallengersEncounter: MysteryEncounter = const hardSpriteKey = hardConfig.getSpriteKey(female, hardConfig.doubleOnly); encounter.enemyPartyConfigs.push({ trainerConfig: hardConfig, - levelAdditiveMultiplier: 1, + levelAdditiveModifier: 1, female: female, }); @@ -98,7 +98,7 @@ export const MysteriousChallengersEncounter: MysteryEncounter = const brutalSpriteKey = brutalConfig.getSpriteKey(female, brutalConfig.doubleOnly); encounter.enemyPartyConfigs.push({ trainerConfig: brutalConfig, - levelAdditiveMultiplier: 1.5, + levelAdditiveModifier: 1.5, female: female, }); diff --git a/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts b/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts index 26e846ed874..18b2db53ba2 100644 --- a/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts +++ b/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts @@ -5,8 +5,8 @@ import { ModifierTier } from "#app/modifier/modifier-tier"; import { randSeedInt } from "#app/utils"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "#app/battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; -import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { getPokemonSpecies } from "#app/data/pokemon-species"; @@ -68,7 +68,7 @@ export const MysteriousChestEncounter: MysteryEncounter = // Calculate boss mon const config: EnemyPartyConfig = { - levelAdditiveMultiplier: 0.5, + levelAdditiveModifier: 0.5, disableSwitch: true, pokemonConfigs: [ { @@ -179,6 +179,7 @@ export const MysteriousChestEncounter: MysteryEncounter = encounter.setDialogueToken("pokeName", highestLevelPokemon.getNameToRender()); await showEncounterText(scene, `${namespace}.option.1.bad`); transitionMysteryEncounterIntroVisuals(scene, true, true, 500); + setEncounterRewards(scene, { fillRemaining: true }); await initBattleWithEnemyConfig(scene, encounter.enemyPartyConfigs[0]); } } diff --git a/src/data/mystery-encounters/encounters/part-timer-encounter.ts b/src/data/mystery-encounters/encounters/part-timer-encounter.ts index 2abbb53c333..f5486d34ea9 100644 --- a/src/data/mystery-encounters/encounters/part-timer-encounter.ts +++ b/src/data/mystery-encounters/encounters/part-timer-encounter.ts @@ -2,8 +2,8 @@ import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/myst import { leaveEncounterWithoutBattle, selectPokemonForOption, setEncounterExp, setEncounterRewards, transitionMysteryEncounterIntroVisuals, updatePlayerMoney } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "#app/battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; -import { MoveRequirement } from "../mystery-encounter-requirements"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MoveRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { Stat } from "#enums/stat"; diff --git a/src/data/mystery-encounters/encounters/safari-zone-encounter.ts b/src/data/mystery-encounters/encounters/safari-zone-encounter.ts index 49abf98cf49..2690460757f 100644 --- a/src/data/mystery-encounters/encounters/safari-zone-encounter.ts +++ b/src/data/mystery-encounters/encounters/safari-zone-encounter.ts @@ -1,7 +1,7 @@ import { initSubsequentOptionSelect, leaveEncounterWithoutBattle, transitionMysteryEncounterIntroVisuals, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "#app/battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import MysteryEncounterOption, { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { TrainerSlot } from "#app/data/trainer-config"; import { HiddenAbilityRateBoosterModifier, IvScannerModifier } from "#app/modifier/modifier"; @@ -25,7 +25,7 @@ const namespace = "mysteryEncounter:safariZone"; const TRAINER_THROW_ANIMATION_TIMES = [512, 184, 768]; -const SAFARI_MONEY_MULTIPLIER = 2.75; +const SAFARI_MONEY_MULTIPLIER = 2; /** * Safari Zone encounter. diff --git a/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts b/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts index 8ee4782def5..933f184351a 100644 --- a/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts +++ b/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts @@ -5,9 +5,9 @@ import { randSeedInt } from "#app/utils"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { Species } from "#enums/species"; import BattleScene from "#app/battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; -import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; -import { MoneyRequirement } from "../mystery-encounter-requirements"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { MoneyRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import { getEncounterText, queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { applyDamageToPokemon, applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; @@ -19,6 +19,9 @@ import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; /** the i18n namespace for this encounter */ const namespace = "mysteryEncounter:shadyVitaminDealer"; +const VITAMIN_DEALER_CHEAP_PRICE_MULTIPLIER = 1.5; +const VITAMIN_DEALER_EXPENSIVE_PRICE_MULTIPLIER = 3.5; + /** * Shady Vitamin Dealer encounter. * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3798 | GitHub Issue #3798} @@ -28,7 +31,7 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter = MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.SHADY_VITAMIN_DEALER) .withEncounterTier(MysteryEncounterTier.COMMON) .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) - .withSceneRequirement(new MoneyRequirement(0, 1.5)) // Must have the money for at least the cheap deal + .withSceneRequirement(new MoneyRequirement(0, VITAMIN_DEALER_CHEAP_PRICE_MULTIPLIER)) // Must have the money for at least the cheap deal .withPrimaryPokemonHealthRatioRequirement([0.5, 1]) // At least 1 Pokemon must have above half HP .withIntroSpriteConfigs([ { @@ -64,7 +67,7 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter = .withOption( MysteryEncounterOptionBuilder .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) - .withSceneMoneyRequirement(0, 1.5) + .withSceneMoneyRequirement(0, VITAMIN_DEALER_CHEAP_PRICE_MULTIPLIER) .withDialogue({ buttonLabel: `${namespace}.option.1.label`, buttonTooltip: `${namespace}.option.1.tooltip`, @@ -115,7 +118,7 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter = await applyModifierTypeToPlayerPokemon(scene, chosenPokemon, modType); } - leaveEncounterWithoutBattle(scene); + leaveEncounterWithoutBattle(scene, true); }) .withPostOptionPhase(async (scene: BattleScene) => { // Damage and status applied after dealer leaves (to make thematic sense) @@ -142,7 +145,7 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter = .withOption( MysteryEncounterOptionBuilder .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) - .withSceneMoneyRequirement(0, 3.5) + .withSceneMoneyRequirement(0, VITAMIN_DEALER_EXPENSIVE_PRICE_MULTIPLIER) .withDialogue({ buttonLabel: `${namespace}.option.2.label`, buttonTooltip: `${namespace}.option.2.tooltip`, @@ -193,7 +196,7 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter = await applyModifierTypeToPlayerPokemon(scene, chosenPokemon, modType); } - leaveEncounterWithoutBattle(scene); + leaveEncounterWithoutBattle(scene, true); }) .withPostOptionPhase(async (scene: BattleScene) => { // Status applied after dealer leaves (to make thematic sense) diff --git a/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts b/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts index b9f08b12ffd..bfccc46ee0f 100644 --- a/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts +++ b/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts @@ -1,22 +1,24 @@ import { STEALING_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups"; -import { modifierTypes } from "#app/modifier/modifier-type"; +import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { Species } from "#enums/species"; import BattleScene from "#app/battle-scene"; import { StatusEffect } from "#app/data/status-effect"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; -import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; -import { MoveRequirement } from "../mystery-encounter-requirements"; -import { EnemyPartyConfig, EnemyPokemonConfig, initBattleWithEnemyConfig, loadCustomMovesForEncounter, leaveEncounterWithoutBattle, setEncounterExp, setEncounterRewards, } from "../utils/encounter-phase-utils"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { MoveRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; +import { EnemyPartyConfig, EnemyPokemonConfig, generateModifierType, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, loadCustomMovesForEncounter, setEncounterExp, setEncounterRewards, } from "../utils/encounter-phase-utils"; import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { Moves } from "#enums/moves"; import { BattlerIndex } from "#app/battle"; -import { PokemonMove } from "#app/field/pokemon"; +import { AiType, PokemonMove } from "#app/field/pokemon"; import { getPokemonSpecies } from "#app/data/pokemon-species"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { PartyHealPhase } from "#app/phases/party-heal-phase"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; +import { BerryType } from "#enums/berry-type"; +import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data"; /** i18n namespace for the encounter */ const namespace = "mysteryEncounter:slumberingSnorlax"; @@ -38,7 +40,7 @@ export const SlumberingSnorlaxEncounter: MysteryEncounter = fileRoot: "pokemon", hasShadow: true, tint: 0.25, - scale: 1.5, + scale: 1.25, repeat: true, y: 5, }, @@ -58,10 +60,22 @@ export const SlumberingSnorlaxEncounter: MysteryEncounter = species: bossSpecies, isBoss: true, status: [StatusEffect.SLEEP, 5], // Extra turns on timer for Snorlax's start of fight moves - moveSet: [Moves.REST, Moves.SLEEP_TALK, Moves.CRUNCH, Moves.GIGA_IMPACT] + moveSet: [Moves.REST, Moves.SLEEP_TALK, Moves.CRUNCH, Moves.GIGA_IMPACT], + modifierConfigs: [ + { + modifier: generateModifierType(scene, modifierTypes.BERRY, [BerryType.SITRUS]) as PokemonHeldItemModifierType, + stackCount: 2 + }, + { + modifier: generateModifierType(scene, modifierTypes.BERRY, [BerryType.ENIGMA]) as PokemonHeldItemModifierType, + stackCount: 2 + }, + ], + mysteryEncounterPokemonData: new MysteryEncounterPokemonData({ spriteScale: 1.25 }), + aiType: AiType.SMART // Required to ensure Snorlax uses Sleep Talk while it is asleep }; const config: EnemyPartyConfig = { - levelAdditiveMultiplier: 0.5, + levelAdditiveModifier: 0.5, pokemonConfigs: [pokemonConfig], }; encounter.enemyPartyConfigs = [config]; diff --git a/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts b/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts index bf976458fdd..10b0e5222b3 100644 --- a/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts +++ b/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts @@ -2,8 +2,8 @@ import { EnemyPartyConfig, generateModifierTypeOption, initBattleWithEnemyConfig import { randSeedInt } from "#app/utils"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "#app/battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; -import { MoneyRequirement, WaveModulusRequirement } from "../mystery-encounter-requirements"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MoneyRequirement, WaveModulusRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import Pokemon, { EnemyPokemon } from "#app/field/pokemon"; import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; @@ -20,12 +20,13 @@ import { getPokemonNameWithAffix } from "#app/messages"; import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; import { Stat } from "#enums/stat"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; +import { getEncounterPokemonLevelForWave, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; /** the i18n namespace for this encounter */ const namespace = "mysteryEncounter:teleportingHijinks"; -const MONEY_COST_MULTIPLIER = 2.5; -const BIOME_CANDIDATES = [Biome.SPACE, Biome.FAIRY_CAVE, Biome.LABORATORY, Biome.ISLAND]; +const MONEY_COST_MULTIPLIER = 1.75; +const BIOME_CANDIDATES = [Biome.SPACE, Biome.FAIRY_CAVE, Biome.LABORATORY, Biome.ISLAND, Biome.WASTELAND, Biome.DOJO]; const MACHINE_INTERFACING_TYPES = [Type.ELECTRIC, Type.STEEL]; /** @@ -130,7 +131,7 @@ export const TeleportingHijinksEncounter: MysteryEncounter = const encounter = scene.currentBattle.mysteryEncounter!; // Init enemy - const level = (scene.currentBattle.enemyLevels?.[0] ?? scene.currentBattle.waveIndex) + Math.max(Math.round((scene.currentBattle.waveIndex / 10)), 0); + const level = getEncounterPokemonLevelForWave(scene, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER); const bossSpecies = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getParty()), true); const bossPokemon = new EnemyPokemon(scene, bossSpecies, level, TrainerSlot.NONE, true); encounter.setDialogueToken("enemyPokemon", getPokemonNameWithAffix(bossPokemon)); @@ -166,7 +167,7 @@ async function doBiomeTransitionDialogueAndBattleInit(scene: BattleScene) { await showEncounterText(scene, `${namespace}.attacked`); // Init enemy - const level = (scene.currentBattle.enemyLevels?.[0] ?? scene.currentBattle.waveIndex) + Math.max(Math.round((scene.currentBattle.waveIndex / 10)), 0); + const level = getEncounterPokemonLevelForWave(scene, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER); const bossSpecies = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getParty()), true); const bossPokemon = new EnemyPokemon(scene, bossSpecies, level, TrainerSlot.NONE, true); encounter.setDialogueToken("enemyPokemon", getPokemonNameWithAffix(bossPokemon)); diff --git a/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts b/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts index 16b0c421bd4..c26c6aa3b7f 100644 --- a/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts @@ -2,8 +2,8 @@ import { leaveEncounterWithoutBattle, transitionMysteryEncounterIntroVisuals, up import { isNullOrUndefined, randSeedInt } from "#app/utils"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "#app/battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; -import { MoneyRequirement } from "../mystery-encounter-requirements"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MoneyRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import { catchPokemon, getRandomSpeciesByStarterTier, getSpriteKeysFromPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { getPokemonSpecies, speciesStarters } from "#app/data/pokemon-species"; import { Species } from "#enums/species"; @@ -15,11 +15,15 @@ import PokemonData from "#app/system/pokemon-data"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; +import { Abilities } from "#enums/abilities"; /** the i18n namespace for this encounter */ const namespace = "mysteryEncounter:pokemonSalesman"; -const MAX_POKEMON_PRICE_MULTIPLIER = 6; +const MAX_POKEMON_PRICE_MULTIPLIER = 4; + +/** Odds of shiny magikarp will be 1/value */ +const SHINY_MAGIKARP_WEIGHT = 100; /** * Pokemon Salesman encounter. @@ -58,12 +62,12 @@ export const ThePokemonSalesmanEncounter: MysteryEncounter = const tries = 0; // Reroll any species that don't have HAs - while (isNullOrUndefined(species.abilityHidden) && tries < 5) { + while ((isNullOrUndefined(species.abilityHidden) || species.abilityHidden === Abilities.NONE) && tries < 5) { species = getPokemonSpecies(getRandomSpeciesByStarterTier([0, 5])); } let pokemon: PlayerPokemon; - if (isNullOrUndefined(species.abilityHidden) || randSeedInt(100) === 0) { + if (randSeedInt(SHINY_MAGIKARP_WEIGHT) === 0 || isNullOrUndefined(species.abilityHidden) || species.abilityHidden === Abilities.NONE) { // If no HA mon found or you roll 1%, give shiny Magikarp species = getPokemonSpecies(Species.MAGIKARP); const hiddenIndex = species.ability2 ? 2 : 1; diff --git a/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts b/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts index 047aa0d83f6..55cb10644e8 100644 --- a/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts @@ -2,7 +2,7 @@ import { EnemyPartyConfig, initBattleWithEnemyConfig, loadCustomMovesForEncounte import { modifierTypes, PokemonHeldItemModifierType, } from "#app/modifier/modifier-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "#app/battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import { getPokemonSpecies } from "#app/data/pokemon-species"; import { Species } from "#enums/species"; import { Nature } from "#app/data/nature"; @@ -36,6 +36,7 @@ export const TheStrongStuffEncounter: MysteryEncounter = .withEncounterTier(MysteryEncounterTier.GREAT) .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) .withScenePartySizeRequirement(3, 6) // Must have at least 3 pokemon in party + .withMaxAllowedEncounters(1) .withHideWildIntroMessage(true) .withAutoHideIntroVisuals(false) .withIntroSpriteConfigs([ @@ -70,7 +71,7 @@ export const TheStrongStuffEncounter: MysteryEncounter = // Calculate boss mon const config: EnemyPartyConfig = { - levelAdditiveMultiplier: 1, + levelAdditiveModifier: 1, disableSwitch: true, pokemonConfigs: [ { @@ -159,6 +160,11 @@ export const TheStrongStuffEncounter: MysteryEncounter = encounter.setDialogueToken("increaseValue", BST_INCREASE_VALUE.toString()); await showEncounterText(scene, `${namespace}.option.1.selected_2`, null, undefined, true); + encounter.dialogue.outro = [ + { + text: `${namespace}.outro`, + } + ]; setEncounterRewards(scene, { fillRemaining: true }); leaveEncounterWithoutBattle(scene, true); return true; @@ -192,6 +198,7 @@ export const TheStrongStuffEncounter: MysteryEncounter = ignorePp: true }); + encounter.dialogue.outro = []; transitionMysteryEncounterIntroVisuals(scene, true, true, 500); await initBattleWithEnemyConfig(scene, encounter.enemyPartyConfigs[0]); } diff --git a/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts b/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts index 902aefcb490..60061efbc7a 100644 --- a/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts @@ -2,7 +2,7 @@ import { EnemyPartyConfig, generateModifierType, generateModifierTypeOption, ini import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "#app/battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { TrainerType } from "#enums/trainer-type"; import { Species } from "#enums/species"; @@ -146,7 +146,7 @@ async function spawnNextTrainerOrEndEncounter(scene: BattleScene) { // Give 10x Voucher const newModifier = modifierTypes.VOUCHER_PREMIUM().newModifier(); - scene.addModifier(newModifier); + await scene.addModifier(newModifier); scene.playSound("item_fanfare"); await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: newModifier?.type.name })); diff --git a/src/data/mystery-encounters/encounters/training-session-encounter.ts b/src/data/mystery-encounters/encounters/training-session-encounter.ts index 6c0f1706fa5..33d841b7f02 100644 --- a/src/data/mystery-encounters/encounters/training-session-encounter.ts +++ b/src/data/mystery-encounters/encounters/training-session-encounter.ts @@ -3,7 +3,7 @@ import { EnemyPartyConfig, initBattleWithEnemyConfig, leaveEncounterWithoutBattl import { getNatureName, Nature } from "#app/data/nature"; import { speciesStarters } from "#app/data/pokemon-species"; import Pokemon, { PlayerPokemon } from "#app/field/pokemon"; -import { PokemonFormChangeItemModifier, PokemonHeldItemModifier } from "#app/modifier/modifier"; +import { PokemonHeldItemModifier } from "#app/modifier/modifier"; import { AbilityAttr } from "#app/system/game-data"; import PokemonData from "#app/system/pokemon-data"; import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; @@ -11,8 +11,8 @@ import { isNullOrUndefined, randSeedShuffle } from "#app/utils"; import { BattlerTagType } from "#enums/battler-tag-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "#app/battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; -import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { getEncounterText, queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; @@ -34,6 +34,7 @@ export const TrainingSessionEncounter: MysteryEncounter = .withEncounterTier(MysteryEncounterTier.ULTRA) .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) .withScenePartySizeRequirement(2, 6, true) // Must have at least 2 unfainted pokemon in party + .withFleeAllowed(false) .withHideWildIntroMessage(true) .withIntroSpriteConfigs([ { @@ -97,12 +98,7 @@ export const TrainingSessionEncounter: MysteryEncounter = 5 ); const modifiers = new ModifiersHolder(); - const config = getEnemyConfig( - scene, - playerPokemon, - segments, - modifiers - ); + const config = getEnemyConfig(scene, playerPokemon, segments, modifiers); scene.removePokemonFromPlayerParty(playerPokemon, false); const onBeforeRewardsPhase = () => { @@ -163,6 +159,7 @@ export const TrainingSessionEncounter: MysteryEncounter = // Add pokemon and mods back scene.getParty().push(playerPokemon); for (const mod of modifiers.value) { + mod.pokemonId = playerPokemon.id; scene.addModifier(mod, true, false, false, true); } scene.updateModifiers(true); @@ -230,17 +227,9 @@ export const TrainingSessionEncounter: MysteryEncounter = // Spawn medium training session with chosen pokemon // Every 40 waves, add +1 boss segment, capping at 6 - const segments = Math.min( - 2 + Math.floor(scene.currentBattle.waveIndex / 40), - 6 - ); + const segments = Math.min(2 + Math.floor(scene.currentBattle.waveIndex / 40), 6); const modifiers = new ModifiersHolder(); - const config = getEnemyConfig( - scene, - playerPokemon, - segments, - modifiers - ); + const config = getEnemyConfig(scene, playerPokemon, segments, modifiers); scene.removePokemonFromPlayerParty(playerPokemon, false); const onBeforeRewardsPhase = () => { @@ -377,6 +366,7 @@ export const TrainingSessionEncounter: MysteryEncounter = // Add pokemon and mods back scene.getParty().push(playerPokemon); for (const mod of modifiers.value) { + mod.pokemonId = playerPokemon.id; scene.addModifier(mod, true, false, false, true); } scene.updateModifiers(true); @@ -410,10 +400,12 @@ function getEnemyConfig(scene: BattleScene, playerPokemon: PlayerPokemon, segmen playerPokemon.resetSummonData(); // Passes modifiers by reference - modifiers.value = playerPokemon.getHeldItems().filter(m => !(m instanceof PokemonFormChangeItemModifier)); + modifiers.value = playerPokemon.getHeldItems(); const modifierConfigs = modifiers.value.map((mod) => { return { - modifier: mod + modifier: mod.clone(), + isTransferable: false, + stackCount: mod.stackCount }; }) as HeldModifierConfig[]; diff --git a/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts b/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts index ec6291f2a8c..d295c8ab548 100644 --- a/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts +++ b/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts @@ -2,8 +2,8 @@ import { EnemyPartyConfig, EnemyPokemonConfig, generateModifierType, initBattleW import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "#app/battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; -import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { Species } from "#enums/species"; @@ -67,7 +67,7 @@ export const TrashToTreasureEncounter: MysteryEncounter = moveSet: [Moves.PAYBACK, Moves.GUNK_SHOT, Moves.STOMPING_TANTRUM, Moves.DRAIN_PUNCH] }; const config: EnemyPartyConfig = { - levelAdditiveMultiplier: 1, + levelAdditiveModifier: 1, pokemonConfigs: [pokemonConfig], disableSwitch: true }; diff --git a/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts b/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts index f9148b87f9b..0816c9cd2a6 100644 --- a/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts +++ b/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts @@ -5,8 +5,8 @@ import Pokemon, { EnemyPokemon, PokemonMove } from "#app/field/pokemon"; import { getPartyLuckValue } from "#app/modifier/modifier-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "#app/battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; -import { MoveRequirement, PersistentModifierRequirement } from "../mystery-encounter-requirements"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MoveRequirement, PersistentModifierRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { TrainerSlot } from "#app/data/trainer-config"; diff --git a/src/data/mystery-encounters/encounters/weird-dream-encounter.ts b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts index 476cc98f503..ed8986d99bb 100644 --- a/src/data/mystery-encounters/encounters/weird-dream-encounter.ts +++ b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts @@ -2,8 +2,8 @@ import { Type } from "#app/data/type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { Species } from "#enums/species"; import BattleScene from "#app/battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; -import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { leaveEncounterWithoutBattle, setEncounterRewards, } from "../utils/encounter-phase-utils"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; @@ -14,7 +14,7 @@ import { HiddenAbilityRateBoosterModifier, PokemonFormChangeItemModifier, Pokemo import { achvs } from "#app/system/achv"; import { speciesEggMoves } from "#app/data/egg-moves"; import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data"; -import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { modifierTypes } from "#app/modifier/modifier-type"; import i18next from "#app/plugins/i18n"; import { doPokemonTransformationSequence, TransformationScreenPosition } from "#app/data/mystery-encounters/utils/encounter-transformation-sequence"; @@ -130,12 +130,7 @@ export const WeirdDreamEncounter: MysteryEncounter = return true; }) .withOnVisualsStart((scene: BattleScene) => { - // Change the bgm - scene.fadeOutBgm(3000, false); - scene.time.delayedCall(3000, () => { - scene.playBgm("mystery_encounter_weird_dream"); - }); - + scene.fadeAndSwitchBgm("mystery_encounter_weird_dream"); return true; }) .withOption( @@ -340,7 +335,7 @@ async function doNewTeamPostProcess(scene: BattleScene, transformations: Pokemon const newStarterUnlocked = await scene.gameData.setPokemonCaught(newPokemon, true, false, false); if (newStarterUnlocked) { atLeastOneNewStarter = true; - queueEncounterMessage(scene, i18next.t("battle:addedAsAStarter", { pokemonName: getPokemonSpecies(speciesRootForm).getName() })); + await showEncounterText(scene, i18next.t("battle:addedAsAStarter", { pokemonName: getPokemonSpecies(speciesRootForm).getName() })); } } diff --git a/src/data/mystery-encounters/mystery-encounter-option.ts b/src/data/mystery-encounters/mystery-encounter-option.ts index fb3daf53a8b..865877445c1 100644 --- a/src/data/mystery-encounters/mystery-encounter-option.ts +++ b/src/data/mystery-encounters/mystery-encounter-option.ts @@ -3,7 +3,7 @@ import { Moves } from "#app/enums/moves"; import Pokemon, { PlayerPokemon } from "#app/field/pokemon"; import BattleScene from "#app/battle-scene"; import { Type } from "../type"; -import { EncounterPokemonRequirement, EncounterSceneRequirement, MoneyRequirement, TypeRequirement } from "./mystery-encounter-requirements"; +import { EncounterPokemonRequirement, EncounterSceneRequirement, MoneyRequirement, TypeRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import { CanLearnMoveRequirement, CanLearnMoveRequirementOptions } from "./requirements/can-learn-move-requirement"; import { isNullOrUndefined, randSeedInt } from "#app/utils"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; diff --git a/src/data/mystery-encounters/mystery-encounter.ts b/src/data/mystery-encounters/mystery-encounter.ts index a204ea848da..2a5f6fda7e1 100644 --- a/src/data/mystery-encounters/mystery-encounter.ts +++ b/src/data/mystery-encounters/mystery-encounter.ts @@ -25,6 +25,9 @@ export interface EncounterStartOfBattleEffect { followUp?: boolean; } +const DEFAULT_MAX_ALLOWED_ENCOUNTERS = 2; +const DEFAULT_MAX_ALLOWED_ROGUE_ENCOUNTERS = 1; + /** * Used by {@linkcode MysteryEncounterBuilder} class to define required/optional properties on the {@linkcode MysteryEncounter} class when building. * @@ -42,6 +45,7 @@ export interface IMysteryEncounter { autoHideIntroVisuals: boolean; enterIntroVisualsFromRight: boolean; catchAllowed: boolean; + fleeAllowed: boolean; continuousEncounter: boolean; maxAllowedEncounters: number; hasBattleAnimationsWithoutTargets: boolean; @@ -110,6 +114,11 @@ export default class MysteryEncounter implements IMysteryEncounter { * Default false */ catchAllowed: boolean; + /** + * If true, allows fleeing from a wild encounter (trainer battle MEs auto-disable fleeing) + * Default true + */ + fleeAllowed: boolean; /** * If true, encounter will continuously run through multiple battles/puzzles/etc. instead of going to next wave * MUST EVENTUALLY BE DISABLED TO CONTINUE TO NEXT WAVE @@ -246,8 +255,8 @@ export default class MysteryEncounter implements IMysteryEncounter { this.encounterTier = this.encounterTier ?? MysteryEncounterTier.COMMON; this.dialogue = this.dialogue ?? {}; this.spriteConfigs = this.spriteConfigs ? [...this.spriteConfigs] : []; - // Default max is 1 for ROGUE encounters, 3 for others - this.maxAllowedEncounters = this.maxAllowedEncounters ?? this.encounterTier === MysteryEncounterTier.ROGUE ? 1 : 3; + // Default max is 1 for ROGUE encounters, 2 for others + this.maxAllowedEncounters = this.maxAllowedEncounters ?? this.encounterTier === MysteryEncounterTier.ROGUE ? DEFAULT_MAX_ALLOWED_ROGUE_ENCOUNTERS : DEFAULT_MAX_ALLOWED_ENCOUNTERS; this.encounterMode = MysteryEncounterMode.DEFAULT; this.requirements = this.requirements ? this.requirements : []; this.hideBattleIntroMessage = this.hideBattleIntroMessage ?? false; @@ -520,6 +529,7 @@ export class MysteryEncounterBuilder implements Partial { enterIntroVisualsFromRight: boolean = false; continuousEncounter: boolean = false; catchAllowed: boolean = false; + fleeAllowed: boolean = true; lockEncounterRewardTiers: boolean = false; startOfBattleEffectsComplete: boolean = false; hasBattleAnimationsWithoutTargets: boolean = false; @@ -580,8 +590,8 @@ export class MysteryEncounterBuilder implements Partial { * There should be at least 2 options defined and no more than 4. * If complex use {@linkcode MysteryEncounterBuilder.withOption} * - * @param dialogue - {@linkcode OptionTextDisplay} - * @param callback - {@linkcode OptionPhaseCallback} + * @param dialogue {@linkcode OptionTextDisplay} + * @param callback {@linkcode OptionPhaseCallback} * @returns */ withSimpleDexProgressOption(dialogue: OptionTextDisplay, callback: OptionPhaseCallback): this & Pick { @@ -732,7 +742,7 @@ export class MysteryEncounterBuilder implements Partial { * * @param min min wave (or exact size if only min is given) * @param max optional max size. If not given, defaults to min => exact wave - * @param excludeFainted - if true, only counts unfainted mons + * @param excludeFainted if true, only counts unfainted mons * @returns */ withScenePartySizeRequirement(min: number, max?: number, excludeFainted: boolean = false): this & Required> { @@ -798,7 +808,7 @@ export class MysteryEncounterBuilder implements Partial { * NOTE: If rewards are dependent on options selected, runtime data, etc., * It may be better to programmatically set doEncounterRewards elsewhere. * There is a helper function in mystery-encounter utils, setEncounterRewards(), which can be called programmatically to set rewards - * @param doEncounterRewards - synchronous callback function to perform during rewards phase of the encounter + * @param doEncounterRewards Synchronous callback function to perform during rewards phase of the encounter * @returns */ withRewards(doEncounterRewards: (scene: BattleScene) => boolean): this & Required> { @@ -812,7 +822,7 @@ export class MysteryEncounterBuilder implements Partial { * NOTE: If rewards are dependent on options selected, runtime data, etc., * It may be better to programmatically set doEncounterExp elsewhere. * There is a helper function in mystery-encounter utils, setEncounterExp(), which can be called programmatically to set rewards - * @param doEncounterExp - synchronous callback function to perform during rewards phase of the encounter + * @param doEncounterExp Synchronous callback function to perform during rewards phase of the encounter * @returns */ withExp(doEncounterExp: (scene: BattleScene) => boolean): this & Required> { @@ -823,7 +833,7 @@ export class MysteryEncounterBuilder implements Partial { * Can be used to perform init logic before intro visuals are shown and before the MysteryEncounterPhase begins * Useful for performing things like procedural generation of intro sprites, etc. * - * @param onInit - synchronous callback function to perform as soon as the encounter is selected for the next phase + * @param onInit Synchronous callback function to perform as soon as the encounter is selected for the next phase * @returns */ withOnInit(onInit: (scene: BattleScene) => boolean): this & Required> { @@ -833,7 +843,7 @@ export class MysteryEncounterBuilder implements Partial { /** * Can be used to perform some extra logic (usually animations) when the enemy field is finished sliding in * - * @param onVisualsStart - synchronous callback function to perform as soon as the enemy field finishes sliding in + * @param onVisualsStart Synchronous callback function to perform as soon as the enemy field finishes sliding in * @returns */ withOnVisualsStart(onVisualsStart: (scene: BattleScene) => boolean): this & Required> { @@ -843,7 +853,7 @@ export class MysteryEncounterBuilder implements Partial { /** * Can set whether catching is allowed or not on the encounter * This flag can also be programmatically set inside option event functions or elsewhere - * @param catchAllowed - if true, allows enemy pokemon to be caught during the encounter + * @param catchAllowed If `true`, allows enemy pokemon to be caught during the encounter * @returns */ withCatchAllowed(catchAllowed: boolean): this & Required> { @@ -851,7 +861,16 @@ export class MysteryEncounterBuilder implements Partial { } /** - * @param hideBattleIntroMessage - if true, will not show the trainerAppeared/wildAppeared/bossAppeared message for an encounter + * Can set whether fleeing is allowed or not on the encounter + * @param fleeAllowed If `false`, prevents fleeing from a wild battle (trainer battle MEs already have flee disabled) + * @returns + */ + withFleeAllowed(fleeAllowed: boolean): this & Required> { + return Object.assign(this, { fleeAllowed }); + } + + /** + * @param hideBattleIntroMessage If `true`, will not show the trainerAppeared/wildAppeared/bossAppeared message for an encounter * @returns */ withHideWildIntroMessage(hideBattleIntroMessage: boolean): this & Required> { @@ -859,7 +878,7 @@ export class MysteryEncounterBuilder implements Partial { } /** - * @param autoHideIntroVisuals - if false, will not hide the intro visuals that are displayed at the beginning of encounter + * @param autoHideIntroVisuals If `false`, will not hide the intro visuals that are displayed at the beginning of encounter * @returns */ withAutoHideIntroVisuals(autoHideIntroVisuals: boolean): this & Required> { @@ -867,7 +886,7 @@ export class MysteryEncounterBuilder implements Partial { } /** - * @param enterIntroVisualsFromRight - If true, will slide in intro visuals from the right side of the screen. If false, slides in from left, as normal + * @param enterIntroVisualsFromRight If `true`, will slide in intro visuals from the right side of the screen. If false, slides in from left, as normal * Default false * @returns */ @@ -878,7 +897,7 @@ export class MysteryEncounterBuilder implements Partial { /** * Add a title for the encounter * - * @param title - title of the encounter + * @param title Title of the encounter * @returns */ withTitle(title: string): this { @@ -898,7 +917,7 @@ export class MysteryEncounterBuilder implements Partial { /** * Add a description of the encounter * - * @param description - description of the encounter + * @param description Description of the encounter * @returns */ withDescription(description: string): this { @@ -918,7 +937,7 @@ export class MysteryEncounterBuilder implements Partial { /** * Add a query for the encounter * - * @param query - query to use for the encounter + * @param query Query to use for the encounter * @returns */ withQuery(query: string): this { @@ -938,7 +957,7 @@ export class MysteryEncounterBuilder implements Partial { /** * Add outro dialogue/s for the encounter * - * @param dialogue - outro dialogue/s + * @param dialogue Outro dialogue(s) * @returns */ withOutroDialogue(dialogue: MysteryEncounterDialogue["outro"] = []): this { diff --git a/src/data/mystery-encounters/requirements/can-learn-move-requirement.ts b/src/data/mystery-encounters/requirements/can-learn-move-requirement.ts index bbdf2ee5a4d..a0b4edd4a36 100644 --- a/src/data/mystery-encounters/requirements/can-learn-move-requirement.ts +++ b/src/data/mystery-encounters/requirements/can-learn-move-requirement.ts @@ -2,7 +2,7 @@ import BattleScene from "#app/battle-scene"; import { Moves } from "#app/enums/moves"; import { PlayerPokemon, PokemonMove } from "#app/field/pokemon"; import { isNullOrUndefined } from "#app/utils"; -import { EncounterPokemonRequirement } from "../mystery-encounter-requirements"; +import { EncounterPokemonRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; /** * {@linkcode CanLearnMoveRequirement} options diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts index 2cd369fbaad..85d583fc898 100644 --- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -3,7 +3,7 @@ import { biomeLinks, BiomePoolTier } from "#app/data/biomes"; import MysteryEncounterOption from "#app/data/mystery-encounters/mystery-encounter-option"; import { AVERAGE_ENCOUNTERS_PER_RUN_TARGET, WEIGHT_INCREMENT_ON_SPAWN_MISS } from "#app/data/mystery-encounters/mystery-encounters"; import { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; -import Pokemon, { FieldPosition, PlayerPokemon, PokemonMove, PokemonSummonData } from "#app/field/pokemon"; +import Pokemon, { AiType, FieldPosition, PlayerPokemon, PokemonMove, PokemonSummonData } from "#app/field/pokemon"; import { CustomModifierSettings, ModifierPoolType, ModifierType, ModifierTypeGenerator, ModifierTypeOption, modifierTypes, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type"; import { MysteryEncounterBattlePhase, MysteryEncounterBattleStartCleanupPhase, MysteryEncounterPhase, MysteryEncounterRewardsPhase } from "#app/phases/mystery-encounter-phases"; import PokemonData from "#app/system/pokemon-data"; @@ -85,21 +85,24 @@ export interface EnemyPokemonConfig { modifierConfigs?: HeldModifierConfig[]; tags?: BattlerTagType[]; dataSource?: PokemonData; + aiType?: AiType; } export interface EnemyPartyConfig { - /** Formula for enemy: level += waveIndex / 10 * levelAdditive */ - levelAdditiveMultiplier?: number; + /** Formula for enemy level: level += waveIndex / 10 * levelAdditiveModifier */ + levelAdditiveModifier?: number; doubleBattle?: boolean; /** Generates trainer battle solely off trainer type */ trainerType?: TrainerType; /** More customizable option for configuring trainer battle */ trainerConfig?: TrainerConfig; pokemonConfigs?: EnemyPokemonConfig[]; - /** True for female trainer, false for male */ + /** `true` for female trainer, false for male */ female?: boolean; - /** True will prevent player from switching */ + /** `true` will prevent player from switching */ disableSwitch?: boolean; + /** `true` or leaving undefined will increment dex seen count for the encounter battle, `false` will not */ + countAsSeen?: boolean; } /** @@ -156,10 +159,10 @@ export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig: // ME levels are modified by an additive value that scales with wave index // Base scaling: Every 10 waves, modifier gets +1 level - // This can be amplified or counteracted by setting levelAdditiveMultiplier in config - // levelAdditiveMultiplier value of 0.5 will halve the modifier scaling, 2 will double it, etc. + // This can be amplified or counteracted by setting levelAdditiveModifier in config + // levelAdditiveModifier value of 0.5 will halve the modifier scaling, 2 will double it, etc. // Leaving null/undefined will disable level scaling - const mult: number = !isNullOrUndefined(partyConfig.levelAdditiveMultiplier) ? partyConfig.levelAdditiveMultiplier! : 0; + const mult: number = !isNullOrUndefined(partyConfig.levelAdditiveModifier) ? partyConfig.levelAdditiveModifier! : 0; const additive = Math.max(Math.round((scene.currentBattle.waveIndex / 10) * mult), 0); battle.enemyLevels = battle.enemyLevels.map(level => level + additive); @@ -210,7 +213,7 @@ export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig: enemyPokemon.resetSummonData(); } - if (!loaded) { + if (!loaded && isNullOrUndefined(partyConfig.countAsSeen) || partyConfig.countAsSeen) { scene.gameData.setPokemonSeen(enemyPokemon, true, !!(trainerType || trainerConfig)); } @@ -286,6 +289,11 @@ export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig: enemyPokemon.summonData.gender = config.gender!; } + // Set AI type + if (!isNullOrUndefined(config.aiType)) { + enemyPokemon.aiType = config.aiType!; + } + // Set moves if (config?.moveSet && config.moveSet.length > 0) { const moves = config.moveSet.map(m => new PokemonMove(m)); @@ -702,19 +710,19 @@ export function handleMysteryEncounterVictory(scene: BattleScene, addHealPhase: if (encounter.continuousEncounter || doNotContinue) { return; } else if (encounter.encounterMode === MysteryEncounterMode.NO_BATTLE) { - scene.pushPhase(new EggLapsePhase(scene)); scene.pushPhase(new MysteryEncounterRewardsPhase(scene, addHealPhase)); + scene.pushPhase(new EggLapsePhase(scene)); } else if (!scene.getEnemyParty().find(p => encounter.encounterMode !== MysteryEncounterMode.TRAINER_BATTLE ? p.isOnField() : !p?.isFainted(true))) { scene.pushPhase(new BattleEndPhase(scene)); if (encounter.encounterMode === MysteryEncounterMode.TRAINER_BATTLE) { scene.pushPhase(new TrainerVictoryPhase(scene)); } if (scene.gameMode.isEndless || !scene.gameMode.isWaveFinal(scene.currentBattle.waveIndex)) { + scene.pushPhase(new MysteryEncounterRewardsPhase(scene, addHealPhase)); if (!encounter.doContinueEncounter) { // Only lapse eggs once for multi-battle encounters scene.pushPhase(new EggLapsePhase(scene)); } - scene.pushPhase(new MysteryEncounterRewardsPhase(scene, addHealPhase)); } } } diff --git a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts index bac8bded9ba..86c86010c29 100644 --- a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts @@ -19,6 +19,10 @@ import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifi import { Gender } from "#app/data/gender"; import { PermanentStat } from "#enums/stat"; import { VictoryPhase } from "#app/phases/victory-phase"; +import { SummaryUiMode } from "#app/ui/summary-ui-handler"; + +/** Will give +1 level every 10 waves */ +export const STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER = 1; /** * Gets the sprite key and file root for a given PokemonSpecies (accounts for gender, shiny, variants, forms, and experimental) @@ -289,10 +293,12 @@ export async function modifyPlayerPokemonBST(pokemon: PlayerPokemon, value: numb */ export async function applyModifierTypeToPlayerPokemon(scene: BattleScene, pokemon: PlayerPokemon, modType: PokemonHeldItemModifierType, fallbackModifierType?: PokemonHeldItemModifierType) { // Check if the Pokemon has max stacks of that item already + const modifier = modType.newModifier(pokemon); const existing = scene.findModifier(m => ( m instanceof PokemonHeldItemModifier && m.type.id === modType.id && - m.pokemonId === pokemon.id + m.pokemonId === pokemon.id && + m.matchType(modifier) )) as PokemonHeldItemModifier; // At max stacks @@ -305,7 +311,6 @@ export async function applyModifierTypeToPlayerPokemon(scene: BattleScene, pokem return applyModifierTypeToPlayerPokemon(scene, pokemon, fallbackModifierType); } - const modifier = modType.newModifier(pokemon); await scene.addModifier(modifier, false, false, false, true); } @@ -327,7 +332,7 @@ export function trainerThrowPokeball(scene: BattleScene, pokemon: EnemyPokemon, const _3m = 3 * pokemon.getMaxHp(); const _2h = 2 * pokemon.hp; const catchRate = pokemon.species.catchRate; - const pokeballMultiplier = getPokeballCatchMultiplier(this.pokeballType); + const pokeballMultiplier = getPokeballCatchMultiplier(pokeballType); const statusMultiplier = pokemon.status ? getStatusEffectCatchRateMultiplier(pokemon.status.effect) : 1; const x = Math.round((((_3m - _2h) * catchRate * pokeballMultiplier) / _3m) * statusMultiplier); ballTwitchRate = Math.round(65536 / Math.sqrt(Math.sqrt(255 / x))); @@ -501,8 +506,6 @@ function failCatch(scene: BattleScene, pokemon: EnemyPokemon, originalY: number, * @param isObtain */ export async function catchPokemon(scene: BattleScene, pokemon: EnemyPokemon, pokeball: Phaser.GameObjects.Sprite | null, pokeballType: PokeballType, showCatchObtainMessage: boolean = true, isObtain: boolean = false): Promise { - scene.unshiftPhase(new VictoryPhase(scene, pokemon.id, true)); - const speciesForm = !pokemon.fusionSpecies ? pokemon.getSpeciesForm() : pokemon.getFusionSpeciesForm(); if (speciesForm.abilityHidden && (pokemon.fusionSpecies ? pokemon.fusionAbilityIndex : pokemon.abilityIndex) === speciesForm.getAbilityCount() - 1) { @@ -528,6 +531,11 @@ export async function catchPokemon(scene: BattleScene, pokemon: EnemyPokemon, po return new Promise(resolve => { const doPokemonCatchMenu = () => { const end = () => { + // Ensure the pokemon is in the enemy party in all situations + if (!scene.getEnemyParty().some(p => p.id === pokemon.id)) { + scene.getEnemyParty().push(pokemon); + } + scene.unshiftPhase(new VictoryPhase(scene, pokemon.id, true)); scene.pokemonInfoContainer.hide(); if (pokeball) { removePb(scene, pokeball); @@ -539,8 +547,8 @@ export async function catchPokemon(scene: BattleScene, pokemon: EnemyPokemon, po scene.field.remove(pokemon, true); } }; - const addToParty = () => { - const newPokemon = pokemon.addToParty(pokeballType); + const addToParty = (slotIndex?: number) => { + const newPokemon = pokemon.addToParty(pokeballType, slotIndex); const modifiers = scene.findModifiers(m => m instanceof PokemonHeldItemModifier, false); if (scene.getParty().filter(p => p.isShiny()).length === 6) { scene.validateAchv(achvs.SHINY_PARTY); @@ -559,12 +567,19 @@ export async function catchPokemon(scene: BattleScene, pokemon: EnemyPokemon, po if (scene.getParty().length === 6) { const promptRelease = () => { scene.ui.showText(i18next.t("battle:partyFull", { pokemonName: pokemon.getNameToRender() }), null, () => { - scene.pokemonInfoContainer.makeRoomForConfirmUi(); + scene.pokemonInfoContainer.makeRoomForConfirmUi(1, true); scene.ui.setMode(Mode.CONFIRM, () => { - scene.ui.setMode(Mode.PARTY, PartyUiMode.RELEASE, 0, (slotIndex: number, _option: PartyOption) => { + const newPokemon = scene.addPlayerPokemon(pokemon.species, pokemon.level, pokemon.abilityIndex, pokemon.formIndex, pokemon.gender, pokemon.shiny, pokemon.variant, pokemon.ivs, pokemon.nature, pokemon); + scene.ui.setMode(Mode.SUMMARY, newPokemon, 0, SummaryUiMode.DEFAULT, () => { + scene.ui.setMode(Mode.MESSAGE).then(() => { + promptRelease(); + }); + }, false); + }, () => { + scene.ui.setMode(Mode.PARTY, PartyUiMode.RELEASE, 0, (slotIndex: integer, _option: PartyOption) => { scene.ui.setMode(Mode.MESSAGE).then(() => { if (slotIndex < 6) { - addToParty(); + addToParty(slotIndex); } else { promptRelease(); } @@ -575,7 +590,7 @@ export async function catchPokemon(scene: BattleScene, pokemon: EnemyPokemon, po removePokemon(); end(); }); - }); + }, "fullParty"); }); }; promptRelease(); @@ -711,13 +726,50 @@ export function getGoldenBugNetSpecies(): PokemonSpecies { const roll = randSeedInt(totalWeight); let w = 0; - for (const species of GOLDEN_BUG_NET_SPECIES_POOL) { - w += species[1]; + for (const speciesWeightPair of GOLDEN_BUG_NET_SPECIES_POOL) { + w += speciesWeightPair[1]; if (roll < w) { - return getPokemonSpecies(species); + return getPokemonSpecies(speciesWeightPair[0]); } } // Defaults to Scyther return getPokemonSpecies(Species.SCYTHER); } + +/** + * Generates a Pokemon level for a given wave, with an option to increase/decrease by a scaling modifier + * @param scene + * @param levelAdditiveModifier Default 0. will add +(1 level / 10 waves * levelAdditiveModifier) to the level calculation + */ +export function getEncounterPokemonLevelForWave(scene: BattleScene, levelAdditiveModifier: number = 0) { + const currentBattle = scene.currentBattle; + // Default to use the first generated level from enemyLevels, or generate a new one if it DNE + const baseLevel = currentBattle.enemyLevels && currentBattle.enemyLevels?.length > 0 ? currentBattle.enemyLevels[0] : currentBattle.getLevelForWave(); + + // Add a level scaling modifier that is (+1 level per 10 waves) * levelAdditiveModifier + return baseLevel + Math.max(Math.round((currentBattle.waveIndex / 10) * levelAdditiveModifier), 0); +} + +export async function addPokemonDataToDexAndValidateAchievements(scene: BattleScene, pokemon: PlayerPokemon) { + const speciesForm = !pokemon.fusionSpecies ? pokemon.getSpeciesForm() : pokemon.getFusionSpeciesForm(); + + if (speciesForm.abilityHidden && (pokemon.fusionSpecies ? pokemon.fusionAbilityIndex : pokemon.abilityIndex) === speciesForm.getAbilityCount() - 1) { + scene.validateAchv(achvs.HIDDEN_ABILITY); + } + + if (pokemon.species.subLegendary) { + scene.validateAchv(achvs.CATCH_SUB_LEGENDARY); + } + + if (pokemon.species.legendary) { + scene.validateAchv(achvs.CATCH_LEGENDARY); + } + + if (pokemon.species.mythical) { + scene.validateAchv(achvs.CATCH_MYTHICAL); + } + + scene.gameData.updateSpeciesDexIvs(pokemon.species.getRootSpeciesId(true), pokemon.ivs); + return scene.gameData.setPokemonCaught(pokemon, true, false, false); +} diff --git a/src/data/mystery-encounters/utils/encounter-transformation-sequence.ts b/src/data/mystery-encounters/utils/encounter-transformation-sequence.ts index fd9d43829e5..fcadb101817 100644 --- a/src/data/mystery-encounters/utils/encounter-transformation-sequence.ts +++ b/src/data/mystery-encounters/utils/encounter-transformation-sequence.ts @@ -103,7 +103,7 @@ export function doPokemonTransformationSequence(scene: BattleScene, previousPoke scene.time.delayedCall(1000, () => { pokemonEvoTintSprite.setScale(0.25); pokemonEvoTintSprite.setVisible(true); - doCycle(scene, 2, 6, pokemonTintSprite, pokemonEvoTintSprite).then(() => { + doCycle(scene, 1.5, 6, pokemonTintSprite, pokemonEvoTintSprite).then(() => { pokemonEvoSprite.setVisible(true); doCircleInward(scene, transformationBaseBg, transformationContainer, xOffset, yOffset); @@ -115,7 +115,7 @@ export function doPokemonTransformationSequence(scene: BattleScene, previousPoke delay: 150, easing: "Sine.easeIn", onComplete: () => { - scene.time.delayedCall(2500, () => { + scene.time.delayedCall(3000, () => { resolve(); scene.tweens.add({ targets: pokemonEvoSprite, diff --git a/src/enums/variant-tiers.ts b/src/enums/variant-tiers.ts deleted file mode 100644 index 20a0e8ec4e4..00000000000 --- a/src/enums/variant-tiers.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum VariantTier { - COMMON, - RARE, - EPIC -} diff --git a/src/field/arena.ts b/src/field/arena.ts index bf4075e5c1c..9ce19c85283 100644 --- a/src/field/arena.ts +++ b/src/field/arena.ts @@ -762,7 +762,7 @@ export class Arena { case Biome.BEACH: return 3.462; case Biome.LAKE: - return 5.350; + return 7.215; case Biome.SEABED: return 2.600; case Biome.MOUNTAIN: @@ -788,7 +788,7 @@ export class Arena { case Biome.FACTORY: return 4.985; case Biome.RUINS: - return 2.270; + return 0.000; case Biome.WASTELAND: return 6.336; case Biome.ABYSS: diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 9a9156e327d..0f2e1d2b385 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -2334,11 +2334,61 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return accuracyMultiplier.value / evasionMultiplier.value; } + /** + * Calculates the base damage of the given move against this Pokemon when attacked by the given source. + * Used during damage calculation and for Shell Side Arm's forecasting effect. + * @param source the attacking {@linkcode Pokemon}. + * @param move the {@linkcode Move} used in the attack. + * @param moveCategory the move's {@linkcode MoveCategory} after variable-category effects are applied. + * @param ignoreAbility if `true`, ignores this Pokemon's defensive ability effects (defaults to `false`). + * @param ignoreSourceAbility if `true`, ignore's the attacking Pokemon's ability effects (defaults to `false`). + * @param isCritical if `true`, calculates effective stats as if the hit were critical (defaults to `false`). + * @param simulated if `true`, suppresses changes to game state during calculation (defaults to `true`). + * @returns The move's base damage against this Pokemon when used by the source Pokemon. + */ + getBaseDamage(source: Pokemon, move: Move, moveCategory: MoveCategory, ignoreAbility: boolean = false, ignoreSourceAbility: boolean = false, isCritical: boolean = false, simulated: boolean = true): number { + const isPhysical = moveCategory === MoveCategory.PHYSICAL; + + /** A base damage multiplier based on the source's level */ + const levelMultiplier = (2 * source.level / 5 + 2); + + /** The power of the move after power boosts from abilities, etc. have applied */ + const power = move.calculateBattlePower(source, this, simulated); + + /** + * The attacker's offensive stat for the given move's category. + * Critical hits cause negative stat stages to be ignored. + */ + const sourceAtk = new Utils.NumberHolder(source.getEffectiveStat(isPhysical ? Stat.ATK : Stat.SPATK, this, undefined, ignoreSourceAbility, ignoreAbility, isCritical, simulated)); + applyMoveAttrs(VariableAtkAttr, source, this, move, sourceAtk); + + /** + * This Pokemon's defensive stat for the given move's category. + * Critical hits cause positive stat stages to be ignored. + */ + const targetDef = new Utils.NumberHolder(this.getEffectiveStat(isPhysical ? Stat.DEF : Stat.SPDEF, source, move, ignoreAbility, ignoreSourceAbility, isCritical, simulated)); + applyMoveAttrs(VariableDefAttr, source, this, move, targetDef); + + /** + * The attack's base damage, as determined by the source's level, move power + * and Attack stat as well as this Pokemon's Defense stat + */ + const baseDamage = ((levelMultiplier * power * sourceAtk.value / targetDef.value) / 50) + 2; + + /** Debug message for non-simulated calls (i.e. when damage is actually dealt) */ + if (!simulated) { + console.log("base damage", baseDamage, move.name, power, sourceAtk.value, targetDef.value); + } + + return baseDamage; + } + /** * Calculates the damage of an attack made by another Pokemon against this Pokemon * @param source {@linkcode Pokemon} the attacking Pokemon * @param move {@linkcode Pokemon} the move used in the attack * @param ignoreAbility If `true`, ignores this Pokemon's defensive ability effects + * @param ignoreSourceAbility If `true`, ignores the attacking Pokemon's ability effects * @param isCritical If `true`, calculates damage for a critical hit. * @param simulated If `true`, suppresses changes to game state during the calculation. * @returns a {@linkcode DamageCalculationResult} object with three fields: @@ -2407,35 +2457,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { }; } - // ----- BEGIN BASE DAMAGE MULTIPLIERS ----- - - /** A base damage multiplier based on the source's level */ - const levelMultiplier = (2 * source.level / 5 + 2); - - /** The power of the move after power boosts from abilities, etc. have applied */ - const power = move.calculateBattlePower(source, this, simulated); - - /** - * The attacker's offensive stat for the given move's category. - * Critical hits ignore negative stat stages. - */ - const sourceAtk = new Utils.NumberHolder(source.getEffectiveStat(isPhysical ? Stat.ATK : Stat.SPATK, this, undefined, ignoreSourceAbility, ignoreAbility, isCritical, simulated)); - applyMoveAttrs(VariableAtkAttr, source, this, move, sourceAtk); - - /** - * This Pokemon's defensive stat for the given move's category. - * Critical hits ignore positive stat stages. - */ - const targetDef = new Utils.NumberHolder(this.getEffectiveStat(isPhysical ? Stat.DEF : Stat.SPDEF, source, move, ignoreAbility, ignoreSourceAbility, isCritical, simulated)); - applyMoveAttrs(VariableDefAttr, source, this, move, targetDef); - /** * The attack's base damage, as determined by the source's level, move power * and Attack stat as well as this Pokemon's Defense stat */ - const baseDamage = ((levelMultiplier * power * sourceAtk.value / targetDef.value) / 50) + 2; - - // ------ END BASE DAMAGE MULTIPLIERS ------ + const baseDamage = this.getBaseDamage(source, move, moveCategory, ignoreAbility, ignoreSourceAbility, isCritical, simulated); /** 25% damage debuff on moves hitting more than one non-fainted target (regardless of immunities) */ const { targets, multiple } = getMoveTargets(source, move.id); @@ -2561,7 +2587,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { // debug message for when damage is applied (i.e. not simulated) if (!simulated) { - console.log("damage", damage.value, move.name, power, sourceAtk, targetDef); + console.log("damage", damage.value, move.name); } let hitResult: HitResult; diff --git a/src/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/en/bgm-name.json b/src/locales/en/bgm-name.json index e13f580def6..f38b62d14c6 100644 --- a/src/locales/en/bgm-name.json +++ b/src/locales/en/bgm-name.json @@ -112,13 +112,13 @@ "island": "PMD EoS Craggy Coast", "jungle": "Lmz - Jungle", "laboratory": "Firel - Laboratory", - "lake": "PMD EoS Crystal Cave", + "lake": "Lmz - Lake", "meadow": "PMD EoS Sky Peak Forest", "metropolis": "Firel - Metropolis", "mountain": "PMD EoS Mt. Horn", "plains": "Firel - Route 888", "power_plant": "Firel - The Klink", - "ruins": "PMD EoS Deep Sealed Ruin", + "ruins": "Lmz - Ancient Ruins", "sea": "Andr06 - Marine Mystique", "seabed": "Firel - Seabed", "slum": "Andr06 - Sneaky Snom", @@ -151,5 +151,6 @@ "mystery_encounter_weird_dream": "PMD EoS Temporal Spire", "mystery_encounter_fun_and_games": "PMD EoS Guildmaster Wigglytuff", "mystery_encounter_gen_5_gts": "BW GTS", - "mystery_encounter_gen_6_gts": "XY GTS" + "mystery_encounter_gen_6_gts": "XY GTS", + "mystery_encounter_delibirdy": "Firel - DeliDelivery!" } diff --git a/src/locales/en/mystery-encounters/an-offer-you-cant-refuse-dialogue.json b/src/locales/en/mystery-encounters/an-offer-you-cant-refuse-dialogue.json index 6dd54d302ab..e286d89691a 100644 --- a/src/locales/en/mystery-encounters/an-offer-you-cant-refuse-dialogue.json +++ b/src/locales/en/mystery-encounters/an-offer-you-cant-refuse-dialogue.json @@ -1,7 +1,7 @@ { "intro": "You're stopped by a rich looking boy.", "speaker": "Rich Boy", - "intro_dialogue": "Good day to you.$I can't help but notice that your\n{{strongestPokemon}} looks positively divine!$I've always wanted to have a pet like that!$I'd pay you handsomely,\nand also give you this old bauble!", + "intro_dialogue": "Good day to you.$I can't help but notice that your\n{{strongestPokemon}} looks positively divine!$I've always wanted to have a Pokémon like that!$I'd pay you handsomely,\nand also give you this old bauble!", "title": "An Offer You Can't Refuse", "description": "You're being offered a @[TOOLTIP_TITLE]{Shiny Charm} and {{price, money}} for your {{strongestPokemon}}!\n\nIt's an extremely good deal, but can you really bear to part with such a strong team member?", "query": "What will you do?", diff --git a/src/locales/en/mystery-encounters/bug-type-superfan-dialogue.json b/src/locales/en/mystery-encounters/bug-type-superfan-dialogue.json index 7df01326aed..09488addb98 100644 --- a/src/locales/en/mystery-encounters/bug-type-superfan-dialogue.json +++ b/src/locales/en/mystery-encounters/bug-type-superfan-dialogue.json @@ -17,9 +17,9 @@ "disabled_tooltip": "You need at least 1 Bug Type Pokémon on your team to select this.", "selected": "You show the trainer all your Bug Type Pokémon...", "selected_0_to_1": "Huh? You only have {{numBugTypes}}...$Guess I'm wasting my breath on someone like you...", - "selected_2_to_3": "Hey, you've got {{numBugTypes}} Bug Types!\nNot bad.$Here, this might help you on your journey to catch more!", - "selected_4_to_5": "What? You have {{numBugTypes}} Bug Types?\nNice!$You're not quite at my level, but I can see shades of myself in you!\n$Take this, my young apprentice!", - "selected_6": "Whoa! {{numBugTypes}} Bug Types!\n$You must love Bug Types almost as much as I do!$Here, take this as a token of our camaraderie!" + "selected_2_to_3": "Hey, you've got {{numBugTypes}}!\nNot bad.$Here, this might help you on your journey to catch more!", + "selected_4_to_5": "What? You have {{numBugTypes}}?\nNice!$You're not quite at my level, but I can see shades of myself in you!\n$Take this, my young apprentice!", + "selected_6": "Whoa! {{numBugTypes}}!\n$You must love Bug Types almost as much as I do!$Here, take this as a token of our camaraderie!" }, "3": { "label": "Gift a Bug Item", @@ -34,5 +34,7 @@ "battle_won": "Your knowledge and skill were perfect at exploiting our weaknesses!$In exchange for the valuable lesson,\nallow me to teach one of your Pokémon a Bug Type Move!", "teach_move_prompt": "Select a move to teach a Pokémon.", "confirm_no_teach": "You sure you don't want to learn one of these great moves?", - "outro": "I see great Bug Pokémon in your future!\nMay our paths cross again!$Bug out!" + "outro": "I see great Bug Pokémon in your future!\nMay our paths cross again!$Bug out!", + "numBugTypes_one": "{{count}} Bug Type", + "numBugTypes_other": "{{count}} Bug Types" } \ No newline at end of file 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/modifier/modifier.ts b/src/modifier/modifier.ts index 0c4d2a63802..81a3f4f81cc 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -600,7 +600,7 @@ export class TerastallizeAccessModifier extends PersistentModifier { export abstract class PokemonHeldItemModifier extends PersistentModifier { public pokemonId: integer; - readonly isTransferrable: boolean = true; + public isTransferable: boolean = true; constructor(type: ModifierType, pokemonId: integer, stackCount?: integer) { super(type, stackCount); @@ -699,7 +699,7 @@ export abstract class PokemonHeldItemModifier extends PersistentModifier { export abstract class LapsingPokemonHeldItemModifier extends PokemonHeldItemModifier { protected battlesLeft: integer; - readonly isTransferrable: boolean = false; + public isTransferable: boolean = false; constructor(type: ModifierTypes.ModifierType, pokemonId: integer, battlesLeft?: integer, stackCount?: integer) { super(type, pokemonId, stackCount); @@ -736,7 +736,7 @@ export abstract class LapsingPokemonHeldItemModifier extends PokemonHeldItemModi export class TerastallizeModifier extends LapsingPokemonHeldItemModifier { public teraType: Type; - readonly isTransferrable: boolean = false; + public isTransferable: boolean = false; constructor(type: ModifierTypes.TerastallizeModifierType, pokemonId: integer, teraType: Type, battlesLeft?: integer, stackCount?: integer) { super(type, pokemonId, battlesLeft || 10, stackCount); @@ -799,7 +799,7 @@ export class TerastallizeModifier extends LapsingPokemonHeldItemModifier { */ export class BaseStatModifier extends PokemonHeldItemModifier { protected stat: PermanentStat; - readonly isTransferrable: boolean = false; + public isTransferable: boolean = false; constructor(type: ModifierType, pokemonId: integer, stat: PermanentStat, stackCount?: integer) { super(type, pokemonId, stackCount); @@ -843,7 +843,7 @@ export class BaseStatModifier extends PokemonHeldItemModifier { export class EvoTrackerModifier extends PokemonHeldItemModifier { protected species: Species; protected required: integer; - readonly isTransferrable: boolean = false; + public isTransferable: boolean = false; constructor(type: ModifierType, pokemonId: integer, species: Species, required: integer, stackCount?: integer) { super(type, pokemonId, stackCount); @@ -880,7 +880,7 @@ export class EvoTrackerModifier extends PokemonHeldItemModifier { */ export class PokemonBaseStatTotalModifier extends PokemonHeldItemModifier { private statModifier: integer; - readonly isTransferrable: boolean = false; + public isTransferable: boolean = false; constructor(type: ModifierTypes.PokemonBaseStatTotalModifierType, pokemonId: integer, statModifier: integer, stackCount?: integer) { super(type, pokemonId, stackCount); @@ -929,7 +929,7 @@ export class PokemonBaseStatTotalModifier extends PokemonHeldItemModifier { export class PokemonBaseStatFlatModifier extends PokemonHeldItemModifier { private statModifier: integer; private stats: Stat[]; - readonly isTransferrable: boolean = false; + public isTransferable: boolean = false; constructor (type: ModifierType, pokemonId: integer, statModifier: integer, stats: Stat[], stackCount?: integer) { super(type, pokemonId, stackCount); @@ -979,7 +979,7 @@ export class PokemonBaseStatFlatModifier extends PokemonHeldItemModifier { * Currently used by Macho Brace item */ export class PokemonIncrementingStatModifier extends PokemonHeldItemModifier { - readonly isTransferrable: boolean = false; + public isTransferable: boolean = false; constructor (type: ModifierType, pokemonId: integer, stackCount?: integer) { super(type, pokemonId, stackCount); @@ -2346,7 +2346,7 @@ export class PokemonMultiHitModifier extends PokemonHeldItemModifier { export class PokemonFormChangeItemModifier extends PokemonHeldItemModifier { public formChangeItem: FormChangeItem; public active: boolean; - readonly isTransferrable: boolean = false; + public isTransferable: boolean = false; constructor(type: ModifierTypes.FormChangeItemModifierType, pokemonId: integer, formChangeItem: FormChangeItem, active: boolean, stackCount?: integer) { super(type, pokemonId, stackCount); @@ -2691,7 +2691,7 @@ export abstract class HeldItemTransferModifier extends PokemonHeldItemModifier { const transferredModifierTypes: ModifierTypes.ModifierType[] = []; const itemModifiers = pokemon.scene.findModifiers(m => m instanceof PokemonHeldItemModifier - && m.pokemonId === targetPokemon.id && m.isTransferrable, targetPokemon.isPlayer()) as PokemonHeldItemModifier[]; + && m.pokemonId === targetPokemon.id && m.isTransferable, targetPokemon.isPlayer()) as PokemonHeldItemModifier[]; let highestItemTier = itemModifiers.map(m => m.type.getOrInferTier(poolType)).reduce((highestTier, tier) => Math.max(tier!, highestTier), 0); // TODO: is this bang correct? let tierItemModifiers = itemModifiers.filter(m => m.type.getOrInferTier(poolType) === highestItemTier); @@ -2736,7 +2736,7 @@ export abstract class HeldItemTransferModifier extends PokemonHeldItemModifier { * @see {@linkcode modifierTypes[MINI_BLACK_HOLE]} */ export class TurnHeldItemTransferModifier extends HeldItemTransferModifier { - isTransferrable: boolean = true; + isTransferable: boolean = true; constructor(type: ModifierType, pokemonId: integer, stackCount?: integer) { super(type, pokemonId, stackCount); } @@ -2762,7 +2762,7 @@ export class TurnHeldItemTransferModifier extends HeldItemTransferModifier { } setTransferrableFalse(): void { - this.isTransferrable = false; + this.isTransferable = false; } } diff --git a/src/overrides.ts b/src/overrides.ts index ec0577ceb3d..35ca299721b 100644 --- a/src/overrides.ts +++ b/src/overrides.ts @@ -6,7 +6,7 @@ import { PokeballType } from "#enums/pokeball"; import { Species } from "#enums/species"; import { StatusEffect } from "#enums/status-effect"; import { TimeOfDay } from "#enums/time-of-day"; -import { VariantTier } from "#enums/variant-tiers"; +import { VariantTier } from "#enums/variant-tier"; import { WeatherType } from "#enums/weather-type"; import { type PokeballCounts } from "./battle-scene"; import { Gender } from "./data/gender"; diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index 86e42acb26b..66e39cf98a5 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -16,6 +16,7 @@ import i18next from "i18next"; import { FieldPhase } from "./field-phase"; import { SelectTargetPhase } from "./select-target-phase"; import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; +import { isNullOrUndefined } from "#app/utils"; export class CommandPhase extends FieldPhase { protected fieldIndex: integer; @@ -179,14 +180,16 @@ export class CommandPhase extends FieldPhase { case Command.POKEMON: case Command.RUN: const isSwitch = command === Command.POKEMON; - if (!isSwitch && this.scene.arena.biomeType === Biome.END) { + const { currentBattle, arena } = this.scene; + const mysteryEncounterFleeAllowed = currentBattle.mysteryEncounter?.fleeAllowed; + if (!isSwitch && (arena.biomeType === Biome.END || (!isNullOrUndefined(mysteryEncounterFleeAllowed) && !mysteryEncounterFleeAllowed))) { this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); this.scene.ui.setMode(Mode.MESSAGE); this.scene.ui.showText(i18next.t("battle:noEscapeForce"), null, () => { this.scene.ui.showText("", 0); this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); }, null, true); - } else if (!isSwitch && (this.scene.currentBattle.battleType === BattleType.TRAINER || this.scene.currentBattle.mysteryEncounter?.encounterMode === MysteryEncounterMode.TRAINER_BATTLE)) { + } else if (!isSwitch && (currentBattle.battleType === BattleType.TRAINER || currentBattle.mysteryEncounter?.encounterMode === MysteryEncounterMode.TRAINER_BATTLE)) { this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); this.scene.ui.setMode(Mode.MESSAGE); this.scene.ui.showText(i18next.t("battle:noEscapeTrainer"), null, () => { @@ -197,12 +200,12 @@ export class CommandPhase extends FieldPhase { const batonPass = isSwitch && args[0] as boolean; const trappedAbMessages: string[] = []; if (batonPass || !playerPokemon.isTrapped(trappedAbMessages)) { - this.scene.currentBattle.turnCommands[this.fieldIndex] = isSwitch + currentBattle.turnCommands[this.fieldIndex] = isSwitch ? { command: Command.POKEMON, cursor: cursor, args: args } : { command: Command.RUN }; success = true; if (!isSwitch && this.fieldIndex) { - this.scene.currentBattle.turnCommands[this.fieldIndex - 1]!.skip = true; + currentBattle.turnCommands[this.fieldIndex - 1]!.skip = true; } } else if (trappedAbMessages.length > 0) { if (!isSwitch) { @@ -219,7 +222,7 @@ export class CommandPhase extends FieldPhase { // trapTag should be defined at this point, but just in case... if (!trapTag) { - this.scene.currentBattle.turnCommands[this.fieldIndex] = isSwitch + currentBattle.turnCommands[this.fieldIndex] = isSwitch ? { command: Command.POKEMON, cursor: cursor, args: args } : { command: Command.RUN }; break; diff --git a/src/phases/game-over-phase.ts b/src/phases/game-over-phase.ts index 8ab191324c6..438ff1aecb0 100644 --- a/src/phases/game-over-phase.ts +++ b/src/phases/game-over-phase.ts @@ -60,6 +60,11 @@ export class GameOverPhase extends BattlePhase { this.scene.ui.fadeOut(1250).then(() => { this.scene.reset(); this.scene.clearPhaseQueue(); + // If this is a ME, clear any residual visual sprites before reloading + const encounter = this.scene.currentBattle.mysteryEncounter; + if (encounter?.introVisuals) { + this.scene.field.remove(encounter.introVisuals, true); + } this.scene.gameData.loadSession(this.scene, this.scene.sessionSlotId).then(() => { this.scene.pushPhase(new EncounterPhase(this.scene, true)); diff --git a/src/phases/message-phase.ts b/src/phases/message-phase.ts index 2244980c899..1d953801178 100644 --- a/src/phases/message-phase.ts +++ b/src/phases/message-phase.ts @@ -6,14 +6,16 @@ export class MessagePhase extends Phase { private callbackDelay: integer | null; private prompt: boolean | null; private promptDelay: integer | null; + private speaker?: string; - constructor(scene: BattleScene, text: string, callbackDelay?: integer | null, prompt?: boolean | null, promptDelay?: integer | null) { + constructor(scene: BattleScene, text: string, callbackDelay?: integer | null, prompt?: boolean | null, promptDelay?: integer | null, speaker?: string) { super(scene); this.text = text; this.callbackDelay = callbackDelay!; // TODO: is this bang correct? this.prompt = prompt!; // TODO: is this bang correct? this.promptDelay = promptDelay!; // TODO: is this bang correct? + this.speaker = speaker; } start() { @@ -21,11 +23,15 @@ export class MessagePhase extends Phase { if (this.text.indexOf("$") > -1) { const pageIndex = this.text.indexOf("$"); - this.scene.unshiftPhase(new MessagePhase(this.scene, this.text.slice(pageIndex + 1), this.callbackDelay, this.prompt, this.promptDelay)); + this.scene.unshiftPhase(new MessagePhase(this.scene, this.text.slice(pageIndex + 1), this.callbackDelay, this.prompt, this.promptDelay, this.speaker)); this.text = this.text.slice(0, pageIndex).trim(); } - this.scene.ui.showText(this.text, null, () => this.end(), this.callbackDelay || (this.prompt ? 0 : 1500), this.prompt, this.promptDelay); + if (this.speaker) { + this.scene.ui.showDialogue(this.text, this.speaker, null, () => this.end(), this.callbackDelay || (this.prompt ? 0 : 1500), this.promptDelay ?? 0); + } else { + this.scene.ui.showText(this.text, null, () => this.end(), this.callbackDelay || (this.prompt ? 0 : 1500), this.prompt, this.promptDelay); + } } end() { diff --git a/src/phases/mystery-encounter-phases.ts b/src/phases/mystery-encounter-phases.ts index 6c9d3fd8c1d..007b69650b9 100644 --- a/src/phases/mystery-encounter-phases.ts +++ b/src/phases/mystery-encounter-phases.ts @@ -3,7 +3,7 @@ import BattleScene from "../battle-scene"; import { Phase } from "../phase"; import { Mode } from "../ui/ui"; import { transitionMysteryEncounterIntroVisuals, OptionSelectSettings } from "../data/mystery-encounters/utils/encounter-phase-utils"; -import MysteryEncounterOption, { OptionPhaseCallback } from "../data/mystery-encounters/mystery-encounter-option"; +import MysteryEncounterOption, { OptionPhaseCallback } from "#app/data/mystery-encounters/mystery-encounter-option"; import { getCharVariantFromDialogue } from "../data/dialogue"; import { TrainerSlot } from "../data/trainer-config"; import { BattleSpec } from "#enums/battle-spec"; diff --git a/src/phases/select-modifier-phase.ts b/src/phases/select-modifier-phase.ts index 39a0da1167f..58fb13ac466 100644 --- a/src/phases/select-modifier-phase.ts +++ b/src/phases/select-modifier-phase.ts @@ -1,7 +1,7 @@ import BattleScene from "#app/battle-scene"; import { ModifierTier } from "#app/modifier/modifier-tier"; import { regenerateModifierPoolThresholds, ModifierTypeOption, ModifierType, getPlayerShopModifierTypeOptionsForWave, PokemonModifierType, FusePokemonModifierType, PokemonMoveModifierType, TmModifierType, RememberMoveModifierType, PokemonPpRestoreModifierType, PokemonPpUpModifierType, ModifierPoolType, getPlayerModifierTypeOptions } from "#app/modifier/modifier-type"; -import { ExtraModifierModifier, Modifier, PokemonHeldItemModifier } from "#app/modifier/modifier"; +import { ExtraModifierModifier, HealShopCostModifier, Modifier, PokemonHeldItemModifier } from "#app/modifier/modifier"; import ModifierSelectUiHandler, { SHOP_OPTIONS_ROW_LIMIT } from "#app/ui/modifier-select-ui-handler"; import PartyUiHandler, { PartyUiMode, PartyOption } from "#app/ui/party-ui-handler"; import { Mode } from "#app/ui/ui"; @@ -10,7 +10,7 @@ import * as Utils from "#app/utils"; import { BattlePhase } from "./battle-phase"; import Overrides from "#app/overrides"; import { CustomModifierSettings } from "#app/modifier/modifier-type"; -import { isNullOrUndefined } from "#app/utils"; +import { isNullOrUndefined, NumberHolder } from "#app/utils"; export class SelectModifierPhase extends BattlePhase { private rerollCount: integer; @@ -69,11 +69,11 @@ export class SelectModifierPhase extends BattlePhase { } let modifierType: ModifierType; let cost: integer; + const rerollCost = this.getRerollCost(typeOptions, this.scene.lockModifierTiers); switch (rowCursor) { case 0: switch (cursor) { case 0: - const rerollCost = this.getRerollCost(typeOptions, this.scene.lockModifierTiers); if (rerollCost < 0 || this.scene.money < rerollCost) { this.scene.ui.playError(); return false; @@ -94,7 +94,7 @@ export class SelectModifierPhase extends BattlePhase { this.scene.ui.setModeWithoutClear(Mode.PARTY, PartyUiMode.MODIFIER_TRANSFER, -1, (fromSlotIndex: integer, itemIndex: integer, itemQuantity: integer, toSlotIndex: integer) => { if (toSlotIndex !== undefined && fromSlotIndex < 6 && toSlotIndex < 6 && fromSlotIndex !== toSlotIndex && itemIndex > -1) { const itemModifiers = this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier - && m.isTransferrable && m.pokemonId === party[fromSlotIndex].id) as PokemonHeldItemModifier[]; + && m.isTransferable && m.pokemonId === party[fromSlotIndex].id) as PokemonHeldItemModifier[]; const itemModifier = itemModifiers[itemIndex]; this.scene.tryTransferHeldItemModifier(itemModifier, party[toSlotIndex], true, itemQuantity); } else { @@ -108,6 +108,11 @@ export class SelectModifierPhase extends BattlePhase { }); break; case 3: + if (rerollCost < 0) { + // Reroll lock button is also disabled when reroll is disabled + this.scene.ui.playError(); + return false; + } this.scene.lockModifierTiers = !this.scene.lockModifierTiers; const uiHandler = this.scene.ui.getHandler() as ModifierSelectUiHandler; uiHandler.setRerollCost(this.getRerollCost(typeOptions, this.scene.lockModifierTiers)); @@ -133,7 +138,10 @@ export class SelectModifierPhase extends BattlePhase { if (shopOption.type) { modifierType = shopOption.type; } - cost = shopOption.cost; + // Apply Black Sludge to healing item cost + const healingItemCost = new NumberHolder(shopOption.cost); + this.scene.applyModifier(HealShopCostModifier, true, healingItemCost); + cost = healingItemCost.value; break; } diff --git a/src/system/egg-data.ts b/src/system/egg-data.ts index 785ae364efe..1c9c903688a 100644 --- a/src/system/egg-data.ts +++ b/src/system/egg-data.ts @@ -1,6 +1,6 @@ import { EggTier } from "#enums/egg-type"; import { Species } from "#enums/species"; -import { VariantTier } from "#enums/variant-tiers"; +import { VariantTier } from "#enums/variant-tier"; import { EGG_SEED, Egg } from "../data/egg"; import { EggSourceType } from "#app/enums/egg-source-types"; diff --git a/src/test/eggs/egg.test.ts b/src/test/eggs/egg.test.ts index 4f00e843b47..053ff8f1112 100644 --- a/src/test/eggs/egg.test.ts +++ b/src/test/eggs/egg.test.ts @@ -1,7 +1,7 @@ import { Egg, getLegendaryGachaSpeciesForTimestamp } from "#app/data/egg"; import { EggSourceType } from "#app/enums/egg-source-types"; import { EggTier } from "#app/enums/egg-type"; -import { VariantTier } from "#app/enums/variant-tiers"; +import { VariantTier } from "#app/enums/variant-tier"; import EggData from "#app/system/egg-data"; import * as Utils from "#app/utils"; import { Species } from "#enums/species"; @@ -136,9 +136,9 @@ describe("Egg Generation Tests", () => { expect(result).toBe(expectedResult); }); - it("should return a shiny common variant", () => { + it("should return a shiny standard variant", () => { const scene = game.scene; - const expectedVariantTier = VariantTier.COMMON; + const expectedVariantTier = VariantTier.STANDARD; const result = new Egg({ scene, isShiny: true, variantTier: expectedVariantTier, species: Species.BULBASAUR }).generatePlayerPokemon(scene).variant; diff --git a/src/test/moves/shell_side_arm.test.ts b/src/test/moves/shell_side_arm.test.ts new file mode 100644 index 00000000000..38d2556a1a2 --- /dev/null +++ b/src/test/moves/shell_side_arm.test.ts @@ -0,0 +1,87 @@ +import { BattlerIndex } from "#app/battle"; +import { allMoves, ShellSideArmCategoryAttr } from "#app/data/move"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, it, expect, vi } from "vitest"; + +describe("Moves - Shell Side Arm", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + const TIMEOUT = 20 * 1000; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .moveset([Moves.SHELL_SIDE_ARM]) + .battleType("single") + .startingLevel(100) + .enemyLevel(100) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("becomes a physical attack if forecasted to deal more damage as physical", async () => { + game.override.enemySpecies(Species.SNORLAX); + + await game.classicMode.startBattle([Species.MANAPHY]); + + const shellSideArm = allMoves[Moves.SHELL_SIDE_ARM]; + const shellSideArmAttr = shellSideArm.getAttrs(ShellSideArmCategoryAttr)[0]; + vi.spyOn(shellSideArmAttr, "apply"); + + game.move.select(Moves.SHELL_SIDE_ARM); + + await game.phaseInterceptor.to("MoveEffectPhase"); + + expect(shellSideArmAttr.apply).toHaveLastReturnedWith(true); + }, TIMEOUT); + + it("remains a special attack if forecasted to deal more damage as special", async () => { + game.override.enemySpecies(Species.SLOWBRO); + + await game.classicMode.startBattle([Species.MANAPHY]); + + const shellSideArm = allMoves[Moves.SHELL_SIDE_ARM]; + const shellSideArmAttr = shellSideArm.getAttrs(ShellSideArmCategoryAttr)[0]; + vi.spyOn(shellSideArmAttr, "apply"); + + game.move.select(Moves.SHELL_SIDE_ARM); + + await game.phaseInterceptor.to("MoveEffectPhase"); + + expect(shellSideArmAttr.apply).toHaveLastReturnedWith(false); + }, TIMEOUT); + + it("respects stat stage changes when forecasting base damage", async () => { + game.override + .enemySpecies(Species.SNORLAX) + .enemyMoveset(Moves.COTTON_GUARD); + + await game.classicMode.startBattle([Species.MANAPHY]); + + const shellSideArm = allMoves[Moves.SHELL_SIDE_ARM]; + const shellSideArmAttr = shellSideArm.getAttrs(ShellSideArmCategoryAttr)[0]; + vi.spyOn(shellSideArmAttr, "apply"); + + game.move.select(Moves.SHELL_SIDE_ARM); + + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + + await game.phaseInterceptor.to("BerryPhase", false); + + expect(shellSideArmAttr.apply).toHaveLastReturnedWith(false); + }, TIMEOUT); +}); diff --git a/src/test/moves/steamroller.test.ts b/src/test/moves/steamroller.test.ts new file mode 100644 index 00000000000..cbbb3a22593 --- /dev/null +++ b/src/test/moves/steamroller.test.ts @@ -0,0 +1,58 @@ +import { BattlerIndex } from "#app/battle"; +import { allMoves } from "#app/data/move"; +import { BattlerTagType } from "#app/enums/battler-tag-type"; +import { DamageCalculationResult } from "#app/field/pokemon"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +describe("Moves - Steamroller", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override.moveset([Moves.STEAMROLLER]).battleType("single").enemyAbility(Abilities.BALL_FETCH); + }); + + it("should always hit a minimzed target with double damage", async () => { + game.override.enemySpecies(Species.DITTO).enemyMoveset(Moves.MINIMIZE); + await game.classicMode.startBattle([Species.IRON_BOULDER]); + + const ditto = game.scene.getEnemyPokemon()!; + vi.spyOn(ditto, "getAttackDamage"); + ditto.hp = 5000; + const steamroller = allMoves[Moves.STEAMROLLER]; + vi.spyOn(steamroller, "calculateBattleAccuracy"); + const ironBoulder = game.scene.getPlayerPokemon()!; + vi.spyOn(ironBoulder, "getAccuracyMultiplier"); + // Turn 1 + game.move.select(Moves.STEAMROLLER); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); + await game.toNextTurn(); + // Turn 2 + game.move.select(Moves.STEAMROLLER); + await game.toNextTurn(); + + const [dmgCalcTurn1, dmgCalcTurn2]: DamageCalculationResult[] = vi + .mocked(ditto.getAttackDamage) + .mock.results.map((r) => r.value); + + expect(dmgCalcTurn2.damage).toBeGreaterThanOrEqual(dmgCalcTurn1.damage * 2); + expect(ditto.getTag(BattlerTagType.MINIMIZED)).toBeDefined(); + expect(steamroller.calculateBattleAccuracy).toHaveReturnedWith(-1); + }); +}); diff --git a/src/test/moves/whirlwind.test.ts b/src/test/moves/whirlwind.test.ts new file mode 100644 index 00000000000..a591a3cd6c5 --- /dev/null +++ b/src/test/moves/whirlwind.test.ts @@ -0,0 +1,53 @@ +import { BattlerIndex } from "#app/battle"; +import { allMoves } from "#app/data/move"; +import { BattlerTagType } from "#app/enums/battler-tag-type"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, it, expect, vi } from "vitest"; + +describe("Moves - Whirlwind", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .battleType("single") + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.WHIRLWIND) + .enemySpecies(Species.PIDGEY); + }); + + it.each([ + { move: Moves.FLY, name: "Fly" }, + { move: Moves.BOUNCE, name: "Bounce" }, + { move: Moves.SKY_DROP, name: "Sky Drop" }, + ])("should not hit a flying target: $name (=$move)", async ({ move }) => { + game.override.moveset([move]); + await game.classicMode.startBattle([Species.STARAPTOR]); + + const staraptor = game.scene.getPlayerPokemon()!; + const whirlwind = allMoves[Moves.WHIRLWIND]; + vi.spyOn(whirlwind, "getFailedText"); + + game.move.select(move); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); + await game.toNextTurn(); + + expect(staraptor.findTag((t) => t.tagType === BattlerTagType.FLYING)).toBeDefined(); + expect(whirlwind.getFailedText).toHaveBeenCalledOnce(); + }); +}); 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 73ffad36258..5d30bc91de9 100644 --- a/src/test/mystery-encounter/encounters/berries-abound-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/berries-abound-encounter.test.ts @@ -98,7 +98,6 @@ describe("Berries Abound - Mystery Encounter", () => { const config = BerriesAboundEncounter.enemyPartyConfigs[0]; expect(config).toBeDefined(); - expect(config.levelAdditiveMultiplier).toBe(1); expect(config.pokemonConfigs?.[0].isBoss).toBe(true); expect(onInitResult).toBe(true); }); @@ -134,7 +133,7 @@ describe("Berries Abound - Mystery Encounter", () => { }); // TODO: there is some severe test flakiness occurring for this file, needs to be looked at/addressed in separate issue - it.skip("should reward the player with X berries based on wave", async () => { + it("should reward the player with X berries based on wave", async () => { await game.runToMysteryEncounter(MysteryEncounterType.BERRIES_ABOUND, defaultParty); const numBerries = game.scene.currentBattle.mysteryEncounter!.misc.numBerries; 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 735dcc709bf..86d79b57f80 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 @@ -96,7 +96,6 @@ describe("Fight or Flight - Mystery Encounter", () => { const config = FightOrFlightEncounter.enemyPartyConfigs[0]; expect(config).toBeDefined(); - expect(config.levelAdditiveMultiplier).toBe(1); expect(config.pokemonConfigs?.[0].isBoss).toBe(true); expect(onInitResult).toBe(true); }); 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 de527538711..2c0d48e59c6 100644 --- a/src/test/mystery-encounter/encounters/mysterious-challengers-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/mysterious-challengers-encounter.test.ts @@ -117,12 +117,12 @@ describe("Mysterious Challengers - Mystery Encounter", () => { }, { trainerConfig: expect.any(TrainerConfig), - levelAdditiveMultiplier: 1, + levelAdditiveModifier: 1, female: expect.any(Boolean), }, { trainerConfig: expect.any(TrainerConfig), - levelAdditiveMultiplier: 1.5, + levelAdditiveModifier: 1.5, female: expect.any(Boolean), } ]); 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 13860e83baa..2505f28aa43 100644 --- a/src/test/mystery-encounter/encounters/teleporting-hijinks-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/teleporting-hijinks-encounter.test.ts @@ -21,6 +21,8 @@ const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; const defaultBiome = Biome.CAVE; const defaultWave = 45; +const TRANSPORT_BIOMES = [Biome.SPACE, Biome.ISLAND, Biome.LABORATORY, Biome.FAIRY_CAVE, Biome.WASTELAND, Biome.DOJO]; + describe("Teleporting Hijinks - Mystery Encounter", () => { let phaserGame: Phaser.Game; let game: GameManager; @@ -183,7 +185,7 @@ describe("Teleporting Hijinks - Mystery Encounter", () => { await runMysteryEncounterToEnd(game, 1, undefined, true); expect(previousBiome).not.toBe(scene.arena.biomeType); - expect([Biome.SPACE, Biome.ISLAND, Biome.LABORATORY, Biome.FAIRY_CAVE]).toContain(scene.arena.biomeType); + expect(TRANSPORT_BIOMES).toContain(scene.arena.biomeType); }); it("should start a battle against an enraged boss", { retry: 5 }, async () => { @@ -246,7 +248,7 @@ describe("Teleporting Hijinks - Mystery Encounter", () => { await runMysteryEncounterToEnd(game, 2, undefined, true); expect(previousBiome).not.toBe(scene.arena.biomeType); - expect([Biome.SPACE, Biome.ISLAND, Biome.LABORATORY, Biome.FAIRY_CAVE]).toContain(scene.arena.biomeType); + expect(TRANSPORT_BIOMES).toContain(scene.arena.biomeType); }); it("should start a battle against an enraged boss", async () => { 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 be35ec31784..ef80271c423 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 @@ -114,7 +114,7 @@ describe("The Strong Stuff - Mystery Encounter", () => { expect(TheStrongStuffEncounter.enemyPartyConfigs).toEqual([ { - levelAdditiveMultiplier: 1, + levelAdditiveModifier: 1, disableSwitch: true, pokemonConfigs: [ { 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 a4bfaea659a..0f89e90031c 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 @@ -102,7 +102,7 @@ describe("Trash to Treasure - Mystery Encounter", () => { expect(TrashToTreasureEncounter.enemyPartyConfigs).toEqual([ { - levelAdditiveMultiplier: 1, + levelAdditiveModifier: 1, disableSwitch: true, pokemonConfigs: [ { 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 f235609ee08..5b17ff3a2f7 100644 --- a/src/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts @@ -107,7 +107,7 @@ describe("Uncommon Breed - Mystery Encounter", () => { expect(onInitResult).toBe(true); }); - describe.skip("Option 1 - Fight", () => { + describe("Option 1 - Fight", () => { it("should have the correct properties", () => { const option = UncommonBreedEncounter.options[0]; expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); @@ -123,7 +123,7 @@ describe("Uncommon Breed - Mystery Encounter", () => { }); }); - it("should start a fight against the boss", async () => { + it.skip("should start a fight against the boss", async () => { const phaseSpy = vi.spyOn(scene, "pushPhase"); const unshiftPhaseSpy = vi.spyOn(scene, "unshiftPhase"); await game.runToMysteryEncounter(MysteryEncounterType.UNCOMMON_BREED, defaultParty); diff --git a/src/ui/abstact-option-select-ui-handler.ts b/src/ui/abstact-option-select-ui-handler.ts index c6abecda4c0..740830595ed 100644 --- a/src/ui/abstact-option-select-ui-handler.ts +++ b/src/ui/abstact-option-select-ui-handler.ts @@ -344,6 +344,7 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler { super.clear(); this.config = null; this.optionSelectContainer.setVisible(false); + this.scrollCursor = 0; this.eraseCursor(); } diff --git a/src/ui/achvs-ui-handler.ts b/src/ui/achvs-ui-handler.ts index 605b8c538a9..491c4acb523 100644 --- a/src/ui/achvs-ui-handler.ts +++ b/src/ui/achvs-ui-handler.ts @@ -1,12 +1,13 @@ -import BattleScene from "../battle-scene"; +import BattleScene from "#app/battle-scene"; import { Button } from "#enums/buttons"; import i18next from "i18next"; -import { Achv, achvs, getAchievementDescription } from "../system/achv"; -import { Voucher, getVoucherTypeIcon, getVoucherTypeName, vouchers } from "../system/voucher"; -import MessageUiHandler from "./message-ui-handler"; -import { addTextObject, TextStyle } from "./text"; -import { Mode } from "./ui"; -import { addWindow } from "./ui-theme"; +import { Achv, achvs, getAchievementDescription } from "#app/system/achv"; +import { Voucher, getVoucherTypeIcon, getVoucherTypeName, vouchers } from "#app/system/voucher"; +import MessageUiHandler from "#app/ui/message-ui-handler"; +import { addTextObject, TextStyle } from "#app/ui/text"; +import { Mode } from "#app/ui/ui"; +import { addWindow } from "#app/ui/ui-theme"; +import { ScrollBar } from "#app/ui/scroll-bar"; import { PlayerGender } from "#enums/player-gender"; enum Page { @@ -49,6 +50,7 @@ export default class AchvsUiHandler extends MessageUiHandler { private vouchersTotal: number; private currentTotal: number; + private scrollBar: ScrollBar; private scrollCursor: number; private cursorObj: Phaser.GameObjects.NineSlice | null; private currentPage: Page; @@ -91,7 +93,10 @@ export default class AchvsUiHandler extends MessageUiHandler { this.iconsBg = addWindow(this.scene, 0, this.headerBg.height, (this.scene.game.canvas.width / 6) - 2, (this.scene.game.canvas.height / 6) - this.headerBg.height - 68); this.iconsBg.setOrigin(0, 0); - this.iconsContainer = this.scene.add.container(6, this.headerBg.height + 6); + const yOffset = 6; + this.scrollBar = new ScrollBar(this.scene, this.iconsBg.width - 9, this.iconsBg.y + yOffset, 4, this.iconsBg.height - yOffset * 2, this.ROWS); + + this.iconsContainer = this.scene.add.container(5, this.headerBg.height + 8); this.icons = []; @@ -148,6 +153,7 @@ export default class AchvsUiHandler extends MessageUiHandler { this.mainContainer.add(this.headerText); this.mainContainer.add(this.headerActionText); this.mainContainer.add(this.iconsBg); + this.mainContainer.add(this.scrollBar); this.mainContainer.add(this.iconsContainer); this.mainContainer.add(titleBg); this.mainContainer.add(this.titleText); @@ -162,6 +168,7 @@ export default class AchvsUiHandler extends MessageUiHandler { this.currentPage = Page.ACHIEVEMENTS; this.setCursor(0); + this.setScrollCursor(0); this.mainContainer.setVisible(false); } @@ -175,6 +182,8 @@ export default class AchvsUiHandler extends MessageUiHandler { this.mainContainer.setVisible(true); this.setCursor(0); this.setScrollCursor(0); + this.scrollBar.setTotalRows(Math.ceil(this.currentTotal / this.COLS)); + this.scrollBar.setScrollCursor(0); this.getUi().moveTo(this.mainContainer, this.getUi().length - 1); @@ -224,6 +233,8 @@ export default class AchvsUiHandler extends MessageUiHandler { this.updateAchvIcons(); } this.setCursor(0, true); + this.scrollBar.setTotalRows(Math.ceil(this.currentTotal / this.COLS)); + this.scrollBar.setScrollCursor(0); this.mainContainer.update(); } if (button === Button.CANCEL) { @@ -237,32 +248,44 @@ export default class AchvsUiHandler extends MessageUiHandler { if (this.cursor < this.COLS) { if (this.scrollCursor) { success = this.setScrollCursor(this.scrollCursor - 1); + } else { + // Wrap around to the last row + success = this.setScrollCursor(Math.ceil(this.currentTotal / this.COLS) - this.ROWS); + let newCursorIndex = this.cursor + (this.ROWS - 1) * this.COLS; + if (newCursorIndex > this.currentTotal - this.scrollCursor * this.COLS -1) { + newCursorIndex -= this.COLS; + } + success = success && this.setCursor(newCursorIndex); } } else { success = this.setCursor(this.cursor - this.COLS); } break; case Button.DOWN: - const canMoveDown = (this.cursor + itemOffset) + this.COLS < this.currentTotal; + const canMoveDown = itemOffset + 1 < this.currentTotal; if (rowIndex >= this.ROWS - 1) { if (this.scrollCursor < Math.ceil(this.currentTotal / this.COLS) - this.ROWS && canMoveDown) { + // scroll down one row success = this.setScrollCursor(this.scrollCursor + 1); + } else { + // wrap back to the first row + success = this.setScrollCursor(0) && this.setCursor(this.cursor % this.COLS); } } else if (canMoveDown) { - success = this.setCursor(this.cursor + this.COLS); + success = this.setCursor(Math.min(this.cursor + this.COLS, this.currentTotal - itemOffset - 1)); } break; case Button.LEFT: - if (!this.cursor && this.scrollCursor) { - success = this.setScrollCursor(this.scrollCursor - 1) && this.setCursor(this.cursor + (this.COLS - 1)); - } else if (this.cursor) { + if (this.cursor % this.COLS === 0) { + success = this.setCursor(Math.min(this.cursor + this.COLS - 1, this.currentTotal - itemOffset - 1)); + } else { success = this.setCursor(this.cursor - 1); } break; case Button.RIGHT: - if (this.cursor + 1 === this.ROWS * this.COLS && this.scrollCursor < Math.ceil(this.currentTotal / this.COLS) - this.ROWS) { - success = this.setScrollCursor(this.scrollCursor + 1) && this.setCursor(this.cursor - (this.COLS - 1)); - } else if (this.cursor + itemOffset < this.currentTotal - 1) { + if ((this.cursor + 1) % this.COLS === 0 || (this.cursor + itemOffset) === (this.currentTotal - 1)) { + success = this.setCursor(this.cursor - this.cursor % this.COLS); + } else { success = this.setCursor(this.cursor + 1); } break; @@ -315,15 +338,22 @@ export default class AchvsUiHandler extends MessageUiHandler { } this.scrollCursor = scrollCursor; + this.scrollBar.setScrollCursor(this.scrollCursor); + + // Cursor cannot go farther than the last element in the list + const maxCursor = Math.min(this.cursor, this.currentTotal - this.scrollCursor * this.COLS - 1); + if (maxCursor !== this.cursor) { + this.setCursor(maxCursor); + } switch (this.currentPage) { case Page.ACHIEVEMENTS: this.updateAchvIcons(); - this.showAchv(achvs[Object.keys(achvs)[Math.min(this.cursor + this.scrollCursor * this.COLS, Object.values(achvs).length - 1)]]); + this.showAchv(achvs[Object.keys(achvs)[this.cursor + this.scrollCursor * this.COLS]]); break; case Page.VOUCHERS: this.updateVoucherIcons(); - this.showVoucher(vouchers[Object.keys(vouchers)[Math.min(this.cursor + this.scrollCursor * this.COLS, Object.values(vouchers).length - 1)]]); + this.showVoucher(vouchers[Object.keys(vouchers)[this.cursor + this.scrollCursor * this.COLS]]); break; } return true; @@ -411,6 +441,7 @@ export default class AchvsUiHandler extends MessageUiHandler { super.clear(); this.currentPage = Page.ACHIEVEMENTS; this.mainContainer.setVisible(false); + this.setScrollCursor(0); this.eraseCursor(); } diff --git a/src/ui/modifier-select-ui-handler.ts b/src/ui/modifier-select-ui-handler.ts index c9d3f195720..a1e10d74c64 100644 --- a/src/ui/modifier-select-ui-handler.ts +++ b/src/ui/modifier-select-ui-handler.ts @@ -160,7 +160,7 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { this.player = args[0]; - const partyHasHeldItem = this.player && !!this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier && m.isTransferrable).length; + const partyHasHeldItem = this.player && !!this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier && m.isTransferable).length; const canLockRarities = !!this.scene.findModifier(m => m instanceof LockModifierTiersModifier); this.transferButtonContainer.setVisible(false); @@ -277,13 +277,13 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { this.lockRarityButtonContainer.setVisible(canLockRarities); this.scene.tweens.add({ - targets: [ this.lockRarityButtonContainer, this.checkButtonContainer, this.continueButtonContainer ], + targets: [ this.checkButtonContainer, this.continueButtonContainer ], alpha: 1, duration: 250 }); this.scene.tweens.add({ - targets: [this.rerollButtonContainer], + targets: [this.rerollButtonContainer, this.lockRarityButtonContainer], alpha: this.rerollCost < 0 ? 0.5 : 1, duration: 250 }); diff --git a/src/ui/mystery-encounter-ui-handler.ts b/src/ui/mystery-encounter-ui-handler.ts index 307bab0a3af..08de740e3ec 100644 --- a/src/ui/mystery-encounter-ui-handler.ts +++ b/src/ui/mystery-encounter-ui-handler.ts @@ -6,7 +6,7 @@ import { Button } from "#enums/buttons"; import { addWindow, WindowVariant } from "./ui-theme"; import { MysteryEncounterPhase } from "../phases/mystery-encounter-phases"; import { PartyUiMode } from "./party-ui-handler"; -import MysteryEncounterOption from "../data/mystery-encounters/mystery-encounter-option"; +import MysteryEncounterOption from "#app/data/mystery-encounters/mystery-encounter-option"; import * as Utils from "../utils"; import { isNullOrUndefined } from "../utils"; import { getPokeballAtlasKey } from "../data/pokeball"; @@ -42,7 +42,8 @@ export default class MysteryEncounterUiHandler extends UiHandler { private encounterOptions: MysteryEncounterOption[] = []; private optionsMeetsReqs: boolean[]; - protected viewPartyIndex: integer = 0; + protected viewPartyIndex: number = 0; + protected viewPartyXPosition: number = 0; protected blockInput: boolean = true; @@ -300,11 +301,11 @@ export default class MysteryEncounterUiHandler extends UiHandler { } } - override getCursor(): integer { + override getCursor(): number { return this.cursor ? this.cursor : 0; } - override setCursor(cursor: integer): boolean { + override setCursor(cursor: number): boolean { const prevCursor = this.getCursor(); const changed = prevCursor !== cursor; if (changed) { @@ -319,7 +320,7 @@ export default class MysteryEncounterUiHandler extends UiHandler { } if (cursor === this.viewPartyIndex) { - this.cursorObj.setPosition(246, -17); + this.cursorObj.setPosition(this.viewPartyXPosition, -17); } else if (this.optionsContainer.getAll()?.length === 3) { // 2 Options this.cursorObj.setPosition(-10.5 + (cursor % 2 === 1 ? 100 : 0), 15); } else if (this.optionsContainer.getAll()?.length === 4) { // 3 Options @@ -419,8 +420,10 @@ export default class MysteryEncounterUiHandler extends UiHandler { } // View Party Button - const viewPartyText = addBBCodeTextObject(this.scene, 256, -24, getBBCodeFrag(i18next.t("mysteryEncounterMessages:view_party_button"), TextStyle.PARTY), TextStyle.PARTY); + const viewPartyText = addBBCodeTextObject(this.scene, (this.scene.game.canvas.width) / 6, -24, getBBCodeFrag(i18next.t("mysteryEncounterMessages:view_party_button"), TextStyle.PARTY), TextStyle.PARTY); this.optionsContainer.add(viewPartyText); + viewPartyText.x -= (viewPartyText.displayWidth + 16); + this.viewPartyXPosition = viewPartyText.x - 10; // Description Window const titleTextObject = addBBCodeTextObject(this.scene, 0, 0, titleText ?? "", TextStyle.TOOLTIP_TITLE, { wordWrap: { width: 750 }, align: "center", lineSpacing: -8 }); diff --git a/src/ui/party-ui-handler.ts b/src/ui/party-ui-handler.ts index 8c777350964..6b6ce2aa789 100644 --- a/src/ui/party-ui-handler.ts +++ b/src/ui/party-ui-handler.ts @@ -355,7 +355,7 @@ export default class PartyUiHandler extends MessageUiHandler { const newPokemon = this.scene.getParty()[p]; // this next line gets all of the transferable items from pokemon [p]; it does this by getting all the held modifiers that are transferable and checking to see if they belong to pokemon [p] const getTransferrableItemsFromPokemon = (newPokemon: PlayerPokemon) => - this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier && (m as PokemonHeldItemModifier).isTransferrable && (m as PokemonHeldItemModifier).pokemonId === newPokemon.id) as PokemonHeldItemModifier[]; + this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier && (m as PokemonHeldItemModifier).isTransferable && (m as PokemonHeldItemModifier).pokemonId === newPokemon.id) as PokemonHeldItemModifier[]; // this next bit checks to see if the the selected item from the original transfer pokemon exists on the new pokemon [p]; this returns undefined if the new pokemon doesn't have the item at all, otherwise it returns the pokemonHeldItemModifier for that item const matchingModifier = newPokemon.scene.findModifier(m => m instanceof PokemonHeldItemModifier && m.pokemonId === newPokemon.id && m.matchType(getTransferrableItemsFromPokemon(pokemon)[this.transferOptionCursor])) as PokemonHeldItemModifier; const partySlot = this.partySlots.filter(m => m.getPokemon() === newPokemon)[0]; // this gets pokemon [p] for us @@ -399,7 +399,7 @@ export default class PartyUiHandler extends MessageUiHandler { || (option === PartyOption.RELEASE && this.partyUiMode === PartyUiMode.RELEASE)) { let filterResult: string | null; const getTransferrableItemsFromPokemon = (pokemon: PlayerPokemon) => - this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier && m.isTransferrable && m.pokemonId === pokemon.id) as PokemonHeldItemModifier[]; + this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier && m.isTransferable && m.pokemonId === pokemon.id) as PokemonHeldItemModifier[]; if (option !== PartyOption.TRANSFER && option !== PartyOption.SPLICE) { filterResult = (this.selectFilter as PokemonSelectFilter)(pokemon); if (filterResult === null && (option === PartyOption.SEND_OUT || option === PartyOption.PASS_BATON)) { @@ -596,7 +596,7 @@ export default class PartyUiHandler extends MessageUiHandler { if (this.partyUiMode === PartyUiMode.MODIFIER_TRANSFER && !this.transferMode) { /** Initialize item quantities for the selected Pokemon */ const itemModifiers = this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier - && m.isTransferrable && m.pokemonId === this.scene.getParty()[this.cursor].id) as PokemonHeldItemModifier[]; + && m.isTransferable && m.pokemonId === this.scene.getParty()[this.cursor].id) as PokemonHeldItemModifier[]; this.transferQuantities = itemModifiers.map(item => item.getStackCount()); this.transferQuantitiesMax = itemModifiers.map(item => item.getStackCount()); } @@ -813,7 +813,7 @@ export default class PartyUiHandler extends MessageUiHandler { const itemModifiers = this.partyUiMode === PartyUiMode.MODIFIER_TRANSFER ? this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier - && m.isTransferrable && m.pokemonId === pokemon.id) as PokemonHeldItemModifier[] + && m.isTransferable && m.pokemonId === pokemon.id) as PokemonHeldItemModifier[] : []; if (this.options.length) { diff --git a/src/ui/scroll-bar.ts b/src/ui/scroll-bar.ts index e756393ae1a..5ed79d0cdad 100644 --- a/src/ui/scroll-bar.ts +++ b/src/ui/scroll-bar.ts @@ -1,36 +1,65 @@ +/** + * A vertical scrollbar element that resizes dynamically based on the current scrolling + * and number of elements that can be shown on screen + */ export class ScrollBar extends Phaser.GameObjects.Container { - private bg: Phaser.GameObjects.Image; + private bg: Phaser.GameObjects.NineSlice; private handleBody: Phaser.GameObjects.Rectangle; - private handleBottom: Phaser.GameObjects.Image; - private pages: number; - private page: number; + private handleBottom: Phaser.GameObjects.NineSlice; + private currentRow: number; + private totalRows: number; + private maxRows: number; - constructor(scene: Phaser.Scene, x: number, y: number, pages: number) { + /** + * @param scene the current scene + * @param x the scrollbar's x position (origin: top left) + * @param y the scrollbar's y position (origin: top left) + * @param width the scrollbar's width + * @param height the scrollbar's height + * @param maxRows the maximum number of rows that can be shown at once + */ + constructor(scene: Phaser.Scene, x: number, y: number, width: number, height: number, maxRows: number) { super(scene, x, y); - this.bg = scene.add.image(0, 0, "scroll_bar"); + this.maxRows = maxRows; + + const borderSize = 2; + width = Math.max(width, 4); + + this.bg = scene.add.nineslice(0, 0, "scroll_bar", undefined, width, height, borderSize, borderSize, borderSize, borderSize); this.bg.setOrigin(0, 0); this.add(this.bg); - this.handleBody = scene.add.rectangle(1, 1, 3, 4, 0xaaaaaa); + this.handleBody = scene.add.rectangle(1, 1, width - 2, 4, 0xaaaaaa); this.handleBody.setOrigin(0, 0); this.add(this.handleBody); - this.handleBottom = scene.add.image(1, 1, "scroll_bar_handle"); + this.handleBottom = scene.add.nineslice(1, 1, "scroll_bar_handle", undefined, width - 2, 2, 2, 0, 0, 0); this.handleBottom.setOrigin(0, 0); this.add(this.handleBottom); } - setPage(page: number): void { - this.page = page; - this.handleBody.y = 1 + (this.bg.displayHeight - 1 - this.handleBottom.displayHeight) / this.pages * page; + /** + * Set the current row that is displayed + * Moves the bar handle up or down accordingly + * @param scrollCursor how many times the view was scrolled down + */ + setScrollCursor(scrollCursor: number): void { + this.currentRow = scrollCursor; + this.handleBody.y = 1 + (this.bg.displayHeight - 1 - this.handleBottom.displayHeight) / this.totalRows * this.currentRow; this.handleBottom.y = this.handleBody.y + this.handleBody.displayHeight; } - setPages(pages: number): void { - this.pages = pages; - this.handleBody.height = (this.bg.displayHeight - 1 - this.handleBottom.displayHeight) * 9 / this.pages; + /** + * Set the total number of rows to display + * If it's smaller than the maximum number of rows on screen the bar will get hidden + * Otherwise the scrollbar handle gets resized based on the ratio to the maximum number of rows + * @param rows how many rows of data there are in total + */ + setTotalRows(rows: number): void { + this.totalRows = rows; + this.handleBody.height = (this.bg.displayHeight - 1 - this.handleBottom.displayHeight) * this.maxRows / this.totalRows; - this.setVisible(this.pages > 9); + this.setVisible(this.totalRows > this.maxRows); } } diff --git a/src/ui/settings/abstract-control-settings-ui-handler.ts b/src/ui/settings/abstract-control-settings-ui-handler.ts index f8dab1bf7cc..efa262bb2e9 100644 --- a/src/ui/settings/abstract-control-settings-ui-handler.ts +++ b/src/ui/settings/abstract-control-settings-ui-handler.ts @@ -1,11 +1,12 @@ -import UiHandler from "../ui-handler"; -import BattleScene from "../../battle-scene"; -import {Mode} from "../ui"; -import {InterfaceConfig} from "../../inputs-controller"; -import {addWindow} from "../ui-theme"; -import {addTextObject, TextStyle} from "../text"; -import {getIconWithSettingName} from "#app/configs/inputs/configHandler"; -import NavigationMenu, {NavigationManager} from "#app/ui/settings/navigationMenu"; +import UiHandler from "#app/ui/ui-handler"; +import BattleScene from "#app/battle-scene"; +import { Mode } from "#app/ui/ui"; +import { InterfaceConfig } from "#app/inputs-controller"; +import { addWindow } from "#app/ui/ui-theme"; +import { addTextObject, TextStyle } from "#app/ui/text"; +import { ScrollBar } from "#app/ui/scroll-bar"; +import { getIconWithSettingName } from "#app/configs/inputs/configHandler"; +import NavigationMenu, { NavigationManager } from "#app/ui/settings/navigationMenu"; import { Device } from "#enums/devices"; import { Button } from "#enums/buttons"; import i18next from "i18next"; @@ -19,7 +20,7 @@ export interface LayoutConfig { inputsIcons: InputsIcons; settingLabels: Phaser.GameObjects.Text[]; optionValueLabels: Phaser.GameObjects.Text[][]; - optionCursors: integer[]; + optionCursors: number[]; keys: string[]; bindingSettings: Array; } @@ -31,8 +32,9 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler protected optionsContainer: Phaser.GameObjects.Container; protected navigationContainer: NavigationMenu; - protected scrollCursor: integer; - protected optionCursors: integer[]; + protected scrollBar: ScrollBar; + protected scrollCursor: number; + protected optionCursors: number[]; protected cursorObj: Phaser.GameObjects.NineSlice | null; protected optionsBg: Phaser.GameObjects.NineSlice; @@ -65,7 +67,7 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler protected device: Device; abstract saveSettingToLocalStorage(setting, cursor): void; - abstract setSetting(scene: BattleScene, setting, value: integer): boolean; + abstract setSetting(scene: BattleScene, setting, value: number): boolean; /** * Constructor for the AbstractSettingsUiHandler. @@ -241,7 +243,7 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler // Calculate the total available space for placing option labels next to their setting label // We reserve space for the setting label and then distribute the remaining space evenly - const totalSpace = (300 - labelWidth) - totalWidth / 6; + const totalSpace = (297 - labelWidth) - totalWidth / 6; // Calculate the spacing between options based on the available space divided by the number of gaps between labels const optionSpacing = Math.floor(totalSpace / (optionValueLabels[s].length - 1)); @@ -269,6 +271,11 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler // Add the options container to the overall settings container to be displayed in the UI. this.settingsContainer.add(optionsContainer); } + + // Add vertical scrollbar + this.scrollBar = new ScrollBar(this.scene, this.optionsBg.width - 9, this.optionsBg.y + 5, 4, this.optionsBg.height - 11, this.rowsToDisplay); + this.settingsContainer.add(this.scrollBar); + // Add the settings container to the UI. ui.add(this.settingsContainer); @@ -413,6 +420,8 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler this.optionCursors = layout.optionCursors; this.inputsIcons = layout.inputsIcons; this.bindingSettings = layout.bindingSettings; + this.scrollBar.setTotalRows(layout.settingLabels.length); + this.scrollBar.setScrollCursor(0); // Return true indicating the layout was successfully applied. return true; @@ -538,7 +547,7 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler * @param cursor - The cursor position to set. * @returns `true` if the cursor was set successfully. */ - setCursor(cursor: integer): boolean { + setCursor(cursor: number): boolean { const ret = super.setCursor(cursor); // If the optionsContainer is not initialized, return the result from the parent class directly. if (!this.optionsContainer) { @@ -547,7 +556,8 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler // Check if the cursor object exists, if not, create it. if (!this.cursorObj) { - this.cursorObj = this.scene.add.nineslice(0, 0, "summary_moves_cursor", undefined, (this.scene.game.canvas.width / 6) - 10, 16, 1, 1, 1, 1); + const cursorWidth = (this.scene.game.canvas.width / 6) - (this.scrollBar.visible? 16 : 10); + this.cursorObj = this.scene.add.nineslice(0, 0, "summary_moves_cursor", undefined, cursorWidth, 16, 1, 1, 1, 1); this.cursorObj.setOrigin(0, 0); // Set the origin to the top-left corner. this.optionsContainer.add(this.cursorObj); // Add the cursor to the options container. } @@ -564,7 +574,7 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler * @param scrollCursor - The scroll cursor position to set. * @returns `true` if the scroll cursor was set successfully. */ - setScrollCursor(scrollCursor: integer): boolean { + setScrollCursor(scrollCursor: number): boolean { // Check if the new scroll position is the same as the current one; if so, do not update. if (scrollCursor === this.scrollCursor) { return false; @@ -572,6 +582,7 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler // Update the internal scroll cursor state this.scrollCursor = scrollCursor; + this.scrollBar.setScrollCursor(this.scrollCursor); // Apply the new scroll position to the settings UI. this.updateSettingsScroll(); @@ -590,7 +601,7 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler * @param save - Whether to save the setting to local storage. * @returns `true` if the option cursor was set successfully. */ - setOptionCursor(settingIndex: integer, cursor: integer, save?: boolean): boolean { + setOptionCursor(settingIndex: number, cursor: number, save?: boolean): boolean { // Retrieve the specific setting using the settingIndex from the settingDevice enumeration. const setting = this.setting[Object.keys(this.setting)[settingIndex]]; diff --git a/src/ui/settings/abstract-settings-ui-handler.ts b/src/ui/settings/abstract-settings-ui-handler.ts index 570377eab43..975a32127ff 100644 --- a/src/ui/settings/abstract-settings-ui-handler.ts +++ b/src/ui/settings/abstract-settings-ui-handler.ts @@ -1,12 +1,13 @@ -import BattleScene from "../../battle-scene"; -import { hasTouchscreen, isMobile } from "../../touch-controls"; -import { TextStyle, addTextObject } from "../text"; -import { Mode } from "../ui"; -import UiHandler from "../ui-handler"; -import { addWindow } from "../ui-theme"; -import {Button} from "#enums/buttons"; -import {InputsIcons} from "#app/ui/settings/abstract-control-settings-ui-handler"; -import NavigationMenu, {NavigationManager} from "#app/ui/settings/navigationMenu"; +import BattleScene from "#app/battle-scene"; +import { hasTouchscreen, isMobile } from "#app/touch-controls"; +import { TextStyle, addTextObject } from "#app/ui/text"; +import { Mode } from "#app/ui/ui"; +import UiHandler from "#app/ui/ui-handler"; +import { addWindow } from "#app/ui/ui-theme"; +import { ScrollBar } from "#app/ui/scroll-bar"; +import { Button } from "#enums/buttons"; +import { InputsIcons } from "#app/ui/settings/abstract-control-settings-ui-handler"; +import NavigationMenu, { NavigationManager } from "#app/ui/settings/navigationMenu"; import { Setting, SettingKeys, SettingType } from "#app/system/settings/settings"; import i18next from "i18next"; @@ -19,11 +20,12 @@ export default class AbstractSettingsUiHandler extends UiHandler { private optionsContainer: Phaser.GameObjects.Container; private navigationContainer: NavigationMenu; - private scrollCursor: integer; + private scrollCursor: number; + private scrollBar: ScrollBar; private optionsBg: Phaser.GameObjects.NineSlice; - private optionCursors: integer[]; + private optionCursors: number[]; private settingLabels: Phaser.GameObjects.Text[]; private optionValueLabels: Phaser.GameObjects.Text[][]; @@ -117,7 +119,7 @@ export default class AbstractSettingsUiHandler extends UiHandler { const labelWidth = Math.max(78, this.settingLabels[s].displayWidth + 8); - const totalSpace = (300 - labelWidth) - totalWidth / 6; + const totalSpace = (297 - labelWidth) - totalWidth / 6; const optionSpacing = Math.floor(totalSpace / (this.optionValueLabels[s].length - 1)); let xOffset = 0; @@ -130,7 +132,11 @@ export default class AbstractSettingsUiHandler extends UiHandler { this.optionCursors = this.settings.map(setting => setting.default); + this.scrollBar = new ScrollBar(this.scene, this.optionsBg.width - 9, this.optionsBg.y + 5, 4, this.optionsBg.height - 11, this.rowsToDisplay); + this.scrollBar.setTotalRows(this.settings.length); + this.settingsContainer.add(this.optionsBg); + this.settingsContainer.add(this.scrollBar); this.settingsContainer.add(this.navigationContainer); this.settingsContainer.add(actionsBg); this.settingsContainer.add(this.optionsContainer); @@ -186,6 +192,7 @@ export default class AbstractSettingsUiHandler extends UiHandler { this.settingsContainer.setVisible(true); this.setCursor(0); + this.setScrollCursor(0); this.getUi().moveTo(this.settingsContainer, this.getUi().length - 1); @@ -301,11 +308,12 @@ export default class AbstractSettingsUiHandler extends UiHandler { * @param cursor - The cursor position to set. * @returns `true` if the cursor was set successfully. */ - setCursor(cursor: integer): boolean { + setCursor(cursor: number): boolean { const ret = super.setCursor(cursor); if (!this.cursorObj) { - this.cursorObj = this.scene.add.nineslice(0, 0, "summary_moves_cursor", undefined, (this.scene.game.canvas.width / 6) - 10, 16, 1, 1, 1, 1); + const cursorWidth = (this.scene.game.canvas.width / 6) - (this.scrollBar.visible? 16 : 10); + this.cursorObj = this.scene.add.nineslice(0, 0, "summary_moves_cursor", undefined, cursorWidth, 16, 1, 1, 1, 1); this.cursorObj.setOrigin(0, 0); this.optionsContainer.add(this.cursorObj); } @@ -323,7 +331,7 @@ export default class AbstractSettingsUiHandler extends UiHandler { * @param save - Whether to save the setting to local storage. * @returns `true` if the option cursor was set successfully. */ - setOptionCursor(settingIndex: integer, cursor: integer, save?: boolean): boolean { + setOptionCursor(settingIndex: number, cursor: number, save?: boolean): boolean { const setting = this.settings[settingIndex]; if (setting.key === SettingKeys.Touch_Controls && cursor && hasTouchscreen() && isMobile()) { @@ -359,12 +367,13 @@ export default class AbstractSettingsUiHandler extends UiHandler { * @param scrollCursor - The scroll cursor position to set. * @returns `true` if the scroll cursor was set successfully. */ - setScrollCursor(scrollCursor: integer): boolean { + setScrollCursor(scrollCursor: number): boolean { if (scrollCursor === this.scrollCursor) { return false; } this.scrollCursor = scrollCursor; + this.scrollBar.setScrollCursor(this.scrollCursor); this.updateSettingsScroll(); @@ -394,6 +403,7 @@ export default class AbstractSettingsUiHandler extends UiHandler { clear() { super.clear(); this.settingsContainer.setVisible(false); + this.setScrollCursor(0); this.eraseCursor(); this.getUi().bgmBar.toggleBgmBar(this.scene.showBgmBar); if (this.reloadRequired) { diff --git a/src/ui/starter-select-ui-handler.ts b/src/ui/starter-select-ui-handler.ts index 0e101d30ce7..5ef26d1ba88 100644 --- a/src/ui/starter-select-ui-handler.ts +++ b/src/ui/starter-select-ui-handler.ts @@ -83,7 +83,7 @@ const languageSettings: { [key: string]: LanguageSetting } = { }, "fr":{ starterInfoTextSize: "54px", - instructionTextSize: "35px", + instructionTextSize: "38px", }, "it":{ starterInfoTextSize: "56px", @@ -627,7 +627,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { const starterBoxContainer = this.scene.add.container(speciesContainerX + 6, 9); //115 - this.starterSelectScrollBar = new ScrollBar(this.scene, 161, 12, 0); + this.starterSelectScrollBar = new ScrollBar(this.scene, 161, 12, 5, starterContainerWindow.height - 6, 9); starterBoxContainer.add(this.starterSelectScrollBar); @@ -2540,8 +2540,8 @@ export default class StarterSelectUiHandler extends MessageUiHandler { } }); - this.starterSelectScrollBar.setPages(Math.max(Math.ceil(this.filteredStarterContainers.length / 9), 1)); - this.starterSelectScrollBar.setPage(0); + this.starterSelectScrollBar.setTotalRows(Math.max(Math.ceil(this.filteredStarterContainers.length / 9), 1)); + this.starterSelectScrollBar.setScrollCursor(0); // sort const sort = this.filterBar.getVals(DropDownColumn.SORT)[0]; @@ -2576,7 +2576,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { const onScreenFirstIndex = this.scrollCursor * maxColumns; const onScreenLastIndex = Math.min(this.filteredStarterContainers.length - 1, onScreenFirstIndex + maxRows * maxColumns -1); - this.starterSelectScrollBar.setPage(this.scrollCursor); + this.starterSelectScrollBar.setScrollCursor(this.scrollCursor); let pokerusCursorIndex = 0; this.filteredStarterContainers.forEach((container, i) => {