[Item] Add Weather and Terrain Extender Item (#4799)

* [Item] Add Weather and Terrain Extender Item

* Add Documentation

* Clean Up Unit Tests

* Add Weight Function

* Include Suggestions
This commit is contained in:
Amani H. 2025-03-26 22:40:46 -04:00 committed by GitHub
parent db850c79cd
commit 6316218bd3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 194 additions and 29 deletions

View File

@ -308,7 +308,7 @@ export class ClearWeatherAbAttr extends AbAttr {
public override apply(pokemon: Pokemon, passive: boolean, simulated:boolean, cancelled: Utils.BooleanHolder, args: any[]): void {
if (!simulated) {
globalScene.arena.trySetWeather(WeatherType.NONE, true);
globalScene.arena.trySetWeather(WeatherType.NONE, pokemon);
}
}
}
@ -334,7 +334,7 @@ export class ClearTerrainAbAttr extends AbAttr {
public override apply(pokemon: Pokemon, passive: boolean, simulated:boolean, cancelled: Utils.BooleanHolder, args: any[]): void {
if (!simulated) {
globalScene.arena.trySetTerrain(TerrainType.NONE, true, true);
globalScene.arena.trySetTerrain(TerrainType.NONE, true, pokemon);
}
}
}
@ -954,7 +954,7 @@ export class PostDefendTerrainChangeAbAttr extends PostDefendAbAttr {
override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, _args: any[]): void {
if (!simulated) {
globalScene.arena.trySetTerrain(this.terrainType, true);
globalScene.arena.trySetTerrain(this.terrainType, false, pokemon);
}
}
}
@ -1126,7 +1126,7 @@ export class PostDefendWeatherChangeAbAttr extends PostDefendAbAttr {
override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, _args: any[]): void {
if (!simulated) {
globalScene.arena.trySetWeather(this.weatherType, true);
globalScene.arena.trySetWeather(this.weatherType, pokemon);
}
}
}
@ -2483,7 +2483,7 @@ export class PostSummonWeatherChangeAbAttr extends PostSummonAbAttr {
override applyPostSummon(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): void {
if (!simulated) {
globalScene.arena.trySetWeather(this.weatherType, true);
globalScene.arena.trySetWeather(this.weatherType, pokemon);
}
}
}
@ -2503,7 +2503,7 @@ export class PostSummonTerrainChangeAbAttr extends PostSummonAbAttr {
override applyPostSummon(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): void {
if (!simulated) {
globalScene.arena.trySetTerrain(this.terrainType, true);
globalScene.arena.trySetTerrain(this.terrainType, false, pokemon);
}
}
}
@ -2886,7 +2886,7 @@ export class PreSwitchOutClearWeatherAbAttr extends PreSwitchOutAbAttr {
}
if (turnOffWeather) {
globalScene.arena.trySetWeather(WeatherType.NONE, false);
globalScene.arena.trySetWeather(WeatherType.NONE);
return true;
}
@ -2986,7 +2986,7 @@ export class PreLeaveFieldClearWeatherAbAttr extends PreLeaveFieldAbAttr {
*/
override applyPreLeaveField(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): void {
if (!simulated) {
globalScene.arena.trySetWeather(WeatherType.NONE, false);
globalScene.arena.trySetWeather(WeatherType.NONE);
}
}
}
@ -4137,7 +4137,7 @@ export class PostBiomeChangeWeatherChangeAbAttr extends PostBiomeChangeAbAttr {
override apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: Utils.BooleanHolder, args: any[]): void {
if (!simulated) {
globalScene.arena.trySetWeather(this.weatherType, true);
globalScene.arena.trySetWeather(this.weatherType, pokemon);
}
}
}
@ -4157,7 +4157,7 @@ export class PostBiomeChangeTerrainChangeAbAttr extends PostBiomeChangeAbAttr {
override apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: Utils.BooleanHolder, args: any[]): void {
if (!simulated) {
globalScene.arena.trySetTerrain(this.terrainType, true);
globalScene.arena.trySetTerrain(this.terrainType, false, pokemon);
}
}
}

View File

