[Ability] Implement Forecast (#3534)
* initial forecast implementation * updates * bug fixes and add tests * bug fixes * update docs * fix issues post-merge * add show ability * add support for simulated abilities * add simulated conditions and fix tests * fix simulated conditions
This commit is contained in:
parent
19b7ebe94c
commit
03de6cfe36
|
@ -13,7 +13,7 @@ import { ArenaTagSide, ArenaTrapTag } from "./arena-tag";
|
|||
import { Stat, getStatName } from "./pokemon-stat";
|
||||
import { BerryModifier, PokemonHeldItemModifier } from "../modifier/modifier";
|
||||
import { TerrainType } from "./terrain";
|
||||
import { SpeciesFormChangeManualTrigger } from "./pokemon-forms";
|
||||
import { SpeciesFormChangeManualTrigger, SpeciesFormChangeRevertWeatherFormTrigger, SpeciesFormChangeWeatherTrigger } from "./pokemon-forms";
|
||||
import i18next from "i18next";
|
||||
import { Localizable } from "#app/interfaces/locales.js";
|
||||
import { Command } from "../ui/command-ui-handler";
|
||||
|
@ -25,10 +25,11 @@ import { ArenaTagType } from "#enums/arena-tag-type";
|
|||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
import { MovePhase } from "#app/phases/move-phase.js";
|
||||
import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase.js";
|
||||
import { ShowAbilityPhase } from "#app/phases/show-ability-phase.js";
|
||||
import { StatChangePhase } from "#app/phases/stat-change-phase.js";
|
||||
import { MovePhase } from "#app/phases/move-phase";
|
||||
import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase";
|
||||
import { ShowAbilityPhase } from "#app/phases/show-ability-phase";
|
||||
import { StatChangePhase } from "#app/phases/stat-change-phase";
|
||||
import BattleScene from "#app/battle-scene";
|
||||
|
||||
export class Ability implements Localizable {
|
||||
public id: Abilities;
|
||||
|
@ -2361,6 +2362,73 @@ export class PostSummonTransformAbAttr extends PostSummonAbAttr {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverts weather-based forms to their normal forms when the user is summoned.
|
||||
* Used by Cloud Nine and Air Lock.
|
||||
* @extends PostSummonAbAttr
|
||||
*/
|
||||
export class PostSummonWeatherSuppressedFormChangeAbAttr extends PostSummonAbAttr {
|
||||
/**
|
||||
* Triggers {@linkcode Arena.triggerWeatherBasedFormChangesToNormal | triggerWeatherBasedFormChangesToNormal}
|
||||
* @param {Pokemon} pokemon the Pokemon with this ability
|
||||
* @param passive n/a
|
||||
* @param args n/a
|
||||
* @returns whether a Pokemon was reverted to its normal form
|
||||
*/
|
||||
applyPostSummon(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]) {
|
||||
const pokemonToTransform = getPokemonWithWeatherBasedForms(pokemon.scene);
|
||||
|
||||
if (pokemonToTransform.length < 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!simulated) {
|
||||
pokemon.scene.arena.triggerWeatherBasedFormChangesToNormal();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers weather-based form change when summoned into an active weather.
|
||||
* Used by Forecast.
|
||||
* @extends PostSummonAbAttr
|
||||
*/
|
||||
export class PostSummonFormChangeByWeatherAbAttr extends PostSummonAbAttr {
|
||||
private ability: Abilities;
|
||||
|
||||
constructor(ability: Abilities) {
|
||||
super(false);
|
||||
|
||||
this.ability = ability;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the {@linkcode BattleScene.triggerPokemonFormChange | triggerPokemonFormChange} for both
|
||||
* {@linkcode SpeciesFormChange.SpeciesFormChangeWeatherTrigger | SpeciesFormChangeWeatherTrigger} and
|
||||
* {@linkcode SpeciesFormChange.SpeciesFormChangeWeatherTrigger | SpeciesFormChangeRevertWeatherFormTrigger} if it
|
||||
* is the specific Pokemon and ability
|
||||
* @param {Pokemon} pokemon the Pokemon with this ability
|
||||
* @param passive n/a
|
||||
* @param args n/a
|
||||
* @returns whether the form change was triggered
|
||||
*/
|
||||
applyPostSummon(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean {
|
||||
if (pokemon.species.speciesId === Species.CASTFORM && this.ability === Abilities.FORECAST) {
|
||||
if (simulated) {
|
||||
return simulated;
|
||||
}
|
||||
|
||||
pokemon.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeWeatherTrigger);
|
||||
pokemon.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeRevertWeatherFormTrigger);
|
||||
queueShowAbility(pokemon, passive);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export class PreSwitchOutAbAttr extends AbAttr {
|
||||
constructor() {
|
||||
super(true);
|
||||
|
@ -3014,6 +3082,49 @@ export class PostWeatherChangeAbAttr extends AbAttr {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers weather-based form change when weather changes.
|
||||
* Used by Forecast.
|
||||
* @extends PostWeatherChangeAbAttr
|
||||
*/
|
||||
export class PostWeatherChangeFormChangeAbAttr extends PostWeatherChangeAbAttr {
|
||||
private ability: Abilities;
|
||||
|
||||
constructor(ability: Abilities) {
|
||||
super(false);
|
||||
|
||||
this.ability = ability;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls {@linkcode Arena.triggerWeatherBasedFormChangesToNormal | triggerWeatherBasedFormChangesToNormal} when the
|
||||
* weather changed to form-reverting weather, otherwise calls {@linkcode Arena.triggerWeatherBasedFormChanges | triggerWeatherBasedFormChanges}
|
||||
* @param {Pokemon} pokemon the Pokemon that changed the weather
|
||||
* @param passive n/a
|
||||
* @param weather n/a
|
||||
* @param args n/a
|
||||
* @returns whether the form change was triggered
|
||||
*/
|
||||
applyPostWeatherChange(pokemon: Pokemon, passive: boolean, simulated: boolean, weather: WeatherType, args: any[]): boolean {
|
||||
if (pokemon.species.speciesId === Species.CASTFORM && this.ability === Abilities.FORECAST) {
|
||||
if (simulated) {
|
||||
return simulated;
|
||||
}
|
||||
|
||||
const formRevertingWeathers: WeatherType[] = [ WeatherType.NONE, WeatherType.SANDSTORM, WeatherType.STRONG_WINDS, WeatherType.FOG ];
|
||||
const weatherType = pokemon.scene.arena.weather?.weatherType;
|
||||
|
||||
if (weatherType && formRevertingWeathers.includes(weatherType)) {
|
||||
pokemon.scene.arena.triggerWeatherBasedFormChangesToNormal();
|
||||
} else {
|
||||
pokemon.scene.arena.triggerWeatherBasedFormChanges();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export class PostWeatherChangeAddBattlerTagAttr extends PostWeatherChangeAbAttr {
|
||||
private tagType: BattlerTagType;
|
||||
private turnCount: integer;
|
||||
|
@ -3784,6 +3895,38 @@ export class PostFaintAbAttr extends AbAttr {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for weather suppressing abilities to trigger weather-based form changes upon being fainted.
|
||||
* Used by Cloud Nine and Air Lock.
|
||||
* @extends PostFaintAbAttr
|
||||
*/
|
||||
export class PostFaintUnsuppressedWeatherFormChangeAbAttr extends PostFaintAbAttr {
|
||||
/**
|
||||
* Triggers {@linkcode Arena.triggerWeatherBasedFormChanges | triggerWeatherBasedFormChanges}
|
||||
* when the user of the ability faints
|
||||
* @param {Pokemon} pokemon the fainted Pokemon
|
||||
* @param passive n/a
|
||||
* @param attacker n/a
|
||||
* @param move n/a
|
||||
* @param hitResult n/a
|
||||
* @param args n/a
|
||||
* @returns whether the form change was triggered
|
||||
*/
|
||||
applyPostFaint(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean {
|
||||
const pokemonToTransform = getPokemonWithWeatherBasedForms(pokemon.scene);
|
||||
|
||||
if (pokemonToTransform.length < 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!simulated) {
|
||||
pokemon.scene.arena.triggerWeatherBasedFormChanges();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears Desolate Land/Primordial Sea/Delta Stream upon the Pokemon fainting
|
||||
*/
|
||||
|
@ -4559,6 +4702,16 @@ function setAbilityRevealed(pokemon: Pokemon): void {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Pokemon with weather-based forms
|
||||
* @param {BattleScene} scene - The current scene
|
||||
*/
|
||||
function getPokemonWithWeatherBasedForms(scene: BattleScene) {
|
||||
return scene.getField(true).filter(p =>
|
||||
p.hasAbility(Abilities.FORECAST) && p.species.speciesId === Species.CASTFORM
|
||||
);
|
||||
}
|
||||
|
||||
export const allAbilities = [ new Ability(Abilities.NONE, 3) ];
|
||||
|
||||
export function initAbilities() {
|
||||
|
@ -4605,7 +4758,10 @@ export function initAbilities() {
|
|||
.ignorable(),
|
||||
new Ability(Abilities.CLOUD_NINE, 3)
|
||||
.attr(SuppressWeatherEffectAbAttr, true)
|
||||
.attr(PostSummonUnnamedMessageAbAttr, i18next.t("abilityTriggers:weatherEffectDisappeared")),
|
||||
.attr(PostSummonUnnamedMessageAbAttr, i18next.t("abilityTriggers:weatherEffectDisappeared"))
|
||||
.attr(PostSummonWeatherSuppressedFormChangeAbAttr)
|
||||
.attr(PostFaintUnsuppressedWeatherFormChangeAbAttr)
|
||||
.bypassFaint(),
|
||||
new Ability(Abilities.COMPOUND_EYES, 3)
|
||||
.attr(BattleStatMultiplierAbAttr, BattleStat.ACC, 1.3),
|
||||
new Ability(Abilities.INSOMNIA, 3)
|
||||
|
@ -4750,7 +4906,8 @@ export function initAbilities() {
|
|||
new Ability(Abilities.FORECAST, 3)
|
||||
.attr(UncopiableAbilityAbAttr)
|
||||
.attr(NoFusionAbilityAbAttr)
|
||||
.unimplemented(),
|
||||
.attr(PostSummonFormChangeByWeatherAbAttr, Abilities.FORECAST)
|
||||
.attr(PostWeatherChangeFormChangeAbAttr, Abilities.FORECAST),
|
||||
new Ability(Abilities.STICKY_HOLD, 3)
|
||||
.attr(BlockItemTheftAbAttr)
|
||||
.bypassFaint()
|
||||
|
@ -4800,7 +4957,10 @@ export function initAbilities() {
|
|||
.ignorable(),
|
||||
new Ability(Abilities.AIR_LOCK, 3)
|
||||
.attr(SuppressWeatherEffectAbAttr, true)
|
||||
.attr(PostSummonUnnamedMessageAbAttr, i18next.t("abilityTriggers:weatherEffectDisappeared")),
|
||||
.attr(PostSummonUnnamedMessageAbAttr, i18next.t("abilityTriggers:weatherEffectDisappeared"))
|
||||
.attr(PostSummonWeatherSuppressedFormChangeAbAttr)
|
||||
.attr(PostFaintUnsuppressedWeatherFormChangeAbAttr)
|
||||
.bypassFaint(),
|
||||
new Ability(Abilities.TANGLED_FEET, 4)
|
||||
.conditionalAttr(pokemon => !!pokemon.getTag(BattlerTagType.CONFUSED), BattleStatMultiplierAbAttr, BattleStat.EVA, 2)
|
||||
.ignorable(),
|
||||
|
|
|
@ -26,16 +26,17 @@ import { BattlerTagType } from "#enums/battler-tag-type";
|
|||
import { Biome } from "#enums/biome";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
import { MoveUsedEvent } from "#app/events/battle-scene.js";
|
||||
import { PartyStatusCurePhase } from "#app/phases/party-status-cure-phase.js";
|
||||
import { BattleEndPhase } from "#app/phases/battle-end-phase.js";
|
||||
import { MoveEndPhase } from "#app/phases/move-end-phase.js";
|
||||
import { MovePhase } from "#app/phases/move-phase.js";
|
||||
import { NewBattlePhase } from "#app/phases/new-battle-phase.js";
|
||||
import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase.js";
|
||||
import { StatChangePhase } from "#app/phases/stat-change-phase.js";
|
||||
import { SwitchPhase } from "#app/phases/switch-phase.js";
|
||||
import { SwitchSummonPhase } from "#app/phases/switch-summon-phase.js";
|
||||
import { MoveUsedEvent } from "#app/events/battle-scene";
|
||||
import { PartyStatusCurePhase } from "#app/phases/party-status-cure-phase";
|
||||
import { BattleEndPhase } from "#app/phases/battle-end-phase";
|
||||
import { MoveEndPhase } from "#app/phases/move-end-phase";
|
||||
import { MovePhase } from "#app/phases/move-phase";
|
||||
import { NewBattlePhase } from "#app/phases/new-battle-phase";
|
||||
import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase";
|
||||
import { StatChangePhase } from "#app/phases/stat-change-phase";
|
||||
import { SwitchPhase } from "#app/phases/switch-phase";
|
||||
import { SwitchSummonPhase } from "#app/phases/switch-summon-phase";
|
||||
import { SpeciesFormChangeRevertWeatherFormTrigger } from "./pokemon-forms";
|
||||
|
||||
export enum MoveCategory {
|
||||
PHYSICAL,
|
||||
|
@ -5739,7 +5740,10 @@ export class AbilityChangeAttr extends MoveEffectAttr {
|
|||
return false;
|
||||
}
|
||||
|
||||
(this.selfTarget ? user : target).summonData.ability = this.ability;
|
||||
const moveTarget = this.selfTarget ? user : target;
|
||||
|
||||
moveTarget.summonData.ability = this.ability;
|
||||
user.scene.triggerPokemonFormChange(moveTarget, SpeciesFormChangeRevertWeatherFormTrigger);
|
||||
|
||||
user.scene.queueMessage(i18next.t("moveTriggers:acquiredAbility", {pokemonName: getPokemonNameWithAffix((this.selfTarget ? user : target)), abilityName: allAbilities[this.ability].name}));
|
||||
|
||||
|
@ -5825,6 +5829,10 @@ export class SwitchAbilitiesAttr extends MoveEffectAttr {
|
|||
target.summonData.ability = tempAbilityId;
|
||||
|
||||
user.scene.queueMessage(i18next.t("moveTriggers:swappedAbilitiesWithTarget", {pokemonName: getPokemonNameWithAffix(user)}));
|
||||
// Swaps Forecast from Castform
|
||||
user.scene.arena.triggerWeatherBasedFormChangesToNormal();
|
||||
// Swaps Forecast to Castform (edge case)
|
||||
user.scene.arena.triggerWeatherBasedFormChanges();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -5850,6 +5858,7 @@ export class SuppressAbilitiesAttr extends MoveEffectAttr {
|
|||
}
|
||||
|
||||
target.summonData.abilitySuppressed = true;
|
||||
target.scene.arena.triggerWeatherBasedFormChangesToNormal();
|
||||
|
||||
target.scene.queueMessage(i18next.t("moveTriggers:suppressAbilities", {pokemonName: getPokemonNameWithAffix(target)}));
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import { Species } from "#enums/species";
|
|||
import { TimeOfDay } from "#enums/time-of-day";
|
||||
import { getPokemonNameWithAffix } from "#app/messages.js";
|
||||
import i18next from "i18next";
|
||||
import { WeatherType } from "./weather";
|
||||
|
||||
export enum FormChangeItem {
|
||||
NONE,
|
||||
|
@ -356,6 +357,78 @@ export class SpeciesDefaultFormMatchTrigger extends SpeciesFormChangeTrigger {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class used for triggering form changes based on weather.
|
||||
* Used by Castform.
|
||||
* @extends SpeciesFormChangeTrigger
|
||||
*/
|
||||
export class SpeciesFormChangeWeatherTrigger extends SpeciesFormChangeTrigger {
|
||||
/** The ability that triggers the form change */
|
||||
public ability: Abilities;
|
||||
/** The list of weathers that trigger the form change */
|
||||
public weathers: WeatherType[];
|
||||
|
||||
constructor(ability: Abilities, weathers: WeatherType[]) {
|
||||
super();
|
||||
this.ability = ability;
|
||||
this.weathers = weathers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the Pokemon has the required ability and is in the correct weather while
|
||||
* the weather or ability is also not suppressed.
|
||||
* @param {Pokemon} pokemon the pokemon that is trying to do the form change
|
||||
* @returns `true` if the Pokemon can change forms, `false` otherwise
|
||||
*/
|
||||
canChange(pokemon: Pokemon): boolean {
|
||||
const currentWeather = pokemon.scene.arena.weather?.weatherType ?? WeatherType.NONE;
|
||||
const isWeatherSuppressed = pokemon.scene.arena.weather?.isEffectSuppressed(pokemon.scene);
|
||||
const isAbilitySuppressed = pokemon.summonData.abilitySuppressed;
|
||||
|
||||
return !isAbilitySuppressed && !isWeatherSuppressed && (pokemon.hasAbility(this.ability) && this.weathers.includes(currentWeather));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class used for reverting to the original form when the weather runs out
|
||||
* or when the user loses the ability/is suppressed.
|
||||
* Used by Castform.
|
||||
* @extends SpeciesFormChangeTrigger
|
||||
*/
|
||||
export class SpeciesFormChangeRevertWeatherFormTrigger extends SpeciesFormChangeTrigger {
|
||||
/** The ability that triggers the form change*/
|
||||
public ability: Abilities;
|
||||
/** The list of weathers that will also trigger a form change to original form */
|
||||
public weathers: WeatherType[];
|
||||
|
||||
constructor(ability: Abilities, weathers: WeatherType[]) {
|
||||
super();
|
||||
this.ability = ability;
|
||||
this.weathers = weathers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the Pokemon has the required ability and the weather is one that will revert
|
||||
* the Pokemon to its original form or the weather or ability is suppressed
|
||||
* @param {Pokemon} pokemon the pokemon that is trying to do the form change
|
||||
* @returns `true` if the Pokemon will revert to its original form, `false` otherwise
|
||||
*/
|
||||
canChange(pokemon: Pokemon): boolean {
|
||||
if (pokemon.hasAbility(this.ability, false, true)) {
|
||||
const currentWeather = pokemon.scene.arena.weather?.weatherType ?? WeatherType.NONE;
|
||||
const isWeatherSuppressed = pokemon.scene.arena.weather?.isEffectSuppressed(pokemon.scene);
|
||||
const isAbilitySuppressed = pokemon.summonData.abilitySuppressed;
|
||||
const summonDataAbility = pokemon.summonData.ability;
|
||||
const isAbilityChanged = summonDataAbility !== this.ability && summonDataAbility !== Abilities.NONE;
|
||||
|
||||
if (this.weathers.includes(currentWeather) || isWeatherSuppressed || isAbilitySuppressed || isAbilityChanged) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function getSpeciesFormChangeMessage(pokemon: Pokemon, formChange: SpeciesFormChange, preName: string): string {
|
||||
const isMega = formChange.formKey.indexOf(SpeciesFormKey.MEGA) > -1;
|
||||
const isGmax = formChange.formKey.indexOf(SpeciesFormKey.GIGANTAMAX) > -1;
|
||||
|
@ -839,7 +912,24 @@ export const pokemonFormChanges: PokemonFormChanges = {
|
|||
new SpeciesFormChange(Species.CRAMORANT, "gorging", "", new SpeciesFormChangeManualTrigger, true),
|
||||
new SpeciesFormChange(Species.CRAMORANT, "gulping", "", new SpeciesFormChangeActiveTrigger(false), true),
|
||||
new SpeciesFormChange(Species.CRAMORANT, "gorging", "", new SpeciesFormChangeActiveTrigger(false), true),
|
||||
]
|
||||
],
|
||||
[Species.CASTFORM]: [
|
||||
new SpeciesFormChange(Species.CASTFORM, "", "sunny", new SpeciesFormChangeWeatherTrigger(Abilities.FORECAST, [WeatherType.SUNNY, WeatherType.HARSH_SUN]), true),
|
||||
new SpeciesFormChange(Species.CASTFORM, "rainy", "sunny", new SpeciesFormChangeWeatherTrigger(Abilities.FORECAST, [WeatherType.SUNNY, WeatherType.HARSH_SUN]), true),
|
||||
new SpeciesFormChange(Species.CASTFORM, "snowy", "sunny", new SpeciesFormChangeWeatherTrigger(Abilities.FORECAST, [WeatherType.SUNNY, WeatherType.HARSH_SUN]), true),
|
||||
new SpeciesFormChange(Species.CASTFORM, "", "rainy", new SpeciesFormChangeWeatherTrigger(Abilities.FORECAST, [WeatherType.RAIN, WeatherType.HEAVY_RAIN]), true),
|
||||
new SpeciesFormChange(Species.CASTFORM, "sunny", "rainy", new SpeciesFormChangeWeatherTrigger(Abilities.FORECAST, [WeatherType.RAIN, WeatherType.HEAVY_RAIN]), true),
|
||||
new SpeciesFormChange(Species.CASTFORM, "snowy", "rainy", new SpeciesFormChangeWeatherTrigger(Abilities.FORECAST, [WeatherType.RAIN, WeatherType.HEAVY_RAIN]), true),
|
||||
new SpeciesFormChange(Species.CASTFORM, "", "snowy", new SpeciesFormChangeWeatherTrigger(Abilities.FORECAST, [WeatherType.HAIL, WeatherType.SNOW]), true),
|
||||
new SpeciesFormChange(Species.CASTFORM, "sunny", "snowy", new SpeciesFormChangeWeatherTrigger(Abilities.FORECAST, [WeatherType.HAIL, WeatherType.SNOW]), true),
|
||||
new SpeciesFormChange(Species.CASTFORM, "rainy", "snowy", new SpeciesFormChangeWeatherTrigger(Abilities.FORECAST, [WeatherType.HAIL, WeatherType.SNOW]), true),
|
||||
new SpeciesFormChange(Species.CASTFORM, "sunny", "", new SpeciesFormChangeRevertWeatherFormTrigger(Abilities.FORECAST, [WeatherType.NONE, WeatherType.SANDSTORM, WeatherType.STRONG_WINDS, WeatherType.FOG]), true),
|
||||
new SpeciesFormChange(Species.CASTFORM, "rainy", "", new SpeciesFormChangeRevertWeatherFormTrigger(Abilities.FORECAST, [WeatherType.NONE, WeatherType.SANDSTORM, WeatherType.STRONG_WINDS, WeatherType.FOG]), true),
|
||||
new SpeciesFormChange(Species.CASTFORM, "snowy", "", new SpeciesFormChangeRevertWeatherFormTrigger(Abilities.FORECAST, [WeatherType.NONE, WeatherType.SANDSTORM, WeatherType.STRONG_WINDS, WeatherType.FOG]), true),
|
||||
new SpeciesFormChange(Species.CASTFORM, "sunny", "", new SpeciesFormChangeActiveTrigger(), true),
|
||||
new SpeciesFormChange(Species.CASTFORM, "rainy", "", new SpeciesFormChangeActiveTrigger(), true),
|
||||
new SpeciesFormChange(Species.CASTFORM, "snowy", "", new SpeciesFormChangeActiveTrigger(), true),
|
||||
],
|
||||
};
|
||||
|
||||
export function initPokemonForms() {
|
||||
|
|
|
@ -20,7 +20,10 @@ import { Moves } from "#enums/moves";
|
|||
import { Species } from "#enums/species";
|
||||
import { TimeOfDay } from "#enums/time-of-day";
|
||||
import { TrainerType } from "#enums/trainer-type";
|
||||
import { CommonAnimPhase } from "#app/phases/common-anim-phase.js";
|
||||
import { Abilities } from "#app/enums/abilities";
|
||||
import { SpeciesFormChangeRevertWeatherFormTrigger, SpeciesFormChangeWeatherTrigger } from "#app/data/pokemon-forms";
|
||||
import { CommonAnimPhase } from "#app/phases/common-anim-phase";
|
||||
import { ShowAbilityPhase } from "#app/phases/show-ability-phase";
|
||||
|
||||
export class Arena {
|
||||
public scene: BattleScene;
|
||||
|
@ -331,6 +334,30 @@ export class Arena {
|
|||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to trigger all weather based form changes
|
||||
*/
|
||||
triggerWeatherBasedFormChanges(): void {
|
||||
this.scene.getField(true).forEach( p => {
|
||||
if (p.hasAbility(Abilities.FORECAST) && p.species.speciesId === Species.CASTFORM) {
|
||||
new ShowAbilityPhase(this.scene, p.getBattlerIndex());
|
||||
this.scene.triggerPokemonFormChange(p, SpeciesFormChangeWeatherTrigger);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to trigger all weather based form changes back into their normal forms
|
||||
*/
|
||||
triggerWeatherBasedFormChangesToNormal(): void {
|
||||
this.scene.getField(true).forEach( p => {
|
||||
if (p.hasAbility(Abilities.FORECAST, false, true) && p.species.speciesId === Species.CASTFORM) {
|
||||
new ShowAbilityPhase(this.scene, p.getBattlerIndex());
|
||||
return this.scene.triggerPokemonFormChange(p, SpeciesFormChangeRevertWeatherFormTrigger);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
trySetTerrain(terrain: TerrainType, hasPokemonSource: boolean, ignoreAnim: boolean = false): boolean {
|
||||
if (this.terrain?.terrainType === (terrain || undefined)) {
|
||||
return false;
|
||||
|
|
|
@ -1140,7 +1140,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||
* effects which can affect whether an ability will be present or in effect, and both passive and
|
||||
* non-passive. This is the primary way to check whether a pokemon has a particular ability.
|
||||
* @param {Abilities} ability The ability to check for
|
||||
* @param {boolean} canApply If false, it doesn't check whether the abiltiy is currently active
|
||||
* @param {boolean} canApply If false, it doesn't check whether the ability is currently active
|
||||
* @param {boolean} ignoreOverride If true, it ignores ability changing effects
|
||||
* @returns {boolean} Whether the ability is present and active
|
||||
*/
|
||||
|
|
|
@ -160,6 +160,8 @@ export class SwitchSummonPhase extends SummonPhase {
|
|||
this.lastPokemon?.resetSummonData();
|
||||
|
||||
this.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true);
|
||||
// Reverts to weather-based forms when weather suppressors (Cloud Nine/Air Lock) are switched out
|
||||
this.scene.arena.triggerWeatherBasedFormChanges();
|
||||
}
|
||||
|
||||
queuePostSummon(): void {
|
||||
|
|
|
@ -60,6 +60,7 @@ export class TurnEndPhase extends FieldPhase {
|
|||
|
||||
if (this.scene.arena.weather && !this.scene.arena.weather.lapse()) {
|
||||
this.scene.arena.trySetWeather(WeatherType.NONE, false);
|
||||
this.scene.arena.triggerWeatherBasedFormChangesToNormal();
|
||||
}
|
||||
|
||||
if (this.scene.arena.terrain && !this.scene.arena.terrain.lapse()) {
|
||||
|
|
|
@ -0,0 +1,347 @@
|
|||
import { Abilities } from "#app/enums/abilities.js";
|
||||
import GameManager from "#test/utils/gameManager";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { SPLASH_ONLY } from "#test/utils/testUtils";
|
||||
import { WeatherType } from "#app/enums/weather-type";
|
||||
import { BattlerIndex } from "#app/battle";
|
||||
import { QuietFormChangePhase } from "#app/phases/quiet-form-change-phase";
|
||||
import { DamagePhase } from "#app/phases/damage-phase";
|
||||
import { MovePhase } from "#app/phases/move-phase";
|
||||
import { PostSummonPhase } from "#app/phases/post-summon-phase";
|
||||
import { TurnEndPhase } from "#app/phases/turn-end-phase";
|
||||
import { VictoryPhase } from "#app/phases/victory-phase";
|
||||
import { allAbilities } from "#app/data/ability";
|
||||
|
||||
describe("Abilities - Forecast", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
const NORMAL_FORM = 0;
|
||||
const SUNNY_FORM = 1;
|
||||
const RAINY_FORM = 2;
|
||||
const SNOWY_FORM = 3;
|
||||
|
||||
/**
|
||||
* Tests form changes based on weather changes
|
||||
* @param {GameManager} game The game manager instance
|
||||
* @param {WeatherType} weather The active weather to set
|
||||
* @param form The expected form based on the active weather
|
||||
* @param initialForm The initial form pre form change
|
||||
*/
|
||||
const testWeatherFormChange = async (game: GameManager, weather: WeatherType, form: number, initialForm?: number) => {
|
||||
game.override.weather(weather).starterForms({[Species.CASTFORM]: initialForm});
|
||||
await game.startBattle([Species.CASTFORM]);
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
|
||||
expect(game.scene.getPlayerPokemon()?.formIndex).toBe(form);
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests reverting to normal form when Cloud Nine/Air Lock is active on the field
|
||||
* @param {GameManager} game The game manager instance
|
||||
* @param {Abilities} ability The ability that is active on the field
|
||||
*/
|
||||
const testRevertFormAgainstAbility = async (game: GameManager, ability: Abilities) => {
|
||||
game.override.starterForms({ [Species.CASTFORM]: SUNNY_FORM }).enemyAbility(ability);
|
||||
await game.startBattle([Species.CASTFORM]);
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
|
||||
expect(game.scene.getPlayerPokemon()?.formIndex).toBe(NORMAL_FORM);
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests transforming back to match the weather when Cloud Nine/Air Lock user is fainted
|
||||
* @param {GameManager} game The game manager instance
|
||||
* @param {Abilities} ability The ability that will go out of battle (faint)
|
||||
*/
|
||||
const testTransformAfterAbilityFaint = async (game: GameManager, ability: Abilities) => {
|
||||
game.override.enemyAbility(ability).weather(WeatherType.SNOW).enemySpecies(Species.SHUCKLE);
|
||||
await game.startBattle([Species.CASTFORM]);
|
||||
const castform = game.scene.getPlayerPokemon();
|
||||
|
||||
expect(castform?.formIndex).toBe(NORMAL_FORM);
|
||||
|
||||
game.move.select(Moves.TACKLE);
|
||||
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
|
||||
await game.phaseInterceptor.to(DamagePhase);
|
||||
|
||||
await game.doKillOpponents();
|
||||
await game.phaseInterceptor.to(VictoryPhase);
|
||||
|
||||
expect(castform?.formIndex).toBe(SNOWY_FORM);
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests transforming back to match the weather when Cloud Nine/Air Lock user is switched out
|
||||
* @param {GameManager} game The game manager instance
|
||||
* @param {Abilities} ability The ability that will go out of battle (switched out)
|
||||
*/
|
||||
const testTransformAfterAbilitySwitchOut = async (game: GameManager, ability: Abilities) => {
|
||||
game.override
|
||||
.weather(WeatherType.SNOW)
|
||||
.enemySpecies(Species.CASTFORM)
|
||||
.enemyAbility(Abilities.FORECAST)
|
||||
.ability(ability);
|
||||
await game.startBattle([Species.PICHU, Species.PIKACHU]);
|
||||
|
||||
const castform = game.scene.getEnemyPokemon();
|
||||
|
||||
// We mock the return value of the second Pokemon to be other than Air Lock/Cloud Nine
|
||||
vi.spyOn(game.scene.getParty()[1]!, "getAbility").mockReturnValue(allAbilities[Abilities.BALL_FETCH]);
|
||||
expect(game.scene.getParty()[1]?.hasAbility(Abilities.BALL_FETCH));
|
||||
|
||||
expect(castform?.formIndex).toBe(NORMAL_FORM);
|
||||
|
||||
game.doSwitchPokemon(1);
|
||||
await game.phaseInterceptor.to(MovePhase);
|
||||
expect(castform?.formIndex).toBe(SNOWY_FORM);
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override.moveset([ Moves.SPLASH, Moves.RAIN_DANCE, Moves.SUNNY_DAY, Moves.TACKLE ])
|
||||
.enemyMoveset(SPLASH_ONLY)
|
||||
.enemyAbility(Abilities.BALL_FETCH);
|
||||
});
|
||||
|
||||
it("changes to Sunny Form during Harsh Sunlight", async () => {
|
||||
await testWeatherFormChange(game, WeatherType.SUNNY, SUNNY_FORM);
|
||||
});
|
||||
|
||||
it("changes to Sunny Form during Extreme Harsh Sunlight", async () => {
|
||||
await testWeatherFormChange(game, WeatherType.HARSH_SUN, SUNNY_FORM);
|
||||
});
|
||||
|
||||
it("changes to Rainy Form during Rain", async () => {
|
||||
await testWeatherFormChange(game, WeatherType.RAIN, RAINY_FORM);
|
||||
});
|
||||
|
||||
it("changes to Rainy Form during Heavy Rain", async () => {
|
||||
await testWeatherFormChange(game, WeatherType.HEAVY_RAIN, RAINY_FORM);
|
||||
});
|
||||
|
||||
it("changes to Snowy Form during Hail", async () => {
|
||||
await testWeatherFormChange(game, WeatherType.HAIL, SNOWY_FORM);
|
||||
});
|
||||
|
||||
it("changes to Snowy Form during Snow", async () => {
|
||||
await testWeatherFormChange(game, WeatherType.SNOW, SNOWY_FORM);
|
||||
});
|
||||
|
||||
it("reverts to Normal Form during Sandstorm", async () => {
|
||||
await testWeatherFormChange(game, WeatherType.SANDSTORM, NORMAL_FORM, SUNNY_FORM);
|
||||
});
|
||||
|
||||
it("reverts to Normal Form during Fog", async () => {
|
||||
await testWeatherFormChange(game, WeatherType.FOG, NORMAL_FORM, SUNNY_FORM);
|
||||
});
|
||||
|
||||
it("reverts to Normal Form during Strong Winds", async () => {
|
||||
await testWeatherFormChange(game, WeatherType.STRONG_WINDS, NORMAL_FORM, SUNNY_FORM);
|
||||
});
|
||||
|
||||
it("reverts to Normal Form during Clear weather", async () => {
|
||||
await testWeatherFormChange(game, WeatherType.NONE, NORMAL_FORM, SUNNY_FORM);
|
||||
});
|
||||
|
||||
it("reverts to Normal Form if a Pokémon on the field has Cloud Nine", async () => {
|
||||
await testRevertFormAgainstAbility(game, Abilities.CLOUD_NINE);
|
||||
});
|
||||
|
||||
it("reverts to Normal Form if a Pokémon on the field has Air Lock", async () => {
|
||||
await testRevertFormAgainstAbility(game, Abilities.AIR_LOCK);
|
||||
});
|
||||
|
||||
it("has no effect on Pokémon other than Castform", async () => {
|
||||
game.override.enemyAbility(Abilities.FORECAST).enemySpecies(Species.SHUCKLE);
|
||||
await game.startBattle([Species.CASTFORM]);
|
||||
|
||||
game.move.select(Moves.RAIN_DANCE);
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
expect(game.scene.getPlayerPokemon()?.formIndex).toBe(RAINY_FORM);
|
||||
expect(game.scene.getEnemyPokemon()?.formIndex).not.toBe(RAINY_FORM);
|
||||
});
|
||||
|
||||
it("cannot be copied", async () => {
|
||||
game.override.enemyAbility(Abilities.TRACE);
|
||||
await game.startBattle([Species.CASTFORM]);
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
|
||||
expect(game.scene.getEnemyPokemon()?.hasAbility(Abilities.FORECAST)).toBe(false);
|
||||
});
|
||||
|
||||
it("(Skill Swap) reverts to Normal Form when Castform loses Forecast, changes form to match the weather when it regains it", async () => {
|
||||
game.override.moveset([Moves.SKILL_SWAP]).weather(WeatherType.RAIN);
|
||||
await game.startBattle([Species.CASTFORM]);
|
||||
const castform = game.scene.getPlayerPokemon();
|
||||
|
||||
expect(castform?.formIndex).toBe(RAINY_FORM);
|
||||
|
||||
// First turn - loses Forecast
|
||||
game.move.select(Moves.SKILL_SWAP);
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
expect(castform?.formIndex).toBe(NORMAL_FORM);
|
||||
|
||||
// Second turn - regains Forecast
|
||||
game.move.select(Moves.SKILL_SWAP);
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
expect(castform?.formIndex).toBe(RAINY_FORM);
|
||||
});
|
||||
|
||||
it("(Worry Seed) reverts to Normal Form when Castform loses Forecast, changes form to match the weather when it regains it", async () => {
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.WORRY_SEED)).weather(WeatherType.RAIN);
|
||||
await game.startBattle([Species.CASTFORM, Species.PIKACHU]);
|
||||
const castform = game.scene.getPlayerPokemon();
|
||||
|
||||
expect(castform?.formIndex).toBe(RAINY_FORM);
|
||||
|
||||
// First turn - loses Forecast
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
expect(castform?.formIndex).toBe(NORMAL_FORM);
|
||||
|
||||
await game.toNextTurn();
|
||||
|
||||
// Second turn - switch out Castform, regains Forecast
|
||||
game.doSwitchPokemon(1);
|
||||
await game.toNextTurn();
|
||||
|
||||
// Third turn - switch in Castform
|
||||
game.doSwitchPokemon(1);
|
||||
await game.phaseInterceptor.to(MovePhase);
|
||||
|
||||
expect(castform?.formIndex).toBe(RAINY_FORM);
|
||||
});
|
||||
|
||||
it("reverts to Normal Form when active weather ends", async () => {
|
||||
await game.startBattle([Species.CASTFORM]);
|
||||
const castform = game.scene.getPlayerPokemon();
|
||||
|
||||
game.move.select(Moves.SUNNY_DAY);
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
while (game.scene.arena.weather && game.scene.arena.weather.turnsLeft > 0) {
|
||||
game.move.select(Moves.SPLASH);
|
||||
expect(castform?.formIndex).toBe(SUNNY_FORM);
|
||||
await game.toNextTurn();
|
||||
}
|
||||
|
||||
expect(castform?.formIndex).toBe(NORMAL_FORM);
|
||||
});
|
||||
|
||||
it("reverts to Normal Form when Forecast is suppressed, changes form to match the weather when it regains it", async () => {
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.GASTRO_ACID)).weather(WeatherType.RAIN);
|
||||
await game.startBattle([Species.CASTFORM, Species.PIKACHU]);
|
||||
const castform = game.scene.getPlayerPokemon();
|
||||
|
||||
expect(castform?.formIndex).toBe(RAINY_FORM);
|
||||
|
||||
// First turn - Forecast is suppressed
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
|
||||
await game.move.forceHit();
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
expect(castform?.summonData.abilitySuppressed).toBe(true);
|
||||
expect(castform?.formIndex).toBe(NORMAL_FORM);
|
||||
|
||||
await game.toNextTurn();
|
||||
|
||||
// Second turn - switch out Castform, regains Forecast
|
||||
game.doSwitchPokemon(1);
|
||||
await game.toNextTurn();
|
||||
|
||||
// Third turn - switch in Castform
|
||||
game.doSwitchPokemon(1);
|
||||
await game.phaseInterceptor.to(MovePhase);
|
||||
|
||||
expect(castform?.summonData.abilitySuppressed).toBe(false);
|
||||
expect(castform?.formIndex).toBe(RAINY_FORM);
|
||||
});
|
||||
|
||||
it("if a Pokémon transforms into Castform, the Pokémon will remain in the same form as the target Castform, regardless of the weather", async () => {
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.TRANSFORM));
|
||||
await game.startBattle([Species.CASTFORM]);
|
||||
|
||||
game.move.select(Moves.SUNNY_DAY);
|
||||
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
expect(game.scene.getPlayerPokemon()?.formIndex).toBe(SUNNY_FORM);
|
||||
expect(game.scene.getEnemyPokemon()?.formIndex).toBe(NORMAL_FORM);
|
||||
});
|
||||
|
||||
it("does not change Castform's form until after Stealth Rock deals damage", async () => {
|
||||
game.override.weather(WeatherType.RAIN).enemyMoveset(Array(4).fill(Moves.STEALTH_ROCK));
|
||||
await game.startBattle([Species.PIKACHU, Species.CASTFORM]);
|
||||
|
||||
// First turn - set up stealth rock
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.toNextTurn();
|
||||
|
||||
// Second turn - switch in Castform, regains Forecast
|
||||
game.doSwitchPokemon(1);
|
||||
await game.phaseInterceptor.to(PostSummonPhase);
|
||||
|
||||
const castform = game.scene.getPlayerPokemon();
|
||||
|
||||
// Damage phase should come first
|
||||
await game.phaseInterceptor.to(DamagePhase);
|
||||
expect(castform?.hp).toBeLessThan(castform?.getMaxHp() ?? 0);
|
||||
|
||||
// Then change form
|
||||
await game.phaseInterceptor.to(QuietFormChangePhase);
|
||||
expect(castform?.formIndex).toBe(RAINY_FORM);
|
||||
});
|
||||
|
||||
it("transforms to weather-based form when Pokemon with Air Lock is fainted", async () => {
|
||||
await testTransformAfterAbilityFaint(game, Abilities.AIR_LOCK);
|
||||
});
|
||||
|
||||
it("transforms to weather-based form when Pokemon with Cloud Nine is fainted", async () => {
|
||||
await testTransformAfterAbilityFaint(game, Abilities.CLOUD_NINE);
|
||||
});
|
||||
|
||||
it("transforms to weather-based form when Pokemon with Air Lock is switched out", async () => {
|
||||
await testTransformAfterAbilitySwitchOut(game, Abilities.AIR_LOCK);
|
||||
});
|
||||
|
||||
it("transforms to weather-based form when Pokemon with Cloud Nine is switched out", async () => {
|
||||
await testTransformAfterAbilitySwitchOut(game, Abilities.CLOUD_NINE);
|
||||
});
|
||||
|
||||
it("should be in Normal Form after the user is switched out", async () => {
|
||||
game.override.weather(WeatherType.RAIN);
|
||||
|
||||
await game.startBattle([Species.CASTFORM, Species.MAGIKARP]);
|
||||
const castform = game.scene.getPlayerPokemon()!;
|
||||
|
||||
expect(castform.formIndex).toBe(RAINY_FORM);
|
||||
|
||||
game.doSwitchPokemon(1);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(castform.formIndex).toBe(NORMAL_FORM);
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue