Merge branch 'beta' into mystery-encounters-translations

This commit is contained in:
Lugiad 2024-09-23 12:08:54 +02:00 committed by GitHub
commit 414456b472
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 456 additions and 96 deletions

62
src/data/ability.ts Executable file → Normal file
View File

@ -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<boolean> {
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>([
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<boolean> {
return false;
@ -4677,6 +4732,10 @@ export function applyStatMultiplierAbAttrs(attrType: Constructor<StatMultiplierA
pokemon: Pokemon, stat: BattleStat, statValue: Utils.NumberHolder, simulated: boolean = false, ...args: any[]): Promise<void> {
return applyAbAttrsInternal<StatMultiplierAbAttr>(attrType, pokemon, (attr, passive) => attr.applyStatStage(pokemon, passive, simulated, stat, statValue, args), args);
}
export function applyPostSetStatusAbAttrs(attrType: Constructor<PostSetStatusAbAttr>,
pokemon: Pokemon, effect: StatusEffect, sourcePokemon?: Pokemon | null, simulated: boolean = false, ...args: any[]): Promise<void> {
return applyAbAttrsInternal<PostSetStatusAbAttr>(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(),

View File

@ -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;
}
}

View File

@ -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}`) });
}

View File

@ -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<boolean> {
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)

View File

@ -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();

View File

@ -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;
}
/**

View File

@ -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;

View File

@ -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!"

View File

@ -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!",

View File

@ -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}} sapprê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 !",

View File

@ -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!"
}

View File

@ -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お言葉に 甘えることにした"

View File

@ -66,6 +66,7 @@
"suppressAbilities": "{{pokemonName}}의\n특성이 효과를 발휘하지 못하게 되었다!",
"revivalBlessing": "{{pokemonName}}[[는]]\n정신을 차려 싸울 수 있게 되었다!",
"swapArenaTags": "{{pokemonName}}[[는]]\n서로의 필드 효과를 교체했다!",
"chillyReception": "{{pokemonName}}[[는]] 썰렁한 개그를 선보였다!",
"exposedMove": "{{pokemonName}}[[는]]\n{{targetPokemonName}}의 정체를 꿰뚫어 보았다!",
"safeguard": "{{targetName}}[[는]] 신비의 베일이 지켜 주고 있다!",
"afterYou": "{{pokemonName}}[[는]]\n배려를 받아들이기로 했다!"

View File

@ -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!"

View File

@ -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接受了对手的好意"

View File

@ -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接受了對手的好意"

View File

@ -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?

View File

@ -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();
}

View File

@ -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);
});

View File

@ -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);
});

View File

@ -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 {