@ -2851,7 +2851,7 @@ export class WeatherChangeAttr extends MoveEffectAttr {
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
return globalScene.arena.trySetWeather(this.weatherType, true);
return globalScene.arena.trySetWeather(this.weatherType, user);
}
getCondition(): MoveConditionFunc {
@ -2870,7 +2870,7 @@ export class ClearWeatherAttr extends MoveEffectAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (globalScene.arena.weather?.weatherType === this.weatherType) {
return globalScene.arena.trySetWeather(WeatherType.NONE, true);
return globalScene.arena.trySetWeather(WeatherType.NONE, user);
}
return false;
@ -2887,7 +2887,7 @@ export class TerrainChangeAttr extends MoveEffectAttr {
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
return globalScene.arena.trySetTerrain(this.terrainType, true, true);
return globalScene.arena.trySetTerrain(this.terrainType, true, user);
}
getCondition(): MoveConditionFunc {
@ -2906,7 +2906,7 @@ export class ClearTerrainAttr extends MoveEffectAttr {
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
return globalScene.arena.trySetTerrain(TerrainType.NONE, true, true);
return globalScene.arena.trySetTerrain(TerrainType.NONE, true, user);
}
}
@ -6454,7 +6454,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
export class ChillyReceptionAttr extends ForceSwitchOutAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
globalScene.arena.trySetWeather(WeatherType.SNOW, true);
globalScene.arena.trySetWeather(WeatherType.SNOW, user);
return super.apply(user, target, move, args);
}

View File

@ -140,7 +140,8 @@ export const FieryFalloutEncounter: MysteryEncounter = MysteryEncounterBuilder.w
// Load animations/sfx for Volcarona moves
loadCustomMovesForEncounter([Moves.FIRE_SPIN, Moves.QUIVER_DANCE]);
globalScene.arena.trySetWeather(WeatherType.SUNNY, true);
const pokemon = globalScene.getEnemyPokemon();
globalScene.arena.trySetWeather(WeatherType.SUNNY, pokemon);
encounter.setDialogueToken("volcaronaName", getPokemonSpecies(Species.VOLCARONA).getName());

View File

