From e7a4d4055f2b48d1d78c1dc2cb6f3794e0c34dd2 Mon Sep 17 00:00:00 2001 From: cadi Date: Tue, 15 Oct 2024 04:39:34 +0900 Subject: [PATCH 1/9] [Move] Implement Power Trick (#2658) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add `PowerTrickTag` * modify getStat() with PowerTrickTag * implement `PowerTrickAttr` * add unit test * enhance docs and tag apply logic --------- Co-authored-by: Lugiad' Co-authored-by: José Ricardo Fleury Oliveira Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> Co-authored-by: Enoch Co-authored-by: Yonmaru40 <47717431+40chyan@users.noreply.github.com> Co-authored-by: Amani H. <109637146+xsn34kzx@users.noreply.github.com> Co-authored-by: Niccolò <123510358+NicusPulcis@users.noreply.github.com> Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/data/battler-tags.ts | 40 ++++++++++ src/data/move.ts | 5 +- src/enums/battler-tag-type.ts | 1 + src/field/pokemon.ts | 6 +- src/test/moves/power_trick.test.ts | 113 +++++++++++++++++++++++++++++ 5 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 src/test/moves/power_trick.test.ts diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index a5016746013..8491307fc76 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -2724,6 +2724,44 @@ export class TelekinesisTag extends BattlerTag { } } +/** + * Tag that swaps the user's base ATK stat with its base DEF stat. + * @extends BattlerTag + */ +export class PowerTrickTag extends BattlerTag { + constructor(sourceMove: Moves, sourceId: number) { + super(BattlerTagType.POWER_TRICK, BattlerTagLapseType.CUSTOM, 0, sourceMove, sourceId, true); + } + + onAdd(pokemon: Pokemon): void { + this.swapStat(pokemon); + pokemon.scene.queueMessage(i18next.t("battlerTags:powerTrickActive", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })); + } + + onRemove(pokemon: Pokemon): void { + this.swapStat(pokemon); + pokemon.scene.queueMessage(i18next.t("battlerTags:powerTrickActive", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })); + } + + /** + * Removes the Power Trick tag and reverts any stat changes if the tag is already applied. + * @param {Pokemon} pokemon The {@linkcode Pokemon} that already has the Power Trick tag. + */ + onOverlap(pokemon: Pokemon): void { + pokemon.removeTag(this.tagType); + } + + /** + * Swaps the user's base ATK stat with its base DEF stat. + * @param {Pokemon} pokemon The {@linkcode Pokemon} whose stats will be swapped. + */ + swapStat(pokemon: Pokemon): void { + const temp = pokemon.getStat(Stat.ATK, false); + pokemon.setStat(Stat.ATK, pokemon.getStat(Stat.DEF, false), false); + pokemon.setStat(Stat.DEF, temp, false); + } +} + /** * Retrieves a {@linkcode BattlerTag} based on the provided tag type, turn count, source move, and source ID. * @param sourceId - The ID of the pokemon adding the tag @@ -2899,6 +2937,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source return new SyrupBombTag(sourceId); case BattlerTagType.TELEKINESIS: return new TelekinesisTag(sourceMove); + case BattlerTagType.POWER_TRICK: + return new PowerTrickTag(sourceMove, sourceId); case BattlerTagType.NONE: default: return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId); diff --git a/src/data/move.ts b/src/data/move.ts index a77e8096672..2d91363955a 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -6430,6 +6430,9 @@ export class TransformAttr extends MoveEffectAttr { 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); + // Copy all stats (except HP) for (const s of EFFECTIVE_STATS) { user.setStat(s, target.getStat(s, false), false); @@ -8153,7 +8156,7 @@ export function initMoves() { .attr(OpponentHighHpPowerAttr, 120) .makesContact(), new SelfStatusMove(Moves.POWER_TRICK, Type.PSYCHIC, -1, 10, -1, 0, 4) - .unimplemented(), + .attr(AddBattlerTagAttr, BattlerTagType.POWER_TRICK, true), new StatusMove(Moves.GASTRO_ACID, Type.POISON, 100, 10, -1, 0, 4) .attr(SuppressAbilitiesAttr), new StatusMove(Moves.LUCKY_CHANT, Type.NORMAL, -1, 30, -1, 0, 4) diff --git a/src/enums/battler-tag-type.ts b/src/enums/battler-tag-type.ts index 2efae9ad359..680dedb93cc 100644 --- a/src/enums/battler-tag-type.ts +++ b/src/enums/battler-tag-type.ts @@ -80,6 +80,7 @@ export enum BattlerTagType { DOUBLE_SHOCKED = "DOUBLE_SHOCKED", AUTOTOMIZED = "AUTOTOMIZED", MYSTERY_ENCOUNTER_POST_SUMMON = "MYSTERY_ENCOUNTER_POST_SUMMON", + POWER_TRICK = "POWER_TRICK", HEAL_BLOCK = "HEAL_BLOCK", TORMENT = "TORMENT", TAUNT = "TAUNT", diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 9ae83753e62..0204672cabd 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -19,7 +19,7 @@ import { initMoveAnim, loadMoveAnimAssets } from "#app/data/battle-anims"; import { Status, StatusEffect, getRandomStatus } from "#app/data/status-effect"; import { pokemonEvolutions, pokemonPrevolutions, SpeciesFormEvolution, SpeciesEvolutionCondition, FusionSpeciesFormEvolution } from "#app/data/balance/pokemon-evolutions"; import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "#app/data/balance/tms"; -import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, SubstituteTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, MoveRestrictionBattlerTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag, TarShotTag, AutotomizedTag } from "../data/battler-tags"; +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 "#app/data/weather"; import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "#app/data/arena-tag"; import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, 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 } from "#app/data/ability"; @@ -3048,6 +3048,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { continue; } + if (tag instanceof PowerTrickTag) { + tag.swapStat(this); + } + this.summonData.tags.push(tag); } diff --git a/src/test/moves/power_trick.test.ts b/src/test/moves/power_trick.test.ts new file mode 100644 index 00000000000..a064a43dec4 --- /dev/null +++ b/src/test/moves/power_trick.test.ts @@ -0,0 +1,113 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import Phaser from "phaser"; +import GameManager from "#app/test/utils/gameManager"; +import { Moves } from "#enums/moves"; +import { Stat } from "#enums/stat"; +import { Species } from "#enums/species"; +import { TurnEndPhase } from "#app/phases/turn-end-phase"; +import { Abilities } from "#enums/abilities"; +import { BattlerTagType } from "#enums/battler-tag-type"; + +describe("Moves - Power Trick", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .battleType("single") + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH) + .enemySpecies(Species.MEW) + .enemyLevel(200) + .moveset([ Moves.POWER_TRICK ]) + .ability(Abilities.BALL_FETCH); + }); + + it("swaps the user's ATK and DEF stats", async () => { + await game.classicMode.startBattle([ Species.SHUCKLE ]); + + const player = game.scene.getPlayerPokemon()!; + const baseATK = player.getStat(Stat.ATK, false); + const baseDEF = player.getStat(Stat.DEF, false); + + game.move.select(Moves.POWER_TRICK); + + await game.phaseInterceptor.to(TurnEndPhase); + + expect(player.getStat(Stat.ATK, false)).toBe(baseDEF); + expect(player.getStat(Stat.DEF, false)).toBe(baseATK); + expect(player.getTag(BattlerTagType.POWER_TRICK)).toBeDefined(); + }); + + it("resets initial ATK and DEF stat swap when used consecutively", async () => { + await game.classicMode.startBattle([ Species.SHUCKLE ]); + + const player = game.scene.getPlayerPokemon()!; + const baseATK = player.getStat(Stat.ATK, false); + const baseDEF = player.getStat(Stat.DEF, false); + + game.move.select(Moves.POWER_TRICK); + + await game.phaseInterceptor.to(TurnEndPhase); + + game.move.select(Moves.POWER_TRICK); + + await game.phaseInterceptor.to(TurnEndPhase); + + expect(player.getStat(Stat.ATK, false)).toBe(baseATK); + expect(player.getStat(Stat.DEF, false)).toBe(baseDEF); + expect(player.getTag(BattlerTagType.POWER_TRICK)).toBeUndefined(); + }); + + it("should pass effect when using BATON_PASS", async () => { + await game.classicMode.startBattle([ Species.SHUCKLE, Species.SHUCKLE ]); + await game.override.moveset([ Moves.POWER_TRICK, Moves.BATON_PASS ]); + + const player = game.scene.getPlayerPokemon()!; + player.addTag(BattlerTagType.POWER_TRICK); + + game.move.select(Moves.BATON_PASS); + game.doSelectPartyPokemon(1); + + await game.phaseInterceptor.to(TurnEndPhase); + + const switchedPlayer = game.scene.getPlayerPokemon()!; + const baseATK = switchedPlayer.getStat(Stat.ATK); + const baseDEF = switchedPlayer.getStat(Stat.DEF); + + expect(switchedPlayer.getStat(Stat.ATK, false)).toBe(baseDEF); + expect(switchedPlayer.getStat(Stat.DEF, false)).toBe(baseATK); + expect(switchedPlayer.getTag(BattlerTagType.POWER_TRICK)).toBeDefined(); + }); + + it("should remove effect after using Transform", async () => { + await game.classicMode.startBattle([ Species.SHUCKLE, Species.SHUCKLE ]); + await game.override.moveset([ Moves.POWER_TRICK, Moves.TRANSFORM ]); + + const player = game.scene.getPlayerPokemon()!; + player.addTag(BattlerTagType.POWER_TRICK); + + game.move.select(Moves.TRANSFORM); + + await game.phaseInterceptor.to(TurnEndPhase); + + const enemy = game.scene.getEnemyPokemon()!; + const baseATK = enemy.getStat(Stat.ATK); + const baseDEF = enemy.getStat(Stat.DEF); + + expect(player.getStat(Stat.ATK, false)).toBe(baseATK); + expect(player.getStat(Stat.DEF, false)).toBe(baseDEF); + expect(player.getTag(BattlerTagType.POWER_TRICK)).toBeUndefined(); + }); +}); From e962ac1f182d33e6f06fef1858c49b7ffb90c4b9 Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Mon, 14 Oct 2024 14:47:23 -0700 Subject: [PATCH 2/9] [Beta Bug] Prevent duplicate move failure message (#4662) --- src/phases/move-phase.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index 94093188571..d3a2a3329fd 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -128,7 +128,9 @@ export class MovePhase extends BattlePhase { this.lapsePreMoveAndMoveTags(); - this.resolveFinalPreMoveCancellationChecks(); + if (!(this.failed || this.cancelled)) { + this.resolveFinalPreMoveCancellationChecks(); + } if (this.cancelled || this.failed) { this.handlePreMoveFailures(); From bb98bc2f8e296de5b27d644193d284be40062625 Mon Sep 17 00:00:00 2001 From: Blitzy <118096277+Blitz425@users.noreply.github.com> Date: Tue, 15 Oct 2024 04:17:20 -0500 Subject: [PATCH 3/9] [Balance] Evil Team Update / Penny Adjustments (#4577) * Update trainer-config.ts * Update trainer-config.ts * Update trainer-config.ts * Fixed Flare Grunt's having Noivern > Noibat * Revert Inkay change, Change Penny * Give Admin aces canonical genders * Update trainer-config.ts * Update trainer-config.ts --------- Co-authored-by: Madmadness65 Co-authored-by: damocleas --- src/data/trainer-config.ts | 63 ++++++++++++++++++++++++++++---------- 1 file changed, 46 insertions(+), 17 deletions(-) diff --git a/src/data/trainer-config.ts b/src/data/trainer-config.ts index bc6596c74bd..bbeecba84e6 100644 --- a/src/data/trainer-config.ts +++ b/src/data/trainer-config.ts @@ -574,13 +574,13 @@ export class TrainerConfig { case "magma": { return { [TrainerPoolTier.COMMON]: [ Species.GROWLITHE, Species.SLUGMA, Species.SOLROCK, Species.HIPPOPOTAS, Species.BALTOY, Species.ROLYCOLY, Species.GLIGAR, Species.TORKOAL, Species.HOUNDOUR, Species.MAGBY ], - [TrainerPoolTier.UNCOMMON]: [ Species.TRAPINCH, Species.SILICOBRA, Species.RHYHORN, Species.ANORITH, Species.LILEEP, Species.HISUI_GROWLITHE, Species.TURTONATOR, Species.ARON, Species.BARBOACH ], + [TrainerPoolTier.UNCOMMON]: [ Species.TRAPINCH, Species.SILICOBRA, Species.RHYHORN, Species.ANORITH, Species.LILEEP, Species.HISUI_GROWLITHE, Species.TURTONATOR, Species.ARON, Species.TOEDSCOOL ], [TrainerPoolTier.RARE]: [ Species.CAPSAKID, Species.CHARCADET ] }; } case "aqua": { return { - [TrainerPoolTier.COMMON]: [ Species.CORPHISH, Species.SPHEAL, Species.CLAMPERL, Species.CHINCHOU, Species.WOOPER, Species.WINGULL, Species.TENTACOOL, Species.AZURILL, Species.LOTAD, Species.WAILMER, Species.REMORAID ], + [TrainerPoolTier.COMMON]: [ Species.CORPHISH, Species.SPHEAL, Species.CLAMPERL, Species.CHINCHOU, Species.WOOPER, Species.WINGULL, Species.TENTACOOL, Species.AZURILL, Species.LOTAD, Species.WAILMER, Species.REMORAID, Species.BARBOACH ], [TrainerPoolTier.UNCOMMON]: [ Species.MANTYKE, Species.HISUI_QWILFISH, Species.ARROKUDA, Species.DHELMISE, Species.CLOBBOPUS, Species.FEEBAS, Species.PALDEA_WOOPER, Species.HORSEA, Species.SKRELP ], [TrainerPoolTier.RARE]: [ Species.DONDOZO, Species.BASCULEGION ] }; @@ -601,9 +601,9 @@ export class TrainerConfig { } case "flare": { return { - [TrainerPoolTier.COMMON]: [ Species.FLETCHLING, Species.LITLEO, Species.INKAY, Species.HELIOPTILE, Species.ELECTRIKE, Species.SKORUPI, Species.PURRLOIN, Species.CLAWITZER, Species.PANCHAM, Species.ESPURR, Species.BUNNELBY ], + [TrainerPoolTier.COMMON]: [ Species.FLETCHLING, Species.LITLEO, Species.INKAY, Species.FOONGUS, Species.HELIOPTILE, Species.ELECTRIKE, Species.SKORUPI, Species.PURRLOIN, Species.CLAWITZER, Species.PANCHAM, Species.ESPURR, Species.BUNNELBY ], [TrainerPoolTier.UNCOMMON]: [ Species.LITWICK, Species.SNEASEL, Species.PUMPKABOO, Species.PHANTUMP, Species.HONEDGE, Species.BINACLE, Species.HOUNDOUR, Species.SKRELP, Species.SLIGGOO ], - [TrainerPoolTier.RARE]: [ Species.NOIVERN, Species.HISUI_AVALUGG, Species.HISUI_SLIGGOO ] + [TrainerPoolTier.RARE]: [ Species.NOIBAT, Species.HISUI_AVALUGG, Species.HISUI_SLIGGOO ] }; } case "aether": { @@ -1504,7 +1504,7 @@ export const trainerConfigs: TrainerConfigs = { .setSpeciesPools({ [TrainerPoolTier.COMMON]: [ Species.SLUGMA, Species.POOCHYENA, Species.NUMEL, Species.ZIGZAGOON, Species.DIGLETT, Species.MAGBY, Species.TORKOAL, Species.GROWLITHE, Species.BALTOY ], [TrainerPoolTier.UNCOMMON]: [ Species.SOLROCK, Species.HIPPOPOTAS, Species.SANDACONDA, Species.PHANPY, Species.ROLYCOLY, Species.GLIGAR, Species.RHYHORN, Species.HEATMOR ], - [TrainerPoolTier.RARE]: [ Species.TRAPINCH, Species.LILEEP, Species.ANORITH, Species.HISUI_GROWLITHE, Species.TURTONATOR, Species.ARON ], + [TrainerPoolTier.RARE]: [ Species.TRAPINCH, Species.LILEEP, Species.ANORITH, Species.HISUI_GROWLITHE, Species.TURTONATOR, Species.ARON, Species.TOEDSCOOL ], [TrainerPoolTier.SUPER_RARE]: [ Species.CAPSAKID, Species.CHARCADET ] }), [TrainerType.TABITHA]: new TrainerConfig(++t).setMoneyMultiplier(1.5).initForEvilTeamAdmin("magma_admin", "magma", [ Species.CAMERUPT ]).setEncounterBgm(TrainerType.PLASMA_GRUNT).setBattleBgm("battle_plasma_grunt").setMixedBattleBgm("battle_aqua_magma_grunt").setVictoryBgm("victory_team_plasma").setPartyTemplateFunc(scene => getEvilGruntPartyTemplate(scene)), @@ -1540,9 +1540,9 @@ export const trainerConfigs: TrainerConfigs = { [TrainerType.FLARE_GRUNT]: new TrainerConfig(++t).setHasGenders("Flare Grunt Female").setHasDouble("Flare Grunts").setMoneyMultiplier(1.0).setEncounterBgm(TrainerType.PLASMA_GRUNT).setBattleBgm("battle_plasma_grunt").setMixedBattleBgm("battle_flare_grunt").setVictoryBgm("victory_team_plasma").setPartyTemplateFunc(scene => getEvilGruntPartyTemplate(scene)) .setSpeciesPools({ [TrainerPoolTier.COMMON]: [ Species.FLETCHLING, Species.LITLEO, Species.PONYTA, Species.INKAY, Species.HOUNDOUR, Species.SKORUPI, Species.SCRAFTY, Species.CROAGUNK, Species.SCATTERBUG, Species.ESPURR ], - [TrainerPoolTier.UNCOMMON]: [ Species.HELIOPTILE, Species.ELECTRIKE, Species.SKRELP, Species.PANCHAM, Species.PURRLOIN, Species.POOCHYENA, Species.BINACLE, Species.CLAUNCHER, Species.PUMPKABOO, Species.PHANTUMP ], + [TrainerPoolTier.UNCOMMON]: [ Species.HELIOPTILE, Species.ELECTRIKE, Species.SKRELP, Species.PANCHAM, Species.PURRLOIN, Species.POOCHYENA, Species.BINACLE, Species.CLAUNCHER, Species.PUMPKABOO, Species.PHANTUMP, Species.FOONGUS ], [TrainerPoolTier.RARE]: [ Species.LITWICK, Species.SNEASEL, Species.PAWNIARD, Species.SLIGGOO ], - [TrainerPoolTier.SUPER_RARE]: [ Species.NOIVERN, Species.HISUI_SLIGGOO, Species.HISUI_AVALUGG ] + [TrainerPoolTier.SUPER_RARE]: [ Species.NOIBAT, Species.HISUI_SLIGGOO, Species.HISUI_AVALUGG ] }), [TrainerType.BRYONY]: new TrainerConfig(++t).setMoneyMultiplier(1.5).initForEvilTeamAdmin("flare_admin_female", "flare", [ Species.LIEPARD ]).setEncounterBgm(TrainerType.PLASMA_GRUNT).setBattleBgm("battle_plasma_grunt").setMixedBattleBgm("battle_flare_grunt").setVictoryBgm("victory_team_plasma").setPartyTemplateFunc(scene => getEvilGruntPartyTemplate(scene)), [TrainerType.XEROSIC]: new TrainerConfig(++t).setMoneyMultiplier(1.5).initForEvilTeamAdmin("flare_admin", "flare", [ Species.MALAMAR ]).setEncounterBgm(TrainerType.PLASMA_GRUNT).setBattleBgm("battle_plasma_grunt").setMixedBattleBgm("battle_flare_grunt").setVictoryBgm("victory_team_plasma").setPartyTemplateFunc(scene => getEvilGruntPartyTemplate(scene)), @@ -1893,7 +1893,10 @@ export const trainerConfigs: TrainerConfigs = { }), [TrainerType.ROCKET_BOSS_GIOVANNI_1]: new TrainerConfig(t = TrainerType.ROCKET_BOSS_GIOVANNI_1).setName("Giovanni").initForEvilTeamLeader("Rocket Boss", []).setMixedBattleBgm("battle_rocket_boss").setVictoryBgm("victory_team_plasma") - .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.PERSIAN, Species.ALOLA_PERSIAN ])) + .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.PERSIAN, Species.ALOLA_PERSIAN ], TrainerSlot.TRAINER, true, p => { + p.generateAndPopulateMoveset(); + p.gender = Gender.MALE; + })) .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.DUGTRIO, Species.ALOLA_DUGTRIO ])) .setPartyMemberFunc(2, getRandomPartyMemberFunc([ Species.HONCHKROW ])) .setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.NIDOKING, Species.NIDOQUEEN ])) @@ -1945,6 +1948,7 @@ export const trainerConfigs: TrainerConfigs = { p.pokeball = PokeballType.ULTRA_BALL; p.formIndex = 1; // Mega Camerupt p.generateName(); + p.gender = Gender.MALE; })), [TrainerType.MAXIE_2]: new TrainerConfig(++t).setName("Maxie").initForEvilTeamLeader("Magma Boss", [], true).setMixedBattleBgm("battle_aqua_magma_boss").setVictoryBgm("victory_team_plasma") .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.SOLROCK, Species.TYPHLOSION ], TrainerSlot.TRAINER, true, p => { @@ -1967,6 +1971,7 @@ export const trainerConfigs: TrainerConfigs = { p.pokeball = PokeballType.ULTRA_BALL; p.formIndex = 1; // Mega Camerupt p.generateName(); + p.gender = Gender.MALE; })) .setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.GROUDON ], TrainerSlot.TRAINER, true, p => { p.setBoss(true, 2); @@ -1985,6 +1990,7 @@ export const trainerConfigs: TrainerConfigs = { p.pokeball = PokeballType.ULTRA_BALL; p.formIndex = 1; // Mega Sharpedo p.generateName(); + p.gender = Gender.MALE; })), [TrainerType.ARCHIE_2]: new TrainerConfig(++t).setName("Archie").initForEvilTeamLeader("Aqua Boss", [], true).setMixedBattleBgm("battle_aqua_magma_boss").setVictoryBgm("victory_team_plasma") .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.EMPOLEON, Species.LUDICOLO ], TrainerSlot.TRAINER, true, p => { @@ -2010,6 +2016,7 @@ export const trainerConfigs: TrainerConfigs = { p.pokeball = PokeballType.ULTRA_BALL; p.formIndex = 1; // Mega Sharpedo p.generateName(); + p.gender = Gender.MALE; })) .setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.KYOGRE ], TrainerSlot.TRAINER, true, p => { p.setBoss(true, 2); @@ -2031,6 +2038,7 @@ export const trainerConfigs: TrainerConfigs = { p.setBoss(true, 2); p.generateAndPopulateMoveset(); p.pokeball = PokeballType.ULTRA_BALL; + p.gender = Gender.MALE; })), [TrainerType.CYRUS_2]: new TrainerConfig(++t).setName("Cyrus").initForEvilTeamLeader("Galactic Boss", [], true).setMixedBattleBgm("battle_galactic_boss").setVictoryBgm("victory_team_plasma") .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.AZELF, Species.UXIE, Species.MESPRIT ], TrainerSlot.TRAINER, true, p => { @@ -2049,6 +2057,7 @@ export const trainerConfigs: TrainerConfigs = { p.setBoss(true, 2); p.generateAndPopulateMoveset(); p.pokeball = PokeballType.ULTRA_BALL; + p.gender = Gender.MALE; })) .setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.DARKRAI ], TrainerSlot.TRAINER, true, p => { p.setBoss(true, 2); @@ -2065,6 +2074,7 @@ export const trainerConfigs: TrainerConfigs = { p.setBoss(true, 2); p.generateAndPopulateMoveset(); p.pokeball = PokeballType.ULTRA_BALL; + p.gender = Gender.MALE; })), [TrainerType.GHETSIS_2]: new TrainerConfig(++t).setName("Ghetsis").initForEvilTeamLeader("Plasma Boss", [], true).setMixedBattleBgm("battle_plasma_boss").setVictoryBgm("victory_team_plasma") .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.GENESECT ], TrainerSlot.TRAINER, true, p => { @@ -2084,6 +2094,11 @@ export const trainerConfigs: TrainerConfigs = { p.setBoss(true, 2); p.generateAndPopulateMoveset(); p.pokeball = PokeballType.ULTRA_BALL; + if (p.species.speciesId === Species.HYDREIGON) { + p.gender = Gender.MALE; + } else if (p.species.speciesId === Species.IRON_JUGULIS) { + p.gender = Gender.GENDERLESS; + } })) .setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.KYUREM ], TrainerSlot.TRAINER, true, p => { p.setBoss(true, 2); @@ -2105,6 +2120,7 @@ export const trainerConfigs: TrainerConfigs = { p.pokeball = PokeballType.ULTRA_BALL; p.formIndex = 1; // Mega Gyarados p.generateName(); + p.gender = Gender.MALE; })), [TrainerType.LYSANDRE_2]: new TrainerConfig(++t).setName("Lysandre").initForEvilTeamLeader("Flare Boss", [], true).setMixedBattleBgm("battle_flare_boss").setVictoryBgm("victory_team_plasma") .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.SCREAM_TAIL, Species.FLUTTER_MANE ], TrainerSlot.TRAINER, true, p => { @@ -2124,6 +2140,7 @@ export const trainerConfigs: TrainerConfigs = { p.pokeball = PokeballType.ULTRA_BALL; p.formIndex = 1; // Mega Gyardos p.generateName(); + p.gender = Gender.MALE; })) .setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.YVELTAL ], TrainerSlot.TRAINER, true, p => { p.setBoss(true, 2); @@ -2131,7 +2148,10 @@ export const trainerConfigs: TrainerConfigs = { p.pokeball = PokeballType.MASTER_BALL; })), [TrainerType.LUSAMINE]: new TrainerConfig(++t).setName("Lusamine").initForEvilTeamLeader("Aether Boss", []).setMixedBattleBgm("battle_aether_boss").setVictoryBgm("victory_team_plasma") - .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.CLEFABLE ])) + .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.CLEFABLE ], TrainerSlot.TRAINER, true, p => { + p.generateAndPopulateMoveset(); + p.gender = Gender.FEMALE; + })) .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.LILLIGANT, Species.HISUI_LILLIGANT ])) .setPartyMemberFunc(2, getRandomPartyMemberFunc([ Species.MILOTIC, Species.PRIMARINA ])) .setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.GALAR_SLOWBRO, Species.GALAR_SLOWKING ])) @@ -2148,7 +2168,10 @@ export const trainerConfigs: TrainerConfigs = { p.pokeball = PokeballType.ROGUE_BALL; })) .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.MILOTIC, Species.PRIMARINA ])) - .setPartyMemberFunc(2, getRandomPartyMemberFunc([ Species.CLEFABLE ])) + .setPartyMemberFunc(2, getRandomPartyMemberFunc([ Species.CLEFABLE ], TrainerSlot.TRAINER, true, p => { + p.generateAndPopulateMoveset(); + p.gender = Gender.FEMALE; + })) .setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.STAKATAKA, Species.CELESTEELA, Species.GUZZLORD ], TrainerSlot.TRAINER, true, p => { p.generateAndPopulateMoveset(); p.pokeball = PokeballType.ROGUE_BALL; @@ -2191,6 +2214,7 @@ export const trainerConfigs: TrainerConfigs = { .setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.GOLISOPOD ], TrainerSlot.TRAINER, true, p => { p.setBoss(true, 2); p.generateAndPopulateMoveset(); + p.gender = Gender.MALE; p.pokeball = PokeballType.ULTRA_BALL; })), [TrainerType.GUZMA_2]: new TrainerConfig(++t).setName("Guzma").initForEvilTeamLeader("Skull Boss", [], true).setMixedBattleBgm("battle_skull_boss").setVictoryBgm("victory_team_plasma") @@ -2198,6 +2222,7 @@ export const trainerConfigs: TrainerConfigs = { p.setBoss(true, 2); p.generateAndPopulateMoveset(); p.abilityIndex = 2; //Anticipation + p.gender = Gender.MALE; p.pokeball = PokeballType.ULTRA_BALL; })) .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.SCIZOR, Species.KLEAVOR ], TrainerSlot.TRAINER, true, p => { @@ -2239,6 +2264,7 @@ export const trainerConfigs: TrainerConfigs = { p.formIndex = 1; // G-Max Copperajah p.generateName(); p.pokeball = PokeballType.ULTRA_BALL; + p.gender = Gender.FEMALE; })), [TrainerType.ROSE_2]: new TrainerConfig(++t).setName("Rose").initForEvilTeamLeader("Macro Boss", [], true).setMixedBattleBgm("battle_macro_boss").setVictoryBgm("victory_team_plasma") .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.ARCHALUDON ], TrainerSlot.TRAINER, true, p => { @@ -2262,6 +2288,7 @@ export const trainerConfigs: TrainerConfigs = { p.formIndex = 1; // G-Max Copperajah p.generateName(); p.pokeball = PokeballType.ULTRA_BALL; + p.gender = Gender.FEMALE; })), [TrainerType.PENNY]: new TrainerConfig(++t).setName("Cassiopeia").initForEvilTeamLeader("Star Boss", []).setMixedBattleBgm("battle_star_boss").setVictoryBgm("victory_team_plasma") .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.VAPOREON, Species.JOLTEON, Species.FLAREON ])) @@ -2275,8 +2302,9 @@ export const trainerConfigs: TrainerConfigs = { p.formIndex = Utils.randSeedInt(5, 1); // Heat, Wash, Frost, Fan, or Mow })) .setPartyMemberFunc(4, getRandomPartyMemberFunc([ Species.SYLVEON ], TrainerSlot.TRAINER, true, p => { - p.generateAndPopulateMoveset(); p.abilityIndex = 2; // Pixilate + p.generateAndPopulateMoveset(); + p.gender = Gender.FEMALE; })) .setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.EEVEE ], TrainerSlot.TRAINER, true, p => { p.setBoss(true, 2); @@ -2290,20 +2318,21 @@ export const trainerConfigs: TrainerConfigs = { return [ modifierTypes.TERA_SHARD().generateType([], [ teraPokemon.species.type1 ])!.withIdFromFunc(modifierTypes.TERA_SHARD).newModifier(teraPokemon) as PersistentModifier ]; //TODO: is the bang correct? }), [TrainerType.PENNY_2]: new TrainerConfig(++t).setName("Cassiopeia").initForEvilTeamLeader("Star Boss", [], true).setMixedBattleBgm("battle_star_boss").setVictoryBgm("victory_team_plasma") - .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.REVAVROOM ], TrainerSlot.TRAINER, true, p => { + .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.SYLVEON ], TrainerSlot.TRAINER, true, p => { p.setBoss(true, 2); - p.formIndex = Utils.randSeedInt(5, 1); //Random Starmobile form + p.abilityIndex = 2; // Pixilate p.generateAndPopulateMoveset(); - p.pokeball = PokeballType.ULTRA_BALL; + p.gender = Gender.FEMALE; })) .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.ENTEI, Species.RAIKOU, Species.SUICUNE ], TrainerSlot.TRAINER, true, p => { p.generateAndPopulateMoveset(); p.pokeball = PokeballType.ULTRA_BALL; })) .setPartyMemberFunc(2, getRandomPartyMemberFunc([ Species.WALKING_WAKE, Species.GOUGING_FIRE, Species.RAGING_BOLT ])) - .setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.SYLVEON ], TrainerSlot.TRAINER, true, p => { + .setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.REVAVROOM ], TrainerSlot.TRAINER, true, p => { + p.formIndex = Utils.randSeedInt(5, 1); //Random Starmobile form p.generateAndPopulateMoveset(); - p.abilityIndex = 2; // Pixilate + p.pokeball = PokeballType.ROGUE_BALL; })) .setPartyMemberFunc(4, getRandomPartyMemberFunc([ Species.EEVEE ], TrainerSlot.TRAINER, true, p => { p.setBoss(true, 2); @@ -2318,7 +2347,7 @@ export const trainerConfigs: TrainerConfigs = { p.pokeball = PokeballType.MASTER_BALL; })) .setGenModifiersFunc(party => { - const teraPokemon = party[3]; + const teraPokemon = party[0]; return [ modifierTypes.TERA_SHARD().generateType([], [ teraPokemon.species.type1 ])!.withIdFromFunc(modifierTypes.TERA_SHARD).newModifier(teraPokemon) as PersistentModifier ]; //TODO: is the bang correct? }), [TrainerType.BUCK]: new TrainerConfig(++t).setName("Buck").initForStatTrainer([], true) From d5f87bbea76428677003276c033cf07beb85c151 Mon Sep 17 00:00:00 2001 From: innerthunder <168692175+innerthunder@users.noreply.github.com> Date: Tue, 15 Oct 2024 07:02:02 -0700 Subject: [PATCH 4/9] [P3][Beta] Fix missing move text when a move fails (#4664) * Fix missing move text when a move fails * Use `cancel` function instead of setting `this.cancelled` --- src/phases/move-phase.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index d3a2a3329fd..f50cfbd78ac 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -147,8 +147,9 @@ export class MovePhase extends BattlePhase { const moveQueue = this.pokemon.getMoveQueue(); if (targets.length === 0 || (moveQueue.length && moveQueue[0].move === Moves.NONE)) { + this.showMoveText(); this.showFailedText(); - this.cancelled = true; + this.cancel(); } } From 21b71595e0292a6a30503dc9d93905c8ade179bc Mon Sep 17 00:00:00 2001 From: PrabbyDD <147005742+PrabbyDD@users.noreply.github.com> Date: Tue, 15 Oct 2024 07:04:26 -0700 Subject: [PATCH 5/9] [P2] Attacks that miss against pokemon in semi invul state that have abilities such as volt absorb will not trigger (#4663) * fixing issue where abilities trigger in semi invul state * fixing targets --- src/phases/move-effect-phase.ts | 10 ++++++---- src/test/abilities/volt_absorb.test.ts | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index 581cd5ff017..e9bff882367 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -99,8 +99,9 @@ export class MoveEffectPhase extends PokemonPhase { const targetHitChecks = Object.fromEntries(targets.map(p => [ p.getBattlerIndex(), this.hitCheck(p) ])); const hasActiveTargets = targets.some(t => t.isActive(true)); - /** Check if the target is immune via ability to the attacking move */ - const isImmune = targets[0].hasAbilityWithAttr(TypeImmunityAbAttr) && (targets[0].getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move)); + /** Check if the target is immune via ability to the attacking move, and NOT in semi invulnerable state */ + const isImmune = targets[0].hasAbilityWithAttr(TypeImmunityAbAttr) && (targets[0].getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move)) + && !targets[0].getTag(SemiInvulnerableTag); /** * If no targets are left for the move to hit (FAIL), or the invoked move is single-target @@ -148,8 +149,9 @@ export class MoveEffectPhase extends PokemonPhase { && (hasConditionalProtectApplied.value || (!target.findTags(t => t instanceof DamageProtectedTag).length && target.findTags(t => t instanceof ProtectedTag).find(t => target.lapseTag(t.tagType))) || (this.move.getMove().category !== MoveCategory.STATUS && target.findTags(t => t instanceof DamageProtectedTag).find(t => target.lapseTag(t.tagType)))); - /** Is the pokemon immune due to an ablility? */ - const isImmune = target.hasAbilityWithAttr(TypeImmunityAbAttr) && (target.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move)); + /** Is the pokemon immune due to an ablility, and also not in a semi invulnerable state? */ + const isImmune = target.hasAbilityWithAttr(TypeImmunityAbAttr) && (target.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move)) + && !target.getTag(SemiInvulnerableTag); /** * If the move missed a target, stop all future hits against that target diff --git a/src/test/abilities/volt_absorb.test.ts b/src/test/abilities/volt_absorb.test.ts index 07907a34566..ec82b00ec5a 100644 --- a/src/test/abilities/volt_absorb.test.ts +++ b/src/test/abilities/volt_absorb.test.ts @@ -71,4 +71,23 @@ describe("Abilities - Volt Absorb", () => { await game.phaseInterceptor.to("BerryPhase", false); expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); }); + it("regardless of accuracy should not trigger on pokemon in semi invulnerable state", async () => { + game.override.moveset(Moves.THUNDERBOLT); + game.override.enemyMoveset(Moves.DIVE); + game.override.enemySpecies(Species.MAGIKARP); + game.override.enemyAbility(Abilities.VOLT_ABSORB); + + await game.classicMode.startBattle(); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.THUNDERBOLT); + enemyPokemon.hp = enemyPokemon.hp - 1; + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("MoveEffectPhase"); + + await game.move.forceMiss(); + await game.phaseInterceptor.to("BerryPhase", false); + expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp()); + }); }); From d01d85689833dea8acb023478e42b6f6d0b2cef2 Mon Sep 17 00:00:00 2001 From: Mumble <171087428+frutescens@users.noreply.github.com> Date: Tue, 15 Oct 2024 07:05:21 -0700 Subject: [PATCH 6/9] [Refactor] Default case to display challenge name (#4656) Co-authored-by: frutescens --- src/ui/run-info-ui-handler.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ui/run-info-ui-handler.ts b/src/ui/run-info-ui-handler.ts index 39927f8e071..1976d5a997b 100644 --- a/src/ui/run-info-ui-handler.ts +++ b/src/ui/run-info-ui-handler.ts @@ -587,13 +587,13 @@ export default class RunInfoUiHandler extends UiHandler { const typeText = typeTextColor + typeShadowColor + i18next.t(`pokemonInfo:Type.${typeRule}`)! + "[/color]" + "[/shadow]"; rules.push(typeText); break; - case Challenges.FRESH_START: - rules.push(i18next.t("challenges:freshStart.name")); - break; case Challenges.INVERSE_BATTLE: - // rules.push(i18next.t("challenges:inverseBattle.shortName")); break; + default: + const localisationKey = Challenges[this.runInfo.challenges[i].id].split("_").map((f, i) => i ? `${f[0]}${f.slice(1).toLowerCase()}` : f.toLowerCase()).join(""); + rules.push(i18next.t(`challenges:${localisationKey}.name`)); + break; } } } From 196633562775b04920956d913c5059ddb9f39a31 Mon Sep 17 00:00:00 2001 From: innerthunder <168692175+innerthunder@users.noreply.github.com> Date: Tue, 15 Oct 2024 10:13:54 -0700 Subject: [PATCH 7/9] [Refactor] Add type inference and support for simulated calls to `ArenaTag.apply` (#4659) * Add simulated support for Arena Tag application * Add type inference to ArenaTag.apply * Fix screen tests * back to `any` again lol * fix missing spread syntax (maybe) * updated docs * named imports for `Utils` --- src/data/arena-tag.ts | 189 ++++++++++++++++---------- src/data/move.ts | 4 +- src/field/arena.ts | 10 +- src/field/pokemon.ts | 4 +- src/phases/move-effect-phase.ts | 2 +- src/phases/post-summon-phase.ts | 2 +- src/phases/stat-stage-change-phase.ts | 3 +- src/phases/turn-start-phase.ts | 2 +- src/test/moves/aurora_veil.test.ts | 2 +- src/test/moves/light_screen.test.ts | 2 +- src/test/moves/reflect.test.ts | 2 +- 11 files changed, 135 insertions(+), 87 deletions(-) diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index 71cf11fa06f..2bd6ae09877 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -1,7 +1,7 @@ import { Arena } from "#app/field/arena"; import BattleScene from "#app/battle-scene"; import { Type } from "#app/data/type"; -import * as Utils from "#app/utils"; +import { BooleanHolder, NumberHolder, toDmgValue } from "#app/utils"; import { MoveCategory, allMoves, MoveTarget, IncrementMovePriorityAttr, applyMoveAttrs } from "#app/data/move"; import { getPokemonNameWithAffix } from "#app/messages"; import Pokemon, { HitResult, PokemonMove } from "#app/field/pokemon"; @@ -36,7 +36,7 @@ export abstract class ArenaTag { public side: ArenaTagSide = ArenaTagSide.BOTH ) {} - apply(arena: Arena, args: any[]): boolean { + apply(arena: Arena, simulated: boolean, ...args: unknown[]): boolean { return true; } @@ -122,10 +122,20 @@ export class MistTag extends ArenaTag { } } - apply(arena: Arena, args: any[]): boolean { - (args[0] as Utils.BooleanHolder).value = true; + /** + * Cancels the lowering of stats + * @param arena the {@linkcode Arena} containing this effect + * @param simulated `true` if the effect should be applied quietly + * @param cancelled a {@linkcode BooleanHolder} whose value is set to `true` + * to flag the stat reduction as cancelled + * @returns `true` if a stat reduction was cancelled; `false` otherwise + */ + override apply(arena: Arena, simulated: boolean, cancelled: BooleanHolder): boolean { + cancelled.value = true; - arena.scene.queueMessage(i18next.t("arenaTag:mistApply")); + if (!simulated) { + arena.scene.queueMessage(i18next.t("arenaTag:mistApply")); + } return true; } @@ -157,17 +167,15 @@ export class WeakenMoveScreenTag extends ArenaTag { /** * Applies the weakening effect to the move. * - * @param arena - The arena where the move is applied. - * @param args - The arguments for the move application. - * @param args[0] - The category of the move. - * @param args[1] - A boolean indicating whether it is a double battle. - * @param args[2] - An object of type `Utils.NumberHolder` that holds the damage multiplier - * - * @returns True if the move was weakened, otherwise false. + * @param arena the {@linkcode Arena} where the move is applied. + * @param simulated n/a + * @param moveCategory the attacking move's {@linkcode MoveCategory}. + * @param damageMultiplier A {@linkcode NumberHolder} containing the damage multiplier + * @returns `true` if the attacking move was weakened; `false` otherwise. */ - apply(arena: Arena, args: any[]): boolean { - if (this.weakenedCategories.includes((args[0] as MoveCategory))) { - (args[2] as Utils.NumberHolder).value = (args[1] as boolean) ? 2732 / 4096 : 0.5; + override apply(arena: Arena, simulated: boolean, moveCategory: MoveCategory, damageMultiplier: NumberHolder): boolean { + if (this.weakenedCategories.includes(moveCategory)) { + damageMultiplier.value = arena.scene.currentBattle.double ? 2732 / 4096 : 0.5; return true; } return false; @@ -249,38 +257,34 @@ export class ConditionalProtectTag extends ArenaTag { onRemove(arena: Arena): void { } /** - * apply(): Checks incoming moves against the condition function + * Checks incoming moves against the condition function * and protects the target if conditions are met - * @param arena The arena containing this tag - * @param args\[0\] (Utils.BooleanHolder) Signals if the move is cancelled - * @param args\[1\] (Pokemon) The Pokemon using the move - * @param args\[2\] (Pokemon) The intended target of the move - * @param args\[3\] (Moves) The parameters to the condition function - * @param args\[4\] (Utils.BooleanHolder) Signals if the applied protection supercedes protection-ignoring effects - * @returns + * @param arena the {@linkcode Arena} containing this tag + * @param simulated `true` if the tag is applied quietly; `false` otherwise. + * @param isProtected a {@linkcode BooleanHolder} used to flag if the move is protected against + * @param attacker the attacking {@linkcode Pokemon} + * @param defender the defending {@linkcode Pokemon} + * @param moveId the {@linkcode Moves | identifier} for the move being used + * @param ignoresProtectBypass a {@linkcode BooleanHolder} used to flag if a protection effect supercedes effects that ignore protection + * @returns `true` if this tag protected against the attack; `false` otherwise */ - apply(arena: Arena, args: any[]): boolean { - const [ cancelled, user, target, moveId, ignoresBypass ] = args; + override apply(arena: Arena, simulated: boolean, isProtected: BooleanHolder, attacker: Pokemon, defender: Pokemon, + moveId: Moves, ignoresProtectBypass: BooleanHolder): boolean { - if (cancelled instanceof Utils.BooleanHolder - && user instanceof Pokemon - && target instanceof Pokemon - && typeof moveId === "number" - && ignoresBypass instanceof Utils.BooleanHolder) { + if ((this.side === ArenaTagSide.PLAYER) === defender.isPlayer() + && this.protectConditionFunc(arena, moveId)) { + if (!isProtected.value) { + isProtected.value = true; + if (!simulated) { + attacker.stopMultiHit(defender); - if ((this.side === ArenaTagSide.PLAYER) === target.isPlayer() - && this.protectConditionFunc(arena, moveId)) { - if (!cancelled.value) { - cancelled.value = true; - user.stopMultiHit(target); - - new CommonBattleAnim(CommonAnim.PROTECT, target).play(arena.scene); - arena.scene.queueMessage(i18next.t("arenaTag:conditionalProtectApply", { moveName: super.getMoveName(), pokemonNameWithAffix: getPokemonNameWithAffix(target) })); + new CommonBattleAnim(CommonAnim.PROTECT, defender).play(arena.scene); + arena.scene.queueMessage(i18next.t("arenaTag:conditionalProtectApply", { moveName: super.getMoveName(), pokemonNameWithAffix: getPokemonNameWithAffix(defender) })); } - - ignoresBypass.value = ignoresBypass.value || this.ignoresBypass; - return true; } + + ignoresProtectBypass.value = ignoresProtectBypass.value || this.ignoresBypass; + return true; } return false; } @@ -296,7 +300,7 @@ export class ConditionalProtectTag extends ArenaTag { */ const QuickGuardConditionFunc: ProtectConditionFunc = (arena, moveId) => { const move = allMoves[moveId]; - const priority = new Utils.NumberHolder(move.priority); + const priority = new NumberHolder(move.priority); const effectPhase = arena.scene.getCurrentPhase(); if (effectPhase instanceof MoveEffectPhase) { @@ -460,7 +464,7 @@ class WishTag extends ArenaTag { if (user) { this.battlerIndex = user.getBattlerIndex(); this.triggerMessage = i18next.t("arenaTag:wishTagOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(user) }); - this.healHp = Utils.toDmgValue(user.getMaxHp() / 2); + this.healHp = toDmgValue(user.getMaxHp() / 2); } else { console.warn("Failed to get source for WishTag onAdd"); } @@ -497,12 +501,19 @@ export class WeakenMoveTypeTag extends ArenaTag { this.weakenedType = type; } - apply(arena: Arena, args: any[]): boolean { - if ((args[0] as Type) === this.weakenedType) { - (args[1] as Utils.NumberHolder).value *= 0.33; + /** + * Reduces an attack's power by 0.33x if it matches this tag's weakened type. + * @param arena n/a + * @param simulated n/a + * @param type the attack's {@linkcode Type} + * @param power a {@linkcode NumberHolder} containing the attack's power + * @returns `true` if the attack's power was reduced; `false` otherwise. + */ + override apply(arena: Arena, simulated: boolean, type: Type, power: NumberHolder): boolean { + if (type === this.weakenedType) { + power.value *= 0.33; return true; } - return false; } } @@ -563,13 +574,12 @@ export class IonDelugeTag extends ArenaTag { /** * Converts Normal-type moves to Electric type * @param arena n/a - * @param args - * - `[0]` {@linkcode Utils.NumberHolder} A container with a move's {@linkcode Type} + * @param simulated n/a + * @param moveType a {@linkcode NumberHolder} containing a move's {@linkcode Type} * @returns `true` if the given move type changed; `false` otherwise. */ - apply(arena: Arena, args: any[]): boolean { - const moveType = args[0]; - if (moveType instanceof Utils.NumberHolder && moveType.value === Type.NORMAL) { + override apply(arena: Arena, simulated: boolean, moveType: NumberHolder): boolean { + if (moveType.value === Type.NORMAL) { moveType.value = Type.ELECTRIC; return true; } @@ -608,16 +618,22 @@ export class ArenaTrapTag extends ArenaTag { } } - apply(arena: Arena, args: any[]): boolean { - const pokemon = args[0] as Pokemon; + /** + * Activates the hazard effect onto a Pokemon when it enters the field + * @param arena the {@linkcode Arena} containing this tag + * @param simulated if `true`, only checks if the hazard would activate. + * @param pokemon the {@linkcode Pokemon} triggering this hazard + * @returns `true` if this hazard affects the given Pokemon; `false` otherwise. + */ + override apply(arena: Arena, simulated: boolean, pokemon: Pokemon): boolean { if (this.sourceId === pokemon.id || (this.side === ArenaTagSide.PLAYER) !== pokemon.isPlayer()) { return false; } - return this.activateTrap(pokemon); + return this.activateTrap(pokemon, simulated); } - activateTrap(pokemon: Pokemon): boolean { + activateTrap(pokemon: Pokemon, simulated: boolean): boolean { return false; } @@ -651,14 +667,18 @@ class SpikesTag extends ArenaTrapTag { } } - activateTrap(pokemon: Pokemon): boolean { + override activateTrap(pokemon: Pokemon, simulated: boolean): boolean { if (pokemon.isGrounded()) { - const cancelled = new Utils.BooleanHolder(false); + const cancelled = new BooleanHolder(false); applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled); + if (simulated) { + return !cancelled.value; + } + if (!cancelled.value) { const damageHpRatio = 1 / (10 - 2 * this.layers); - const damage = Utils.toDmgValue(pokemon.getMaxHp() * damageHpRatio); + const damage = toDmgValue(pokemon.getMaxHp() * damageHpRatio); pokemon.scene.queueMessage(i18next.t("arenaTag:spikesActivateTrap", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })); pokemon.damageAndUpdate(damage, HitResult.OTHER); @@ -702,8 +722,11 @@ class ToxicSpikesTag extends ArenaTrapTag { } } - activateTrap(pokemon: Pokemon): boolean { + override activateTrap(pokemon: Pokemon, simulated: boolean): boolean { if (pokemon.isGrounded()) { + if (simulated) { + return true; + } if (pokemon.isOfType(Type.POISON)) { this.neutralized = true; if (pokemon.scene.arena.removeTag(this.tagType)) { @@ -807,8 +830,8 @@ class StealthRockTag extends ArenaTrapTag { return damageHpRatio; } - activateTrap(pokemon: Pokemon): boolean { - const cancelled = new Utils.BooleanHolder(false); + override activateTrap(pokemon: Pokemon, simulated: boolean): boolean { + const cancelled = new BooleanHolder(false); applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled); if (cancelled.value) { @@ -818,12 +841,16 @@ class StealthRockTag extends ArenaTrapTag { const damageHpRatio = this.getDamageHpRatio(pokemon); if (damageHpRatio) { - const damage = Utils.toDmgValue(pokemon.getMaxHp() * damageHpRatio); + if (simulated) { + return true; + } + const damage = toDmgValue(pokemon.getMaxHp() * damageHpRatio); pokemon.scene.queueMessage(i18next.t("arenaTag:stealthRockActivateTrap", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })); pokemon.damageAndUpdate(damage, HitResult.OTHER); if (pokemon.turnData) { pokemon.turnData.damageTaken += damage; } + return true; } return false; @@ -853,14 +880,20 @@ class StickyWebTag extends ArenaTrapTag { } } - activateTrap(pokemon: Pokemon): boolean { + override activateTrap(pokemon: Pokemon, simulated: boolean): boolean { if (pokemon.isGrounded()) { - const cancelled = new Utils.BooleanHolder(false); + const cancelled = new BooleanHolder(false); applyAbAttrs(ProtectStatAbAttr, pokemon, cancelled); + + if (simulated) { + return !cancelled.value; + } + if (!cancelled.value) { pokemon.scene.queueMessage(i18next.t("arenaTag:stickyWebActivateTrap", { pokemonName: pokemon.getNameToRender() })); - const stages = new Utils.NumberHolder(-1); + const stages = new NumberHolder(-1); pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), false, [ Stat.SPD ], stages.value)); + return true; } } @@ -879,8 +912,15 @@ export class TrickRoomTag extends ArenaTag { super(ArenaTagType.TRICK_ROOM, turnCount, Moves.TRICK_ROOM, sourceId); } - apply(arena: Arena, args: any[]): boolean { - const speedReversed = args[0] as Utils.BooleanHolder; + /** + * Reverses Speed-based turn order for all Pokemon on the field + * @param arena n/a + * @param simulated n/a + * @param speedReversed a {@linkcode BooleanHolder} used to flag if Speed-based + * turn order should be reversed. + * @returns `true` if turn order is successfully reversed; `false` otherwise + */ + override apply(arena: Arena, simulated: boolean, speedReversed: BooleanHolder): boolean { speedReversed.value = !speedReversed.value; return true; } @@ -1087,7 +1127,7 @@ class FireGrassPledgeTag extends ArenaTag { pokemon.scene.queueMessage(i18next.t("arenaTag:fireGrassPledgeLapse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })); // TODO: Replace this with a proper animation pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, pokemon.getBattlerIndex(), pokemon.getBattlerIndex(), CommonAnim.MAGMA_STORM)); - pokemon.damageAndUpdate(Utils.toDmgValue(pokemon.getMaxHp() / 8)); + pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 8)); }); return super.lapse(arena); @@ -1111,8 +1151,15 @@ class WaterFirePledgeTag extends ArenaTag { arena.scene.queueMessage(i18next.t(`arenaTag:waterFirePledgeOnAdd${this.side === ArenaTagSide.PLAYER ? "Player" : this.side === ArenaTagSide.ENEMY ? "Enemy" : ""}`)); } - override apply(arena: Arena, args: any[]): boolean { - const moveChance = args[0] as Utils.NumberHolder; + /** + * Doubles the chance for the given move's secondary effect(s) to trigger + * @param arena the {@linkcode Arena} containing this tag + * @param simulated n/a + * @param moveChance a {@linkcode NumberHolder} containing + * the move's current effect chance + * @returns `true` if the move's effect chance was doubled (currently always `true`) + */ + override apply(arena: Arena, simulated: boolean, moveChance: NumberHolder): boolean { moveChance.value *= 2; return true; } diff --git a/src/data/move.ts b/src/data/move.ts index 2d91363955a..b0078c32f12 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -808,7 +808,7 @@ export default class Move implements Localizable { source.scene.applyModifiers(PokemonMultiHitModifier, source.isPlayer(), source, new Utils.IntegerHolder(0), power); if (!this.hasAttr(TypelessAttr)) { - source.scene.arena.applyTags(WeakenMoveTypeTag, this.type, power); + source.scene.arena.applyTags(WeakenMoveTypeTag, simulated, this.type, power); source.scene.applyModifiers(AttackTypeBoosterModifier, source.isPlayer(), source, this.type, power); } @@ -1026,7 +1026,7 @@ export class MoveEffectAttr extends MoveAttr { if (!move.hasAttr(FlinchAttr) || moveChance.value <= move.chance) { const userSide = user.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; - user.scene.arena.applyTagsForSide(ArenaTagType.WATER_FIRE_PLEDGE, userSide, moveChance); + user.scene.arena.applyTagsForSide(ArenaTagType.WATER_FIRE_PLEDGE, userSide, false, moveChance); } if (!selfEffect) { diff --git a/src/field/arena.ts b/src/field/arena.ts index 1e164903e9d..ad1846884fc 100644 --- a/src/field/arena.ts +++ b/src/field/arena.ts @@ -579,26 +579,28 @@ export class Arena { * Applies each `ArenaTag` in this Arena, based on which side (self, enemy, or both) is passed in as a parameter * @param tagType Either an {@linkcode ArenaTagType} string, or an actual {@linkcode ArenaTag} class to filter which ones to apply * @param side {@linkcode ArenaTagSide} which side's arena tags to apply + * @param simulated if `true`, this applies arena tags without changing game state * @param args array of parameters that the called upon tags may need */ - applyTagsForSide(tagType: ArenaTagType | Constructor, side: ArenaTagSide, ...args: unknown[]): void { + applyTagsForSide(tagType: ArenaTagType | Constructor, side: ArenaTagSide, simulated: boolean, ...args: unknown[]): void { let tags = typeof tagType === "string" ? this.tags.filter(t => t.tagType === tagType) : this.tags.filter(t => t instanceof tagType); if (side !== ArenaTagSide.BOTH) { tags = tags.filter(t => t.side === side); } - tags.forEach(t => t.apply(this, args)); + tags.forEach(t => t.apply(this, simulated, ...args)); } /** * Applies the specified tag to both sides (ie: both user and trainer's tag that match the Tag specified) * by calling {@linkcode applyTagsForSide()} * @param tagType Either an {@linkcode ArenaTagType} string, or an actual {@linkcode ArenaTag} class to filter which ones to apply + * @param simulated if `true`, this applies arena tags without changing game state * @param args array of parameters that the called upon tags may need */ - applyTags(tagType: ArenaTagType | Constructor, ...args: unknown[]): void { - this.applyTagsForSide(tagType, ArenaTagSide.BOTH, ...args); + applyTags(tagType: ArenaTagType | Constructor, simulated: boolean, ...args: unknown[]): void { + this.applyTagsForSide(tagType, ArenaTagSide.BOTH, simulated, ...args); } /** diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 0204672cabd..8eca37f38ac 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -1538,7 +1538,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { applyMoveAttrs(VariableMoveTypeAttr, this, null, move, moveTypeHolder); applyPreAttackAbAttrs(MoveTypeChangeAbAttr, this, null, move, simulated, moveTypeHolder); - this.scene.arena.applyTags(ArenaTagType.ION_DELUGE, moveTypeHolder); + this.scene.arena.applyTags(ArenaTagType.ION_DELUGE, simulated, moveTypeHolder); if (this.getTag(BattlerTagType.ELECTRIFIED)) { moveTypeHolder.value = Type.ELECTRIC; } @@ -2605,7 +2605,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { /** Reduces damage if this Pokemon has a relevant screen (e.g. Light Screen for special attacks) */ const screenMultiplier = new Utils.NumberHolder(1); - this.scene.arena.applyTagsForSide(WeakenMoveScreenTag, defendingSide, move.category, this.scene.currentBattle.double, screenMultiplier); + this.scene.arena.applyTagsForSide(WeakenMoveScreenTag, defendingSide, simulated, moveCategory, screenMultiplier); /** * For each {@linkcode HitsTagAttr} the move has, doubles the damage of the move if: diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index e9bff882367..dc880f85e23 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -141,7 +141,7 @@ export class MoveEffectPhase extends PokemonPhase { const bypassIgnoreProtect = new Utils.BooleanHolder(false); /** If the move is not targeting a Pokemon on the user's side, try to apply conditional protection effects */ if (!this.move.getMove().isAllyTarget()) { - this.scene.arena.applyTagsForSide(ConditionalProtectTag, targetSide, hasConditionalProtectApplied, user, target, move.id, bypassIgnoreProtect); + this.scene.arena.applyTagsForSide(ConditionalProtectTag, targetSide, false, hasConditionalProtectApplied, user, target, move.id, bypassIgnoreProtect); } /** Is the target protected by Protect, etc. or a relevant conditional protection effect? */ diff --git a/src/phases/post-summon-phase.ts b/src/phases/post-summon-phase.ts index b99c0b90fd8..617bb8b1cfe 100644 --- a/src/phases/post-summon-phase.ts +++ b/src/phases/post-summon-phase.ts @@ -20,7 +20,7 @@ export class PostSummonPhase extends PokemonPhase { if (pokemon.status?.effect === StatusEffect.TOXIC) { pokemon.status.turnCount = 0; } - this.scene.arena.applyTags(ArenaTrapTag, pokemon); + this.scene.arena.applyTags(ArenaTrapTag, false, pokemon); // If this is mystery encounter and has post summon phase tag, apply post summon effects if (this.scene.currentBattle.isBattleMysteryEncounter() && pokemon.findTags(t => t instanceof MysteryEncounterPostSummonTag).length > 0) { diff --git a/src/phases/stat-stage-change-phase.ts b/src/phases/stat-stage-change-phase.ts index bfe19ea9ca5..4c13b883445 100644 --- a/src/phases/stat-stage-change-phase.ts +++ b/src/phases/stat-stage-change-phase.ts @@ -64,8 +64,7 @@ export class StatStageChangePhase extends PokemonPhase { const cancelled = new BooleanHolder(false); if (!this.selfTarget && stages.value < 0) { - // TODO: Include simulate boolean when tag applications can be simulated - this.scene.arena.applyTagsForSide(MistTag, pokemon.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY, cancelled); + this.scene.arena.applyTagsForSide(MistTag, pokemon.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY, false, cancelled); } if (!cancelled.value && !this.selfTarget && stages.value < 0) { diff --git a/src/phases/turn-start-phase.ts b/src/phases/turn-start-phase.ts index 627cee4b06a..25c079007fd 100644 --- a/src/phases/turn-start-phase.ts +++ b/src/phases/turn-start-phase.ts @@ -45,7 +45,7 @@ export class TurnStartPhase extends FieldPhase { // Next, a check for Trick Room is applied to determine sort order. const speedReversed = new Utils.BooleanHolder(false); - this.scene.arena.applyTags(TrickRoomTag, speedReversed); + this.scene.arena.applyTags(TrickRoomTag, false, speedReversed); // Adjust the sort function based on whether Trick Room is active. orderedTargets.sort((a: Pokemon, b: Pokemon) => { diff --git a/src/test/moves/aurora_veil.test.ts b/src/test/moves/aurora_veil.test.ts index e71d4ab9d11..243ba3a3269 100644 --- a/src/test/moves/aurora_veil.test.ts +++ b/src/test/moves/aurora_veil.test.ts @@ -111,7 +111,7 @@ const getMockedMoveDamage = (defender: Pokemon, attacker: Pokemon, move: Move) = const side = defender.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; if (defender.scene.arena.getTagOnSide(ArenaTagType.AURORA_VEIL, side)) { - defender.scene.arena.applyTagsForSide(ArenaTagType.AURORA_VEIL, side, move.category, defender.scene.currentBattle.double, multiplierHolder); + defender.scene.arena.applyTagsForSide(ArenaTagType.AURORA_VEIL, side, false, move.category, multiplierHolder); } return move.power * multiplierHolder.value; diff --git a/src/test/moves/light_screen.test.ts b/src/test/moves/light_screen.test.ts index 2308458003d..11b8144bb4e 100644 --- a/src/test/moves/light_screen.test.ts +++ b/src/test/moves/light_screen.test.ts @@ -94,7 +94,7 @@ const getMockedMoveDamage = (defender: Pokemon, attacker: Pokemon, move: Move) = const side = defender.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; if (defender.scene.arena.getTagOnSide(ArenaTagType.LIGHT_SCREEN, side)) { - defender.scene.arena.applyTagsForSide(ArenaTagType.LIGHT_SCREEN, side, move.category, defender.scene.currentBattle.double, multiplierHolder); + defender.scene.arena.applyTagsForSide(ArenaTagType.LIGHT_SCREEN, side, false, move.category, multiplierHolder); } return move.power * multiplierHolder.value; diff --git a/src/test/moves/reflect.test.ts b/src/test/moves/reflect.test.ts index 41a10988552..b18b2423895 100644 --- a/src/test/moves/reflect.test.ts +++ b/src/test/moves/reflect.test.ts @@ -94,7 +94,7 @@ const getMockedMoveDamage = (defender: Pokemon, attacker: Pokemon, move: Move) = const side = defender.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; if (defender.scene.arena.getTagOnSide(ArenaTagType.REFLECT, side)) { - defender.scene.arena.applyTagsForSide(ArenaTagType.REFLECT, side, move.category, defender.scene.currentBattle.double, multiplierHolder); + defender.scene.arena.applyTagsForSide(ArenaTagType.REFLECT, side, false, move.category, multiplierHolder); } return move.power * multiplierHolder.value; From c04d81bd65364726e252754866e0859e93f25b56 Mon Sep 17 00:00:00 2001 From: Madmadness65 <59298170+Madmadness65@users.noreply.github.com> Date: Tue, 15 Oct 2024 14:02:30 -0500 Subject: [PATCH 8/9] [Ability] Allow Power Construct to transform 10% PC Zygardes (#4626) * Allow Power Construct to transform 10% PC Zygarde * Add additional test for 10% PC Zygarde --- public/audio/cry/718-10-complete.m4a | Bin 0 -> 20709 bytes src/battle-scene.ts | 2 +- src/data/ability.ts | 14 +++++---- src/data/pokemon-forms.ts | 4 +-- src/data/pokemon-species.ts | 4 ++- src/test/abilities/power_construct.test.ts | 34 +++++++++++++++++++-- 6 files changed, 46 insertions(+), 12 deletions(-) create mode 100644 public/audio/cry/718-10-complete.m4a diff --git a/public/audio/cry/718-10-complete.m4a b/public/audio/cry/718-10-complete.m4a new file mode 100644 index 0000000000000000000000000000000000000000..94d953605539c1ef2eef1252a75a6ec5d0b63f6e GIT binary patch literal 20709 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 2) - .attr(PostSummonFormChangeAbAttr, p => p.getHpRatio() <= 0.5 || p.getFormKey() === "complete" ? 4 : 2) - .attr(PostTurnFormChangeAbAttr, p => p.getHpRatio() <= 0.5 || p.getFormKey() === "complete" ? 4 : 2) + new Ability(Abilities.POWER_CONSTRUCT, 7) + .conditionalAttr(pokemon => pokemon.formIndex === 2 || pokemon.formIndex === 4, PostBattleInitFormChangeAbAttr, () => 2) + .conditionalAttr(pokemon => pokemon.formIndex === 3 || pokemon.formIndex === 5, PostBattleInitFormChangeAbAttr, () => 3) + .conditionalAttr(pokemon => pokemon.formIndex === 2 || pokemon.formIndex === 4, PostSummonFormChangeAbAttr, p => p.getHpRatio() <= 0.5 || p.getFormKey() === "complete" ? 4 : 2) + .conditionalAttr(pokemon => pokemon.formIndex === 2 || pokemon.formIndex === 4, PostTurnFormChangeAbAttr, p => p.getHpRatio() <= 0.5 || p.getFormKey() === "complete" ? 4 : 2) + .conditionalAttr(pokemon => pokemon.formIndex === 3 || pokemon.formIndex === 5, PostSummonFormChangeAbAttr, p => p.getHpRatio() <= 0.5 || p.getFormKey() === "10-complete" ? 5 : 3) + .conditionalAttr(pokemon => pokemon.formIndex === 3 || pokemon.formIndex === 5, PostTurnFormChangeAbAttr, p => p.getHpRatio() <= 0.5 || p.getFormKey() === "10-complete" ? 5 : 3) .attr(UncopiableAbilityAbAttr) .attr(UnswappableAbilityAbAttr) .attr(UnsuppressableAbilityAbAttr) .attr(NoFusionAbilityAbAttr) - .bypassFaint() - .partial(), + .bypassFaint(), new Ability(Abilities.CORROSION, 7) .attr(IgnoreTypeStatusEffectImmunityAbAttr, [ StatusEffect.POISON, StatusEffect.TOXIC ], [ Type.STEEL, Type.POISON ]) .edgeCase(), // Should interact correctly with magic coat/bounce (not yet implemented), fling with toxic orb (not implemented yet), and synchronize (not fully implemented yet) diff --git a/src/data/pokemon-forms.ts b/src/data/pokemon-forms.ts index 03b6b89e5b1..7cc20d50fb9 100644 --- a/src/data/pokemon-forms.ts +++ b/src/data/pokemon-forms.ts @@ -799,8 +799,8 @@ export const pokemonFormChanges: PokemonFormChanges = { [Species.ZYGARDE]: [ new SpeciesFormChange(Species.ZYGARDE, "50-pc", "complete", new SpeciesFormChangeManualTrigger(), true), new SpeciesFormChange(Species.ZYGARDE, "complete", "50-pc", new SpeciesFormChangeManualTrigger(), true), - new SpeciesFormChange(Species.ZYGARDE, "10-pc", "complete", new SpeciesFormChangeManualTrigger(), true), - new SpeciesFormChange(Species.ZYGARDE, "complete", "10-pc", new SpeciesFormChangeManualTrigger(), true) + new SpeciesFormChange(Species.ZYGARDE, "10-pc", "10-complete", new SpeciesFormChangeManualTrigger(), true), + new SpeciesFormChange(Species.ZYGARDE, "10-complete", "10-pc", new SpeciesFormChangeManualTrigger(), true) ], [Species.DIANCIE]: [ new SpeciesFormChange(Species.DIANCIE, "", SpeciesFormKey.MEGA, new SpeciesFormChangeItemTrigger(FormChangeItem.DIANCITE)) diff --git a/src/data/pokemon-species.ts b/src/data/pokemon-species.ts index 8bb23cfc208..eb1b761e306 100644 --- a/src/data/pokemon-species.ts +++ b/src/data/pokemon-species.ts @@ -425,6 +425,7 @@ export abstract class PokemonSpeciesForm { case "hero": case "roaming": case "complete": + case "10-complete": case "10": case "10-pc": case "super": @@ -2135,7 +2136,8 @@ export function initSpecies() { new PokemonForm("10% Forme", "10", Type.DRAGON, Type.GROUND, 1.2, 33.5, Abilities.AURA_BREAK, Abilities.NONE, Abilities.NONE, 486, 54, 100, 71, 61, 85, 115, 3, 0, 300, false, null, true), new PokemonForm("50% Forme Power Construct", "50-pc", Type.DRAGON, Type.GROUND, 5, 305, Abilities.POWER_CONSTRUCT, Abilities.NONE, Abilities.NONE, 600, 108, 100, 121, 81, 95, 95, 3, 0, 300, false, "", true), new PokemonForm("10% Forme Power Construct", "10-pc", Type.DRAGON, Type.GROUND, 1.2, 33.5, Abilities.POWER_CONSTRUCT, Abilities.NONE, Abilities.NONE, 486, 54, 100, 71, 61, 85, 115, 3, 0, 300, false, "10", true), - new PokemonForm("Complete Forme", "complete", Type.DRAGON, Type.GROUND, 4.5, 610, Abilities.POWER_CONSTRUCT, Abilities.NONE, Abilities.NONE, 708, 216, 100, 121, 91, 95, 85, 3, 0, 300), + new PokemonForm("Complete Forme (50% PC)", "complete", Type.DRAGON, Type.GROUND, 4.5, 610, Abilities.POWER_CONSTRUCT, Abilities.NONE, Abilities.NONE, 708, 216, 100, 121, 91, 95, 85, 3, 0, 300), + new PokemonForm("Complete Forme (10% PC)", "10-complete", Type.DRAGON, Type.GROUND, 4.5, 610, Abilities.POWER_CONSTRUCT, Abilities.NONE, Abilities.NONE, 708, 216, 100, 121, 91, 95, 85, 3, 0, 300, false, "complete"), ), new PokemonSpecies(Species.DIANCIE, 6, false, false, true, "Jewel Pokémon", Type.ROCK, Type.FAIRY, 0.7, 8.8, Abilities.CLEAR_BODY, Abilities.NONE, Abilities.NONE, 600, 50, 100, 150, 100, 150, 50, 3, 50, 300, GrowthRate.SLOW, null, false, true, new PokemonForm("Normal", "", Type.ROCK, Type.FAIRY, 0.7, 8.8, Abilities.CLEAR_BODY, Abilities.NONE, Abilities.NONE, 600, 50, 100, 150, 100, 150, 50, 3, 50, 300, false, null, true), diff --git a/src/test/abilities/power_construct.test.ts b/src/test/abilities/power_construct.test.ts index 662f5d06258..1a9e7d4818a 100644 --- a/src/test/abilities/power_construct.test.ts +++ b/src/test/abilities/power_construct.test.ts @@ -32,7 +32,7 @@ describe("Abilities - POWER CONSTRUCT", () => { }); test( - "check if fainted pokemon switches to base form on arena reset", + "check if fainted 50% Power Construct Pokemon switches to base form on arena reset", async () => { const baseForm = 2, completeForm = 4; @@ -41,7 +41,37 @@ describe("Abilities - POWER CONSTRUCT", () => { [Species.ZYGARDE]: completeForm, }); - await game.startBattle([ Species.MAGIKARP, Species.ZYGARDE ]); + await game.classicMode.startBattle([ Species.MAGIKARP, Species.ZYGARDE ]); + + const zygarde = game.scene.getParty().find((p) => p.species.speciesId === Species.ZYGARDE); + expect(zygarde).not.toBe(undefined); + expect(zygarde!.formIndex).toBe(completeForm); + + zygarde!.hp = 0; + zygarde!.status = new Status(StatusEffect.FAINT); + expect(zygarde!.isFainted()).toBe(true); + + game.move.select(Moves.SPLASH); + await game.doKillOpponents(); + await game.phaseInterceptor.to(TurnEndPhase); + game.doSelectModifier(); + await game.phaseInterceptor.to(QuietFormChangePhase); + + expect(zygarde!.formIndex).toBe(baseForm); + }, + ); + + test( + "check if fainted 10% Power Construct Pokemon switches to base form on arena reset", + async () => { + const baseForm = 3, + completeForm = 5; + game.override.startingWave(4); + game.override.starterForms({ + [Species.ZYGARDE]: completeForm, + }); + + await game.classicMode.startBattle([ Species.MAGIKARP, Species.ZYGARDE ]); const zygarde = game.scene.getParty().find((p) => p.species.speciesId === Species.ZYGARDE); expect(zygarde).not.toBe(undefined); From 093f3d90f5a4eab68a1ffe1317d1f76a50fa0a28 Mon Sep 17 00:00:00 2001 From: innerthunder <168692175+innerthunder@users.noreply.github.com> Date: Tue, 15 Oct 2024 18:06:56 -0700 Subject: [PATCH 9/9] [Balance] Add Memory Mushroom to Shop (#4555) * Add Memory Mushroom to Shop + escape TM selection * consolidate learn move type params into an enum * Rewrite lock capsule test * Disable luck upgrades for copied SMPhases * Mem Mushroom Cost 4x Update modifier-type.ts * Add undefined cost check to `addModifier` * Increase shop options row limit * Prevent SMPhase copies from updating the seed --------- Co-authored-by: damocleas Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/battle-scene.ts | 6 +- src/modifier/modifier-type.ts | 3 +- src/modifier/modifier.ts | 9 +-- src/phases/learn-move-phase.ts | 37 +++++++++-- src/phases/select-modifier-phase.ts | 66 +++++++++++++------ src/test/items/lock_capsule.test.ts | 20 +++--- src/test/phases/select-modifier-phase.test.ts | 8 +-- src/ui/modifier-select-ui-handler.ts | 4 +- 8 files changed, 104 insertions(+), 49 deletions(-) diff --git a/src/battle-scene.ts b/src/battle-scene.ts index a84baa55266..75a19b8efaa 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -4,7 +4,7 @@ import Pokemon, { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon"; import PokemonSpecies, { allSpecies, getPokemonSpecies, PokemonSpeciesFilter } from "#app/data/pokemon-species"; import { Constructor, isNullOrUndefined, randSeedInt } from "#app/utils"; import * as Utils from "#app/utils"; -import { ConsumableModifier, ConsumablePokemonModifier, DoubleBattleChanceBoosterModifier, ExpBalanceModifier, ExpShareModifier, FusePokemonModifier, HealingBoosterModifier, Modifier, ModifierBar, ModifierPredicate, MultipleParticipantExpBonusModifier, overrideHeldItems, overrideModifiers, PersistentModifier, PokemonExpBoosterModifier, PokemonFormChangeItemModifier, PokemonHeldItemModifier, PokemonHpRestoreModifier, PokemonIncrementingStatModifier, TerastallizeModifier, TurnHeldItemTransferModifier } from "./modifier/modifier"; +import { ConsumableModifier, ConsumablePokemonModifier, DoubleBattleChanceBoosterModifier, ExpBalanceModifier, ExpShareModifier, FusePokemonModifier, HealingBoosterModifier, Modifier, ModifierBar, ModifierPredicate, MultipleParticipantExpBonusModifier, overrideHeldItems, overrideModifiers, PersistentModifier, PokemonExpBoosterModifier, PokemonFormChangeItemModifier, PokemonHeldItemModifier, PokemonHpRestoreModifier, PokemonIncrementingStatModifier, RememberMoveModifier, TerastallizeModifier, TurnHeldItemTransferModifier } from "./modifier/modifier"; import { PokeballType } from "#app/data/pokeball"; import { initCommonAnims, initMoveAnim, loadCommonAnimAssets, loadMoveAnimAssets, populateAnims } from "#app/data/battle-anims"; import { Phase } from "#app/phase"; @@ -2425,7 +2425,7 @@ export default class BattleScene extends SceneBase { return Math.floor(moneyValue / 10) * 10; } - addModifier(modifier: Modifier | null, ignoreUpdate?: boolean, playSound?: boolean, virtual?: boolean, instant?: boolean): Promise { + addModifier(modifier: Modifier | null, ignoreUpdate?: boolean, playSound?: boolean, virtual?: boolean, instant?: boolean, cost?: number): Promise { if (!modifier) { return Promise.resolve(false); } @@ -2482,6 +2482,8 @@ export default class BattleScene extends SceneBase { } } else if (modifier instanceof FusePokemonModifier) { args.push(this.getPokemonById(modifier.fusePokemonId) as PlayerPokemon); + } else if (modifier instanceof RememberMoveModifier && !Utils.isNullOrUndefined(cost)) { + args.push(cost); } if (modifier.shouldApply(pokemon, ...args)) { diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index f20aa854bdf..32173a6fead 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -2227,7 +2227,8 @@ export function getPlayerShopModifierTypeOptionsForWave(waveIndex: integer, base ], [ new ModifierTypeOption(modifierTypes.HYPER_POTION(), 0, baseCost * 0.8), - new ModifierTypeOption(modifierTypes.MAX_REVIVE(), 0, baseCost * 2.75) + new ModifierTypeOption(modifierTypes.MAX_REVIVE(), 0, baseCost * 2.75), + new ModifierTypeOption(modifierTypes.MEMORY_MUSHROOM(), 0, baseCost * 4) ], [ new ModifierTypeOption(modifierTypes.MAX_POTION(), 0, baseCost * 1.5), diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index dd8c82357a7..689b81be82f 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -11,7 +11,7 @@ import Pokemon, { type PlayerPokemon } from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; import Overrides from "#app/overrides"; import { EvolutionPhase } from "#app/phases/evolution-phase"; -import { LearnMovePhase } from "#app/phases/learn-move-phase"; +import { LearnMovePhase, LearnMoveType } from "#app/phases/learn-move-phase"; import { LevelUpPhase } from "#app/phases/level-up-phase"; import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase"; import { achvs } from "#app/system/achv"; @@ -2235,7 +2235,7 @@ export class TmModifier extends ConsumablePokemonModifier { */ override apply(playerPokemon: PlayerPokemon): boolean { - playerPokemon.scene.unshiftPhase(new LearnMovePhase(playerPokemon.scene, playerPokemon.scene.getParty().indexOf(playerPokemon), this.type.moveId, true)); + playerPokemon.scene.unshiftPhase(new LearnMovePhase(playerPokemon.scene, playerPokemon.scene.getParty().indexOf(playerPokemon), this.type.moveId, LearnMoveType.TM)); return true; } @@ -2255,8 +2255,9 @@ export class RememberMoveModifier extends ConsumablePokemonModifier { * @param playerPokemon The {@linkcode PlayerPokemon} that should remember the move * @returns always `true` */ - override apply(playerPokemon: PlayerPokemon): boolean { - playerPokemon.scene.unshiftPhase(new LearnMovePhase(playerPokemon.scene, playerPokemon.scene.getParty().indexOf(playerPokemon), playerPokemon.getLearnableLevelMoves()[this.levelMoveIndex])); + override apply(playerPokemon: PlayerPokemon, cost?: number): boolean { + + playerPokemon.scene.unshiftPhase(new LearnMovePhase(playerPokemon.scene, playerPokemon.scene.getParty().indexOf(playerPokemon), playerPokemon.getLearnableLevelMoves()[this.levelMoveIndex], LearnMoveType.MEMORY, cost)); return true; } diff --git a/src/phases/learn-move-phase.ts b/src/phases/learn-move-phase.ts index 6480577258a..eb7cfbb65ef 100644 --- a/src/phases/learn-move-phase.ts +++ b/src/phases/learn-move-phase.ts @@ -2,24 +2,37 @@ import BattleScene from "#app/battle-scene"; import { initMoveAnim, loadMoveAnimAssets } from "#app/data/battle-anims"; import Move, { allMoves } from "#app/data/move"; import { SpeciesFormChangeMoveLearnedTrigger } from "#app/data/pokemon-forms"; -import { Moves } from "#app/enums/moves"; +import { Moves } from "#enums/moves"; import { getPokemonNameWithAffix } from "#app/messages"; +import Overrides from "#app/overrides"; import EvolutionSceneHandler from "#app/ui/evolution-scene-handler"; import { SummaryUiMode } from "#app/ui/summary-ui-handler"; import { Mode } from "#app/ui/ui"; import i18next from "i18next"; -import { PlayerPartyMemberPokemonPhase } from "./player-party-member-pokemon-phase"; +import { PlayerPartyMemberPokemonPhase } from "#app/phases/player-party-member-pokemon-phase"; import Pokemon from "#app/field/pokemon"; +import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; + +export enum LearnMoveType { + /** For learning a move via level-up, evolution, or other non-item-based event */ + LEARN_MOVE, + /** For learning a move via Memory Mushroom */ + MEMORY, + /** For learning a move via TM */ + TM +} export class LearnMovePhase extends PlayerPartyMemberPokemonPhase { private moveId: Moves; private messageMode: Mode; - private fromTM: boolean; + private learnMoveType; + private cost: number; - constructor(scene: BattleScene, partyMemberIndex: integer, moveId: Moves, fromTM?: boolean) { + constructor(scene: BattleScene, partyMemberIndex: integer, moveId: Moves, learnMoveType: LearnMoveType = LearnMoveType.LEARN_MOVE, cost: number = -1) { super(scene, partyMemberIndex); this.moveId = moveId; - this.fromTM = fromTM ?? false; + this.learnMoveType = learnMoveType; + this.cost = cost; } start() { @@ -136,11 +149,23 @@ export class LearnMovePhase extends PlayerPartyMemberPokemonPhase { * @param Pokemon The Pokemon learning the move */ async learnMove(index: number, move: Move, pokemon: Pokemon, textMessage?: string) { - if (this.fromTM) { + if (this.learnMoveType === LearnMoveType.TM) { if (!pokemon.usedTMs) { pokemon.usedTMs = []; } pokemon.usedTMs.push(this.moveId); + this.scene.tryRemovePhase((phase) => phase instanceof SelectModifierPhase); + } else if (this.learnMoveType === LearnMoveType.MEMORY) { + if (this.cost !== -1) { + if (!Overrides.WAIVE_ROLL_FEE_OVERRIDE) { + this.scene.money -= this.cost; + this.scene.updateMoneyText(); + this.scene.animateMoneyChanged(false); + } + this.scene.playSound("se/buy"); + } else { + this.scene.tryRemovePhase((phase) => phase instanceof SelectModifierPhase); + } } pokemon.setMove(index, this.moveId); initMoveAnim(this.scene, this.moveId).then(() => { diff --git a/src/phases/select-modifier-phase.ts b/src/phases/select-modifier-phase.ts index 159af979fa0..f9b3e978923 100644 --- a/src/phases/select-modifier-phase.ts +++ b/src/phases/select-modifier-phase.ts @@ -16,26 +16,32 @@ export class SelectModifierPhase extends BattlePhase { private rerollCount: integer; private modifierTiers?: ModifierTier[]; private customModifierSettings?: CustomModifierSettings; + private isCopy: boolean; - constructor(scene: BattleScene, rerollCount: integer = 0, modifierTiers?: ModifierTier[], customModifierSettings?: CustomModifierSettings) { + private typeOptions: ModifierTypeOption[]; + + constructor(scene: BattleScene, rerollCount: integer = 0, modifierTiers?: ModifierTier[], customModifierSettings?: CustomModifierSettings, isCopy: boolean = false) { super(scene); this.rerollCount = rerollCount; this.modifierTiers = modifierTiers; this.customModifierSettings = customModifierSettings; + this.isCopy = isCopy; } start() { super.start(); - if (!this.rerollCount) { + if (!this.rerollCount && !this.isCopy) { this.updateSeed(); - } else { + } else if (this.rerollCount) { this.scene.reroll = false; } const party = this.scene.getParty(); - regenerateModifierPoolThresholds(party, this.getPoolType(), this.rerollCount); + if (!this.isCopy) { + regenerateModifierPoolThresholds(party, this.getPoolType(), this.rerollCount); + } const modifierCount = new Utils.IntegerHolder(3); if (this.isPlayer()) { this.scene.applyModifiers(ExtraModifierModifier, true, modifierCount); @@ -54,7 +60,7 @@ export class SelectModifierPhase extends BattlePhase { } } - const typeOptions: ModifierTypeOption[] = this.getModifierTypeOptions(modifierCount.value); + this.typeOptions = this.getModifierTypeOptions(modifierCount.value); const modifierSelectCallback = (rowCursor: integer, cursor: integer) => { if (rowCursor < 0 || cursor < 0) { @@ -63,13 +69,13 @@ export class SelectModifierPhase extends BattlePhase { this.scene.ui.revertMode(); this.scene.ui.setMode(Mode.MESSAGE); super.end(); - }, () => this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), typeOptions, modifierSelectCallback, this.getRerollCost(typeOptions, this.scene.lockModifierTiers))); + }, () => this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), this.typeOptions, modifierSelectCallback, this.getRerollCost(this.scene.lockModifierTiers))); }); return false; } let modifierType: ModifierType; let cost: integer; - const rerollCost = this.getRerollCost(typeOptions, this.scene.lockModifierTiers); + const rerollCost = this.getRerollCost(this.scene.lockModifierTiers); switch (rowCursor) { case 0: switch (cursor) { @@ -79,7 +85,7 @@ export class SelectModifierPhase extends BattlePhase { return false; } else { this.scene.reroll = true; - this.scene.unshiftPhase(new SelectModifierPhase(this.scene, this.rerollCount + 1, typeOptions.map(o => o.type?.tier).filter(t => t !== undefined) as ModifierTier[])); + this.scene.unshiftPhase(new SelectModifierPhase(this.scene, this.rerollCount + 1, this.typeOptions.map(o => o.type?.tier).filter(t => t !== undefined) as ModifierTier[])); this.scene.ui.clearText(); this.scene.ui.setMode(Mode.MESSAGE).then(() => super.end()); if (!Overrides.WAIVE_ROLL_FEE_OVERRIDE) { @@ -98,13 +104,13 @@ export class SelectModifierPhase extends BattlePhase { const itemModifier = itemModifiers[itemIndex]; this.scene.tryTransferHeldItemModifier(itemModifier, party[toSlotIndex], true, itemQuantity); } else { - this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), typeOptions, modifierSelectCallback, this.getRerollCost(typeOptions, this.scene.lockModifierTiers)); + this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), this.typeOptions, modifierSelectCallback, this.getRerollCost(this.scene.lockModifierTiers)); } }, PartyUiHandler.FilterItemMaxStacks); break; case 2: this.scene.ui.setModeWithoutClear(Mode.PARTY, PartyUiMode.CHECK, -1, () => { - this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), typeOptions, modifierSelectCallback, this.getRerollCost(typeOptions, this.scene.lockModifierTiers)); + this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), this.typeOptions, modifierSelectCallback, this.getRerollCost(this.scene.lockModifierTiers)); }); break; case 3: @@ -115,21 +121,21 @@ export class SelectModifierPhase extends BattlePhase { } this.scene.lockModifierTiers = !this.scene.lockModifierTiers; const uiHandler = this.scene.ui.getHandler() as ModifierSelectUiHandler; - uiHandler.setRerollCost(this.getRerollCost(typeOptions, this.scene.lockModifierTiers)); + uiHandler.setRerollCost(this.getRerollCost(this.scene.lockModifierTiers)); uiHandler.updateLockRaritiesText(); uiHandler.updateRerollCostText(); return false; } return true; case 1: - if (typeOptions.length === 0) { + if (this.typeOptions.length === 0) { this.scene.ui.clearText(); this.scene.ui.setMode(Mode.MESSAGE); super.end(); return true; } - if (typeOptions[cursor].type) { - modifierType = typeOptions[cursor].type; + if (this.typeOptions[cursor].type) { + modifierType = this.typeOptions[cursor].type; } break; default: @@ -151,8 +157,16 @@ export class SelectModifierPhase extends BattlePhase { } const applyModifier = (modifier: Modifier, playSound: boolean = false) => { - const result = this.scene.addModifier(modifier, false, playSound); - if (cost) { + const result = this.scene.addModifier(modifier, false, playSound, undefined, undefined, cost); + // Queue a copy of this phase when applying a TM or Memory Mushroom. + // If the player selects either of these, then escapes out of consuming them, + // they are returned to a shop in the same state. + if (modifier.type instanceof RememberMoveModifierType || + modifier.type instanceof TmModifierType) { + this.scene.unshiftPhase(this.copy()); + } + + if (cost && !(modifier.type instanceof RememberMoveModifierType)) { result.then(success => { if (success) { if (!Overrides.WAIVE_ROLL_FEE_OVERRIDE) { @@ -189,7 +203,7 @@ export class SelectModifierPhase extends BattlePhase { applyModifier(modifier, true); }); } else { - this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), typeOptions, modifierSelectCallback, this.getRerollCost(typeOptions, this.scene.lockModifierTiers)); + this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), this.typeOptions, modifierSelectCallback, this.getRerollCost(this.scene.lockModifierTiers)); } }, modifierType.selectFilter); } else { @@ -216,7 +230,7 @@ export class SelectModifierPhase extends BattlePhase { applyModifier(modifier!, true); // TODO: is the bang correct? }); } else { - this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), typeOptions, modifierSelectCallback, this.getRerollCost(typeOptions, this.scene.lockModifierTiers)); + this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), this.typeOptions, modifierSelectCallback, this.getRerollCost(this.scene.lockModifierTiers)); } }, pokemonModifierType.selectFilter, modifierType instanceof PokemonMoveModifierType ? (modifierType as PokemonMoveModifierType).moveSelectFilter : undefined, tmMoveId, isPpRestoreModifier); } @@ -226,7 +240,7 @@ export class SelectModifierPhase extends BattlePhase { return !cost!;// TODO: is the bang correct? }; - this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), typeOptions, modifierSelectCallback, this.getRerollCost(typeOptions, this.scene.lockModifierTiers)); + this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), this.typeOptions, modifierSelectCallback, this.getRerollCost(this.scene.lockModifierTiers)); } updateSeed(): void { @@ -237,13 +251,13 @@ export class SelectModifierPhase extends BattlePhase { return true; } - getRerollCost(typeOptions: ModifierTypeOption[], lockRarities: boolean): number { + getRerollCost(lockRarities: boolean): number { let baseValue = 0; if (Overrides.WAIVE_ROLL_FEE_OVERRIDE) { return baseValue; } else if (lockRarities) { const tierValues = [ 50, 125, 300, 750, 2000 ]; - for (const opt of typeOptions) { + for (const opt of this.typeOptions) { baseValue += tierValues[opt.type.tier ?? 0]; } } else { @@ -271,6 +285,16 @@ export class SelectModifierPhase extends BattlePhase { return getPlayerModifierTypeOptions(modifierCount, this.scene.getParty(), this.scene.lockModifierTiers ? this.modifierTiers : undefined, this.customModifierSettings); } + copy(): SelectModifierPhase { + return new SelectModifierPhase( + this.scene, + this.rerollCount, + this.modifierTiers, + { guaranteedModifierTypeOptions: this.typeOptions, rerollMultiplier: this.customModifierSettings?.rerollMultiplier, allowLuckUpgrades: false }, + true + ); + } + addModifier(modifier: Modifier): Promise { return this.scene.addModifier(modifier, false, true); } diff --git a/src/test/items/lock_capsule.test.ts b/src/test/items/lock_capsule.test.ts index 2667ecea2dc..0b6534b5eaf 100644 --- a/src/test/items/lock_capsule.test.ts +++ b/src/test/items/lock_capsule.test.ts @@ -1,7 +1,8 @@ import { Abilities } from "#app/enums/abilities"; import { Moves } from "#app/enums/moves"; -import { ModifierTypeOption, modifierTypes } from "#app/modifier/modifier-type"; +import { ModifierTier } from "#app/modifier/modifier-tier"; import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; +import { Mode } from "#app/ui/ui"; import GameManager from "#test/utils/gameManager"; import Phase from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; @@ -32,15 +33,16 @@ describe("Items - Lock Capsule", () => { }); it("doesn't set the cost of common tier items to 0", async () => { - await game.startBattle(); + await game.classicMode.startBattle(); + game.scene.overridePhase(new SelectModifierPhase(game.scene, 0, undefined, { guaranteedModifierTiers: [ ModifierTier.COMMON, ModifierTier.COMMON, ModifierTier.COMMON ], fillRemaining: false })); - game.move.select(Moves.SURF); - await game.phaseInterceptor.to(SelectModifierPhase, false); + game.onNextPrompt("SelectModifierPhase", Mode.MODIFIER_SELECT, () => { + const selectModifierPhase = game.scene.getCurrentPhase() as SelectModifierPhase; + const rerollCost = selectModifierPhase.getRerollCost(true); + expect(rerollCost).toBe(150); + }); - const rewards = game.scene.getCurrentPhase() as SelectModifierPhase; - const potion = new ModifierTypeOption(modifierTypes.POTION(), 0, 40); // Common tier item - const rerollCost = rewards.getRerollCost([ potion, potion, potion ], true); - - expect(rerollCost).toBe(150); + game.doSelectModifier(); + await game.phaseInterceptor.to("SelectModifierPhase"); }, 20000); }); diff --git a/src/test/phases/select-modifier-phase.test.ts b/src/test/phases/select-modifier-phase.test.ts index ea50c7e6524..a945aff055b 100644 --- a/src/test/phases/select-modifier-phase.test.ts +++ b/src/test/phases/select-modifier-phase.test.ts @@ -63,11 +63,11 @@ describe("SelectModifierPhase", () => { new ModifierTypeOption(modifierTypes.REVIVE(), 0, 1000) ]; - const selectModifierPhase1 = new SelectModifierPhase(scene); - const selectModifierPhase2 = new SelectModifierPhase(scene, 0, undefined, { rerollMultiplier: 2 }); + const selectModifierPhase1 = new SelectModifierPhase(scene, 0, undefined, { guaranteedModifierTypeOptions: options }); + const selectModifierPhase2 = new SelectModifierPhase(scene, 0, undefined, { guaranteedModifierTypeOptions: options, rerollMultiplier: 2 }); - const cost1 = selectModifierPhase1.getRerollCost(options, false); - const cost2 = selectModifierPhase2.getRerollCost(options, false); + const cost1 = selectModifierPhase1.getRerollCost(false); + const cost2 = selectModifierPhase2.getRerollCost(false); expect(cost2).toEqual(cost1 * 2); }); diff --git a/src/ui/modifier-select-ui-handler.ts b/src/ui/modifier-select-ui-handler.ts index f7e57b53193..0bae56c03b4 100644 --- a/src/ui/modifier-select-ui-handler.ts +++ b/src/ui/modifier-select-ui-handler.ts @@ -16,7 +16,7 @@ import { ShopCursorTarget } from "#app/enums/shop-cursor-target"; import { IntegerHolder } from "./../utils"; import Phaser from "phaser"; -export const SHOP_OPTIONS_ROW_LIMIT = 6; +export const SHOP_OPTIONS_ROW_LIMIT = 7; export default class ModifierSelectUiHandler extends AwaitableUiHandler { private modifierContainer: Phaser.GameObjects.Container; @@ -211,7 +211,7 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { const row = m < SHOP_OPTIONS_ROW_LIMIT ? 0 : 1; const col = m < SHOP_OPTIONS_ROW_LIMIT ? m : m - SHOP_OPTIONS_ROW_LIMIT; const rowOptions = shopTypeOptions.slice(row ? SHOP_OPTIONS_ROW_LIMIT : 0, row ? undefined : SHOP_OPTIONS_ROW_LIMIT); - const sliceWidth = (this.scene.game.canvas.width / SHOP_OPTIONS_ROW_LIMIT) / (rowOptions.length + 2); + const sliceWidth = (this.scene.game.canvas.width / 6) / (rowOptions.length + 2); const option = new ModifierOption(this.scene, sliceWidth * (col + 1) + (sliceWidth * 0.5), ((-this.scene.game.canvas.height / 12) - (this.scene.game.canvas.height / 32) - (40 - (28 * row - 1))), shopTypeOptions[m]); option.setScale(0.375); this.scene.add.existing(option);