pokemon-showdown/sim/field.ts
Guangcong Luo f9fdc73133
Support per-pokemon Residual handlers in Side/Field conditions (#8222)
For side conditions, `onStart`/`onRestart`/`onResidual`/`onEnd`
have been renamed `onSideStart`/`onSideRestart`/`onSideResidual`/`onSideEnd`,
with the `onResidualOrder` properties renamed `onSideResidualOrder`.

For field conditions, `onStart`/`onRestart`/`onResidual`/`onEnd`
have been renamed `onFieldStart`/`onFieldRestart`/`onFieldResidual`/`onFieldEnd`,
with the `onResidualOrder` properties renamed `onFieldResidualOrder`.

(The `onField` and `onSide` part helps make it clear to the type system
that the first argument is a Field or Side, not a Pokemon.)

Side and field conditions can now use `onResidual` to tick separately
on each pokemon in Speed order. `onResidualOrder` (the per-pokemon
tick) can be timed separate from `onSideResidualOrder` (the
per-condition tick), allowing conditions to end at a different priority
than they tick per-pokemon.

Relatedly, `onTeamPreview` and `onStart` in formats now need to be
`onFieldTeamPreview` and `onFieldStart`.

Unrelatedly, `effectData` has been renamed `effectState`, and the
corresponding state containers (`pokemon.statusData`,
`pokemon.speciesData`, `pokemon.itemData`, `pokemon.abilityData`,
`field.weatherData`, `field.terrainData`) have been similarly renamed. I
renamed the types a while ago, but I was holding off renaming the fields
because it would be a breaking change. But this is a breaking change
anyway, so we might as well do it now.

Note: `onResidual` will tick even on `onSideEnd` turns, although
`onSideResidual` won't. When refactoring weather, remember to
check `this.state.duration` so you don't deal weather damage on the
ending turn.

Intended as a better fix for #8216
2021-04-25 10:55:54 -07:00

235 lines
7.2 KiB
TypeScript

/**
* Simulator Field
* Pokemon Showdown - http://pokemonshowdown.com/
*
* @license MIT
*/
import {State} from './state';
import {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 = {id: ''};
this.terrain = '';
this.terrainState = {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 && 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 = {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.runEvent('WeatherStart', source, source, status);
return true;
}
clearWeather() {
if (!this.weather) return false;
const prevWeather = this.getWeather();
this.battle.singleEvent('FieldEnd', prevWeather, this.weatherState, this);
this.weather = '';
this.weatherState = {id: ''};
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) {
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 && 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 = {
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.runEvent('TerrainStart', source, source, status);
return true;
}
clearTerrain() {
if (!this.terrain) return false;
const prevTerrain = this.getTerrain();
this.battle.singleEvent('FieldEnd', prevTerrain, this.terrainState, this);
this.terrain = '';
this.terrainState = {id: ''};
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 && 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] = {
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;
}
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;
}
destroy() {
// deallocate ourself
// get rid of some possibly-circular references
(this as any).battle = null!;
}
}