@ -42,6 +42,7 @@ import { SpeciesFormChangeRevertWeatherFormTrigger, SpeciesFormChangeWeatherTrig
import { CommonAnimPhase } from "#app/phases/common-anim-phase";
import { ShowAbilityPhase } from "#app/phases/show-ability-phase";
import { WeatherType } from "#enums/weather-type";
import { FieldEffectModifier } from "#app/modifier/modifier";
export class Arena {
public biomeType: Biome;
@ -311,10 +312,10 @@ export class Arena {
/**
* Attempts to set a new weather to the battle
* @param weather {@linkcode WeatherType} new {@linkcode WeatherType} to set
* @param hasPokemonSource boolean if the new weather is from a pokemon
* @param user {@linkcode Pokemon} that caused the weather effect
* @returns true if new weather set, false if no weather provided or attempting to set the same weather as currently in use
*/
trySetWeather(weather: WeatherType, hasPokemonSource: boolean): boolean {
trySetWeather(weather: WeatherType, user?: Pokemon): boolean {
if (Overrides.WEATHER_OVERRIDE) {
return this.trySetWeatherOverride(Overrides.WEATHER_OVERRIDE);
}
@ -336,7 +337,14 @@ export class Arena {
return false;
}
this.weather = weather ? new Weather(weather, hasPokemonSource ? 5 : 0) : null;
const weatherDuration = new Utils.NumberHolder(0);
if (!Utils.isNullOrUndefined(user)) {
weatherDuration.value = 5;
globalScene.applyModifier(FieldEffectModifier, user.isPlayer(), user, weatherDuration);
}
this.weather = weather ? new Weather(weather, weatherDuration.value) : null;
this.eventTarget.dispatchEvent(
new WeatherChangedEvent(oldWeatherType, this.weather?.weatherType!, this.weather?.turnsLeft!),
); // TODO: is this bang correct?
@ -398,14 +406,29 @@ export class Arena {
return !(this.terrain?.terrainType === (terrain || undefined));
}
trySetTerrain(terrain: TerrainType, hasPokemonSource: boolean, ignoreAnim = false): boolean {
/**
* Attempts to set a new terrain effect to the battle
* @param terrain {@linkcode TerrainType} new {@linkcode TerrainType} to set
* @param ignoreAnim boolean if the terrain animation should be ignored
* @param user {@linkcode Pokemon} that caused the terrain effect
* @returns true if new terrain set, false if no terrain provided or attempting to set the same terrain as currently in use
*/
trySetTerrain(terrain: TerrainType, ignoreAnim = false, user?: Pokemon): boolean {
if (!this.canSetTerrain(terrain)) {
return false;
}
const oldTerrainType = this.terrain?.terrainType || TerrainType.NONE;
this.terrain = terrain ? new Terrain(terrain, hasPokemonSource ? 5 : 0) : null;
const terrainDuration = new Utils.NumberHolder(0);
if (!Utils.isNullOrUndefined(user)) {
terrainDuration.value = 5;
globalScene.applyModifier(FieldEffectModifier, user.isPlayer(), user, terrainDuration);
}
this.terrain = terrain ? new Terrain(terrain, terrainDuration.value) : null;
this.eventTarget.dispatchEvent(
new TerrainChangedEvent(oldTerrainType, this.terrain?.terrainType!, this.terrain?.turnsLeft!),
); // TODO: are those bangs correct?
@ -802,9 +825,9 @@ export class Arena {
resetArenaEffects(): void {
// Don't reset weather if a Biome's permanent weather is active
if (this.weather?.turnsLeft !== 0) {
this.trySetWeather(WeatherType.NONE, false);
this.trySetWeather(WeatherType.NONE);
}
this.trySetTerrain(TerrainType.NONE, false, true);
this.trySetTerrain(TerrainType.NONE, true);
this.resetPlayerFaintCount();
this.removeAllTags();
}

View File

@ -97,6 +97,7 @@ import {
type PersistentModifier,
TempExtraModifierModifier,
CriticalCatchChanceBoosterModifier,
FieldEffectModifier,
} from "#app/modifier/modifier";
import { ModifierTier } from "#app/modifier/modifier-tier";
import Overrides from "#app/overrides";
@ -1998,6 +1999,13 @@ export const modifierTypes = {
return new PokemonNatureChangeModifierType(randSeedInt(getEnumValues(Nature).length) as Nature);
}),
MYSTICAL_ROCK: () =>
new ModifierType(
"modifierType:ModifierType.MYSTICAL_ROCK",
"mystical_rock",
(type, args) => new FieldEffectModifier(type, (args[0] as Pokemon).id),
),
TERA_SHARD: () =>
new ModifierTypeGenerator((party: Pokemon[], pregenArgs?: any[]) => {
if (pregenArgs && pregenArgs.length === 1 && pregenArgs[0] in PokemonType) {
@ -2810,6 +2818,47 @@ const modifierPool: ModifierPool = {
},
10,
),
new WeightedModifierType(
modifierTypes.MYSTICAL_ROCK,
(party: Pokemon[]) => {
return party.some(p => {
const moveset = p.getMoveset(true).map(m => m.moveId);
const hasAbility = [
Abilities.DRIZZLE,
Abilities.ORICHALCUM_PULSE,
Abilities.DRIZZLE,
Abilities.SAND_STREAM,
Abilities.SAND_SPIT,
Abilities.SNOW_WARNING,
Abilities.ELECTRIC_SURGE,
Abilities.HADRON_ENGINE,
Abilities.PSYCHIC_SURGE,
Abilities.GRASSY_SURGE,
Abilities.SEED_SOWER,
Abilities.MISTY_SURGE,
].some(a => p.hasAbility(a, false, true));
const hasMoves = [
Moves.SUNNY_DAY,
Moves.RAIN_DANCE,
Moves.SANDSTORM,
Moves.SNOWSCAPE,
Moves.HAIL,
Moves.CHILLY_RECEPTION,
Moves.ELECTRIC_TERRAIN,
Moves.PSYCHIC_TERRAIN,
Moves.GRASSY_TERRAIN,
Moves.MISTY_TERRAIN,
].some(m => moveset.includes(m));
return hasAbility || hasMoves;
})
? 10
: 0;
},
10,
),
new WeightedModifierType(modifierTypes.REVIVER_SEED, 4),
new WeightedModifierType(modifierTypes.CANDY_JAR, skipInLastClassicWaveOrDefault(5)),
new WeightedModifierType(modifierTypes.ATTACK_TYPE_BOOSTER, 9),

View File

@ -2014,6 +2014,38 @@ export class ResetNegativeStatStageModifier extends PokemonHeldItemModifier {
}
}
/**
* Modifier used for held items, namely Mystical Rock, that extend the
* duration of weather and terrain effects.
* @extends PokemonHeldItemModifier
* @see {@linkcode apply}
*/
export class FieldEffectModifier extends PokemonHeldItemModifier {
/**
* Provides two more turns per stack to any weather or terrain effect caused
* by the holder.
* @param pokemon {@linkcode Pokemon} that holds the held item
* @param fieldDuration {@linkcode NumberHolder} that stores the current field effect duration
* @returns `true` if the field effect extension was applied successfully
*/
override apply(_pokemon: Pokemon, fieldDuration: NumberHolder): boolean {
fieldDuration.value += 2 * this.stackCount;
return true;
}
override matchType(modifier: Modifier): boolean {
return modifier instanceof FieldEffectModifier;
}
override clone(): FieldEffectModifier {
return new FieldEffectModifier(this.type, this.pokemonId, this.stackCount);
}
override getMaxHeldItemCount(_pokemon?: Pokemon): number {
return 2;
}
}
export abstract class ConsumablePokemonModifier extends ConsumableModifier {
public pokemonId: number;

View File

@ -684,7 +684,7 @@ export class EncounterPhase extends BattlePhase {
*/
trySetWeatherIfNewBiome(): void {
if (!this.loaded) {
globalScene.arena.trySetWeather(getRandomWeatherType(globalScene.arena), false);
globalScene.arena.trySetWeather(getRandomWeatherType(globalScene.arena));
}
}
}

View File

@ -45,6 +45,6 @@ export class NewBiomeEncounterPhase extends NextEncounterPhase {
* Set biome weather.
*/
trySetWeatherIfNewBiome(): void {
globalScene.arena.trySetWeather(getRandomWeatherType(globalScene.arena), false);
globalScene.arena.trySetWeather(getRandomWeatherType(globalScene.arena));
}
}

View File

@ -68,12 +68,12 @@ export class TurnEndPhase extends FieldPhase {
globalScene.arena.lapseTags();
if (globalScene.arena.weather && !globalScene.arena.weather.lapse()) {
globalScene.arena.trySetWeather(WeatherType.NONE, false);
globalScene.arena.trySetWeather(WeatherType.NONE);
globalScene.arena.triggerWeatherBasedFormChangesToNormal();
}
if (globalScene.arena.terrain && !globalScene.arena.terrain.lapse()) {
globalScene.arena.trySetTerrain(TerrainType.NONE, false);
globalScene.arena.trySetTerrain(TerrainType.NONE);
}
this.end();

View File

@ -181,7 +181,7 @@ describe("Abilities - Forecast", () => {
expect(castform.formIndex).toBe(SNOWY_FORM);
game.scene.arena.trySetWeather(WeatherType.FOG, false);
game.scene.arena.trySetWeather(WeatherType.FOG);
game.move.select(Moves.SPLASH);
game.move.select(Moves.SPLASH, 1);
await game.phaseInterceptor.to("TurnStartPhase");

View File

@ -0,0 +1,60 @@
import { globalScene } from "#app/global-scene";
import { Moves } from "#enums/moves";
import { Abilities } from "#enums/abilities";
import { Species } from "#enums/species";
import GameManager from "#test/testUtils/gameManager";
import Phase from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Items - Mystical Rock", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phase.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.enemySpecies(Species.SHUCKLE)
.enemyMoveset(Moves.SPLASH)
.enemyAbility(Abilities.BALL_FETCH)
.moveset([Moves.SUNNY_DAY, Moves.GRASSY_TERRAIN])
.startingHeldItems([{ name: "MYSTICAL_ROCK", count: 2 }])
.battleType("single");
});
it("should increase weather duration by +2 turns per stack", async () => {
await game.classicMode.startBattle([Species.GASTLY]);
game.move.select(Moves.SUNNY_DAY);
await game.phaseInterceptor.to("MoveEndPhase");
const weather = globalScene.arena.weather;
expect(weather).toBeDefined();
expect(weather!.turnsLeft).to.equal(9);
});
it("should increase terrain duration by +2 turns per stack", async () => {
await game.classicMode.startBattle([Species.GASTLY]);
game.move.select(Moves.GRASSY_TERRAIN);
await game.phaseInterceptor.to("MoveEndPhase");
const terrain = globalScene.arena.terrain;
expect(terrain).toBeDefined();
expect(terrain!.turnsLeft).to.equal(9);
});
});

View File

@ -120,7 +120,7 @@ describe("Moves - Dive", () => {
await game.phaseInterceptor.to("TurnEndPhase");
await game.phaseInterceptor.to("TurnStartPhase", false);
game.scene.arena.trySetWeather(WeatherType.HARSH_SUN, false);
game.scene.arena.trySetWeather(WeatherType.HARSH_SUN);
await game.phaseInterceptor.to("MoveEndPhase");
expect(playerPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.FAIL);