From d1294caeb60b9a291a2558900a841e3d407d5019 Mon Sep 17 00:00:00 2001 From: star-krieg <113486223+star-krieg@users.noreply.github.com> Date: Sun, 1 Dec 2024 03:28:29 +0800 Subject: [PATCH 01/13] [Balance] Candy friendship changes (#4947) Changes: - Increase default value for friendship gain from winning a battle (from 2 to 3) - Increase Classic candy friendship multiplier (from 2 to 3) - Increase Rare Candy friendship gain (from 5 to 6) - Decrease value for friendship decrease from losing a battle (from 10 to 5) - Update Candy friendship thresholds for getting candy for some starter costs. - Change Soothe Bell from Rogue to Great (Weight: 2 in Great) - Adjust Map weight to account for change (Weight: 1 to 2) - Clowning around ME: When shuffling items, soothe bells will get replaced by a random Ultra Tier item - Clowning around ME: When shuffling items, no item will get replaced by a Soothe Bell Commit history: * Candy friendship changes Update default value for friendship gain from battle and adjust Soothe Bell tier to Great * Update modifier-type.ts * Update friendship constants * Clowning around encounter no longer consider soothe bell rogue tier * Expert Breeder ME test will no longer fail if candy gain per battle is changed --------- Co-authored-by: Starkrieg Co-authored-by: damocleas Co-authored-by: Moka Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/data/balance/starters.ts | 18 +++++++++--------- .../encounters/clowning-around-encounter.ts | 5 +++-- src/modifier/modifier-type.ts | 4 ++-- .../clowning-around-encounter.test.ts | 5 ++++- .../the-expert-breeder-encounter.test.ts | 10 +++++++--- 5 files changed, 25 insertions(+), 17 deletions(-) diff --git a/src/data/balance/starters.ts b/src/data/balance/starters.ts index d6a1f0c3eaf..a4a3c9d011c 100644 --- a/src/data/balance/starters.ts +++ b/src/data/balance/starters.ts @@ -3,10 +3,10 @@ import { Species } from "#enums/species"; export const POKERUS_STARTER_COUNT = 5; // #region Friendship constants -export const CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER = 2; -export const FRIENDSHIP_GAIN_FROM_BATTLE = 2; -export const FRIENDSHIP_GAIN_FROM_RARE_CANDY = 5; -export const FRIENDSHIP_LOSS_FROM_FAINT = 10; +export const CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER = 3; +export const FRIENDSHIP_GAIN_FROM_BATTLE = 3; +export const FRIENDSHIP_GAIN_FROM_RARE_CANDY = 6; +export const FRIENDSHIP_LOSS_FROM_FAINT = 5; /** * Function to get the cumulative friendship threshold at which a candy is earned @@ -16,19 +16,19 @@ export const FRIENDSHIP_LOSS_FROM_FAINT = 10; export function getStarterValueFriendshipCap(starterCost: number): number { switch (starterCost) { case 1: - return 20; + return 25; case 2: - return 40; + return 50; case 3: - return 60; + return 75; case 4: return 100; case 5: - return 140; + return 150; case 6: return 200; case 7: - return 280; + return 300; case 8: case 9: return 450; diff --git a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts index ae6cabd4dae..6bd6856604b 100644 --- a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts +++ b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts @@ -276,6 +276,8 @@ export const ClowningAroundEncounter: MysteryEncounter = generateItemsOfTier(scene, mostHeldItemsPokemon, numBerries, "Berries"); // Shuffle Transferable held items in the same tier (only shuffles Ultra and Rogue atm) + // For the purpose of this ME, Soothe Bells and Lucky Eggs are counted as Ultra tier + // And Golden Eggs as Rogue tier let numUltra = 0; let numRogue = 0; items.filter(m => m.isTransferable && !(m instanceof BerryModifier)) @@ -285,7 +287,7 @@ export const ClowningAroundEncounter: MysteryEncounter = if (type.id === "GOLDEN_EGG" || tier === ModifierTier.ROGUE) { numRogue += m.stackCount; scene.removeModifier(m); - } else if (type.id === "LUCKY_EGG" || tier === ModifierTier.ULTRA) { + } else if (type.id === "LUCKY_EGG" || type.id === "SOOTHE_BELL" || tier === ModifierTier.ULTRA) { numUltra += m.stackCount; scene.removeModifier(m); } @@ -456,7 +458,6 @@ function generateItemsOfTier(scene: BattleScene, pokemon: PlayerPokemon, numItem [ modifierTypes.LEFTOVERS, 4 ], [ modifierTypes.SHELL_BELL, 4 ], [ modifierTypes.SOUL_DEW, 10 ], - [ modifierTypes.SOOTHE_BELL, 3 ], [ modifierTypes.SCOPE_LENS, 1 ], [ modifierTypes.BATON, 1 ], [ modifierTypes.FOCUS_BAND, 5 ], diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index 901aa422c61..04776afc624 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -1702,7 +1702,8 @@ const modifierPool: ModifierPool = { new WeightedModifierType(modifierTypes.EVOLUTION_ITEM, (party: Pokemon[]) => { return Math.min(Math.ceil(party[0].scene.currentBattle.waveIndex / 15), 8); }, 8), - new WeightedModifierType(modifierTypes.MAP, (party: Pokemon[]) => party[0].scene.gameMode.isClassic && party[0].scene.currentBattle.waveIndex < 180 ? 1 : 0, 1), + new WeightedModifierType(modifierTypes.MAP, (party: Pokemon[]) => party[0].scene.gameMode.isClassic && party[0].scene.currentBattle.waveIndex < 180 ? 2 : 0, 2), + new WeightedModifierType(modifierTypes.SOOTHE_BELL, 2), new WeightedModifierType(modifierTypes.TM_GREAT, 3), new WeightedModifierType(modifierTypes.MEMORY_MUSHROOM, (party: Pokemon[]) => { if (!party.find(p => p.getLearnableLevelMoves().length)) { @@ -1800,7 +1801,6 @@ const modifierPool: ModifierPool = { new WeightedModifierType(modifierTypes.SOUL_DEW, 7), //new WeightedModifierType(modifierTypes.OVAL_CHARM, 6), new WeightedModifierType(modifierTypes.CATCHING_CHARM, (party: Pokemon[]) => !party[0].scene.gameMode.isFreshStartChallenge() && party[0].scene.gameData.getSpeciesCount(d => !!d.caughtAttr) > 100 ? 4 : 0, 4), - new WeightedModifierType(modifierTypes.SOOTHE_BELL, 4), new WeightedModifierType(modifierTypes.ABILITY_CHARM, skipInClassicAfterWave(189, 6)), new WeightedModifierType(modifierTypes.FOCUS_BAND, 5), new WeightedModifierType(modifierTypes.KINGS_ROCK, 3), diff --git a/src/test/mystery-encounter/encounters/clowning-around-encounter.test.ts b/src/test/mystery-encounter/encounters/clowning-around-encounter.test.ts index a403a306b3d..e7ea6eea0ea 100644 --- a/src/test/mystery-encounter/encounters/clowning-around-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/clowning-around-encounter.test.ts @@ -266,6 +266,9 @@ describe("Clowning Around - Mystery Encounter", () => { // 5 Lucky Egg on lead (ultra) itemType = generateModifierType(scene, modifierTypes.LUCKY_EGG) as PokemonHeldItemModifierType; await addItemToPokemon(scene, scene.getPlayerParty()[0], 5, itemType); + // 3 Soothe Bell on lead (great tier, but counted as ultra by this ME) + itemType = generateModifierType(scene, modifierTypes.SOOTHE_BELL) as PokemonHeldItemModifierType; + await addItemToPokemon(scene, scene.getPlayerParty()[0], 3, itemType); // 5 Soul Dew on lead (rogue) itemType = generateModifierType(scene, modifierTypes.SOUL_DEW) as PokemonHeldItemModifierType; await addItemToPokemon(scene, scene.getPlayerParty()[0], 5, itemType); @@ -286,7 +289,7 @@ describe("Clowning Around - Mystery Encounter", () => { const rogueCountAfter = leadItemsAfter .filter(m => m.type.tier === ModifierTier.ROGUE) .reduce((a, b) => a + b.stackCount, 0); - expect(ultraCountAfter).toBe(10); + expect(ultraCountAfter).toBe(13); expect(rogueCountAfter).toBe(7); const secondItemsAfter = scene.getPlayerParty()[1].getHeldItems(); diff --git a/src/test/mystery-encounter/encounters/the-expert-breeder-encounter.test.ts b/src/test/mystery-encounter/encounters/the-expert-breeder-encounter.test.ts index 7fc2490fcc9..87ccff71e22 100644 --- a/src/test/mystery-encounter/encounters/the-expert-breeder-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/the-expert-breeder-encounter.test.ts @@ -18,6 +18,7 @@ import { TheExpertPokemonBreederEncounter } from "#app/data/mystery-encounters/e import { TrainerType } from "#enums/trainer-type"; import { EggTier } from "#enums/egg-type"; import { PostMysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; +import { FRIENDSHIP_GAIN_FROM_BATTLE } from "#app/data/balance/starters"; const namespace = "mysteryEncounters/theExpertPokemonBreeder"; const defaultParty = [ Species.LAPRAS, Species.GENGAR, Species.ABRA ]; @@ -182,7 +183,10 @@ describe("The Expert Pokémon Breeder - Mystery Encounter", () => { await game.phaseInterceptor.to(PostMysteryEncounterPhase); const friendshipAfter = scene.currentBattle.mysteryEncounter!.misc.pokemon1.friendship; - expect(friendshipAfter).toBe(friendshipBefore + 20 + 2); // +2 extra for friendship gained from winning battle + // 20 from ME + extra from winning battle (that extra is not accurate to what happens in game. + // The Pokemon normally gets FRIENDSHIP_GAIN_FROM_BATTLE 3 times, once for each defeated Pokemon + // but due to how skipBattleRunMysteryEncounterRewardsPhase is implemented, it only receives it once) + expect(friendshipAfter).toBe(friendshipBefore + 20 + FRIENDSHIP_GAIN_FROM_BATTLE); }); }); @@ -261,7 +265,7 @@ describe("The Expert Pokémon Breeder - Mystery Encounter", () => { await game.phaseInterceptor.to(PostMysteryEncounterPhase); const friendshipAfter = scene.currentBattle.mysteryEncounter!.misc.pokemon2.friendship; - expect(friendshipAfter).toBe(friendshipBefore + 20 + 2); // +2 extra for friendship gained from winning battle + expect(friendshipAfter).toBe(friendshipBefore + 20 + FRIENDSHIP_GAIN_FROM_BATTLE); // 20 from ME + extra for friendship gained from winning battle }); }); @@ -340,7 +344,7 @@ describe("The Expert Pokémon Breeder - Mystery Encounter", () => { await game.phaseInterceptor.to(PostMysteryEncounterPhase); const friendshipAfter = scene.currentBattle.mysteryEncounter!.misc.pokemon3.friendship; - expect(friendshipAfter).toBe(friendshipBefore + 20 + 2); // +2 extra for friendship gained from winning battle + expect(friendshipAfter).toBe(friendshipBefore + 20 + FRIENDSHIP_GAIN_FROM_BATTLE); // 20 + extra for friendship gained from winning battle }); }); }); From 5af2bcd5ec0a44dedbf49ae6d6f72b2536b359c4 Mon Sep 17 00:00:00 2001 From: muscode Date: Sat, 30 Nov 2024 13:44:06 -0600 Subject: [PATCH 02/13] [Bug] Prevent battle skip with Wimp Out (#4931) Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> Co-authored-by: Mumble <171087428+frutescens@users.noreply.github.com> Co-authored-by: PigeonBar <56974298+PigeonBar@users.noreply.github.com> Co-authored-by: Moka <54149968+MokaStitcher@users.noreply.github.com> --- src/data/ability.ts | 12 ++++----- src/data/arena-tag.ts | 2 +- src/data/move.ts | 2 +- src/field/pokemon.ts | 3 +++ src/phases/move-effect-phase.ts | 6 +++-- src/phases/post-turn-status-effect-phase.ts | 2 +- src/phases/turn-end-phase.ts | 26 +++++++++--------- src/phases/weather-effect-phase.ts | 10 ++++--- src/test/abilities/wimp_out.test.ts | 30 +++++++++++++++++++++ 9 files changed, 67 insertions(+), 26 deletions(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index 66624f03436..234b502c23f 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -3720,16 +3720,16 @@ export class PostTurnHurtIfSleepingAbAttr extends PostTurnAbAttr { /** * Deals damage to all sleeping opponents equal to 1/8 of their max hp (min 1) - * @param {Pokemon} pokemon Pokemon that has this ability - * @param {boolean} passive N/A - * @param {boolean} simulated true if applying in a simulated call. - * @param {any[]} args N/A - * @returns {boolean} true if any opponents are sleeping + * @param pokemon Pokemon that has this ability + * @param passive N/A + * @param simulated `true` if applying in a simulated call. + * @param args N/A + * @returns `true` if any opponents are sleeping */ applyPostTurn(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean | Promise { let hadEffect: boolean = false; for (const opp of pokemon.getOpponents()) { - if ((opp.status?.effect === StatusEffect.SLEEP || opp.hasAbility(Abilities.COMATOSE)) && !opp.hasAbilityWithAttr(BlockNonDirectDamageAbAttr)) { + if ((opp.status?.effect === StatusEffect.SLEEP || opp.hasAbility(Abilities.COMATOSE)) && !opp.hasAbilityWithAttr(BlockNonDirectDamageAbAttr) && !opp.switchOutStatus) { if (!simulated) { opp.damageAndUpdate(Utils.toDmgValue(opp.getMaxHp() / 8), HitResult.OTHER); pokemon.scene.queueMessage(i18next.t("abilityTriggers:badDreams", { pokemonName: getPokemonNameWithAffix(opp) })); diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index 2f57650c65d..8bb74d29a4e 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -1144,7 +1144,7 @@ class FireGrassPledgeTag extends ArenaTag { ? arena.scene.getPlayerField() : arena.scene.getEnemyField(); - field.filter(pokemon => !pokemon.isOfType(Type.FIRE)).forEach(pokemon => { + field.filter(pokemon => !pokemon.isOfType(Type.FIRE) && !pokemon.switchOutStatus).forEach(pokemon => { // "{pokemonNameWithAffix} was hurt by the sea of fire!" pokemon.scene.queueMessage(i18next.t("arenaTag:fireGrassPledgeLapse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })); // TODO: Replace this with a proper animation diff --git a/src/data/move.ts b/src/data/move.ts index 091301990f3..166058178f5 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -1867,7 +1867,7 @@ export class FlameBurstAttr extends MoveEffectAttr { applyAbAttrs(BlockNonDirectDamageAbAttr, targetAlly, cancelled); } - if (cancelled.value || !targetAlly) { + if (cancelled.value || !targetAlly || targetAlly.switchOutStatus) { return false; } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 397bf2cbf36..2638a6cfc62 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -3007,6 +3007,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { damageAndUpdate(damage: integer, result?: DamageResult, critical: boolean = false, ignoreSegments: boolean = false, preventEndure: boolean = false, ignoreFaintPhase: boolean = false, source?: Pokemon): integer { const damagePhase = new DamageAnimPhase(this.scene, this.getBattlerIndex(), damage, result as DamageResult, critical); this.scene.unshiftPhase(damagePhase); + if (this.switchOutStatus && source) { + damage = 0; + } damage = this.damage(damage, ignoreSegments, preventEndure, ignoreFaintPhase); // Damage amount may have changed, but needed to be queued before calling damage function damagePhase.updateAmount(damage); diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index f7a9b22c396..b85be5b46aa 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -228,9 +228,11 @@ export class MoveEffectPhase extends PokemonPhase { * If the move missed a target, stop all future hits against that target * and move on to the next target (if there is one). */ - if (isCommanding || (!isImmune && !isProtected && !targetHitChecks[target.getBattlerIndex()])) { + if (target.switchOutStatus || isCommanding || (!isImmune && !isProtected && !targetHitChecks[target.getBattlerIndex()])) { this.stopMultiHit(target); - this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: getPokemonNameWithAffix(target) })); + if (!target.switchOutStatus) { + this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: getPokemonNameWithAffix(target) })); + } if (moveHistoryEntry.result === MoveResult.PENDING) { moveHistoryEntry.result = MoveResult.MISS; } diff --git a/src/phases/post-turn-status-effect-phase.ts b/src/phases/post-turn-status-effect-phase.ts index 08e4d7cb952..378a932cdc5 100644 --- a/src/phases/post-turn-status-effect-phase.ts +++ b/src/phases/post-turn-status-effect-phase.ts @@ -16,7 +16,7 @@ export class PostTurnStatusEffectPhase extends PokemonPhase { start() { const pokemon = this.getPokemon(); - if (pokemon?.isActive(true) && pokemon.status && pokemon.status.isPostTurn()) { + if (pokemon?.isActive(true) && pokemon.status && pokemon.status.isPostTurn() && !pokemon.switchOutStatus) { pokemon.status.incrementTurn(); const cancelled = new Utils.BooleanHolder(false); applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled); diff --git a/src/phases/turn-end-phase.ts b/src/phases/turn-end-phase.ts index 60a2e6600db..e5f1850758d 100644 --- a/src/phases/turn-end-phase.ts +++ b/src/phases/turn-end-phase.ts @@ -23,22 +23,24 @@ export class TurnEndPhase extends FieldPhase { this.scene.eventTarget.dispatchEvent(new TurnEndEvent(this.scene.currentBattle.turn)); const handlePokemon = (pokemon: Pokemon) => { - pokemon.lapseTags(BattlerTagLapseType.TURN_END); + if (!pokemon.switchOutStatus) { + pokemon.lapseTags(BattlerTagLapseType.TURN_END); - this.scene.applyModifiers(TurnHealModifier, pokemon.isPlayer(), pokemon); + this.scene.applyModifiers(TurnHealModifier, pokemon.isPlayer(), pokemon); - if (this.scene.arena.terrain?.terrainType === TerrainType.GRASSY && pokemon.isGrounded()) { - this.scene.unshiftPhase(new PokemonHealPhase(this.scene, pokemon.getBattlerIndex(), - Math.max(pokemon.getMaxHp() >> 4, 1), i18next.t("battle:turnEndHpRestore", { pokemonName: getPokemonNameWithAffix(pokemon) }), true)); + if (this.scene.arena.terrain?.terrainType === TerrainType.GRASSY && pokemon.isGrounded()) { + this.scene.unshiftPhase(new PokemonHealPhase(this.scene, pokemon.getBattlerIndex(), + Math.max(pokemon.getMaxHp() >> 4, 1), i18next.t("battle:turnEndHpRestore", { pokemonName: getPokemonNameWithAffix(pokemon) }), true)); + } + + if (!pokemon.isPlayer()) { + this.scene.applyModifiers(EnemyTurnHealModifier, false, pokemon); + this.scene.applyModifier(EnemyStatusEffectHealChanceModifier, false, pokemon); + } + + applyPostTurnAbAttrs(PostTurnAbAttr, pokemon); } - if (!pokemon.isPlayer()) { - this.scene.applyModifiers(EnemyTurnHealModifier, false, pokemon); - this.scene.applyModifier(EnemyStatusEffectHealChanceModifier, false, pokemon); - } - - applyPostTurnAbAttrs(PostTurnAbAttr, pokemon); - this.scene.applyModifiers(TurnStatusEffectModifier, pokemon.isPlayer(), pokemon); this.scene.applyModifiers(TurnHeldItemTransferModifier, pokemon.isPlayer(), pokemon); diff --git a/src/phases/weather-effect-phase.ts b/src/phases/weather-effect-phase.ts index b48ee342780..442bafa0ca7 100644 --- a/src/phases/weather-effect-phase.ts +++ b/src/phases/weather-effect-phase.ts @@ -51,7 +51,7 @@ export class WeatherEffectPhase extends CommonAnimPhase { }; this.executeForAll((pokemon: Pokemon) => { - const immune = !pokemon || !!pokemon.getTypes(true, true).filter(t => this.weather?.isTypeDamageImmune(t)).length; + const immune = !pokemon || !!pokemon.getTypes(true, true).filter(t => this.weather?.isTypeDamageImmune(t)).length || pokemon.switchOutStatus; if (!immune) { inflictDamage(pokemon); } @@ -59,8 +59,12 @@ export class WeatherEffectPhase extends CommonAnimPhase { } } - this.scene.ui.showText(getWeatherLapseMessage(this.weather.weatherType)!, null, () => { // TODO: is this bang correct? - this.executeForAll((pokemon: Pokemon) => applyPostWeatherLapseAbAttrs(PostWeatherLapseAbAttr, pokemon, this.weather)); + this.scene.ui.showText(getWeatherLapseMessage(this.weather.weatherType) ?? "", null, () => { + this.executeForAll((pokemon: Pokemon) => { + if (!pokemon.switchOutStatus) { + applyPostWeatherLapseAbAttrs(PostWeatherLapseAbAttr, pokemon, this.weather); + } + }); super.start(); }); diff --git a/src/test/abilities/wimp_out.test.ts b/src/test/abilities/wimp_out.test.ts index 4283386b248..bff68b54c75 100644 --- a/src/test/abilities/wimp_out.test.ts +++ b/src/test/abilities/wimp_out.test.ts @@ -632,4 +632,34 @@ describe("Abilities - Wimp Out", () => { const hasFled = enemyPokemon.switchOutStatus; expect(isVisible && !hasFled).toBe(true); }); + it("wimp out will not skip battles when triggered in a double battle", async () => { + const wave = 2; + game.override + .enemyMoveset(Moves.SPLASH) + .enemySpecies(Species.WIMPOD) + .enemyAbility(Abilities.WIMP_OUT) + .moveset([ Moves.MATCHA_GOTCHA, Moves.FALSE_SWIPE ]) + .startingLevel(50) + .enemyLevel(1) + .battleType("double") + .startingWave(wave); + await game.classicMode.startBattle([ + Species.RAICHU, + Species.PIKACHU + ]); + const [ wimpod0, wimpod1 ] = game.scene.getEnemyField(); + + game.move.select(Moves.FALSE_SWIPE, 0, BattlerIndex.ENEMY); + game.move.select(Moves.MATCHA_GOTCHA, 1); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2 ]); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(wimpod0.hp).toBeGreaterThan(0); + expect(wimpod0.switchOutStatus).toBe(true); + expect(wimpod0.isFainted()).toBe(false); + expect(wimpod1.isFainted()).toBe(true); + + await game.toNextWave(); + expect(game.scene.currentBattle.waveIndex).toBe(wave + 1); + }); }); From 5fc41dfd16c00d061645e06dd625bcf0ed87d88b Mon Sep 17 00:00:00 2001 From: muscode Date: Sat, 30 Nov 2024 15:05:55 -0600 Subject: [PATCH 03/13] [Bug] Fix Wimp Out/EE Trainer Battle causing enemy stack with multi hit (#4935) * added fix for wimp out stack * Replace `integer` with `number` * Apply suggestions from code review Co-authored-by: innerthunder <168692175+innerthunder@users.noreply.github.com> * Update imports * Update `if` check * Modify `if` check conditions --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> Co-authored-by: innerthunder <168692175+innerthunder@users.noreply.github.com> --- src/field/pokemon.ts | 20 +++++++++----------- src/phases/move-effect-phase.ts | 9 +++++++++ 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 2638a6cfc62..d46e729274c 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -23,7 +23,7 @@ import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "#app/data/balance/ import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, SubstituteTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, MoveRestrictionBattlerTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag, TarShotTag, AutotomizedTag, PowerTrickTag } from "../data/battler-tags"; import { WeatherType } from "#enums/weather-type"; import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "#app/data/arena-tag"; -import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr, PostSetStatusAbAttr, applyPostSetStatusAbAttrs, InfiltratorAbAttr, AlliedFieldDamageReductionAbAttr, PostDamageAbAttr, applyPostDamageAbAttrs, PostDamageForceSwitchAbAttr, CommanderAbAttr, applyPostItemLostAbAttrs, PostItemLostAbAttr } from "#app/data/ability"; +import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr, PostSetStatusAbAttr, applyPostSetStatusAbAttrs, InfiltratorAbAttr, AlliedFieldDamageReductionAbAttr, PostDamageAbAttr, applyPostDamageAbAttrs, CommanderAbAttr, applyPostItemLostAbAttrs, PostItemLostAbAttr } from "#app/data/ability"; import PokemonData from "#app/system/pokemon-data"; import { BattlerIndex } from "#app/battle"; import { Mode } from "#app/ui/ui"; @@ -2896,14 +2896,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.turnData.damageTaken += damage; this.battleData.hitCount++; - // Multi-Lens and Parental Bond check for Wimp Out/Emergency Exit - if (this.hasAbilityWithAttr(PostDamageForceSwitchAbAttr)) { - const multiHitModifier = source.getHeldItems().find(m => m instanceof PokemonMultiHitModifier); - if (multiHitModifier || source.hasAbilityWithAttr(AddSecondStrikeAbAttr)) { - applyPostDamageAbAttrs(PostDamageAbAttr, this, damage, this.hasPassive(), false, [], source); - } - } - const attackResult = { move: move.id, result: result as DamageResult, damage: damage, critical: isCritical, sourceId: source.id, sourceBattlerIndex: source.getBattlerIndex() }; this.turnData.attacksReceived.unshift(attackResult); if (source.isPlayer() && !this.isPlayer()) { @@ -3004,7 +2996,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @param ignoreFaintPhase boolean to ignore adding a FaintPhase, passsed to damage() * @returns integer of damage done */ - damageAndUpdate(damage: integer, result?: DamageResult, critical: boolean = false, ignoreSegments: boolean = false, preventEndure: boolean = false, ignoreFaintPhase: boolean = false, source?: Pokemon): integer { + damageAndUpdate(damage: number, result?: DamageResult, critical: boolean = false, ignoreSegments: boolean = false, preventEndure: boolean = false, ignoreFaintPhase: boolean = false, source?: Pokemon): number { const damagePhase = new DamageAnimPhase(this.scene, this.getBattlerIndex(), damage, result as DamageResult, critical); this.scene.unshiftPhase(damagePhase); if (this.switchOutStatus && source) { @@ -3013,7 +3005,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { damage = this.damage(damage, ignoreSegments, preventEndure, ignoreFaintPhase); // Damage amount may have changed, but needed to be queued before calling damage function damagePhase.updateAmount(damage); - applyPostDamageAbAttrs(PostDamageAbAttr, this, damage, this.hasPassive(), false, [], source); + /** + * Run PostDamageAbAttr from any source of damage that is not from a multi-hit + * Multi-hits are handled in move-effect-phase.ts for PostDamageAbAttr + */ + if (!source || source.turnData.hitCount <= 1) { + applyPostDamageAbAttrs(PostDamageAbAttr, this, damage, this.hasPassive(), false, [], source); + } return damage; } diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index b85be5b46aa..ea45dc2b9e2 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -4,11 +4,13 @@ import { AddSecondStrikeAbAttr, AlwaysHitAbAttr, applyPostAttackAbAttrs, + applyPostDamageAbAttrs, applyPostDefendAbAttrs, applyPreAttackAbAttrs, IgnoreMoveEffectsAbAttr, MaxMultiHitAbAttr, PostAttackAbAttr, + PostDamageAbAttr, PostDefendAbAttr, TypeImmunityAbAttr, } from "#app/data/ability"; @@ -301,6 +303,13 @@ export class MoveEffectPhase extends PokemonPhase { */ if (lastHit) { this.scene.triggerPokemonFormChange(user, SpeciesFormChangePostMoveTrigger); + /** + * Multi-Lens, Multi Hit move and Parental Bond check for PostDamageAbAttr + * other damage source are calculated in damageAndUpdate in pokemon.ts + */ + if (user.turnData.hitCount > 1) { + applyPostDamageAbAttrs(PostDamageAbAttr, target, 0, target.hasPassive(), false, [], user); + } } /** From d5146a57b9a1e5dccbe09ba8a8d19549d8c67ab3 Mon Sep 17 00:00:00 2001 From: geeilhan <107366005+geeilhan@users.noreply.github.com> Date: Sat, 30 Nov 2024 22:08:32 +0100 Subject: [PATCH 04/13] [Bug] Fix Lingering Arena Trap if Pokemon Switches Out (#4755) * [P2 BUG] Fixed Lingering Arena Trap if Pokemon Switches Out (#3713) * added switchOutStatus for all relevant moves * Added Lingering Arena Trap Fix for Mystery Encounters * Removing Redundant switchOutStatus Sets * added automated test case to arena trap test * Update src/field/pokemon.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Update src/test/abilities/arena_trap.test.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> Co-authored-by: Moka <54149968+MokaStitcher@users.noreply.github.com> --- src/field/pokemon.ts | 10 ++++++- src/test/abilities/arena_trap.test.ts | 38 ++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index d46e729274c..5706725f763 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -325,6 +325,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (!this.scene) { return false; } + if (this.switchOutStatus) { + return false; + } return this.scene.field.getIndex(this) > -1; } @@ -1583,7 +1586,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } const trappedByAbility = new Utils.BooleanHolder(false); - const opposingField = this.isPlayer() ? this.scene.getEnemyField() : this.scene.getPlayerField(); + /** + * Contains opposing Pokemon (Enemy/Player Pokemon) depending on perspective + * Afterwards, it filters out Pokemon that have been switched out of the field so trapped abilities/moves do not trigger + */ + const opposingFieldUnfiltered = this.isPlayer() ? this.scene.getEnemyField() : this.scene.getPlayerField(); + const opposingField = opposingFieldUnfiltered.filter(enemyPkm => enemyPkm.switchOutStatus === false); opposingField.forEach((opponent) => applyCheckTrappedAbAttrs(CheckTrappedAbAttr, opponent, trappedByAbility, this, trappedAbMessages, simulated) diff --git a/src/test/abilities/arena_trap.test.ts b/src/test/abilities/arena_trap.test.ts index 5068fed6b77..12b9673080d 100644 --- a/src/test/abilities/arena_trap.test.ts +++ b/src/test/abilities/arena_trap.test.ts @@ -1,9 +1,10 @@ +import { allAbilities } from "#app/data/ability"; 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 } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, it, expect, vi } from "vitest"; describe("Abilities - Arena Trap", () => { let phaserGame: Phaser.Game; @@ -55,4 +56,39 @@ describe("Abilities - Arena Trap", () => { expect(game.scene.getEnemyField().length).toBe(2); }); + + /** + * This checks if the Player Pokemon is able to switch out/run away after the Enemy Pokemon with {@linkcode Abilities.ARENA_TRAP} + * is forcefully moved out of the field from moves such as Roar {@linkcode Moves.ROAR} + * + * Note: It should be able to switch out/run away + */ + it("should lift if pokemon with this ability leaves the field", async () => { + game.override + .battleType("double") + .enemyMoveset(Moves.SPLASH) + .moveset([ Moves.ROAR, Moves.SPLASH ]) + .ability(Abilities.BALL_FETCH); + await game.classicMode.startBattle([ Species.MAGIKARP, Species.SUDOWOODO, Species.LUNATONE ]); + + const [ enemy1, enemy2 ] = game.scene.getEnemyField(); + const [ player1, player2 ] = game.scene.getPlayerField(); + + vi.spyOn(enemy1, "getAbility").mockReturnValue(allAbilities[Abilities.ARENA_TRAP]); + + game.move.select(Moves.ROAR); + game.move.select(Moves.SPLASH, 1); + + // This runs the fist command phase where the moves are selected + await game.toNextTurn(); + // During the next command phase the player pokemons should not be trapped anymore + game.move.select(Moves.SPLASH); + game.move.select(Moves.SPLASH, 1); + await game.toNextTurn(); + + expect(player1.isTrapped()).toBe(false); + expect(player2.isTrapped()).toBe(false); + expect(enemy1.isOnField()).toBe(false); + expect(enemy2.isOnField()).toBe(true); + }); }); From 80555be22c57c6bcc5f4f2a51e953b9f5de12628 Mon Sep 17 00:00:00 2001 From: innerthunder <168692175+innerthunder@users.noreply.github.com> Date: Sat, 30 Nov 2024 16:07:01 -0800 Subject: [PATCH 05/13] Fix Post-Summon abilities not applying after switch prompt (#4908) --- src/phases/check-switch-phase.ts | 2 -- src/phases/post-summon-phase.ts | 9 ++++++--- src/phases/switch-phase.ts | 4 ++++ 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/phases/check-switch-phase.ts b/src/phases/check-switch-phase.ts index acf17c75668..18b999ed210 100644 --- a/src/phases/check-switch-phase.ts +++ b/src/phases/check-switch-phase.ts @@ -5,7 +5,6 @@ import { getPokemonNameWithAffix } from "#app/messages"; import { Mode } from "#app/ui/ui"; import i18next from "i18next"; import { BattlePhase } from "./battle-phase"; -import { PostSummonPhase } from "./post-summon-phase"; import { SummonMissingPhase } from "./summon-missing-phase"; import { SwitchPhase } from "./switch-phase"; import { SwitchType } from "#enums/switch-type"; @@ -54,7 +53,6 @@ export class CheckSwitchPhase extends BattlePhase { this.scene.ui.showText(i18next.t("battle:switchQuestion", { pokemonName: this.useName ? getPokemonNameWithAffix(pokemon) : i18next.t("battle:pokemon") }), null, () => { this.scene.ui.setMode(Mode.CONFIRM, () => { this.scene.ui.setMode(Mode.MESSAGE); - this.scene.tryRemovePhase(p => p instanceof PostSummonPhase && p.player && p.fieldIndex === this.fieldIndex); this.scene.unshiftPhase(new SwitchPhase(this.scene, SwitchType.INITIAL_SWITCH, this.fieldIndex, false, true)); this.end(); }, () => { diff --git a/src/phases/post-summon-phase.ts b/src/phases/post-summon-phase.ts index 644a6235a42..42e5b930eb1 100644 --- a/src/phases/post-summon-phase.ts +++ b/src/phases/post-summon-phase.ts @@ -27,9 +27,12 @@ export class PostSummonPhase extends PokemonPhase { pokemon.lapseTag(BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON); } - applyPostSummonAbAttrs(PostSummonAbAttr, pokemon).then(() => this.end()); + applyPostSummonAbAttrs(PostSummonAbAttr, pokemon) + .then(() => { + const field = pokemon.isPlayer() ? this.scene.getPlayerField() : this.scene.getEnemyField(); + field.forEach((p) => applyAbAttrs(CommanderAbAttr, p, null, false)); - const field = pokemon.isPlayer() ? this.scene.getPlayerField() : this.scene.getEnemyField(); - field.forEach((p) => applyAbAttrs(CommanderAbAttr, p, null, false)); + this.end(); + }); } } diff --git a/src/phases/switch-phase.ts b/src/phases/switch-phase.ts index 2abb109a529..481d64c451e 100644 --- a/src/phases/switch-phase.ts +++ b/src/phases/switch-phase.ts @@ -3,6 +3,7 @@ import PartyUiHandler, { PartyOption, PartyUiMode } from "#app/ui/party-ui-handl import { Mode } from "#app/ui/ui"; import { SwitchType } from "#enums/switch-type"; import { BattlePhase } from "./battle-phase"; +import { PostSummonPhase } from "./post-summon-phase"; import { SwitchSummonPhase } from "./switch-summon-phase"; /** @@ -63,6 +64,9 @@ export class SwitchPhase extends BattlePhase { this.scene.ui.setMode(Mode.PARTY, this.isModal ? PartyUiMode.FAINT_SWITCH : PartyUiMode.POST_BATTLE_SWITCH, fieldIndex, (slotIndex: integer, option: PartyOption) => { if (slotIndex >= this.scene.currentBattle.getBattlerCount() && slotIndex < 6) { + // Remove any pre-existing PostSummonPhase under the same field index. + // Pre-existing PostSummonPhases may occur when this phase is invoked during a prompt to switch at the start of a wave. + this.scene.tryRemovePhase(p => p instanceof PostSummonPhase && p.player && p.fieldIndex === this.fieldIndex); const switchType = (option === PartyOption.PASS_BATON) ? SwitchType.BATON_PASS : this.switchType; this.scene.unshiftPhase(new SwitchSummonPhase(this.scene, switchType, fieldIndex, slotIndex, this.doReturn)); } From 38d7a26053f9f6ac11862adbe94496adfceb6088 Mon Sep 17 00:00:00 2001 From: Moka <54149968+MokaStitcher@users.noreply.github.com> Date: Sun, 1 Dec 2024 01:08:53 +0100 Subject: [PATCH 06/13] [Sprite][Bug][ME] Fix ME Intro visuals for shinies and other shiny related fixes (#4827) * [ME] Fix GTS Wonder Trade shiny not giving luck * [ME] Shiny Magikarp from Pokemon Salesman can have any variant * [ME] Shiny lock MEs with custom or special sprites * [ME] GTS shows shiny sparkle for received Pokemon * [ME] Shiny lock 'Slumbering Snorlax' and 'The Strong Stuff' * [ME] Dancing Lessson: show shiny sparkle for Oricorio in intro * [ME] Show shiny sparkles for Pokemon in ME intro * fix tests * Ensure shiny sparkle animation is initialized before playing it (Fixes #3924) * make loading variant assets cleaner * cleanup EnemyPokemon shiny initialization * test fixes and final cleanup * Make 'getSpeciesFilterRandomPartyMemberFunc' more readable --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/battle-scene.ts | 33 ++++++++- .../encounters/absolute-avarice-encounter.ts | 5 +- .../encounters/berries-abound-encounter.ts | 4 +- .../encounters/dancing-lessons-encounter.ts | 12 ++-- .../encounters/fight-or-flight-encounter.ts | 4 +- .../encounters/fun-and-games-encounter.ts | 4 +- .../global-trade-system-encounter.ts | 21 ++++-- .../slumbering-snorlax-encounter.ts | 1 + .../the-pokemon-salesman-encounter.ts | 12 ++-- .../encounters/the-strong-stuff-encounter.ts | 1 + .../encounters/trash-to-treasure-encounter.ts | 3 +- .../encounters/uncommon-breed-encounter.ts | 8 ++- .../utils/encounter-phase-utils.ts | 4 +- src/data/pokemon-species.ts | 27 +------ src/data/trainer-config.ts | 26 +++++-- src/field/anims.ts | 37 ++++++++-- src/field/mystery-encounter-intro.ts | 71 ++++++++++++++++--- src/field/pokemon.ts | 41 +++++------ src/phases/egg-hatch-phase.ts | 4 +- src/phases/encounter-phase.ts | 3 + src/system/pokemon-data.ts | 2 +- .../global-trade-system-encounter.test.ts | 18 +++++ .../the-pokemon-salesman-encounter.test.ts | 16 ++++- .../the-strong-stuff-encounter.test.ts | 1 + .../trash-to-treasure-encounter.test.ts | 1 + 25 files changed, 260 insertions(+), 99 deletions(-) diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 62e9d8ea717..2c38bb6632f 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -47,7 +47,7 @@ import PokemonInfoContainer from "#app/ui/pokemon-info-container"; import { biomeDepths, getBiomeName } from "#app/data/balance/biomes"; import { SceneBase } from "#app/scene-base"; import CandyBar from "#app/ui/candy-bar"; -import { Variant, variantData } from "#app/data/variant"; +import { Variant, variantColorCache, variantData, VariantSet } from "#app/data/variant"; import { Localizable } from "#app/interfaces/locales"; import Overrides from "#app/overrides"; import { InputsController } from "#app/inputs-controller"; @@ -345,6 +345,33 @@ export default class BattleScene extends SceneBase { this.load.atlas(key, `images/pokemon/${variant ? "variant/" : ""}${experimental ? "exp/" : ""}${atlasPath}.png`, `images/pokemon/${variant ? "variant/" : ""}${experimental ? "exp/" : ""}${atlasPath}.json`); } + /** + * Load the variant assets for the given sprite and stores them in {@linkcode variantColorCache} + */ + loadPokemonVariantAssets(spriteKey: string, fileRoot: string, variant?: Variant) { + const useExpSprite = this.experimentalSprites && this.hasExpSprite(spriteKey); + if (useExpSprite) { + fileRoot = `exp/${fileRoot}`; + } + let variantConfig = variantData; + fileRoot.split("/").map(p => variantConfig ? variantConfig = variantConfig[p] : null); + const variantSet = variantConfig as VariantSet; + if (variantSet && (variant !== undefined && variantSet[variant] === 1)) { + const populateVariantColors = (key: string): Promise => { + return new Promise(resolve => { + if (variantColorCache.hasOwnProperty(key)) { + return resolve(); + } + this.cachedFetch(`./images/pokemon/variant/${fileRoot}.json`).then(res => res.json()).then(c => { + variantColorCache[key] = c; + resolve(); + }); + }); + }; + populateVariantColors(spriteKey); + } + } + async preload() { if (DEBUG_RNG) { const scene = this; @@ -891,7 +918,7 @@ export default class BattleScene extends SceneBase { return pokemon; } - addEnemyPokemon(species: PokemonSpecies, level: integer, trainerSlot: TrainerSlot, boss: boolean = false, dataSource?: PokemonData, postProcess?: (enemyPokemon: EnemyPokemon) => void): EnemyPokemon { + addEnemyPokemon(species: PokemonSpecies, level: integer, trainerSlot: TrainerSlot, boss: boolean = false, shinyLock: boolean = false, dataSource?: PokemonData, postProcess?: (enemyPokemon: EnemyPokemon) => void): EnemyPokemon { if (Overrides.OPP_LEVEL_OVERRIDE > 0) { level = Overrides.OPP_LEVEL_OVERRIDE; } @@ -901,7 +928,7 @@ export default class BattleScene extends SceneBase { boss = this.getEncounterBossSegments(this.currentBattle.waveIndex, level, species) > 1; } - const pokemon = new EnemyPokemon(this, species, level, trainerSlot, boss, dataSource); + const pokemon = new EnemyPokemon(this, species, level, trainerSlot, boss, shinyLock, dataSource); if (Overrides.OPP_FUSION_OVERRIDE) { pokemon.generateFusionSpecies(); } diff --git a/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts b/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts index 9c00148fbac..6b0f239d28d 100644 --- a/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts +++ b/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts @@ -216,6 +216,7 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = species: getPokemonSpecies(Species.GREEDENT), isBoss: true, bossSegments: 3, + shiny: false, // Shiny lock because of consistency issues between the different options moveSet: [ Moves.THRASH, Moves.BODY_PRESS, Moves.STUFF_CHEEKS, Moves.CRUNCH ], modifierConfigs: bossModifierConfigs, tags: [ BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON ], @@ -353,9 +354,9 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = }) .withOptionPhase(async (scene: BattleScene) => { // Let it have the food - // Greedent joins the team, level equal to 2 below highest party member + // Greedent joins the team, level equal to 2 below highest party member (shiny locked) const level = getHighestLevelPlayerPokemon(scene, false, true).level - 2; - const greedent = new EnemyPokemon(scene, getPokemonSpecies(Species.GREEDENT), level, TrainerSlot.NONE, false); + const greedent = new EnemyPokemon(scene, getPokemonSpecies(Species.GREEDENT), level, TrainerSlot.NONE, false, true); greedent.moveset = [ new PokemonMove(Moves.THRASH), new PokemonMove(Moves.BODY_PRESS), new PokemonMove(Moves.STUFF_CHEEKS), new PokemonMove(Moves.SLACK_OFF) ]; greedent.passive = true; diff --git a/src/data/mystery-encounters/encounters/berries-abound-encounter.ts b/src/data/mystery-encounters/encounters/berries-abound-encounter.ts index 5524511c67b..786ca3e8fc0 100644 --- a/src/data/mystery-encounters/encounters/berries-abound-encounter.ts +++ b/src/data/mystery-encounters/encounters/berries-abound-encounter.ts @@ -98,7 +98,9 @@ export const BerriesAboundEncounter: MysteryEncounter = tint: 0.25, x: -5, repeat: true, - isPokemon: true + isPokemon: true, + isShiny: bossPokemon.shiny, + variant: bossPokemon.variant } ]; diff --git a/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts b/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts index bae5a8790e9..841aadd7c36 100644 --- a/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts +++ b/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts @@ -92,9 +92,13 @@ export const DancingLessonsEncounter: MysteryEncounter = .withCatchAllowed(true) .withFleeAllowed(false) .withOnVisualsStart((scene: BattleScene) => { - const danceAnim = new EncounterBattleAnim(EncounterAnim.DANCE, scene.getEnemyPokemon()!, scene.getPlayerPokemon()!); - danceAnim.play(scene); - + const oricorio = scene.getEnemyPokemon()!; + const danceAnim = new EncounterBattleAnim(EncounterAnim.DANCE, oricorio, scene.getPlayerPokemon()!); + danceAnim.play(scene, false, () => { + if (oricorio.shiny) { + oricorio.sparkle(); + } + }); return true; }) .withIntroDialogue([ @@ -136,7 +140,7 @@ export const DancingLessonsEncounter: MysteryEncounter = } const oricorioData = new PokemonData(enemyPokemon); - const oricorio = scene.addEnemyPokemon(species, level, TrainerSlot.NONE, false, oricorioData); + const oricorio = scene.addEnemyPokemon(species, level, TrainerSlot.NONE, false, false, oricorioData); // Adds a real Pokemon sprite to the field (required for the animation) scene.getEnemyParty().forEach(enemyPokemon => { diff --git a/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts b/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts index 3f9030dc3b2..3533e10df29 100644 --- a/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts +++ b/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts @@ -114,7 +114,9 @@ export const FightOrFlightEncounter: MysteryEncounter = tint: 0.25, x: -5, repeat: true, - isPokemon: true + isPokemon: true, + isShiny: bossPokemon.shiny, + variant: bossPokemon.variant }, ]; 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 c286fffe0de..84c3e56a836 100644 --- a/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts +++ b/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts @@ -194,10 +194,10 @@ async function summonPlayerPokemon(scene: BattleScene) { playerAnimationPromise = summonPlayerPokemonAnimation(scene, playerPokemon); }); - // Also loads Wobbuffet data + // Also loads Wobbuffet data (cannot be shiny) const enemySpecies = getPokemonSpecies(Species.WOBBUFFET); scene.currentBattle.enemyParty = []; - const wobbuffet = scene.addEnemyPokemon(enemySpecies, encounter.misc.playerPokemon.level, TrainerSlot.NONE, false); + const wobbuffet = scene.addEnemyPokemon(enemySpecies, encounter.misc.playerPokemon.level, TrainerSlot.NONE, false, true); wobbuffet.ivs = [ 0, 0, 0, 0, 0, 0 ]; wobbuffet.setNature(Nature.MILD); wobbuffet.setAlpha(0); 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 2d569621449..934fc1b805b 100644 --- a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts +++ b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts @@ -12,8 +12,7 @@ import PokemonSpecies, { allSpecies, getPokemonSpecies } from "#app/data/pokemon import { getTypeRgb } from "#app/data/type"; import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; -import * as Utils from "#app/utils"; -import { IntegerHolder, isNullOrUndefined, randInt, randSeedInt, randSeedShuffle } from "#app/utils"; +import { NumberHolder, isNullOrUndefined, randInt, randSeedInt, randSeedShuffle } from "#app/utils"; import Pokemon, { EnemyPokemon, PlayerPokemon, PokemonMove } from "#app/field/pokemon"; import { HiddenAbilityRateBoosterModifier, PokemonFormChangeItemModifier, PokemonHeldItemModifier, ShinyRateBoosterModifier, SpeciesStatBoosterModifier } from "#app/modifier/modifier"; import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; @@ -27,6 +26,7 @@ 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"; import type { PokeballType } from "#enums/pokeball"; +import { doShinySparkleAnim } from "#app/field/anims"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounters/globalTradeSystem"; @@ -230,7 +230,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = 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) { - const shinyThreshold = new Utils.IntegerHolder(WONDER_TRADE_SHINY_CHANCE); + const shinyThreshold = new NumberHolder(WONDER_TRADE_SHINY_CHANCE); if (scene.eventManager.isEventActive()) { shinyThreshold.value *= scene.eventManager.getShinyMultiplier(); } @@ -247,7 +247,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = const hiddenIndex = tradePokemon.species.ability2 ? 2 : 1; if (tradePokemon.species.abilityHidden) { if (tradePokemon.abilityIndex < hiddenIndex) { - const hiddenAbilityChance = new IntegerHolder(64); + const hiddenAbilityChance = new NumberHolder(64); scene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance); const hasHiddenAbility = !randSeedInt(hiddenAbilityChance.value); @@ -797,6 +797,14 @@ function doTradeReceivedSequence(scene: BattleScene, receivedPokemon: PlayerPoke receivedPokeballSprite.x = tradeBaseBg.displayWidth / 2; receivedPokeballSprite.y = tradeBaseBg.displayHeight / 2 - 100; + // Received pokemon sparkles + let pokemonShinySparkle: Phaser.GameObjects.Sprite; + if (receivedPokemon.shiny) { + pokemonShinySparkle = scene.add.sprite(receivedPokemonSprite.x, receivedPokemonSprite.y, "shiny"); + pokemonShinySparkle.setVisible(false); + tradeContainer.add(pokemonShinySparkle); + } + const BASE_ANIM_DURATION = 1000; // Pokeball falls to the screen @@ -835,6 +843,11 @@ function doTradeReceivedSequence(scene: BattleScene, receivedPokemon: PlayerPoke scale: 1, alpha: 0, onComplete: () => { + if (receivedPokemon.shiny) { + scene.time.delayedCall(500, () => { + doShinySparkleAnim(scene, pokemonShinySparkle, receivedPokemon.variant); + }); + } receivedPokeballSprite.destroy(); scene.time.delayedCall(2000, () => resolve()); } diff --git a/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts b/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts index 3fb502be545..8dd03e12caa 100644 --- a/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts +++ b/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts @@ -60,6 +60,7 @@ export const SlumberingSnorlaxEncounter: MysteryEncounter = const pokemonConfig: EnemyPokemonConfig = { species: bossSpecies, isBoss: true, + shiny: false, // Shiny lock because shiny is rolled only if the battle option is picked 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 ], modifierConfigs: [ 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 61a4de908e9..feb6e68d1d1 100644 --- a/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts @@ -72,13 +72,11 @@ export const ThePokemonSalesmanEncounter: MysteryEncounter = let pokemon: PlayerPokemon; 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 + // If no HA mon found or you roll 1%, give shiny Magikarp with random variant species = getPokemonSpecies(Species.MAGIKARP); - const hiddenIndex = species.ability2 ? 2 : 1; - pokemon = new PlayerPokemon(scene, species, 5, hiddenIndex, species.formIndex, undefined, true, 0); + pokemon = new PlayerPokemon(scene, species, 5, 2, species.formIndex, undefined, true); } else { - const hiddenIndex = species.ability2 ? 2 : 1; - pokemon = new PlayerPokemon(scene, species, 5, hiddenIndex, species.formIndex); + pokemon = new PlayerPokemon(scene, species, 5, 2, species.formIndex); } pokemon.generateAndPopulateMoveset(); @@ -88,7 +86,9 @@ export const ThePokemonSalesmanEncounter: MysteryEncounter = fileRoot: fileRoot, hasShadow: true, repeat: true, - isPokemon: true + isPokemon: true, + isShiny: pokemon.shiny, + variant: pokemon.variant }); const starterTier = speciesStarterCosts[species.speciesId]; 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 754632aedea..c5cfd3f954e 100644 --- a/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts @@ -79,6 +79,7 @@ export const TheStrongStuffEncounter: MysteryEncounter = species: getPokemonSpecies(Species.SHUCKLE), isBoss: true, bossSegments: 5, + shiny: false, // Shiny lock because shiny is rolled only if the battle option is picked customPokemonData: new CustomPokemonData({ spriteScale: 1.25 }), nature: Nature.BOLD, moveSet: [ Moves.INFESTATION, Moves.SALT_CURE, Moves.GASTRO_ACID, Moves.HEAL_ORDER ], 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 fba3a6ca95e..dfd89cdfb63 100644 --- a/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts +++ b/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts @@ -61,11 +61,12 @@ export const TrashToTreasureEncounter: MysteryEncounter = .withOnInit((scene: BattleScene) => { const encounter = scene.currentBattle.mysteryEncounter!; - // Calculate boss mon + // Calculate boss mon (shiny locked) const bossSpecies = getPokemonSpecies(Species.GARBODOR); const pokemonConfig: EnemyPokemonConfig = { species: bossSpecies, isBoss: true, + shiny: false, // Shiny lock because of custom intro sprite formIndex: 1, // Gmax bossSegmentModifier: 1, // +1 Segment from normal moveSet: [ Moves.PAYBACK, Moves.GUNK_SHOT, Moves.STOMPING_TANTRUM, Moves.DRAIN_PUNCH ] diff --git a/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts b/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts index a2c32c6af40..d3679825ac8 100644 --- a/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts +++ b/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts @@ -100,7 +100,9 @@ export const UncommonBreedEncounter: MysteryEncounter = hasShadow: true, x: -5, repeat: true, - isPokemon: true + isPokemon: true, + isShiny: pokemon.shiny, + variant: pokemon.variant }, ]; @@ -113,13 +115,15 @@ export const UncommonBreedEncounter: MysteryEncounter = const encounter = scene.currentBattle.mysteryEncounter!; const pokemonSprite = encounter.introVisuals!.getSprites(); - scene.tweens.add({ // Bounce at the end + // Bounce at the end, then shiny sparkle if the Pokemon is shiny + scene.tweens.add({ targets: pokemonSprite, duration: 300, ease: "Cubic.easeOut", yoyo: true, y: "-=20", loop: 1, + onComplete: () => encounter.introVisuals?.playShinySparkles() }); scene.time.delayedCall(500, () => scene.playSound("battle_anims/PRSFX- Spotlight2")); diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts index b5dd43a9221..d43bce0ace5 100644 --- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -184,7 +184,7 @@ export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig: dataSource = config.dataSource; enemySpecies = config.species; isBoss = config.isBoss; - battle.enemyParty[e] = scene.addEnemyPokemon(enemySpecies, level, TrainerSlot.TRAINER, isBoss, dataSource); + battle.enemyParty[e] = scene.addEnemyPokemon(enemySpecies, level, TrainerSlot.TRAINER, isBoss, false, dataSource); } else { battle.enemyParty[e] = battle.trainer.genPartyMember(e); } @@ -202,7 +202,7 @@ export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig: enemySpecies = scene.randomSpecies(battle.waveIndex, level, true); } - battle.enemyParty[e] = scene.addEnemyPokemon(enemySpecies, level, TrainerSlot.NONE, isBoss, dataSource); + battle.enemyParty[e] = scene.addEnemyPokemon(enemySpecies, level, TrainerSlot.NONE, isBoss, false, dataSource); } } diff --git a/src/data/pokemon-species.ts b/src/data/pokemon-species.ts index ec104d4d4aa..09788e353cf 100644 --- a/src/data/pokemon-species.ts +++ b/src/data/pokemon-species.ts @@ -15,7 +15,7 @@ import { EvolutionLevel, SpeciesWildEvolutionDelay, pokemonEvolutions, pokemonPr import { Type } from "#enums/type"; import { LevelMoves, pokemonFormLevelMoves, pokemonFormLevelMoves as pokemonSpeciesFormLevelMoves, pokemonSpeciesLevelMoves } from "#app/data/balance/pokemon-level-moves"; import { Stat } from "#enums/stat"; -import { Variant, VariantSet, variantColorCache, variantData } from "#app/data/variant"; +import { Variant, VariantSet, variantData } from "#app/data/variant"; import { speciesStarterCosts, POKERUS_STARTER_COUNT } from "#app/data/balance/starters"; import { SpeciesFormKey } from "#enums/species-form-key"; @@ -511,29 +511,8 @@ export abstract class PokemonSpeciesForm { } else { scene.anims.get(spriteKey).frameRate = 10; } - let spritePath = this.getSpriteAtlasPath(female, formIndex, shiny, variant).replace("variant/", "").replace(/_[1-3]$/, ""); - const useExpSprite = scene.experimentalSprites && scene.hasExpSprite(spriteKey); - if (useExpSprite) { - spritePath = `exp/${spritePath}`; - } - let config = variantData; - spritePath.split("/").map(p => config ? config = config[p] : null); - const variantSet = config as VariantSet; - if (variantSet && (variant !== undefined && variantSet[variant] === 1)) { - const populateVariantColors = (key: string): Promise => { - return new Promise(resolve => { - if (variantColorCache.hasOwnProperty(key)) { - return resolve(); - } - scene.cachedFetch(`./images/pokemon/variant/${spritePath}.json`).then(res => res.json()).then(c => { - variantColorCache[key] = c; - resolve(); - }); - }); - }; - populateVariantColors(spriteKey).then(() => resolve()); - return; - } + const spritePath = this.getSpriteAtlasPath(female, formIndex, shiny, variant).replace("variant/", "").replace(/_[1-3]$/, ""); + scene.loadPokemonVariantAssets(spriteKey, spritePath, variant); resolve(); }); if (startLoad) { diff --git a/src/data/trainer-config.ts b/src/data/trainer-config.ts index 5e5f38bd00d..d99ca601bdf 100644 --- a/src/data/trainer-config.ts +++ b/src/data/trainer-config.ts @@ -1173,16 +1173,28 @@ export function getRandomPartyMemberFunc(speciesPool: Species[], trainerSlot: Tr if (!ignoreEvolution) { species = getPokemonSpecies(species).getTrainerSpeciesForLevel(level, true, strength, scene.currentBattle.waveIndex); } - return scene.addEnemyPokemon(getPokemonSpecies(species), level, trainerSlot, undefined, undefined, postProcess); + return scene.addEnemyPokemon(getPokemonSpecies(species), level, trainerSlot, undefined, false, undefined, postProcess); }; } -function getSpeciesFilterRandomPartyMemberFunc(speciesFilter: PokemonSpeciesFilter, trainerSlot: TrainerSlot = TrainerSlot.TRAINER, allowLegendaries?: boolean, postProcess?: (EnemyPokemon: EnemyPokemon) => void): PartyMemberFunc { - const originalSpeciesFilter = speciesFilter; - speciesFilter = (species: PokemonSpecies) => (allowLegendaries || (!species.legendary && !species.subLegendary && !species.mythical)) && !species.isTrainerForbidden() && originalSpeciesFilter(species); - return (scene: BattleScene, level: integer, strength: PartyMemberStrength) => { - const ret = scene.addEnemyPokemon(getPokemonSpecies(scene.randomSpecies(scene.currentBattle.waveIndex, level, false, speciesFilter).getTrainerSpeciesForLevel(level, true, strength, scene.currentBattle.waveIndex)), level, trainerSlot, undefined, undefined, postProcess); - return ret; +function getSpeciesFilterRandomPartyMemberFunc( + originalSpeciesFilter: PokemonSpeciesFilter, + trainerSlot: TrainerSlot = TrainerSlot.TRAINER, + allowLegendaries?: boolean, + postProcess?: (EnemyPokemon: EnemyPokemon) => void +): PartyMemberFunc { + + const speciesFilter = (species: PokemonSpecies): boolean => { + const notLegendary = !species.legendary && !species.subLegendary && !species.mythical; + return (allowLegendaries || notLegendary) && !species.isTrainerForbidden() && originalSpeciesFilter(species); + }; + + return (scene: BattleScene, level: number, strength: PartyMemberStrength) => { + const waveIndex = scene.currentBattle.waveIndex; + const species = getPokemonSpecies(scene.randomSpecies(waveIndex, level, false, speciesFilter) + .getTrainerSpeciesForLevel(level, true, strength, waveIndex)); + + return scene.addEnemyPokemon(species, level, trainerSlot, undefined, false, undefined, postProcess); }; } diff --git a/src/field/anims.ts b/src/field/anims.ts index dddf38e4a7e..10198c29005 100644 --- a/src/field/anims.ts +++ b/src/field/anims.ts @@ -1,6 +1,7 @@ -import BattleScene from "../battle-scene"; +import BattleScene from "#app/battle-scene"; import { PokeballType } from "#enums/pokeball"; -import * as Utils from "../utils"; +import { Variant } from "#app/data/variant"; +import { getFrameMs, randGauss } from "#app/utils"; export function addPokeballOpenParticles(scene: BattleScene, x: number, y: number, pokeballType: PokeballType): void { switch (pokeballType) { @@ -127,7 +128,7 @@ function doFanOutParticle(scene: BattleScene, trigIndex: integer, x: integer, y: const particleTimer = scene.tweens.addCounter({ repeat: -1, - duration: Utils.getFrameMs(1), + duration: getFrameMs(1), onRepeat: () => { updateParticle(); } @@ -159,7 +160,7 @@ export function addPokeballCaptureStars(scene: BattleScene, pokeball: Phaser.Gam } }); - const dist = Utils.randGauss(25); + const dist = randGauss(25); scene.tweens.add({ targets: particle, x: pokeball.x + dist, @@ -185,3 +186,31 @@ export function sin(index: integer, amplitude: integer): number { export function cos(index: integer, amplitude: integer): number { return amplitude * Math.cos(index * (Math.PI / 128)); } + +/** + * Play the shiny sparkle animation and sound effect for the given sprite + * First ensures that the animation has been properly initialized + * @param sparkleSprite the Sprite to play the animation on + * @param variant which shiny {@linkcode variant} to play the animation for + */ +export function doShinySparkleAnim(scene: BattleScene, sparkleSprite: Phaser.GameObjects.Sprite, variant: Variant) { + const keySuffix = variant ? `_${variant + 1}` : ""; + const spriteKey = `shiny${keySuffix}`; + const animationKey = `sparkle${keySuffix}`; + + // Make sure the animation exists, and create it if not + if (!scene.anims.exists(animationKey)) { + const frameNames = scene.anims.generateFrameNames(spriteKey, { suffix: ".png", end: 34 }); + scene.anims.create({ + key: `sparkle${keySuffix}`, + frames: frameNames, + frameRate: 32, + showOnStart: true, + hideOnComplete: true, + }); + } + + // Play the animation + sparkleSprite.play(animationKey); + scene.playSound("se/sparkle"); +} diff --git a/src/field/mystery-encounter-intro.ts b/src/field/mystery-encounter-intro.ts index 1577d1157d7..b1b85de9b29 100644 --- a/src/field/mystery-encounter-intro.ts +++ b/src/field/mystery-encounter-intro.ts @@ -1,10 +1,12 @@ import { GameObjects } from "phaser"; -import BattleScene from "../battle-scene"; -import MysteryEncounter from "../data/mystery-encounters/mystery-encounter"; +import BattleScene from "#app/battle-scene"; +import MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; import { Species } from "#enums/species"; import { isNullOrUndefined } from "#app/utils"; import { getSpriteKeysFromSpecies } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import PlayAnimationConfig = Phaser.Types.Animations.PlayAnimationConfig; +import { Variant } from "#app/data/variant"; +import { doShinySparkleAnim } from "#app/field/anims"; type KnownFileRoot = | "arenas" @@ -59,6 +61,10 @@ export class MysteryEncounterSpriteConfig { scale?: number; /** If you are using a Pokemon sprite, set to `true`. This will ensure variant, form, gender, shiny sprites are loaded properly */ isPokemon?: boolean; + /** If using a Pokemon shiny sprite, needs to be set to ensure the correct variant assets get loaded and displayed */ + isShiny?: boolean; + /** If using a Pokemon shiny sprite, needs to be set to ensure the correct variant assets get loaded and displayed */ + variant?: Variant; /** If you are using an item sprite, set to `true` */ isItem?: boolean; /** The sprites alpha. `0` - `1` The lower the number, the more transparent */ @@ -74,6 +80,7 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con public encounter: MysteryEncounter; public spriteConfigs: MysteryEncounterSpriteConfig[]; public enterFromRight: boolean; + private shinySparkleSprites: { sprite: Phaser.GameObjects.Sprite, variant: Variant }[]; constructor(scene: BattleScene, encounter: MysteryEncounter) { super(scene, -72, 76); @@ -86,7 +93,7 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con }; if (!isNullOrUndefined(result.species)) { - const keys = getSpriteKeysFromSpecies(result.species); + const keys = getSpriteKeysFromSpecies(result.species, undefined, undefined, result.isShiny, result.variant); result.spriteKey = keys.spriteKey; result.fileRoot = keys.fileRoot; result.isPokemon = true; @@ -120,18 +127,36 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con // Sprites with custom X or Y defined will not count for normal spacing requirements const spacingValue = Math.round((maxX - minX) / Math.max(this.spriteConfigs.filter(s => !s.x && !s.y).length, 1)); + this.shinySparkleSprites = []; + const shinySparkleSprites = scene.add.container(0, 0); this.spriteConfigs?.forEach((config) => { - const { spriteKey, isItem, hasShadow, scale, x, y, yShadow, alpha } = config; + const { spriteKey, isItem, hasShadow, scale, x, y, yShadow, alpha, isPokemon, isShiny, variant } = config; let sprite: GameObjects.Sprite; let tintSprite: GameObjects.Sprite; + let pokemonShinySparkle: Phaser.GameObjects.Sprite | undefined; - if (!isItem) { - sprite = getSprite(spriteKey, hasShadow, yShadow); - tintSprite = getSprite(spriteKey); - } else { + if (isItem) { sprite = getItemSprite(spriteKey, hasShadow, yShadow); tintSprite = getItemSprite(spriteKey); + } else { + sprite = getSprite(spriteKey, hasShadow, yShadow); + tintSprite = getSprite(spriteKey); + if (isPokemon && isShiny) { + // Set Pipeline for shiny variant + sprite.setPipelineData("spriteKey", spriteKey); + tintSprite.setPipelineData("spriteKey", spriteKey); + sprite.setPipelineData("shiny", true); + sprite.setPipelineData("variant", variant); + tintSprite.setPipelineData("shiny", true); + tintSprite.setPipelineData("variant", variant); + // Create Sprite for shiny Sparkle + pokemonShinySparkle = scene.add.sprite(sprite.x, sprite.y, "shiny"); + pokemonShinySparkle.setOrigin(0.5, 1); + pokemonShinySparkle.setVisible(false); + this.shinySparkleSprites.push({ sprite: pokemonShinySparkle, variant: variant ?? 0 }); + shinySparkleSprites.add(pokemonShinySparkle); + } } sprite.setVisible(!config.hidden); @@ -165,6 +190,11 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con } } + if (!isNullOrUndefined(pokemonShinySparkle)) { + // Offset the sparkle to match the Pokemon's position + pokemonShinySparkle.setPosition(sprite.x, sprite.y); + } + if (!isNullOrUndefined(alpha)) { sprite.setAlpha(alpha); tintSprite.setAlpha(alpha); @@ -173,6 +203,7 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con this.add(sprite); this.add(tintSprite); }); + this.add(shinySparkleSprites); } /** @@ -187,6 +218,9 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con this.spriteConfigs.forEach((config) => { if (config.isPokemon) { this.scene.loadPokemonAtlas(config.spriteKey, config.fileRoot); + if (config.isShiny) { + this.scene.loadPokemonVariantAssets(config.spriteKey, config.fileRoot, config.variant); + } } else if (config.isItem) { this.scene.loadAtlas("items", ""); } else { @@ -240,11 +274,21 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con this.getSprites().map((sprite, i) => { if (!this.spriteConfigs[i].isItem) { sprite.setTexture(this.spriteConfigs[i].spriteKey).setFrame(0); + if (sprite.texture.frameTotal > 1) { + // Show the first animation frame for a smooth transition when the animation starts. + const firstFrame = sprite.texture.frames["0001.png"]; + sprite.setFrame(firstFrame ?? 0); + } } }); this.getTintSprites().map((tintSprite, i) => { if (!this.spriteConfigs[i].isItem) { tintSprite.setTexture(this.spriteConfigs[i].spriteKey).setFrame(0); + if (tintSprite.texture.frameTotal > 1) { + // Show the first frame for a smooth transition when the animation starts. + const firstFrame = tintSprite.texture.frames["0001.png"]; + tintSprite.setFrame(firstFrame ?? 0); + } } }); @@ -288,6 +332,17 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con return true; } + /** + * Play shiny sparkle animations if there are shiny Pokemon + */ + playShinySparkles() { + for (const sparkleConfig of this.shinySparkleSprites) { + this.scene.time.delayedCall(500, () => { + doShinySparkleAnim(this.scene, sparkleConfig.sprite, sparkleConfig.variant); + }); + } + } + /** * For sprites with animation and that do not have animation disabled, will begin frame animation */ diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 5706725f763..0675b9485cf 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -69,6 +69,7 @@ import { SpeciesFormKey } from "#enums/species-form-key"; import { BASE_HIDDEN_ABILITY_CHANCE, BASE_SHINY_CHANCE, SHINY_EPIC_CHANCE, SHINY_VARIANT_CHANCE } from "#app/data/balance/rates"; import { Nature } from "#enums/nature"; import { StatusEffect } from "#enums/status-effect"; +import { doShinySparkleAnim } from "#app/field/anims"; export enum FieldPosition { CENTER, @@ -673,21 +674,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } initShinySparkle(): void { - const keySuffix = this.variant ? `_${this.variant + 1}` : ""; - const key = `shiny${keySuffix}`; - const shinySparkle = this.scene.addFieldSprite(0, 0, key); + const shinySparkle = this.scene.addFieldSprite(0, 0, "shiny"); shinySparkle.setVisible(false); shinySparkle.setOrigin(0.5, 1); - const frameNames = this.scene.anims.generateFrameNames(key, { suffix: ".png", end: 34 }); - if (!(this.scene.anims.exists(`sparkle${keySuffix}`))) { - this.scene.anims.create({ - key: `sparkle${keySuffix}`, - frames: frameNames, - frameRate: 32, - showOnStart: true, - hideOnComplete: true, - }); - } this.add(shinySparkle); this.shinySparkle = shinySparkle; @@ -1976,6 +1965,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { /** * Function that tries to set a Pokemon shiny based on seed. * For manual use only, usually to roll a Pokemon's shiny chance a second time. + * If it rolls shiny, also sets a random variant and give the Pokemon the associated luck. * * The base shiny odds are {@linkcode BASE_SHINY_CHANCE} / `65536` * @param thresholdOverride number that is divided by `2^16` (`65536`) to get the shiny chance, overrides {@linkcode shinyThreshold} if set (bypassing shiny rate modifiers such as Shiny Charm) @@ -2001,6 +1991,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.shiny = randSeedInt(65536) < shinyThreshold.value; if (this.shiny) { + this.variant = this.generateShinyVariant(); + this.luck = this.variant + 1 + (this.fusionShiny ? this.fusionVariant + 1 : 0); this.initShinySparkle(); } @@ -3802,8 +3794,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { sparkle(): void { if (this.shinySparkle) { - this.shinySparkle.play(`sparkle${this.variant ? `_${this.variant + 1}` : ""}`); - this.scene.playSound("se/sparkle"); + doShinySparkleAnim(this.scene, this.shinySparkle, this.variant); } } @@ -4646,12 +4637,13 @@ export class EnemyPokemon extends Pokemon { public aiType: AiType; public bossSegments: integer; public bossSegmentIndex: integer; - /** To indicate of the instance was populated with a dataSource -> e.g. loaded & populated from session data */ + /** To indicate if the instance was populated with a dataSource -> e.g. loaded & populated from session data */ public readonly isPopulatedFromDataSource: boolean; - constructor(scene: BattleScene, species: PokemonSpecies, level: integer, trainerSlot: TrainerSlot, boss: boolean, dataSource?: PokemonData) { - super(scene, 236, 84, species, level, dataSource?.abilityIndex, dataSource?.formIndex, - dataSource?.gender, dataSource ? dataSource.shiny : false, dataSource ? dataSource.variant : undefined, undefined, dataSource ? dataSource.nature : undefined, dataSource); + constructor(scene: BattleScene, species: PokemonSpecies, level: integer, trainerSlot: TrainerSlot, boss: boolean, shinyLock: boolean = false, dataSource?: PokemonData) { + super(scene, 236, 84, species, level, dataSource?.abilityIndex, dataSource?.formIndex, dataSource?.gender, + (!shinyLock && dataSource) ? dataSource.shiny : false, (!shinyLock && dataSource) ? dataSource.variant : undefined, + undefined, dataSource ? dataSource.nature : undefined, dataSource); this.trainerSlot = trainerSlot; this.isPopulatedFromDataSource = !!dataSource; // if a dataSource is provided, then it was populated from dataSource @@ -4680,12 +4672,15 @@ export class EnemyPokemon extends Pokemon { if (!dataSource) { this.generateAndPopulateMoveset(); - this.trySetShiny(); - if (Overrides.OPP_SHINY_OVERRIDE) { + if (shinyLock || Overrides.OPP_SHINY_OVERRIDE === false) { + this.shiny = false; + } else { + this.trySetShiny(); + } + + if (!this.shiny && Overrides.OPP_SHINY_OVERRIDE) { this.shiny = true; this.initShinySparkle(); - } else if (Overrides.OPP_SHINY_OVERRIDE === false) { - this.shiny = false; } if (this.shiny) { diff --git a/src/phases/egg-hatch-phase.ts b/src/phases/egg-hatch-phase.ts index 90aceeb46bc..d45c580228c 100644 --- a/src/phases/egg-hatch-phase.ts +++ b/src/phases/egg-hatch-phase.ts @@ -14,6 +14,7 @@ import SoundFade from "phaser3-rex-plugins/plugins/soundfade"; import * as Utils from "#app/utils"; import { EggLapsePhase } from "./egg-lapse-phase"; import { EggHatchData } from "#app/data/egg-hatch-data"; +import { doShinySparkleAnim } from "#app/field/anims"; /** @@ -341,8 +342,7 @@ export class EggHatchPhase extends Phase { this.pokemon.cry(); if (isShiny) { this.scene.time.delayedCall(Utils.fixedInt(500), () => { - this.pokemonShinySparkle.play(`sparkle${this.pokemon.variant ? `_${this.pokemon.variant + 1}` : ""}`); - this.scene.playSound("se/sparkle"); + doShinySparkleAnim(this.scene, this.pokemonShinySparkle, this.pokemon.variant); }); } this.scene.time.delayedCall(Utils.fixedInt(!this.skipped ? !isShiny ? 1250 : 1750 : !isShiny ? 250 : 750), () => { diff --git a/src/phases/encounter-phase.ts b/src/phases/encounter-phase.ts index 03126ba81bb..b524172af9d 100644 --- a/src/phases/encounter-phase.ts +++ b/src/phases/encounter-phase.ts @@ -379,6 +379,9 @@ export class EncounterPhase extends BattlePhase { if (encounter.onVisualsStart) { encounter.onVisualsStart(this.scene); + } else if (encounter.spriteConfigs && introVisuals) { + // If the encounter doesn't have any special visual intro, show sparkle for shiny Pokemon + introVisuals.playShinySparkles(); } const doEncounter = () => { diff --git a/src/system/pokemon-data.ts b/src/system/pokemon-data.ts index 443382186c7..64801cc0ff1 100644 --- a/src/system/pokemon-data.ts +++ b/src/system/pokemon-data.ts @@ -171,7 +171,7 @@ export default class PokemonData { playerPokemon.nickname = this.nickname; } }) - : scene.addEnemyPokemon(species, this.level, battleType === BattleType.TRAINER ? !double || !(partyMemberIndex % 2) ? TrainerSlot.TRAINER : TrainerSlot.TRAINER_PARTNER : TrainerSlot.NONE, this.boss, this); + : scene.addEnemyPokemon(species, this.level, battleType === BattleType.TRAINER ? !double || !(partyMemberIndex % 2) ? TrainerSlot.TRAINER : TrainerSlot.TRAINER_PARTNER : TrainerSlot.NONE, this.boss, false, this); if (this.summonData) { ret.primeSummonData(this.summonData); } diff --git a/src/test/mystery-encounter/encounters/global-trade-system-encounter.test.ts b/src/test/mystery-encounter/encounters/global-trade-system-encounter.test.ts index e8d19ff50b9..2c226df3c8c 100644 --- a/src/test/mystery-encounter/encounters/global-trade-system-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/global-trade-system-encounter.test.ts @@ -18,6 +18,7 @@ import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; import { Mode } from "#app/ui/ui"; import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; import { ModifierTier } from "#app/modifier/modifier-tier"; +import * as Utils from "#app/utils"; const namespace = "mysteryEncounters/globalTradeSystem"; const defaultParty = [ Species.LAPRAS, Species.GENGAR, Species.ABRA ]; @@ -176,6 +177,23 @@ describe("Global Trade System - Mystery Encounter", () => { expect(defaultParty.includes(speciesAfter!)).toBeFalsy(); }); + it("Should roll for shiny twice, with random variant and associated luck", async () => { + // This ensures that the first shiny roll gets ignored, to test the ME rerolling for shiny + game.override.enemyShiny(false); + + await game.runToMysteryEncounter(MysteryEncounterType.GLOBAL_TRADE_SYSTEM, defaultParty); + + vi.spyOn(Utils, "randSeedInt").mockReturnValue(1); // force shiny on reroll + + await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1 }); + + const receivedPokemon = scene.getPlayerParty().at(-1)!; + + expect(receivedPokemon.shiny).toBeTruthy(); + expect(receivedPokemon.variant).toBeDefined(); + expect(receivedPokemon.luck).toBe(receivedPokemon.variant + 1); + }); + it("should leave encounter without battle", async () => { const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); diff --git a/src/test/mystery-encounter/encounters/the-pokemon-salesman-encounter.test.ts b/src/test/mystery-encounter/encounters/the-pokemon-salesman-encounter.test.ts index 91b4a4bcab5..e90bc4efe56 100644 --- a/src/test/mystery-encounter/encounters/the-pokemon-salesman-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/the-pokemon-salesman-encounter.test.ts @@ -123,7 +123,7 @@ describe("The Pokemon Salesman - Mystery Encounter", () => { }); }); - it("Should update the player's money properly", async () => { + it("should update the player's money properly", async () => { const initialMoney = 20000; scene.money = initialMoney; const updateMoneySpy = vi.spyOn(EncounterPhaseUtils, "updatePlayerMoney"); @@ -137,7 +137,7 @@ describe("The Pokemon Salesman - Mystery Encounter", () => { expect(scene.money).toBe(initialMoney - price); }); - it("Should add the Pokemon to the party", async () => { + it("should add the Pokemon to the party", async () => { scene.money = 20000; await game.runToMysteryEncounter(MysteryEncounterType.THE_POKEMON_SALESMAN, defaultParty); @@ -153,6 +153,18 @@ describe("The Pokemon Salesman - Mystery Encounter", () => { expect(newlyPurchasedPokemon!.moveset.length > 0).toBeTruthy(); }); + it("should give the purchased Pokemon its HA or make it shiny", async () => { + scene.money = 20000; + await game.runToMysteryEncounter(MysteryEncounterType.THE_POKEMON_SALESMAN, defaultParty); + await runMysteryEncounterToEnd(game, 1); + + const newlyPurchasedPokemon = scene.getPlayerParty()[scene.getPlayerParty().length - 1]; + const isshiny = newlyPurchasedPokemon.shiny; + const hasHA = newlyPurchasedPokemon.abilityIndex === 2; + expect(isshiny || hasHA).toBeTruthy(); + expect(isshiny && hasHA).toBeFalsy(); + }); + it("should be disabled if player does not have enough money", async () => { scene.money = 0; await game.runToMysteryEncounter(MysteryEncounterType.THE_POKEMON_SALESMAN, defaultParty); 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 ae725f3480a..5c965b13bd4 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 @@ -109,6 +109,7 @@ describe("The Strong Stuff - Mystery Encounter", () => { species: getPokemonSpecies(Species.SHUCKLE), isBoss: true, bossSegments: 5, + shiny: false, customPokemonData: new CustomPokemonData({ spriteScale: 1.25 }), nature: Nature.BOLD, moveSet: [ Moves.INFESTATION, Moves.SALT_CURE, Moves.GASTRO_ACID, Moves.HEAL_ORDER ], 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 8286c6a694b..f8d96487092 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 @@ -92,6 +92,7 @@ describe("Trash to Treasure - Mystery Encounter", () => { { species: getPokemonSpecies(Species.GARBODOR), isBoss: true, + shiny: false, formIndex: 1, bossSegmentModifier: 1, moveSet: [ Moves.PAYBACK, Moves.GUNK_SHOT, Moves.STOMPING_TANTRUM, Moves.DRAIN_PUNCH ], From d6854c49690d78a43394298b7cce27713f2663c5 Mon Sep 17 00:00:00 2001 From: Mumble <171087428+frutescens@users.noreply.github.com> Date: Sat, 30 Nov 2024 16:09:34 -0800 Subject: [PATCH 07/13] [Tests] Enemy Held Item overrides are now applied after generateEnemyModifiers() (#4922) * Moved overrideModifiers and overrideHeldItems to after generateEnemyModifiers * Removed functions from scene * Fixed the test. Thank you pigeonbar. --- src/battle-scene.ts | 4 +--- src/phases/encounter-phase.ts | 6 ++++++ src/test/moves/effectiveness.test.ts | 14 ++++++++------ 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 2c38bb6632f..f5e3a714df6 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -4,7 +4,7 @@ import Pokemon, { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon"; import PokemonSpecies, { allSpecies, getPokemonSpecies, PokemonSpeciesFilter } from "#app/data/pokemon-species"; import { Constructor, isNullOrUndefined, randSeedInt } from "#app/utils"; import * as Utils from "#app/utils"; -import { ConsumableModifier, ConsumablePokemonModifier, DoubleBattleChanceBoosterModifier, ExpBalanceModifier, ExpShareModifier, FusePokemonModifier, HealingBoosterModifier, Modifier, ModifierBar, ModifierPredicate, MultipleParticipantExpBonusModifier, overrideHeldItems, overrideModifiers, PersistentModifier, PokemonExpBoosterModifier, PokemonFormChangeItemModifier, PokemonHeldItemModifier, PokemonHpRestoreModifier, PokemonIncrementingStatModifier, RememberMoveModifier, TerastallizeModifier, TurnHeldItemTransferModifier } from "./modifier/modifier"; +import { ConsumableModifier, ConsumablePokemonModifier, DoubleBattleChanceBoosterModifier, ExpBalanceModifier, ExpShareModifier, FusePokemonModifier, HealingBoosterModifier, Modifier, ModifierBar, ModifierPredicate, MultipleParticipantExpBonusModifier, PersistentModifier, PokemonExpBoosterModifier, PokemonFormChangeItemModifier, PokemonHeldItemModifier, PokemonHpRestoreModifier, PokemonIncrementingStatModifier, RememberMoveModifier, TerastallizeModifier, TurnHeldItemTransferModifier } from "./modifier/modifier"; import { PokeballType } from "#enums/pokeball"; import { initCommonAnims, initMoveAnim, loadCommonAnimAssets, loadMoveAnimAssets, populateAnims } from "#app/data/battle-anims"; import { Phase } from "#app/phase"; @@ -933,8 +933,6 @@ export default class BattleScene extends SceneBase { pokemon.generateFusionSpecies(); } - overrideModifiers(this, false); - overrideHeldItems(this, pokemon, false); if (boss && !dataSource) { const secondaryIvs = Utils.getIvsFromId(Utils.randSeedInt(4294967296)); diff --git a/src/phases/encounter-phase.ts b/src/phases/encounter-phase.ts index b524172af9d..a4c9aa44b36 100644 --- a/src/phases/encounter-phase.ts +++ b/src/phases/encounter-phase.ts @@ -34,6 +34,7 @@ import { Biome } from "#enums/biome"; import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; import { PlayerGender } from "#enums/player-gender"; import { Species } from "#enums/species"; +import { overrideHeldItems, overrideModifiers } from "#app/modifier/modifier"; import i18next from "i18next"; import { WEIGHT_INCREMENT_ON_SPAWN_MISS } from "#app/data/mystery-encounters/mystery-encounters"; @@ -216,6 +217,11 @@ export class EncounterPhase extends BattlePhase { if (!this.loaded && battle.battleType !== BattleType.MYSTERY_ENCOUNTER) { regenerateModifierPoolThresholds(this.scene.getEnemyField(), battle.battleType === BattleType.TRAINER ? ModifierPoolType.TRAINER : ModifierPoolType.WILD); this.scene.generateEnemyModifiers(); + overrideModifiers(this.scene, false); + this.scene.getEnemyField().forEach(enemy => { + overrideHeldItems(this.scene, enemy, false); + }); + } this.scene.ui.setMode(Mode.MESSAGE).then(() => { diff --git a/src/test/moves/effectiveness.test.ts b/src/test/moves/effectiveness.test.ts index 829d4533f9b..5ad258d7990 100644 --- a/src/test/moves/effectiveness.test.ts +++ b/src/test/moves/effectiveness.test.ts @@ -6,7 +6,7 @@ import { Abilities } from "#app/enums/abilities"; import { Moves } from "#app/enums/moves"; import { Species } from "#app/enums/species"; import * as Messages from "#app/messages"; -import { TerastallizeModifier } from "#app/modifier/modifier"; +import { TerastallizeModifier, overrideHeldItems } from "#app/modifier/modifier"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; @@ -15,15 +15,17 @@ function testMoveEffectiveness(game: GameManager, move: Moves, targetSpecies: Sp expected: number, targetAbility: Abilities = Abilities.BALL_FETCH, teraType?: Type): void { // Suppress getPokemonNameWithAffix because it calls on a null battle spec vi.spyOn(Messages, "getPokemonNameWithAffix").mockReturnValue(""); - game.override.enemyAbility(targetAbility); - - if (teraType !== undefined) { - game.override.enemyHeldItems([{ name:"TERA_SHARD", type: teraType }]); - } + game.override + .enemyAbility(targetAbility) + .enemyHeldItems([{ name:"TERA_SHARD", type: teraType }]); const user = game.scene.addPlayerPokemon(getPokemonSpecies(Species.SNORLAX), 5); const target = game.scene.addEnemyPokemon(getPokemonSpecies(targetSpecies), 5, TrainerSlot.NONE); + if (teraType !== undefined) { + overrideHeldItems(game.scene, target, false); + } + expect(target.getMoveEffectiveness(user, allMoves[move])).toBe(expected); user.destroy(); target.destroy(); From cef2f2adf7f26abf4f93d6fa2ee0f670408146ae Mon Sep 17 00:00:00 2001 From: Mumble <171087428+frutescens@users.noreply.github.com> Date: Sat, 30 Nov 2024 18:40:05 -0800 Subject: [PATCH 08/13] [Ability] Fully implement Sheer Force (#4890) * Added checks for Sheer Force interactions currently in the code. * Test for Relic Song interaction * Test for Shell Bell interaction * Created new Modifier class MoveEffectModifier * Applied new modifier class. * Revert "Applied new modifier class." This reverts commit 222bc8d42875485742ba8bd38fa5e9b978bbd53a. * Revert "Created new Modifier class MoveEffectModifier" This reverts commit 0e57ed03ff7c0e6fb59c7b3c2ea74b6fe6327f59. * Added checks for Shell Bell, Scope Lens, Wide Lens, Leek, and Golden Punch * Fixing function calls. * Fixed getSecondaryChanceMultiplier to just look at sheer force. * Rewrote old Sheer Force tests in accordance to current testing standards. * Resetting modifiers.ts * Update src/data/pokemon-forms.ts Co-authored-by: innerthunder <168692175+innerthunder@users.noreply.github.com> * Moved getSecondaryChanceMultiplier to FlinchChanceModifier and revised Serene Grace tests * Adding an additional override to prevent test failures. * Removed Serene Grace factor from modifier. * Added forgotten conditional. * Added comment --------- Co-authored-by: frutescens Co-authored-by: innerthunder <168692175+innerthunder@users.noreply.github.com> --- src/data/ability.ts | 4 +- src/data/pokemon-forms.ts | 4 + src/modifier/modifier.ts | 29 ++-- src/test/abilities/serene_grace.test.ts | 81 +++------ src/test/abilities/sheer_force.test.ts | 218 ++++++++++-------------- 5 files changed, 123 insertions(+), 213 deletions(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index 234b502c23f..7fa046e2369 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -5713,9 +5713,7 @@ export function initAbilities() { .condition(getSheerForceHitDisableAbCondition()), new Ability(Abilities.SHEER_FORCE, 5) .attr(MovePowerBoostAbAttr, (user, target, move) => move.chance >= 1, 5461 / 4096) - .attr(MoveEffectChanceMultiplierAbAttr, 0) - .edgeCase() // Should disable shell bell and Meloetta's relic song transformation - .edgeCase(), // Should disable life orb, eject button, red card, kee/maranga berry if they get implemented + .attr(MoveEffectChanceMultiplierAbAttr, 0), // Should disable life orb, eject button, red card, kee/maranga berry if they get implemented new Ability(Abilities.CONTRARY, 5) .attr(StatStageChangeMultiplierAbAttr, -1) .ignorable(), diff --git a/src/data/pokemon-forms.ts b/src/data/pokemon-forms.ts index 2db0ed54294..a1b2d7896d7 100644 --- a/src/data/pokemon-forms.ts +++ b/src/data/pokemon-forms.ts @@ -351,6 +351,10 @@ export class MeloettaFormChangePostMoveTrigger extends SpeciesFormChangePostMove if (pokemon.scene.gameMode.hasChallenge(Challenges.SINGLE_TYPE)) { return false; } else { + // Meloetta will not transform if it has the ability Sheer Force when using Relic Song + if (pokemon.hasAbility(Abilities.SHEER_FORCE)) { + return false; + } return super.canChange(pokemon); } } diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index 5e5246269a3..05d9e8b9897 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -18,7 +18,6 @@ import type { VoucherType } from "#app/system/voucher"; import { Command } from "#app/ui/command-ui-handler"; import { addTextObject, TextStyle } from "#app/ui/text"; import { BooleanHolder, hslToHex, isNullOrUndefined, NumberHolder, toDmgValue } from "#app/utils"; -import { Abilities } from "#enums/abilities"; import { BattlerTagType } from "#enums/battler-tag-type"; import { BerryType } from "#enums/berry-type"; import { Moves } from "#enums/moves"; @@ -726,22 +725,6 @@ export abstract class PokemonHeldItemModifier extends PersistentModifier { return 1; } - //Applies to items with chance of activating secondary effects ie Kings Rock - getSecondaryChanceMultiplier(pokemon: Pokemon): number { - // Temporary quickfix to stop game from freezing when the opponet uses u-turn while holding on to king's rock - if (!pokemon.getLastXMoves()[0]) { - return 1; - } - const sheerForceAffected = allMoves[pokemon.getLastXMoves()[0].move].chance >= 0 && pokemon.hasAbility(Abilities.SHEER_FORCE); - - if (sheerForceAffected) { - return 0; - } else if (pokemon.hasAbility(Abilities.SERENE_GRACE)) { - return 2; - } - return 1; - } - getMaxStackCount(scene: BattleScene, forThreshold?: boolean): number { const pokemon = this.getPokemon(scene); if (!pokemon) { @@ -1614,9 +1597,16 @@ export class BypassSpeedChanceModifier extends PokemonHeldItemModifier { } } +/** + * Class for Pokemon held items like King's Rock + * Because King's Rock can be stacked in PokeRogue, unlike mainline, it does not receive a boost from Abilities.SERENE_GRACE + */ export class FlinchChanceModifier extends PokemonHeldItemModifier { + private chance: number; constructor(type: ModifierType, pokemonId: number, stackCount?: number) { super(type, pokemonId, stackCount); + + this.chance = 10; } matchType(modifier: Modifier) { @@ -1644,7 +1634,8 @@ export class FlinchChanceModifier extends PokemonHeldItemModifier { * @returns `true` if {@linkcode FlinchChanceModifier} has been applied */ override apply(pokemon: Pokemon, flinched: BooleanHolder): boolean { - if (!flinched.value && pokemon.randSeedInt(10) < (this.getStackCount() * this.getSecondaryChanceMultiplier(pokemon))) { + // The check for pokemon.battleSummonData is to ensure that a crash doesn't occur when a Pokemon with King's Rock procs a flinch + if (pokemon.battleSummonData && !flinched.value && pokemon.randSeedInt(100) < (this.getStackCount() * this.chance)) { flinched.value = true; return true; } @@ -1652,7 +1643,7 @@ export class FlinchChanceModifier extends PokemonHeldItemModifier { return false; } - getMaxHeldItemCount(pokemon: Pokemon): number { + getMaxHeldItemCount(_pokemon: Pokemon): number { return 3; } } diff --git a/src/test/abilities/serene_grace.test.ts b/src/test/abilities/serene_grace.test.ts index 3318c7fc27a..a19b5c82546 100644 --- a/src/test/abilities/serene_grace.test.ts +++ b/src/test/abilities/serene_grace.test.ts @@ -1,15 +1,12 @@ import { BattlerIndex } from "#app/battle"; -import { applyAbAttrs, MoveEffectChanceMultiplierAbAttr } from "#app/data/ability"; -import { Stat } from "#enums/stat"; -import { MoveEffectPhase } from "#app/phases/move-effect-phase"; -import * as Utils from "#app/utils"; 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 } from "vitest"; - +import { allMoves } from "#app/data/move"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { FlinchAttr } from "#app/data/move"; describe("Abilities - Serene Grace", () => { let phaserGame: Phaser.Game; @@ -27,66 +24,26 @@ describe("Abilities - Serene Grace", () => { beforeEach(() => { game = new GameManager(phaserGame); - const movesToUse = [ Moves.AIR_SLASH, Moves.TACKLE ]; - game.override.battleType("single"); - game.override.enemySpecies(Species.ONIX); - game.override.startingLevel(100); - game.override.moveset(movesToUse); - game.override.enemyMoveset([ Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE ]); + game.override + .battleType("single") + .ability(Abilities.SERENE_GRACE) + .moveset([ Moves.AIR_SLASH, Moves.TACKLE ]) + .enemyLevel(10) + .enemyMoveset([ Moves.SPLASH ]); }); - it("Move chance without Serene Grace", async () => { - const moveToUse = Moves.AIR_SLASH; - await game.startBattle([ - Species.PIDGEOT - ]); + it("Serene Grace should double the secondary effect chance of a move", async () => { + await game.classicMode.startBattle([ Species.SHUCKLE ]); + const airSlashMove = allMoves[Moves.AIR_SLASH]; + const airSlashFlinchAttr = airSlashMove.getAttrs(FlinchAttr)[0]; + vi.spyOn(airSlashFlinchAttr, "getMoveChance"); - game.scene.getEnemyParty()[0].stats[Stat.SPDEF] = 10000; - expect(game.scene.getPlayerParty()[0].formIndex).toBe(0); - - game.move.select(moveToUse); - + game.move.select(Moves.AIR_SLASH); await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); - await game.phaseInterceptor.to(MoveEffectPhase, false); + await game.move.forceHit(); + await game.phaseInterceptor.to("BerryPhase"); - // Check chance of Air Slash without Serene Grace - const phase = game.scene.getCurrentPhase() as MoveEffectPhase; - const move = phase.move.getMove(); - expect(move.id).toBe(Moves.AIR_SLASH); - - const chance = new Utils.IntegerHolder(move.chance); - console.log(move.chance + " Their ability is " + phase.getUserPokemon()!.getAbility().name); - applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false); - expect(chance.value).toBe(30); - - }, 20000); - - it("Move chance with Serene Grace", async () => { - const moveToUse = Moves.AIR_SLASH; - game.override.ability(Abilities.SERENE_GRACE); - await game.startBattle([ - Species.TOGEKISS - ]); - - game.scene.getEnemyParty()[0].stats[Stat.SPDEF] = 10000; - expect(game.scene.getPlayerParty()[0].formIndex).toBe(0); - - game.move.select(moveToUse); - - await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); - await game.phaseInterceptor.to(MoveEffectPhase, false); - - // Check chance of Air Slash with Serene Grace - const phase = game.scene.getCurrentPhase() as MoveEffectPhase; - const move = phase.move.getMove(); - expect(move.id).toBe(Moves.AIR_SLASH); - - const chance = new Utils.IntegerHolder(move.chance); - applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false); - expect(chance.value).toBe(60); - - }, 20000); - - //TODO King's Rock Interaction Unit Test + expect(airSlashFlinchAttr.getMoveChance).toHaveLastReturnedWith(60); + }); }); diff --git a/src/test/abilities/sheer_force.test.ts b/src/test/abilities/sheer_force.test.ts index 826694752b7..a0ddf5bb9c6 100644 --- a/src/test/abilities/sheer_force.test.ts +++ b/src/test/abilities/sheer_force.test.ts @@ -1,15 +1,13 @@ import { BattlerIndex } from "#app/battle"; -import { applyAbAttrs, applyPostDefendAbAttrs, applyPreAttackAbAttrs, MoveEffectChanceMultiplierAbAttr, MovePowerBoostAbAttr, PostDefendTypeChangeAbAttr } from "#app/data/ability"; -import { MoveEffectPhase } from "#app/phases/move-effect-phase"; -import { NumberHolder } from "#app/utils"; +import { Type } from "#app/enums/type"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import { Stat } from "#enums/stat"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; -import { allMoves } from "#app/data/move"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { allMoves, FlinchAttr } from "#app/data/move"; describe("Abilities - Sheer Force", () => { let phaserGame: Phaser.Game; @@ -27,143 +25,91 @@ describe("Abilities - Sheer Force", () => { beforeEach(() => { game = new GameManager(phaserGame); - const movesToUse = [ Moves.AIR_SLASH, Moves.BIND, Moves.CRUSH_CLAW, Moves.TACKLE ]; - game.override.battleType("single"); - game.override.enemySpecies(Species.ONIX); - game.override.startingLevel(100); - game.override.moveset(movesToUse); - game.override.enemyMoveset([ Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE ]); + game.override + .battleType("single") + .ability(Abilities.SHEER_FORCE) + .enemySpecies(Species.ONIX) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset([ Moves.SPLASH ]) + .disableCrits(); }); - it("Sheer Force", async () => { - const moveToUse = Moves.AIR_SLASH; - game.override.ability(Abilities.SHEER_FORCE); + const SHEER_FORCE_MULT = 5461 / 4096; + + it("Sheer Force should boost the power of the move but disable secondary effects", async () => { + game.override.moveset([ Moves.AIR_SLASH ]); + await game.classicMode.startBattle([ Species.SHUCKLE ]); + + const airSlashMove = allMoves[Moves.AIR_SLASH]; + vi.spyOn(airSlashMove, "calculateBattlePower"); + const airSlashFlinchAttr = airSlashMove.getAttrs(FlinchAttr)[0]; + vi.spyOn(airSlashFlinchAttr, "getMoveChance"); + + game.move.select(Moves.AIR_SLASH); + + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.move.forceHit(); + await game.phaseInterceptor.to("BerryPhase", false); + + expect(airSlashMove.calculateBattlePower).toHaveLastReturnedWith(airSlashMove.power * SHEER_FORCE_MULT); + expect(airSlashFlinchAttr.getMoveChance).toHaveLastReturnedWith(0); + }); + + it("Sheer Force does not affect the base damage or secondary effects of binding moves", async () => { + game.override.moveset([ Moves.BIND ]); + await game.classicMode.startBattle([ Species.SHUCKLE ]); + + const bindMove = allMoves[Moves.BIND]; + vi.spyOn(bindMove, "calculateBattlePower"); + + game.move.select(Moves.BIND); + + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.move.forceHit(); + await game.phaseInterceptor.to("BerryPhase", false); + + expect(bindMove.calculateBattlePower).toHaveLastReturnedWith(bindMove.power); + }, 20000); + + it("Sheer Force does not boost the base damage of moves with no secondary effect", async () => { + game.override.moveset([ Moves.TACKLE ]); await game.classicMode.startBattle([ Species.PIDGEOT ]); - game.scene.getEnemyPokemon()!.stats[Stat.SPDEF] = 10000; - expect(game.scene.getPlayerPokemon()!.formIndex).toBe(0); - - game.move.select(moveToUse); + const tackleMove = allMoves[Moves.TACKLE]; + vi.spyOn(tackleMove, "calculateBattlePower"); + game.move.select(Moves.TACKLE); await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); - await game.phaseInterceptor.to(MoveEffectPhase, false); + await game.move.forceHit(); + await game.phaseInterceptor.to("BerryPhase", false); - const phase = game.scene.getCurrentPhase() as MoveEffectPhase; - const move = phase.move.getMove(); - expect(move.id).toBe(Moves.AIR_SLASH); + expect(tackleMove.calculateBattlePower).toHaveLastReturnedWith(tackleMove.power); + }); - //Verify the move is boosted and has no chance of secondary effects - const power = new NumberHolder(move.power); - const chance = new NumberHolder(move.chance); + it("Sheer Force can disable the on-hit activation of specific abilities", async () => { + game.override + .moveset([ Moves.HEADBUTT ]) + .enemySpecies(Species.SQUIRTLE) + .enemyLevel(10) + .enemyAbility(Abilities.COLOR_CHANGE); - applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false); - applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getFirstTarget()!, move, false, power); - - expect(chance.value).toBe(0); - expect(power.value).toBe(move.power * 5461 / 4096); - - - }, 20000); - - it("Sheer Force with exceptions including binding moves", async () => { - const moveToUse = Moves.BIND; - game.override.ability(Abilities.SHEER_FORCE); await game.classicMode.startBattle([ Species.PIDGEOT ]); + const enemyPokemon = game.scene.getEnemyPokemon(); + const headbuttMove = allMoves[Moves.HEADBUTT]; + vi.spyOn(headbuttMove, "calculateBattlePower"); + const headbuttFlinchAttr = headbuttMove.getAttrs(FlinchAttr)[0]; + vi.spyOn(headbuttFlinchAttr, "getMoveChance"); - - game.scene.getEnemyPokemon()!.stats[Stat.DEF] = 10000; - expect(game.scene.getPlayerPokemon()!.formIndex).toBe(0); - - game.move.select(moveToUse); + game.move.select(Moves.HEADBUTT); await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); - await game.phaseInterceptor.to(MoveEffectPhase, false); + await game.move.forceHit(); + await game.phaseInterceptor.to("BerryPhase", false); - const phase = game.scene.getCurrentPhase() as MoveEffectPhase; - const move = phase.move.getMove(); - expect(move.id).toBe(Moves.BIND); - - //Binding moves and other exceptions are not affected by Sheer Force and have a chance.value of -1 - const power = new NumberHolder(move.power); - const chance = new NumberHolder(move.chance); - - applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false); - applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getFirstTarget()!, move, false, power); - - expect(chance.value).toBe(-1); - expect(power.value).toBe(move.power); - - - }, 20000); - - it("Sheer Force with moves with no secondary effect", async () => { - const moveToUse = Moves.TACKLE; - game.override.ability(Abilities.SHEER_FORCE); - await game.classicMode.startBattle([ Species.PIDGEOT ]); - - - game.scene.getEnemyPokemon()!.stats[Stat.DEF] = 10000; - expect(game.scene.getPlayerPokemon()!.formIndex).toBe(0); - - game.move.select(moveToUse); - - await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); - await game.phaseInterceptor.to(MoveEffectPhase, false); - - const phase = game.scene.getCurrentPhase() as MoveEffectPhase; - const move = phase.move.getMove(); - expect(move.id).toBe(Moves.TACKLE); - - //Binding moves and other exceptions are not affected by Sheer Force and have a chance.value of -1 - const power = new NumberHolder(move.power); - const chance = new NumberHolder(move.chance); - - applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false); - applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getFirstTarget()!, move, false, power); - - expect(chance.value).toBe(-1); - expect(power.value).toBe(move.power); - - - }, 20000); - - it("Sheer Force Disabling Specific Abilities", async () => { - const moveToUse = Moves.CRUSH_CLAW; - game.override.enemyAbility(Abilities.COLOR_CHANGE); - game.override.startingHeldItems([{ name: "KINGS_ROCK", count: 1 }]); - game.override.ability(Abilities.SHEER_FORCE); - await game.startBattle([ Species.PIDGEOT ]); - - - game.scene.getEnemyPokemon()!.stats[Stat.DEF] = 10000; - expect(game.scene.getPlayerPokemon()!.formIndex).toBe(0); - - game.move.select(moveToUse); - - await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); - await game.phaseInterceptor.to(MoveEffectPhase, false); - - const phase = game.scene.getCurrentPhase() as MoveEffectPhase; - const move = phase.move.getMove(); - expect(move.id).toBe(Moves.CRUSH_CLAW); - - //Disable color change due to being hit by Sheer Force - const power = new NumberHolder(move.power); - const chance = new NumberHolder(move.chance); - const user = phase.getUserPokemon()!; - const target = phase.getFirstTarget()!; - const opponentType = target.getTypes()[0]; - - applyAbAttrs(MoveEffectChanceMultiplierAbAttr, user, null, false, chance, move, target, false); - applyPreAttackAbAttrs(MovePowerBoostAbAttr, user, target, move, false, power); - applyPostDefendAbAttrs(PostDefendTypeChangeAbAttr, target, user, move, target.apply(user, move)); - - expect(chance.value).toBe(0); - expect(power.value).toBe(move.power * 5461 / 4096); - expect(target.getTypes().length).toBe(2); - expect(target.getTypes()[0]).toBe(opponentType); - - }, 20000); + expect(enemyPokemon?.getTypes()[0]).toBe(Type.WATER); + expect(headbuttMove.calculateBattlePower).toHaveLastReturnedWith(headbuttMove.power * SHEER_FORCE_MULT); + expect(headbuttFlinchAttr.getMoveChance).toHaveLastReturnedWith(0); + }); it("Two Pokemon with abilities disabled by Sheer Force hitting each other should not cause a crash", async () => { const moveToUse = Moves.CRUNCH; @@ -191,5 +137,19 @@ describe("Abilities - Sheer Force", () => { expect(onix.getTypes()).toStrictEqual(expectedTypes); }); - //TODO King's Rock Interaction Unit Test + it("Sheer Force should disable Meloetta's transformation from Relic Song", async () => { + game.override + .ability(Abilities.SHEER_FORCE) + .moveset([ Moves.RELIC_SONG ]) + .enemyMoveset([ Moves.SPLASH ]) + .enemyLevel(100); + await game.classicMode.startBattle([ Species.MELOETTA ]); + + const playerPokemon = game.scene.getPlayerPokemon(); + const formKeyStart = playerPokemon?.getFormKey(); + + game.move.select(Moves.RELIC_SONG); + await game.phaseInterceptor.to("TurnEndPhase"); + expect(formKeyStart).toBe(playerPokemon?.getFormKey()); + }); }); From 9ce4d5eecaaf96766577f5bdb03640db513a1147 Mon Sep 17 00:00:00 2001 From: AJ Fontaine <36677462+Fontbane@users.noreply.github.com> Date: Sun, 1 Dec 2024 11:21:40 -0500 Subject: [PATCH 09/13] [Balance] Remove candy friendship loss from fainting (#4953) * Remove candy friendship loss from fainting * Apply Moka suggestions Co-authored-by: Moka <54149968+MokaStitcher@users.noreply.github.com> * Fix starterAmount using friendship instead of adjusted amount --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> Co-authored-by: Moka <54149968+MokaStitcher@users.noreply.github.com> --- src/field/pokemon.ts | 39 +++++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 0675b9485cf..54b0b871b51 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -4271,28 +4271,29 @@ export class PlayerPokemon extends Pokemon { }); } - addFriendship(friendship: integer): void { - const starterSpeciesId = this.species.getRootSpeciesId(); - const fusionStarterSpeciesId = this.isFusion() && this.fusionSpecies ? this.fusionSpecies.getRootSpeciesId() : 0; - const starterData = [ - this.scene.gameData.starterData[starterSpeciesId], - fusionStarterSpeciesId ? this.scene.gameData.starterData[fusionStarterSpeciesId] : null - ].filter(d => !!d); - const amount = new Utils.IntegerHolder(friendship); - let candyFriendshipMultiplier = CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER; - if (this.scene.eventManager.isEventActive()) { - candyFriendshipMultiplier *= this.scene.eventManager.getFriendshipMultiplier(); - } - const starterAmount = new Utils.IntegerHolder(Math.floor(friendship * (this.scene.gameMode.isClassic && friendship > 0 ? candyFriendshipMultiplier : 1) / (fusionStarterSpeciesId ? 2 : 1))); - if (amount.value > 0) { + addFriendship(friendship: number): void { + if (friendship > 0) { + const starterSpeciesId = this.species.getRootSpeciesId(); + const fusionStarterSpeciesId = this.isFusion() && this.fusionSpecies ? this.fusionSpecies.getRootSpeciesId() : 0; + const starterData = [ + this.scene.gameData.starterData[starterSpeciesId], + fusionStarterSpeciesId ? this.scene.gameData.starterData[fusionStarterSpeciesId] : null + ].filter(d => !!d); + const amount = new Utils.NumberHolder(friendship); this.scene.applyModifier(PokemonFriendshipBoosterModifier, true, this, amount); - this.scene.applyModifier(PokemonFriendshipBoosterModifier, true, this, starterAmount); + let candyFriendshipMultiplier = CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER; + if (this.scene.eventManager.isEventActive()) { + candyFriendshipMultiplier *= this.scene.eventManager.getFriendshipMultiplier(); + } + const starterAmount = new Utils.NumberHolder(Math.floor(amount.value * (this.scene.gameMode.isClassic ? candyFriendshipMultiplier : 1) / (fusionStarterSpeciesId ? 2 : 1))); + // Add friendship to this PlayerPokemon this.friendship = Math.min(this.friendship + amount.value, 255); if (this.friendship === 255) { this.scene.validateAchv(achvs.MAX_FRIENDSHIP); } - starterData.forEach((sd: StarterDataEntry, i: integer) => { + // Add to candy progress for this mon's starter species and its fused species (if it has one) + starterData.forEach((sd: StarterDataEntry, i: number) => { const speciesId = !i ? starterSpeciesId : fusionStarterSpeciesId as Species; sd.friendship = (sd.friendship || 0) + starterAmount.value; if (sd.friendship >= getStarterValueFriendshipCap(speciesStarterCosts[speciesId])) { @@ -4301,10 +4302,8 @@ export class PlayerPokemon extends Pokemon { } }); } else { - this.friendship = Math.max(this.friendship + amount.value, 0); - for (const sd of starterData) { - sd.friendship = Math.max((sd.friendship || 0) + starterAmount.value, 0); - } + // Lose friendship upon fainting + this.friendship = Math.max(this.friendship + friendship, 0); } } /** From 7b06314940660ec4ffd05a61a2700679a9483830 Mon Sep 17 00:00:00 2001 From: AJ Fontaine <36677462+Fontbane@users.noreply.github.com> Date: Sun, 1 Dec 2024 11:38:16 -0500 Subject: [PATCH 10/13] [Bug] Fix fusions learning moves of wrong component mon on evolution (#4921) * Fix fusions learning moves of wrong component mon on evolution * Apply suggestions from code review Co-authored-by: Moka <54149968+MokaStitcher@users.noreply.github.com> --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> Co-authored-by: Moka <54149968+MokaStitcher@users.noreply.github.com> --- src/data/balance/pokemon-level-moves.ts | 4 +- src/field/pokemon.ts | 57 +++++++++++++++---------- src/phases/evolution-phase.ts | 10 +++-- 3 files changed, 44 insertions(+), 27 deletions(-) diff --git a/src/data/balance/pokemon-level-moves.ts b/src/data/balance/pokemon-level-moves.ts index 71d98fb4fc2..2a3ab431424 100644 --- a/src/data/balance/pokemon-level-moves.ts +++ b/src/data/balance/pokemon-level-moves.ts @@ -16,9 +16,9 @@ interface PokemonSpeciesFormLevelMoves { } /** Moves that can only be learned with a memory-mushroom */ -const RELEARN_MOVE = -1; +export const RELEARN_MOVE = -1; /** Moves that can only be learned with an evolve */ -const EVOLVE_MOVE = 0; +export const EVOLVE_MOVE = 0; export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.BULBASAUR]: [ diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 54b0b871b51..c22c755fc6e 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -29,7 +29,7 @@ import { BattlerIndex } from "#app/battle"; import { Mode } from "#app/ui/ui"; import PartyUiHandler, { PartyOption, PartyUiMode } from "#app/ui/party-ui-handler"; import SoundFade from "phaser3-rex-plugins/plugins/soundfade"; -import { LevelMoves } from "#app/data/balance/pokemon-level-moves"; +import { EVOLVE_MOVE, LevelMoves, RELEARN_MOVE } from "#app/data/balance/pokemon-level-moves"; import { DamageAchv, achvs } from "#app/system/achv"; import { DexAttr, StarterDataEntry, StarterMoveset } from "#app/system/game-data"; import { QuantizerCelebi, argbFromRgba, rgbaFromArgb } from "@material/material-color-utilities"; @@ -71,6 +71,15 @@ import { Nature } from "#enums/nature"; import { StatusEffect } from "#enums/status-effect"; import { doShinySparkleAnim } from "#app/field/anims"; +export enum LearnMoveSituation { + MISC, + LEVEL_UP, + RELEARN, + EVOLUTION, + EVOLUTION_FUSED, // If fusionSpecies has Evolved + EVOLUTION_FUSED_BASE // If fusion's base species has Evolved +} + export enum FieldPosition { CENTER, LEFT, @@ -1817,40 +1826,44 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @param {boolean} includeRelearnerMoves Whether to include moves that would require a relearner. Note the move relearner inherently allows evolution moves * @returns {LevelMoves} A list of moves and the levels they can be learned at */ - getLevelMoves(startingLevel?: integer, includeEvolutionMoves: boolean = false, simulateEvolutionChain: boolean = false, includeRelearnerMoves: boolean = false): LevelMoves { + getLevelMoves(startingLevel?: integer, includeEvolutionMoves: boolean = false, simulateEvolutionChain: boolean = false, includeRelearnerMoves: boolean = false, learnSituation: LearnMoveSituation = LearnMoveSituation.MISC): LevelMoves { const ret: LevelMoves = []; let levelMoves: LevelMoves = []; if (!startingLevel) { startingLevel = this.level; } - if (simulateEvolutionChain) { - const evolutionChain = this.species.getSimulatedEvolutionChain(this.level, this.hasTrainer(), this.isBoss(), this.isPlayer()); - for (let e = 0; e < evolutionChain.length; e++) { - // TODO: Might need to pass specific form index in simulated evolution chain - const speciesLevelMoves = getPokemonSpeciesForm(evolutionChain[e][0], this.formIndex).getLevelMoves(); - if (includeRelearnerMoves) { - levelMoves.push(...speciesLevelMoves); - } else { - levelMoves.push(...speciesLevelMoves.filter(lm => (includeEvolutionMoves && lm[0] === 0) || ((!e || lm[0] > 1) && (e === evolutionChain.length - 1 || lm[0] <= evolutionChain[e + 1][1])))); - } - } + if (learnSituation === LearnMoveSituation.EVOLUTION_FUSED && this.fusionSpecies) { // For fusion evolutions, get ONLY the moves of the component mon that evolved + levelMoves = this.getFusionSpeciesForm(true).getLevelMoves().filter(lm => (includeEvolutionMoves && lm[0] === EVOLVE_MOVE) || (includeRelearnerMoves && lm[0] === RELEARN_MOVE) || lm[0] > 0); } else { - levelMoves = this.getSpeciesForm(true).getLevelMoves().filter(lm => (includeEvolutionMoves && lm[0] === 0) || (includeRelearnerMoves && lm[0] === -1) || lm[0] > 0); - } - if (this.fusionSpecies) { if (simulateEvolutionChain) { - const fusionEvolutionChain = this.fusionSpecies.getSimulatedEvolutionChain(this.level, this.hasTrainer(), this.isBoss(), this.isPlayer()); - for (let e = 0; e < fusionEvolutionChain.length; e++) { + const evolutionChain = this.species.getSimulatedEvolutionChain(this.level, this.hasTrainer(), this.isBoss(), this.isPlayer()); + for (let e = 0; e < evolutionChain.length; e++) { // TODO: Might need to pass specific form index in simulated evolution chain - const speciesLevelMoves = getPokemonSpeciesForm(fusionEvolutionChain[e][0], this.fusionFormIndex).getLevelMoves(); + const speciesLevelMoves = getPokemonSpeciesForm(evolutionChain[e][0], this.formIndex).getLevelMoves(); if (includeRelearnerMoves) { - levelMoves.push(...speciesLevelMoves.filter(lm => (includeEvolutionMoves && lm[0] === 0) || lm[0] !== 0)); + levelMoves.push(...speciesLevelMoves); } else { - levelMoves.push(...speciesLevelMoves.filter(lm => (includeEvolutionMoves && lm[0] === 0) || ((!e || lm[0] > 1) && (e === fusionEvolutionChain.length - 1 || lm[0] <= fusionEvolutionChain[e + 1][1])))); + levelMoves.push(...speciesLevelMoves.filter(lm => (includeEvolutionMoves && lm[0] === EVOLVE_MOVE) || ((!e || lm[0] > 1) && (e === evolutionChain.length - 1 || lm[0] <= evolutionChain[e + 1][1])))); } } } else { - levelMoves.push(...this.getFusionSpeciesForm(true).getLevelMoves().filter(lm => (includeEvolutionMoves && lm[0] === 0) || (includeRelearnerMoves && lm[0] === -1) || lm[0] > 0)); + levelMoves = this.getSpeciesForm(true).getLevelMoves().filter(lm => (includeEvolutionMoves && lm[0] === EVOLVE_MOVE) || (includeRelearnerMoves && lm[0] === RELEARN_MOVE) || lm[0] > 0); + } + if (this.fusionSpecies && learnSituation !== LearnMoveSituation.EVOLUTION_FUSED_BASE) { // For fusion evolutions, get ONLY the moves of the component mon that evolved + if (simulateEvolutionChain) { + const fusionEvolutionChain = this.fusionSpecies.getSimulatedEvolutionChain(this.level, this.hasTrainer(), this.isBoss(), this.isPlayer()); + for (let e = 0; e < fusionEvolutionChain.length; e++) { + // TODO: Might need to pass specific form index in simulated evolution chain + const speciesLevelMoves = getPokemonSpeciesForm(fusionEvolutionChain[e][0], this.fusionFormIndex).getLevelMoves(); + if (includeRelearnerMoves) { + levelMoves.push(...speciesLevelMoves.filter(lm => (includeEvolutionMoves && lm[0] === EVOLVE_MOVE) || lm[0] !== EVOLVE_MOVE)); + } else { + levelMoves.push(...speciesLevelMoves.filter(lm => (includeEvolutionMoves && lm[0] === EVOLVE_MOVE) || ((!e || lm[0] > 1) && (e === fusionEvolutionChain.length - 1 || lm[0] <= fusionEvolutionChain[e + 1][1])))); + } + } + } else { + levelMoves.push(...this.getFusionSpeciesForm(true).getLevelMoves().filter(lm => (includeEvolutionMoves && lm[0] === EVOLVE_MOVE) || (includeRelearnerMoves && lm[0] === RELEARN_MOVE) || lm[0] > 0)); + } } } levelMoves.sort((lma: [integer, integer], lmb: [integer, integer]) => lma[0] > lmb[0] ? 1 : lma[0] < lmb[0] ? -1 : 0); diff --git a/src/phases/evolution-phase.ts b/src/phases/evolution-phase.ts index 01994263688..36bd3b7bd81 100644 --- a/src/phases/evolution-phase.ts +++ b/src/phases/evolution-phase.ts @@ -1,17 +1,18 @@ import SoundFade from "phaser3-rex-plugins/plugins/soundfade"; import { Phase } from "#app/phase"; import BattleScene, { AnySound } from "#app/battle-scene"; -import { SpeciesFormEvolution } from "#app/data/balance/pokemon-evolutions"; +import { FusionSpeciesFormEvolution, SpeciesFormEvolution } from "#app/data/balance/pokemon-evolutions"; import EvolutionSceneHandler from "#app/ui/evolution-scene-handler"; import * as Utils from "#app/utils"; import { Mode } from "#app/ui/ui"; import { cos, sin } from "#app/field/anims"; -import Pokemon, { PlayerPokemon } from "#app/field/pokemon"; +import Pokemon, { LearnMoveSituation, PlayerPokemon } from "#app/field/pokemon"; import { getTypeRgb } from "#app/data/type"; import i18next from "i18next"; import { getPokemonNameWithAffix } from "#app/messages"; import { LearnMovePhase } from "#app/phases/learn-move-phase"; import { EndEvolutionPhase } from "#app/phases/end-evolution-phase"; +import { EVOLVE_MOVE } from "#app/data/balance/pokemon-level-moves"; export class EvolutionPhase extends Phase { protected pokemon: PlayerPokemon; @@ -20,6 +21,7 @@ export class EvolutionPhase extends Phase { private preEvolvedPokemonName: string; private evolution: SpeciesFormEvolution | null; + private fusionSpeciesEvolved: boolean; // Whether the evolution is of the fused species private evolutionBgm: AnySound; private evolutionHandler: EvolutionSceneHandler; @@ -39,6 +41,7 @@ export class EvolutionPhase extends Phase { this.pokemon = pokemon; this.evolution = evolution; this.lastLevel = lastLevel; + this.fusionSpeciesEvolved = evolution instanceof FusionSpeciesFormEvolution; } validate(): boolean { @@ -261,7 +264,8 @@ export class EvolutionPhase extends Phase { this.evolutionHandler.canCancel = false; this.pokemon.evolve(this.evolution, this.pokemon.species).then(() => { - const levelMoves = this.pokemon.getLevelMoves(this.lastLevel + 1, true); + const learnSituation: LearnMoveSituation = this.fusionSpeciesEvolved ? LearnMoveSituation.EVOLUTION_FUSED : this.pokemon.fusionSpecies ? LearnMoveSituation.EVOLUTION_FUSED_BASE : LearnMoveSituation.EVOLUTION; + const levelMoves = this.pokemon.getLevelMoves(this.lastLevel + 1, true, false, false, learnSituation).filter(lm => lm[0] === EVOLVE_MOVE); for (const lm of levelMoves) { this.scene.unshiftPhase(new LearnMovePhase(this.scene, this.scene.getPlayerParty().indexOf(this.pokemon), lm[1])); } From 37b8c337e17aeb095ab25d14ec808016a06c20d7 Mon Sep 17 00:00:00 2001 From: Moka <54149968+MokaStitcher@users.noreply.github.com> Date: Sun, 1 Dec 2024 18:58:44 +0100 Subject: [PATCH 11/13] [UI] Add error message for too many eggs in starter UI (#4317) * [qol] add missing error messages in starter UI * remove locale change * revert empty party message change --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/ui/starter-select-ui-handler.ts | 32 ++++++++++++++++++----------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/src/ui/starter-select-ui-handler.ts b/src/ui/starter-select-ui-handler.ts index be78e7e1b44..691e339eafc 100644 --- a/src/ui/starter-select-ui-handler.ts +++ b/src/ui/starter-select-ui-handler.ts @@ -39,7 +39,6 @@ import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import { Button } from "#enums/buttons"; import { EggSourceType } from "#enums/egg-source-types"; -import AwaitableUiHandler from "#app/ui/awaitable-ui-handler"; import { DropDown, DropDownLabel, DropDownOption, DropDownState, DropDownType, SortCriteria } from "#app/ui/dropdown"; import { StarterContainer } from "#app/ui/starter-container"; import { DropDownColumn, FilterBar } from "#app/ui/filter-bar"; @@ -1062,15 +1061,21 @@ export default class StarterSelectUiHandler extends MessageUiHandler { } } - showText(text: string, delay?: integer, callback?: Function, callbackDelay?: integer, prompt?: boolean, promptDelay?: integer) { + showText(text: string, delay?: integer, callback?: Function, callbackDelay?: integer, prompt?: boolean, promptDelay?: integer, moveToTop?: boolean) { super.showText(text, delay, callback, callbackDelay, prompt, promptDelay); - if (text?.indexOf("\n") === -1) { - this.starterSelectMessageBox.setSize(318, 28); - this.message.setY(-22); + const singleLine = text?.indexOf("\n") === -1; + + this.starterSelectMessageBox.setSize(318, singleLine ? 28 : 42); + + if (moveToTop) { + this.starterSelectMessageBox.setOrigin(0, 0); + this.starterSelectMessageBoxContainer.setY(0); + this.message.setY(4); } else { - this.starterSelectMessageBox.setSize(318, 42); - this.message.setY(-37); + this.starterSelectMessageBoxContainer.setY(this.scene.game.canvas.height / 6); + this.starterSelectMessageBox.setOrigin(0, 1); + this.message.setY(singleLine ? -22 : -37); } this.starterSelectMessageBoxContainer.setVisible(!!text?.length); @@ -1804,8 +1809,12 @@ export default class StarterSelectUiHandler extends MessageUiHandler { options.push({ label: `x${sameSpeciesEggCost} ${i18next.t("starterSelectUiHandler:sameSpeciesEgg")}`, handler: () => { - if ((this.scene.gameData.eggs.length < 99 || Overrides.UNLIMITED_EGG_COUNT_OVERRIDE) - && (Overrides.FREE_CANDY_UPGRADE_OVERRIDE || candyCount >= sameSpeciesEggCost)) { + if (Overrides.FREE_CANDY_UPGRADE_OVERRIDE || candyCount >= sameSpeciesEggCost) { + if (this.scene.gameData.eggs.length >= 99 && !Overrides.UNLIMITED_EGG_COUNT_OVERRIDE) { + // Egg list full, show error message at the top of the screen and abort + this.showText(i18next.t("egg:tooManyEggs"), undefined, () => this.showText("", 0, () => this.tutorialActive = false), 2000, false, undefined, true); + return false; + } if (!Overrides.FREE_CANDY_UPGRADE_OVERRIDE) { starterData.candyCount -= sameSpeciesEggCost; } @@ -3565,9 +3574,8 @@ export default class StarterSelectUiHandler extends MessageUiHandler { }, cancel, null, null, 19); }); } else { - const handler = this.scene.ui.getHandler() as AwaitableUiHandler; - handler.tutorialActive = true; - this.scene.ui.showText(i18next.t("starterSelectUiHandler:invalidParty"), null, () => this.scene.ui.showText("", 0, () => handler.tutorialActive = false), null, true); + this.tutorialActive = true; + this.showText(i18next.t("starterSelectUiHandler:invalidParty"), undefined, () => this.showText("", 0, () => this.tutorialActive = false), undefined, true); } return true; } From fad29ffc5195dac3bcc038cae60f6251faf27a7e Mon Sep 17 00:00:00 2001 From: Daniel Pochert Date: Sun, 1 Dec 2024 19:07:48 +0100 Subject: [PATCH 12/13] [P1 Bug] Prevent crash from missing pokemon sprite (#4821) * catch Sprite.play() errors in egg hatch phase * catch more Sprite.play() errors causing crashes if sprite missing * Add changes suggested by PigeonBar --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> Co-authored-by: Moka <54149968+MokaStitcher@users.noreply.github.com> --- .../encounters/global-trade-system-encounter.ts | 16 ++++++++++++++-- .../utils/encounter-transformation-sequence.ts | 16 ++++++++++++++-- src/field/pokemon.ts | 12 ++++++++++-- src/phases/egg-hatch-phase.ts | 7 ++++++- src/phases/evolution-phase.ts | 16 ++++++++++++++-- src/phases/form-change-phase.ts | 8 +++++++- src/phases/quiet-form-change-phase.ts | 14 ++++++++++++-- src/ui/summary-ui-handler.ts | 8 ++++++-- 8 files changed, 83 insertions(+), 14 deletions(-) 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 934fc1b805b..fa445d75d4f 100644 --- a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts +++ b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts @@ -582,7 +582,13 @@ function doPokemonTradeSequence(scene: BattleScene, tradedPokemon: PlayerPokemon receivedPokemonTintSprite.setTintFill(getPokeballTintColor(receivedPokemon.pokeball)); [ tradedPokemonSprite, tradedPokemonTintSprite ].map(sprite => { - sprite.play(tradedPokemon.getSpriteKey(true)); + const spriteKey = tradedPokemon.getSpriteKey(true); + try { + sprite.play(spriteKey); + } catch (err: unknown) { + console.error(`Failed to play animation for ${spriteKey}`, err); + } + sprite.setPipeline(scene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], hasShadow: false, teraColor: getTypeRgb(tradedPokemon.getTeraType()) }); sprite.setPipelineData("ignoreTimeTint", true); sprite.setPipelineData("spriteKey", tradedPokemon.getSpriteKey()); @@ -597,7 +603,13 @@ function doPokemonTradeSequence(scene: BattleScene, tradedPokemon: PlayerPokemon }); [ receivedPokemonSprite, receivedPokemonTintSprite ].map(sprite => { - sprite.play(receivedPokemon.getSpriteKey(true)); + const spriteKey = receivedPokemon.getSpriteKey(true); + try { + sprite.play(spriteKey); + } catch (err: unknown) { + console.error(`Failed to play animation for ${spriteKey}`, err); + } + sprite.setPipeline(scene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], hasShadow: false, teraColor: getTypeRgb(tradedPokemon.getTeraType()) }); sprite.setPipelineData("ignoreTimeTint", true); sprite.setPipelineData("spriteKey", receivedPokemon.getSpriteKey()); diff --git a/src/data/mystery-encounters/utils/encounter-transformation-sequence.ts b/src/data/mystery-encounters/utils/encounter-transformation-sequence.ts index fcadb101817..424ba15f811 100644 --- a/src/data/mystery-encounters/utils/encounter-transformation-sequence.ts +++ b/src/data/mystery-encounters/utils/encounter-transformation-sequence.ts @@ -54,7 +54,13 @@ export function doPokemonTransformationSequence(scene: BattleScene, previousPoke pokemonEvoTintSprite.setTintFill(0xFFFFFF); [ pokemonSprite, pokemonTintSprite, pokemonEvoSprite, pokemonEvoTintSprite ].map(sprite => { - sprite.play(previousPokemon.getSpriteKey(true)); + const spriteKey = previousPokemon.getSpriteKey(true); + try { + sprite.play(spriteKey); + } catch (err: unknown) { + console.error(`Failed to play animation for ${spriteKey}`, err); + } + sprite.setPipeline(scene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], hasShadow: false, teraColor: getTypeRgb(previousPokemon.getTeraType()) }); sprite.setPipelineData("ignoreTimeTint", true); sprite.setPipelineData("spriteKey", previousPokemon.getSpriteKey()); @@ -69,7 +75,13 @@ export function doPokemonTransformationSequence(scene: BattleScene, previousPoke }); [ pokemonEvoSprite, pokemonEvoTintSprite ].map(sprite => { - sprite.play(transformPokemon.getSpriteKey(true)); + const spriteKey = transformPokemon.getSpriteKey(true); + try { + sprite.play(spriteKey); + } catch (err: unknown) { + console.error(`Failed to play animation for ${spriteKey}`, err); + } + sprite.setPipelineData("ignoreTimeTint", true); sprite.setPipelineData("spriteKey", transformPokemon.getSpriteKey()); sprite.setPipelineData("shiny", transformPokemon.shiny); diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index c22c755fc6e..6aa4bd46a68 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -3741,8 +3741,16 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { setFrameRate(frameRate: integer) { this.scene.anims.get(this.getBattleSpriteKey()).frameRate = frameRate; - this.getSprite().play(this.getBattleSpriteKey()); - this.getTintSprite()?.play(this.getBattleSpriteKey()); + try { + this.getSprite().play(this.getBattleSpriteKey()); + } catch (err: unknown) { + console.error(`Failed to play animation for ${this.getBattleSpriteKey()}`, err); + } + try { + this.getTintSprite()?.play(this.getBattleSpriteKey()); + } catch (err: unknown) { + console.error(`Failed to play animation for ${this.getBattleSpriteKey()}`, err); + } } tint(color: number, alpha?: number, duration?: integer, ease?: string) { diff --git a/src/phases/egg-hatch-phase.ts b/src/phases/egg-hatch-phase.ts index d45c580228c..803fd478fd4 100644 --- a/src/phases/egg-hatch-phase.ts +++ b/src/phases/egg-hatch-phase.ts @@ -330,7 +330,12 @@ export class EggHatchPhase extends Phase { this.scene.validateAchv(achvs.HATCH_SHINY); } this.eggContainer.setVisible(false); - this.pokemonSprite.play(this.pokemon.getSpriteKey(true)); + const spriteKey = this.pokemon.getSpriteKey(true); + try { + this.pokemonSprite.play(spriteKey); + } catch (err: unknown) { + console.error(`Failed to play animation for ${spriteKey}`, err); + } this.pokemonSprite.setPipelineData("ignoreTimeTint", true); this.pokemonSprite.setPipelineData("spriteKey", this.pokemon.getSpriteKey()); this.pokemonSprite.setPipelineData("shiny", this.pokemon.shiny); diff --git a/src/phases/evolution-phase.ts b/src/phases/evolution-phase.ts index 36bd3b7bd81..76e521c9b3d 100644 --- a/src/phases/evolution-phase.ts +++ b/src/phases/evolution-phase.ts @@ -105,7 +105,13 @@ export class EvolutionPhase extends Phase { this.scene.ui.add(this.evolutionOverlay); [ this.pokemonSprite, this.pokemonTintSprite, this.pokemonEvoSprite, this.pokemonEvoTintSprite ].map(sprite => { - sprite.play(this.pokemon.getSpriteKey(true)); + const spriteKey = this.pokemon.getSpriteKey(true); + try { + sprite.play(spriteKey); + } catch (err: unknown) { + console.error(`Failed to play animation for ${spriteKey}`, err); + } + sprite.setPipeline(this.scene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], hasShadow: false, teraColor: getTypeRgb(this.pokemon.getTeraType()) }); sprite.setPipelineData("ignoreTimeTint", true); sprite.setPipelineData("spriteKey", this.pokemon.getSpriteKey()); @@ -130,7 +136,13 @@ export class EvolutionPhase extends Phase { this.pokemon.getPossibleEvolution(this.evolution).then(evolvedPokemon => { [ this.pokemonEvoSprite, this.pokemonEvoTintSprite ].map(sprite => { - sprite.play(evolvedPokemon.getSpriteKey(true)); + const spriteKey = evolvedPokemon.getSpriteKey(true); + try { + sprite.play(spriteKey); + } catch (err: unknown) { + console.error(`Failed to play animation for ${spriteKey}`, err); + } + sprite.setPipelineData("ignoreTimeTint", true); sprite.setPipelineData("spriteKey", evolvedPokemon.getSpriteKey()); sprite.setPipelineData("shiny", evolvedPokemon.shiny); diff --git a/src/phases/form-change-phase.ts b/src/phases/form-change-phase.ts index 410163a70e4..b042cd98294 100644 --- a/src/phases/form-change-phase.ts +++ b/src/phases/form-change-phase.ts @@ -39,7 +39,13 @@ export class FormChangePhase extends EvolutionPhase { this.pokemon.getPossibleForm(this.formChange).then(transformedPokemon => { [ this.pokemonEvoSprite, this.pokemonEvoTintSprite ].map(sprite => { - sprite.play(transformedPokemon.getSpriteKey(true)); + const spriteKey = transformedPokemon.getSpriteKey(true); + try { + sprite.play(spriteKey); + } catch (err: unknown) { + console.error(`Failed to play animation for ${spriteKey}`, err); + } + sprite.setPipelineData("ignoreTimeTint", true); sprite.setPipelineData("spriteKey", transformedPokemon.getSpriteKey()); sprite.setPipelineData("shiny", transformedPokemon.shiny); diff --git a/src/phases/quiet-form-change-phase.ts b/src/phases/quiet-form-change-phase.ts index 6c84c0d1a8a..c9e5bec845d 100644 --- a/src/phases/quiet-form-change-phase.ts +++ b/src/phases/quiet-form-change-phase.ts @@ -43,7 +43,12 @@ export class QuietFormChangePhase extends BattlePhase { const getPokemonSprite = () => { const sprite = this.scene.addPokemonSprite(this.pokemon, this.pokemon.x + this.pokemon.getSprite().x, this.pokemon.y + this.pokemon.getSprite().y, "pkmn__sub"); sprite.setOrigin(0.5, 1); - sprite.play(this.pokemon.getBattleSpriteKey()).stop(); + const spriteKey = this.pokemon.getBattleSpriteKey(); + try { + sprite.play(spriteKey).stop(); + } catch (err: unknown) { + console.error(`Failed to play animation for ${spriteKey}`, err); + } sprite.setPipeline(this.scene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], hasShadow: false, teraColor: getTypeRgb(this.pokemon.getTeraType()) }); [ "spriteColors", "fusionSpriteColors" ].map(k => { if (this.pokemon.summonData?.speciesForm) { @@ -81,7 +86,12 @@ export class QuietFormChangePhase extends BattlePhase { this.pokemon.setVisible(false); this.pokemon.changeForm(this.formChange).then(() => { pokemonFormTintSprite.setScale(0.01); - pokemonFormTintSprite.play(this.pokemon.getBattleSpriteKey()).stop(); + const spriteKey = this.pokemon.getBattleSpriteKey(); + try { + pokemonFormTintSprite.play(spriteKey).stop(); + } catch (err: unknown) { + console.error(`Failed to play animation for ${spriteKey}`, err); + } pokemonFormTintSprite.setVisible(true); this.scene.tweens.add({ targets: pokemonTintSprite, diff --git a/src/ui/summary-ui-handler.ts b/src/ui/summary-ui-handler.ts index 2e05f3de4c1..63ef6155fbc 100644 --- a/src/ui/summary-ui-handler.ts +++ b/src/ui/summary-ui-handler.ts @@ -321,8 +321,12 @@ export default class SummaryUiHandler extends UiHandler { this.numberText.setText(Utils.padInt(this.pokemon.species.speciesId, 4)); this.numberText.setColor(this.getTextColor(!this.pokemon.isShiny() ? TextStyle.SUMMARY : TextStyle.SUMMARY_GOLD)); this.numberText.setShadowColor(this.getTextColor(!this.pokemon.isShiny() ? TextStyle.SUMMARY : TextStyle.SUMMARY_GOLD, true)); - - this.pokemonSprite.play(this.pokemon.getSpriteKey(true)); + const spriteKey = this.pokemon.getSpriteKey(true); + try { + this.pokemonSprite.play(spriteKey); + } catch (err: unknown) { + console.error(`Failed to play animation for ${spriteKey}`, err); + } this.pokemonSprite.setPipelineData("teraColor", getTypeRgb(this.pokemon.getTeraType())); this.pokemonSprite.setPipelineData("ignoreTimeTint", true); this.pokemonSprite.setPipelineData("spriteKey", this.pokemon.getSpriteKey()); From 2b59a532856abe6490902158990f5a099984082a Mon Sep 17 00:00:00 2001 From: Mumble <171087428+frutescens@users.noreply.github.com> Date: Sun, 1 Dec 2024 10:48:43 -0800 Subject: [PATCH 13/13] [Bug][UI] Fixes to Run History Logging (#4716) * new function * weak attempt of documentation * mysterious chest handled...? maybe. * override comments * one more instruction * fixing up the logging lol * lah * fixing it up * coommrent * lalal * run info fixes * Addressed PigeonBar's comments * Centered run info text + fixed trainer sprites. * Fixed function name. * Update tsdoc in `overrides.ts` Co-authored-by: Moka <54149968+MokaStitcher@users.noreply.github.com> * Fix tsdoc comment * sligthly rewrite centering of biome and wave text in run info --------- Co-authored-by: frutescens Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> Co-authored-by: Moka <54149968+MokaStitcher@users.noreply.github.com> Co-authored-by: Moka --- .../encounters/mysterious-chest-encounter.ts | 1 + .../utils/encounter-pokemon-utils.ts | 5 ++- src/overrides.ts | 6 ++- src/phases/game-over-phase.ts | 44 +++++++++++++++++-- src/ui/run-info-ui-handler.ts | 25 +++++++---- 5 files changed, 68 insertions(+), 13 deletions(-) diff --git a/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts b/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts index ab6517e97af..877deee66b7 100644 --- a/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts +++ b/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts @@ -34,6 +34,7 @@ export const MysteriousChestEncounter: MysteryEncounter = MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.MYSTERIOUS_CHEST) .withEncounterTier(MysteryEncounterTier.COMMON) .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + .withScenePartySizeRequirement(2, 6, true) .withAutoHideIntroVisuals(false) .withCatchAllowed(true) .withIntroSpriteConfigs([ diff --git a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts index 7adcfdc4974..072b5e5b160 100644 --- a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts @@ -290,7 +290,10 @@ export function applyDamageToPokemon(scene: BattleScene, pokemon: PlayerPokemon, if (damage <= 0) { console.warn("Healing pokemon with `applyDamageToPokemon` is not recommended! Please use `applyHealToPokemon` instead."); } - + // If a Pokemon would faint from the damage applied, its HP is instead set to 1. + if (pokemon.isAllowedInBattle() && pokemon.hp - damage <= 0) { + damage = pokemon.hp - 1; + } applyHpChangeToPokemon(scene, pokemon, -damage); } diff --git a/src/overrides.ts b/src/overrides.ts index dc166307314..85be47d95cc 100644 --- a/src/overrides.ts +++ b/src/overrides.ts @@ -177,7 +177,11 @@ class DefaultOverrides { // MYSTERY ENCOUNTER OVERRIDES // ------------------------- - /** 1 to 256, set to null to ignore */ + /** + * `1` (almost never) to `256` (always), set to `null` to disable the override + * + * Note: Make sure `STARTING_WAVE_OVERRIDE > 10`, otherwise MEs won't trigger + */ readonly MYSTERY_ENCOUNTER_RATE_OVERRIDE: number | null = null; readonly MYSTERY_ENCOUNTER_TIER_OVERRIDE: MysteryEncounterTier | null = null; readonly MYSTERY_ENCOUNTER_OVERRIDE: MysteryEncounterType | null = null; diff --git a/src/phases/game-over-phase.ts b/src/phases/game-over-phase.ts index 84a4a4e8ef9..52d0996b946 100644 --- a/src/phases/game-over-phase.ts +++ b/src/phases/game-over-phase.ts @@ -23,6 +23,12 @@ import * as Utils from "#app/utils"; import { PlayerGender } from "#enums/player-gender"; import { TrainerType } from "#enums/trainer-type"; import i18next from "i18next"; +import { SessionSaveData } from "#app/system/game-data"; +import PersistentModifierData from "#app/system/modifier-data"; +import PokemonData from "#app/system/pokemon-data"; +import ChallengeData from "#app/system/challenge-data"; +import TrainerData from "#app/system/trainer-data"; +import ArenaData from "#app/system/arena-data"; import { pokerogueApi } from "#app/plugins/api/pokerogue-api"; export class GameOverPhase extends BattlePhase { @@ -109,7 +115,7 @@ export class GameOverPhase extends BattlePhase { this.scene.gameData.gameStats.dailyRunSessionsWon++; } } - this.scene.gameData.saveRunHistory(this.scene, this.scene.gameData.getSessionSaveData(this.scene), this.isVictory); + const fadeDuration = this.isVictory ? 10000 : 5000; this.scene.fadeOutBgm(fadeDuration, true); const activeBattlers = this.scene.getField().filter(p => p?.isActive(true)); @@ -135,8 +141,11 @@ export class GameOverPhase extends BattlePhase { this.scene.unshiftPhase(new GameOverModifierRewardPhase(this.scene, modifierTypes.VOUCHER_PREMIUM)); } } - this.scene.pushPhase(new PostGameOverPhase(this.scene, endCardPhase)); - this.end(); + this.getRunHistoryEntry().then(runHistoryEntry => { + this.scene.gameData.saveRunHistory(this.scene, runHistoryEntry, this.isVictory); + this.scene.pushPhase(new PostGameOverPhase(this.scene, endCardPhase)); + this.end(); + }); }; if (this.isVictory && this.scene.gameMode.isClassic) { @@ -212,5 +221,34 @@ export class GameOverPhase extends BattlePhase { this.firstRibbons.push(getPokemonSpecies(pokemon.species.getRootSpeciesId(forStarter))); } } + + /** + * Slightly modified version of {@linkcode GameData.getSessionSaveData}. + * @returns A promise containing the {@linkcode SessionSaveData} + */ + private async getRunHistoryEntry(): Promise { + const preWaveSessionData = await this.scene.gameData.getSession(this.scene.sessionSlotId); + return { + seed: this.scene.seed, + playTime: this.scene.sessionPlayTime, + gameMode: this.scene.gameMode.modeId, + party: this.scene.getPlayerParty().map(p => new PokemonData(p)), + enemyParty: this.scene.getEnemyParty().map(p => new PokemonData(p)), + modifiers: preWaveSessionData ? preWaveSessionData.modifiers : this.scene.findModifiers(() => true).map(m => new PersistentModifierData(m, true)), + enemyModifiers: preWaveSessionData ? preWaveSessionData.enemyModifiers : this.scene.findModifiers(() => true, false).map(m => new PersistentModifierData(m, false)), + arena: new ArenaData(this.scene.arena), + pokeballCounts: this.scene.pokeballCounts, + money: Math.floor(this.scene.money), + score: this.scene.score, + waveIndex: this.scene.currentBattle.waveIndex, + battleType: this.scene.currentBattle.battleType, + trainer: this.scene.currentBattle.trainer ? new TrainerData(this.scene.currentBattle.trainer) : null, + gameVersion: this.scene.game.config.gameVersion, + timestamp: new Date().getTime(), + challenges: this.scene.gameMode.challenges.map(c => new ChallengeData(c)), + mysteryEncounterType: this.scene.currentBattle.mysteryEncounter?.encounterType ?? -1, + mysteryEncounterSaveData: this.scene.mysteryEncounterSaveData + } as SessionSaveData; + } } diff --git a/src/ui/run-info-ui-handler.ts b/src/ui/run-info-ui-handler.ts index ace9d956dd9..071690aee54 100644 --- a/src/ui/run-info-ui-handler.ts +++ b/src/ui/run-info-ui-handler.ts @@ -118,6 +118,7 @@ export default class RunInfoUiHandler extends UiHandler { this.runResultContainer = this.scene.add.container(0, 24); const runResultWindow = addWindow(this.scene, 0, 0, this.statsBgWidth - 11, 65); runResultWindow.setOrigin(0, 0); + runResultWindow.setName("Run_Result_Window"); this.runResultContainer.add(runResultWindow); if (this.runDisplayMode === RunDisplayMode.RUN_HISTORY) { this.parseRunResult(); @@ -254,8 +255,6 @@ export default class RunInfoUiHandler extends UiHandler { * Mystery Encounters contain sprites associated with MEs + the title of the specific ME. */ private parseRunStatus() { - const runStatusText = addTextObject(this.scene, 6, 5, `${i18next.t("saveSlotSelectUiHandler:wave")} ${this.runInfo.waveIndex} - ${getBiomeName(this.runInfo.arena.biome)}`, TextStyle.WINDOW, { fontSize : "65px", lineSpacing: 0.1 }); - const enemyContainer = this.scene.add.container(0, 0); this.runResultContainer.add(enemyContainer); if (this.runInfo.battleType === BattleType.WILD) { @@ -271,7 +270,7 @@ export default class RunInfoUiHandler extends UiHandler { const pokeball = this.scene.add.sprite(0, 0, "pb"); pokeball.setFrame(getPokeballAtlasKey(p.pokeball)); pokeball.setScale(0.5); - pokeball.setPosition(52 + ((i % row_limit) * 8), (i <= 2) ? 18 : 25); + pokeball.setPosition(58 + ((i % row_limit) * 8), (i <= 2) ? 18 : 25); enemyContainer.add(pokeball); }); const trainerObj = this.runInfo.trainer.toTrainer(this.scene); @@ -286,7 +285,7 @@ export default class RunInfoUiHandler extends UiHandler { const descContainer = this.scene.add.container(0, 0); const textBox = addTextObject(this.scene, 0, 0, boxString, TextStyle.WINDOW, { fontSize : "35px", wordWrap: { width: 200 }}); descContainer.add(textBox); - descContainer.setPosition(52, 29); + descContainer.setPosition(55, 32); this.runResultContainer.add(descContainer); } else if (this.runInfo.battleType === BattleType.MYSTERY_ENCOUNTER) { const encounterExclaim = this.scene.add.sprite(0, 0, "encounter_exclaim"); @@ -303,7 +302,17 @@ export default class RunInfoUiHandler extends UiHandler { this.runResultContainer.add([ encounterExclaim, subSprite, descContainer ]); } - this.runResultContainer.add(runStatusText); + const runResultWindow = this.runResultContainer.getByName("Run_Result_Window") as Phaser.GameObjects.Image; + const windowCenterX = runResultWindow.getTopCenter().x; + const windowBottomY = runResultWindow.getBottomCenter().y; + + const runStatusText = addTextObject(this.scene, windowCenterX, 5, `${i18next.t("saveSlotSelectUiHandler:wave")} ${this.runInfo.waveIndex}`, TextStyle.WINDOW, { fontSize : "60px", lineSpacing: 0.1 }); + runStatusText.setOrigin(0.5, 0); + + const currentBiomeText = addTextObject(this.scene, windowCenterX, windowBottomY - 5, `${getBiomeName(this.runInfo.arena.biome)}`, TextStyle.WINDOW, { fontSize: "60px" }); + currentBiomeText.setOrigin(0.5, 1); + + this.runResultContainer.add([ runStatusText, currentBiomeText ]); this.runContainer.add(this.runResultContainer); } @@ -387,12 +396,12 @@ export default class RunInfoUiHandler extends UiHandler { tObjSprite.setPosition(-9, -3); tObjPartnerSprite.setScale(0.55); doubleContainer.add([ tObjSprite, tObjPartnerSprite ]); - doubleContainer.setPosition(28, 40); + doubleContainer.setPosition(28, 34); } enemyContainer.add(doubleContainer); } else { - const scale = (this.runDisplayMode === RunDisplayMode.RUN_HISTORY) ? 0.35 : 0.65; - const position = (this.runDisplayMode === RunDisplayMode.RUN_HISTORY) ? [ 12, 28 ] : [ 32, 36 ]; + const scale = (this.runDisplayMode === RunDisplayMode.RUN_HISTORY) ? 0.35 : 0.55; + const position = (this.runDisplayMode === RunDisplayMode.RUN_HISTORY) ? [ 12, 28 ] : [ 30, 32 ]; tObjSprite.setScale(scale, scale); tObjSprite.setPosition(position[0], position[1]); enemyContainer.add(tObjSprite);