From de4ec5e1cb2d550b9ee9c8803faab4c6fbe552d5 Mon Sep 17 00:00:00 2001 From: AJ Fontaine <36677462+Fontbane@users.noreply.github.com> Date: Sat, 9 Nov 2024 01:38:45 -0500 Subject: [PATCH 01/37] [Feature] [Item] Add Catching Charm item (#4811) * Add catching charm item * Add Catching Charm item * Disable catching charm in item pool when dex isn't full enough * Replace catching charm icon --------- Co-authored-by: Madmadness65 <59298170+Madmadness65@users.noreply.github.com> --- public/images/items.png | Bin 58629 -> 59627 bytes public/images/items/catching_charm.png | Bin 454 -> 322 bytes src/data/pokeball.ts | 3 ++- src/modifier/modifier-type.ts | 4 +++- src/modifier/modifier.ts | 32 +++++++++++++++++++++++++ 5 files changed, 37 insertions(+), 2 deletions(-) diff --git a/public/images/items.png b/public/images/items.png index cb4f8fa7d065f53b4861bc32eb0867b9eda94b5d..191766f520e852149c71b728781f7feb401f4514 100644 GIT binary patch literal 59627 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 X1m@~7!j6GBz~zy~6ZIN3n^*q_+l|+J delta 58282 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
RY4!*yekQ&gfCG%r@kb`(v^8OaANm<<#%b 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
  • _n(YR4Gokl~ClQ$4;Z#bnwLvU#*=}H89kgbNdPS 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 diff --git a/public/images/items/catching_charm.png b/public/images/items/catching_charm.png index 9d72fe465e3f1322e38b724e6ec333797e79f5fb..c220ff70c03408c81c400f715fa3d94f0da43788 100644 GIT binary patch delta 306 zcmX@ce28g+WIZzj1B1(wu46!ou{g-xiDBJ2nU_G0L4Z$)E09)DP>?F93dzea&lPcwu{-CPI3BF6xtBu@a z%3Bzf_z&qe%u()^7iN-kO;Y%Kk&*Am+o3K{4mBP#dL*U&?A(Ft`Vz;ZO3GEg+&jN{&7wEltcE*VLSL({Q8KJMeW#*{SzFcr lfYx20yZBNgJkxxA8MJ_G4j=}BOTm*tl&7no%Q~loCIAiueS!c0 delta 439 zcmV;o0Z9JB0>%T78Gi-<0047(dh`GQ0gXvSK~z}7?Uubu#4s4f`zLf4hrlTYI@tXd zR~MmUM^_h5C{Aupy83}|c)N&;gNq0v=W^ia=%{1I4i2w*18+%ly*Af0J%<(^C{4-p z%lkz<0Pt3+m74be{~Mt0q5t@RgD!oa)&b}rf13llJuMhUCw~ce!w_)ISp>*DP7Yzw zNpZ~t!Wuw@nA<}{!WjTTkT4Zx>CkJU^&-@ZQB4g3IRmJIQ6tg`KpBXMd8;YVQ{fDt z5b8h}Z54I!kV%=CFAboc63ver_aR`W+O#Ivv5 zm{Nk#K|pJOsvd{mRGK4%3V?-4=GFjZ7mf8Z5#E47G)rOf_ykO8M=VV24z>O40p7;B z&>XdMbtfU1GHB#L8t}q5YXCn8AwEiA?fgb6hcOwihA?BhwgwPl^K~Mn-}jbz8Us{a hG&=$4LOnna@CPb(llSSAz !!d.caughtAttr); const catchingCharmMultiplier = new NumberHolder(1); - //scene.findModifier(m => m instanceof CriticalCatchChanceBoosterModifier)?.apply(catchingCharmMultiplier); + scene.findModifier(m => m instanceof CriticalCatchChanceBoosterModifier)?.apply(catchingCharmMultiplier); const dexMultiplier = scene.gameMode.isDaily || dexCount > 800 ? 2.5 : dexCount > 600 ? 2 : dexCount > 400 ? 1.5 diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index 571c54d76e93..3e459f2dc748 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -10,7 +10,7 @@ import { getStatusEffectDescriptor } from "#app/data/status-effect"; import { Type } from "#enums/type"; import Pokemon, { EnemyPokemon, PlayerPokemon, PokemonMove } from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; -import { AddPokeballModifier, AddVoucherModifier, AttackTypeBoosterModifier, BaseStatModifier, BerryModifier, BoostBugSpawnModifier, BypassSpeedChanceModifier, ContactHeldItemTransferChanceModifier, CritBoosterModifier, DamageMoneyRewardModifier, DoubleBattleChanceBoosterModifier, EnemyAttackStatusEffectChanceModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, EnemyStatusEffectHealChanceModifier, EnemyTurnHealModifier, EvolutionItemModifier, EvolutionStatBoosterModifier, EvoTrackerModifier, ExpBalanceModifier, ExpBoosterModifier, ExpShareModifier, ExtraModifierModifier, FlinchChanceModifier, FusePokemonModifier, GigantamaxAccessModifier, HealingBoosterModifier, HealShopCostModifier, HiddenAbilityRateBoosterModifier, HitHealModifier, IvScannerModifier, LevelIncrementBoosterModifier, LockModifierTiersModifier, MapModifier, MegaEvolutionAccessModifier, MoneyInterestModifier, MoneyMultiplierModifier, MoneyRewardModifier, MultipleParticipantExpBonusModifier, PokemonAllMovePpRestoreModifier, PokemonBaseStatFlatModifier, PokemonBaseStatTotalModifier, PokemonExpBoosterModifier, PokemonFormChangeItemModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonHpRestoreModifier, PokemonIncrementingStatModifier, PokemonInstantReviveModifier, PokemonLevelIncrementModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, PokemonNatureChangeModifier, PokemonNatureWeightModifier, PokemonPpRestoreModifier, PokemonPpUpModifier, PokemonStatusHealModifier, PreserveBerryModifier, RememberMoveModifier, ResetNegativeStatStageModifier, ShinyRateBoosterModifier, SpeciesCritBoosterModifier, SpeciesStatBoosterModifier, SurviveDamageModifier, SwitchEffectTransferModifier, TempCritBoosterModifier, TempStatStageBoosterModifier, TerastallizeAccessModifier, TerastallizeModifier, TmModifier, TurnHealModifier, TurnHeldItemTransferModifier, TurnStatusEffectModifier, type EnemyPersistentModifier, type Modifier, type PersistentModifier, TempExtraModifierModifier } from "#app/modifier/modifier"; +import { AddPokeballModifier, AddVoucherModifier, AttackTypeBoosterModifier, BaseStatModifier, BerryModifier, BoostBugSpawnModifier, BypassSpeedChanceModifier, ContactHeldItemTransferChanceModifier, CritBoosterModifier, DamageMoneyRewardModifier, DoubleBattleChanceBoosterModifier, EnemyAttackStatusEffectChanceModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, EnemyStatusEffectHealChanceModifier, EnemyTurnHealModifier, EvolutionItemModifier, EvolutionStatBoosterModifier, EvoTrackerModifier, ExpBalanceModifier, ExpBoosterModifier, ExpShareModifier, ExtraModifierModifier, FlinchChanceModifier, FusePokemonModifier, GigantamaxAccessModifier, HealingBoosterModifier, HealShopCostModifier, HiddenAbilityRateBoosterModifier, HitHealModifier, IvScannerModifier, LevelIncrementBoosterModifier, LockModifierTiersModifier, MapModifier, MegaEvolutionAccessModifier, MoneyInterestModifier, MoneyMultiplierModifier, MoneyRewardModifier, MultipleParticipantExpBonusModifier, PokemonAllMovePpRestoreModifier, PokemonBaseStatFlatModifier, PokemonBaseStatTotalModifier, PokemonExpBoosterModifier, PokemonFormChangeItemModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonHpRestoreModifier, PokemonIncrementingStatModifier, PokemonInstantReviveModifier, PokemonLevelIncrementModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, PokemonNatureChangeModifier, PokemonNatureWeightModifier, PokemonPpRestoreModifier, PokemonPpUpModifier, PokemonStatusHealModifier, PreserveBerryModifier, RememberMoveModifier, ResetNegativeStatStageModifier, ShinyRateBoosterModifier, SpeciesCritBoosterModifier, SpeciesStatBoosterModifier, SurviveDamageModifier, SwitchEffectTransferModifier, TempCritBoosterModifier, TempStatStageBoosterModifier, TerastallizeAccessModifier, TerastallizeModifier, TmModifier, TurnHealModifier, TurnHeldItemTransferModifier, TurnStatusEffectModifier, type EnemyPersistentModifier, type Modifier, type PersistentModifier, TempExtraModifierModifier, CriticalCatchChanceBoosterModifier } from "#app/modifier/modifier"; import { ModifierTier } from "#app/modifier/modifier-tier"; import Overrides from "#app/overrides"; import { Unlockables } from "#app/system/unlockables"; @@ -1554,6 +1554,7 @@ export const modifierTypes = { SHINY_CHARM: () => new ModifierType("modifierType:ModifierType.SHINY_CHARM", "shiny_charm", (type, _args) => new ShinyRateBoosterModifier(type)), ABILITY_CHARM: () => new ModifierType("modifierType:ModifierType.ABILITY_CHARM", "ability_charm", (type, _args) => new HiddenAbilityRateBoosterModifier(type)), + CATCHING_CHARM: () => new ModifierType("modifierType:ModifierType.CATCHING_CHARM", "catching_charm", (type, _args) => new CriticalCatchChanceBoosterModifier(type)), IV_SCANNER: () => new ModifierType("modifierType:ModifierType.IV_SCANNER", "scanner", (type, _args) => new IvScannerModifier(type)), @@ -1791,6 +1792,7 @@ const modifierPool: ModifierPool = { new WeightedModifierType(modifierTypes.BATON, 2), 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, (party: Pokemon[]) => party[0].scene.eventManager.isEventActive() ? 0 : 4), new WeightedModifierType(modifierTypes.ABILITY_CHARM, skipInClassicAfterWave(189, 6)), new WeightedModifierType(modifierTypes.FOCUS_BAND, 5), diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index d2965247826e..9e97c8667188 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -2957,6 +2957,38 @@ export class ShinyRateBoosterModifier extends PersistentModifier { } } +export class CriticalCatchChanceBoosterModifier extends PersistentModifier { + constructor(type: ModifierType, stackCount?: number) { + super(type, stackCount); + } + + match(modifier: Modifier): boolean { + return modifier instanceof CriticalCatchChanceBoosterModifier; + } + + clone(): CriticalCatchChanceBoosterModifier { + return new CriticalCatchChanceBoosterModifier(this.type, this.stackCount); + } + + /** + * Applies {@linkcode CriticalCatchChanceBoosterModifier} + * @param boost {@linkcode NumberHolder} holding the boost value + * @returns always `true` + */ + override apply(boost: NumberHolder): boolean { + // 1 stack: 2x + // 2 stack: 2.5x + // 3 stack: 3x + boost.value *= 1.5 + this.getStackCount() / 2; + + return true; + } + + getMaxStackCount(scene: BattleScene): number { + return 3; + } +} + export class LockModifierTiersModifier extends PersistentModifier { constructor(type: ModifierType, stackCount?: number) { super(type, stackCount); From c6cc187c96e0543a3d7c2b446890e854529f5b57 Mon Sep 17 00:00:00 2001 From: AJ Fontaine <36677462+Fontbane@users.noreply.github.com> Date: Sat, 9 Nov 2024 04:10:49 -0500 Subject: [PATCH 02/37] [Balance] Modify potion and ether weight funcs (#4829) * Adjust for low HP mons --- src/modifier/modifier-type.ts | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index 3e459f2dc748..ae1aef3ff884 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -1621,19 +1621,21 @@ const modifierPool: ModifierPool = { new WeightedModifierType(modifierTypes.POKEBALL, (party: Pokemon[]) => (hasMaximumBalls(party, PokeballType.POKEBALL)) ? 0 : 6, 6), new WeightedModifierType(modifierTypes.RARE_CANDY, 2), new WeightedModifierType(modifierTypes.POTION, (party: Pokemon[]) => { - const thresholdPartyMemberCount = Math.min(party.filter(p => (p.getInverseHp() >= 10 || p.getHpRatio() <= 0.875) && !p.isFainted()).length, 3); + const thresholdPartyMemberCount = Math.min(party.filter(p => (p.getInverseHp() >= 10 && p.getHpRatio() <= 0.875) && !p.isFainted()).length, 3); return thresholdPartyMemberCount * 3; }, 9), new WeightedModifierType(modifierTypes.SUPER_POTION, (party: Pokemon[]) => { - const thresholdPartyMemberCount = Math.min(party.filter(p => (p.getInverseHp() >= 25 || p.getHpRatio() <= 0.75) && !p.isFainted()).length, 3); + const thresholdPartyMemberCount = Math.min(party.filter(p => (p.getInverseHp() >= 25 && p.getHpRatio() <= 0.75) && !p.isFainted()).length, 3); return thresholdPartyMemberCount; }, 3), new WeightedModifierType(modifierTypes.ETHER, (party: Pokemon[]) => { - const thresholdPartyMemberCount = Math.min(party.filter(p => p.hp && p.getMoveset().filter(m => m?.ppUsed && (m.getMovePp() - m.ppUsed) <= 5 && m.ppUsed >= Math.floor(m.getMovePp() / 2)).length).length, 3); + const thresholdPartyMemberCount = Math.min(party.filter(p => p.hp && !p.getHeldItems().some(m => m instanceof BerryModifier && m.berryType === BerryType.LEPPA) + && p.getMoveset().filter(m => m?.ppUsed && (m.getMovePp() - m.ppUsed) <= 5 && m.ppUsed > Math.floor(m.getMovePp() / 2)).length).length, 3); return thresholdPartyMemberCount * 3; }, 9), new WeightedModifierType(modifierTypes.MAX_ETHER, (party: Pokemon[]) => { - const thresholdPartyMemberCount = Math.min(party.filter(p => p.hp && p.getMoveset().filter(m => m?.ppUsed && (m.getMovePp() - m.ppUsed) <= 5 && m.ppUsed >= Math.floor(m.getMovePp() / 2)).length).length, 3); + const thresholdPartyMemberCount = Math.min(party.filter(p => p.hp && !p.getHeldItems().some(m => m instanceof BerryModifier && m.berryType === BerryType.LEPPA) + && p.getMoveset().filter(m => m?.ppUsed && (m.getMovePp() - m.ppUsed) <= 5 && m.ppUsed > Math.floor(m.getMovePp() / 2)).length).length, 3); return thresholdPartyMemberCount; }, 3), new WeightedModifierType(modifierTypes.LURE, lureWeightFunc(10, 2)), @@ -1667,11 +1669,11 @@ const modifierPool: ModifierPool = { return party.filter(p => p.isFainted()).length >= Math.ceil(party.length / 2) ? 1 : 0; }, 1), new WeightedModifierType(modifierTypes.HYPER_POTION, (party: Pokemon[]) => { - const thresholdPartyMemberCount = Math.min(party.filter(p => (p.getInverseHp() >= 100 || p.getHpRatio() <= 0.625) && !p.isFainted()).length, 3); + const thresholdPartyMemberCount = Math.min(party.filter(p => (p.getInverseHp() >= 100 && p.getHpRatio() <= 0.625) && !p.isFainted()).length, 3); return thresholdPartyMemberCount * 3; }, 9), new WeightedModifierType(modifierTypes.MAX_POTION, (party: Pokemon[]) => { - const thresholdPartyMemberCount = Math.min(party.filter(p => (p.getInverseHp() >= 150 || p.getHpRatio() <= 0.5) && !p.isFainted()).length, 3); + const thresholdPartyMemberCount = Math.min(party.filter(p => (p.getInverseHp() >= 100 && p.getHpRatio() <= 0.5) && !p.isFainted()).length, 3); return thresholdPartyMemberCount; }, 3), new WeightedModifierType(modifierTypes.FULL_RESTORE, (party: Pokemon[]) => { @@ -1681,15 +1683,17 @@ const modifierPool: ModifierPool = { } return false; })).length, 3); - const thresholdPartyMemberCount = Math.floor((Math.min(party.filter(p => (p.getInverseHp() >= 150 || p.getHpRatio() <= 0.5) && !p.isFainted()).length, 3) + statusEffectPartyMemberCount) / 2); + const thresholdPartyMemberCount = Math.floor((Math.min(party.filter(p => (p.getInverseHp() >= 100 && p.getHpRatio() <= 0.5) && !p.isFainted()).length, 3) + statusEffectPartyMemberCount) / 2); return thresholdPartyMemberCount; }, 3), new WeightedModifierType(modifierTypes.ELIXIR, (party: Pokemon[]) => { - const thresholdPartyMemberCount = Math.min(party.filter(p => p.hp && p.getMoveset().filter(m => m?.ppUsed && (m.getMovePp() - m.ppUsed) <= 5 && m.ppUsed >= Math.floor(m.getMovePp() / 2)).length).length, 3); + const thresholdPartyMemberCount = Math.min(party.filter(p => p.hp && !p.getHeldItems().some(m => m instanceof BerryModifier && m.berryType === BerryType.LEPPA) + && p.getMoveset().filter(m => m?.ppUsed && (m.getMovePp() - m.ppUsed) <= 5 && m.ppUsed > Math.floor(m.getMovePp() / 2)).length).length, 3); return thresholdPartyMemberCount * 3; }, 9), new WeightedModifierType(modifierTypes.MAX_ELIXIR, (party: Pokemon[]) => { - const thresholdPartyMemberCount = Math.min(party.filter(p => p.hp && p.getMoveset().filter(m => m?.ppUsed && (m.getMovePp() - m.ppUsed) <= 5 && m.ppUsed >= Math.floor(m.getMovePp() / 2)).length).length, 3); + const thresholdPartyMemberCount = Math.min(party.filter(p => p.hp && !p.getHeldItems().some(m => m instanceof BerryModifier && m.berryType === BerryType.LEPPA) + && p.getMoveset().filter(m => m?.ppUsed && (m.getMovePp() - m.ppUsed) <= 5 && m.ppUsed > Math.floor(m.getMovePp() / 2)).length).length, 3); return thresholdPartyMemberCount; }, 3), new WeightedModifierType(modifierTypes.DIRE_HIT, 4), From c54d21c313a6f3aafff9849b12ca772672689f09 Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Sat, 9 Nov 2024 10:12:22 -0800 Subject: [PATCH 03/37] [Test] Fix flaky Wimp Out test (#4830) --- src/test/abilities/wimp_out.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/test/abilities/wimp_out.test.ts b/src/test/abilities/wimp_out.test.ts index 6f56a2f4e7e3..df965fc340d9 100644 --- a/src/test/abilities/wimp_out.test.ts +++ b/src/test/abilities/wimp_out.test.ts @@ -296,7 +296,9 @@ describe("Abilities - Wimp Out", () => { Species.TYRUNT ]); - game.move.select(Moves.SPLASH); + game.scene.getPlayerPokemon()!.hp *= 0.51; + + game.move.select(Moves.ENDURE); await game.phaseInterceptor.to("TurnEndPhase"); confirmNoSwitch(); From 329e43ad48519486125a54e534b7b6e01d4cac05 Mon Sep 17 00:00:00 2001 From: innerthunder <168692175+innerthunder@users.noreply.github.com> Date: Sat, 9 Nov 2024 10:13:12 -0800 Subject: [PATCH 04/37] [P2] Removed incorrect calls to `resetBattleData` on switchout (#4828) --- src/field/pokemon.ts | 3 +-- src/phases/switch-summon-phase.ts | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index e0b7bf1094f1..5478a6e5aaa4 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -4029,8 +4029,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.resetTurnData(); if (clearEffects) { this.destroySubstitute(); - this.resetSummonData(); - this.resetBattleData(); + this.resetSummonData(); // this also calls `resetBattleSummonData` } if (hideInfo) { this.hideInfo(); diff --git a/src/phases/switch-summon-phase.ts b/src/phases/switch-summon-phase.ts index 36db8b7a7e70..51d54315165b 100644 --- a/src/phases/switch-summon-phase.ts +++ b/src/phases/switch-summon-phase.ts @@ -138,7 +138,6 @@ export class SwitchSummonPhase extends SummonPhase { switchedInPokemon.setAlpha(0.5); } } else { - switchedInPokemon.resetBattleData(); switchedInPokemon.resetSummonData(); } this.summon(); From a763cd173dbdf193d26c3ace9c569c8a62713b7f Mon Sep 17 00:00:00 2001 From: innerthunder <168692175+innerthunder@users.noreply.github.com> Date: Sat, 9 Nov 2024 10:14:11 -0800 Subject: [PATCH 05/37] [Beta][P1-3] Fix Commander implementation bugs (#4826) --- src/battle-scene.ts | 18 +++++++++++------- src/field/pokemon.ts | 14 ++++++++++++-- src/phases/check-switch-phase.ts | 22 +++++++++++++--------- src/phases/encounter-phase.ts | 2 -- 4 files changed, 36 insertions(+), 20 deletions(-) diff --git a/src/battle-scene.ts b/src/battle-scene.ts index ed8a79125bc1..c5acadc8eb61 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -774,7 +774,7 @@ export default class BattleScene extends SceneBase { /** * @returns An array of {@linkcode PlayerPokemon} filtered from the player's party - * that are {@linkcode PlayerPokemon.isAllowedInBattle | allowed in battle}. + * that are {@linkcode Pokemon.isAllowedInBattle | allowed in battle}. */ public getPokemonAllowedInBattle(): PlayerPokemon[] { return this.getPlayerParty().filter(p => p.isAllowedInBattle()); @@ -1243,23 +1243,27 @@ export default class BattleScene extends SceneBase { const lastBattle = this.currentBattle; - if (lastBattle?.double && !newDouble) { - this.tryRemovePhase(p => p instanceof SwitchPhase); - } - const maxExpLevel = this.getMaxExpLevel(); this.lastEnemyTrainer = lastBattle?.trainer ?? null; this.lastMysteryEncounter = lastBattle?.mysteryEncounter; + if (newBattleType === BattleType.MYSTERY_ENCOUNTER) { + // Disable double battle on mystery encounters (it may be re-enabled as part of encounter) + newDouble = false; + } + + if (lastBattle?.double && !newDouble) { + this.tryRemovePhase(p => p instanceof SwitchPhase); + this.getPlayerField().forEach(p => p.lapseTag(BattlerTagType.COMMANDED)); + } + this.executeWithSeedOffset(() => { this.currentBattle = new Battle(this.gameMode, newWaveIndex, newBattleType, newTrainer, newDouble); }, newWaveIndex << 3, this.waveSeed); this.currentBattle.incrementTurn(this); if (newBattleType === BattleType.MYSTERY_ENCOUNTER) { - // Disable double battle on mystery encounters (it may be re-enabled as part of encounter) - this.currentBattle.double = false; // Will generate the actual Mystery Encounter during NextEncounterPhase, to ensure it uses proper biome this.currentBattle.mysteryEncounterType = mysteryEncounterType; } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 5478a6e5aaa4..5d77aea248d6 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 } 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, PostDamageForceSwitchAbAttr, CommanderAbAttr } from "#app/data/ability"; import PokemonData from "#app/system/pokemon-data"; import { BattlerIndex } from "#app/battle"; import { Mode } from "#app/ui/ui"; @@ -3081,7 +3081,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } lapseTag(tagType: BattlerTagType): boolean { - const tags = this.summonData.tags; + const tags = this.summonData?.tags; + if (isNullOrUndefined(tags)) { + return false; + } const tag = tags.find(t => t.tagType === tagType); if (tag && !(tag.lapse(this, BattlerTagLapseType.CUSTOM))) { tag.onRemove(this); @@ -3646,6 +3649,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.scene.triggerPokemonBattleAnim(this, PokemonAnimType.SUBSTITUTE_ADD); this.getTag(SubstituteTag)!.sourceInFocus = false; } + + // If this Pokemon has Commander and Dondozo as an active ally, hide this Pokemon's sprite. + if (this.hasAbilityWithAttr(CommanderAbAttr) + && this.scene.currentBattle.double + && this.getAlly()?.species.speciesId === Species.DONDOZO) { + this.setVisible(false); + } this.summonDataPrimer = null; } this.updateInfo(); diff --git a/src/phases/check-switch-phase.ts b/src/phases/check-switch-phase.ts index b87dff32f608..acf17c756682 100644 --- a/src/phases/check-switch-phase.ts +++ b/src/phases/check-switch-phase.ts @@ -26,25 +26,29 @@ export class CheckSwitchPhase extends BattlePhase { const pokemon = this.scene.getPlayerField()[this.fieldIndex]; + // End this phase early... + + // ...if the user is playing in Set Mode if (this.scene.battleStyle === BattleStyle.SET) { - super.end(); - return; + return super.end(); } + // ...if the checked Pokemon is somehow not on the field if (this.scene.field.getAll().indexOf(pokemon) === -1) { this.scene.unshiftPhase(new SummonMissingPhase(this.scene, this.fieldIndex)); - super.end(); - return; + return super.end(); } + // ...if there are no other allowed Pokemon in the player's party to switch with if (!this.scene.getPlayerParty().slice(1).filter(p => p.isActive()).length) { - super.end(); - return; + return super.end(); } - if (pokemon.getTag(BattlerTagType.FRENZY)) { - super.end(); - return; + // ...or if any player Pokemon has an effect that prevents the checked Pokemon from switching + if (pokemon.getTag(BattlerTagType.FRENZY) + || pokemon.isTrapped() + || this.scene.getPlayerField().some(p => p.getTag(BattlerTagType.COMMANDED))) { + return super.end(); } this.scene.ui.showText(i18next.t("battle:switchQuestion", { pokemonName: this.useName ? getPokemonNameWithAffix(pokemon) : i18next.t("battle:pokemon") }), null, () => { diff --git a/src/phases/encounter-phase.ts b/src/phases/encounter-phase.ts index 123f9ded9fc3..c4d919c03258 100644 --- a/src/phases/encounter-phase.ts +++ b/src/phases/encounter-phase.ts @@ -36,7 +36,6 @@ import { PlayerGender } from "#enums/player-gender"; import { Species } from "#enums/species"; import i18next from "i18next"; import { WEIGHT_INCREMENT_ON_SPAWN_MISS } from "#app/data/mystery-encounters/mystery-encounters"; -import { BattlerTagType } from "#enums/battler-tag-type"; export class EncounterPhase extends BattlePhase { private loaded: boolean; @@ -483,7 +482,6 @@ export class EncounterPhase extends BattlePhase { } } else { if (availablePartyMembers.length > 1 && availablePartyMembers[1].isOnField()) { - this.scene.getPlayerField().forEach((pokemon) => pokemon.lapseTag(BattlerTagType.COMMANDED)); this.scene.pushPhase(new ReturnPhase(this.scene, 1)); } this.scene.pushPhase(new ToggleDoublePositionPhase(this.scene, false)); From 2bf8acea06bd0e536b3ac9c49127fd6f0797a28c Mon Sep 17 00:00:00 2001 From: PigeonBar <56974298+PigeonBar@users.noreply.github.com> Date: Sat, 9 Nov 2024 13:15:24 -0500 Subject: [PATCH 06/37] [Beta][P2] Fix Sketch failing to sketch moves that call other moves virtually (#4823) * [P2][Beta] Fix Sketch failing to sketch Metronome et al * Suggested changes to `getLastXMoves()` * Renamed turnCount to moveCount --- src/data/move.ts | 11 +++- src/field/pokemon.ts | 16 +++++- src/modifier/modifier.ts | 4 +- src/test/abilities/sap_sipper.test.ts | 72 ++++++++++----------------- src/test/moves/sketch.test.ts | 21 +++++++- 5 files changed, 70 insertions(+), 54 deletions(-) diff --git a/src/data/move.ts b/src/data/move.ts index 071d7fa1e65a..ed2b176f54c9 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -6373,10 +6373,17 @@ export class RandomMovesetMoveAttr extends OverrideMoveEffectAttr { } export class RandomMoveAttr extends OverrideMoveEffectAttr { + /** + * This function exists solely to allow tests to override the randomly selected move by mocking this function. + */ + public getMoveOverride(): Moves | null { + return null; + } + apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise { return new Promise(resolve => { const moveIds = Utils.getEnumValues(Moves).filter(m => !allMoves[m].hasFlag(MoveFlags.IGNORE_VIRTUAL) && !allMoves[m].name.endsWith(" (N)")); - const moveId = moveIds[user.randSeedInt(moveIds.length)]; + const moveId = this.getMoveOverride() ?? moveIds[user.randSeedInt(moveIds.length)]; const moveTargets = getMoveTargets(user, moveId); if (!moveTargets.targets.length) { @@ -6759,7 +6766,7 @@ export class SketchAttr extends MoveEffectAttr { return false; } - const targetMove = target.getLastXMoves(target.battleSummonData.turnCount) + const targetMove = target.getLastXMoves(-1) .find(m => m.move !== Moves.NONE && m.move !== Moves.STRUGGLE && !m.virtual); if (!targetMove) { return false; diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 5d77aea248d6..221cc8f818a0 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -3226,9 +3226,21 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.getMoveHistory().push(turnMove); } - getLastXMoves(turnCount: integer = 0): TurnMove[] { + /** + * Returns a list of the most recent move entries in this Pokemon's move history. + * The retrieved move entries are sorted in order from NEWEST to OLDEST. + * @param moveCount The number of move entries to retrieve. + * If negative, retrieve the Pokemon's entire move history (equivalent to reversing the output of {@linkcode getMoveHistory()}). + * Default is `1`. + * @returns A list of {@linkcode TurnMove}, as specified above. + */ + getLastXMoves(moveCount: number = 1): TurnMove[] { const moveHistory = this.getMoveHistory(); - return moveHistory.slice(turnCount >= 0 ? Math.max(moveHistory.length - (turnCount || 1), 0) : 0, moveHistory.length).reverse(); + if (moveCount >= 0) { + return moveHistory.slice(Math.max(moveHistory.length - moveCount, 0)).reverse(); + } else { + return moveHistory.slice(0).reverse(); + } } getMoveQueue(): QueuedMove[] { diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index 9e97c8667188..90336780ba64 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -728,10 +728,10 @@ export abstract class PokemonHeldItemModifier extends PersistentModifier { //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)[0]) { + if (!pokemon.getLastXMoves()[0]) { return 1; } - const sheerForceAffected = allMoves[pokemon.getLastXMoves(0)[0].move].chance >= 0 && pokemon.hasAbility(Abilities.SHEER_FORCE); + const sheerForceAffected = allMoves[pokemon.getLastXMoves()[0].move].chance >= 0 && pokemon.hasAbility(Abilities.SHEER_FORCE); if (sheerForceAffected) { return 0; diff --git a/src/test/abilities/sap_sipper.test.ts b/src/test/abilities/sap_sipper.test.ts index a4ce0c1b8f66..dc254a54b54e 100644 --- a/src/test/abilities/sap_sipper.test.ts +++ b/src/test/abilities/sap_sipper.test.ts @@ -8,7 +8,8 @@ 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 { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { allMoves, RandomMoveAttr } from "#app/data/move"; // See also: TypeImmunityAbAttr describe("Abilities - Sap Sipper", () => { @@ -27,20 +28,20 @@ describe("Abilities - Sap Sipper", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleType("single"); - game.override.disableCrits(); + game.override.battleType("single") + .disableCrits() + .ability(Abilities.SAP_SIPPER) + .enemySpecies(Species.RATTATA) + .enemyAbility(Abilities.SAP_SIPPER) + .enemyMoveset(Moves.SPLASH); }); it("raises ATK stat stage by 1 and block effects when activated against a grass attack", async() => { const moveToUse = Moves.LEAFAGE; - const enemyAbility = Abilities.SAP_SIPPER; - game.override.moveset([ moveToUse ]); - game.override.enemyMoveset(Moves.SPLASH); - game.override.enemySpecies(Species.DUSKULL); - game.override.enemyAbility(enemyAbility); + game.override.moveset(moveToUse); - await game.startBattle(); + await game.classicMode.startBattle([ Species.BULBASAUR ]); const enemyPokemon = game.scene.getEnemyPokemon()!; const initialEnemyHp = enemyPokemon.hp; @@ -55,14 +56,10 @@ describe("Abilities - Sap Sipper", () => { it("raises ATK stat stage by 1 and block effects when activated against a grass status move", async() => { const moveToUse = Moves.SPORE; - const enemyAbility = Abilities.SAP_SIPPER; - game.override.moveset([ moveToUse ]); - game.override.enemyMoveset(Moves.SPLASH); - game.override.enemySpecies(Species.RATTATA); - game.override.enemyAbility(enemyAbility); + game.override.moveset(moveToUse); - await game.startBattle(); + await game.classicMode.startBattle([ Species.BULBASAUR ]); const enemyPokemon = game.scene.getEnemyPokemon()!; @@ -76,14 +73,10 @@ describe("Abilities - Sap Sipper", () => { it("do not activate against status moves that target the field", async () => { const moveToUse = Moves.GRASSY_TERRAIN; - const enemyAbility = Abilities.SAP_SIPPER; - game.override.moveset([ moveToUse ]); - game.override.enemyMoveset(Moves.SPLASH); - game.override.enemySpecies(Species.RATTATA); - game.override.enemyAbility(enemyAbility); + game.override.moveset(moveToUse); - await game.startBattle(); + await game.classicMode.startBattle([ Species.BULBASAUR ]); game.move.select(moveToUse); @@ -96,14 +89,10 @@ describe("Abilities - Sap Sipper", () => { it("activate once against multi-hit grass attacks", async () => { const moveToUse = Moves.BULLET_SEED; - const enemyAbility = Abilities.SAP_SIPPER; - game.override.moveset([ moveToUse ]); - game.override.enemyMoveset(Moves.SPLASH); - game.override.enemySpecies(Species.RATTATA); - game.override.enemyAbility(enemyAbility); + game.override.moveset(moveToUse); - await game.startBattle(); + await game.classicMode.startBattle([ Species.BULBASAUR ]); const enemyPokemon = game.scene.getEnemyPokemon()!; const initialEnemyHp = enemyPokemon.hp; @@ -118,15 +107,10 @@ describe("Abilities - Sap Sipper", () => { it("do not activate against status moves that target the user", async () => { const moveToUse = Moves.SPIKY_SHIELD; - const ability = Abilities.SAP_SIPPER; - game.override.moveset([ moveToUse ]); - game.override.ability(ability); - game.override.enemyMoveset(Moves.SPLASH); - game.override.enemySpecies(Species.RATTATA); - game.override.enemyAbility(Abilities.NONE); + game.override.moveset(moveToUse); - await game.startBattle(); + await game.classicMode.startBattle([ Species.BULBASAUR ]); const playerPokemon = game.scene.getPlayerPokemon()!; @@ -142,18 +126,15 @@ describe("Abilities - Sap Sipper", () => { expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase"); }); - // TODO Add METRONOME outcome override - // To run this testcase, manually modify the METRONOME move to always give SAP_SIPPER, then uncomment - it.todo("activate once against multi-hit grass attacks (metronome)", async () => { + it("activate once against multi-hit grass attacks (metronome)", async () => { const moveToUse = Moves.METRONOME; - const enemyAbility = Abilities.SAP_SIPPER; - game.override.moveset([ moveToUse ]); - game.override.enemyMoveset([ Moves.SPLASH, Moves.NONE, Moves.NONE, Moves.NONE ]); - game.override.enemySpecies(Species.RATTATA); - game.override.enemyAbility(enemyAbility); + const randomMoveAttr = allMoves[Moves.METRONOME].findAttr(attr => attr instanceof RandomMoveAttr) as RandomMoveAttr; + vi.spyOn(randomMoveAttr, "getMoveOverride").mockReturnValue(Moves.BULLET_SEED); - await game.startBattle(); + game.override.moveset(moveToUse); + + await game.classicMode.startBattle([ Species.BULBASAUR ]); const enemyPokemon = game.scene.getEnemyPokemon()!; const initialEnemyHp = enemyPokemon.hp; @@ -168,11 +149,8 @@ describe("Abilities - Sap Sipper", () => { it("still activates regardless of accuracy check", async () => { game.override.moveset(Moves.LEAF_BLADE); - game.override.enemyMoveset(Moves.SPLASH); - game.override.enemySpecies(Species.MAGIKARP); - game.override.enemyAbility(Abilities.SAP_SIPPER); - await game.classicMode.startBattle(); + await game.classicMode.startBattle([ Species.BULBASAUR ]); const enemyPokemon = game.scene.getEnemyPokemon()!; diff --git a/src/test/moves/sketch.test.ts b/src/test/moves/sketch.test.ts index 4386ce5868ea..f531f44ef0c3 100644 --- a/src/test/moves/sketch.test.ts +++ b/src/test/moves/sketch.test.ts @@ -4,9 +4,10 @@ import { Species } from "#enums/species"; import { MoveResult, PokemonMove } from "#app/field/pokemon"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { StatusEffect } from "#app/enums/status-effect"; import { BattlerIndex } from "#app/battle"; +import { allMoves, RandomMoveAttr } from "#app/data/move"; describe("Moves - Sketch", () => { let phaserGame: Phaser.Game; @@ -76,4 +77,22 @@ describe("Moves - Sketch", () => { expect(playerPokemon.moveset[0]?.moveId).toBe(Moves.SPLASH); expect(playerPokemon.moveset[1]?.moveId).toBe(Moves.GROWL); }); + + it("should sketch moves that call other moves", async () => { + const randomMoveAttr = allMoves[Moves.METRONOME].findAttr(attr => attr instanceof RandomMoveAttr) as RandomMoveAttr; + vi.spyOn(randomMoveAttr, "getMoveOverride").mockReturnValue(Moves.FALSE_SWIPE); + + game.override.enemyMoveset([ Moves.METRONOME ]); + await game.classicMode.startBattle([ Species.REGIELEKI ]); + const playerPokemon = game.scene.getPlayerPokemon()!; + playerPokemon.moveset = [ new PokemonMove(Moves.SKETCH) ]; + + // Opponent uses Metronome -> False Swipe, then player uses Sketch, which should sketch Metronome + game.move.select(Moves.SKETCH); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("TurnEndPhase"); + expect(playerPokemon.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); + expect(playerPokemon.moveset[0]?.moveId).toBe(Moves.METRONOME); + expect(playerPokemon.hp).toBeLessThan(playerPokemon.getMaxHp()); // Make sure opponent actually used False Swipe + }); }); From 198ac2431d11536f20b68d56b06056eecd8c73fa Mon Sep 17 00:00:00 2001 From: damocleas Date: Sat, 9 Nov 2024 17:26:55 -0500 Subject: [PATCH 07/37] Undo Event modifier-type.ts item table changes (#4836) --- src/modifier/modifier-type.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index ae1aef3ff884..4986c1feab14 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -1702,10 +1702,7 @@ 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 ? party[0].scene.eventManager.isEventActive() ? 2 : 1 : 0, - (party: Pokemon[]) => party[0].scene.eventManager.isEventActive() ? 2 : 1), - new WeightedModifierType(modifierTypes.SOOTHE_BELL, (party: Pokemon[]) => party[0].scene.eventManager.isEventActive() ? 3 : 0), + new WeightedModifierType(modifierTypes.MAP, (party: Pokemon[]) => party[0].scene.gameMode.isClassic && party[0].scene.currentBattle.waveIndex < 180 ? 1 : 0, 1), new WeightedModifierType(modifierTypes.TM_GREAT, 3), new WeightedModifierType(modifierTypes.MEMORY_MUSHROOM, (party: Pokemon[]) => { if (!party.find(p => p.getLearnableLevelMoves().length)) { @@ -1773,7 +1770,7 @@ const modifierPool: ModifierPool = { new WeightedModifierType(modifierTypes.CANDY_JAR, skipInLastClassicWaveOrDefault(5)), new WeightedModifierType(modifierTypes.ATTACK_TYPE_BOOSTER, 9), new WeightedModifierType(modifierTypes.TM_ULTRA, 11), - new WeightedModifierType(modifierTypes.RARER_CANDY, (party: Pokemon[]) => party[0].scene.eventManager.isEventActive() ? 6 : 4), + new WeightedModifierType(modifierTypes.RARER_CANDY, 4), new WeightedModifierType(modifierTypes.GOLDEN_PUNCH, skipInLastClassicWaveOrDefault(2)), new WeightedModifierType(modifierTypes.IV_SCANNER, skipInLastClassicWaveOrDefault(4)), new WeightedModifierType(modifierTypes.EXP_CHARM, skipInLastClassicWaveOrDefault(8)), @@ -1797,7 +1794,7 @@ 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, (party: Pokemon[]) => party[0].scene.eventManager.isEventActive() ? 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), From 265b3cb938a7f8a5685f5279bb6127d37919adf8 Mon Sep 17 00:00:00 2001 From: Payton Rogers Date: Sat, 9 Nov 2024 23:35:16 -0600 Subject: [PATCH 08/37] [P3] Fix visual bug with level text remaining the same when pokemon levels are reduced in weird dream ME (#4837) --- src/data/mystery-encounters/encounters/weird-dream-encounter.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/data/mystery-encounters/encounters/weird-dream-encounter.ts b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts index 3c541e20bf45..3d2e8493d448 100644 --- a/src/data/mystery-encounters/encounters/weird-dream-encounter.ts +++ b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts @@ -312,6 +312,7 @@ export const WeirdDreamEncounter: MysteryEncounter = pokemon.levelExp = 0; pokemon.calculateStats(); + pokemon.getBattleInfo().setLevel(pokemon.level); await pokemon.updateInfo(); } From 44a68a91bac22b5982f98012846e66174ee16d96 Mon Sep 17 00:00:00 2001 From: Moka <54149968+MokaStitcher@users.noreply.github.com> Date: Sun, 10 Nov 2024 06:35:49 +0100 Subject: [PATCH 09/37] [P1] Fix crash when newly aquired Pokemon are sent in battle (#4835) --- .../encounters/fun-and-games-encounter.ts | 2 +- src/field/pokemon.ts | 8 ++++---- src/phases/encounter-phase.ts | 2 +- src/phases/summon-phase.ts | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) 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 7bf48aa59261..c286fffe0de3 100644 --- a/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts +++ b/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts @@ -305,7 +305,7 @@ async function showWobbuffetHealthBar(scene: BattleScene) { scene.field.add(wobbuffet); const playerPokemon = scene.getPlayerPokemon() as Pokemon; - if (playerPokemon?.visible) { + if (playerPokemon?.isOnField()) { scene.field.moveBelow(wobbuffet, playerPokemon); } // Show health bar and trigger cry diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 221cc8f818a0..daa731645777 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -5113,7 +5113,7 @@ export class EnemyPokemon extends Pokemon { /** * Add a new pokemon to the player's party (at `slotIndex` if set). - * If the first slot is replaced, the new pokemon's visibility will be set to `false`. + * The new pokemon's visibility will be set to `false`. * @param pokeballType the type of pokeball the pokemon was caught with * @param slotIndex an optional index to place the pokemon in the party * @returns the pokemon that was added or null if the pokemon could not be added @@ -5131,14 +5131,14 @@ export class EnemyPokemon extends Pokemon { const newPokemon = this.scene.addPlayerPokemon(this.species, this.level, this.abilityIndex, this.formIndex, this.gender, this.shiny, this.variant, this.ivs, this.nature, this); if (Utils.isBetween(slotIndex, 0, PLAYER_PARTY_MAX_SIZE - 1)) { - if (slotIndex === 0) { - newPokemon.setVisible(false); // Hide if replaced with first pokemon - } party.splice(slotIndex, 0, newPokemon); } else { party.push(newPokemon); } + // Hide the Pokemon since it is not on the field + newPokemon.setVisible(false); + ret = newPokemon; this.scene.triggerPokemonFormChange(newPokemon, SpeciesFormChangeActiveTrigger, true); } diff --git a/src/phases/encounter-phase.ts b/src/phases/encounter-phase.ts index c4d919c03258..fc022ab96475 100644 --- a/src/phases/encounter-phase.ts +++ b/src/phases/encounter-phase.ts @@ -202,7 +202,7 @@ export class EncounterPhase extends BattlePhase { this.scene.field.add(enemyPokemon); battle.seenEnemyPartyMemberIds.add(enemyPokemon.id); const playerPokemon = this.scene.getPlayerPokemon(); - if (playerPokemon?.visible) { + if (playerPokemon?.isOnField()) { this.scene.field.moveBelow(enemyPokemon as Pokemon, playerPokemon); } enemyPokemon.tint(0, 0.5); diff --git a/src/phases/summon-phase.ts b/src/phases/summon-phase.ts index 119e550293cd..177e09c45275 100644 --- a/src/phases/summon-phase.ts +++ b/src/phases/summon-phase.ts @@ -140,7 +140,7 @@ export class SummonPhase extends PartyMemberPokemonPhase { this.scene.field.add(pokemon); if (!this.player) { const playerPokemon = this.scene.getPlayerPokemon() as Pokemon; - if (playerPokemon?.visible) { + if (playerPokemon?.isOnField()) { this.scene.field.moveBelow(pokemon, playerPokemon); } this.scene.currentBattle.seenEnemyPartyMemberIds.add(pokemon.id); @@ -193,7 +193,7 @@ export class SummonPhase extends PartyMemberPokemonPhase { this.scene.field.add(pokemon); if (!this.player) { const playerPokemon = this.scene.getPlayerPokemon() as Pokemon; - if (playerPokemon?.visible) { + if (playerPokemon?.isOnField()) { this.scene.field.moveBelow(pokemon, playerPokemon); } this.scene.currentBattle.seenEnemyPartyMemberIds.add(pokemon.id); From b3a94e6a6bebc8c1b948b4196a410b3f4c23f60e Mon Sep 17 00:00:00 2001 From: Mumble <171087428+frutescens@users.noreply.github.com> Date: Sat, 9 Nov 2024 21:37:09 -0800 Subject: [PATCH 10/37] [Telemetry][Misc] Client-Side changes to log run results at the end of runs (#4834) * Added new telemetry-related parameters * Update test with new parameters. * Removing extra parameters. * Cat in front of keyboar d sorry * Changed variable name to isVictory. * Apply suggestions from code review Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Thank you Torranx * Condensed if-else pair to else if statement * Update src/phases/game-over-phase.ts Co-authored-by: Adrian T. <68144167+torranx@users.noreply.github.com> * inhale... exhale... corrected variable name to pass linter --------- Co-authored-by: frutescens Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> Co-authored-by: Adrian T. <68144167+torranx@users.noreply.github.com> --- src/@types/PokerogueSessionSavedataApi.ts | 1 + src/phases/game-over-phase.ts | 50 +++++++++---------- .../pokerogue-session-savedata-api.test.ts | 3 +- 3 files changed, 26 insertions(+), 28 deletions(-) diff --git a/src/@types/PokerogueSessionSavedataApi.ts b/src/@types/PokerogueSessionSavedataApi.ts index 5fcd8575b15a..c4650611c4f6 100644 --- a/src/@types/PokerogueSessionSavedataApi.ts +++ b/src/@types/PokerogueSessionSavedataApi.ts @@ -8,6 +8,7 @@ export class UpdateSessionSavedataRequest { /** This is **NOT** similar to {@linkcode ClearSessionSavedataRequest} */ export interface NewClearSessionSavedataRequest { slot: number; + isVictory: boolean; clientSessionId: string; } diff --git a/src/phases/game-over-phase.ts b/src/phases/game-over-phase.ts index 84fad257897d..26a0c45f449b 100644 --- a/src/phases/game-over-phase.ts +++ b/src/phases/game-over-phase.ts @@ -26,13 +26,13 @@ import i18next from "i18next"; import { pokerogueApi } from "#app/plugins/api/pokerogue-api"; export class GameOverPhase extends BattlePhase { - private victory: boolean; + private isVictory: boolean; private firstRibbons: PokemonSpecies[] = []; - constructor(scene: BattleScene, victory?: boolean) { + constructor(scene: BattleScene, isVictory: boolean = false) { super(scene); - this.victory = !!victory; + this.isVictory = isVictory; } start() { @@ -40,22 +40,22 @@ export class GameOverPhase extends BattlePhase { // Failsafe if players somehow skip floor 200 in classic mode if (this.scene.gameMode.isClassic && this.scene.currentBattle.waveIndex > 200) { - this.victory = true; + this.isVictory = true; } // Handle Mystery Encounter special Game Over cases // Situations such as when player lost a battle, but it isn't treated as full Game Over - if (!this.victory && this.scene.currentBattle.mysteryEncounter?.onGameOver && !this.scene.currentBattle.mysteryEncounter.onGameOver(this.scene)) { + if (!this.isVictory && this.scene.currentBattle.mysteryEncounter?.onGameOver && !this.scene.currentBattle.mysteryEncounter.onGameOver(this.scene)) { // Do not end the game return this.end(); } // Otherwise, continue standard Game Over logic - if (this.victory && this.scene.gameMode.isEndless) { + if (this.isVictory && this.scene.gameMode.isEndless) { const genderIndex = this.scene.gameData.gender ?? PlayerGender.UNSET; const genderStr = PlayerGender[genderIndex].toLowerCase(); this.scene.ui.showDialogue(i18next.t("miscDialogue:ending_endless", { context: genderStr }), i18next.t("miscDialogue:ending_name"), 0, () => this.handleGameOver()); - } else if (this.victory || !this.scene.enableRetries) { + } else if (this.isVictory || !this.scene.enableRetries) { this.handleGameOver(); } else { this.scene.ui.showText(i18next.t("battle:retryBattle"), null, () => { @@ -93,7 +93,7 @@ export class GameOverPhase extends BattlePhase { this.scene.disableMenu = true; this.scene.time.delayedCall(1000, () => { let firstClear = false; - if (this.victory && newClear) { + if (this.isVictory && newClear) { if (this.scene.gameMode.isClassic) { firstClear = this.scene.validateAchv(achvs.CLASSIC_VICTORY); this.scene.validateAchv(achvs.UNEVOLVED_CLASSIC_VICTORY); @@ -109,8 +109,8 @@ export class GameOverPhase extends BattlePhase { this.scene.gameData.gameStats.dailyRunSessionsWon++; } } - this.scene.gameData.saveRunHistory(this.scene, this.scene.gameData.getSessionSaveData(this.scene), this.victory); - const fadeDuration = this.victory ? 10000 : 5000; + 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)); activeBattlers.map(p => p.hideInfo()); @@ -120,7 +120,7 @@ export class GameOverPhase extends BattlePhase { this.scene.clearPhaseQueue(); this.scene.ui.clearText(); - if (this.victory && this.scene.gameMode.isChallenge) { + if (this.isVictory && this.scene.gameMode.isChallenge) { this.scene.gameMode.challenges.forEach(c => this.scene.validateAchvs(ChallengeAchv, c)); } @@ -128,7 +128,7 @@ export class GameOverPhase extends BattlePhase { if (newClear) { this.handleUnlocks(); } - if (this.victory && newClear) { + if (this.isVictory && newClear) { for (const species of this.firstRibbons) { this.scene.unshiftPhase(new RibbonModifierRewardPhase(this.scene, modifierTypes.VOUCHER_PLUS, species)); } @@ -140,7 +140,7 @@ export class GameOverPhase extends BattlePhase { this.end(); }; - if (this.victory && this.scene.gameMode.isClassic) { + if (this.isVictory && this.scene.gameMode.isClassic) { const dialogueKey = "miscDialogue:ending"; if (!this.scene.ui.shouldSkipDialogue(dialogueKey)) { @@ -173,25 +173,21 @@ export class GameOverPhase extends BattlePhase { }); }; - /* Added a local check to see if the game is running offline on victory + /* Added a local check to see if the game is running offline If Online, execute apiFetch as intended - If Offline, execute offlineNewClear(), a localStorage implementation of newClear daily run checks */ - if (this.victory) { - if (!Utils.isLocal || Utils.isLocalServerConnected) { - pokerogueApi.savedata.session.newclear({ slot: this.scene.sessionSlotId, clientSessionId }) - .then((success) => doGameOver(!!success)); - } else { - this.scene.gameData.offlineNewClear(this.scene).then(result => { - doGameOver(result); - }); - } - } else { - doGameOver(false); + If Offline, execute offlineNewClear() only for victory, a localStorage implementation of newClear daily run checks */ + if (!Utils.isLocal || Utils.isLocalServerConnected) { + pokerogueApi.savedata.session.newclear({ slot: this.scene.sessionSlotId, isVictory: this.isVictory, clientSessionId: clientSessionId }) + .then((success) => doGameOver(!!success)); + } else if (this.isVictory) { + this.scene.gameData.offlineNewClear(this.scene).then(result => { + doGameOver(result); + }); } } handleUnlocks(): void { - if (this.victory && this.scene.gameMode.isClassic) { + if (this.isVictory && this.scene.gameMode.isClassic) { if (!this.scene.gameData.unlocks[Unlockables.ENDLESS_MODE]) { this.scene.unshiftPhase(new UnlockPhase(this.scene, Unlockables.ENDLESS_MODE)); } diff --git a/src/test/plugins/api/pokerogue-session-savedata-api.test.ts b/src/test/plugins/api/pokerogue-session-savedata-api.test.ts index d9f6216c4cf3..f453c5edd884 100644 --- a/src/test/plugins/api/pokerogue-session-savedata-api.test.ts +++ b/src/test/plugins/api/pokerogue-session-savedata-api.test.ts @@ -28,7 +28,8 @@ describe("Pokerogue Session Savedata API", () => { describe("Newclear", () => { const params: NewClearSessionSavedataRequest = { clientSessionId: "test-session-id", - slot: 3, + isVictory: true, + slot: 3 }; it("should return true on SUCCESS", async () => { From 2968059814dd714a1a855006adc6016b1f081a1b Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Sat, 9 Nov 2024 21:39:05 -0800 Subject: [PATCH 11/37] [P1] Transform and Imposter will now fail when either Pokemon is fused (#4824) * Transform and Imposter will now fail when either Pokemon is fused * Prevent Ditto from being randomly generated as part of a fusion --- src/data/ability.ts | 27 ++++++++++++++++++++++----- src/data/move.ts | 8 +++++--- src/data/pokemon-species.ts | 19 +++++++++++++------ src/field/pokemon.ts | 18 ++++++++++-------- 4 files changed, 50 insertions(+), 22 deletions(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index 08dc1ed27a4c..736f58625305 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -2463,12 +2463,15 @@ export class PostSummonCopyAllyStatsAbAttr extends PostSummonAbAttr { } } +/** + * Used by Imposter + */ export class PostSummonTransformAbAttr extends PostSummonAbAttr { constructor() { super(true); } - async applyPostSummon(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): Promise { + async applyPostSummon(pokemon: Pokemon, _passive: boolean, simulated: boolean, _args: any[]): Promise { const targets = pokemon.getOpponents(); if (simulated || !targets.length) { return simulated; @@ -2477,17 +2480,31 @@ export class PostSummonTransformAbAttr extends PostSummonAbAttr { let target: Pokemon; if (targets.length > 1) { - pokemon.scene.executeWithSeedOffset(() => target = Utils.randSeedItem(targets), pokemon.scene.currentBattle.waveIndex); + pokemon.scene.executeWithSeedOffset(() => { + // in a double battle, if one of the opposing pokemon is fused the other one will be chosen + // if both are fused, then Imposter will fail below + if (targets[0].fusionSpecies) { + target = targets[1]; + return; + } else if (targets[1].fusionSpecies) { + target = targets[0]; + return; + } + target = Utils.randSeedItem(targets); + }, pokemon.scene.currentBattle.waveIndex); } else { target = targets[0]; } - target = target!; + + // transforming from or into fusion pokemon causes various problems (including crashes and save corruption) + if (target.fusionSpecies || pokemon.fusionSpecies) { + return false; + } + pokemon.summonData.speciesForm = target.getSpeciesForm(); - pokemon.summonData.fusionSpeciesForm = target.getFusionSpeciesForm(); pokemon.summonData.ability = target.getAbility().id; pokemon.summonData.gender = target.getGender(); - pokemon.summonData.fusionGender = target.getFusionGender(); // Copy all stats (except HP) for (const s of EFFECTIVE_STATS) { diff --git a/src/data/move.ts b/src/data/move.ts index ed2b176f54c9..37afe6518611 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -6995,6 +6995,9 @@ export class SuppressAbilitiesIfActedAttr extends MoveEffectAttr { } } +/** + * Used by Transform + */ export class TransformAttr extends MoveEffectAttr { async apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise { if (!super.apply(user, target, move, args)) { @@ -7003,10 +7006,8 @@ export class TransformAttr extends MoveEffectAttr { const promises: Promise[] = []; user.summonData.speciesForm = target.getSpeciesForm(); - user.summonData.fusionSpeciesForm = target.getFusionSpeciesForm(); user.summonData.ability = target.getAbility().id; user.summonData.gender = target.getGender(); - user.summonData.fusionGender = target.getFusionGender(); // Power Trick's effect will not preserved after using Transform user.removeTag(BattlerTagType.POWER_TRICK); @@ -8077,7 +8078,8 @@ export function initMoves() { .ignoresVirtual(), new StatusMove(Moves.TRANSFORM, Type.NORMAL, -1, 10, -1, 0, 1) .attr(TransformAttr) - .condition((user, target, move) => !target.getTag(BattlerTagType.SUBSTITUTE)) + // transforming from or into fusion pokemon causes various problems (such as crashes) + .condition((user, target, move) => !target.getTag(BattlerTagType.SUBSTITUTE) && !user.fusionSpecies && !target.fusionSpecies) .ignoresProtect(), new AttackMove(Moves.BUBBLE, Type.WATER, MoveCategory.SPECIAL, 40, 100, 30, 10, 0, 1) .attr(StatStageChangeAttr, [ Stat.SPD ], -1) diff --git a/src/data/pokemon-species.ts b/src/data/pokemon-species.ts index e7fe902956c7..ff53fdb9392f 100644 --- a/src/data/pokemon-species.ts +++ b/src/data/pokemon-species.ts @@ -888,17 +888,24 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali getCompatibleFusionSpeciesFilter(): PokemonSpeciesFilter { const hasEvolution = pokemonEvolutions.hasOwnProperty(this.speciesId); const hasPrevolution = pokemonPrevolutions.hasOwnProperty(this.speciesId); - const pseudoLegendary = this.subLegendary; + const subLegendary = this.subLegendary; const legendary = this.legendary; const mythical = this.mythical; return species => { - return (pseudoLegendary || legendary || mythical || - (pokemonEvolutions.hasOwnProperty(species.speciesId) === hasEvolution - && pokemonPrevolutions.hasOwnProperty(species.speciesId) === hasPrevolution)) - && species.subLegendary === pseudoLegendary + return ( + subLegendary + || legendary + || mythical + || ( + pokemonEvolutions.hasOwnProperty(species.speciesId) === hasEvolution + && pokemonPrevolutions.hasOwnProperty(species.speciesId) === hasPrevolution + ) + ) + && species.subLegendary === subLegendary && species.legendary === legendary && species.mythical === mythical - && (this.isTrainerForbidden() || !species.isTrainerForbidden()); + && (this.isTrainerForbidden() || !species.isTrainerForbidden()) + && species.speciesId !== Species.DITTO; }; } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index daa731645777..d413e618381c 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -2030,15 +2030,17 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const hasHiddenAbility = !Utils.randSeedInt(hiddenAbilityChance.value); const randAbilityIndex = Utils.randSeedInt(2); - const filter = !forStarter ? this.species.getCompatibleFusionSpeciesFilter() - : species => { + const filter = !forStarter ? + this.species.getCompatibleFusionSpeciesFilter() + : (species: PokemonSpecies) => { return pokemonEvolutions.hasOwnProperty(species.speciesId) - && !pokemonPrevolutions.hasOwnProperty(species.speciesId) - && !species.pseudoLegendary - && !species.legendary - && !species.mythical - && !species.isTrainerForbidden() - && species.speciesId !== this.species.speciesId; + && !pokemonPrevolutions.hasOwnProperty(species.speciesId) + && !species.subLegendary + && !species.legendary + && !species.mythical + && !species.isTrainerForbidden() + && species.speciesId !== this.species.speciesId + && species.speciesId !== Species.DITTO; }; let fusionOverride: PokemonSpecies | undefined = undefined; From 63ffab027dcddcca9122e7270242e88845703e25 Mon Sep 17 00:00:00 2001 From: PigeonBar <56974298+PigeonBar@users.noreply.github.com> Date: Sun, 10 Nov 2024 14:21:29 -0500 Subject: [PATCH 12/37] [Beta][P2] Several Unburden bug fixes (#4820) * [P2][Beta] Several Unburden bug fixes * Unburden test adjustments * Some further test cleanup * Add suggested `.bypassFaint()` to Unburden --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/battle-scene.ts | 29 +- src/data/ability.ts | 6 +- src/data/berry.ts | 26 +- src/data/move.ts | 16 +- .../encounters/bug-type-superfan-encounter.ts | 7 +- .../encounters/delibirdy-encounter.ts | 18 +- .../global-trade-system-encounter.ts | 8 +- src/field/pokemon.ts | 28 +- src/phases/berry-phase.ts | 7 +- src/phases/faint-phase.ts | 10 +- src/phases/select-modifier-phase.ts | 2 +- src/phases/stat-stage-change-phase.ts | 5 +- src/phases/switch-summon-phase.ts | 2 +- src/test/abilities/unburden.test.ts | 344 +++++++++++++----- 14 files changed, 338 insertions(+), 170 deletions(-) diff --git a/src/battle-scene.ts b/src/battle-scene.ts index c5acadc8eb61..c30ab2e29123 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -2572,14 +2572,15 @@ export default class BattleScene extends SceneBase { * The quantity to transfer is automatically capped at how much the recepient can take before reaching the maximum stack size for the item. * A transfer that moves a quantity smaller than what is specified in the transferQuantity parameter is still considered successful. * @param itemModifier {@linkcode PokemonHeldItemModifier} item to transfer (represents the whole stack) - * @param target {@linkcode Pokemon} pokemon recepient in this transfer - * @param playSound {boolean} - * @param transferQuantity {@linkcode integer} how many items of the stack to transfer. Optional, defaults to 1 - * @param instant {boolean} - * @param ignoreUpdate {boolean} - * @returns true if the transfer was successful + * @param target {@linkcode Pokemon} recepient in this transfer + * @param playSound `true` to play a sound when transferring the item + * @param transferQuantity How many items of the stack to transfer. Optional, defaults to `1` + * @param instant ??? (Optional) + * @param ignoreUpdate ??? (Optional) + * @param itemLost If `true`, treat the item's current holder as losing the item (for now, this simply enables Unburden). Default is `true`. + * @returns `true` if the transfer was successful */ - tryTransferHeldItemModifier(itemModifier: PokemonHeldItemModifier, target: Pokemon, playSound: boolean, transferQuantity: integer = 1, instant?: boolean, ignoreUpdate?: boolean): Promise { + tryTransferHeldItemModifier(itemModifier: PokemonHeldItemModifier, target: Pokemon, playSound: boolean, transferQuantity: number = 1, instant?: boolean, ignoreUpdate?: boolean, itemLost: boolean = true): Promise { return new Promise(resolve => { const source = itemModifier.pokemonId ? itemModifier.getPokemon(target.scene) : null; const cancelled = new Utils.BooleanHolder(false); @@ -2612,14 +2613,14 @@ export default class BattleScene extends SceneBase { if (!matchingModifier || this.removeModifier(matchingModifier, !target.isPlayer())) { if (target.isPlayer()) { this.addModifier(newItemModifier, ignoreUpdate, playSound, false, instant).then(() => { - if (source) { + if (source && itemLost) { applyPostItemLostAbAttrs(PostItemLostAbAttr, source, false); } resolve(true); }); } else { this.addEnemyModifier(newItemModifier, ignoreUpdate, instant).then(() => { - if (source) { + if (source && itemLost) { applyPostItemLostAbAttrs(PostItemLostAbAttr, source, false); } resolve(true); @@ -2791,7 +2792,15 @@ export default class BattleScene extends SceneBase { }); } - removeModifier(modifier: PersistentModifier, enemy?: boolean): boolean { + /** + * Removes a currently owned item. If the item is stacked, the entire item stack + * gets removed. This function does NOT apply in-battle effects, such as Unburden. + * If in-battle effects are needed, use {@linkcode Pokemon.loseHeldItem} instead. + * @param modifier The item to be removed. + * @param enemy If `true`, remove an item owned by the enemy. If `false`, remove an item owned by the player. Default is `false`. + * @returns `true` if the item exists and was successfully removed, `false` otherwise. + */ + removeModifier(modifier: PersistentModifier, enemy: boolean = false): boolean { const modifiers = !enemy ? this.modifiers : this.enemyModifiers; const modifierIndex = modifiers.indexOf(modifier); if (modifierIndex > -1) { diff --git a/src/data/ability.ts b/src/data/ability.ts index 736f58625305..49763991e0e7 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -4152,7 +4152,7 @@ export class PostBattleLootAbAttr extends PostBattleAbAttr { if (!simulated && postBattleLoot.length) { const randItem = Utils.randSeedItem(postBattleLoot); //@ts-ignore - TODO see below - if (pokemon.scene.tryTransferHeldItemModifier(randItem, pokemon, true, 1, true)) { // TODO: fix. This is a promise!? + if (pokemon.scene.tryTransferHeldItemModifier(randItem, pokemon, true, 1, true, undefined, false)) { // TODO: fix. This is a promise!? postBattleLoot.splice(postBattleLoot.indexOf(randItem), 1); pokemon.scene.queueMessage(i18next.t("abilityTriggers:postBattleLoot", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), itemName: randItem.type.name })); return true; @@ -5616,7 +5616,9 @@ export function initAbilities() { new Ability(Abilities.ANGER_POINT, 4) .attr(PostDefendCritStatStageChangeAbAttr, Stat.ATK, 6), new Ability(Abilities.UNBURDEN, 4) - .attr(PostItemLostApplyBattlerTagAbAttr, BattlerTagType.UNBURDEN), + .attr(PostItemLostApplyBattlerTagAbAttr, BattlerTagType.UNBURDEN) + .bypassFaint() // Allows reviver seed to activate Unburden + .edgeCase(), // Should not restore Unburden boost if Pokemon loses then regains Unburden ability new Ability(Abilities.HEATPROOF, 4) .attr(ReceivedTypeDamageMultiplierAbAttr, Type.FIRE, 0.5) .attr(ReduceBurnDamageAbAttr, 0.5) diff --git a/src/data/berry.ts b/src/data/berry.ts index d2bbd0fdd1cc..dfd6a7ddcf08 100644 --- a/src/data/berry.ts +++ b/src/data/berry.ts @@ -61,13 +61,13 @@ export function getBerryPredicate(berryType: BerryType): BerryPredicate { } } -export type BerryEffectFunc = (pokemon: Pokemon) => void; +export type BerryEffectFunc = (pokemon: Pokemon, berryOwner?: Pokemon) => void; export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc { switch (berryType) { case BerryType.SITRUS: case BerryType.ENIGMA: - return (pokemon: Pokemon) => { + return (pokemon: Pokemon, berryOwner?: Pokemon) => { if (pokemon.battleData) { pokemon.battleData.berriesEaten.push(berryType); } @@ -75,10 +75,10 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc { applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, false, hpHealed); pokemon.scene.unshiftPhase(new PokemonHealPhase(pokemon.scene, pokemon.getBattlerIndex(), hpHealed.value, i18next.t("battle:hpHealBerry", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), berryName: getBerryName(berryType) }), true)); - applyPostItemLostAbAttrs(PostItemLostAbAttr, pokemon, false); + applyPostItemLostAbAttrs(PostItemLostAbAttr, berryOwner ?? pokemon, false); }; case BerryType.LUM: - return (pokemon: Pokemon) => { + return (pokemon: Pokemon, berryOwner?: Pokemon) => { if (pokemon.battleData) { pokemon.battleData.berriesEaten.push(berryType); } @@ -87,14 +87,14 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc { } pokemon.resetStatus(true, true); pokemon.updateInfo(); - applyPostItemLostAbAttrs(PostItemLostAbAttr, pokemon, false); + applyPostItemLostAbAttrs(PostItemLostAbAttr, berryOwner ?? pokemon, false); }; case BerryType.LIECHI: case BerryType.GANLON: case BerryType.PETAYA: case BerryType.APICOT: case BerryType.SALAC: - return (pokemon: Pokemon) => { + return (pokemon: Pokemon, berryOwner?: Pokemon) => { if (pokemon.battleData) { pokemon.battleData.berriesEaten.push(berryType); } @@ -103,18 +103,18 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc { const statStages = new Utils.NumberHolder(1); applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, false, statStages); pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ stat ], statStages.value)); - applyPostItemLostAbAttrs(PostItemLostAbAttr, pokemon, false); + applyPostItemLostAbAttrs(PostItemLostAbAttr, berryOwner ?? pokemon, false); }; case BerryType.LANSAT: - return (pokemon: Pokemon) => { + return (pokemon: Pokemon, berryOwner?: Pokemon) => { if (pokemon.battleData) { pokemon.battleData.berriesEaten.push(berryType); } pokemon.addTag(BattlerTagType.CRIT_BOOST); - applyPostItemLostAbAttrs(PostItemLostAbAttr, pokemon, false); + applyPostItemLostAbAttrs(PostItemLostAbAttr, berryOwner ?? pokemon, false); }; case BerryType.STARF: - return (pokemon: Pokemon) => { + return (pokemon: Pokemon, berryOwner?: Pokemon) => { if (pokemon.battleData) { pokemon.battleData.berriesEaten.push(berryType); } @@ -122,10 +122,10 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc { const stages = new Utils.NumberHolder(2); applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, false, stages); pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ randStat ], stages.value)); - applyPostItemLostAbAttrs(PostItemLostAbAttr, pokemon, false); + applyPostItemLostAbAttrs(PostItemLostAbAttr, berryOwner ?? pokemon, false); }; case BerryType.LEPPA: - return (pokemon: Pokemon) => { + return (pokemon: Pokemon, berryOwner?: Pokemon) => { if (pokemon.battleData) { pokemon.battleData.berriesEaten.push(berryType); } @@ -133,7 +133,7 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc { if (ppRestoreMove !== undefined) { ppRestoreMove!.ppUsed = Math.max(ppRestoreMove!.ppUsed - 10, 0); pokemon.scene.queueMessage(i18next.t("battle:ppHealBerry", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), moveName: ppRestoreMove!.getName(), berryName: getBerryName(berryType) })); - applyPostItemLostAbAttrs(PostItemLostAbAttr, pokemon, false); + applyPostItemLostAbAttrs(PostItemLostAbAttr, berryOwner ?? pokemon, false); } }; } diff --git a/src/data/move.ts b/src/data/move.ts index 37afe6518611..9cd4881488cc 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -2417,9 +2417,8 @@ export class RemoveHeldItemAttr extends MoveEffectAttr { const removedItem = heldItems[user.randSeedInt(heldItems.length)]; // Decrease item amount and update icon - !--removedItem.stackCount; + target.loseHeldItem(removedItem); target.scene.updateModifiers(target.isPlayer()); - applyPostItemLostAbAttrs(PostItemLostAbAttr, target, false); if (this.berriesOnly) { @@ -2489,18 +2488,15 @@ export class EatBerryAttr extends MoveEffectAttr { } reduceBerryModifier(target: Pokemon) { - if (this.chosenBerry?.stackCount === 1) { - target.scene.removeModifier(this.chosenBerry, !target.isPlayer()); - } else if (this.chosenBerry !== undefined && this.chosenBerry.stackCount > 1) { - this.chosenBerry.stackCount--; + if (this.chosenBerry) { + target.loseHeldItem(this.chosenBerry); } target.scene.updateModifiers(target.isPlayer()); } - eatBerry(consumer: Pokemon) { - getBerryEffectFunc(this.chosenBerry!.berryType)(consumer); // consumer eats the berry + eatBerry(consumer: Pokemon, berryOwner?: Pokemon) { + getBerryEffectFunc(this.chosenBerry!.berryType)(consumer, berryOwner); // consumer eats the berry applyAbAttrs(HealFromBerryUseAbAttr, consumer, new Utils.BooleanHolder(false)); - applyPostItemLostAbAttrs(PostItemLostAbAttr, consumer, false); } } @@ -2540,7 +2536,7 @@ export class StealEatBerryAttr extends EatBerryAttr { const message = i18next.t("battle:stealEatBerry", { pokemonName: user.name, targetName: target.name, berryName: this.chosenBerry.type.name }); user.scene.queueMessage(message); this.reduceBerryModifier(target); - this.eatBerry(user); + this.eatBerry(user, target); return true; } } diff --git a/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts b/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts index 7a03e6efdd2f..ecd6972902b6 100644 --- a/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts +++ b/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts @@ -477,12 +477,9 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = .withOptionPhase(async (scene: BattleScene) => { const encounter = scene.currentBattle.mysteryEncounter!; const modifier = encounter.misc.chosenModifier; + const chosenPokemon: PlayerPokemon = encounter.misc.chosenPokemon; - // Remove the modifier if its stacks go to 0 - modifier.stackCount -= 1; - if (modifier.stackCount === 0) { - scene.removeModifier(modifier); - } + chosenPokemon.loseHeldItem(modifier, false); scene.updateModifiers(true, true); const bugNet = generateModifierTypeOption(scene, modifierTypes.MYSTERY_ENCOUNTER_GOLDEN_BUG_NET)!; diff --git a/src/data/mystery-encounters/encounters/delibirdy-encounter.ts b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts index d5a938b9cef3..a3a97a012387 100644 --- a/src/data/mystery-encounters/encounters/delibirdy-encounter.ts +++ b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts @@ -8,7 +8,7 @@ import { applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/u import { getPokemonSpecies } from "#app/data/pokemon-species"; import Pokemon, { PlayerPokemon } from "#app/field/pokemon"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; -import { BerryModifier, HealingBoosterModifier, LevelIncrementBoosterModifier, MoneyMultiplierModifier, PokemonHeldItemModifier, PreserveBerryModifier } from "#app/modifier/modifier"; +import { BerryModifier, HealingBoosterModifier, LevelIncrementBoosterModifier, MoneyMultiplierModifier, PokemonHeldItemModifier, PokemonInstantReviveModifier, PreserveBerryModifier } from "#app/modifier/modifier"; import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; import { ModifierRewardPhase } from "#app/phases/modifier-reward-phase"; import i18next from "#app/plugins/i18n"; @@ -197,7 +197,8 @@ export const DelibirdyEncounter: MysteryEncounter = }) .withOptionPhase(async (scene: BattleScene) => { const encounter = scene.currentBattle.mysteryEncounter!; - const modifier: BerryModifier | HealingBoosterModifier = encounter.misc.chosenModifier; + const modifier: BerryModifier | PokemonInstantReviveModifier = encounter.misc.chosenModifier; + const chosenPokemon: PlayerPokemon = encounter.misc.chosenPokemon; // Give the player a Candy Jar if they gave a Berry, and a Berry Pouch for Reviver Seed if (modifier instanceof BerryModifier) { @@ -228,11 +229,7 @@ export const DelibirdyEncounter: MysteryEncounter = } } - // Remove the modifier if its stacks go to 0 - modifier.stackCount -= 1; - if (modifier.stackCount === 0) { - scene.removeModifier(modifier); - } + chosenPokemon.loseHeldItem(modifier, false); leaveEncounterWithoutBattle(scene, true); }) @@ -292,6 +289,7 @@ export const DelibirdyEncounter: MysteryEncounter = .withOptionPhase(async (scene: BattleScene) => { const encounter = scene.currentBattle.mysteryEncounter!; const modifier = encounter.misc.chosenModifier; + const chosenPokemon: PlayerPokemon = encounter.misc.chosenPokemon; // Check if the player has max stacks of Healing Charm already const existing = scene.findModifier(m => m instanceof HealingBoosterModifier) as HealingBoosterModifier; @@ -306,11 +304,7 @@ export const DelibirdyEncounter: MysteryEncounter = scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.HEALING_CHARM)); } - // Remove the modifier if its stacks go to 0 - modifier.stackCount -= 1; - if (modifier.stackCount === 0) { - scene.removeModifier(modifier); - } + chosenPokemon.loseHeldItem(modifier, false); leaveEncounterWithoutBattle(scene, true); }) 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 b0d547e36cf9..2d5696214492 100644 --- a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts +++ b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts @@ -345,6 +345,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = // Pokemon and item selected encounter.setDialogueToken("chosenItem", modifier.type.name); encounter.misc.chosenModifier = modifier; + encounter.misc.chosenPokemon = pokemon; return true; }, }; @@ -370,6 +371,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = const encounter = scene.currentBattle.mysteryEncounter!; const modifier = encounter.misc.chosenModifier as PokemonHeldItemModifier; const party = scene.getPlayerParty(); + const chosenPokemon: PlayerPokemon = encounter.misc.chosenPokemon; // Check tier of the traded item, the received item will be one tier up const type = modifier.type.withTierFromPool(ModifierPoolType.PLAYER, party); @@ -397,11 +399,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = encounter.setDialogueToken("itemName", item.type.name); setEncounterRewards(scene, { guaranteedModifierTypeOptions: [ item ], fillRemaining: false }); - // Remove the chosen modifier if its stacks go to 0 - modifier.stackCount -= 1; - if (modifier.stackCount === 0) { - scene.removeModifier(modifier); - } + chosenPokemon.loseHeldItem(modifier, false); await scene.updateModifiers(true, true); // Generate a trainer name diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index d413e618381c..d806a9b605c0 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 } 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, PostDamageForceSwitchAbAttr, 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"; @@ -985,7 +985,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (this.status && this.status.effect === StatusEffect.PARALYSIS) { ret >>= 1; } - if (this.getTag(BattlerTagType.UNBURDEN) && !this.scene.getField(true).some(pokemon => pokemon !== this && pokemon.hasAbilityWithAttr(SuppressFieldAbilitiesAbAttr))) { + if (this.getTag(BattlerTagType.UNBURDEN) && this.hasAbility(Abilities.UNBURDEN)) { ret *= 2; } break; @@ -4102,6 +4102,28 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } return false; } + + /** + * Reduces one of this Pokemon's held item stacks by 1, and removes the item if applicable. + * Does nothing if this Pokemon is somehow not the owner of the held item. + * @param heldItem The item stack to be reduced by 1. + * @param forBattle If `false`, do not trigger in-battle effects (such as Unburden) from losing the item. For example, set this to `false` if the Pokemon is giving away the held item for a Mystery Encounter. Default is `true`. + * @returns `true` if the item was removed successfully, `false` otherwise. + */ + public loseHeldItem(heldItem: PokemonHeldItemModifier, forBattle: boolean = true): boolean { + if (heldItem.pokemonId === -1 || heldItem.pokemonId === this.id) { + heldItem.stackCount--; + if (heldItem.stackCount <= 0) { + this.scene.removeModifier(heldItem, !this.isPlayer()); + } + if (forBattle) { + applyPostItemLostAbAttrs(PostItemLostAbAttr, this, false); + } + return true; + } else { + return false; + } + } } export default interface Pokemon { @@ -4544,7 +4566,7 @@ export class PlayerPokemon extends Pokemon { && m.pokemonId === pokemon.id, true) as PokemonHeldItemModifier[]; const transferModifiers: Promise[] = []; for (const modifier of fusedPartyMemberHeldModifiers) { - transferModifiers.push(this.scene.tryTransferHeldItemModifier(modifier, this, false, modifier.getStackCount(), true, true)); + transferModifiers.push(this.scene.tryTransferHeldItemModifier(modifier, this, false, modifier.getStackCount(), true, true, false)); } Promise.allSettled(transferModifiers).then(() => { this.scene.updateModifiers(true, true).then(() => { diff --git a/src/phases/berry-phase.ts b/src/phases/berry-phase.ts index e419aa6692d2..5c33ae4b3430 100644 --- a/src/phases/berry-phase.ts +++ b/src/phases/berry-phase.ts @@ -31,11 +31,8 @@ export class BerryPhase extends FieldPhase { for (const berryModifier of this.scene.applyModifiers(BerryModifier, pokemon.isPlayer(), pokemon)) { if (berryModifier.consumed) { - if (!--berryModifier.stackCount) { - this.scene.removeModifier(berryModifier); - } else { - berryModifier.consumed = false; - } + berryModifier.consumed = false; + pokemon.loseHeldItem(berryModifier); } this.scene.eventTarget.dispatchEvent(new BerryUsedEvent(berryModifier)); // Announce a berry was used } diff --git a/src/phases/faint-phase.ts b/src/phases/faint-phase.ts index d66c5b661441..1c48bdfb37a2 100644 --- a/src/phases/faint-phase.ts +++ b/src/phases/faint-phase.ts @@ -55,21 +55,21 @@ export class FaintPhase extends PokemonPhase { start() { super.start(); + const faintPokemon = this.getPokemon(); + if (!isNullOrUndefined(this.destinyTag) && !isNullOrUndefined(this.source)) { this.destinyTag.lapse(this.source, BattlerTagLapseType.CUSTOM); } if (!isNullOrUndefined(this.grudgeTag) && !isNullOrUndefined(this.source)) { - this.grudgeTag.lapse(this.getPokemon(), BattlerTagLapseType.CUSTOM, this.source); + this.grudgeTag.lapse(faintPokemon, BattlerTagLapseType.CUSTOM, this.source); } if (!this.preventEndure) { - const instantReviveModifier = this.scene.applyModifier(PokemonInstantReviveModifier, this.player, this.getPokemon()) as PokemonInstantReviveModifier; + const instantReviveModifier = this.scene.applyModifier(PokemonInstantReviveModifier, this.player, faintPokemon) as PokemonInstantReviveModifier; if (instantReviveModifier) { - if (!--instantReviveModifier.stackCount) { - this.scene.removeModifier(instantReviveModifier); - } + faintPokemon.loseHeldItem(instantReviveModifier); this.scene.updateModifiers(this.player); return this.end(); } diff --git a/src/phases/select-modifier-phase.ts b/src/phases/select-modifier-phase.ts index 98975e30720c..19e1ccc12aea 100644 --- a/src/phases/select-modifier-phase.ts +++ b/src/phases/select-modifier-phase.ts @@ -103,7 +103,7 @@ export class SelectModifierPhase extends BattlePhase { const itemModifiers = this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier && m.isTransferable && m.pokemonId === party[fromSlotIndex].id) as PokemonHeldItemModifier[]; const itemModifier = itemModifiers[itemIndex]; - this.scene.tryTransferHeldItemModifier(itemModifier, party[toSlotIndex], true, itemQuantity); + this.scene.tryTransferHeldItemModifier(itemModifier, party[toSlotIndex], true, itemQuantity, undefined, undefined, false); } else { this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), this.typeOptions, modifierSelectCallback, this.getRerollCost(this.scene.lockModifierTiers)); } diff --git a/src/phases/stat-stage-change-phase.ts b/src/phases/stat-stage-change-phase.ts index ce6ebea24423..44144f9d0478 100644 --- a/src/phases/stat-stage-change-phase.ts +++ b/src/phases/stat-stage-change-phase.ts @@ -125,10 +125,7 @@ export class StatStageChangePhase extends PokemonPhase { const whiteHerb = this.scene.applyModifier(ResetNegativeStatStageModifier, this.player, pokemon) as ResetNegativeStatStageModifier; // If the White Herb was applied, consume it if (whiteHerb) { - whiteHerb.stackCount--; - if (whiteHerb.stackCount <= 0) { - this.scene.removeModifier(whiteHerb); - } + pokemon.loseHeldItem(whiteHerb); this.scene.updateModifiers(this.player); } } diff --git a/src/phases/switch-summon-phase.ts b/src/phases/switch-summon-phase.ts index 51d54315165b..a667e17edf14 100644 --- a/src/phases/switch-summon-phase.ts +++ b/src/phases/switch-summon-phase.ts @@ -111,7 +111,7 @@ export class SwitchSummonPhase extends SummonPhase { const batonPassModifier = this.scene.findModifier(m => m instanceof SwitchEffectTransferModifier && (m as SwitchEffectTransferModifier).pokemonId === this.lastPokemon.id) as SwitchEffectTransferModifier; if (batonPassModifier && !this.scene.findModifier(m => m instanceof SwitchEffectTransferModifier && (m as SwitchEffectTransferModifier).pokemonId === switchedInPokemon.id)) { - this.scene.tryTransferHeldItemModifier(batonPassModifier, switchedInPokemon, false); + this.scene.tryTransferHeldItemModifier(batonPassModifier, switchedInPokemon, false, undefined, undefined, undefined, false); } } } diff --git a/src/test/abilities/unburden.test.ts b/src/test/abilities/unburden.test.ts index 7cd69f4a0756..ba14c7fdcd00 100644 --- a/src/test/abilities/unburden.test.ts +++ b/src/test/abilities/unburden.test.ts @@ -1,18 +1,35 @@ +import { BattlerIndex } from "#app/battle"; +import { PostItemLostAbAttr } from "#app/data/ability"; +import { allMoves, StealHeldItemChanceAttr } from "#app/data/move"; +import Pokemon from "#app/field/pokemon"; +import type { ContactHeldItemTransferChanceModifier } from "#app/modifier/modifier"; import { Abilities } from "#enums/abilities"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { BerryType } from "#enums/berry-type"; 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, vi } from "vitest"; -import { Stat } from "#enums/stat"; -import { BerryType } from "#app/enums/berry-type"; -import { allMoves, StealHeldItemChanceAttr } from "#app/data/move"; describe("Abilities - Unburden", () => { let phaserGame: Phaser.Game; let game: GameManager; + /** + * Count the number of held items a Pokemon has, accounting for stacks of multiple items. + */ + function getHeldItemCount(pokemon: Pokemon): number { + const stackCounts = pokemon.getHeldItems().map(m => m.getStackCount()); + if (stackCounts.length) { + return stackCounts.reduce((a, b) => a + b); + } else { + return 0; + } + } + beforeAll(() => { phaserGame = new Phaser.Game({ type: Phaser.HEADLESS, @@ -27,9 +44,9 @@ describe("Abilities - Unburden", () => { game = new GameManager(phaserGame); game.override .battleType("single") - .starterSpecies(Species.TREECKO) .startingLevel(1) - .moveset([ Moves.POPULATION_BOMB, Moves.KNOCK_OFF, Moves.PLUCK, Moves.THIEF ]) + .ability(Abilities.UNBURDEN) + .moveset([ Moves.SPLASH, Moves.KNOCK_OFF, Moves.PLUCK, Moves.FALSE_SWIPE ]) .startingHeldItems([ { name: "BERRY", count: 1, type: BerryType.SITRUS }, { name: "BERRY", count: 2, type: BerryType.APICOT }, @@ -37,209 +54,348 @@ describe("Abilities - Unburden", () => { ]) .enemySpecies(Species.NINJASK) .enemyLevel(100) - .enemyMoveset([ Moves.FALSE_SWIPE ]) + .enemyMoveset(Moves.SPLASH) .enemyAbility(Abilities.UNBURDEN) .enemyPassiveAbility(Abilities.NO_GUARD) .enemyHeldItems([ { name: "BERRY", type: BerryType.SITRUS, count: 1 }, { name: "BERRY", type: BerryType.LUM, count: 1 }, ]); + // For the various tests that use Thief, give it a 100% steal rate + vi.spyOn(allMoves[Moves.THIEF], "attrs", "get").mockReturnValue([ new StealHeldItemChanceAttr(1.0) ]); }); it("should activate when a berry is eaten", async () => { - await game.classicMode.startBattle(); + game.override.enemyMoveset(Moves.FALSE_SWIPE); + await game.classicMode.startBattle([ Species.TREECKO ]); const playerPokemon = game.scene.getPlayerPokemon()!; - playerPokemon.abilityIndex = 2; - const playerHeldItems = playerPokemon.getHeldItems().length; + const playerHeldItems = getHeldItemCount(playerPokemon); const initialPlayerSpeed = playerPokemon.getStat(Stat.SPD); - game.move.select(Moves.FALSE_SWIPE); + // Player gets hit by False Swipe and eats its own Sitrus Berry + game.move.select(Moves.SPLASH); await game.toNextTurn(); - expect(playerPokemon.getHeldItems().length).toBeLessThan(playerHeldItems); - expect(playerPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialPlayerSpeed * 2); + expect(getHeldItemCount(playerPokemon)).toBeLessThan(playerHeldItems); + expect(playerPokemon.getEffectiveStat(Stat.SPD)).toBe(initialPlayerSpeed * 2); }); - it("should activate when a berry is stolen", async () => { - await game.classicMode.startBattle(); + it("should activate when a berry is eaten, even if Berry Pouch preserves the berry", async () => { + game.override.enemyMoveset(Moves.FALSE_SWIPE) + .startingModifier([{ name: "BERRY_POUCH", count: 5850 }]); + await game.classicMode.startBattle([ Species.TREECKO ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const playerHeldItems = getHeldItemCount(playerPokemon); + const initialPlayerSpeed = playerPokemon.getStat(Stat.SPD); + // Player gets hit by False Swipe and eats its own Sitrus Berry + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + + expect(getHeldItemCount(playerPokemon)).toBe(playerHeldItems); + expect(playerPokemon.getEffectiveStat(Stat.SPD)).toBe(initialPlayerSpeed * 2); + }); + + it("should activate for the target, and not the stealer, when a berry is stolen", async () => { + await game.classicMode.startBattle([ Species.TREECKO ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const initialPlayerSpeed = playerPokemon.getStat(Stat.SPD); const enemyPokemon = game.scene.getEnemyPokemon()!; - const enemyHeldItemCt = enemyPokemon.getHeldItems().length; + const enemyHeldItemCt = getHeldItemCount(enemyPokemon); const initialEnemySpeed = enemyPokemon.getStat(Stat.SPD); + // Player uses Pluck and eats the opponent's berry game.move.select(Moves.PLUCK); await game.toNextTurn(); - expect(enemyPokemon.getHeldItems().length).toBeLessThan(enemyHeldItemCt); - expect(enemyPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialEnemySpeed * 2); + expect(getHeldItemCount(enemyPokemon)).toBeLessThan(enemyHeldItemCt); + expect(enemyPokemon.getEffectiveStat(Stat.SPD)).toBe(initialEnemySpeed * 2); + expect(playerPokemon.getEffectiveStat(Stat.SPD)).toBe(initialPlayerSpeed); }); it("should activate when an item is knocked off", async () => { - await game.classicMode.startBattle(); + await game.classicMode.startBattle([ Species.TREECKO ]); const enemyPokemon = game.scene.getEnemyPokemon()!; - const enemyHeldItemCt = enemyPokemon.getHeldItems().length; + const enemyHeldItemCt = getHeldItemCount(enemyPokemon); const initialEnemySpeed = enemyPokemon.getStat(Stat.SPD); + // Player uses Knock Off and removes the opponent's item game.move.select(Moves.KNOCK_OFF); await game.toNextTurn(); - expect(enemyPokemon.getHeldItems().length).toBeLessThan(enemyHeldItemCt); - expect(enemyPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialEnemySpeed * 2); + expect(getHeldItemCount(enemyPokemon)).toBeLessThan(enemyHeldItemCt); + expect(enemyPokemon.getEffectiveStat(Stat.SPD)).toBe(initialEnemySpeed * 2); }); it("should activate when an item is stolen via attacking ability", async () => { game.override .ability(Abilities.MAGICIAN) - .startingHeldItems([ - { name: "MULTI_LENS", count: 3 }, - ]); - await game.classicMode.startBattle(); + .startingHeldItems([]); // Remove player's full stacks of held items so it can steal opponent's held items + await game.classicMode.startBattle([ Species.TREECKO ]); const enemyPokemon = game.scene.getEnemyPokemon()!; - const enemyHeldItemCt = enemyPokemon.getHeldItems().length; + const enemyHeldItemCt = getHeldItemCount(enemyPokemon); const initialEnemySpeed = enemyPokemon.getStat(Stat.SPD); - game.move.select(Moves.POPULATION_BOMB); + // Player steals the opponent's item via ability Magician + game.move.select(Moves.FALSE_SWIPE); await game.toNextTurn(); - expect(enemyPokemon.getHeldItems().length).toBeLessThan(enemyHeldItemCt); - expect(enemyPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialEnemySpeed * 2); + expect(getHeldItemCount(enemyPokemon)).toBeLessThan(enemyHeldItemCt); + expect(enemyPokemon.getEffectiveStat(Stat.SPD)).toBe(initialEnemySpeed * 2); }); it("should activate when an item is stolen via defending ability", async () => { game.override - .startingLevel(45) .enemyAbility(Abilities.PICKPOCKET) - .startingHeldItems([ - { name: "MULTI_LENS", count: 3 }, - { name: "SOUL_DEW", count: 1 }, - { name: "LUCKY_EGG", count: 1 }, - ]); - await game.classicMode.startBattle(); + .enemyHeldItems([]); // Remove opponent's full stacks of held items so it can steal player's held items + await game.classicMode.startBattle([ Species.TREECKO ]); const playerPokemon = game.scene.getPlayerPokemon()!; - playerPokemon.abilityIndex = 2; - const playerHeldItems = playerPokemon.getHeldItems().length; + const playerHeldItems = getHeldItemCount(playerPokemon); const initialPlayerSpeed = playerPokemon.getStat(Stat.SPD); - game.move.select(Moves.POPULATION_BOMB); + // Player's item gets stolen via ability Pickpocket + game.move.select(Moves.FALSE_SWIPE); await game.toNextTurn(); - expect(playerPokemon.getHeldItems().length).toBeLessThan(playerHeldItems); - expect(playerPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialPlayerSpeed * 2); + expect(getHeldItemCount(playerPokemon)).toBeLessThan(playerHeldItems); + expect(playerPokemon.getEffectiveStat(Stat.SPD)).toBe(initialPlayerSpeed * 2); }); it("should activate when an item is stolen via move", async () => { - vi.spyOn(allMoves[Moves.THIEF], "attrs", "get").mockReturnValue([ new StealHeldItemChanceAttr(1.0) ]); // give Thief 100% steal rate - game.override.startingHeldItems([ - { name: "MULTI_LENS", count: 3 }, - ]); - await game.classicMode.startBattle(); + game.override.moveset(Moves.THIEF) + .startingHeldItems([]); // Remove player's full stacks of held items so it can steal opponent's held items + await game.classicMode.startBattle([ Species.TREECKO ]); const enemyPokemon = game.scene.getEnemyPokemon()!; - const enemyHeldItemCt = enemyPokemon.getHeldItems().length; + const enemyHeldItemCt = getHeldItemCount(enemyPokemon); const initialEnemySpeed = enemyPokemon.getStat(Stat.SPD); + // Player uses Thief and steals the opponent's item game.move.select(Moves.THIEF); await game.toNextTurn(); - expect(enemyPokemon.getHeldItems().length).toBeLessThan(enemyHeldItemCt); - expect(enemyPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialEnemySpeed * 2); + expect(getHeldItemCount(enemyPokemon)).toBeLessThan(enemyHeldItemCt); + expect(enemyPokemon.getEffectiveStat(Stat.SPD)).toBe(initialEnemySpeed * 2); }); it("should activate when an item is stolen via grip claw", async () => { game.override - .startingLevel(5) .startingHeldItems([ - { name: "GRIP_CLAW", count: 5 }, - { name: "MULTI_LENS", count: 3 }, - ]) - .enemyHeldItems([ - { name: "SOUL_DEW", count: 1 }, - { name: "LUCKY_EGG", count: 1 }, - { name: "LEFTOVERS", count: 1 }, { name: "GRIP_CLAW", count: 1 }, - { name: "MULTI_LENS", count: 1 }, - { name: "BERRY", type: BerryType.SITRUS, count: 1 }, - { name: "BERRY", type: BerryType.LUM, count: 1 }, ]); - await game.classicMode.startBattle(); + await game.classicMode.startBattle([ Species.TREECKO ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const gripClaw = playerPokemon.getHeldItems()[0] as ContactHeldItemTransferChanceModifier; + vi.spyOn(gripClaw, "chance", "get").mockReturnValue(100); const enemyPokemon = game.scene.getEnemyPokemon()!; - const enemyHeldItemCt = enemyPokemon.getHeldItems().length; + const enemyHeldItemCt = getHeldItemCount(enemyPokemon); const initialEnemySpeed = enemyPokemon.getStat(Stat.SPD); - while (enemyPokemon.getHeldItems().length === enemyHeldItemCt) { - game.move.select(Moves.POPULATION_BOMB); - await game.toNextTurn(); - } + // Player steals the opponent's item using Grip Claw + game.move.select(Moves.FALSE_SWIPE); + await game.toNextTurn(); - expect(enemyPokemon.getHeldItems().length).toBeLessThan(enemyHeldItemCt); - expect(enemyPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialEnemySpeed * 2); + expect(getHeldItemCount(enemyPokemon)).toBeLessThan(enemyHeldItemCt); + expect(enemyPokemon.getEffectiveStat(Stat.SPD)).toBe(initialEnemySpeed * 2); }); it("should not activate when a neutralizing ability is present", async () => { - game.override.enemyAbility(Abilities.NEUTRALIZING_GAS); - await game.classicMode.startBattle(); + game.override.enemyAbility(Abilities.NEUTRALIZING_GAS) + .enemyMoveset(Moves.FALSE_SWIPE); + await game.classicMode.startBattle([ Species.TREECKO ]); const playerPokemon = game.scene.getPlayerPokemon()!; - const playerHeldItems = playerPokemon.getHeldItems().length; + const playerHeldItems = getHeldItemCount(playerPokemon); const initialPlayerSpeed = playerPokemon.getStat(Stat.SPD); - game.move.select(Moves.FALSE_SWIPE); + // Player gets hit by False Swipe and eats Sitrus Berry, which should not trigger Unburden + game.move.select(Moves.SPLASH); await game.toNextTurn(); - expect(playerPokemon.getHeldItems().length).toBeLessThan(playerHeldItems); - expect(playerPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialPlayerSpeed); + expect(getHeldItemCount(playerPokemon)).toBeLessThan(playerHeldItems); + expect(playerPokemon.getEffectiveStat(Stat.SPD)).toBe(initialPlayerSpeed); + expect(playerPokemon.getTag(BattlerTagType.UNBURDEN)).toBeUndefined(); }); it("should activate when a move that consumes a berry is used", async () => { - game.override.enemyMoveset([ Moves.STUFF_CHEEKS ]); - await game.classicMode.startBattle(); + game.override.moveset(Moves.STUFF_CHEEKS); + await game.classicMode.startBattle([ Species.TREECKO ]); - const enemyPokemon = game.scene.getEnemyPokemon()!; - const enemyHeldItemCt = enemyPokemon.getHeldItems().length; - const initialEnemySpeed = enemyPokemon.getStat(Stat.SPD); + const playerPokemon = game.scene.getPlayerPokemon()!; + const playerHeldItemCt = getHeldItemCount(playerPokemon); + const initialPlayerSpeed = playerPokemon.getStat(Stat.SPD); + // Player uses Stuff Cheeks and eats its own berry + // Caution: Do not test this using opponent, there is a known issue where opponent can randomly generate with Salac Berry game.move.select(Moves.STUFF_CHEEKS); await game.toNextTurn(); - expect(enemyPokemon.getHeldItems().length).toBeLessThan(enemyHeldItemCt); - expect(enemyPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialEnemySpeed * 2); + expect(getHeldItemCount(playerPokemon)).toBeLessThan(playerHeldItemCt); + expect(playerPokemon.getEffectiveStat(Stat.SPD)).toBe(initialPlayerSpeed * 2); }); - it("should deactivate when a neutralizing gas user enters the field", async () => { + it("should deactivate temporarily when a neutralizing gas user is on the field", async () => { game.override .battleType("double") - .moveset([ Moves.SPLASH ]); + .ability(Abilities.NONE); // Disable ability override so that we can properly set abilities below await game.classicMode.startBattle([ Species.TREECKO, Species.MEOWTH, Species.WEEZING ]); - const playerPokemon = game.scene.getPlayerParty(); - const treecko = playerPokemon[0]; - const weezing = playerPokemon[2]; - treecko.abilityIndex = 2; - weezing.abilityIndex = 1; - const playerHeldItems = treecko.getHeldItems().length; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [ treecko, _meowth, weezing ] = game.scene.getPlayerParty(); + treecko.abilityIndex = 2; // Treecko has Unburden + weezing.abilityIndex = 1; // Weezing has Neutralizing Gas + const playerHeldItems = getHeldItemCount(treecko); const initialPlayerSpeed = treecko.getStat(Stat.SPD); + // Turn 1: Treecko gets hit by False Swipe and eats Sitrus Berry, activating Unburden game.move.select(Moves.SPLASH); - game.move.select(Moves.SPLASH); + game.move.select(Moves.SPLASH, 1); await game.forceEnemyMove(Moves.FALSE_SWIPE, 0); await game.forceEnemyMove(Moves.FALSE_SWIPE, 0); await game.phaseInterceptor.to("TurnEndPhase"); - expect(treecko.getHeldItems().length).toBeLessThan(playerHeldItems); - expect(treecko.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialPlayerSpeed * 2); + expect(getHeldItemCount(treecko)).toBeLessThan(playerHeldItems); + expect(treecko.getEffectiveStat(Stat.SPD)).toBe(initialPlayerSpeed * 2); + + // Turn 2: Switch Meowth to Weezing, activating Neutralizing Gas + await game.toNextTurn(); + game.move.select(Moves.SPLASH); + game.doSwitchPokemon(2); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(getHeldItemCount(treecko)).toBeLessThan(playerHeldItems); + expect(treecko.getEffectiveStat(Stat.SPD)).toBe(initialPlayerSpeed); + // Turn 3: Switch Weezing to Meowth, deactivating Neutralizing Gas await game.toNextTurn(); game.move.select(Moves.SPLASH); game.doSwitchPokemon(2); await game.phaseInterceptor.to("TurnEndPhase"); - expect(treecko.getHeldItems().length).toBeLessThan(playerHeldItems); - expect(treecko.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialPlayerSpeed); + expect(getHeldItemCount(treecko)).toBeLessThan(playerHeldItems); + expect(treecko.getEffectiveStat(Stat.SPD)).toBe(initialPlayerSpeed * 2); + }); + + it("should not activate when passing a baton to a teammate switching in", async () => { + game.override.startingHeldItems([{ name: "BATON" }]) + .moveset(Moves.BATON_PASS); + await game.classicMode.startBattle([ Species.TREECKO, Species.PURRLOIN ]); + + const [ treecko, purrloin ] = game.scene.getPlayerParty(); + const initialTreeckoSpeed = treecko.getStat(Stat.SPD); + const initialPurrloinSpeed = purrloin.getStat(Stat.SPD); + const unburdenAttr = treecko.getAbilityAttrs(PostItemLostAbAttr)[0]; + vi.spyOn(unburdenAttr, "applyPostItemLost"); + + // Player uses Baton Pass, which also passes the Baton item + game.move.select(Moves.BATON_PASS); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + expect(getHeldItemCount(treecko)).toBe(0); + expect(getHeldItemCount(purrloin)).toBe(1); + expect(treecko.getEffectiveStat(Stat.SPD)).toBe(initialTreeckoSpeed); + expect(purrloin.getEffectiveStat(Stat.SPD)).toBe(initialPurrloinSpeed); + expect(unburdenAttr.applyPostItemLost).not.toHaveBeenCalled(); + }); + + it("should not speed up a Pokemon after it loses the ability Unburden", async () => { + game.override.enemyMoveset([ Moves.FALSE_SWIPE, Moves.WORRY_SEED ]); + await game.classicMode.startBattle([ Species.PURRLOIN ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const playerHeldItems = getHeldItemCount(playerPokemon); + const initialPlayerSpeed = playerPokemon.getStat(Stat.SPD); + + // Turn 1: Get hit by False Swipe and eat Sitrus Berry, activating Unburden + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.FALSE_SWIPE); + await game.toNextTurn(); + + expect(getHeldItemCount(playerPokemon)).toBeLessThan(playerHeldItems); + expect(playerPokemon.getEffectiveStat(Stat.SPD)).toBe(initialPlayerSpeed * 2); + + // Turn 2: Get hit by Worry Seed, deactivating Unburden + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.WORRY_SEED); + await game.toNextTurn(); + + expect(getHeldItemCount(playerPokemon)).toBeLessThan(playerHeldItems); + expect(playerPokemon.getEffectiveStat(Stat.SPD)).toBe(initialPlayerSpeed); + }); + + it("should activate when a reviver seed is used", async () => { + game.override.startingHeldItems([{ name: "REVIVER_SEED" }]) + .enemyMoveset([ Moves.WING_ATTACK ]); + await game.classicMode.startBattle([ Species.TREECKO ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const playerHeldItems = getHeldItemCount(playerPokemon); + const initialPlayerSpeed = playerPokemon.getStat(Stat.SPD); + + // Turn 1: Get hit by Wing Attack and faint, activating Reviver Seed + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + + expect(getHeldItemCount(playerPokemon)).toBeLessThan(playerHeldItems); + expect(playerPokemon.getEffectiveStat(Stat.SPD)).toBe(initialPlayerSpeed * 2); }); + // test for `.bypassFaint()` - singles + it("shouldn't persist when revived normally if activated while fainting", async () => { + game.override.enemyMoveset([ Moves.SPLASH, Moves.THIEF ]); + await game.classicMode.startBattle([ Species.TREECKO, Species.FEEBAS ]); + + const treecko = game.scene.getPlayerPokemon()!; + const treeckoInitialHeldItems = getHeldItemCount(treecko); + const initialSpeed = treecko.getStat(Stat.SPD); + + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.THIEF); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + game.doRevivePokemon(1); + game.doSwitchPokemon(1); + await game.forceEnemyMove(Moves.SPLASH); + await game.toNextTurn(); + + expect(game.scene.getPlayerPokemon()!).toBe(treecko); + expect(getHeldItemCount(treecko)).toBeLessThan(treeckoInitialHeldItems); + expect(treecko.getEffectiveStat(Stat.SPD)).toBe(initialSpeed); + }); + + // test for `.bypassFaint()` - doubles + it("shouldn't persist when revived by revival blessing if activated while fainting", async () => { + game.override + .battleType("double") + .enemyMoveset([ Moves.SPLASH, Moves.THIEF ]) + .moveset([ Moves.SPLASH, Moves.REVIVAL_BLESSING ]) + .startingHeldItems([{ name: "WIDE_LENS" }]); + await game.classicMode.startBattle([ Species.TREECKO, Species.FEEBAS, Species.MILOTIC ]); + + const treecko = game.scene.getPlayerField()[0]; + const treeckoInitialHeldItems = getHeldItemCount(treecko); + const initialSpeed = treecko.getStat(Stat.SPD); + + game.move.select(Moves.SPLASH); + game.move.select(Moves.REVIVAL_BLESSING, 1); + await game.forceEnemyMove(Moves.THIEF, BattlerIndex.PLAYER); + await game.forceEnemyMove(Moves.SPLASH); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER_2 ]); + game.doSelectPartyPokemon(0, "MoveEffectPhase"); + await game.toNextTurn(); + + expect(game.scene.getPlayerField()[0]).toBe(treecko); + expect(getHeldItemCount(treecko)).toBeLessThan(treeckoInitialHeldItems); + expect(treecko.getEffectiveStat(Stat.SPD)).toBe(initialSpeed); + }); }); From f2a2281ff11c5b644698121f6ce33f32830898b0 Mon Sep 17 00:00:00 2001 From: chaosgrimmon <31082757+chaosgrimmon@users.noreply.github.com> Date: Sun, 10 Nov 2024 14:37:21 -0500 Subject: [PATCH 13/37] [Sprite] Implement female icon assets for Meganium and Doduo + Torchic lines (#4841) * [Sprite] Implement more female icons * [Sprite] Add female Doduo/Dodrio icons * [Sprite] Add female Meganium icons * [Sprite] Add female Torchic line icons * [Sprite] Add female Meganium icons * [Sprite] Add female Torchic line icons Identical to male counterpart icons --- public/images/pokemon/icons/2/154-f.png | Bin 0 -> 427 bytes public/images/pokemon/icons/2/154s-f.png | Bin 0 -> 464 bytes public/images/pokemon/icons/3/255-f.png | Bin 0 -> 267 bytes public/images/pokemon/icons/3/255s-f.png | Bin 0 -> 268 bytes public/images/pokemon/icons/3/256-f.png | Bin 0 -> 373 bytes public/images/pokemon/icons/3/256s-f.png | Bin 0 -> 451 bytes public/images/pokemon/icons/3/257-f-mega.png | Bin 0 -> 493 bytes public/images/pokemon/icons/3/257-f.png | Bin 0 -> 451 bytes public/images/pokemon/icons/3/257s-f-mega.png | Bin 0 -> 524 bytes public/images/pokemon/icons/3/257s-f.png | Bin 0 -> 530 bytes public/images/pokemon_icons_1.json | 84 ++++++++++++ public/images/pokemon_icons_2.json | 42 ++++++ public/images/pokemon_icons_3.json | 126 ++++++++++++++++++ src/data/pokemon-species.ts | 6 + 14 files changed, 258 insertions(+) create mode 100644 public/images/pokemon/icons/2/154-f.png create mode 100644 public/images/pokemon/icons/2/154s-f.png create mode 100644 public/images/pokemon/icons/3/255-f.png create mode 100644 public/images/pokemon/icons/3/255s-f.png create mode 100644 public/images/pokemon/icons/3/256-f.png create mode 100644 public/images/pokemon/icons/3/256s-f.png create mode 100644 public/images/pokemon/icons/3/257-f-mega.png create mode 100644 public/images/pokemon/icons/3/257-f.png create mode 100644 public/images/pokemon/icons/3/257s-f-mega.png create mode 100644 public/images/pokemon/icons/3/257s-f.png diff --git a/public/images/pokemon/icons/2/154-f.png b/public/images/pokemon/icons/2/154-f.png new file mode 100644 index 0000000000000000000000000000000000000000..6481cdd8a00bbba52c035ebd8b9867d44a82ef37 GIT binary patch literal 427 zcmV;c0aX5pP)X0004UNklb>!3`Ko}9KmPln9qguG1rl}A06mWOg+BJP{@*(Z> z*U#S8x+pO~00cn7X+CwTv7v{Tg2X20oMx2_ zw1mNw@a3K%Z-Bn!wY4UIghXOU2xsc>W$LpBBwSbzLNWTC9|{e&K{()iav;Q%S#G>jIK2-6 z0WbcEP)X0004(Nkllfr0~AoPrB*0NimODkP2q1r-!7({Tnwl_StlEdCpR>u0nZM`U)rBaMQ_e2=M9gCO(cYSKsaYI&Oy2Wi6fA0 zjq7C!MdDnB&(}lF$keiT2mbEgmMTT)6cICpZTEtlfwtv(%@s%_5<^0uJ&UO5&-48e zRI%nb_yF+_6{r|{jn}^(t|OF4fD`A{cSy9I`hmGBS|GoWJJ6mTcHif10hS9?Z$hG)8|!y91bFN*V(1a)r719vX0002eNkl}K_3|xr&Rb$_)Au<X0002fNkl+%NOFXINQwd!$zv4G=~ z>)W)`AU&>EIw0}|5Z(Mal4s9uXEKP)X0003zNklNN?OW{zzQi@!B`^SD8|W!!j2B2yMMrOhhHT9AJQ~? z?e$%?ZR;UbPFxz9Ug%m&fhO4f(=&5sJV*)Zs@H) z+v$i^IF{g~RCR4moA2EcoLWFK2=AZt(!6GqQRZ};oL}bZk~g!;$jlU zoEtaoY7P`DQXG;Fa^__e0OpJWqs8w-6$OMrNN&tO5@%u3Rk^Rcv!SrlRRASqm%`fV zUoHSviysMz4gpF^0@Vv|cVwlmOd&VBCojbXZiTo2m5-i$_X&W-4e?XY$A4K*f(EN@ Twsxr800000NkvXXu0mjf8iAT> literal 0 HcmV?d00001 diff --git a/public/images/pokemon/icons/3/256s-f.png b/public/images/pokemon/icons/3/256s-f.png new file mode 100644 index 0000000000000000000000000000000000000000..ce6608f7bc5c4a2cb11ca0705cf6b7f442acbe70 GIT binary patch literal 451 zcmV;!0X+VRP)X0004sNklQLj4P1H&kDG(<1EUoLXyy&F zu)Lr5&xXP|S3I+d7oauZt)nbUM@^lZ6i~1K?3Y1+5uaXMgNCk~1&~k@j>ptP0FRr6 z7s1+jfRl5=ngjlx-lKwX0-evbYuEnaJ}+>wedd7QhjkDj0N2jl*T8SlaQN?yM8Jb3GM?*^1jm#V6&2Z$j7aRXzJ z$pB#lVMF*Rz{#c@Lb_dFa>JY=ooN8vM<-VkV%t7se z?h!Uw%|ozB0b_751~JUBa3fb^piB`nK2#W79OaWOS5CHMk0^wMD zRpx646>>TYpoH|zTEKEHA?ZES9E?HILxhr&MaH>#k<@kVrQ3&wH1C6B9vm{xM#vU-`U8!~)f{J@Yb*c&002ovPDHLkV1gct%q#!^ literal 0 HcmV?d00001 diff --git a/public/images/pokemon/icons/3/257-f-mega.png b/public/images/pokemon/icons/3/257-f-mega.png new file mode 100644 index 0000000000000000000000000000000000000000..ed64fe8f41f3cedeca980ad3befcf181ac75f327 GIT binary patch literal 493 zcmVX0005BNkl*MZf5rBvT3fMi{R$gRx5+FvJ0Exi} zhi)5S&TcL)2Ll4BdjM)KtMscI6KHY3wqqvd(7}Uv)yojqF?1Ym@G)zF8)G9cH6DB1 z^Q7?yV!b~Qdo-QaAWs6OBb7KDfbKnx8#%w6>UEk4lSyz6DT6W$AfE8I3dPwIhcG}t zLgvC~R41e1apet!gGR`xFE|IBrK@q{OX0004sNkl zF>b>!3`KK{j=n}lUqM&t*fC?*jv0H6jvh01%~jNp%@ckI*GU1JS|WguV;S+&C;8>w z?m3djFbuUf!y;ULE%hFxt#{eTBY=-?A`K9=h;DFmkbYe!k5Y=?{}@OA-3<~z2MTag z**Cy$kN_IY_w?4_P|xlG2xKa#z0Ba6iUH1MRnfxkr$Yh;oIwMwwHVb*fm1;0t{l#_ zp2izBiOo~4Hex`QT2~0AE?^FQ1>_lpRM1sZ?oeN!9>@1Xo1=;bXHW$7XwL#@*Q=%+ zIKX&0f2@{cK^;bgHRTGq3Lud7Qk09&pqQ$s4Z|jY)J?9EVvV_V(iyGAo9AjpwmF4N z>*xk|n)g2xqf}N^@1CmETWb{YKTaA{q>hQrAOX{$A#O}sW)##p1&|YjPPMfN3m~ea ziX0005gNkl{prMAI1<+7ZrDX+_)QDx!A$CBS3K<MzwOx4 z$j;UE_{-c!oK;a2t0l+50iA($N0w#v9Y#2M1BH8i+!k$+>O(9F2YD<-Y&V5s5YUq}0V7U*QWaFr&)rBs5&L}1|^=pm0xTzrefe(|^FG)f=lAKeqx_ebWexIJGO9wxZF1`}dF3daON(#t{K-jXD9*M3 O0000X0005mNkllc3X0T`H~<9&H5Wj%IRW*uZ?&KG7&#F?_5u!U_6}i z;Uz~R4x_`qUKLfVhs6 zO=A$q&Bf6Oq}EFhkbeBBf0&oUquKkQCy2mYu|873X@MN>+r6-Qjf`-5I2v6VprV>P zSSsWZ?hvFF188%=5>$i(D7rX{gIS8Hf;a{<)bv8p6!5bbArSAY{BZyOZ{`Df1F=YV U=XA_GX#fBK07*qoM6N<$g1^J=Hvj+t literal 0 HcmV?d00001 diff --git a/public/images/pokemon_icons_1.json b/public/images/pokemon_icons_1.json index 49e471514cd6..12e26b380a5d 100644 --- a/public/images/pokemon_icons_1.json +++ b/public/images/pokemon_icons_1.json @@ -1647,6 +1647,27 @@ "h": 25 } }, + { + "filename": "85-f", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 5, + "y": 3, + "w": 29, + "h": 25 + }, + "frame": { + "x": 55, + "y": 270, + "w": 29, + "h": 25 + } + }, { "filename": "22s", "rotated": false, @@ -1731,6 +1752,27 @@ "h": 25 } }, + { + "filename": "85s-f", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 5, + "y": 3, + "w": 29, + "h": 25 + }, + "frame": { + "x": 56, + "y": 317, + "w": 29, + "h": 25 + } + }, { "filename": "9s", "rotated": false, @@ -6456,6 +6498,27 @@ "h": 18 } }, + { + "filename": "84-f", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 9, + "y": 10, + "w": 21, + "h": 18 + }, + "frame": { + "x": 98, + "y": 712, + "w": 21, + "h": 18 + } + }, { "filename": "107", "rotated": false, @@ -6519,6 +6582,27 @@ "h": 18 } }, + { + "filename": "84s-f", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 9, + "y": 10, + "w": 21, + "h": 18 + }, + "frame": { + "x": 96, + "y": 770, + "w": 21, + "h": 18 + } + }, { "filename": "88", "rotated": false, diff --git a/public/images/pokemon_icons_2.json b/public/images/pokemon_icons_2.json index 5a389362bc0c..c5ebfe614875 100644 --- a/public/images/pokemon_icons_2.json +++ b/public/images/pokemon_icons_2.json @@ -786,6 +786,27 @@ "h": 27 } }, + { + "filename": "154-f", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 8, + "y": 1, + "w": 23, + "h": 27 + }, + "frame": { + "x": 29, + "y": 147, + "w": 23, + "h": 27 + } + }, { "filename": "154s", "rotated": false, @@ -807,6 +828,27 @@ "h": 27 } }, + { + "filename": "154s-f", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 8, + "y": 1, + "w": 23, + "h": 27 + }, + "frame": { + "x": 29, + "y": 174, + "w": 23, + "h": 27 + } + }, { "filename": "229-mega", "rotated": false, diff --git a/public/images/pokemon_icons_3.json b/public/images/pokemon_icons_3.json index 220d91f52225..a1aefa0ff0b1 100644 --- a/public/images/pokemon_icons_3.json +++ b/public/images/pokemon_icons_3.json @@ -198,6 +198,27 @@ "h": 27 } }, + { + "filename": "257-f-mega", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 4, + "y": 2, + "w": 32, + "h": 27 + }, + "frame": { + "x": 0, + "y": 79, + "w": 32, + "h": 27 + } + }, { "filename": "257s-mega", "rotated": false, @@ -219,6 +240,27 @@ "h": 27 } }, + { + "filename": "257s-f-mega", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 4, + "y": 2, + "w": 32, + "h": 27 + }, + "frame": { + "x": 0, + "y": 106, + "w": 32, + "h": 27 + } + }, { "filename": "323-mega", "rotated": false, @@ -1248,6 +1290,27 @@ "h": 26 } }, + { + "filename": "257-f", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 7, + "y": 2, + "w": 25, + "h": 26 + }, + "frame": { + "x": 28, + "y": 556, + "w": 25, + "h": 26 + } + }, { "filename": "257s", "rotated": false, @@ -1269,6 +1332,27 @@ "h": 26 } }, + { + "filename": "257s-f", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 7, + "y": 2, + "w": 25, + "h": 26 + }, + "frame": { + "x": 28, + "y": 582, + "w": 25, + "h": 26 + } + }, { "filename": "359-mega", "rotated": false, @@ -1605,6 +1689,27 @@ "h": 25 } }, + { + "filename": "256-f", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 8, + "y": 3, + "w": 23, + "h": 25 + }, + "frame": { + "x": 98, + "y": 72, + "w": 23, + "h": 25 + } + }, { "filename": "282s-mega", "rotated": false, @@ -5553,6 +5658,27 @@ "h": 19 } }, + { + "filename": "255-f", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 40, + "h": 30 + }, + "spriteSourceSize": { + "x": 13, + "y": 9, + "w": 13, + "h": 19 + }, + "frame": { + "x": 204, + "y": 342, + "w": 13, + "h": 19 + } + }, { "filename": "307s", "rotated": false, diff --git a/src/data/pokemon-species.ts b/src/data/pokemon-species.ts index ff53fdb9392f..203e545503a2 100644 --- a/src/data/pokemon-species.ts +++ b/src/data/pokemon-species.ts @@ -363,6 +363,12 @@ export abstract class PokemonSpeciesForm { } switch (this.speciesId) { + case Species.DODUO: + case Species.DODRIO: + case Species.MEGANIUM: + case Species.TORCHIC: + case Species.COMBUSKEN: + case Species.BLAZIKEN: case Species.HIPPOPOTAS: case Species.HIPPOWDON: case Species.UNFEZANT: From efa9f119a0e803ff13409e67adaa9f1dbef206b0 Mon Sep 17 00:00:00 2001 From: PigeonBar <56974298+PigeonBar@users.noreply.github.com> Date: Mon, 11 Nov 2024 02:18:57 -0500 Subject: [PATCH 14/37] [Beta][P3] Fix shiny Pokemon being displayed before shiny colours are loaded (#4843) --- src/field/pokemon.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index d806a9b605c0..9e5103656d38 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -442,7 +442,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { }; if (this.shiny) { const populateVariantColors = (isBackSprite: boolean = false): Promise => { - return new Promise(resolve => { + return new Promise(async resolve => { const battleSpritePath = this.getBattleSpriteAtlasPath(isBackSprite, ignoreOverride).replace("variant/", "").replace(/_[1-3]$/, ""); let config = variantData; const useExpSprite = this.scene.experimentalSprites && this.scene.hasExpSprite(this.getBattleSpriteKey(isBackSprite, ignoreOverride)); @@ -451,7 +451,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (variantSet && variantSet[this.variant] === 1) { const cacheKey = this.getBattleSpriteKey(isBackSprite); if (!variantColorCache.hasOwnProperty(cacheKey)) { - this.populateVariantColorCache(cacheKey, useExpSprite, battleSpritePath); + await this.populateVariantColorCache(cacheKey, useExpSprite, battleSpritePath); } } resolve(); @@ -483,10 +483,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @param battleSpritePath the filename of the sprite * @param optionalParams any additional params to log */ - fallbackVariantColor(cacheKey: string, attemptedSpritePath: string, useExpSprite: boolean, battleSpritePath: string, ...optionalParams: any[]) { + async fallbackVariantColor(cacheKey: string, attemptedSpritePath: string, useExpSprite: boolean, battleSpritePath: string, ...optionalParams: any[]) { console.warn(`Could not load ${attemptedSpritePath}!`, ...optionalParams); if (useExpSprite) { - this.populateVariantColorCache(cacheKey, false, battleSpritePath); + await this.populateVariantColorCache(cacheKey, false, battleSpritePath); } } @@ -497,18 +497,20 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @param useExpSprite should the experimental sprite be used * @param battleSpritePath the filename of the sprite */ - populateVariantColorCache(cacheKey: string, useExpSprite: boolean, battleSpritePath: string) { + async populateVariantColorCache(cacheKey: string, useExpSprite: boolean, battleSpritePath: string) { const spritePath = `./images/pokemon/variant/${useExpSprite ? "exp/" : ""}${battleSpritePath}.json`; - this.scene.cachedFetch(spritePath).then(res => { + return this.scene.cachedFetch(spritePath).then(res => { // Prevent the JSON from processing if it failed to load if (!res.ok) { return this.fallbackVariantColor(cacheKey, res.url, useExpSprite, battleSpritePath, res.status, res.statusText); } return res.json(); }).catch(error => { - this.fallbackVariantColor(cacheKey, spritePath, useExpSprite, battleSpritePath, error); + return this.fallbackVariantColor(cacheKey, spritePath, useExpSprite, battleSpritePath, error); }).then(c => { - variantColorCache[cacheKey] = c; + if (!isNullOrUndefined(c)) { + variantColorCache[cacheKey] = c; + } }); } From 6799594bbb43a8a7a8431a5f7c52062ee917dd3e Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Sun, 10 Nov 2024 23:21:06 -0800 Subject: [PATCH 15/37] [Test] Update Zen Mode test (#4845) --- src/test/abilities/zen_mode.test.ts | 87 ++++++++++------------------- 1 file changed, 31 insertions(+), 56 deletions(-) diff --git a/src/test/abilities/zen_mode.test.ts b/src/test/abilities/zen_mode.test.ts index 4ba5e3d5929b..e0cc457c4d50 100644 --- a/src/test/abilities/zen_mode.test.ts +++ b/src/test/abilities/zen_mode.test.ts @@ -1,14 +1,8 @@ -import { BattlerIndex } from "#app/battle"; import { Status } from "#app/data/status-effect"; -import { DamagePhase } from "#app/phases/damage-phase"; -import { SwitchSummonPhase } from "#app/phases/switch-summon-phase"; -import { Mode } from "#app/ui/ui"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; -import { Stat } from "#enums/stat"; import { StatusEffect } from "#enums/status-effect"; -import { SwitchType } from "#enums/switch-type"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; @@ -34,78 +28,60 @@ describe("Abilities - ZEN MODE", () => { game = new GameManager(phaserGame); game.override .battleType("single") - .enemySpecies(Species.RATTATA) - .enemyAbility(Abilities.HYDRATION) + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyLevel(5) .ability(Abilities.ZEN_MODE) - .startingLevel(100) .moveset(Moves.SPLASH) - .enemyMoveset(Moves.TACKLE); + .enemyMoveset(Moves.SEISMIC_TOSS); }); it("shouldn't change form when taking damage if not dropping below 50% HP", async () => { await game.classicMode.startBattle([ Species.DARMANITAN ]); - const player = game.scene.getPlayerPokemon()!; - player.stats[Stat.HP] = 100; - player.hp = 100; - expect(player.formIndex).toBe(baseForm); + const darmanitan = game.scene.getPlayerPokemon()!; + expect(darmanitan.formIndex).toBe(baseForm); game.move.select(Moves.SPLASH); - await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); - await game.phaseInterceptor.to("BerryPhase"); + await game.toNextTurn(); - expect(player.hp).toBeLessThan(100); - expect(player.formIndex).toBe(baseForm); + expect(darmanitan.getHpRatio()).toBeLessThan(1); + expect(darmanitan.getHpRatio()).toBeGreaterThan(0.5); + expect(darmanitan.formIndex).toBe(baseForm); }); it("should change form when falling below 50% HP", async () => { await game.classicMode.startBattle([ Species.DARMANITAN ]); - const player = game.scene.getPlayerPokemon()!; - player.stats[Stat.HP] = 1000; - player.hp = 100; - expect(player.formIndex).toBe(baseForm); + const darmanitan = game.scene.getPlayerPokemon()!; + darmanitan.hp = (darmanitan.getMaxHp() / 2) + 1; + expect(darmanitan.formIndex).toBe(baseForm); game.move.select(Moves.SPLASH); + await game.toNextTurn(); - await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); - await game.phaseInterceptor.to("QuietFormChangePhase"); - await game.phaseInterceptor.to("TurnInitPhase", false); - - expect(player.hp).not.toBe(100); - expect(player.formIndex).toBe(zenForm); + expect(darmanitan.getHpRatio()).toBeLessThan(0.5); + expect(darmanitan.formIndex).toBe(zenForm); }); it("should stay zen mode when fainted", async () => { await game.classicMode.startBattle([ Species.DARMANITAN, Species.CHARIZARD ]); - const player = game.scene.getPlayerPokemon()!; - player.stats[Stat.HP] = 1000; - player.hp = 100; - expect(player.formIndex).toBe(baseForm); + const darmanitan = game.scene.getPlayerPokemon()!; + darmanitan.hp = (darmanitan.getMaxHp() / 2) + 1; + expect(darmanitan.formIndex).toBe(baseForm); game.move.select(Moves.SPLASH); + await game.toNextTurn(); - await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); - await game.phaseInterceptor.to(DamagePhase, false); - const damagePhase = game.scene.getCurrentPhase() as DamagePhase; - damagePhase.updateAmount(80); - await game.phaseInterceptor.to("QuietFormChangePhase"); - - expect(player.hp).not.toBe(100); - expect(player.formIndex).toBe(zenForm); - - await game.killPokemon(player); - expect(player.isFainted()).toBe(true); + expect(darmanitan.getHpRatio()).toBeLessThan(0.5); + expect(darmanitan.formIndex).toBe(zenForm); - await game.phaseInterceptor.to("TurnStartPhase"); - game.onNextPrompt("SwitchPhase", Mode.PARTY, () => { - game.scene.unshiftPhase(new SwitchSummonPhase(game.scene, SwitchType.SWITCH, 0, 1, false)); - game.scene.ui.setMode(Mode.MESSAGE); - }); - game.onNextPrompt("SwitchPhase", Mode.MESSAGE, () => { - game.endPhase(); - }); - await game.phaseInterceptor.to("PostSummonPhase"); + game.move.select(Moves.SPLASH); + await game.killPokemon(darmanitan); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + expect(darmanitan.isFainted()).toBe(true); expect(game.scene.getPlayerParty()[1].formIndex).toBe(zenForm); }); @@ -117,7 +93,8 @@ describe("Abilities - ZEN MODE", () => { await game.classicMode.startBattle([ Species.MAGIKARP, Species.DARMANITAN ]); - const darmanitan = game.scene.getPlayerParty().find((p) => p.species.speciesId === Species.DARMANITAN)!; + const darmanitan = game.scene.getPlayerParty()[1]; + darmanitan.hp = 1; expect(darmanitan.formIndex).toBe(zenForm); darmanitan.hp = 0; @@ -126,9 +103,7 @@ describe("Abilities - ZEN MODE", () => { game.move.select(Moves.SPLASH); await game.doKillOpponents(); - await game.phaseInterceptor.to("TurnEndPhase"); - game.doSelectModifier(); - await game.phaseInterceptor.to("QuietFormChangePhase"); + await game.toNextWave(); expect(darmanitan.formIndex).toBe(baseForm); }); From 6feb63484c308d3f376e1f77ed2c05ae760d9fb3 Mon Sep 17 00:00:00 2001 From: Mumble <171087428+frutescens@users.noreply.github.com> Date: Mon, 11 Nov 2024 09:29:20 -0800 Subject: [PATCH 16/37] [P3] Added `failIfSingleBattle` condtion to Doubles-only moves and display failure message when used in singles (#4839) * Added failIfSingleBattle condtion to Helping Hand * Added failIfSingleBattle conditions to Doubles-Only moves * Adjusted canMove failure condition. * Updated moves that failIfSingleBattle * Fixed condtional. --------- Co-authored-by: frutescens --- src/data/move.ts | 11 ++++++++--- src/phases/move-phase.ts | 19 ++++++------------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/data/move.ts b/src/data/move.ts index 9cd4881488cc..a79ac386a7e3 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -8480,7 +8480,8 @@ export function initMoves() { new StatusMove(Moves.HELPING_HAND, Type.NORMAL, -1, 20, -1, 5, 3) .attr(AddBattlerTagAttr, BattlerTagType.HELPING_HAND) .ignoresSubstitute() - .target(MoveTarget.NEAR_ALLY), + .target(MoveTarget.NEAR_ALLY) + .condition(failIfSingleBattle), new StatusMove(Moves.TRICK, Type.PSYCHIC, 100, 10, -1, 0, 3) .unimplemented(), new StatusMove(Moves.ROLE_PLAY, Type.PSYCHIC, -1, 10, -1, 0, 3) @@ -9172,6 +9173,7 @@ export function initMoves() { .target(MoveTarget.ALL_NEAR_ENEMIES) .attr(RemoveHeldItemAttr, true), new StatusMove(Moves.QUASH, Type.DARK, 100, 15, -1, 0, 5) + .condition(failIfSingleBattle) .unimplemented(), new AttackMove(Moves.ACROBATICS, Type.FLYING, MoveCategory.PHYSICAL, 55, 100, 15, -1, 0, 5) .attr(MovePowerMultiplierAttr, (user, target, move) => Math.max(1, 2 - 0.2 * user.getHeldItems().filter(i => i.isTransferable).reduce((v, m) => v + m.stackCount, 0))), @@ -9459,6 +9461,7 @@ export function initMoves() { new StatusMove(Moves.AROMATIC_MIST, Type.FAIRY, -1, 20, -1, 0, 6) .attr(StatStageChangeAttr, [ Stat.SPDEF ], 1) .ignoresSubstitute() + .condition(failIfSingleBattle) .target(MoveTarget.NEAR_ALLY), new StatusMove(Moves.EERIE_IMPULSE, Type.ELECTRIC, 100, 15, -1, 0, 6) .attr(StatStageChangeAttr, [ Stat.SPATK ], -2), @@ -9687,7 +9690,8 @@ export function initMoves() { new AttackMove(Moves.LEAFAGE, Type.GRASS, MoveCategory.PHYSICAL, 40, 100, 40, -1, 0, 7) .makesContact(false), new StatusMove(Moves.SPOTLIGHT, Type.NORMAL, -1, 15, -1, 3, 7) - .attr(AddBattlerTagAttr, BattlerTagType.CENTER_OF_ATTENTION, false), + .attr(AddBattlerTagAttr, BattlerTagType.CENTER_OF_ATTENTION, false) + .condition(failIfSingleBattle), new StatusMove(Moves.TOXIC_THREAD, Type.POISON, 100, 20, -1, 0, 7) .attr(StatusEffectAttr, StatusEffect.POISON) .attr(StatStageChangeAttr, [ Stat.SPD ], -1), @@ -10144,7 +10148,8 @@ export function initMoves() { .unimplemented(), new StatusMove(Moves.COACHING, Type.FIGHTING, -1, 10, -1, 0, 8) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF ], 1) - .target(MoveTarget.NEAR_ALLY), + .target(MoveTarget.NEAR_ALLY) + .condition(failIfSingleBattle), new AttackMove(Moves.FLIP_TURN, Type.WATER, MoveCategory.PHYSICAL, 60, 100, 20, -1, 0, 8) .attr(ForceSwitchOutAttr, true), new AttackMove(Moves.TRIPLE_AXEL, Type.ICE, MoveCategory.PHYSICAL, 20, 90, 10, -1, 0, 8) diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index 6bdef281d701..7cfa3b12476d 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -120,13 +120,10 @@ export class MovePhase extends BattlePhase { console.log(Moves[this.move.moveId]); // Check if move is unusable (e.g. because it's out of PP due to a mid-turn Spite). - if (!this.canMove(true)) { - if (this.pokemon.isActive(true) && this.move.ppUsed >= this.move.getMovePp()) { - this.fail(); - this.showMoveText(); - this.showFailedText(); - } - + if (!this.canMove(true) && (this.pokemon.isActive(true) || this.move.ppUsed >= this.move.getMovePp())) { + this.fail(); + this.showMoveText(); + this.showFailedText(); return this.end(); } @@ -378,16 +375,12 @@ export class MovePhase extends BattlePhase { } else { this.pokemon.pushMoveHistory({ move: this.move.moveId, targets: this.targets, result: MoveResult.FAIL, virtual: this.move.virtual }); - let failedText: string | undefined; const failureMessage = move.getFailedText(this.pokemon, targets[0], move, new BooleanHolder(false)); - if (failureMessage) { - failedText = failureMessage; + this.showMoveText(); + this.showFailedText(failureMessage); } - this.showMoveText(); - this.showFailedText(failedText); - // Remove the user from its semi-invulnerable state (if applicable) this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT); } From e5e392617615ec5023afe5662ceb3cd7c0720f6d Mon Sep 17 00:00:00 2001 From: Mumble <171087428+frutescens@users.noreply.github.com> Date: Mon, 11 Nov 2024 12:13:15 -0800 Subject: [PATCH 17/37] [beta] Fix MovePhase not ending properly. (#4848) Co-authored-by: frutescens --- src/phases/move-phase.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index 7cfa3b12476d..378b72e1f56a 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -120,10 +120,12 @@ export class MovePhase extends BattlePhase { console.log(Moves[this.move.moveId]); // Check if move is unusable (e.g. because it's out of PP due to a mid-turn Spite). - if (!this.canMove(true) && (this.pokemon.isActive(true) || this.move.ppUsed >= this.move.getMovePp())) { - this.fail(); - this.showMoveText(); - this.showFailedText(); + if (!this.canMove(true)) { + if (this.pokemon.isActive(true)) { + this.fail(); + this.showMoveText(); + this.showFailedText(); + } return this.end(); } From cebedd220bd4259b4bee449bb88cc686853b3474 Mon Sep 17 00:00:00 2001 From: innerthunder <168692175+innerthunder@users.noreply.github.com> Date: Mon, 11 Nov 2024 14:56:16 -0800 Subject: [PATCH 18/37] [Balance] Rework Multi-Lens (#4831) * Rework Multi-Lens * Multi-Lens integration tests * Apply suggestions from code review Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Fix obsolete tests related to Multi-Lens * Fix flaky unburden tests * maybe fix flaky ceaseless edge test? * Fixed Multi-Lens apply comment * Fix ceaseless edge test for real this time * Update locales * Another locale update --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- public/locales | 2 +- src/data/ability.ts | 61 ++++----------- src/data/move.ts | 38 ++++++++- src/field/pokemon.ts | 9 ++- src/modifier/modifier.ts | 62 ++++++++++----- src/phases/move-effect-phase.ts | 11 +-- src/test/abilities/parental_bond.test.ts | 60 ++------------- src/test/items/multi_lens.test.ts | 98 ++++++++++++++++++++++++ src/test/moves/beat_up.test.ts | 25 ------ src/test/moves/ceaseless_edge.test.ts | 12 +-- src/test/moves/dragon_rage.test.ts | 11 --- src/test/moves/electro_shot.test.ts | 2 +- 12 files changed, 214 insertions(+), 177 deletions(-) create mode 100644 src/test/items/multi_lens.test.ts diff --git a/public/locales b/public/locales index d600913dbf1f..5775faa6b318 160000 --- a/public/locales +++ b/public/locales @@ -1 +1 @@ -Subproject commit d600913dbf1f8b47dae8dccbd8296df78f1c51b5 +Subproject commit 5775faa6b3184082df73f6cdb96b253ea7dae3fe diff --git a/src/data/ability.ts b/src/data/ability.ts index 49763991e0e7..4194be314052 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -7,7 +7,7 @@ import { Weather } from "#app/data/weather"; import { BattlerTag, BattlerTagLapseType, GroundedTag } from "./battler-tags"; import { getNonVolatileStatusEffects, getStatusEffectDescriptor, getStatusEffectHealText } from "#app/data/status-effect"; import { Gender } from "./gender"; -import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr, MoveAttr, MultiHitAttr, SacrificialAttr, SacrificialAttrOnHit, NeutralDamageAgainstFlyingTypeMultiplierAttr, FixedDamageAttr } from "./move"; +import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr, NeutralDamageAgainstFlyingTypeMultiplierAttr, FixedDamageAttr } from "./move"; import { ArenaTagSide, ArenaTrapTag } from "./arena-tag"; import { BerryModifier, HitHealModifier, PokemonHeldItemModifier } from "../modifier/modifier"; import { TerrainType } from "./terrain"; @@ -1351,65 +1351,30 @@ export class AddSecondStrikeAbAttr extends PreAttackAbAttr { this.damageMultiplier = damageMultiplier; } - /** - * Determines whether this attribute can apply to a given move. - * @param {Move} move the move to which this attribute may apply - * @param numTargets the number of {@linkcode Pokemon} targeted by this move - * @returns true if the attribute can apply to the move, false otherwise - */ - canApplyPreAttack(move: Move, numTargets: integer): boolean { - /** - * Parental Bond cannot apply to multi-hit moves, charging moves, or - * moves that cause the user to faint. - */ - const exceptAttrs: Constructor[] = [ - MultiHitAttr, - SacrificialAttr, - SacrificialAttrOnHit - ]; - - /** Parental Bond cannot apply to these specific moves */ - const exceptMoves: Moves[] = [ - Moves.FLING, - Moves.UPROAR, - Moves.ROLLOUT, - Moves.ICE_BALL, - Moves.ENDEAVOR - ]; - - /** Also check if this move is an Attack move and if it's only targeting one Pokemon */ - return numTargets === 1 - && !move.isChargingMove() - && !exceptAttrs.some(attr => move.hasAttr(attr)) - && !exceptMoves.some(id => move.id === id) - && move.category !== MoveCategory.STATUS; - } - /** * If conditions are met, this doubles the move's hit count (via args[1]) * or multiplies the damage of secondary strikes (via args[2]) - * @param {Pokemon} pokemon the Pokemon using the move + * @param pokemon the {@linkcode Pokemon} using the move * @param passive n/a * @param defender n/a - * @param {Move} move the move used by the ability source - * @param args\[0\] the number of Pokemon this move is targeting - * @param {Utils.IntegerHolder} args\[1\] the number of strikes with this move - * @param {Utils.NumberHolder} args\[2\] the damage multiplier for the current strike + * @param move the {@linkcode Move} used by the ability source + * @param args Additional arguments: + * - `[0]` the number of strikes this move currently has ({@linkcode Utils.NumberHolder}) + * - `[1]` the damage multiplier for the current strike ({@linkcode Utils.NumberHolder}) * @returns */ applyPreAttack(pokemon: Pokemon, passive: boolean, simulated: boolean, defender: Pokemon, move: Move, args: any[]): boolean { - const numTargets = args[0] as integer; - const hitCount = args[1] as Utils.IntegerHolder; - const multiplier = args[2] as Utils.NumberHolder; + const hitCount = args[0] as Utils.NumberHolder; + const multiplier = args[1] as Utils.NumberHolder; - if (this.canApplyPreAttack(move, numTargets)) { + if (move.canBeMultiStrikeEnhanced(pokemon)) { this.showAbility = !!hitCount?.value; - if (!!hitCount?.value) { - hitCount.value *= 2; + if (hitCount?.value) { + hitCount.value += 1; } - if (!!multiplier?.value && pokemon.turnData.hitsLeft % 2 === 1 && pokemon.turnData.hitsLeft !== pokemon.turnData.hitCount) { - multiplier.value *= this.damageMultiplier; + if (multiplier?.value && pokemon.turnData.hitsLeft === 1) { + multiplier.value = this.damageMultiplier; } return true; } diff --git a/src/data/move.ts b/src/data/move.ts index a79ac386a7e3..0de9d9b53a2d 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -818,7 +818,7 @@ export default class Move implements Localizable { applyMoveAttrs(VariablePowerAttr, source, target, this, power); - source.scene.applyModifiers(PokemonMultiHitModifier, source.isPlayer(), source, new Utils.IntegerHolder(0), power); + source.scene.applyModifiers(PokemonMultiHitModifier, source.isPlayer(), source, this.id, null, power); if (!this.hasAttr(TypelessAttr)) { source.scene.arena.applyTags(WeakenMoveTypeTag, simulated, this.type, power); @@ -840,6 +840,42 @@ export default class Move implements Localizable { return priority.value; } + + /** + * Returns `true` if this move can be given additional strikes + * by enhancing effects. + * Currently used for {@link https://bulbapedia.bulbagarden.net/wiki/Parental_Bond_(Ability) | Parental Bond} + * and {@linkcode PokemonMultiHitModifier | Multi-Lens}. + */ + canBeMultiStrikeEnhanced(user: Pokemon): boolean { + // Multi-strike enhancers... + + // ...cannot enhance moves that hit multiple targets + const { targets, multiple } = getMoveTargets(user, this.id); + const isMultiTarget = multiple && targets.length > 1; + + // ...cannot enhance multi-hit or sacrificial moves + const exceptAttrs: Constructor[] = [ + MultiHitAttr, + SacrificialAttr, + SacrificialAttrOnHit + ]; + + // ...and cannot enhance these specific moves. + const exceptMoves: Moves[] = [ + Moves.FLING, + Moves.UPROAR, + Moves.ROLLOUT, + Moves.ICE_BALL, + Moves.ENDEAVOR + ]; + + return !isMultiTarget + && !this.isChargingMove() + && !exceptAttrs.some(attr => this.hasAttr(attr)) + && !exceptMoves.some(id => this.id === id) + && this.category !== MoveCategory.STATUS; + } } export class AttackMove extends Move { diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 9e5103656d38..5d912f7d6e6a 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -2642,10 +2642,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const numTargets = multiple ? targets.length : 1; const targetMultiplier = (numTargets > 1) ? 0.75 : 1; - /** 0.25x multiplier if this is an added strike from the attacker's Parental Bond */ - const parentalBondMultiplier = new Utils.NumberHolder(1); + /** Multiplier for moves enhanced by Multi-Lens and/or Parental Bond */ + const multiStrikeEnhancementMultiplier = new Utils.NumberHolder(1); + source.scene.applyModifiers(PokemonMultiHitModifier, source.isPlayer(), source, move.id, null, multiStrikeEnhancementMultiplier); if (!ignoreSourceAbility) { - applyPreAttackAbAttrs(AddSecondStrikeAbAttr, source, this, move, simulated, numTargets, new Utils.IntegerHolder(0), parentalBondMultiplier); + applyPreAttackAbAttrs(AddSecondStrikeAbAttr, source, this, move, simulated, null, multiStrikeEnhancementMultiplier); } /** Doubles damage if this Pokemon's last move was Glaive Rush */ @@ -2722,7 +2723,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { damage.value = Utils.toDmgValue( baseDamage * targetMultiplier - * parentalBondMultiplier.value + * multiStrikeEnhancementMultiplier.value * arenaAttackTypeMultiplier.value * glaiveRushMultiplier.value * criticalMultiplier.value diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index 90336780ba64..5e60d8880723 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -6,7 +6,6 @@ import { allMoves } from "#app/data/move"; import { MAX_PER_TYPE_POKEBALLS } from "#app/data/pokeball"; import { type FormChangeItem, SpeciesFormChangeItemTrigger, SpeciesFormChangeLapseTeraTrigger, SpeciesFormChangeTeraTrigger } from "#app/data/pokemon-forms"; import { getStatusEffectHealText } from "#app/data/status-effect"; -import { Type } from "#enums/type"; import Pokemon, { type PlayerPokemon } from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; import Overrides from "#app/overrides"; @@ -22,11 +21,13 @@ import { BooleanHolder, hslToHex, isNullOrUndefined, NumberHolder, toDmgValue } import { Abilities } from "#enums/abilities"; import { BattlerTagType } from "#enums/battler-tag-type"; import { BerryType } from "#enums/berry-type"; +import { Moves } from "#enums/moves"; import type { Nature } from "#enums/nature"; import type { PokeballType } from "#enums/pokeball"; import { Species } from "#enums/species"; import { type PermanentStat, type TempBattleStat, BATTLE_STATS, Stat, TEMP_BATTLE_STATS } from "#enums/stat"; import { StatusEffect } from "#enums/status-effect"; +import { Type } from "#enums/type"; import i18next from "i18next"; import { type DoubleBattleChanceBoosterModifierType, type EvolutionItemModifierType, type FormChangeItemModifierType, type ModifierOverride, type ModifierType, type PokemonBaseStatTotalModifierType, type PokemonExpBoosterModifierType, type PokemonFriendshipBoosterModifierType, type PokemonMoveAccuracyBoosterModifierType, type PokemonMultiHitModifierType, type TerastallizeModifierType, type TmModifierType, getModifierType, ModifierPoolType, ModifierTypeGenerator, modifierTypes, PokemonHeldItemModifierType } from "./modifier-type"; import { Color, ShadowColor } from "#enums/color"; @@ -2689,32 +2690,57 @@ export class PokemonMultiHitModifier extends PokemonHeldItemModifier { } /** - * Applies {@linkcode PokemonMultiHitModifier} - * @param _pokemon The {@linkcode Pokemon} using the move - * @param count {@linkcode NumberHolder} holding the number of items - * @param power {@linkcode NumberHolder} holding the power of the move + * For each stack, converts 25 percent of attack damage into an additional strike. + * @param pokemon The {@linkcode Pokemon} using the move + * @param moveId The {@linkcode Moves | identifier} for the move being used + * @param count {@linkcode NumberHolder} holding the move's hit count for this turn + * @param damageMultiplier {@linkcode NumberHolder} holding a damage multiplier applied to a strike of this move * @returns always `true` */ - override apply(_pokemon: Pokemon, count: NumberHolder, power: NumberHolder): boolean { - count.value *= (this.getStackCount() + 1); + override apply(pokemon: Pokemon, moveId: Moves, count: NumberHolder | null = null, damageMultiplier: NumberHolder | null = null): boolean { + const move = allMoves[moveId]; + /** + * The move must meet Parental Bond's restrictions for this item + * to apply. This means + * - Only attacks are boosted + * - Multi-strike moves, charge moves, and self-sacrificial moves are not boosted + * (though Multi-Lens can still affect moves boosted by Parental Bond) + * - Multi-target moves are not boosted *unless* they can only hit a single Pokemon + * - Fling, Uproar, Rollout, Ice Ball, and Endeavor are not boosted + */ + if (!move.canBeMultiStrikeEnhanced(pokemon)) { + return false; + } - switch (this.getStackCount()) { - case 1: - power.value *= 0.4; - break; - case 2: - power.value *= 0.25; - break; - case 3: - power.value *= 0.175; - break; + if (!isNullOrUndefined(count)) { + return this.applyHitCountBoost(count); + } else if (!isNullOrUndefined(damageMultiplier)) { + return this.applyPowerModifier(pokemon, damageMultiplier); } + return false; + } + + /** Adds strikes to a move equal to the number of stacked Multi-Lenses */ + private applyHitCountBoost(count: NumberHolder): boolean { + count.value += this.getStackCount(); + return true; + } + + /** + * If applied to the first hit of a move, sets the damage multiplier + * equal to (1 - the number of stacked Multi-Lenses). + * Additional strikes beyond that are given a 0.25x damage multiplier + */ + private applyPowerModifier(pokemon: Pokemon, damageMultiplier: NumberHolder): boolean { + damageMultiplier.value = (pokemon.turnData.hitsLeft === pokemon.turnData.hitCount) + ? (1 - (0.25 * this.getStackCount())) + : 0.25; return true; } getMaxHeldItemCount(pokemon: Pokemon): number { - return 3; + return 2; } } diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index ef863d64c506..24a0b51da96c 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -26,7 +26,6 @@ import { applyMoveAttrs, AttackMove, DelayedAttackAttr, - FixedDamageAttr, HitsTagAttr, MissEffectAttr, MoveAttr, @@ -122,12 +121,10 @@ export class MoveEffectPhase extends PokemonPhase { const hitCount = new NumberHolder(1); // Assume single target for multi hit applyMoveAttrs(MultiHitAttr, user, this.getFirstTarget() ?? null, move, hitCount); - // If Parental Bond is applicable, double the hit count - applyPreAttackAbAttrs(AddSecondStrikeAbAttr, user, null, move, false, targets.length, hitCount, new NumberHolder(0)); - // If Multi-Lens is applicable, multiply the hit count by 1 + the number of Multi-Lenses held by the user - if (move instanceof AttackMove && !move.hasAttr(FixedDamageAttr)) { - this.scene.applyModifiers(PokemonMultiHitModifier, user.isPlayer(), user, hitCount, new NumberHolder(0)); - } + // If Parental Bond is applicable, add another hit + applyPreAttackAbAttrs(AddSecondStrikeAbAttr, user, null, move, false, hitCount, null); + // If Multi-Lens is applicable, add hits equal to the number of held Multi-Lenses + this.scene.applyModifiers(PokemonMultiHitModifier, user.isPlayer(), user, move.id, hitCount); // Set the user's relevant turnData fields to reflect the final hit count user.turnData.hitCount = hitCount.value; user.turnData.hitsLeft = hitCount.value; diff --git a/src/test/abilities/parental_bond.test.ts b/src/test/abilities/parental_bond.test.ts index d8f952ae6ade..4189941a51ef 100644 --- a/src/test/abilities/parental_bond.test.ts +++ b/src/test/abilities/parental_bond.test.ts @@ -274,7 +274,7 @@ describe("Abilities - Parental Bond", () => { ); it( - "Moves boosted by this ability and Multi-Lens should strike 4 times", + "Moves boosted by this ability and Multi-Lens should strike 3 times", async () => { game.override.moveset([ Moves.TACKLE ]); game.override.startingHeldItems([{ name: "MULTI_LENS", count: 1 }]); @@ -287,36 +287,12 @@ describe("Abilities - Parental Bond", () => { await game.phaseInterceptor.to("DamagePhase"); - expect(leadPokemon.turnData.hitCount).toBe(4); + expect(leadPokemon.turnData.hitCount).toBe(3); } ); it( - "Super Fang boosted by this ability and Multi-Lens should strike twice", - async () => { - game.override.moveset([ Moves.SUPER_FANG ]); - game.override.startingHeldItems([{ name: "MULTI_LENS", count: 1 }]); - - await game.classicMode.startBattle([ Species.MAGIKARP ]); - - const leadPokemon = game.scene.getPlayerPokemon()!; - const enemyPokemon = game.scene.getEnemyPokemon()!; - - game.move.select(Moves.SUPER_FANG); - await game.move.forceHit(); - - await game.phaseInterceptor.to("DamagePhase"); - - expect(leadPokemon.turnData.hitCount).toBe(2); - - await game.phaseInterceptor.to("MoveEndPhase", false); - - expect(enemyPokemon.hp).toBe(Math.ceil(enemyPokemon.getMaxHp() * 0.25)); - } - ); - - it( - "Seismic Toss boosted by this ability and Multi-Lens should strike twice", + "Seismic Toss boosted by this ability and Multi-Lens should strike 3 times", async () => { game.override.moveset([ Moves.SEISMIC_TOSS ]); game.override.startingHeldItems([{ name: "MULTI_LENS", count: 1 }]); @@ -333,11 +309,11 @@ describe("Abilities - Parental Bond", () => { await game.phaseInterceptor.to("DamagePhase"); - expect(leadPokemon.turnData.hitCount).toBe(2); + expect(leadPokemon.turnData.hitCount).toBe(3); await game.phaseInterceptor.to("MoveEndPhase", false); - expect(enemyPokemon.hp).toBe(enemyStartingHp - 200); + expect(enemyPokemon.hp).toBe(enemyStartingHp - 300); } ); @@ -494,30 +470,4 @@ describe("Abilities - Parental Bond", () => { expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(1); } ); - - it( - "should not apply to multi-target moves with Multi-Lens", - async () => { - game.override.battleType("double"); - game.override.moveset([ Moves.EARTHQUAKE, Moves.SPLASH ]); - game.override.passiveAbility(Abilities.LEVITATE); - game.override.startingHeldItems([{ name: "MULTI_LENS", count: 1 }]); - - await game.classicMode.startBattle([ Species.MAGIKARP, Species.FEEBAS ]); - - const enemyPokemon = game.scene.getEnemyField(); - - const enemyStartingHp = enemyPokemon.map(p => p.hp); - - game.move.select(Moves.EARTHQUAKE); - game.move.select(Moves.SPLASH, 1); - - await game.phaseInterceptor.to("DamagePhase"); - const enemyFirstHitDamage = enemyStartingHp.map((hp, i) => hp - enemyPokemon[i].hp); - - await game.phaseInterceptor.to("BerryPhase", false); - - enemyPokemon.forEach((p, i) => expect(enemyStartingHp[i] - p.hp).toBe(2 * enemyFirstHitDamage[i])); - } - ); }); diff --git a/src/test/items/multi_lens.test.ts b/src/test/items/multi_lens.test.ts new file mode 100644 index 000000000000..e4e4ab9863e9 --- /dev/null +++ b/src/test/items/multi_lens.test.ts @@ -0,0 +1,98 @@ +import { BattlerIndex } from "#app/battle"; +import { Stat } from "#enums/stat"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +describe("Items - Multi Lens", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .moveset([ Moves.TACKLE, Moves.TRAILBLAZE, Moves.TACHYON_CUTTER ]) + .ability(Abilities.BALL_FETCH) + .startingHeldItems([{ name: "MULTI_LENS" }]) + .battleType("single") + .disableCrits() + .enemySpecies(Species.SNORLAX) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH) + .startingLevel(100) + .enemyLevel(100); + }); + + it.each([ + { stackCount: 1, firstHitDamage: 0.75 }, + { stackCount: 2, firstHitDamage: 0.50 } + ])("$stackCount count: should deal {$firstHitDamage}x damage on the first hit, then hit $stackCount times for 0.25x", + async ({ stackCount, firstHitDamage }) => { + game.override.startingHeldItems([{ name: "MULTI_LENS", count: stackCount }]); + + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + const spy = vi.spyOn(enemyPokemon, "getAttackDamage"); + vi.spyOn(enemyPokemon, "getBaseDamage").mockReturnValue(100); + + game.move.select(Moves.TACKLE); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + + await game.phaseInterceptor.to("MoveEndPhase"); + const damageResults = spy.mock.results.map(result => result.value?.damage); + + expect(damageResults).toHaveLength(1 + stackCount); + expect(damageResults[0]).toBe(firstHitDamage * 100); + damageResults.slice(1).forEach(dmg => expect(dmg).toBe(25)); + }); + + it("should stack additively with Parental Bond", async () => { + game.override.ability(Abilities.PARENTAL_BOND); + + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.TACKLE); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + + await game.phaseInterceptor.to("MoveEndPhase"); + expect(playerPokemon.turnData.hitCount).toBe(3); + }); + + it("should apply secondary effects on each hit", async () => { + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.TRAILBLAZE); + + await game.phaseInterceptor.to("BerryPhase", false); + expect(playerPokemon.getStatStage(Stat.SPD)).toBe(2); + }); + + it("should not enhance multi-hit moves", async () => { + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.TACHYON_CUTTER); + + await game.phaseInterceptor.to("BerryPhase", false); + expect(playerPokemon.turnData.hitCount).toBe(2); + }); +}); diff --git a/src/test/moves/beat_up.test.ts b/src/test/moves/beat_up.test.ts index a0129621f0e1..41e5b63471f0 100644 --- a/src/test/moves/beat_up.test.ts +++ b/src/test/moves/beat_up.test.ts @@ -74,29 +74,4 @@ describe("Moves - Beat Up", () => { expect(playerPokemon.turnData.hitCount).toBe(5); } ); - - it( - "should hit twice for each player Pokemon if the user has Multi-Lens", - async () => { - game.override.startingHeldItems([{ name: "MULTI_LENS", count: 1 }]); - await game.startBattle([ Species.MAGIKARP, Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE, Species.PIKACHU, Species.EEVEE ]); - - const playerPokemon = game.scene.getPlayerPokemon()!; - const enemyPokemon = game.scene.getEnemyPokemon()!; - let enemyStartingHp = enemyPokemon.hp; - - game.move.select(Moves.BEAT_UP); - - await game.phaseInterceptor.to(MoveEffectPhase); - - expect(playerPokemon.turnData.hitCount).toBe(12); - expect(enemyPokemon.hp).toBeLessThan(enemyStartingHp); - - while (playerPokemon.turnData.hitsLeft > 0) { - enemyStartingHp = enemyPokemon.hp; - await game.phaseInterceptor.to(MoveEffectPhase); - expect(enemyPokemon.hp).toBeLessThan(enemyStartingHp); - } - } - ); }); diff --git a/src/test/moves/ceaseless_edge.test.ts b/src/test/moves/ceaseless_edge.test.ts index 88c8c8cf0115..3fbbb7b0aaf3 100644 --- a/src/test/moves/ceaseless_edge.test.ts +++ b/src/test/moves/ceaseless_edge.test.ts @@ -34,7 +34,7 @@ describe("Moves - Ceaseless Edge", () => { game.override.startingLevel(100); game.override.enemyLevel(100); game.override.moveset([ Moves.CEASELESS_EDGE, Moves.SPLASH, Moves.ROAR ]); - game.override.enemyMoveset([ Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH ]); + game.override.enemyMoveset(Moves.SPLASH); vi.spyOn(allMoves[Moves.CEASELESS_EDGE], "accuracy", "get").mockReturnValue(100); }); @@ -42,7 +42,7 @@ describe("Moves - Ceaseless Edge", () => { test( "move should hit and apply spikes", async () => { - await game.startBattle([ Species.ILLUMISE ]); + await game.classicMode.startBattle([ Species.ILLUMISE ]); const enemyPokemon = game.scene.getEnemyPokemon()!; @@ -67,7 +67,7 @@ describe("Moves - Ceaseless Edge", () => { "move should hit twice with multi lens and apply two layers of spikes", async () => { game.override.startingHeldItems([{ name: "MULTI_LENS" }]); - await game.startBattle([ Species.ILLUMISE ]); + await game.classicMode.startBattle([ Species.ILLUMISE ]); const enemyPokemon = game.scene.getEnemyPokemon()!; @@ -92,9 +92,9 @@ describe("Moves - Ceaseless Edge", () => { "trainer - move should hit twice, apply two layers of spikes, force switch opponent - opponent takes damage", async () => { game.override.startingHeldItems([{ name: "MULTI_LENS" }]); - game.override.startingWave(5); + game.override.startingWave(25); - await game.startBattle([ Species.ILLUMISE ]); + await game.classicMode.startBattle([ Species.ILLUMISE ]); game.move.select(Moves.CEASELESS_EDGE); await game.phaseInterceptor.to(MoveEffectPhase, false); @@ -102,7 +102,7 @@ describe("Moves - Ceaseless Edge", () => { const tagBefore = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as ArenaTrapTag; expect(tagBefore instanceof ArenaTrapTag).toBeFalsy(); - await game.phaseInterceptor.to(TurnEndPhase, false); + await game.toNextTurn(); const tagAfter = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as ArenaTrapTag; expect(tagAfter instanceof ArenaTrapTag).toBeTruthy(); expect(tagAfter.layers).toBe(2); diff --git a/src/test/moves/dragon_rage.test.ts b/src/test/moves/dragon_rage.test.ts index e8185f013e52..d5536ff9d2f0 100644 --- a/src/test/moves/dragon_rage.test.ts +++ b/src/test/moves/dragon_rage.test.ts @@ -2,7 +2,6 @@ import { Stat } from "#enums/stat"; import { Type } from "#enums/type"; import { Species } from "#app/enums/species"; import { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon"; -import { modifierTypes } from "#app/modifier/modifier-type"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { Abilities } from "#enums/abilities"; import { BattlerTagType } from "#enums/battler-tag-type"; @@ -114,14 +113,4 @@ describe("Moves - Dragon Rage", () => { expect(enemyPokemon.getInverseHp()).toBe(dragonRageDamage); }); - - it("ignores multi hit", async () => { - game.override.disableCrits(); - game.scene.addModifier(modifierTypes.MULTI_LENS().newModifier(partyPokemon), false); - - game.move.select(Moves.DRAGON_RAGE); - await game.phaseInterceptor.to(TurnEndPhase); - - expect(enemyPokemon.getInverseHp()).toBe(dragonRageDamage); - }); }); diff --git a/src/test/moves/electro_shot.test.ts b/src/test/moves/electro_shot.test.ts index 1373b4941eb7..283154b3408e 100644 --- a/src/test/moves/electro_shot.test.ts +++ b/src/test/moves/electro_shot.test.ts @@ -98,7 +98,7 @@ describe("Moves - Electro Shot", () => { game.move.select(Moves.ELECTRO_SHOT); await game.phaseInterceptor.to("MoveEndPhase"); - expect(playerPokemon.turnData.hitCount).toBe(2); + expect(playerPokemon.turnData.hitCount).toBe(1); expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(1); }); }); From 4802f512ff7925037ecb90bf4d19505d1831d2a4 Mon Sep 17 00:00:00 2001 From: PigeonBar <56974298+PigeonBar@users.noreply.github.com> Date: Mon, 11 Nov 2024 18:22:27 -0500 Subject: [PATCH 19/37] [P1][Beta] Fix softlock when losing a run on local build (#4846) --- src/phases/game-over-phase.ts | 7 ++- src/test/phases/game-over-phase.test.ts | 77 +++++++++++++++++++++++++ src/test/utils/phaseInterceptor.ts | 24 +++++++- 3 files changed, 103 insertions(+), 5 deletions(-) create mode 100644 src/test/phases/game-over-phase.test.ts diff --git a/src/phases/game-over-phase.ts b/src/phases/game-over-phase.ts index 26a0c45f449b..84a4a4e8ef90 100644 --- a/src/phases/game-over-phase.ts +++ b/src/phases/game-over-phase.ts @@ -125,10 +125,9 @@ export class GameOverPhase extends BattlePhase { } const clear = (endCardPhase?: EndCardPhase) => { - if (newClear) { - this.handleUnlocks(); - } if (this.isVictory && newClear) { + this.handleUnlocks(); + for (const species of this.firstRibbons) { this.scene.unshiftPhase(new RibbonModifierRewardPhase(this.scene, modifierTypes.VOUCHER_PLUS, species)); } @@ -183,6 +182,8 @@ export class GameOverPhase extends BattlePhase { this.scene.gameData.offlineNewClear(this.scene).then(result => { doGameOver(result); }); + } else { + doGameOver(false); } } diff --git a/src/test/phases/game-over-phase.test.ts b/src/test/phases/game-over-phase.test.ts new file mode 100644 index 000000000000..2e19d5fe9540 --- /dev/null +++ b/src/test/phases/game-over-phase.test.ts @@ -0,0 +1,77 @@ +import { Biome } from "#enums/biome"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { achvs } from "#app/system/achv"; +import { Unlockables } from "#app/system/unlockables"; + +describe("Game Over Phase", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .moveset([ Moves.MEMENTO, Moves.ICE_BEAM, Moves.SPLASH ]) + .ability(Abilities.BALL_FETCH) + .battleType("single") + .disableCrits() + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH) + .startingWave(200) + .startingBiome(Biome.END) + .startingLevel(10000); + }); + + it("winning a run should give rewards", async () => { + await game.classicMode.startBattle([ Species.BULBASAUR ]); + vi.spyOn(game.scene, "validateAchv"); + + // Note: `game.doKillOpponents()` does not properly handle final boss + // Final boss phase 1 + game.move.select(Moves.ICE_BEAM); + await game.toNextTurn(); + + // Final boss phase 2 + game.move.select(Moves.ICE_BEAM); + await game.phaseInterceptor.to("PostGameOverPhase", false); + + // The game refused to actually give the vouchers during tests, + // so the best we can do is to check that their reward phases occurred. + expect(game.phaseInterceptor.log.includes("GameOverPhase")).toBe(true); + expect(game.phaseInterceptor.log.includes("UnlockPhase")).toBe(true); + expect(game.phaseInterceptor.log.includes("RibbonModifierRewardPhase")).toBe(true); + expect(game.scene.gameData.unlocks[Unlockables.ENDLESS_MODE]).toBe(true); + expect(game.scene.validateAchv).toHaveBeenCalledWith(achvs.CLASSIC_VICTORY); + expect(game.scene.gameData.achvUnlocks[achvs.CLASSIC_VICTORY.id]).toBeTruthy(); + }); + + it("losing a run should not give rewards", async () => { + await game.classicMode.startBattle([ Species.BULBASAUR ]); + vi.spyOn(game.scene, "validateAchv"); + + game.move.select(Moves.MEMENTO); + await game.phaseInterceptor.to("PostGameOverPhase", false); + + expect(game.phaseInterceptor.log.includes("GameOverPhase")).toBe(true); + expect(game.phaseInterceptor.log.includes("UnlockPhase")).toBe(false); + expect(game.phaseInterceptor.log.includes("RibbonModifierRewardPhase")).toBe(false); + expect(game.phaseInterceptor.log.includes("GameOverModifierRewardPhase")).toBe(false); + expect(game.scene.gameData.unlocks[Unlockables.ENDLESS_MODE]).toBe(false); + expect(game.scene.validateAchv).not.toHaveBeenCalledWith(achvs.CLASSIC_VICTORY); + expect(game.scene.gameData.achvUnlocks[achvs.CLASSIC_VICTORY.id]).toBeFalsy(); + }); +}); diff --git a/src/test/utils/phaseInterceptor.ts b/src/test/utils/phaseInterceptor.ts index 17b6c7a6c814..4029e5e168c4 100644 --- a/src/test/utils/phaseInterceptor.ts +++ b/src/test/utils/phaseInterceptor.ts @@ -55,6 +55,11 @@ import { import { ModifierRewardPhase } from "#app/phases/modifier-reward-phase"; import { PartyExpPhase } from "#app/phases/party-exp-phase"; import { ExpPhase } from "#app/phases/exp-phase"; +import { GameOverPhase } from "#app/phases/game-over-phase"; +import { RibbonModifierRewardPhase } from "#app/phases/ribbon-modifier-reward-phase"; +import { GameOverModifierRewardPhase } from "#app/phases/game-over-modifier-reward-phase"; +import { UnlockPhase } from "#app/phases/unlock-phase"; +import { PostGameOverPhase } from "#app/phases/post-game-over-phase"; export interface PromptHandler { phaseTarget?: string; @@ -113,10 +118,15 @@ type PhaseClass = | typeof MysteryEncounterBattlePhase | typeof MysteryEncounterRewardsPhase | typeof PostMysteryEncounterPhase + | typeof RibbonModifierRewardPhase + | typeof GameOverModifierRewardPhase | typeof ModifierRewardPhase | typeof PartyExpPhase | typeof ExpPhase - | typeof EncounterPhase; + | typeof EncounterPhase + | typeof GameOverPhase + | typeof UnlockPhase + | typeof PostGameOverPhase; type PhaseString = | "LoginPhase" @@ -167,10 +177,15 @@ type PhaseString = | "MysteryEncounterBattlePhase" | "MysteryEncounterRewardsPhase" | "PostMysteryEncounterPhase" + | "RibbonModifierRewardPhase" + | "GameOverModifierRewardPhase" | "ModifierRewardPhase" | "PartyExpPhase" | "ExpPhase" - | "EncounterPhase"; + | "EncounterPhase" + | "GameOverPhase" + | "UnlockPhase" + | "PostGameOverPhase"; type PhaseInterceptorPhase = PhaseClass | PhaseString; @@ -245,10 +260,15 @@ export default class PhaseInterceptor { [ MysteryEncounterBattlePhase, this.startPhase ], [ MysteryEncounterRewardsPhase, this.startPhase ], [ PostMysteryEncounterPhase, this.startPhase ], + [ RibbonModifierRewardPhase, this.startPhase ], + [ GameOverModifierRewardPhase, this.startPhase ], [ ModifierRewardPhase, this.startPhase ], [ PartyExpPhase, this.startPhase ], [ ExpPhase, this.startPhase ], [ EncounterPhase, this.startPhase ], + [ GameOverPhase, this.startPhase ], + [ UnlockPhase, this.startPhase ], + [ PostGameOverPhase, this.startPhase ], ]; private endBySetMode = [ From 8e26db944d27ee031de891ea834d0c32c426d7a8 Mon Sep 17 00:00:00 2001 From: innerthunder <168692175+innerthunder@users.noreply.github.com> Date: Mon, 11 Nov 2024 21:13:37 -0800 Subject: [PATCH 20/37] [Balance][Beta] Revert Spread Move Restriction on Multi-Lens (#4851) * Multi-Lens now applies to spread moves * Fix Multi-Lens applying to both damage and power --- src/data/ability.ts | 2 +- src/data/move.ts | 9 +++++---- src/modifier/modifier.ts | 4 ++-- src/test/items/multi_lens.test.ts | 19 +++++++++++++++++++ 4 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index 4194be314052..d58c6c5c9b9c 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -1367,7 +1367,7 @@ export class AddSecondStrikeAbAttr extends PreAttackAbAttr { const hitCount = args[0] as Utils.NumberHolder; const multiplier = args[1] as Utils.NumberHolder; - if (move.canBeMultiStrikeEnhanced(pokemon)) { + if (move.canBeMultiStrikeEnhanced(pokemon, true)) { this.showAbility = !!hitCount?.value; if (hitCount?.value) { hitCount.value += 1; diff --git a/src/data/move.ts b/src/data/move.ts index 0de9d9b53a2d..98c679b923e4 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -818,8 +818,6 @@ export default class Move implements Localizable { applyMoveAttrs(VariablePowerAttr, source, target, this, power); - source.scene.applyModifiers(PokemonMultiHitModifier, source.isPlayer(), source, this.id, null, power); - if (!this.hasAttr(TypelessAttr)) { source.scene.arena.applyTags(WeakenMoveTypeTag, simulated, this.type, power); source.scene.applyModifiers(AttackTypeBoosterModifier, source.isPlayer(), source, this.type, power); @@ -846,8 +844,11 @@ export default class Move implements Localizable { * by enhancing effects. * Currently used for {@link https://bulbapedia.bulbagarden.net/wiki/Parental_Bond_(Ability) | Parental Bond} * and {@linkcode PokemonMultiHitModifier | Multi-Lens}. + * @param user The {@linkcode Pokemon} using the move + * @param restrictSpread `true` if the enhancing effect + * should not affect multi-target moves (default `false`) */ - canBeMultiStrikeEnhanced(user: Pokemon): boolean { + canBeMultiStrikeEnhanced(user: Pokemon, restrictSpread: boolean = false): boolean { // Multi-strike enhancers... // ...cannot enhance moves that hit multiple targets @@ -870,7 +871,7 @@ export default class Move implements Localizable { Moves.ENDEAVOR ]; - return !isMultiTarget + return (!restrictSpread || !isMultiTarget) && !this.isChargingMove() && !exceptAttrs.some(attr => this.hasAttr(attr)) && !exceptMoves.some(id => this.id === id) diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index 5e60d8880723..7aa4b9308d10 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -2715,7 +2715,7 @@ export class PokemonMultiHitModifier extends PokemonHeldItemModifier { if (!isNullOrUndefined(count)) { return this.applyHitCountBoost(count); } else if (!isNullOrUndefined(damageMultiplier)) { - return this.applyPowerModifier(pokemon, damageMultiplier); + return this.applyDamageModifier(pokemon, damageMultiplier); } return false; @@ -2732,7 +2732,7 @@ export class PokemonMultiHitModifier extends PokemonHeldItemModifier { * equal to (1 - the number of stacked Multi-Lenses). * Additional strikes beyond that are given a 0.25x damage multiplier */ - private applyPowerModifier(pokemon: Pokemon, damageMultiplier: NumberHolder): boolean { + private applyDamageModifier(pokemon: Pokemon, damageMultiplier: NumberHolder): boolean { damageMultiplier.value = (pokemon.turnData.hitsLeft === pokemon.turnData.hitCount) ? (1 - (0.25 * this.getStackCount())) : 0.25; diff --git a/src/test/items/multi_lens.test.ts b/src/test/items/multi_lens.test.ts index e4e4ab9863e9..d389ca705552 100644 --- a/src/test/items/multi_lens.test.ts +++ b/src/test/items/multi_lens.test.ts @@ -95,4 +95,23 @@ describe("Items - Multi Lens", () => { await game.phaseInterceptor.to("BerryPhase", false); expect(playerPokemon.turnData.hitCount).toBe(2); }); + + it("should enhance multi-target moves", async () => { + game.override + .battleType("double") + .moveset([ Moves.SWIFT, Moves.SPLASH ]); + + await game.classicMode.startBattle([ Species.MAGIKARP, Species.FEEBAS ]); + + const [ magikarp, ] = game.scene.getPlayerField(); + + game.move.select(Moves.SWIFT, 0); + game.move.select(Moves.SPLASH, 1); + + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2 ]); + + await game.phaseInterceptor.to("MoveEndPhase"); + + expect(magikarp.turnData.hitCount).toBe(2); + }); }); From 6f3fd0f138c554a21ea3f37a02e4457ae9219815 Mon Sep 17 00:00:00 2001 From: PigeonBar <56974298+PigeonBar@users.noreply.github.com> Date: Tue, 12 Nov 2024 06:29:37 -0500 Subject: [PATCH 21/37] [Beta][P3] Fix failed charge moves not displaying failed text (#4853) --- src/data/move.ts | 4 ++-- src/phases/move-phase.ts | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/data/move.ts b/src/data/move.ts index 98c679b923e4..089bb51bf5e1 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -668,12 +668,12 @@ export default class Move implements Localizable { } /** - * Sees if, given the target pokemon, a move fails on it (by looking at each {@linkcode MoveAttr} of this move + * Sees if a move has a custom failure text (by looking at each {@linkcode MoveAttr} of this move) * @param user {@linkcode Pokemon} using the move * @param target {@linkcode Pokemon} receiving the move * @param move {@linkcode Move} using the move * @param cancelled {@linkcode Utils.BooleanHolder} to hold boolean value - * @returns string of the failed text, or null + * @returns string of the custom failure text, or `null` if it uses the default text ("But it failed!") */ getFailedText(user: Pokemon, target: Pokemon, move: Move, cancelled: Utils.BooleanHolder): string | null { for (const attr of this.attrs) { diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index 378b72e1f56a..005cdbe17167 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -378,10 +378,8 @@ export class MovePhase extends BattlePhase { this.pokemon.pushMoveHistory({ move: this.move.moveId, targets: this.targets, result: MoveResult.FAIL, virtual: this.move.virtual }); const failureMessage = move.getFailedText(this.pokemon, targets[0], move, new BooleanHolder(false)); - if (failureMessage) { - this.showMoveText(); - this.showFailedText(failureMessage); - } + this.showMoveText(); + this.showFailedText(failureMessage ?? undefined); // Remove the user from its semi-invulnerable state (if applicable) this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT); From b6b756a1620c488557e9c6bf5e2b0990242e138e Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Tue, 12 Nov 2024 03:44:28 -0800 Subject: [PATCH 22/37] [P2] Fix issue with Pokemon not evolving until the next floor and clean up `LevelUpPhase` (#4854) --- src/phases/level-up-phase.ts | 47 +++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/src/phases/level-up-phase.ts b/src/phases/level-up-phase.ts index a2fa8a16533a..4f26abc5af30 100644 --- a/src/phases/level-up-phase.ts +++ b/src/phases/level-up-phase.ts @@ -1,59 +1,66 @@ -import BattleScene from "#app/battle-scene"; +import type BattleScene from "#app/battle-scene"; import { ExpNotification } from "#app/enums/exp-notification"; -import { EvolutionPhase } from "#app/phases/evolution-phase"; -import { PlayerPokemon } from "#app/field/pokemon"; +import type { PlayerPokemon } from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; +import { EvolutionPhase } from "#app/phases/evolution-phase"; +import { LearnMovePhase } from "#app/phases/learn-move-phase"; +import { PlayerPartyMemberPokemonPhase } from "#app/phases/player-party-member-pokemon-phase"; import { LevelAchv } from "#app/system/achv"; +import { NumberHolder } from "#app/utils"; import i18next from "i18next"; -import * as Utils from "#app/utils"; -import { PlayerPartyMemberPokemonPhase } from "./player-party-member-pokemon-phase"; -import { LearnMovePhase } from "./learn-move-phase"; export class LevelUpPhase extends PlayerPartyMemberPokemonPhase { - private lastLevel: integer; - private level: integer; + protected lastLevel: number; + protected level: number; + protected pokemon: PlayerPokemon = this.getPlayerPokemon(); - constructor(scene: BattleScene, partyMemberIndex: integer, lastLevel: integer, level: integer) { + constructor(scene: BattleScene, partyMemberIndex: number, lastLevel: number, level: number) { super(scene, partyMemberIndex); this.lastLevel = lastLevel; this.level = level; - this.scene = scene; } - start() { + public override start() { super.start(); if (this.level > this.scene.gameData.gameStats.highestLevel) { this.scene.gameData.gameStats.highestLevel = this.level; } - this.scene.validateAchvs(LevelAchv, new Utils.NumberHolder(this.level)); + this.scene.validateAchvs(LevelAchv, new NumberHolder(this.level)); - const pokemon = this.getPokemon(); - const prevStats = pokemon.stats.slice(0); - pokemon.calculateStats(); - pokemon.updateInfo(); + const prevStats = this.pokemon.stats.slice(0); + this.pokemon.calculateStats(); + this.pokemon.updateInfo(); if (this.scene.expParty === ExpNotification.DEFAULT) { this.scene.playSound("level_up_fanfare"); - this.scene.ui.showText(i18next.t("battle:levelUp", { pokemonName: getPokemonNameWithAffix(this.getPokemon()), level: this.level }), null, () => this.scene.ui.getMessageHandler().promptLevelUpStats(this.partyMemberIndex, prevStats, false).then(() => this.end()), null, true); + this.scene.ui.showText( + i18next.t("battle:levelUp", { pokemonName: getPokemonNameWithAffix(this.pokemon), level: this.level }), + null, + () => this.scene.ui.getMessageHandler().promptLevelUpStats(this.partyMemberIndex, prevStats, false) + .then(() => this.end()), null, true); } else if (this.scene.expParty === ExpNotification.SKIP) { this.end(); } else { // we still want to display the stats if activated this.scene.ui.getMessageHandler().promptLevelUpStats(this.partyMemberIndex, prevStats, false).then(() => this.end()); } + } + + public override end() { if (this.lastLevel < 100) { // this feels like an unnecessary optimization const levelMoves = this.getPokemon().getLevelMoves(this.lastLevel + 1); for (const lm of levelMoves) { this.scene.unshiftPhase(new LearnMovePhase(this.scene, this.partyMemberIndex, lm[1])); } } - if (!pokemon.pauseEvolutions) { - const evolution = pokemon.getEvolution(); + if (!this.pokemon.pauseEvolutions) { + const evolution = this.pokemon.getEvolution(); if (evolution) { - this.scene.unshiftPhase(new EvolutionPhase(this.scene, pokemon as PlayerPokemon, evolution, this.lastLevel)); + this.scene.unshiftPhase(new EvolutionPhase(this.scene, this.pokemon, evolution, this.lastLevel)); } } + return super.end(); } } From e45cb42f7ee86899e45d9aa40aa390b65251dd76 Mon Sep 17 00:00:00 2001 From: innerthunder <168692175+innerthunder@users.noreply.github.com> Date: Tue, 12 Nov 2024 18:42:47 -0800 Subject: [PATCH 23/37] [Balance] Disable King's Rock for moves that can already flinch (#4860) --- src/phases/move-effect-phase.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index 24a0b51da96c..afc8dd0475d6 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -26,6 +26,7 @@ import { applyMoveAttrs, AttackMove, DelayedAttackAttr, + FlinchAttr, HitsTagAttr, MissEffectAttr, MoveAttr, @@ -502,6 +503,10 @@ export class MoveEffectPhase extends PokemonPhase { */ protected applyHeldItemFlinchCheck(user: Pokemon, target: Pokemon, dealsDamage: boolean) : () => void { return () => { + if (this.move.getMove().hasAttr(FlinchAttr)) { + return; + } + if (dealsDamage && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && !this.move.getMove().hitsSubstitute(user, target)) { const flinched = new BooleanHolder(false); user.scene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched); From 162eea500dcfaa5e39b06481339a60ebfb2d0c78 Mon Sep 17 00:00:00 2001 From: muscode Date: Wed, 13 Nov 2024 00:28:22 -0600 Subject: [PATCH 24/37] Fixed wild form changes messages, and form-changed Cramorant crashing the game when both sides faint at the same time (#4859) --- src/phases/quiet-form-change-phase.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/phases/quiet-form-change-phase.ts b/src/phases/quiet-form-change-phase.ts index c28cc28b5927..6c84c0d1a8ad 100644 --- a/src/phases/quiet-form-change-phase.ts +++ b/src/phases/quiet-form-change-phase.ts @@ -29,10 +29,14 @@ export class QuietFormChangePhase extends BattlePhase { const preName = getPokemonNameWithAffix(this.pokemon); - if (!this.pokemon.isOnField() || this.pokemon.getTag(SemiInvulnerableTag)) { - this.pokemon.changeForm(this.formChange).then(() => { - this.scene.ui.showText(getSpeciesFormChangeMessage(this.pokemon, this.formChange, preName), null, () => this.end(), 1500); - }); + if (!this.pokemon.isOnField() || this.pokemon.getTag(SemiInvulnerableTag) || this.pokemon.isFainted()) { + if (this.pokemon.isPlayer() || this.pokemon.isActive()) { + this.pokemon.changeForm(this.formChange).then(() => { + this.scene.ui.showText(getSpeciesFormChangeMessage(this.pokemon, this.formChange, preName), null, () => this.end(), 1500); + }); + } else { + this.end(); + } return; } From 0c521bbe0828746ace0dd7310340fb6340bee6e5 Mon Sep 17 00:00:00 2001 From: geeilhan <107366005+geeilhan@users.noreply.github.com> Date: Wed, 13 Nov 2024 16:41:39 +0100 Subject: [PATCH 25/37] [Move] Implement Freeze Dry type-changed interactions (#4840) * Full implementation of freeze-dry including edge cases such as Normalize and Electrify plus tests * Update comments * renamed WaterSuperEffectTypeMultiplierAttr to FreezeDryAttr * Added test case for freeze dry during inverse battles * cleaned up code making it more general * Added some more documentation * implementing reviewed changes * used getMoveType() instead of move.type * added additional test cases to freeze dry * Revert "used getMoveType() instead of move.type" This reverts commit 03445dfab4db52b0dddbe7abf7d4b4dfa8b9c583. * added reviewed changes without changing public/locales --------- Co-authored-by: ga27lok --- src/data/move.ts | 40 ++++++-- src/test/moves/freeze_dry.test.ts | 163 +++++++++++++++++++++++++++++- 2 files changed, 192 insertions(+), 11 deletions(-) diff --git a/src/data/move.ts b/src/data/move.ts index 089bb51bf5e1..74ac61af8844 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -4971,16 +4971,42 @@ export class NeutralDamageAgainstFlyingTypeMultiplierAttr extends VariableMoveTy } } -export class WaterSuperEffectTypeMultiplierAttr extends VariableMoveTypeMultiplierAttr { +/** + * This class forces Freeze-Dry to be super effective against Water Type. + * It considers if target is Mono or Dual Type and calculates the new Multiplier accordingly. + * @see {@linkcode apply} + */ +export class FreezeDryAttr extends VariableMoveTypeMultiplierAttr { + /** + * If the target is Mono Type (Water only) then a 2x Multiplier is always forced. + * If target is Dual Type (containing Water) then only a 2x Multiplier is forced for the Water Type. + * + * Additionally Freeze-Dry's effectiveness against water is always forced during {@linkcode InverseBattleChallenge}. + * The multiplier is recalculated for the non-Water Type in case of Dual Type targets containing Water Type. + * + * @param user The {@linkcode Pokemon} applying the move + * @param target The {@linkcode Pokemon} targeted by the move + * @param move The move used by the user + * @param args `[0]` a {@linkcode Utils.NumberHolder | NumberHolder} containing a type effectiveness multiplier + * @returns `true` if super effectiveness on water type is forced; `false` otherwise + */ apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { const multiplier = args[0] as Utils.NumberHolder; - if (target.isOfType(Type.WATER)) { - const effectivenessAgainstWater = new Utils.NumberHolder(getTypeDamageMultiplier(move.type, Type.WATER)); - applyChallenges(user.scene.gameMode, ChallengeType.TYPE_EFFECTIVENESS, effectivenessAgainstWater); - if (effectivenessAgainstWater.value !== 0) { - multiplier.value *= 2 / effectivenessAgainstWater.value; + if (target.isOfType(Type.WATER) && multiplier.value !== 0) { + const multipleTypes = (target.getTypes().length > 1); + + if (multipleTypes) { + const nonWaterType = target.getTypes().filter(type => type !== Type.WATER)[0]; + const effectivenessAgainstTarget = new Utils.NumberHolder(getTypeDamageMultiplier(user.getMoveType(move), nonWaterType)); + + applyChallenges(user.scene.gameMode, ChallengeType.TYPE_EFFECTIVENESS, effectivenessAgainstTarget); + + multiplier.value = effectivenessAgainstTarget.value * 2; return true; } + + multiplier.value = 2; + return true; } return false; @@ -9422,7 +9448,7 @@ export function initMoves() { .target(MoveTarget.ALL_NEAR_OTHERS), new AttackMove(Moves.FREEZE_DRY, Type.ICE, MoveCategory.SPECIAL, 70, 100, 20, 10, 0, 6) .attr(StatusEffectAttr, StatusEffect.FREEZE) - .attr(WaterSuperEffectTypeMultiplierAttr) + .attr(FreezeDryAttr) .edgeCase(), // This currently just multiplies the move's power instead of changing its effectiveness. It also doesn't account for abilities that modify type effectiveness such as tera shell. new AttackMove(Moves.DISARMING_VOICE, Type.FAIRY, MoveCategory.SPECIAL, 40, -1, 15, -1, 0, 6) .soundBased() diff --git a/src/test/moves/freeze_dry.test.ts b/src/test/moves/freeze_dry.test.ts index f766ed41a820..8bc6717f4352 100644 --- a/src/test/moves/freeze_dry.test.ts +++ b/src/test/moves/freeze_dry.test.ts @@ -2,6 +2,7 @@ import { BattlerIndex } from "#app/battle"; import { Abilities } from "#app/enums/abilities"; import { Moves } from "#app/enums/moves"; import { Species } from "#app/enums/species"; +import { Challenges } from "#enums/challenges"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; @@ -97,8 +98,7 @@ describe("Moves - Freeze-Dry", () => { expect(enemy.hp).toBeLessThan(enemy.getMaxHp()); }); - // enable if this is ever fixed (lol) - it.todo("should deal 2x damage to water types under Normalize", async () => { + it("should deal 2x damage to water type under Normalize", async () => { game.override.ability(Abilities.NORMALIZE); await game.classicMode.startBattle(); @@ -112,8 +112,39 @@ describe("Moves - Freeze-Dry", () => { expect(enemy.getMoveEffectiveness).toHaveReturnedWith(2); }); - // enable once Electrify is implemented (and the interaction is fixed, as above) - it.todo("should deal 2x damage to water types under Electrify", async () => { + it("should deal 0.25x damage to rock/steel type under Normalize", async () => { + game.override + .ability(Abilities.NORMALIZE) + .enemySpecies(Species.SHIELDON); + await game.classicMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.FREEZE_DRY); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("MoveEffectPhase"); + + expect(enemy.getMoveEffectiveness).toHaveReturnedWith(0.25); + }); + + it("should deal 0x damage to water/ghost type under Normalize", async () => { + game.override + .ability(Abilities.NORMALIZE) + .enemySpecies(Species.JELLICENT); + await game.classicMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.FREEZE_DRY); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("BerryPhase"); + + expect(enemy.getMoveEffectiveness).toHaveReturnedWith(0); + }); + + it("should deal 2x damage to water type under Electrify", async () => { game.override.enemyMoveset([ Moves.ELECTRIFY ]); await game.classicMode.startBattle(); @@ -126,4 +157,128 @@ describe("Moves - Freeze-Dry", () => { expect(enemy.getMoveEffectiveness).toHaveReturnedWith(2); }); + + it("should deal 4x damage to water/flying type under Electrify", async () => { + game.override + .enemyMoveset([ Moves.ELECTRIFY ]) + .enemySpecies(Species.GYARADOS); + await game.classicMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.FREEZE_DRY); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("BerryPhase"); + + expect(enemy.getMoveEffectiveness).toHaveReturnedWith(4); + }); + + it("should deal 0x damage to water/ground type under Electrify", async () => { + game.override + .enemyMoveset([ Moves.ELECTRIFY ]) + .enemySpecies(Species.BARBOACH); + await game.classicMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.FREEZE_DRY); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("BerryPhase"); + + expect(enemy.getMoveEffectiveness).toHaveReturnedWith(0); + }); + + it("should deal 0.25x damage to Grass/Dragon type under Electrify", async () => { + game.override + .enemyMoveset([ Moves.ELECTRIFY ]) + .enemySpecies(Species.FLAPPLE); + await game.classicMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.FREEZE_DRY); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("BerryPhase"); + + expect(enemy.getMoveEffectiveness).toHaveReturnedWith(0.25); + }); + + it("should deal 2x damage to Water type during inverse battle", async () => { + game.override + .moveset([ Moves.FREEZE_DRY ]) + .enemySpecies(Species.MAGIKARP); + game.challengeMode.addChallenge(Challenges.INVERSE_BATTLE, 1, 1); + + + await game.challengeMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.FREEZE_DRY); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("MoveEffectPhase"); + + expect(enemy.getMoveEffectiveness).toHaveLastReturnedWith(2); + }); + + it("should deal 2x damage to Water type during inverse battle under Normalize", async () => { + game.override + .moveset([ Moves.FREEZE_DRY ]) + .ability(Abilities.NORMALIZE) + .enemySpecies(Species.MAGIKARP); + game.challengeMode.addChallenge(Challenges.INVERSE_BATTLE, 1, 1); + + await game.challengeMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.FREEZE_DRY); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("MoveEffectPhase"); + + expect(enemy.getMoveEffectiveness).toHaveLastReturnedWith(2); + }); + + it("should deal 2x damage to Water type during inverse battle under Electrify", async () => { + game.override + .moveset([ Moves.FREEZE_DRY ]) + .enemySpecies(Species.MAGIKARP) + .enemyMoveset([ Moves.ELECTRIFY ]); + game.challengeMode.addChallenge(Challenges.INVERSE_BATTLE, 1, 1); + + await game.challengeMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.FREEZE_DRY); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("MoveEffectPhase"); + + expect(enemy.getMoveEffectiveness).toHaveLastReturnedWith(2); + }); + + it("should deal 1x damage to water/flying type during inverse battle under Electrify", async () => { + game.override + .enemyMoveset([ Moves.ELECTRIFY ]) + .enemySpecies(Species.GYARADOS); + + game.challengeMode.addChallenge(Challenges.INVERSE_BATTLE, 1, 1); + + await game.challengeMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.FREEZE_DRY); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("BerryPhase"); + + expect(enemy.getMoveEffectiveness).toHaveReturnedWith(1); + }); }); From d0d9eb78da7beadb980f32a6c466f521f9e600cf Mon Sep 17 00:00:00 2001 From: Mumble <171087428+frutescens@users.noreply.github.com> Date: Thu, 14 Nov 2024 11:35:13 -0800 Subject: [PATCH 26/37] Set the IVs of default starters to 15. (#4861) Co-authored-by: frutescens --- src/data/challenge.ts | 2 +- src/system/game-data.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/data/challenge.ts b/src/data/challenge.ts index af6bbf5b00f7..4301ea7b3758 100644 --- a/src/data/challenge.ts +++ b/src/data/challenge.ts @@ -653,7 +653,7 @@ export class FreshStartChallenge extends Challenge { pokemon.shiny = false; // Not shiny pokemon.variant = 0; // Not shiny pokemon.formIndex = 0; // Froakie should be base form - pokemon.ivs = [ 10, 10, 10, 10, 10, 10 ]; // Default IVs of 10 for all stats + pokemon.ivs = [ 15, 15, 15, 15, 15, 15 ]; // Default IVs of 15 for all stats (Updated to 15 from 10 in 1.2.0) return true; } diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 8f179ddb6772..a9726ac0713b 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -1540,7 +1540,7 @@ export class GameData { entry.caughtAttr = defaultStarterAttr; entry.natureAttr = 1 << (defaultStarterNatures[ds] + 1); for (const i in entry.ivs) { - entry.ivs[i] = 10; + entry.ivs[i] = 15; } } From 640ac237412dd82863f7ef7e704e8406c2d3c3fe Mon Sep 17 00:00:00 2001 From: PigeonBar <56974298+PigeonBar@users.noreply.github.com> Date: Thu, 14 Nov 2024 14:40:01 -0500 Subject: [PATCH 27/37] [Dev] Add overrides for alternating between single and double battles (#4833) * [Dev] Add overrides for alternating between single and double battles * PR feedback * Add type `DoubleType` * Fixed Gastro Acid test using `game.override.battleType(null)` * Changed new type name to `SingleOrDoubleType` * `SingleOrDoubleType` is now `BattleStyle` * Update src/test/utils/helpers/overridesHelper.ts --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> Co-authored-by: Moka <54149968+MokaStitcher@users.noreply.github.com> --- src/battle-scene.ts | 34 ++++++++++++++---- src/overrides.ts | 15 +++++++- src/test/battle/double_battle.test.ts | 42 ++++++++++++++++++++++- src/test/moves/gastro_acid.test.ts | 2 +- src/test/utils/helpers/overridesHelper.ts | 9 ++--- 5 files changed, 89 insertions(+), 13 deletions(-) diff --git a/src/battle-scene.ts b/src/battle-scene.ts index c30ab2e29123..e659b588208c 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -1233,12 +1233,34 @@ export default class BattleScene extends SceneBase { newDouble = !!double; } - if (Overrides.BATTLE_TYPE_OVERRIDE === "double") { - newDouble = true; - } - /* Override battles into single only if not fighting with trainers */ - if (newBattleType !== BattleType.TRAINER && Overrides.BATTLE_TYPE_OVERRIDE === "single") { - newDouble = false; + if (!isNullOrUndefined(Overrides.BATTLE_TYPE_OVERRIDE)) { + let doubleOverrideForWave: "single" | "double" | null = null; + + switch (Overrides.BATTLE_TYPE_OVERRIDE) { + case "double": + doubleOverrideForWave = "double"; + break; + case "single": + doubleOverrideForWave = "single"; + break; + case "even-doubles": + doubleOverrideForWave = (newWaveIndex % 2) ? "single" : "double"; + break; + case "odd-doubles": + doubleOverrideForWave = (newWaveIndex % 2) ? "double" : "single"; + break; + } + + if (doubleOverrideForWave === "double") { + newDouble = true; + } + /** + * Override battles into single only if not fighting with trainers. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/1948 | GitHub Issue #1948} + */ + if (newBattleType !== BattleType.TRAINER && doubleOverrideForWave === "single") { + newDouble = false; + } } const lastBattle = this.currentBattle; diff --git a/src/overrides.ts b/src/overrides.ts index d7a8ee18f155..7b73cd47b031 100644 --- a/src/overrides.ts +++ b/src/overrides.ts @@ -47,7 +47,18 @@ class DefaultOverrides { /** a specific seed (default: a random string of 24 characters) */ readonly SEED_OVERRIDE: string = ""; readonly WEATHER_OVERRIDE: WeatherType = WeatherType.NONE; - readonly BATTLE_TYPE_OVERRIDE: "double" | "single" | null = null; + /** + * If `null`, ignore this override. + * + * If `"single"`, set every non-trainer battle to be a single battle. + * + * If `"double"`, set every battle (including trainer battles) to be a double battle. + * + * If `"even-doubles"`, follow the `"double"` rule on even wave numbers, and follow the `"single"` rule on odd wave numbers. + * + * If `"odd-doubles"`, follow the `"double"` rule on odd wave numbers, and follow the `"single"` rule on even wave numbers. + */ + readonly BATTLE_TYPE_OVERRIDE: BattleStyle | null = null; readonly STARTING_WAVE_OVERRIDE: number = 0; readonly STARTING_BIOME_OVERRIDE: Biome = Biome.TOWN; readonly ARENA_TINT_OVERRIDE: TimeOfDay | null = null; @@ -229,3 +240,5 @@ export default { ...defaultOverrides, ...overrides } satisfies InstanceType; + +export type BattleStyle = "double" | "single" | "even-doubles" | "odd-doubles"; diff --git a/src/test/battle/double_battle.test.ts b/src/test/battle/double_battle.test.ts index 1fc24bfc027b..b48f2a96a5be 100644 --- a/src/test/battle/double_battle.test.ts +++ b/src/test/battle/double_battle.test.ts @@ -1,4 +1,6 @@ import { Status } from "#app/data/status-effect"; +import { Abilities } from "#enums/abilities"; +import { GameModes, getGameMode } from "#app/game-mode"; import { BattleEndPhase } from "#app/phases/battle-end-phase"; import { TurnInitPhase } from "#app/phases/turn-init-phase"; import { Moves } from "#enums/moves"; @@ -6,9 +8,11 @@ import { Species } from "#enums/species"; import { StatusEffect } from "#enums/status-effect"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; describe("Double Battles", () => { + const DOUBLE_CHANCE = 8; // Normal chance of double battle is 1/8 + let phaserGame: Phaser.Game; let game: GameManager; @@ -56,4 +60,40 @@ describe("Double Battles", () => { await game.phaseInterceptor.to(TurnInitPhase); expect(game.scene.getPlayerField().filter(p => !p.isFainted())).toHaveLength(2); }, 20000); + + it("randomly chooses between single and double battles if there is no battle type override", async () => { + let rngSweepProgress = 0; // Will simulate RNG rolls by slowly increasing from 0 to 1 + let doubleCount = 0; + let singleCount = 0; + + vi.spyOn(Phaser.Math.RND, "realInRange").mockImplementation((min: number, max: number) => { + return rngSweepProgress * (max - min) + min; + }); + + game.override.enemyMoveset(Moves.SPLASH) + .moveset(Moves.SPLASH) + .enemyAbility(Abilities.BALL_FETCH) + .ability(Abilities.BALL_FETCH); + + // Play through endless, waves 1 to 9, counting number of double battles from waves 2 to 9 + await game.classicMode.startBattle([ Species.BULBASAUR ]); + game.scene.gameMode = getGameMode(GameModes.ENDLESS); + + for (let i = 0; i < DOUBLE_CHANCE; i++) { + rngSweepProgress = (i + 0.5) / DOUBLE_CHANCE; + + game.move.select(Moves.SPLASH); + await game.doKillOpponents(); + await game.toNextWave(); + + if (game.scene.getEnemyParty().length === 1) { + singleCount++; + } else if (game.scene.getEnemyParty().length === 2) { + doubleCount++; + } + } + + expect(doubleCount).toBe(1); + expect(singleCount).toBe(DOUBLE_CHANCE - 1); + }); }); diff --git a/src/test/moves/gastro_acid.test.ts b/src/test/moves/gastro_acid.test.ts index fdd75b90b136..ec9246c855c2 100644 --- a/src/test/moves/gastro_acid.test.ts +++ b/src/test/moves/gastro_acid.test.ts @@ -62,7 +62,7 @@ describe("Moves - Gastro Acid", () => { }); it("fails if used on an enemy with an already-suppressed ability", async () => { - game.override.battleType(null); + game.override.battleType("single"); await game.startBattle(); diff --git a/src/test/utils/helpers/overridesHelper.ts b/src/test/utils/helpers/overridesHelper.ts index 02950d497ee6..1c05f92a3348 100644 --- a/src/test/utils/helpers/overridesHelper.ts +++ b/src/test/utils/helpers/overridesHelper.ts @@ -4,7 +4,7 @@ import { Abilities } from "#app/enums/abilities"; import * as GameMode from "#app/game-mode"; import { GameModes, getGameMode } from "#app/game-mode"; import { ModifierOverride } from "#app/modifier/modifier-type"; -import Overrides from "#app/overrides"; +import Overrides, { BattleStyle } from "#app/overrides"; import { Unlockables } from "#app/system/unlockables"; import { Biome } from "#enums/biome"; import { Moves } from "#enums/moves"; @@ -238,13 +238,14 @@ export class OverridesHelper extends GameManagerHelper { } /** - * Override the battle type (single or double) + * Override the battle type (e.g., single or double). + * @see {@linkcode Overrides.BATTLE_TYPE_OVERRIDE} * @param battleType battle type to set * @returns `this` */ - public battleType(battleType: "single" | "double" | null): this { + public battleType(battleType: BattleStyle | null): this { vi.spyOn(Overrides, "BATTLE_TYPE_OVERRIDE", "get").mockReturnValue(battleType); - this.log(`Battle type set to ${battleType} only!`); + this.log(battleType === null ? "Battle type override disabled!" : `Battle type set to ${battleType}!`); return this; } From 58912db8f1044a8ea5fa23a6f4989bfdc1cc426f Mon Sep 17 00:00:00 2001 From: Mumble <171087428+frutescens@users.noreply.github.com> Date: Thu, 14 Nov 2024 13:16:05 -0800 Subject: [PATCH 28/37] [P2] Telekinesis now sets FloatingTag to 3 turns instead of 5 turns (#4869) Co-authored-by: frutescens --- src/data/battler-tags.ts | 6 +++--- src/data/move.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 25d65fbc3722..5c6d9d66b7cf 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -1809,8 +1809,8 @@ export class TypeImmuneTag extends BattlerTag { * @see {@link https://bulbapedia.bulbagarden.net/wiki/Telekinesis_(move) | Moves.TELEKINESIS} */ export class FloatingTag extends TypeImmuneTag { - constructor(tagType: BattlerTagType, sourceMove: Moves) { - super(tagType, sourceMove, Type.GROUND, 5); + constructor(tagType: BattlerTagType, sourceMove: Moves, turnCount: number) { + super(tagType, sourceMove, Type.GROUND, turnCount); } onAdd(pokemon: Pokemon): void { @@ -3053,7 +3053,7 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source case BattlerTagType.CHARGED: return new TypeBoostTag(tagType, sourceMove, Type.ELECTRIC, 2, true); case BattlerTagType.FLOATING: - return new FloatingTag(tagType, sourceMove); + return new FloatingTag(tagType, sourceMove, turnCount); case BattlerTagType.MINIMIZED: return new MinimizeTag(); case BattlerTagType.DESTINY_BOND: diff --git a/src/data/move.ts b/src/data/move.ts index 74ac61af8844..a288c0e96183 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -8897,7 +8897,7 @@ export function initMoves() { new SelfStatusMove(Moves.AQUA_RING, Type.WATER, -1, 20, -1, 0, 4) .attr(AddBattlerTagAttr, BattlerTagType.AQUA_RING, true, true), new SelfStatusMove(Moves.MAGNET_RISE, Type.ELECTRIC, -1, 10, -1, 0, 4) - .attr(AddBattlerTagAttr, BattlerTagType.FLOATING, true, true) + .attr(AddBattlerTagAttr, BattlerTagType.FLOATING, true, true, 5) .condition((user, target, move) => !user.scene.arena.getTag(ArenaTagType.GRAVITY) && [ BattlerTagType.FLOATING, BattlerTagType.IGNORE_FLYING, BattlerTagType.INGRAIN ].every((tag) => !user.getTag(tag))), new AttackMove(Moves.FLARE_BLITZ, Type.FIRE, MoveCategory.PHYSICAL, 120, 100, 15, 10, 0, 4) .attr(RecoilAttr, false, 0.33) From f778bd587700c5def856d038a51b1b25fc42da90 Mon Sep 17 00:00:00 2001 From: Moka <54149968+MokaStitcher@users.noreply.github.com> Date: Fri, 15 Nov 2024 00:01:04 +0100 Subject: [PATCH 29/37] Prevent crash from null dexData attributes (#4871) --- src/system/game-data.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/system/game-data.ts b/src/system/game-data.ts index a9726ac0713b..4d01ae9998a5 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -679,7 +679,7 @@ export class GameData { return ret; } - return k.endsWith("Attr") && ![ "natureAttr", "abilityAttr", "passiveAttr" ].includes(k) ? BigInt(v) : v; + return k.endsWith("Attr") && ![ "natureAttr", "abilityAttr", "passiveAttr" ].includes(k) ? BigInt(v ?? 0) : v; }) as SystemSaveData; } From b1138c1d70a9428e767539ff64c858bf5dc80f36 Mon Sep 17 00:00:00 2001 From: PigeonBar <56974298+PigeonBar@users.noreply.github.com> Date: Fri, 15 Nov 2024 00:07:19 -0500 Subject: [PATCH 30/37] [P2][Beta] Freeze-dry Re-implementation (#4874) --- src/data/ability.ts | 4 +- src/data/move.ts | 82 ++++++++++++++----------------- src/field/pokemon.ts | 10 ++-- src/test/moves/freeze_dry.test.ts | 68 ++++++++++++++++++++++++- 4 files changed, 113 insertions(+), 51 deletions(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index d58c6c5c9b9c..b77b18947be8 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -516,7 +516,7 @@ export class NonSuperEffectiveImmunityAbAttr extends TypeImmunityAbAttr { applyPreDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, cancelled: Utils.BooleanHolder, args: any[]): boolean { const modifierValue = args.length > 0 ? (args[0] as Utils.NumberHolder).value - : pokemon.getAttackTypeEffectiveness(attacker.getMoveType(move), attacker); + : pokemon.getAttackTypeEffectiveness(attacker.getMoveType(move), attacker, undefined, undefined, move); if (move instanceof AttackMove && modifierValue < 2) { cancelled.value = true; // Suppresses "No Effect" message @@ -3180,7 +3180,7 @@ function getAnticipationCondition(): AbAttrCondition { continue; } // the move's base type (not accounting for variable type changes) is super effective - if (move.getMove() instanceof AttackMove && pokemon.getAttackTypeEffectiveness(move.getMove().type, opponent, true) >= 2) { + if (move.getMove() instanceof AttackMove && pokemon.getAttackTypeEffectiveness(move.getMove().type, opponent, true, undefined, move.getMove()) >= 2) { return true; } // move is a OHKO diff --git a/src/data/move.ts b/src/data/move.ts index a288c0e96183..e7cd9d106158 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -897,7 +897,7 @@ export class AttackMove extends Move { let attackScore = 0; - const effectiveness = target.getAttackTypeEffectiveness(this.type, user); + const effectiveness = target.getAttackTypeEffectiveness(this.type, user, undefined, undefined, this); attackScore = Math.pow(effectiveness - 1, 2) * effectiveness < 1 ? -2 : 2; if (attackScore) { if (this.category === MoveCategory.PHYSICAL) { @@ -4971,48 +4971,6 @@ export class NeutralDamageAgainstFlyingTypeMultiplierAttr extends VariableMoveTy } } -/** - * This class forces Freeze-Dry to be super effective against Water Type. - * It considers if target is Mono or Dual Type and calculates the new Multiplier accordingly. - * @see {@linkcode apply} - */ -export class FreezeDryAttr extends VariableMoveTypeMultiplierAttr { - /** - * If the target is Mono Type (Water only) then a 2x Multiplier is always forced. - * If target is Dual Type (containing Water) then only a 2x Multiplier is forced for the Water Type. - * - * Additionally Freeze-Dry's effectiveness against water is always forced during {@linkcode InverseBattleChallenge}. - * The multiplier is recalculated for the non-Water Type in case of Dual Type targets containing Water Type. - * - * @param user The {@linkcode Pokemon} applying the move - * @param target The {@linkcode Pokemon} targeted by the move - * @param move The move used by the user - * @param args `[0]` a {@linkcode Utils.NumberHolder | NumberHolder} containing a type effectiveness multiplier - * @returns `true` if super effectiveness on water type is forced; `false` otherwise - */ - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - const multiplier = args[0] as Utils.NumberHolder; - if (target.isOfType(Type.WATER) && multiplier.value !== 0) { - const multipleTypes = (target.getTypes().length > 1); - - if (multipleTypes) { - const nonWaterType = target.getTypes().filter(type => type !== Type.WATER)[0]; - const effectivenessAgainstTarget = new Utils.NumberHolder(getTypeDamageMultiplier(user.getMoveType(move), nonWaterType)); - - applyChallenges(user.scene.gameMode, ChallengeType.TYPE_EFFECTIVENESS, effectivenessAgainstTarget); - - multiplier.value = effectivenessAgainstTarget.value * 2; - return true; - } - - multiplier.value = 2; - return true; - } - - return false; - } -} - export class IceNoEffectTypeAttr extends VariableMoveTypeMultiplierAttr { /** * Checks to see if the Target is Ice-Type or not. If so, the move will have no effect. @@ -5040,6 +4998,41 @@ export class FlyingTypeMultiplierAttr extends VariableMoveTypeMultiplierAttr { } } +/** + * Attribute for moves which have a custom type chart interaction. + */ +export class VariableMoveTypeChartAttr extends MoveAttr { + /** + * @param user {@linkcode Pokemon} using the move + * @param target {@linkcode Pokemon} target of the move + * @param move {@linkcode Move} with this attribute + * @param args [0] {@linkcode NumberHolder} holding the type effectiveness + * @param args [1] A single defensive type of the target + * + * @returns true if application of the attribute succeeds + */ + apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + return false; + } +} + +/** + * This class forces Freeze-Dry to be super effective against Water Type. + */ +export class FreezeDryAttr extends VariableMoveTypeChartAttr { + apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + const multiplier = args[0] as Utils.NumberHolder; + const defType = args[1] as Type; + + if (defType === Type.WATER) { + multiplier.value = 2; + return true; + } else { + return false; + } + } +} + export class OneHitKOAccuracyAttr extends VariableAccuracyAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { const accuracy = args[0] as Utils.NumberHolder; @@ -9448,8 +9441,7 @@ export function initMoves() { .target(MoveTarget.ALL_NEAR_OTHERS), new AttackMove(Moves.FREEZE_DRY, Type.ICE, MoveCategory.SPECIAL, 70, 100, 20, 10, 0, 6) .attr(StatusEffectAttr, StatusEffect.FREEZE) - .attr(FreezeDryAttr) - .edgeCase(), // This currently just multiplies the move's power instead of changing its effectiveness. It also doesn't account for abilities that modify type effectiveness such as tera shell. + .attr(FreezeDryAttr), new AttackMove(Moves.DISARMING_VOICE, Type.FAIRY, MoveCategory.SPECIAL, 40, -1, 15, -1, 0, 6) .soundBased() .target(MoveTarget.ALL_NEAR_ENEMIES), diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 5d912f7d6e6a..bc7e844a290e 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -3,7 +3,7 @@ import BattleScene, { AnySound } from "#app/battle-scene"; import { Variant, VariantSet, variantColorCache } from "#app/data/variant"; import { variantData } from "#app/data/variant"; import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from "#app/ui/battle-info"; -import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, VariableMoveTypeAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatStagesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatStageChangeAttr, RechargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, OneHitKOAccuracyAttr, RespectAttackTypeImmunityAttr, MoveTarget, CombinedPledgeStabBoostAttr } from "#app/data/move"; +import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, VariableMoveTypeAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatStagesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatStageChangeAttr, RechargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, OneHitKOAccuracyAttr, RespectAttackTypeImmunityAttr, MoveTarget, CombinedPledgeStabBoostAttr, VariableMoveTypeChartAttr } from "#app/data/move"; import { default as PokemonSpecies, PokemonSpeciesForm, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm } from "#app/data/pokemon-species"; import { CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER, getStarterValueFriendshipCap, speciesStarterCosts } from "#app/data/balance/starters"; import { starterPassiveAbilities } from "#app/data/balance/passives"; @@ -1632,7 +1632,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const moveType = source.getMoveType(move); const typeMultiplier = new Utils.NumberHolder((move.category !== MoveCategory.STATUS || move.hasAttr(RespectAttackTypeImmunityAttr)) - ? this.getAttackTypeEffectiveness(moveType, source, false, simulated) + ? this.getAttackTypeEffectiveness(moveType, source, false, simulated, move) : 1); applyMoveAttrs(VariableMoveTypeMultiplierAttr, source, this, move, typeMultiplier); @@ -1684,9 +1684,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @param source {@linkcode Pokemon} the Pokemon using the move * @param ignoreStrongWinds whether or not this ignores strong winds (anticipation, forewarn, stealth rocks) * @param simulated tag to only apply the strong winds effect message when the move is used + * @param move (optional) the move whose type effectiveness is to be checked. Used for applying {@linkcode VariableMoveTypeChartAttr} * @returns a multiplier for the type effectiveness */ - getAttackTypeEffectiveness(moveType: Type, source?: Pokemon, ignoreStrongWinds: boolean = false, simulated: boolean = true): TypeDamageMultiplier { + getAttackTypeEffectiveness(moveType: Type, source?: Pokemon, ignoreStrongWinds: boolean = false, simulated: boolean = true, move?: Move): TypeDamageMultiplier { if (moveType === Type.STELLAR) { return this.isTerastallized() ? 2 : 1; } @@ -1705,6 +1706,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { let multiplier = types.map(defType => { const multiplier = new Utils.NumberHolder(getTypeDamageMultiplier(moveType, defType)); applyChallenges(this.scene.gameMode, ChallengeType.TYPE_EFFECTIVENESS, multiplier); + if (move) { + applyMoveAttrs(VariableMoveTypeChartAttr, null, this, move, multiplier, defType); + } if (source) { const ignoreImmunity = new Utils.BooleanHolder(false); if (source.isActive(true) && source.hasAbilityWithAttr(IgnoreTypeImmunityAbAttr)) { diff --git a/src/test/moves/freeze_dry.test.ts b/src/test/moves/freeze_dry.test.ts index 8bc6717f4352..9206a103a35d 100644 --- a/src/test/moves/freeze_dry.test.ts +++ b/src/test/moves/freeze_dry.test.ts @@ -2,6 +2,7 @@ import { BattlerIndex } from "#app/battle"; import { Abilities } from "#app/enums/abilities"; import { Moves } from "#app/enums/moves"; import { Species } from "#app/enums/species"; +import { Type } from "#enums/type"; import { Challenges } from "#enums/challenges"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; @@ -29,7 +30,7 @@ describe("Moves - Freeze-Dry", () => { .enemyMoveset(Moves.SPLASH) .starterSpecies(Species.FEEBAS) .ability(Abilities.BALL_FETCH) - .moveset([ Moves.FREEZE_DRY ]); + .moveset([ Moves.FREEZE_DRY, Moves.FORESTS_CURSE, Moves.SOAK ]); }); it("should deal 2x damage to pure water types", async () => { @@ -98,6 +99,71 @@ describe("Moves - Freeze-Dry", () => { expect(enemy.hp).toBeLessThan(enemy.getMaxHp()); }); + it("should deal 8x damage to water/ground/grass type under Forest's Curse", async () => { + game.override.enemySpecies(Species.QUAGSIRE); + await game.classicMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.FORESTS_CURSE); + await game.toNextTurn(); + + game.move.select(Moves.FREEZE_DRY); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("MoveEffectPhase"); + + expect(enemy.getMoveEffectiveness).toHaveReturnedWith(8); + }); + + it("should deal 2x damage to steel type terastallized into water", async () => { + game.override.enemySpecies(Species.SKARMORY) + .enemyHeldItems([{ name: "TERA_SHARD", type: Type.WATER }]); + await game.classicMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.FREEZE_DRY); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("MoveEffectPhase"); + + expect(enemy.getMoveEffectiveness).toHaveReturnedWith(2); + }); + + it("should deal 0.5x damage to water type terastallized into fire", async () => { + game.override.enemySpecies(Species.PELIPPER) + .enemyHeldItems([{ name: "TERA_SHARD", type: Type.FIRE }]); + await game.classicMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.FREEZE_DRY); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("MoveEffectPhase"); + + expect(enemy.getMoveEffectiveness).toHaveReturnedWith(0.5); + }); + + it("should deal 0.5x damage to water type Terapagos with Tera Shell", async () => { + game.override.enemySpecies(Species.TERAPAGOS) + .enemyAbility(Abilities.TERA_SHELL); + await game.classicMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.SOAK); + await game.toNextTurn(); + + game.move.select(Moves.FREEZE_DRY); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("MoveEffectPhase"); + + expect(enemy.getMoveEffectiveness).toHaveReturnedWith(0.5); + }); + it("should deal 2x damage to water type under Normalize", async () => { game.override.ability(Abilities.NORMALIZE); await game.classicMode.startBattle(); From 6dec84e39c0e1229160af14dddb0b9df6aeedfcb Mon Sep 17 00:00:00 2001 From: Mumble <171087428+frutescens@users.noreply.github.com> Date: Thu, 14 Nov 2024 21:13:23 -0800 Subject: [PATCH 31/37] [Balance] No more double 50x bosses in Endless + cleaning up RNG (#4862) * Added double battle exclusion to Endless bosses. * Brought Endure token RNG in line with game RNG formatting. * Corrected incorrect modulo condition in isEndlessBoss * Moved new doubles conditional to be above overrides. * Fixed bad RNG calls for Covet and Thief too. * Updated unburden test. * Revert "Updated unburden test." This reverts commit 01788d40c2f5c32d89bd0cb899bfc67fc77f881e. * Revert "Fixed bad RNG calls for Covet and Thief too." This reverts commit c7fcfd195de7e98c1e582ec67c829e003663cad0. --------- Co-authored-by: frutescens --- src/battle-scene.ts | 5 +++++ src/game-mode.ts | 2 +- src/modifier/modifier.ts | 8 ++++---- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/battle-scene.ts b/src/battle-scene.ts index e659b588208c..061fc6e28f20 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -1233,6 +1233,11 @@ export default class BattleScene extends SceneBase { newDouble = !!double; } + // Disable double battles on Endless/Endless Spliced Wave 50x boss battles (Introduced 1.2.0) + if (this.gameMode.isEndlessBoss(newWaveIndex)) { + newDouble = false; + } + if (!isNullOrUndefined(Overrides.BATTLE_TYPE_OVERRIDE)) { let doubleOverrideForWave: "single" | "double" | null = null; diff --git a/src/game-mode.ts b/src/game-mode.ts index 8f1bb9356e6b..0f997bf651e3 100644 --- a/src/game-mode.ts +++ b/src/game-mode.ts @@ -236,7 +236,7 @@ export class GameMode implements GameModeConfig { * @returns true if waveIndex is a multiple of 50 in Endless */ isEndlessBoss(waveIndex: integer): boolean { - return !!(waveIndex % 50) && + return waveIndex % 50 === 0 && (this.modeId === GameModes.ENDLESS || this.modeId === GameModes.SPLICED_ENDLESS); } diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index 7aa4b9308d10..ac8dc556b98a 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -3631,7 +3631,7 @@ export class EnemyEndureChanceModifier extends EnemyPersistentModifier { super(type, stackCount || 10); //Hardcode temporarily - this.chance = .02; + this.chance = 2; } match(modifier: Modifier) { @@ -3639,11 +3639,11 @@ export class EnemyEndureChanceModifier extends EnemyPersistentModifier { } clone() { - return new EnemyEndureChanceModifier(this.type, this.chance * 100, this.stackCount); + return new EnemyEndureChanceModifier(this.type, this.chance, this.stackCount); } getArgs(): any[] { - return [ this.chance * 100 ]; + return [ this.chance ]; } /** @@ -3652,7 +3652,7 @@ export class EnemyEndureChanceModifier extends EnemyPersistentModifier { * @returns `true` if {@linkcode Pokemon} endured */ override apply(target: Pokemon): boolean { - if (target.battleData.endured || Phaser.Math.RND.realInRange(0, 1) >= (this.chance * this.getStackCount())) { + if (target.battleData.endured || target.randSeedInt(100) >= (this.chance * this.getStackCount())) { return false; } From 8326e3556b90c95379bf7c5c95ed31c33485d2cc Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Fri, 15 Nov 2024 08:29:52 -0800 Subject: [PATCH 32/37] Remove `.edgeCase()` from fully implemented moves (#4876) This includes Sunsteel Strike, Moongeist Beam and Photon Geyser --- src/data/move.ts | 9 ++-- src/test/moves/moongeist_beam.test.ts | 59 +++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 6 deletions(-) create mode 100644 src/test/moves/moongeist_beam.test.ts diff --git a/src/data/move.ts b/src/data/move.ts index e7cd9d106158..ae2ba919b526 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -9876,11 +9876,9 @@ export function initMoves() { .ignoresSubstitute() .partial(), // Does not steal stats new AttackMove(Moves.SUNSTEEL_STRIKE, Type.STEEL, MoveCategory.PHYSICAL, 100, 100, 5, -1, 0, 7) - .ignoresAbilities() - .edgeCase(), // Should not ignore abilities when called virtually (metronome) + .ignoresAbilities(), new AttackMove(Moves.MOONGEIST_BEAM, Type.GHOST, MoveCategory.SPECIAL, 100, 100, 5, -1, 0, 7) - .ignoresAbilities() - .edgeCase(), // Should not ignore abilities when called virtually (metronome) + .ignoresAbilities(), new StatusMove(Moves.TEARFUL_LOOK, Type.NORMAL, -1, 20, -1, 0, 7) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1), new AttackMove(Moves.ZING_ZAP, Type.ELECTRIC, MoveCategory.PHYSICAL, 80, 100, 10, 30, 0, 7) @@ -9903,8 +9901,7 @@ export function initMoves() { .punchingMove(), new AttackMove(Moves.PHOTON_GEYSER, Type.PSYCHIC, MoveCategory.SPECIAL, 100, 100, 5, -1, 0, 7) .attr(PhotonGeyserCategoryAttr) - .ignoresAbilities() - .edgeCase(), // Should not ignore abilities when called virtually (metronome) + .ignoresAbilities(), /* Unused */ new AttackMove(Moves.LIGHT_THAT_BURNS_THE_SKY, Type.PSYCHIC, MoveCategory.SPECIAL, 200, -1, 1, -1, 0, 7) .attr(PhotonGeyserCategoryAttr) diff --git a/src/test/moves/moongeist_beam.test.ts b/src/test/moves/moongeist_beam.test.ts new file mode 100644 index 000000000000..216eee482fb6 --- /dev/null +++ b/src/test/moves/moongeist_beam.test.ts @@ -0,0 +1,59 @@ +import { allMoves, RandomMoveAttr } from "#app/data/move"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +describe("Moves - Moongeist Beam", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .moveset([ Moves.MOONGEIST_BEAM, Moves.METRONOME ]) + .ability(Abilities.BALL_FETCH) + .startingLevel(200) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.STURDY) + .enemyMoveset(Moves.SPLASH); + }); + + // Also covers Photon Geyser and Sunsteel Strike + it("should ignore enemy abilities", async () => { + await game.classicMode.startBattle([ Species.MILOTIC ]); + + const enemy = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.MOONGEIST_BEAM); + await game.phaseInterceptor.to("BerryPhase"); + + expect(enemy.isFainted()).toBe(true); + }); + + // Also covers Photon Geyser and Sunsteel Strike + it("should not ignore enemy abilities when called by another move, such as metronome", async () => { + await game.classicMode.startBattle([ Species.MILOTIC ]); + vi.spyOn(allMoves[Moves.METRONOME].getAttrs(RandomMoveAttr)[0], "getMoveOverride").mockReturnValue(Moves.MOONGEIST_BEAM); + + game.move.select(Moves.METRONOME); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getEnemyPokemon()!.isFainted()).toBe(false); + expect(game.scene.getPlayerPokemon()!.getLastXMoves()[0].move).toBe(Moves.MOONGEIST_BEAM); + }); +}); From 9273b4930d929cd9c8fb67466ea2501e8ffb8bf4 Mon Sep 17 00:00:00 2001 From: innerthunder <168692175+innerthunder@users.noreply.github.com> Date: Fri, 15 Nov 2024 08:56:05 -0800 Subject: [PATCH 33/37] [Beta][P1] Fix crash when resetting Commanded Dondozo before Trainer battles (#4873) * Add failsafe to Commander remove anim * Commanded tag saves Tatsu form on reload --- src/data/battler-tags.ts | 5 +++++ src/phases/pokemon-anim-phase.ts | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 5c6d9d66b7cf..28ab5ff2a4f1 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -2158,6 +2158,11 @@ export class CommandedTag extends BattlerTag { pokemon.scene.triggerPokemonBattleAnim(pokemon, PokemonAnimType.COMMANDER_REMOVE); } } + + override loadTag(source: BattlerTag | any): void { + super.loadTag(source); + this._tatsugiriFormKey = source._tatsugiriFormKey; + } } /** diff --git a/src/phases/pokemon-anim-phase.ts b/src/phases/pokemon-anim-phase.ts index ad0be34af7d0..eb5431cbc56b 100644 --- a/src/phases/pokemon-anim-phase.ts +++ b/src/phases/pokemon-anim-phase.ts @@ -312,6 +312,10 @@ export class PokemonAnimPhase extends BattlePhase { // Note: unlike the other Commander animation, this is played through the // Dondozo instead of the Tatsugiri. const tatsugiri = this.pokemon.getAlly(); + if (isNullOrUndefined(tatsugiri)) { + console.warn("Aborting COMMANDER_REMOVE anim: Tatsugiri is undefined"); + return this.end(); + } const tatsuSprite = this.scene.addPokemonSprite( tatsugiri, From ef7d860166136e126973a69bd80450dfd92c1c25 Mon Sep 17 00:00:00 2001 From: AJ Fontaine <36677462+Fontbane@users.noreply.github.com> Date: Fri, 15 Nov 2024 11:57:02 -0500 Subject: [PATCH 34/37] [Balance] Remove from trainers: Pika/Eevee forms before 30, BB Greninja, Rival starter HA (#4863) * Remove Pika/Eevee forms from Trainers before wave 30, and BB Gren * Fix `egg` test * Ban hidden ability from Rival starter --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/battle-scene.ts | 9 +++++++++ src/data/trainer-config.ts | 18 +++++++++++++----- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 061fc6e28f20..2f0626678086 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -1424,10 +1424,19 @@ export default class BattleScene extends SceneBase { case Species.PALDEA_TAUROS: return Utils.randSeedInt(species.forms.length); case Species.PIKACHU: + if (this.currentBattle?.battleType === BattleType.TRAINER && this.currentBattle?.waveIndex < 30) { + return 0; // Ban Cosplay and Partner Pika from Trainers before wave 30 + } return Utils.randSeedInt(8); case Species.EEVEE: + if (this.currentBattle?.battleType === BattleType.TRAINER && this.currentBattle?.waveIndex < 30) { + return 0; // No Partner Eevee for Wave 12 Preschoolers + } return Utils.randSeedInt(2); case Species.GRENINJA: + if (this.currentBattle?.battleType === BattleType.TRAINER) { + return 0; // Don't give trainers Battle Bond Greninja + } return Utils.randSeedInt(2); case Species.ZYGARDE: return Utils.randSeedInt(4); diff --git a/src/data/trainer-config.ts b/src/data/trainer-config.ts index d82d568ecc6c..5e5f38bd00d8 100644 --- a/src/data/trainer-config.ts +++ b/src/data/trainer-config.ts @@ -1841,21 +1841,25 @@ export const trainerConfigs: TrainerConfigs = { [TrainerType.RIVAL]: new TrainerConfig((t = TrainerType.RIVAL)).setName("Finn").setHasGenders("Ivy").setHasCharSprite().setTitle("Rival").setStaticParty().setEncounterBgm(TrainerType.RIVAL).setBattleBgm("battle_rival").setMixedBattleBgm("battle_rival").setPartyTemplates(trainerPartyTemplates.RIVAL) .setModifierRewardFuncs(() => modifierTypes.SUPER_EXP_CHARM, () => modifierTypes.EXP_SHARE) .setEventModifierRewardFuncs(() => modifierTypes.SHINY_CHARM, () => modifierTypes.ABILITY_CHARM) - .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE, Species.CHIKORITA, Species.CYNDAQUIL, Species.TOTODILE, Species.TREECKO, Species.TORCHIC, Species.MUDKIP, Species.TURTWIG, Species.CHIMCHAR, Species.PIPLUP, Species.SNIVY, Species.TEPIG, Species.OSHAWOTT, Species.CHESPIN, Species.FENNEKIN, Species.FROAKIE, Species.ROWLET, Species.LITTEN, Species.POPPLIO, Species.GROOKEY, Species.SCORBUNNY, Species.SOBBLE, Species.SPRIGATITO, Species.FUECOCO, Species.QUAXLY ], TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE, Species.CHIKORITA, Species.CYNDAQUIL, Species.TOTODILE, Species.TREECKO, Species.TORCHIC, Species.MUDKIP, Species.TURTWIG, Species.CHIMCHAR, Species.PIPLUP, Species.SNIVY, Species.TEPIG, Species.OSHAWOTT, Species.CHESPIN, Species.FENNEKIN, Species.FROAKIE, Species.ROWLET, Species.LITTEN, Species.POPPLIO, Species.GROOKEY, Species.SCORBUNNY, Species.SOBBLE, Species.SPRIGATITO, Species.FUECOCO, Species.QUAXLY ], TrainerSlot.TRAINER, true, + (p => p.abilityIndex = 0))) .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.PIDGEY, Species.HOOTHOOT, Species.TAILLOW, Species.STARLY, Species.PIDOVE, Species.FLETCHLING, Species.PIKIPEK, Species.ROOKIDEE, Species.WATTREL ], TrainerSlot.TRAINER, true)), [TrainerType.RIVAL_2]: new TrainerConfig(++t).setName("Finn").setHasGenders("Ivy").setHasCharSprite().setTitle("Rival").setStaticParty().setMoneyMultiplier(1.25).setEncounterBgm(TrainerType.RIVAL).setBattleBgm("battle_rival").setMixedBattleBgm("battle_rival").setPartyTemplates(trainerPartyTemplates.RIVAL_2) .setModifierRewardFuncs(() => modifierTypes.EXP_SHARE) .setEventModifierRewardFuncs(() => modifierTypes.SHINY_CHARM) - .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.IVYSAUR, Species.CHARMELEON, Species.WARTORTLE, Species.BAYLEEF, Species.QUILAVA, Species.CROCONAW, Species.GROVYLE, Species.COMBUSKEN, Species.MARSHTOMP, Species.GROTLE, Species.MONFERNO, Species.PRINPLUP, Species.SERVINE, Species.PIGNITE, Species.DEWOTT, Species.QUILLADIN, Species.BRAIXEN, Species.FROGADIER, Species.DARTRIX, Species.TORRACAT, Species.BRIONNE, Species.THWACKEY, Species.RABOOT, Species.DRIZZILE, Species.FLORAGATO, Species.CROCALOR, Species.QUAXWELL ], TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.IVYSAUR, Species.CHARMELEON, Species.WARTORTLE, Species.BAYLEEF, Species.QUILAVA, Species.CROCONAW, Species.GROVYLE, Species.COMBUSKEN, Species.MARSHTOMP, Species.GROTLE, Species.MONFERNO, Species.PRINPLUP, Species.SERVINE, Species.PIGNITE, Species.DEWOTT, Species.QUILLADIN, Species.BRAIXEN, Species.FROGADIER, Species.DARTRIX, Species.TORRACAT, Species.BRIONNE, Species.THWACKEY, Species.RABOOT, Species.DRIZZILE, Species.FLORAGATO, Species.CROCALOR, Species.QUAXWELL ], TrainerSlot.TRAINER, true, + (p => p.abilityIndex = 0))) .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.PIDGEOTTO, Species.HOOTHOOT, Species.TAILLOW, Species.STARAVIA, Species.TRANQUILL, Species.FLETCHINDER, Species.TRUMBEAK, Species.CORVISQUIRE, Species.WATTREL ], TrainerSlot.TRAINER, true)) .setPartyMemberFunc(2, getSpeciesFilterRandomPartyMemberFunc((species: PokemonSpecies) => !pokemonEvolutions.hasOwnProperty(species.speciesId) && !pokemonPrevolutions.hasOwnProperty(species.speciesId) && species.baseTotal >= 450)), [TrainerType.RIVAL_3]: new TrainerConfig(++t).setName("Finn").setHasGenders("Ivy").setHasCharSprite().setTitle("Rival").setStaticParty().setMoneyMultiplier(1.5).setEncounterBgm(TrainerType.RIVAL).setBattleBgm("battle_rival").setMixedBattleBgm("battle_rival").setPartyTemplates(trainerPartyTemplates.RIVAL_3) - .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.VENUSAUR, Species.CHARIZARD, Species.BLASTOISE, Species.MEGANIUM, Species.TYPHLOSION, Species.FERALIGATR, Species.SCEPTILE, Species.BLAZIKEN, Species.SWAMPERT, Species.TORTERRA, Species.INFERNAPE, Species.EMPOLEON, Species.SERPERIOR, Species.EMBOAR, Species.SAMUROTT, Species.CHESNAUGHT, Species.DELPHOX, Species.GRENINJA, Species.DECIDUEYE, Species.INCINEROAR, Species.PRIMARINA, Species.RILLABOOM, Species.CINDERACE, Species.INTELEON, Species.MEOWSCARADA, Species.SKELEDIRGE, Species.QUAQUAVAL ], TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.VENUSAUR, Species.CHARIZARD, Species.BLASTOISE, Species.MEGANIUM, Species.TYPHLOSION, Species.FERALIGATR, Species.SCEPTILE, Species.BLAZIKEN, Species.SWAMPERT, Species.TORTERRA, Species.INFERNAPE, Species.EMPOLEON, Species.SERPERIOR, Species.EMBOAR, Species.SAMUROTT, Species.CHESNAUGHT, Species.DELPHOX, Species.GRENINJA, Species.DECIDUEYE, Species.INCINEROAR, Species.PRIMARINA, Species.RILLABOOM, Species.CINDERACE, Species.INTELEON, Species.MEOWSCARADA, Species.SKELEDIRGE, Species.QUAQUAVAL ], TrainerSlot.TRAINER, true, + (p => p.abilityIndex = 0))) .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.PIDGEOT, Species.NOCTOWL, Species.SWELLOW, Species.STARAPTOR, Species.UNFEZANT, Species.TALONFLAME, Species.TOUCANNON, Species.CORVIKNIGHT, Species.KILOWATTREL ], TrainerSlot.TRAINER, true)) .setPartyMemberFunc(2, getSpeciesFilterRandomPartyMemberFunc((species: PokemonSpecies) => !pokemonEvolutions.hasOwnProperty(species.speciesId) && !pokemonPrevolutions.hasOwnProperty(species.speciesId) && species.baseTotal >= 450)) .setSpeciesFilter(species => species.baseTotal >= 540), [TrainerType.RIVAL_4]: new TrainerConfig(++t).setName("Finn").setHasGenders("Ivy").setHasCharSprite().setTitle("Rival").setBoss().setStaticParty().setMoneyMultiplier(1.75).setEncounterBgm(TrainerType.RIVAL).setBattleBgm("battle_rival_2").setMixedBattleBgm("battle_rival_2").setPartyTemplates(trainerPartyTemplates.RIVAL_4) - .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.VENUSAUR, Species.CHARIZARD, Species.BLASTOISE, Species.MEGANIUM, Species.TYPHLOSION, Species.FERALIGATR, Species.SCEPTILE, Species.BLAZIKEN, Species.SWAMPERT, Species.TORTERRA, Species.INFERNAPE, Species.EMPOLEON, Species.SERPERIOR, Species.EMBOAR, Species.SAMUROTT, Species.CHESNAUGHT, Species.DELPHOX, Species.GRENINJA, Species.DECIDUEYE, Species.INCINEROAR, Species.PRIMARINA, Species.RILLABOOM, Species.CINDERACE, Species.INTELEON, Species.MEOWSCARADA, Species.SKELEDIRGE, Species.QUAQUAVAL ], TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.VENUSAUR, Species.CHARIZARD, Species.BLASTOISE, Species.MEGANIUM, Species.TYPHLOSION, Species.FERALIGATR, Species.SCEPTILE, Species.BLAZIKEN, Species.SWAMPERT, Species.TORTERRA, Species.INFERNAPE, Species.EMPOLEON, Species.SERPERIOR, Species.EMBOAR, Species.SAMUROTT, Species.CHESNAUGHT, Species.DELPHOX, Species.GRENINJA, Species.DECIDUEYE, Species.INCINEROAR, Species.PRIMARINA, Species.RILLABOOM, Species.CINDERACE, Species.INTELEON, Species.MEOWSCARADA, Species.SKELEDIRGE, Species.QUAQUAVAL ], TrainerSlot.TRAINER, true, + (p => p.abilityIndex = 0))) .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.PIDGEOT, Species.NOCTOWL, Species.SWELLOW, Species.STARAPTOR, Species.UNFEZANT, Species.TALONFLAME, Species.TOUCANNON, Species.CORVIKNIGHT, Species.KILOWATTREL ], TrainerSlot.TRAINER, true)) .setPartyMemberFunc(2, getSpeciesFilterRandomPartyMemberFunc((species: PokemonSpecies) => !pokemonEvolutions.hasOwnProperty(species.speciesId) && !pokemonPrevolutions.hasOwnProperty(species.speciesId) && species.baseTotal >= 450)) .setSpeciesFilter(species => species.baseTotal >= 540) @@ -1865,7 +1869,10 @@ export const trainerConfigs: TrainerConfigs = { }), [TrainerType.RIVAL_5]: new TrainerConfig(++t).setName("Finn").setHasGenders("Ivy").setHasCharSprite().setTitle("Rival").setBoss().setStaticParty().setMoneyMultiplier(2.25).setEncounterBgm(TrainerType.RIVAL).setBattleBgm("battle_rival_3").setMixedBattleBgm("battle_rival_3").setPartyTemplates(trainerPartyTemplates.RIVAL_5) .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.VENUSAUR, Species.CHARIZARD, Species.BLASTOISE, Species.MEGANIUM, Species.TYPHLOSION, Species.FERALIGATR, Species.SCEPTILE, Species.BLAZIKEN, Species.SWAMPERT, Species.TORTERRA, Species.INFERNAPE, Species.EMPOLEON, Species.SERPERIOR, Species.EMBOAR, Species.SAMUROTT, Species.CHESNAUGHT, Species.DELPHOX, Species.GRENINJA, Species.DECIDUEYE, Species.INCINEROAR, Species.PRIMARINA, Species.RILLABOOM, Species.CINDERACE, Species.INTELEON, Species.MEOWSCARADA, Species.SKELEDIRGE, Species.QUAQUAVAL ], TrainerSlot.TRAINER, true, - p => p.setBoss(true, 2))) + p => { + p.setBoss(true, 2); + p.abilityIndex = 0; + })) .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.PIDGEOT, Species.NOCTOWL, Species.SWELLOW, Species.STARAPTOR, Species.UNFEZANT, Species.TALONFLAME, Species.TOUCANNON, Species.CORVIKNIGHT, Species.KILOWATTREL ], TrainerSlot.TRAINER, true)) .setPartyMemberFunc(2, getSpeciesFilterRandomPartyMemberFunc((species: PokemonSpecies) => !pokemonEvolutions.hasOwnProperty(species.speciesId) && !pokemonPrevolutions.hasOwnProperty(species.speciesId) && species.baseTotal >= 450)) .setSpeciesFilter(species => species.baseTotal >= 540) @@ -1883,6 +1890,7 @@ export const trainerConfigs: TrainerConfigs = { .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.VENUSAUR, Species.CHARIZARD, Species.BLASTOISE, Species.MEGANIUM, Species.TYPHLOSION, Species.FERALIGATR, Species.SCEPTILE, Species.BLAZIKEN, Species.SWAMPERT, Species.TORTERRA, Species.INFERNAPE, Species.EMPOLEON, Species.SERPERIOR, Species.EMBOAR, Species.SAMUROTT, Species.CHESNAUGHT, Species.DELPHOX, Species.GRENINJA, Species.DECIDUEYE, Species.INCINEROAR, Species.PRIMARINA, Species.RILLABOOM, Species.CINDERACE, Species.INTELEON, Species.MEOWSCARADA, Species.SKELEDIRGE, Species.QUAQUAVAL ], TrainerSlot.TRAINER, true, p => { p.setBoss(true, 3); + p.abilityIndex = 0; p.generateAndPopulateMoveset(); })) .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.PIDGEOT, Species.NOCTOWL, Species.SWELLOW, Species.STARAPTOR, Species.UNFEZANT, Species.TALONFLAME, Species.TOUCANNON, Species.CORVIKNIGHT, Species.KILOWATTREL ], TrainerSlot.TRAINER, true, From 5ca1fd5cfd8cc8f98c9cfc939a484cea6399a57d Mon Sep 17 00:00:00 2001 From: pom-eranian Date: Fri, 15 Nov 2024 11:58:50 -0500 Subject: [PATCH 35/37] [Sprite] Set default fps to 10 instead of 12 on pokemon animations (#4842) * Set default fps to 10 instead of 12 for pokemon sprites * [Sprite] Set pokemon animation framerate to 10 where assigned --- src/data/pokemon-species.ts | 4 ++-- src/field/mystery-encounter-intro.ts | 2 +- src/field/pokemon.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/data/pokemon-species.ts b/src/data/pokemon-species.ts index 203e545503a2..ec104d4d4aa0 100644 --- a/src/data/pokemon-species.ts +++ b/src/data/pokemon-species.ts @@ -505,11 +505,11 @@ export abstract class PokemonSpeciesForm { scene.anims.create({ key: this.getSpriteKey(female, formIndex, shiny, variant), frames: frameNames, - frameRate: 12, + frameRate: 10, repeat: -1 }); } else { - scene.anims.get(spriteKey).frameRate = 12; + 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); diff --git a/src/field/mystery-encounter-intro.ts b/src/field/mystery-encounter-intro.ts index 12bcace500c1..1577d1157d75 100644 --- a/src/field/mystery-encounter-intro.ts +++ b/src/field/mystery-encounter-intro.ts @@ -212,7 +212,7 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con this.scene.anims.create({ key: config.spriteKey, frames: frameNames, - frameRate: 12, + frameRate: 10, repeat: -1 }); } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index bc7e844a290e..30d1aceea4b7 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -427,7 +427,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.scene.anims.create({ key: this.getBattleSpriteKey(), frames: battleFrameNames, - frameRate: 12, + frameRate: 10, repeat: -1 }); } @@ -3612,7 +3612,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } this.status = null; if (lastStatus === StatusEffect.SLEEP) { - this.setFrameRate(12); + this.setFrameRate(10); if (this.getTag(BattlerTagType.NIGHTMARE)) { this.lapseTag(BattlerTagType.NIGHTMARE); } From eb3c0d731a38178722636391f175002c9b467f5d Mon Sep 17 00:00:00 2001 From: Mumble <171087428+frutescens@users.noreply.github.com> Date: Fri, 15 Nov 2024 09:17:46 -0800 Subject: [PATCH 36/37] [P2] Lunar Blessing / Jungle Healing now heal Freeze (#4877) * Added Freeze to statuses healed by Jungle Healing / Lunar Blessing * Fixed up documentation. --------- Co-authored-by: frutescens --- src/data/move.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/data/move.ts b/src/data/move.ts index ae2ba919b526..2ac4d74b712f 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -2589,12 +2589,11 @@ export class HealStatusEffectAttr extends MoveEffectAttr { /** * @param selfTarget - Whether this move targets the user - * @param ...effects - List of status effects to cure + * @param effects - status effect or list of status effects to cure */ - constructor(selfTarget: boolean, ...effects: StatusEffect[]) { + constructor(selfTarget: boolean, effects: StatusEffect | StatusEffect[]) { super(selfTarget, { lastHitOnly: true }); - - this.effects = effects; + this.effects = [ effects ].flat(1); } /** @@ -8583,7 +8582,7 @@ export function initMoves() { .attr(AddArenaTagAttr, ArenaTagType.IMPRISON, 1, true, false) .target(MoveTarget.ENEMY_SIDE), new SelfStatusMove(Moves.REFRESH, Type.NORMAL, -1, 20, -1, 0, 3) - .attr(HealStatusEffectAttr, true, StatusEffect.PARALYSIS, StatusEffect.POISON, StatusEffect.TOXIC, StatusEffect.BURN) + .attr(HealStatusEffectAttr, true, [ StatusEffect.PARALYSIS, StatusEffect.POISON, StatusEffect.TOXIC, StatusEffect.BURN ]) .condition((user, target, move) => !!user.status && (user.status.effect === StatusEffect.PARALYSIS || user.status.effect === StatusEffect.POISON || user.status.effect === StatusEffect.TOXIC || user.status.effect === StatusEffect.BURN)), new SelfStatusMove(Moves.GRUDGE, Type.GHOST, -1, 5, -1, 0, 3) .attr(AddBattlerTagAttr, BattlerTagType.GRUDGE, true, undefined, 1), @@ -9792,7 +9791,7 @@ export function initMoves() { .condition( (user: Pokemon, target: Pokemon, move: Move) => isNonVolatileStatusEffect(target.status?.effect!)) // TODO: is this bang correct? .attr(HealAttr, 0.5) - .attr(HealStatusEffectAttr, false, ...getNonVolatileStatusEffects()) + .attr(HealStatusEffectAttr, false, getNonVolatileStatusEffects()) .triageMove(), new AttackMove(Moves.REVELATION_DANCE, Type.NORMAL, MoveCategory.SPECIAL, 90, 100, 15, -1, 0, 7) .danceMove() @@ -10216,7 +10215,7 @@ export function initMoves() { .attr(StatusEffectAttr, StatusEffect.BURN), new StatusMove(Moves.JUNGLE_HEALING, Type.GRASS, -1, 10, -1, 0, 8) .attr(HealAttr, 0.25, true, false) - .attr(HealStatusEffectAttr, false, StatusEffect.PARALYSIS, StatusEffect.POISON, StatusEffect.TOXIC, StatusEffect.BURN, StatusEffect.SLEEP) + .attr(HealStatusEffectAttr, false, getNonVolatileStatusEffects()) .target(MoveTarget.USER_AND_ALLIES), new AttackMove(Moves.WICKED_BLOW, Type.DARK, MoveCategory.PHYSICAL, 75, 100, 5, -1, 0, 8) .attr(CritOnlyAttr) @@ -10320,12 +10319,12 @@ export function initMoves() { .target(MoveTarget.ALL_NEAR_ENEMIES), new StatusMove(Moves.LUNAR_BLESSING, Type.PSYCHIC, -1, 5, -1, 0, 8) .attr(HealAttr, 0.25, true, false) - .attr(HealStatusEffectAttr, false, StatusEffect.PARALYSIS, StatusEffect.POISON, StatusEffect.TOXIC, StatusEffect.BURN, StatusEffect.SLEEP) + .attr(HealStatusEffectAttr, false, getNonVolatileStatusEffects()) .target(MoveTarget.USER_AND_ALLIES) .triageMove(), new SelfStatusMove(Moves.TAKE_HEART, Type.PSYCHIC, -1, 10, -1, 0, 8) .attr(StatStageChangeAttr, [ Stat.SPATK, Stat.SPDEF ], 1, true) - .attr(HealStatusEffectAttr, true, StatusEffect.PARALYSIS, StatusEffect.POISON, StatusEffect.TOXIC, StatusEffect.BURN, StatusEffect.SLEEP), + .attr(HealStatusEffectAttr, true, [ StatusEffect.PARALYSIS, StatusEffect.POISON, StatusEffect.TOXIC, StatusEffect.BURN, StatusEffect.SLEEP ]), /* Unused new AttackMove(Moves.G_MAX_WILDFIRE, Type.FIRE, MoveCategory.PHYSICAL, 10, -1, 10, -1, 0, 8) .target(MoveTarget.ALL_NEAR_ENEMIES) From c535e928d84688b361c54e9bb8992a079f71013d Mon Sep 17 00:00:00 2001 From: Mumble <171087428+frutescens@users.noreply.github.com> Date: Fri, 15 Nov 2024 09:45:21 -0800 Subject: [PATCH 37/37] [Beta][QoL] Improved cursor memory for target selection in Doubles (#4849) * Added more intelligent cursor memory for target selection in Doubles * Added documentation * Fixed variable name. * Apply suggestions from code review Co-authored-by: Moka <54149968+MokaStitcher@users.noreply.github.com> --------- Co-authored-by: frutescens Co-authored-by: Moka <54149968+MokaStitcher@users.noreply.github.com> --- src/ui/target-select-ui-handler.ts | 40 +++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/src/ui/target-select-ui-handler.ts b/src/ui/target-select-ui-handler.ts index ecc15e5985e3..249ae7b8b015 100644 --- a/src/ui/target-select-ui-handler.ts +++ b/src/ui/target-select-ui-handler.ts @@ -13,9 +13,11 @@ import { SubstituteTag } from "#app/data/battler-tags"; export type TargetSelectCallback = (targets: BattlerIndex[]) => void; export default class TargetSelectUiHandler extends UiHandler { - private fieldIndex: integer; + private fieldIndex: number; private move: Moves; private targetSelectCallback: TargetSelectCallback; + private cursor0: number; // associated with BattlerIndex.PLAYER + private cursor1: number; // associated with BattlerIndex.PLAYER_2 private isMultipleTargets: boolean = false; private targets: BattlerIndex[]; @@ -42,8 +44,9 @@ export default class TargetSelectUiHandler extends UiHandler { this.fieldIndex = args[0] as integer; this.move = args[1] as Moves; this.targetSelectCallback = args[2] as TargetSelectCallback; + const user = this.scene.getPlayerField()[this.fieldIndex]; - const moveTargets = getMoveTargets(this.scene.getPlayerField()[this.fieldIndex], this.move); + const moveTargets = getMoveTargets(user, this.move); this.targets = moveTargets.targets; this.isMultipleTargets = moveTargets.multiple ?? false; @@ -53,11 +56,29 @@ export default class TargetSelectUiHandler extends UiHandler { this.enemyModifiers = this.scene.getModifierBar(true); - this.setCursor(this.targets.includes(this.cursor) ? this.cursor : this.targets[0]); - + if (this.fieldIndex === BattlerIndex.PLAYER) { + this.resetCursor(this.cursor0, user); + } else if (this.fieldIndex === BattlerIndex.PLAYER_2) { + this.resetCursor(this.cursor1, user); + } return true; } + /** + * Determines what value to assign the main cursor based on the previous turn's target or the user's status + * @param cursorN the cursor associated with the user's field index + * @param user the Pokemon using the move + */ + resetCursor(cursorN: number, user: Pokemon): void { + if (!Utils.isNullOrUndefined(cursorN)) { + if ([ BattlerIndex.PLAYER, BattlerIndex.PLAYER_2 ].includes(cursorN) || user.battleSummonData.waveTurnCount === 1) { + // Reset cursor on the first turn of a fight or if an ally was targeted last turn + cursorN = -1; + } + } + this.setCursor(this.targets.includes(cursorN) ? cursorN : this.targets[0]); + } + processInput(button: Button): boolean { const ui = this.getUi(); @@ -67,6 +88,15 @@ export default class TargetSelectUiHandler extends UiHandler { const targetIndexes: BattlerIndex[] = this.isMultipleTargets ? this.targets : [ this.cursor ]; this.targetSelectCallback(button === Button.ACTION ? targetIndexes : []); success = true; + if (this.fieldIndex === BattlerIndex.PLAYER) { + if (Utils.isNullOrUndefined(this.cursor0) || this.cursor0 !== this.cursor) { + this.cursor0 = this.cursor; + } + } else if (this.fieldIndex === BattlerIndex.PLAYER_2) { + if (Utils.isNullOrUndefined(this.cursor1) || this.cursor1 !== this.cursor) { + this.cursor1 = this.cursor; + } + } } else if (this.isMultipleTargets) { success = false; } else { @@ -152,7 +182,6 @@ export default class TargetSelectUiHandler extends UiHandler { yoyo: true })); }); - return ret; } @@ -184,7 +213,6 @@ export default class TargetSelectUiHandler extends UiHandler { } clear() { - this.cursor = -1; super.clear(); this.eraseCursor(); }