mirror of
https://github.com/smogon/pokemon-showdown.git
synced 2026-06-02 22:08:36 -05:00
284 lines
9.0 KiB
TypeScript
284 lines
9.0 KiB
TypeScript
/**
|
|
* Simulator Field
|
|
* Pokemon Showdown - http://pokemonshowdown.com/
|
|
*
|
|
* @license MIT
|
|
*/
|
|
|
|
import { State } from './state';
|
|
import { type EffectState } from './pokemon';
|
|
import { toID } from './dex';
|
|
|
|
export class Field {
|
|
readonly battle: Battle;
|
|
readonly id: ID;
|
|
|
|
weather: ID;
|
|
weatherState: EffectState;
|
|
terrain: ID;
|
|
terrainState: EffectState;
|
|
pseudoWeather: { [id: string]: EffectState };
|
|
|
|
constructor(battle: Battle) {
|
|
this.battle = battle;
|
|
const fieldScripts = this.battle.format.field || this.battle.dex.data.Scripts.field;
|
|
if (fieldScripts) Object.assign(this, fieldScripts);
|
|
this.id = '';
|
|
|
|
this.weather = '';
|
|
this.weatherState = this.battle.initEffectState({ id: '' });
|
|
this.terrain = '';
|
|
this.terrainState = this.battle.initEffectState({ id: '' });
|
|
this.pseudoWeather = {};
|
|
}
|
|
|
|
toJSON(): AnyObject {
|
|
return State.serializeField(this);
|
|
}
|
|
|
|
setWeather(status: string | Condition, source: Pokemon | 'debug' | null = null, sourceEffect: Effect | null = null) {
|
|
status = this.battle.dex.conditions.get(status);
|
|
if (!sourceEffect && this.battle.effect) sourceEffect = this.battle.effect;
|
|
if (!source && this.battle.event?.target) source = this.battle.event.target;
|
|
if (source === 'debug') source = this.battle.sides[0].active[0];
|
|
|
|
if (this.weather === status.id) {
|
|
if (sourceEffect && sourceEffect.effectType === 'Ability') {
|
|
if (this.battle.gen > 5 || this.weatherState.duration === 0) {
|
|
return false;
|
|
}
|
|
} else if (this.battle.gen > 2 || status.id === 'sandstorm') {
|
|
return false;
|
|
}
|
|
}
|
|
if (source) {
|
|
const result = this.battle.runEvent('SetWeather', source, source, status);
|
|
if (!result) {
|
|
if (result === false) {
|
|
if ((sourceEffect as Move)?.weather) {
|
|
this.battle.add('-fail', source, sourceEffect, '[from] ' + this.weather);
|
|
} else if (sourceEffect && sourceEffect.effectType === 'Ability') {
|
|
this.battle.add('-ability', source, sourceEffect, '[from] ' + this.weather, '[fail]');
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
const prevWeather = this.weather;
|
|
const prevWeatherState = this.weatherState;
|
|
this.weather = status.id;
|
|
this.weatherState = this.battle.initEffectState({ id: status.id });
|
|
if (source) {
|
|
this.weatherState.source = source;
|
|
this.weatherState.sourceSlot = source.getSlot();
|
|
}
|
|
if (status.duration) {
|
|
this.weatherState.duration = status.duration;
|
|
}
|
|
if (status.durationCallback) {
|
|
if (!source) throw new Error(`setting weather without a source`);
|
|
this.weatherState.duration = status.durationCallback.call(this.battle, source, source, sourceEffect);
|
|
}
|
|
if (!this.battle.singleEvent('FieldStart', status, this.weatherState, this, source, sourceEffect)) {
|
|
this.weather = prevWeather;
|
|
this.weatherState = prevWeatherState;
|
|
return false;
|
|
}
|
|
this.battle.eachEvent('WeatherChange', sourceEffect);
|
|
return true;
|
|
}
|
|
|
|
clearWeather() {
|
|
if (!this.weather) return false;
|
|
const prevWeather = this.getWeather();
|
|
this.battle.singleEvent('FieldEnd', prevWeather, this.weatherState, this);
|
|
this.weather = '';
|
|
this.battle.clearEffectState(this.weatherState);
|
|
this.battle.eachEvent('WeatherChange');
|
|
return true;
|
|
}
|
|
|
|
effectiveWeather() {
|
|
if (this.suppressingWeather()) return '';
|
|
return this.weather;
|
|
}
|
|
|
|
suppressingWeather() {
|
|
for (const side of this.battle.sides) {
|
|
for (const pokemon of side.active) {
|
|
if (pokemon && !pokemon.fainted && !pokemon.ignoringAbility() &&
|
|
pokemon.getAbility().suppressWeather && !pokemon.abilityState.ending) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
isWeather(weather: string | string[]) {
|
|
const ourWeather = this.effectiveWeather();
|
|
if (!Array.isArray(weather)) {
|
|
return ourWeather === toID(weather);
|
|
}
|
|
return weather.map(toID).includes(ourWeather);
|
|
}
|
|
|
|
getWeather() {
|
|
return this.battle.dex.conditions.getByID(this.weather);
|
|
}
|
|
|
|
setTerrain(status: string | Effect, source: Pokemon | 'debug' | null = null, sourceEffect: Effect | null = null) {
|
|
status = this.battle.dex.conditions.get(status);
|
|
if (!sourceEffect && this.battle.effect) sourceEffect = this.battle.effect;
|
|
if (!source && this.battle.event?.target) source = this.battle.event.target;
|
|
if (source === 'debug') source = this.battle.sides[0].active[0];
|
|
if (!source) throw new Error(`setting terrain without a source`);
|
|
|
|
if (this.terrain === status.id) return false;
|
|
const prevTerrain = this.terrain;
|
|
const prevTerrainState = this.terrainState;
|
|
this.terrain = status.id;
|
|
this.terrainState = this.battle.initEffectState({
|
|
id: status.id,
|
|
source,
|
|
sourceSlot: source.getSlot(),
|
|
duration: status.duration,
|
|
});
|
|
if (status.durationCallback) {
|
|
this.terrainState.duration = status.durationCallback.call(this.battle, source, source, sourceEffect);
|
|
}
|
|
if (!this.battle.singleEvent('FieldStart', status, this.terrainState, this, source, sourceEffect)) {
|
|
this.terrain = prevTerrain;
|
|
this.terrainState = prevTerrainState;
|
|
return false;
|
|
}
|
|
this.battle.eachEvent('TerrainChange', sourceEffect);
|
|
return true;
|
|
}
|
|
|
|
clearTerrain() {
|
|
if (!this.terrain) return false;
|
|
const prevTerrain = this.getTerrain();
|
|
this.battle.singleEvent('FieldEnd', prevTerrain, this.terrainState, this);
|
|
this.terrain = '';
|
|
this.battle.clearEffectState(this.terrainState);
|
|
this.battle.eachEvent('TerrainChange');
|
|
return true;
|
|
}
|
|
|
|
effectiveTerrain(target?: Pokemon | Side | Battle) {
|
|
if (this.battle.event && !target) target = this.battle.event.target;
|
|
return this.battle.runEvent('TryTerrain', target) ? this.terrain : '';
|
|
}
|
|
|
|
isTerrain(terrain: string | string[], target?: Pokemon | Side | Battle) {
|
|
const ourTerrain = this.effectiveTerrain(target);
|
|
if (!Array.isArray(terrain)) {
|
|
return ourTerrain === toID(terrain);
|
|
}
|
|
return terrain.map(toID).includes(ourTerrain);
|
|
}
|
|
|
|
getTerrain() {
|
|
return this.battle.dex.conditions.getByID(this.terrain);
|
|
}
|
|
|
|
addPseudoWeather(
|
|
status: string | Condition,
|
|
source: Pokemon | 'debug' | null = null,
|
|
sourceEffect: Effect | null = null
|
|
): boolean {
|
|
if (!source && this.battle.event?.target) source = this.battle.event.target;
|
|
if (source === 'debug') source = this.battle.sides[0].active[0];
|
|
status = this.battle.dex.conditions.get(status);
|
|
|
|
let state = this.pseudoWeather[status.id];
|
|
if (state) {
|
|
if (!(status as any).onFieldRestart) return false;
|
|
return this.battle.singleEvent('FieldRestart', status, state, this, source, sourceEffect);
|
|
}
|
|
state = this.pseudoWeather[status.id] = this.battle.initEffectState({
|
|
id: status.id,
|
|
source,
|
|
sourceSlot: source?.getSlot(),
|
|
duration: status.duration,
|
|
});
|
|
if (status.durationCallback) {
|
|
if (!source) throw new Error(`setting fieldcond without a source`);
|
|
state.duration = status.durationCallback.call(this.battle, source, source, sourceEffect);
|
|
}
|
|
if (!this.battle.singleEvent('FieldStart', status, state, this, source, sourceEffect)) {
|
|
delete this.pseudoWeather[status.id];
|
|
return false;
|
|
}
|
|
this.battle.runEvent('PseudoWeatherChange', source, source, status);
|
|
return true;
|
|
}
|
|
|
|
getPseudoWeather(status: string | Effect) {
|
|
status = this.battle.dex.conditions.get(status);
|
|
return this.pseudoWeather[status.id] ? status : null;
|
|
}
|
|
|
|
removePseudoWeather(status: string | Effect) {
|
|
status = this.battle.dex.conditions.get(status);
|
|
const state = this.pseudoWeather[status.id];
|
|
if (!state) return false;
|
|
this.battle.singleEvent('FieldEnd', status, state, this);
|
|
delete this.pseudoWeather[status.id];
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Start a field effect if it is not already active and maintain a list of sources.
|
|
* The field effect is only removed when all sources deactivate it.
|
|
*/
|
|
addSourcedPseudoWeather(
|
|
status: string | Condition,
|
|
source: Pokemon,
|
|
sourceEffect: Effect | null = null
|
|
): boolean {
|
|
const returnValue = this.addPseudoWeather(status, source, sourceEffect);
|
|
status = this.battle.dex.conditions.get(status);
|
|
const state = this.pseudoWeather[status.id];
|
|
if (state) {
|
|
if (!state.activeSources) state.activeSources = [];
|
|
state.activeSources.push(source);
|
|
}
|
|
return returnValue;
|
|
}
|
|
|
|
/**
|
|
* Remove a source from a field effect. If no sources remain, the effect is removed.
|
|
*/
|
|
removePseudoWeatherSource(status: string | Effect, source: Pokemon) {
|
|
status = this.battle.dex.conditions.get(status);
|
|
const state = this.pseudoWeather[status.id];
|
|
if (!state) return false;
|
|
if (!state.activeSources) throw new Error(`removing pseudoweather without a source`);
|
|
state.activeSources = state.activeSources.filter((s: Pokemon) => s !== source);
|
|
if (state.activeSources.length) return false;
|
|
delete this.pseudoWeather[status.id];
|
|
this.battle.singleEvent('FieldEnd', status, state, this);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Remove a source from all active field effects. Used when a Pokemon leaves the field.
|
|
*/
|
|
removeSourceFromPseudoWeather(source: Pokemon) {
|
|
for (const id in this.pseudoWeather) {
|
|
if (this.pseudoWeather[id].activeSources) {
|
|
this.removePseudoWeatherSource(id, source);
|
|
}
|
|
}
|
|
}
|
|
|
|
destroy() {
|
|
// deallocate ourself
|
|
|
|
// get rid of some possibly-circular references
|
|
(this as any).battle = null!;
|
|
}
|
|
}
|