diff --git a/src/data/ability.ts b/src/data/ability.ts old mode 100755 new mode 100644 index 7a8d77cc022..5e261f46316 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -1798,6 +1798,61 @@ export class PostDefendStealHeldItemAbAttr extends PostDefendAbAttr { } } +/** + * Base class for defining all {@linkcode Ability} Attributes after a status effect has been set. + * @see {@linkcode applyPostSetStatus()}. + */ +export class PostSetStatusAbAttr extends AbAttr { + /** + * Does nothing after a status condition is set. + * @param pokemon {@linkcode Pokemon} that status condition was set on. + * @param sourcePokemon {@linkcode Pokemon} that that set the status condition. Is `null` if status was not set by a Pokemon. + * @param passive Whether this ability is a passive. + * @param effect {@linkcode StatusEffect} that was set. + * @param args Set of unique arguments needed by this attribute. + * @returns `true` if application of the ability succeeds. + */ + applyPostSetStatus(pokemon: Pokemon, sourcePokemon: Pokemon | null = null, passive: boolean, effect: StatusEffect, simulated: boolean, args: any[]) : boolean | Promise { + return false; + } +} + +/** + * If another Pokemon burns, paralyzes, poisons, or badly poisons this Pokemon, + * that Pokemon receives the same non-volatile status condition as part of this + * ability attribute. For Synchronize ability. + */ +export class SynchronizeStatusAbAttr extends PostSetStatusAbAttr { + /** + * If the `StatusEffect` that was set is Burn, Paralysis, Poison, or Toxic, and the status + * was set by a source Pokemon, set the source Pokemon's status to the same `StatusEffect`. + * @param pokemon {@linkcode Pokemon} that status condition was set on. + * @param sourcePokemon {@linkcode Pokemon} that that set the status condition. Is null if status was not set by a Pokemon. + * @param passive Whether this ability is a passive. + * @param effect {@linkcode StatusEffect} that was set. + * @param args Set of unique arguments needed by this attribute. + * @returns `true` if application of the ability succeeds. + */ + override applyPostSetStatus(pokemon: Pokemon, sourcePokemon: Pokemon | null = null, passive: boolean, effect: StatusEffect, simulated: boolean, args: any[]): boolean { + /** Synchronizable statuses */ + const syncStatuses = new Set([ + StatusEffect.BURN, + StatusEffect.PARALYSIS, + StatusEffect.POISON, + StatusEffect.TOXIC + ]); + + if (sourcePokemon && syncStatuses.has(effect)) { + if (!simulated) { + sourcePokemon.trySetStatus(effect, true, pokemon); + } + return true; + } + + return false; + } +} + export class PostVictoryAbAttr extends AbAttr { applyPostVictory(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean | Promise { return false; @@ -4677,6 +4732,10 @@ export function applyStatMultiplierAbAttrs(attrType: Constructor { return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyStatStage(pokemon, passive, simulated, stat, statValue, args), args); } +export function applyPostSetStatusAbAttrs(attrType: Constructor, + pokemon: Pokemon, effect: StatusEffect, sourcePokemon?: Pokemon | null, simulated: boolean = false, ...args: any[]): Promise { + return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyPostSetStatus(pokemon, sourcePokemon, passive, effect, simulated, args), args, false, simulated); +} /** * Applies a field Stat multiplier attribute @@ -4907,7 +4966,8 @@ export function initAbilities() { .attr(EffectSporeAbAttr), new Ability(Abilities.SYNCHRONIZE, 3) .attr(SyncEncounterNatureAbAttr) - .unimplemented(), + .attr(SynchronizeStatusAbAttr) + .partial(), // interaction with psycho shift needs work, keeping to old Gen interaction for now new Ability(Abilities.CLEAR_BODY, 3) .attr(ProtectStatAbAttr) .ignorable(), diff --git a/src/data/battle-anims.ts b/src/data/battle-anims.ts index eb0dce3bf0c..7c149ec54d5 100644 --- a/src/data/battle-anims.ts +++ b/src/data/battle-anims.ts @@ -915,12 +915,12 @@ export abstract class BattleAnim { this.srcLine = [ userFocusX, userFocusY, targetFocusX, targetFocusY ]; this.dstLine = [ userInitialX, userInitialY, targetInitialX, targetInitialY ]; - let r = anim!.frames.length; // TODO: is this bang correct? + let r = anim?.frames.length ?? 0; let f = 0; scene.tweens.addCounter({ duration: Utils.getFrameMs(3), - repeat: anim!.frames.length, // TODO: is this bang correct? + repeat: anim?.frames.length ?? 0, onRepeat: () => { if (!f) { userSprite.setVisible(false); @@ -1264,7 +1264,7 @@ export class CommonBattleAnim extends BattleAnim { } getAnim(): AnimConfig | null { - return this.commonAnim ? commonAnims.get(this.commonAnim)! : null; // TODO: is this bang correct? + return this.commonAnim ? commonAnims.get(this.commonAnim) ?? null : null; } isOppAnim(): boolean { @@ -1284,7 +1284,7 @@ export class MoveAnim extends BattleAnim { getAnim(): AnimConfig { return moveAnims.get(this.move) instanceof AnimConfig ? moveAnims.get(this.move) as AnimConfig - : moveAnims.get(this.move)![this.user?.isPlayer() ? 0 : 1] as AnimConfig; // TODO: is this bang correct? + : moveAnims.get(this.move)?.[this.user?.isPlayer() ? 0 : 1] as AnimConfig; } isOppAnim(): boolean { @@ -1316,7 +1316,7 @@ export class MoveChargeAnim extends MoveAnim { getAnim(): AnimConfig { return chargeAnims.get(this.chargeAnim) instanceof AnimConfig ? chargeAnims.get(this.chargeAnim) as AnimConfig - : chargeAnims.get(this.chargeAnim)![this.user?.isPlayer() ? 0 : 1] as AnimConfig; // TODO: is this bang correct? + : chargeAnims.get(this.chargeAnim)?.[this.user?.isPlayer() ? 0 : 1] as AnimConfig; } } diff --git a/src/data/challenge.ts b/src/data/challenge.ts index 46b56a30835..f0a928a78fc 100644 --- a/src/data/challenge.ts +++ b/src/data/challenge.ts @@ -172,11 +172,9 @@ export abstract class Challenge { * @param overrideValue {@link integer} The value to check for. If undefined, gets the current value. * @returns {@link string} The localised name for the current value. */ - getValue(overrideValue?: integer): string { - if (overrideValue === undefined) { - overrideValue = this.value; - } - return i18next.t(`challenges:${this.geti18nKey()}.value.${this.value}`); + getValue(overrideValue?: number): string { + const value = overrideValue ?? this.value; + return i18next.t(`challenges:${this.geti18nKey()}.value.${value}`); } /** @@ -184,11 +182,9 @@ export abstract class Challenge { * @param overrideValue {@link integer} The value to check for. If undefined, gets the current value. * @returns {@link string} The localised description for the current value. */ - getDescription(overrideValue?: integer): string { - if (overrideValue === undefined) { - overrideValue = this.value; - } - return `${i18next.t([`challenges:${this.geti18nKey()}.desc.${this.value}`, `challenges:${this.geti18nKey()}.desc`])}`; + getDescription(overrideValue?: number): string { + const value = overrideValue ?? this.value; + return `${i18next.t([`challenges:${this.geti18nKey()}.desc.${value}`, `challenges:${this.geti18nKey()}.desc`])}`; } /** @@ -511,14 +507,12 @@ export class SingleGenerationChallenge extends Challenge { * @param {value} overrideValue The value to check for. If undefined, gets the current value. * @returns {string} The localised name for the current value. */ - getValue(overrideValue?: integer): string { - if (overrideValue === undefined) { - overrideValue = this.value; - } - if (this.value === 0) { + getValue(overrideValue?: number): string { + const value = overrideValue ?? this.value; + if (value === 0) { return i18next.t("settings:off"); } - return i18next.t(`starterSelectUiHandler:gen${this.value}`); + return i18next.t(`starterSelectUiHandler:gen${value}`); } /** @@ -526,14 +520,12 @@ export class SingleGenerationChallenge extends Challenge { * @param {value} overrideValue The value to check for. If undefined, gets the current value. * @returns {string} The localised description for the current value. */ - getDescription(overrideValue?: integer): string { - if (overrideValue === undefined) { - overrideValue = this.value; - } - if (this.value === 0) { + getDescription(overrideValue?: number): string { + const value = overrideValue ?? this.value; + if (value === 0) { return i18next.t("challenges:singleGeneration.desc_default"); } - return i18next.t("challenges:singleGeneration.desc", { gen: i18next.t(`challenges:singleGeneration.gen_${this.value}`) }); + return i18next.t("challenges:singleGeneration.desc", { gen: i18next.t(`challenges:singleGeneration.gen_${value}`) }); } diff --git a/src/data/move.ts b/src/data/move.ts index 86139a22adf..44d01c71055 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -650,7 +650,7 @@ export default class Move implements Localizable { } /** - * Applies each {@linkcode MoveCondition} of this move to the params + * Applies each {@linkcode MoveCondition} function of this move to the params, determines if the move can be used prior to calling each attribute's apply() * @param user {@linkcode Pokemon} to apply conditions to * @param target {@linkcode Pokemon} to apply conditions to * @param move {@linkcode Move} to apply conditions to @@ -2091,21 +2091,20 @@ export class PsychoShiftEffectAttr extends MoveEffectAttr { if (target.status) { return false; - } - //@ts-ignore - how can target.status.effect be checked when we return `false` before when it's defined? - if (!target.status || (target.status.effect === statusToApply && move.chance < 0)) { // TODO: resolve ts-ignore - const statusAfflictResult = target.trySetStatus(statusToApply, true, user); - if (statusAfflictResult) { + } else { + const canSetStatus = target.canSetStatus(statusToApply, true, false, user); + + if (canSetStatus) { if (user.status) { user.scene.queueMessage(getStatusEffectHealText(user.status.effect, getPokemonNameWithAffix(user))); } user.resetStatus(); user.updateInfo(); + target.trySetStatus(statusToApply, true, user); } - return statusAfflictResult; - } - return false; + return canSetStatus; + } } getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { @@ -5296,6 +5295,21 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { } } + +export class ChillyReceptionAttr extends ForceSwitchOutAttr { + + // using inherited constructor + + apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise { + user.scene.arena.trySetWeather(WeatherType.SNOW, true); + return super.apply(user, target, move, args); + } + + getCondition(): MoveConditionFunc { + // chilly reception move will go through if the weather is change-able to snow, or the user can switch out, else move will fail + return (user, target, move) => user.scene.arena.trySetWeather(WeatherType.SNOW, true) || super.getSwitchOutCondition()(user, target, move); + } +} export class RemoveTypeAttr extends MoveEffectAttr { private removedType: Type; @@ -9485,10 +9499,9 @@ export function initMoves() { .makesContact(), new SelfStatusMove(Moves.SHED_TAIL, Type.NORMAL, -1, 10, -1, 0, 9) .unimplemented(), - new StatusMove(Moves.CHILLY_RECEPTION, Type.ICE, -1, 10, -1, 0, 9) - .attr(WeatherChangeAttr, WeatherType.SNOW) - .attr(ForceSwitchOutAttr, true, false) - .target(MoveTarget.BOTH_SIDES), + new SelfStatusMove(Moves.CHILLY_RECEPTION, Type.ICE, -1, 10, -1, 0, 9) + .attr(PreMoveMessageAttr, (user, move) => i18next.t("moveTriggers:chillyReception", {pokemonName: getPokemonNameWithAffix(user)})) + .attr(ChillyReceptionAttr, true, false), new SelfStatusMove(Moves.TIDY_UP, Type.NORMAL, -1, 10, -1, 0, 9) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPD ], 1, true, null, true, true) .attr(RemoveArenaTrapAttr, true) diff --git a/src/data/trainer-config.ts b/src/data/trainer-config.ts index 62f9589b7a3..21ba3a9890f 100644 --- a/src/data/trainer-config.ts +++ b/src/data/trainer-config.ts @@ -597,7 +597,7 @@ 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.UNCOMMON]: [Species.LITWICK, Species.SNEASEL, Species.PUMPKABOO, Species.PHANTUMP, Species.HONEDGE, Species.BINACLE, Species.BERGMITE, Species.HOUNDOUR, Species.SKRELP, Species.SLIGGOO], + [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] }; } @@ -640,14 +640,14 @@ export class TrainerConfig { return { [TrainerPoolTier.COMMON]: [ Species.ZUBAT, Species.GRIMER, Species.STUNKY, Species.FOONGUS, Species.MAREANIE, Species.TOXEL, Species.SHROODLE, Species.PALDEA_WOOPER ], [TrainerPoolTier.UNCOMMON]: [ Species.GASTLY, Species.SEVIPER, Species.SKRELP, Species.ALOLA_GRIMER, Species.GALAR_SLOWPOKE, Species.HISUI_QWILFISH ], - [TrainerPoolTier.RARE]: [ Species.BULBASAUR, Species.GLIMMET ] + [TrainerPoolTier.RARE]: [ Species.GLIMMET, Species.BULBASAUR ] }; } case "star_4": { return { [TrainerPoolTier.COMMON]: [ Species.CLEFFA, Species.IGGLYBUFF, Species.AZURILL, Species.COTTONEE, Species.FLABEBE, Species.HATENNA, Species.IMPIDIMP, Species.TINKATINK ], [TrainerPoolTier.UNCOMMON]: [ Species.TOGEPI, Species.GARDEVOIR, Species.SYLVEON, Species.KLEFKI, Species.MIMIKYU, Species.ALOLA_VULPIX ], - [TrainerPoolTier.RARE]: [ Species.POPPLIO, Species.GALAR_PONYTA ] + [TrainerPoolTier.RARE]: [ Species.GALAR_PONYTA, Species.POPPLIO ] }; } case "star_5": { @@ -1509,7 +1509,7 @@ export const trainerConfigs: TrainerConfigs = { .setSpeciesPools({ [TrainerPoolTier.COMMON]: [Species.CARVANHA, Species.WAILMER, Species.ZIGZAGOON, Species.LOTAD, Species.CORPHISH, Species.SPHEAL, Species.REMORAID, Species.QWILFISH, Species.BARBOACH], [TrainerPoolTier.UNCOMMON]: [Species.CLAMPERL, Species.CHINCHOU, Species.WOOPER, Species.WINGULL, Species.TENTACOOL, Species.AZURILL, Species.CLOBBOPUS, Species.HORSEA], - [TrainerPoolTier.RARE]: [Species.MANTINE, Species.DHELMISE, Species.HISUI_QWILFISH, Species.ARROKUDA, Species.PALDEA_WOOPER, Species.SKRELP], + [TrainerPoolTier.RARE]: [Species.MANTYKE, Species.DHELMISE, Species.HISUI_QWILFISH, Species.ARROKUDA, Species.PALDEA_WOOPER, Species.SKRELP], [TrainerPoolTier.SUPER_RARE]: [Species.DONDOZO, Species.BASCULEGION] }), [TrainerType.MATT]: new TrainerConfig(++t).setMoneyMultiplier(1.5).initForEvilTeamAdmin("aqua_admin", "aqua", [Species.SHARPEDO]).setEncounterBgm(TrainerType.PLASMA_GRUNT).setBattleBgm("battle_plasma_grunt").setMixedBattleBgm("battle_aqua_magma_grunt").setVictoryBgm("victory_team_plasma").setPartyTemplateFunc(scene => getEvilGruntPartyTemplate(scene)), @@ -1527,8 +1527,8 @@ export const trainerConfigs: TrainerConfigs = { [TrainerType.PLASMA_GRUNT]: new TrainerConfig(++t).setHasGenders("Plasma Grunt Female").setHasDouble("Plasma Grunts").setMoneyMultiplier(1.0).setEncounterBgm(TrainerType.PLASMA_GRUNT).setBattleBgm("battle_plasma_grunt").setMixedBattleBgm("battle_plasma_grunt").setVictoryBgm("victory_team_plasma").setPartyTemplateFunc(scene => getEvilGruntPartyTemplate(scene)) .setSpeciesPools({ [TrainerPoolTier.COMMON]: [Species.PATRAT, Species.LILLIPUP, Species.PURRLOIN, Species.SCRAFTY, Species.WOOBAT, Species.VANILLITE, Species.SANDILE, Species.TRUBBISH, Species.TYMPOLE], - [TrainerPoolTier.UNCOMMON]: [Species.FRILLISH, Species.VENIPEDE, Species.GOLETT, Species.TIMBURR, Species.DARUMAKA, Species.FOONGUS, Species.JOLTIK], - [TrainerPoolTier.RARE]: [Species.PAWNIARD, Species.RUFFLET, Species.VULLABY, Species.ZORUA, Species.DRILBUR, Species.KLINK, Species.CUBCHOO, Species.MIENFOO, Species.DURANT, Species.BOUFFALANT], + [TrainerPoolTier.UNCOMMON]: [Species.FRILLISH, Species.VENIPEDE, Species.GOLETT, Species.TIMBURR, Species.DARUMAKA, Species.FOONGUS, Species.JOLTIK, Species.CUBCHOO, Species.KLINK], + [TrainerPoolTier.RARE]: [Species.PAWNIARD, Species.RUFFLET, Species.VULLABY, Species.ZORUA, Species.DRILBUR, Species.MIENFOO, Species.DURANT, Species.BOUFFALANT], [TrainerPoolTier.SUPER_RARE]: [Species.DRUDDIGON, Species.HISUI_ZORUA, Species.AXEW, Species.DEINO] }), [TrainerType.ZINZOLIN]: new TrainerConfig(++t).setMoneyMultiplier(1.5).initForEvilTeamAdmin("plasma_sage", "plasma", [Species.CRYOGONAL]).setEncounterBgm(TrainerType.PLASMA_GRUNT).setBattleBgm("battle_plasma_grunt").setMixedBattleBgm("battle_plasma_grunt").setVictoryBgm("victory_team_plasma").setPartyTemplateFunc(scene => getEvilGruntPartyTemplate(scene)), @@ -1537,7 +1537,7 @@ export const trainerConfigs: TrainerConfigs = { .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.RARE]: [Species.LITWICK, Species.SNEASEL, Species.PAWNIARD, Species.BERGMITE, Species.SLIGGOO], + [TrainerPoolTier.RARE]: [Species.LITWICK, Species.SNEASEL, Species.PAWNIARD, Species.SLIGGOO], [TrainerPoolTier.SUPER_RARE]: [Species.NOIVERN, 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)), @@ -1545,15 +1545,15 @@ export const trainerConfigs: TrainerConfigs = { [TrainerType.AETHER_GRUNT]: new TrainerConfig(++t).setHasGenders("Aether Grunt Female").setHasDouble("Aether Grunts").setMoneyMultiplier(1.0).setEncounterBgm(TrainerType.PLASMA_GRUNT).setBattleBgm("battle_plasma_grunt").setMixedBattleBgm("battle_aether_grunt").setVictoryBgm("victory_team_plasma").setPartyTemplateFunc(scene => getEvilGruntPartyTemplate(scene)) .setSpeciesPools({ [TrainerPoolTier.COMMON]: [ Species.PIKIPEK, Species.ROCKRUFF, Species.ALOLA_DIGLETT, Species.ALOLA_EXEGGUTOR, Species.YUNGOOS, Species.CORSOLA, Species.ALOLA_GEODUDE, Species.ALOLA_RAICHU, Species.BOUNSWEET, Species.LILLIPUP, Species.KOMALA, Species.MORELULL, Species.COMFEY, Species.TOGEDEMARU], - [TrainerPoolTier.UNCOMMON]: [ Species.POLIWAG, Species.STUFFUL, Species.ORANGURU, Species.PASSIMIAN, Species.BRUXISH, Species.MINIOR, Species.WISHIWASHI, Species.CRABRAWLER, Species.CUTIEFLY, Species.ORICORIO, Species.MUDBRAY, Species.PYUKUMUKU, Species.ALOLA_MAROWAK], - [TrainerPoolTier.RARE]: [ Species.GALAR_CORSOLA, Species.ALOLA_SANDSHREW, Species.ALOLA_VULPIX, Species.TURTONATOR, Species.DRAMPA], + [TrainerPoolTier.UNCOMMON]: [ Species.POLIWAG, Species.STUFFUL, Species.ORANGURU, Species.PASSIMIAN, Species.BRUXISH, Species.MINIOR, Species.WISHIWASHI, Species.ALOLA_SANDSHREW, Species.ALOLA_VULPIX, Species.CRABRAWLER, Species.CUTIEFLY, Species.ORICORIO, Species.MUDBRAY, Species.PYUKUMUKU, Species.ALOLA_MAROWAK], + [TrainerPoolTier.RARE]: [ Species.GALAR_CORSOLA, Species.TURTONATOR, Species.MIMIKYU, Species.MAGNEMITE, Species.DRAMPA], [TrainerPoolTier.SUPER_RARE]: [Species.JANGMO_O, Species.PORYGON] }), [TrainerType.FABA]: new TrainerConfig(++t).setMoneyMultiplier(1.5).initForEvilTeamAdmin("aether_admin", "aether", [Species.HYPNO]).setEncounterBgm(TrainerType.PLASMA_GRUNT).setBattleBgm("battle_plasma_grunt").setMixedBattleBgm("battle_aether_grunt").setVictoryBgm("victory_team_plasma").setPartyTemplateFunc(scene => getEvilGruntPartyTemplate(scene)), [TrainerType.SKULL_GRUNT]: new TrainerConfig(++t).setHasGenders("Skull Grunt Female").setHasDouble("Skull Grunts").setMoneyMultiplier(1.0).setEncounterBgm(TrainerType.PLASMA_GRUNT).setBattleBgm("battle_plasma_grunt").setMixedBattleBgm("battle_skull_grunt").setVictoryBgm("victory_team_plasma").setPartyTemplateFunc(scene => getEvilGruntPartyTemplate(scene)) .setSpeciesPools({ - [TrainerPoolTier.COMMON]: [ Species.SALANDIT, Species.ALOLA_RATTATA, Species.EKANS, Species.ALOLA_MEOWTH, Species.SCRAGGY, Species.KOFFING, Species.ALOLA_GRIMER, Species.MAREANIE, Species.SPINARAK, Species.TRUBBISH], - [TrainerPoolTier.UNCOMMON]: [ Species.FOMANTIS, Species.SABLEYE, Species.SANDILE, Species.HOUNDOUR, Species.ALOLA_MAROWAK, Species.GASTLY, Species.PANCHAM, Species.DROWZEE, Species.ZUBAT, Species.VENIPEDE, Species.VULLABY], + [TrainerPoolTier.COMMON]: [ Species.SALANDIT, Species.ALOLA_RATTATA, Species.EKANS, Species.ALOLA_MEOWTH, Species.SCRAGGY, Species.KOFFING, Species.ALOLA_GRIMER, Species.MAREANIE, Species.SPINARAK, Species.TRUBBISH, Species.DROWZEE], + [TrainerPoolTier.UNCOMMON]: [ Species.FOMANTIS, Species.SABLEYE, Species.SANDILE, Species.HOUNDOUR, Species.ALOLA_MAROWAK, Species.GASTLY, Species.PANCHAM, Species.ZUBAT, Species.VENIPEDE, Species.VULLABY], [TrainerPoolTier.RARE]: [Species.SANDYGAST, Species.PAWNIARD, Species.MIMIKYU, Species.DHELMISE, Species.WISHIWASHI, Species.NYMBLE], [TrainerPoolTier.SUPER_RARE]: [Species.GRUBBIN, Species.DEWPIDER] }), @@ -1916,7 +1916,14 @@ export const trainerConfigs: TrainerConfigs = { p.formIndex = 1; // Mega Kangaskhan p.generateName(); })) - .setPartyMemberFunc(4, getRandomPartyMemberFunc([Species.GASTRODON, Species.SEISMITOAD])) + .setPartyMemberFunc(4, getRandomPartyMemberFunc([Species.GASTRODON, Species.SEISMITOAD], TrainerSlot.TRAINER, true, p => { + //Storm Drain Gastrodon, Water Absorb Seismitoad + if (p.species.speciesId === Species.GASTRODON) { + p.abilityIndex = 0; + } else if (p.species.speciesId === Species.SEISMITOAD) { + p.abilityIndex = 2; + } + })) .setPartyMemberFunc(5, getRandomPartyMemberFunc([Species.MEWTWO], TrainerSlot.TRAINER, true, p => { p.setBoss(true, 2); p.generateAndPopulateMoveset(); @@ -2153,9 +2160,23 @@ export const trainerConfigs: TrainerConfigs = { p.pokeball = PokeballType.MASTER_BALL; })), [TrainerType.GUZMA]: new TrainerConfig(++t).setName("Guzma").initForEvilTeamLeader("Skull Boss", []).setMixedBattleBgm("battle_skull_boss").setVictoryBgm("victory_team_plasma") - .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.LOKIX, Species.YANMEGA ])) + .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.LOKIX, Species.YANMEGA ], TrainerSlot.TRAINER, true, p => { + //Tinted Lens Lokix, Tinted Lens Yanmega + if (p.species.speciesId === Species.LOKIX) { + p.abilityIndex = 2; + } else if (p.species.speciesId === Species.YANMEGA) { + p.abilityIndex = 1; + } + })) .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.HERACROSS ])) - .setPartyMemberFunc(2, getRandomPartyMemberFunc([ Species.SCIZOR, Species.KLEAVOR ])) + .setPartyMemberFunc(2, getRandomPartyMemberFunc([ Species.SCIZOR, Species.KLEAVOR ], TrainerSlot.TRAINER, true, p => { + //Technician Scizor, Sharpness Kleavor + if (p.species.speciesId === Species.SCIZOR) { + p.abilityIndex = 1; + } else if (p.species.speciesId === Species.KLEAVOR) { + p.abilityIndex = 2; + } + })) .setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.GALVANTULA, Species.VIKAVOLT])) .setPartyMemberFunc(4, getRandomPartyMemberFunc([ Species.PINSIR ], TrainerSlot.TRAINER, true, p => { p.generateAndPopulateMoveset(); @@ -2175,25 +2196,32 @@ export const trainerConfigs: TrainerConfigs = { p.abilityIndex = 2; //Anticipation p.pokeball = PokeballType.ULTRA_BALL; })) - .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.HISUI_SAMUROTT, Species.CRAWDAUNT ], TrainerSlot.TRAINER, true, p => { + .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.SCIZOR, Species.KLEAVOR ], TrainerSlot.TRAINER, true, p => { + //Technician Scizor, Sharpness Kleavor + if (p.species.speciesId === Species.SCIZOR) { + p.abilityIndex = 1; + } else if (p.species.speciesId === Species.KLEAVOR) { + p.abilityIndex = 2; + } + })) + .setPartyMemberFunc(2, getRandomPartyMemberFunc([ Species.HISUI_SAMUROTT, Species.CRAWDAUNT ], TrainerSlot.TRAINER, true, p => { p.abilityIndex = 2; //Sharpness Hisui Samurott, Adaptability Crawdaunt })) - .setPartyMemberFunc(2, getRandomPartyMemberFunc([ Species.SCIZOR, Species.KLEAVOR ])) - .setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.PINSIR ], TrainerSlot.TRAINER, true, p => { + .setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.BUZZWOLE ], TrainerSlot.TRAINER, true, p => { p.generateAndPopulateMoveset(); + p.pokeball = PokeballType.ROGUE_BALL; + })) + .setPartyMemberFunc(4, getRandomPartyMemberFunc([ Species.XURKITREE ], TrainerSlot.TRAINER, true, p => { + p.setBoss(true, 2); + p.generateAndPopulateMoveset(); + p.pokeball = PokeballType.ROGUE_BALL; + })) + .setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.PINSIR ], TrainerSlot.TRAINER, true, p => { + p.setBoss(true, 2); p.formIndex = 1; + p.generateAndPopulateMoveset(); p.generateName(); p.pokeball = PokeballType.ULTRA_BALL; - })) - .setPartyMemberFunc(4, getRandomPartyMemberFunc([ Species.BUZZWOLE ], TrainerSlot.TRAINER, true, p => { - p.setBoss(true, 2); - p.generateAndPopulateMoveset(); - p.pokeball = PokeballType.ROGUE_BALL; - })) - .setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.XURKITREE ], TrainerSlot.TRAINER, true, p => { - p.setBoss(true, 2); - p.generateAndPopulateMoveset(); - p.pokeball = PokeballType.ROGUE_BALL; })), [TrainerType.ROSE]: new TrainerConfig(++t).setName("Rose").initForEvilTeamLeader("Macro Boss", []).setMixedBattleBgm("battle_macro_boss").setVictoryBgm("victory_team_plasma") .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.ARCHALUDON ])) @@ -2209,17 +2237,16 @@ export const trainerConfigs: TrainerConfigs = { p.pokeball = PokeballType.ULTRA_BALL; })), [TrainerType.ROSE_2]: new TrainerConfig(++t).setName("Rose").initForEvilTeamLeader("Macro Boss", [], true).setMixedBattleBgm("battle_macro_boss").setVictoryBgm("victory_team_plasma") - .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.MELMETAL ], TrainerSlot.TRAINER, true, p => { + .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.ARCHALUDON ], TrainerSlot.TRAINER, true, p => { p.setBoss(true, 2); p.generateAndPopulateMoveset(); - p.pokeball = PokeballType.ULTRA_BALL; })) .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.AEGISLASH, Species.GHOLDENGO ])) .setPartyMemberFunc(2, getRandomPartyMemberFunc([ Species.DRACOVISH, Species.DRACOZOLT ], TrainerSlot.TRAINER, true, p => { p.generateAndPopulateMoveset(); p.abilityIndex = 1; //Strong Jaw Dracovish, Hustle Dracozolt })) - .setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.ARCHALUDON ])) + .setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.MELMETAL ])) .setPartyMemberFunc(4, getRandomPartyMemberFunc([ Species.GALAR_ARTICUNO, Species.GALAR_ZAPDOS, Species.GALAR_MOLTRES ], TrainerSlot.TRAINER, true, p => { p.setBoss(true, 2); p.generateAndPopulateMoveset(); diff --git a/src/field/arena.ts b/src/field/arena.ts index 9897da7cfd7..dc9ad84f09d 100644 --- a/src/field/arena.ts +++ b/src/field/arena.ts @@ -33,6 +33,7 @@ export class Arena { public tags: ArenaTag[]; public bgm: string; public ignoreAbilities: boolean; + public ignoringEffectSource: BattlerIndex | null; private lastTimeOfDay: TimeOfDay; @@ -569,8 +570,9 @@ export class Arena { } } - setIgnoreAbilities(ignoreAbilities: boolean = true): void { + setIgnoreAbilities(ignoreAbilities: boolean, ignoringEffectSource: BattlerIndex | null = null): void { this.ignoreAbilities = ignoreAbilities; + this.ignoringEffectSource = ignoreAbilities ? ignoringEffectSource : null; } /** diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index e17272cd955..a8d82003ca5 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -20,7 +20,7 @@ import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "../data/tms"; import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, SubstituteTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, MoveRestrictionBattlerTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag, TarShotTag } from "../data/battler-tags"; import { WeatherType } from "../data/weather"; import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "../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 } from "../data/ability"; +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 "../data/ability"; import PokemonData from "../system/pokemon-data"; import { BattlerIndex } from "../battle"; import { Mode } from "../ui/ui"; @@ -1364,7 +1364,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (this.isFusion() && ability.hasAttr(NoFusionAbilityAbAttr)) { return false; } - if (this.scene?.arena.ignoreAbilities && ability.isIgnorable) { + const arena = this.scene?.arena; + if (arena.ignoreAbilities && arena.ignoringEffectSource !== this.getBattlerIndex() && ability.isIgnorable) { return false; } if (this.summonData?.abilitySuppressed && !ability.hasAttr(UnsuppressableAbilityAbAttr)) { @@ -3365,7 +3366,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } if (asPhase) { - this.scene.unshiftPhase(new ObtainStatusEffectPhase(this.scene, this.getBattlerIndex(), effect, cureTurn, sourceText!, sourcePokemon!)); // TODO: are these bangs correct? + this.scene.unshiftPhase(new ObtainStatusEffectPhase(this.scene, this.getBattlerIndex(), effect, cureTurn, sourceText, sourcePokemon)); return true; } @@ -3399,6 +3400,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (effect !== StatusEffect.FAINT) { this.scene.triggerPokemonFormChange(this, SpeciesFormChangeStatusEffectTrigger, true); + applyPostSetStatusAbAttrs(PostSetStatusAbAttr, this, effect, sourcePokemon); } return true; diff --git a/src/locales/de/move-trigger.json b/src/locales/de/move-trigger.json index 01b22429fb3..9b59c4b79ed 100644 --- a/src/locales/de/move-trigger.json +++ b/src/locales/de/move-trigger.json @@ -65,6 +65,7 @@ "suppressAbilities": "Die Fähigkeit von {{pokemonName}} wirkt nicht mehr!", "revivalBlessing": "{{pokemonName}} ist wieder fit und kampfbereit!", "swapArenaTags": "{{pokemonName}} hat die Effekte, die auf den beiden Seiten des Kampffeldes wirken, miteinander getauscht!", + "chillyReception": "{{pokemonName}} erzählt einen schlechten Witz, der nicht besonders gut ankommt...", "exposedMove": "{{pokemonName}} erkennt {{targetPokemonName}}!", "safeguard": "{{targetName}} wird durch Bodyguard geschützt!", "afterYou": "{{targetName}} lässt sich auf Galanterie ein!" diff --git a/src/locales/en/move-trigger.json b/src/locales/en/move-trigger.json index bc58e2878dd..93d25e506ba 100644 --- a/src/locales/en/move-trigger.json +++ b/src/locales/en/move-trigger.json @@ -66,6 +66,7 @@ "suppressAbilities": "{{pokemonName}}'s ability\nwas suppressed!", "revivalBlessing": "{{pokemonName}} was revived!", "swapArenaTags": "{{pokemonName}} swapped the battle effects affecting each side of the field!", + "chillyReception": "{{pokemonName}} is preparing to tell a chillingly bad joke!", "exposedMove": "{{pokemonName}} identified\n{{targetPokemonName}}!", "safeguard": "{{targetName}} is protected by Safeguard!", "substituteOnOverlap": "{{pokemonName}} already\nhas a substitute!", diff --git a/src/locales/fr/move-trigger.json b/src/locales/fr/move-trigger.json index 6f9d9d4dd63..7564718e7ce 100644 --- a/src/locales/fr/move-trigger.json +++ b/src/locales/fr/move-trigger.json @@ -66,6 +66,7 @@ "suppressAbilities": "Le talent de {{pokemonName}}\na été rendu inactif !", "revivalBlessing": "{{pokemonName}} a repris connaissance\net est prêt à se battre de nouveau !", "swapArenaTags": "Les effets affectant chaque côté du terrain\nont été échangés par {{pokemonName}} !", + "chillyReception": "{{pokemonName}} s’apprête\nà faire un mauvais jeu de mots…", "exposedMove": "{{targetPokemonName}} est identifié\npar {{pokemonName}} !", "safeguard": "{{targetName}} est protégé\npar la capacité Rune Protect !", "substituteOnOverlap": "{{pokemonName}} a déjà\nun clone !", diff --git a/src/locales/it/move-trigger.json b/src/locales/it/move-trigger.json index c8fb390e53f..fba671a6813 100644 --- a/src/locales/it/move-trigger.json +++ b/src/locales/it/move-trigger.json @@ -66,6 +66,7 @@ "revivalBlessing": "{{pokemonName}} torna in forze!", "swapArenaTags": "{{pokemonName}} ha invertito gli effetti attivi\nnelle due metà del campo!", "exposedMove": "{{pokemonName}} ha identificato\n{{targetPokemonName}}!", + "chillyReception": "{{pokemonName}} sta per fare una battuta!", "safeguard": "Salvaguardia protegge {{targetName}}!", "afterYou": "{{pokemonName}} approfitta della cortesia!" } diff --git a/src/locales/ja/move-trigger.json b/src/locales/ja/move-trigger.json index fbefe883836..afede7edfb3 100644 --- a/src/locales/ja/move-trigger.json +++ b/src/locales/ja/move-trigger.json @@ -64,6 +64,8 @@ "copyType": "{{pokemonName}}は {{targetPokemonName}}と\n同じタイプに なった!", "suppressAbilities": "{{pokemonName}}の 特性が 効かなくなった!", "revivalBlessing": "{{pokemonName}}は\n復活して 戦えるようになった!", + "swapArenaTags": "{{pokemonName}}は\nお互いの 場の効果を 入れ替えた!", + "chillyReception": "{{pokemonName}}は\n寒い ギャグを かました!", "swapArenaTags": "{{pokemonName}}は\nお互いの 場の 効果を 入れ替えた!", "exposedMove": "{{pokemonName}}は {{targetPokemonName}}の\n正体を 見破った!", "afterYou": "{{pokemonName}}は\nお言葉に 甘えることにした!" diff --git a/src/locales/ko/move-trigger.json b/src/locales/ko/move-trigger.json index a8a6c0cf86f..12a126baf9d 100644 --- a/src/locales/ko/move-trigger.json +++ b/src/locales/ko/move-trigger.json @@ -66,6 +66,7 @@ "suppressAbilities": "{{pokemonName}}의\n특성이 효과를 발휘하지 못하게 되었다!", "revivalBlessing": "{{pokemonName}}[[는]]\n정신을 차려 싸울 수 있게 되었다!", "swapArenaTags": "{{pokemonName}}[[는]]\n서로의 필드 효과를 교체했다!", + "chillyReception": "{{pokemonName}}[[는]] 썰렁한 개그를 선보였다!", "exposedMove": "{{pokemonName}}[[는]]\n{{targetPokemonName}}의 정체를 꿰뚫어 보았다!", "safeguard": "{{targetName}}[[는]] 신비의 베일이 지켜 주고 있다!", "afterYou": "{{pokemonName}}[[는]]\n배려를 받아들이기로 했다!" diff --git a/src/locales/pt_BR/move-trigger.json b/src/locales/pt_BR/move-trigger.json index a2ffa6500b4..307364e1b55 100644 --- a/src/locales/pt_BR/move-trigger.json +++ b/src/locales/pt_BR/move-trigger.json @@ -61,6 +61,7 @@ "suppressAbilities": "A habilidade de {{pokemonName}}\nfoi suprimida!", "revivalBlessing": "{{pokemonName}} foi reanimado!", "swapArenaTags": "{{pokemonName}} trocou os efeitos de batalha que afetam cada lado do campo!", + "chillyReception": "{{pokemonName}} está prestes a contar uma piada gelada!", "exposedMove": "{{pokemonName}} identificou\n{{targetPokemonName}}!", "safeguard": "{{targetName}} está protegido por Safeguard!", "afterYou": "{{pokemonName}} aceitou a gentil oferta!" diff --git a/src/locales/zh_CN/move-trigger.json b/src/locales/zh_CN/move-trigger.json index 436f1805c4e..60de3591915 100644 --- a/src/locales/zh_CN/move-trigger.json +++ b/src/locales/zh_CN/move-trigger.json @@ -65,6 +65,7 @@ "suppressAbilities": "{{pokemonName}}的特性\n变得无效了!", "revivalBlessing": "{{pokemonName}}复活了!", "swapArenaTags": "{{pokemonName}}\n交换了双方的场地效果!", + "chillyReception": "{{pokemonName}}\n说出了冷笑话!", "exposedMove": "{{pokemonName}}识破了\n{{targetPokemonName}}的原型!", "safeguard": "{{targetName}}\n正受到神秘之幕的保护!", "afterYou": "{{pokemonName}}\n接受了对手的好意!" diff --git a/src/locales/zh_TW/move-trigger.json b/src/locales/zh_TW/move-trigger.json index db88f6df57f..2cd33a3a416 100644 --- a/src/locales/zh_TW/move-trigger.json +++ b/src/locales/zh_TW/move-trigger.json @@ -65,6 +65,7 @@ "suppressAbilities": "{{pokemonName}}的特性\n變得無效了!", "revivalBlessing": "{{pokemonName}}復活了!", "swapArenaTags": "{{pokemonName}}\n交換了雙方的場地效果!", + "chillyReception": "{{pokemonName}}\n說了冷笑話!", "exposedMove": "{{pokemonName}}識破了\n{{targetPokemonName}}的原形!", "safeguard": "{{targetName}}\n正受到神秘之幕的保護!", "afterYou": "{{pokemonName}}\n接受了對手的好意!" diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index e63096360dd..0a75c32bac3 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -74,7 +74,7 @@ export class MovePhase extends BattlePhase { if (!this.followUp) { if (this.move.getMove().checkFlag(MoveFlags.IGNORE_ABILITIES, this.pokemon, null)) { - this.scene.arena.setIgnoreAbilities(); + this.scene.arena.setIgnoreAbilities(true, this.pokemon.getBattlerIndex()); } } else { this.pokemon.turnData.hitsLeft = 0; // TODO: is `0` correct? diff --git a/src/phases/obtain-status-effect-phase.ts b/src/phases/obtain-status-effect-phase.ts index 93bf4cd41d5..bf38c432394 100644 --- a/src/phases/obtain-status-effect-phase.ts +++ b/src/phases/obtain-status-effect-phase.ts @@ -9,26 +9,26 @@ import { PokemonPhase } from "./pokemon-phase"; import { PostTurnStatusEffectPhase } from "./post-turn-status-effect-phase"; export class ObtainStatusEffectPhase extends PokemonPhase { - private statusEffect: StatusEffect | undefined; - private cureTurn: integer | null; - private sourceText: string | null; - private sourcePokemon: Pokemon | null; + private statusEffect?: StatusEffect | undefined; + private cureTurn?: integer | null; + private sourceText?: string | null; + private sourcePokemon?: Pokemon | null; - constructor(scene: BattleScene, battlerIndex: BattlerIndex, statusEffect?: StatusEffect, cureTurn?: integer | null, sourceText?: string, sourcePokemon?: Pokemon) { + constructor(scene: BattleScene, battlerIndex: BattlerIndex, statusEffect?: StatusEffect, cureTurn?: integer | null, sourceText?: string | null, sourcePokemon?: Pokemon | null) { super(scene, battlerIndex); this.statusEffect = statusEffect; - this.cureTurn = cureTurn!; // TODO: is this bang correct? - this.sourceText = sourceText!; // TODO: is this bang correct? - this.sourcePokemon = sourcePokemon!; // For tracking which Pokemon caused the status effect // TODO: is this bang correct? + this.cureTurn = cureTurn; + this.sourceText = sourceText; + this.sourcePokemon = sourcePokemon; // For tracking which Pokemon caused the status effect } start() { const pokemon = this.getPokemon(); - if (!pokemon?.status) { - if (pokemon?.trySetStatus(this.statusEffect, false, this.sourcePokemon)) { + if (pokemon && !pokemon.status) { + if (pokemon.trySetStatus(this.statusEffect, false, this.sourcePokemon)) { if (this.cureTurn) { - pokemon.status!.cureTurn = this.cureTurn; // TODO: is this bang correct? + pokemon.status!.cureTurn = this.cureTurn; // TODO: is this bang correct? } pokemon.updateInfo(true); new CommonBattleAnim(CommonAnim.POISON + (this.statusEffect! - 1), pokemon).play(this.scene, false, () => { @@ -40,8 +40,8 @@ export class ObtainStatusEffectPhase extends PokemonPhase { }); return; } - } else if (pokemon.status.effect === this.statusEffect) { - this.scene.queueMessage(getStatusEffectOverlapText(this.statusEffect, getPokemonNameWithAffix(pokemon))); + } else if (pokemon.status?.effect === this.statusEffect) { + this.scene.queueMessage(getStatusEffectOverlapText(this.statusEffect ?? StatusEffect.NONE, getPokemonNameWithAffix(pokemon))); } this.end(); } diff --git a/src/test/abilities/synchronize.test.ts b/src/test/abilities/synchronize.test.ts new file mode 100644 index 00000000000..6e0aa46763f --- /dev/null +++ b/src/test/abilities/synchronize.test.ts @@ -0,0 +1,109 @@ +import { StatusEffect } from "#app/data/status-effect"; +import GameManager from "#app/test/utils/gameManager"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Abilities - Synchronize", () => { + 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") + .startingLevel(100) + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.SYNCHRONIZE) + .moveset([Moves.SPLASH, Moves.THUNDER_WAVE, Moves.SPORE, Moves.PSYCHO_SHIFT]) + .ability(Abilities.NO_GUARD); + }, 20000); + + it("does not trigger when no status is applied by opponent Pokemon", async () => { + await game.classicMode.startBattle([Species.FEEBAS]); + + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getParty()[0].status).toBeUndefined(); + expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase"); + }, 20000); + + it("sets the status of the source pokemon to Paralysis when paralyzed by it", async () => { + await game.classicMode.startBattle([Species.FEEBAS]); + + game.move.select(Moves.THUNDER_WAVE); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getParty()[0].status?.effect).toBe(StatusEffect.PARALYSIS); + expect(game.scene.getEnemyParty()[0].status?.effect).toBe(StatusEffect.PARALYSIS); + expect(game.phaseInterceptor.log).toContain("ShowAbilityPhase"); + }, 20000); + + it("does not trigger on Sleep", async () => { + await game.classicMode.startBattle(); + + game.move.select(Moves.SPORE); + + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getParty()[0].status?.effect).toBeUndefined(); + expect(game.scene.getEnemyParty()[0].status?.effect).toBe(StatusEffect.SLEEP); + expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase"); + }, 20000); + + it("does not trigger when Pokemon is statused by Toxic Spikes", async () => { + game.override + .ability(Abilities.SYNCHRONIZE) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Array(4).fill(Moves.TOXIC_SPIKES)); + + await game.classicMode.startBattle([Species.FEEBAS, Species.MILOTIC]); + + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + + game.doSwitchPokemon(1); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getParty()[0].status?.effect).toBe(StatusEffect.POISON); + expect(game.scene.getEnemyParty()[0].status?.effect).toBeUndefined(); + expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase"); + }, 20000); + + it("shows ability even if it fails to set the status of the opponent Pokemon", async () => { + await game.classicMode.startBattle([Species.PIKACHU]); + + game.move.select(Moves.THUNDER_WAVE); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getParty()[0].status?.effect).toBeUndefined(); + expect(game.scene.getEnemyParty()[0].status?.effect).toBe(StatusEffect.PARALYSIS); + expect(game.phaseInterceptor.log).toContain("ShowAbilityPhase"); + }, 20000); + + it("should activate with Psycho Shift after the move clears the status", async () => { + game.override.statusEffect(StatusEffect.PARALYSIS); + await game.classicMode.startBattle(); + + game.move.select(Moves.PSYCHO_SHIFT); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getParty()[0].status?.effect).toBe(StatusEffect.PARALYSIS); // keeping old gen < V impl for now since it's buggy otherwise + expect(game.scene.getEnemyParty()[0].status?.effect).toBe(StatusEffect.PARALYSIS); + expect(game.phaseInterceptor.log).toContain("ShowAbilityPhase"); + }, 20000); +}); diff --git a/src/test/moves/chilly_reception.test.ts b/src/test/moves/chilly_reception.test.ts new file mode 100644 index 00000000000..969c1b97192 --- /dev/null +++ b/src/test/moves/chilly_reception.test.ts @@ -0,0 +1,71 @@ +import { Abilities } from "#app/enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { WeatherType } from "#enums/weather-type"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +const TIMEOUT = 20 * 1000; + +describe("Moves - Chilly Reception", () => { + 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") + .moveset([Moves.CHILLY_RECEPTION, Moves.SNOWSCAPE]) + .enemyMoveset(Array(4).fill(Moves.SPLASH)) + .enemyAbility(Abilities.NONE) + .ability(Abilities.NONE); + + }); + + it("should still change the weather if user can't switch out", async () => { + await game.classicMode.startBattle([Species.SLOWKING]); + + game.move.select(Moves.CHILLY_RECEPTION); + + await game.phaseInterceptor.to("BerryPhase", false); + expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW); + }, TIMEOUT); + + it("should switch out even if it's snowing", async () => { + await game.classicMode.startBattle([Species.SLOWKING, Species.MEOWTH]); + // first turn set up snow with snowscape, try chilly reception on second turn + game.move.select(Moves.SNOWSCAPE); + await game.phaseInterceptor.to("BerryPhase", false); + expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW); + + await game.phaseInterceptor.to("TurnInitPhase", false); + game.move.select(Moves.CHILLY_RECEPTION); + game.doSelectPartyPokemon(1); + + await game.phaseInterceptor.to("BerryPhase", false); + expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW); + expect(game.scene.getPlayerField()[0].species.speciesId).toBe(Species.MEOWTH); + }, TIMEOUT); + + it("happy case - switch out and weather changes", async () => { + + await game.classicMode.startBattle([Species.SLOWKING, Species.MEOWTH]); + + game.move.select(Moves.CHILLY_RECEPTION); + game.doSelectPartyPokemon(1); + + await game.phaseInterceptor.to("BerryPhase", false); + expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW); + expect(game.scene.getPlayerField()[0].species.speciesId).toBe(Species.MEOWTH); + }, TIMEOUT); +}); diff --git a/src/ui/challenges-select-ui-handler.ts b/src/ui/challenges-select-ui-handler.ts index e08736d2b70..924186de789 100644 --- a/src/ui/challenges-select-ui-handler.ts +++ b/src/ui/challenges-select-ui-handler.ts @@ -28,7 +28,7 @@ export default class GameChallengesUiHandler extends UiHandler { private descriptionText: BBCodeText; - private challengeLabels: Array<{ label: Phaser.GameObjects.Text, value: Phaser.GameObjects.Text }>; + private challengeLabels: Array<{ label: Phaser.GameObjects.Text, value: Phaser.GameObjects.Text, leftArrow: Phaser.GameObjects.Image, rightArrow: Phaser.GameObjects.Image }>; private monoTypeValue: Phaser.GameObjects.Sprite; private cursorObj: Phaser.GameObjects.NineSlice | null; @@ -40,6 +40,11 @@ export default class GameChallengesUiHandler extends UiHandler { private optionsWidth: number; + private widestTextBox: number; + + private readonly leftArrowGap: number = 90; // distance from the label to the left arrow + private readonly arrowSpacing: number = 3; // distance between the arrows and the value area + constructor(scene: BattleScene, mode: Mode | null = null) { super(scene, mode); } @@ -47,6 +52,8 @@ export default class GameChallengesUiHandler extends UiHandler { setup() { const ui = this.getUi(); + this.widestTextBox = 0; + this.challengesContainer = this.scene.add.container(1, -(this.scene.game.canvas.height / 6) + 1); this.challengesContainer.setName("challenges"); @@ -135,6 +142,20 @@ export default class GameChallengesUiHandler extends UiHandler { this.valuesContainer.add(label); + const leftArrow = this.scene.add.image(0, 0, "cursor_reverse"); + leftArrow.setName(`challenge-left-arrow-${i}`); + leftArrow.setOrigin(0, 0); + leftArrow.setVisible(false); + leftArrow.setScale(0.75); + this.valuesContainer.add(leftArrow); + + const rightArrow = this.scene.add.image(0, 0, "cursor"); + rightArrow.setName(`challenge-right-arrow-${i}`); + rightArrow.setOrigin(0, 0); + rightArrow.setScale(0.75); + rightArrow.setVisible(false); + this.valuesContainer.add(rightArrow); + const value = addTextObject(this.scene, 0, 28 + i * 16, "", TextStyle.SETTINGS_LABEL); value.setName(`challenge-value-text-${i}`); value.setPositionRelative(label, 100, 0); @@ -142,7 +163,9 @@ export default class GameChallengesUiHandler extends UiHandler { this.challengeLabels[i] = { label: label, - value: value + value: value, + leftArrow: leftArrow, + rightArrow: rightArrow }; } @@ -187,10 +210,26 @@ export default class GameChallengesUiHandler extends UiHandler { */ initLabels(): void { this.setDescription(this.scene.gameMode.challenges[0].getDescription()); + this.widestTextBox = 0; for (let i = 0; i < 9; i++) { if (i < this.scene.gameMode.challenges.length) { this.challengeLabels[i].label.setVisible(true); this.challengeLabels[i].value.setVisible(true); + this.challengeLabels[i].leftArrow.setVisible(true); + this.challengeLabels[i].rightArrow.setVisible(true); + + const tempText = addTextObject(this.scene, 0, 0, "", TextStyle.SETTINGS_LABEL); // this is added here to get the widest text object for this language, which will be used for the arrow placement + + for (let j = 0; j <= this.scene.gameMode.challenges[i].maxValue; j++) { // this goes through each challenge's value to find out what the max width will be + if (this.scene.gameMode.challenges[i].id !== Challenges.SINGLE_TYPE) { + tempText.setText(this.scene.gameMode.challenges[i].getValue(j)); + if (tempText.displayWidth > this.widestTextBox) { + this.widestTextBox = tempText.displayWidth; + } + } + } + + tempText.destroy(); } } } @@ -203,16 +242,33 @@ export default class GameChallengesUiHandler extends UiHandler { let monoTypeVisible = false; for (let i = 0; i < Math.min(9, this.scene.gameMode.challenges.length); i++) { const challenge = this.scene.gameMode.challenges[this.scrollCursor + i]; - this.challengeLabels[i].label.setText(challenge.getName()); + const challengeLabel = this.challengeLabels[i]; + challengeLabel.label.setText(challenge.getName()); + challengeLabel.leftArrow.setPositionRelative(challengeLabel.label, this.leftArrowGap, 4.5); + challengeLabel.leftArrow.setVisible(challenge.value !== 0); + challengeLabel.rightArrow.setPositionRelative(challengeLabel.leftArrow, Math.max(this.monoTypeValue.width, this.widestTextBox) + challengeLabel.leftArrow.displayWidth + 2 * this.arrowSpacing, 0); + challengeLabel.rightArrow.setVisible(challenge.value !== challenge.maxValue); + + // this check looks to make sure that the arrows and value textbox don't take up too much space that they'll clip the right edge of the options background + if (challengeLabel.rightArrow.x + challengeLabel.rightArrow.width + this.optionsBg.rightWidth + this.arrowSpacing > this.optionsWidth) { + // if we go out of bounds of the box, set the x position as far right as we can without going past the box, with this.arrowSpacing to allow a small gap between the arrow and border + challengeLabel.rightArrow.setX(this.optionsWidth - this.arrowSpacing - this.optionsBg.rightWidth); + } + + // this line of code gets the center point between the left and right arrows from their left side (Arrow.x gives middle point), taking into account the width of the arrows + const xLocation = Math.round((challengeLabel.leftArrow.x + challengeLabel.rightArrow.x + challengeLabel.leftArrow.displayWidth) / 2); if (challenge.id === Challenges.SINGLE_TYPE) { - this.monoTypeValue.setPositionRelative(this.challengeLabels[i].label, 113, 8); + this.monoTypeValue.setX(xLocation); + this.monoTypeValue.setY(challengeLabel.label.y + 8); this.monoTypeValue.setFrame(challenge.getValue()); this.monoTypeValue.setVisible(true); - this.challengeLabels[i].value.setVisible(false); + challengeLabel.value.setVisible(false); monoTypeVisible = true; } else { - this.challengeLabels[i].value.setText(challenge.getValue()); - this.challengeLabels[i].value.setVisible(true); + challengeLabel.value.setText(challenge.getValue()); + challengeLabel.value.setX(xLocation); + challengeLabel.value.setOrigin(0.5, 0); + challengeLabel.value.setVisible(true); } } if (!monoTypeVisible) { @@ -244,6 +300,7 @@ export default class GameChallengesUiHandler extends UiHandler { super.show(args); this.startCursor.setVisible(false); + this.updateChallengeArrows(false); this.challengesContainer.setVisible(true); // Should always be false at the start this.hasSelectedChallenge = this.scene.gameMode.challenges.some(c => c.value !== 0); @@ -259,6 +316,21 @@ export default class GameChallengesUiHandler extends UiHandler { return true; } + /* This code updates the challenge starter arrows to be tinted/not tinted when the start button is selected to show they can't be changed + */ + updateChallengeArrows(tinted: boolean) { + for (let i = 0; i < Math.min(9, this.scene.gameMode.challenges.length); i++) { + const challengeLabel = this.challengeLabels[i]; + if (tinted) { + challengeLabel.leftArrow.setTint(0x808080); + challengeLabel.rightArrow.setTint(0x808080); + } else { + challengeLabel.leftArrow.clearTint(); + challengeLabel.rightArrow.clearTint(); + } + } + } + /** * Processes input from a specified button. * This method handles navigation through a UI menu, including movement through menu items @@ -280,6 +352,7 @@ export default class GameChallengesUiHandler extends UiHandler { // If the user presses cancel when the start cursor has been activated, the game deactivates the start cursor and allows typical challenge selection behavior this.startCursor.setVisible(false); this.cursorObj?.setVisible(true); + this.updateChallengeArrows(this.startCursor.visible); } else { this.scene.clearPhaseQueue(); this.scene.pushPhase(new TitlePhase(this.scene)); @@ -294,6 +367,7 @@ export default class GameChallengesUiHandler extends UiHandler { } else { this.startCursor.setVisible(true); this.cursorObj?.setVisible(false); + this.updateChallengeArrows(this.startCursor.visible); } success = true; } else {