diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index fdc32b75c19..60eff2492b2 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -1,23 +1,23 @@ -import { Arena } from "../field/arena"; -import { Type } from "./type"; -import * as Utils from "../utils"; -import { MoveCategory, allMoves, MoveTarget, IncrementMovePriorityAttr, applyMoveAttrs } from "./move"; -import { getPokemonNameWithAffix } from "../messages"; -import Pokemon, { HitResult, PokemonMove } from "../field/pokemon"; -import { StatusEffect } from "./status-effect"; -import { BattlerIndex } from "../battle"; -import { BlockNonDirectDamageAbAttr, ChangeMovePriorityAbAttr, ProtectStatAbAttr, applyAbAttrs } from "./ability"; -import { Stat } from "#enums/stat"; -import { CommonAnim, CommonBattleAnim } from "./battle-anims"; -import i18next from "i18next"; -import { Abilities } from "#enums/abilities"; -import { ArenaTagType } from "#enums/arena-tag-type"; -import { BattlerTagType } from "#enums/battler-tag-type"; -import { Moves } from "#enums/moves"; +import { BattlerIndex } from "#app/battle"; +import { BlockNonDirectDamageAbAttr, ChangeMovePriorityAbAttr, ProtectStatAbAttr, applyAbAttrs } from "#app/data/ability"; +import { CommonAnim, CommonBattleAnim } from "#app/data/battle-anims"; +import { IncrementMovePriorityAttr, MoveCategory, MoveTarget, allMoves, applyMoveAttrs } from "#app/data/move"; +import { StatusEffect } from "#app/data/status-effect"; +import { Type } from "#app/data/type"; +import { Arena } from "#app/field/arena"; +import Pokemon, { HitResult, PokemonMove } from "#app/field/pokemon"; +import { getPokemonNameWithAffix } from "#app/messages"; import { MoveEffectPhase } from "#app/phases/move-effect-phase"; import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase"; import { ShowAbilityPhase } from "#app/phases/show-ability-phase"; import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; +import * as Utils from "#app/utils"; +import { Abilities } from "#enums/abilities"; +import { ArenaTagType } from "#enums/arena-tag-type"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { Moves } from "#enums/moves"; +import { Stat } from "#enums/stat"; +import i18next from "i18next"; export enum ArenaTagSide { BOTH, @@ -26,20 +26,13 @@ export enum ArenaTagSide { } export abstract class ArenaTag { - public tagType: ArenaTagType; - public turnCount: integer; - public sourceMove?: Moves; - public sourceId?: integer; - public side: ArenaTagSide; - - - constructor(tagType: ArenaTagType, turnCount: integer, sourceMove: Moves | undefined, sourceId?: integer, side: ArenaTagSide = ArenaTagSide.BOTH) { - this.tagType = tagType; - this.turnCount = turnCount; - this.sourceMove = sourceMove; - this.sourceId = sourceId; - this.side = side; - } + constructor( + public tagType: ArenaTagType, + public turnCount: number, + public sourceMove?: Moves, + public sourceId?: number, + public side: ArenaTagSide = ArenaTagSide.BOTH + ) {} apply(arena: Arena, args: any[]): boolean { return true; @@ -64,6 +57,18 @@ export abstract class ArenaTag { ? allMoves[this.sourceMove].name : null; } + + /** + * When given a arena tag or json representing one, load the data for it. + * This is meant to be inherited from by any arena tag with custom attributes + * @param {ArenaTag | any} source An arena tag + */ + loadTag(source : ArenaTag | any) : void { + this.turnCount = source.turnCount; + this.sourceMove = source.sourceMove; + this.sourceId = source.sourceId; + this.side = source.side; + } } /** @@ -71,7 +76,7 @@ export abstract class ArenaTag { * Prevents Pokémon on the opposing side from lowering the stats of the Pokémon in the Mist. */ export class MistTag extends ArenaTag { - constructor(turnCount: integer, sourceId: integer, side: ArenaTagSide) { + constructor(turnCount: number, sourceId: number, side: ArenaTagSide) { super(ArenaTagType.MIST, turnCount, Moves.MIST, sourceId, side); } @@ -115,7 +120,7 @@ export class WeakenMoveScreenTag extends ArenaTag { * @param side - The side (player or enemy) the tag affects. * @param weakenedCategories - The categories of moves that are weakened by this tag. */ - constructor(tagType: ArenaTagType, turnCount: integer, sourceMove: Moves, sourceId: integer, side: ArenaTagSide, weakenedCategories: MoveCategory[]) { + constructor(tagType: ArenaTagType, turnCount: number, sourceMove: Moves, sourceId: number, side: ArenaTagSide, weakenedCategories: MoveCategory[]) { super(tagType, turnCount, sourceMove, sourceId, side); this.weakenedCategories = weakenedCategories; @@ -146,7 +151,7 @@ export class WeakenMoveScreenTag extends ArenaTag { * Used by {@linkcode Moves.REFLECT} */ class ReflectTag extends WeakenMoveScreenTag { - constructor(turnCount: integer, sourceId: integer, side: ArenaTagSide) { + constructor(turnCount: number, sourceId: number, side: ArenaTagSide) { super(ArenaTagType.REFLECT, turnCount, Moves.REFLECT, sourceId, side, [MoveCategory.PHYSICAL]); } @@ -162,7 +167,7 @@ class ReflectTag extends WeakenMoveScreenTag { * Used by {@linkcode Moves.LIGHT_SCREEN} */ class LightScreenTag extends WeakenMoveScreenTag { - constructor(turnCount: integer, sourceId: integer, side: ArenaTagSide) { + constructor(turnCount: number, sourceId: number, side: ArenaTagSide) { super(ArenaTagType.LIGHT_SCREEN, turnCount, Moves.LIGHT_SCREEN, sourceId, side, [MoveCategory.SPECIAL]); } @@ -178,7 +183,7 @@ class LightScreenTag extends WeakenMoveScreenTag { * Used by {@linkcode Moves.AURORA_VEIL} */ class AuroraVeilTag extends WeakenMoveScreenTag { - constructor(turnCount: integer, sourceId: integer, side: ArenaTagSide) { + constructor(turnCount: number, sourceId: number, side: ArenaTagSide) { super(ArenaTagType.AURORA_VEIL, turnCount, Moves.AURORA_VEIL, sourceId, side, [MoveCategory.SPECIAL, MoveCategory.PHYSICAL]); } @@ -201,7 +206,7 @@ export class ConditionalProtectTag extends ArenaTag { /** Does this apply to all moves, including those that ignore other forms of protection? */ protected ignoresBypass: boolean; - constructor(tagType: ArenaTagType, sourceMove: Moves, sourceId: integer, side: ArenaTagSide, condition: ProtectConditionFunc, ignoresBypass: boolean = false) { + constructor(tagType: ArenaTagType, sourceMove: Moves, sourceId: number, side: ArenaTagSide, condition: ProtectConditionFunc, ignoresBypass: boolean = false) { super(tagType, 1, sourceMove, sourceId, side); this.protectConditionFunc = condition; @@ -263,7 +268,7 @@ export class ConditionalProtectTag extends ArenaTag { */ const QuickGuardConditionFunc: ProtectConditionFunc = (arena, moveId) => { const move = allMoves[moveId]; - const priority = new Utils.IntegerHolder(move.priority); + const priority = new Utils.NumberHolder(move.priority); const effectPhase = arena.scene.getCurrentPhase(); if (effectPhase instanceof MoveEffectPhase) { @@ -279,7 +284,7 @@ const QuickGuardConditionFunc: ProtectConditionFunc = (arena, moveId) => { * Condition: The incoming move has increased priority. */ class QuickGuardTag extends ConditionalProtectTag { - constructor(sourceId: integer, side: ArenaTagSide) { + constructor(sourceId: number, side: ArenaTagSide) { super(ArenaTagType.QUICK_GUARD, Moves.QUICK_GUARD, sourceId, side, QuickGuardConditionFunc); } } @@ -310,7 +315,7 @@ const WideGuardConditionFunc: ProtectConditionFunc = (arena, moveId) : boolean = * can be an ally or enemy. */ class WideGuardTag extends ConditionalProtectTag { - constructor(sourceId: integer, side: ArenaTagSide) { + constructor(sourceId: number, side: ArenaTagSide) { super(ArenaTagType.WIDE_GUARD, Moves.WIDE_GUARD, sourceId, side, WideGuardConditionFunc); } } @@ -332,7 +337,7 @@ const MatBlockConditionFunc: ProtectConditionFunc = (arena, moveId) : boolean => * Condition: The incoming move is a Physical or Special attack move. */ class MatBlockTag extends ConditionalProtectTag { - constructor(sourceId: integer, side: ArenaTagSide) { + constructor(sourceId: number, side: ArenaTagSide) { super(ArenaTagType.MAT_BLOCK, Moves.MAT_BLOCK, sourceId, side, MatBlockConditionFunc); } @@ -370,7 +375,7 @@ const CraftyShieldConditionFunc: ProtectConditionFunc = (arena, moveId) => { * not target all Pokemon or sides of the field. */ class CraftyShieldTag extends ConditionalProtectTag { - constructor(sourceId: integer, side: ArenaTagSide) { + constructor(sourceId: number, side: ArenaTagSide) { super(ArenaTagType.CRAFTY_SHIELD, Moves.CRAFTY_SHIELD, sourceId, side, CraftyShieldConditionFunc, true); } } @@ -382,12 +387,12 @@ class CraftyShieldTag extends ConditionalProtectTag { export class NoCritTag extends ArenaTag { /** * Constructor method for the NoCritTag class - * @param turnCount `integer` the number of turns this effect lasts + * @param turnCount `number` the number of turns this effect lasts * @param sourceMove {@linkcode Moves} the move that created this effect - * @param sourceId `integer` the ID of the {@linkcode Pokemon} that created this effect + * @param sourceId `number` the ID of the {@linkcode Pokemon} that created this effect * @param side {@linkcode ArenaTagSide} the side to which this effect belongs */ - constructor(turnCount: integer, sourceMove: Moves, sourceId: integer, side: ArenaTagSide) { + constructor(turnCount: number, sourceMove: Moves, sourceId: number, side: ArenaTagSide) { super(ArenaTagType.NO_CRIT, turnCount, sourceMove, sourceId, side); } @@ -417,7 +422,7 @@ class WishTag extends ArenaTag { private triggerMessage: string; private healHp: number; - constructor(turnCount: integer, sourceId: integer, side: ArenaTagSide) { + constructor(turnCount: number, sourceId: number, side: ArenaTagSide) { super(ArenaTagType.WISH, turnCount, Moves.WISH, sourceId, side); } @@ -458,7 +463,7 @@ export class WeakenMoveTypeTag extends ArenaTag { * @param sourceMove - The move that created the tag. * @param sourceId - The ID of the source of the tag. */ - constructor(tagType: ArenaTagType, turnCount: integer, type: Type, sourceMove: Moves, sourceId: integer) { + constructor(tagType: ArenaTagType, turnCount: number, type: Type, sourceMove: Moves, sourceId: number) { super(tagType, turnCount, sourceMove, sourceId); this.weakenedType = type; @@ -479,7 +484,7 @@ export class WeakenMoveTypeTag extends ArenaTag { * Weakens Electric type moves for a set amount of turns, usually 5. */ class MudSportTag extends WeakenMoveTypeTag { - constructor(turnCount: integer, sourceId: integer) { + constructor(turnCount: number, sourceId: number) { super(ArenaTagType.MUD_SPORT, turnCount, Type.ELECTRIC, Moves.MUD_SPORT, sourceId); } @@ -497,7 +502,7 @@ class MudSportTag extends WeakenMoveTypeTag { * Weakens Fire type moves for a set amount of turns, usually 5. */ class WaterSportTag extends WeakenMoveTypeTag { - constructor(turnCount: integer, sourceId: integer) { + constructor(turnCount: number, sourceId: number) { super(ArenaTagType.WATER_SPORT, turnCount, Type.FIRE, Moves.WATER_SPORT, sourceId); } @@ -514,8 +519,8 @@ class WaterSportTag extends WeakenMoveTypeTag { * Abstract class to implement arena traps. */ export class ArenaTrapTag extends ArenaTag { - public layers: integer; - public maxLayers: integer; + public layers: number; + public maxLayers: number; /** * Creates a new instance of the ArenaTrapTag class. @@ -526,7 +531,7 @@ export class ArenaTrapTag extends ArenaTag { * @param side - The side (player or enemy) the tag affects. * @param maxLayers - The maximum amount of layers this tag can have. */ - constructor(tagType: ArenaTagType, sourceMove: Moves, sourceId: integer, side: ArenaTagSide, maxLayers: integer) { + constructor(tagType: ArenaTagType, sourceMove: Moves, sourceId: number, side: ArenaTagSide, maxLayers: number) { super(tagType, 0, sourceMove, sourceId, side); this.layers = 1; @@ -557,6 +562,12 @@ export class ArenaTrapTag extends ArenaTag { getMatchupScoreMultiplier(pokemon: Pokemon): number { return pokemon.isGrounded() ? 1 : Phaser.Math.Linear(0, 1 / Math.pow(2, this.layers), Math.min(pokemon.getHpRatio(), 0.5) * 2); } + + loadTag(source: any): void { + super.loadTag(source); + this.layers = source.layers; + this.maxLayers = source.maxLayers; + } } /** @@ -565,7 +576,7 @@ export class ArenaTrapTag extends ArenaTag { * in damage for 1, 2, or 3 layers of Spikes respectively if they are summoned into this trap. */ class SpikesTag extends ArenaTrapTag { - constructor(sourceId: integer, side: ArenaTagSide) { + constructor(sourceId: number, side: ArenaTagSide) { super(ArenaTagType.SPIKES, Moves.SPIKES, sourceId, side, 3); } @@ -609,7 +620,7 @@ class SpikesTag extends ArenaTrapTag { class ToxicSpikesTag extends ArenaTrapTag { private neutralized: boolean; - constructor(sourceId: integer, side: ArenaTagSide) { + constructor(sourceId: number, side: ArenaTagSide) { super(ArenaTagType.TOXIC_SPIKES, Moves.TOXIC_SPIKES, sourceId, side, 2); this.neutralized = false; } @@ -667,7 +678,7 @@ class ToxicSpikesTag extends ArenaTrapTag { class DelayedAttackTag extends ArenaTag { public targetIndex: BattlerIndex; - constructor(tagType: ArenaTagType, sourceMove: Moves | undefined, sourceId: integer, targetIndex: BattlerIndex) { + constructor(tagType: ArenaTagType, sourceMove: Moves | undefined, sourceId: number, targetIndex: BattlerIndex) { super(tagType, 3, sourceMove, sourceId); this.targetIndex = targetIndex; @@ -692,7 +703,7 @@ class DelayedAttackTag extends ArenaTag { * who is summoned into the trap, based on the Rock type's type effectiveness. */ class StealthRockTag extends ArenaTrapTag { - constructor(sourceId: integer, side: ArenaTagSide) { + constructor(sourceId: number, side: ArenaTagSide) { super(ArenaTagType.STEALTH_ROCK, Moves.STEALTH_ROCK, sourceId, side, 1); } @@ -768,7 +779,7 @@ class StealthRockTag extends ArenaTrapTag { * to any Pokémon who is summoned into this trap. */ class StickyWebTag extends ArenaTrapTag { - constructor(sourceId: integer, side: ArenaTagSide) { + constructor(sourceId: number, side: ArenaTagSide) { super(ArenaTagType.STICKY_WEB, Moves.STICKY_WEB, sourceId, side, 1); } @@ -802,7 +813,7 @@ class StickyWebTag extends ArenaTrapTag { * also reversing the turn order for all Pokémon on the field as well. */ export class TrickRoomTag extends ArenaTag { - constructor(turnCount: integer, sourceId: integer) { + constructor(turnCount: number, sourceId: number) { super(ArenaTagType.TRICK_ROOM, turnCount, Moves.TRICK_ROOM, sourceId); } @@ -830,7 +841,7 @@ export class TrickRoomTag extends ArenaTag { * {@linkcode Abilities.LEVITATE} for the duration of the arena tag, usually 5 turns. */ export class GravityTag extends ArenaTag { - constructor(turnCount: integer) { + constructor(turnCount: number) { super(ArenaTagType.GRAVITY, turnCount, Moves.GRAVITY); } @@ -854,7 +865,7 @@ export class GravityTag extends ArenaTag { * Applies this arena tag for 4 turns (including the turn the move was used). */ class TailwindTag extends ArenaTag { - constructor(turnCount: integer, sourceId: integer, side: ArenaTagSide) { + constructor(turnCount: number, sourceId: number, side: ArenaTagSide) { super(ArenaTagType.TAILWIND, turnCount, Moves.TAILWIND, sourceId, side); } @@ -892,7 +903,7 @@ class TailwindTag extends ArenaTag { * Doubles the prize money from trainers and money moves like {@linkcode Moves.PAY_DAY} and {@linkcode Moves.MAKE_IT_RAIN}. */ class HappyHourTag extends ArenaTag { - constructor(turnCount: integer, sourceId: integer, side: ArenaTagSide) { + constructor(turnCount: number, sourceId: number, side: ArenaTagSide) { super(ArenaTagType.HAPPY_HOUR, turnCount, Moves.HAPPY_HOUR, sourceId, side); } @@ -906,7 +917,7 @@ class HappyHourTag extends ArenaTag { } class SafeguardTag extends ArenaTag { - constructor(turnCount: integer, sourceId: integer, side: ArenaTagSide) { + constructor(turnCount: number, sourceId: number, side: ArenaTagSide) { super(ArenaTagType.SAFEGUARD, turnCount, Moves.SAFEGUARD, sourceId, side); } @@ -919,8 +930,14 @@ class SafeguardTag extends ArenaTag { } } +class NoneTag extends ArenaTag { + constructor() { + super(ArenaTagType.NONE, 0); + } +} -export function getArenaTag(tagType: ArenaTagType, turnCount: integer, sourceMove: Moves | undefined, sourceId: integer, targetIndex?: BattlerIndex, side: ArenaTagSide = ArenaTagSide.BOTH): ArenaTag | null { +// TODO: swap `sourceMove` and `sourceId` and make `sourceMove` an optional parameter +export function getArenaTag(tagType: ArenaTagType, turnCount: number, sourceMove: Moves | undefined, sourceId: number, targetIndex?: BattlerIndex, side: ArenaTagSide = ArenaTagSide.BOTH): ArenaTag | null { switch (tagType) { case ArenaTagType.MIST: return new MistTag(turnCount, sourceId, side); @@ -971,3 +988,16 @@ export function getArenaTag(tagType: ArenaTagType, turnCount: integer, sourceMov return null; } } + +/** + * When given a battler tag or json representing one, creates an actual ArenaTag object with the same data. + * @param {ArenaTag | any} source An arena tag + * @return {ArenaTag} The valid arena tag + */ +export function loadArenaTag(source: ArenaTag | any): ArenaTag { + const tag = getArenaTag(source.tagType, source.turnCount, source.sourceMove, source.sourceId, source.targetIndex, source.side) + ?? new NoneTag(); + tag.loadTag(source); + return tag; +} + diff --git a/src/phases/check-status-effect-phase.ts b/src/phases/check-status-effect-phase.ts new file mode 100644 index 00000000000..44918b54966 --- /dev/null +++ b/src/phases/check-status-effect-phase.ts @@ -0,0 +1,23 @@ +import { PostTurnStatusEffectPhase } from "#app/phases/post-turn-status-effect-phase"; +import { Phase } from "#app/phase"; +import { BattlerIndex } from "#app/battle"; +import BattleScene from "#app/battle-scene"; + +export class CheckStatusEffectPhase extends Phase { + private order : BattlerIndex[]; + constructor(scene : BattleScene, order : BattlerIndex[]) { + super(scene); + this.scene = scene; + this.order = order; + } + + start() { + const field = this.scene.getField(); + for (const o of this.order) { + if (field[o].status && field[o].status.isPostTurn()) { + this.scene.unshiftPhase(new PostTurnStatusEffectPhase(this.scene, o)); + } + } + this.end(); + } +} diff --git a/src/phases/obtain-status-effect-phase.ts b/src/phases/obtain-status-effect-phase.ts index 93bf4cd41d5..3af1b6cd4c0 100644 --- a/src/phases/obtain-status-effect-phase.ts +++ b/src/phases/obtain-status-effect-phase.ts @@ -6,7 +6,6 @@ import { StatusEffect } from "#app/enums/status-effect"; import Pokemon from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; import { PokemonPhase } from "./pokemon-phase"; -import { PostTurnStatusEffectPhase } from "./post-turn-status-effect-phase"; export class ObtainStatusEffectPhase extends PokemonPhase { private statusEffect: StatusEffect | undefined; @@ -33,9 +32,6 @@ export class ObtainStatusEffectPhase extends PokemonPhase { pokemon.updateInfo(true); new CommonBattleAnim(CommonAnim.POISON + (this.statusEffect! - 1), pokemon).play(this.scene, false, () => { this.scene.queueMessage(getStatusEffectObtainText(this.statusEffect, getPokemonNameWithAffix(pokemon), this.sourceText ?? undefined)); - if (pokemon.status?.isPostTurn()) { - this.scene.pushPhase(new PostTurnStatusEffectPhase(this.scene, this.battlerIndex)); - } this.end(); }); return; diff --git a/src/phases/turn-start-phase.ts b/src/phases/turn-start-phase.ts index 5c1af4228c6..40b5b57aac5 100644 --- a/src/phases/turn-start-phase.ts +++ b/src/phases/turn-start-phase.ts @@ -13,10 +13,10 @@ import { BerryPhase } from "./berry-phase"; import { FieldPhase } from "./field-phase"; import { MoveHeaderPhase } from "./move-header-phase"; import { MovePhase } from "./move-phase"; -import { PostTurnStatusEffectPhase } from "./post-turn-status-effect-phase"; import { SwitchSummonPhase } from "./switch-summon-phase"; import { TurnEndPhase } from "./turn-end-phase"; import { WeatherEffectPhase } from "./weather-effect-phase"; +import { CheckStatusEffectPhase } from "#app/phases/check-status-effect-phase"; import { BattlerIndex } from "#app/battle"; import { TrickRoomTag } from "#app/data/arena-tag"; @@ -207,11 +207,8 @@ export class TurnStartPhase extends FieldPhase { this.scene.pushPhase(new WeatherEffectPhase(this.scene)); - for (const o of moveOrder) { - if (field[o].status && field[o].status.isPostTurn()) { - this.scene.pushPhase(new PostTurnStatusEffectPhase(this.scene, o)); - } - } + /** Add a new phase to check who should be taking status damage */ + this.scene.pushPhase(new CheckStatusEffectPhase(this.scene, moveOrder)); this.scene.pushPhase(new BerryPhase(this.scene)); this.scene.pushPhase(new TurnEndPhase(this.scene)); diff --git a/src/system/arena-data.ts b/src/system/arena-data.ts index 5b907805372..ba37de0ed0e 100644 --- a/src/system/arena-data.ts +++ b/src/system/arena-data.ts @@ -1,5 +1,5 @@ import { Arena } from "../field/arena"; -import { ArenaTag } from "../data/arena-tag"; +import { ArenaTag, loadArenaTag } from "../data/arena-tag"; import { Biome } from "#enums/biome"; import { Weather } from "../data/weather"; import { Terrain } from "#app/data/terrain"; @@ -15,6 +15,10 @@ export default class ArenaData { this.biome = sourceArena ? sourceArena.biomeType : source.biome; this.weather = sourceArena ? sourceArena.weather : source.weather ? new Weather(source.weather.weatherType, source.weather.turnsLeft) : null; this.terrain = sourceArena ? sourceArena.terrain : source.terrain ? new Terrain(source.terrain.terrainType, source.terrain.turnsLeft) : null; - this.tags = sourceArena ? sourceArena.tags : []; + this.tags = []; + + if (source.tags) { + this.tags = source.tags.map(t => loadArenaTag(t)); + } } } diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 04fef4a81da..a0a78a41162 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -28,8 +28,8 @@ import { speciesEggMoves } from "../data/egg-moves"; import { allMoves } from "../data/move"; import { TrainerVariant } from "../field/trainer"; import { Variant } from "#app/data/variant"; -import {setSettingGamepad, SettingGamepad, settingGamepadDefaults} from "./settings/settings-gamepad"; -import {setSettingKeyboard, SettingKeyboard} from "#app/system/settings/settings-keyboard"; +import { setSettingGamepad, SettingGamepad, settingGamepadDefaults } from "./settings/settings-gamepad"; +import { setSettingKeyboard, SettingKeyboard } from "#app/system/settings/settings-keyboard"; import { TerrainChangedEvent, WeatherChangedEvent } from "#app/events/arena"; import * as Modifier from "../modifier/modifier"; import { StatusEffect } from "#app/data/status-effect"; @@ -45,6 +45,8 @@ import { TerrainType } from "#app/data/terrain"; import { OutdatedPhase } from "#app/phases/outdated-phase"; import { ReloadSessionPhase } from "#app/phases/reload-session-phase"; import { RUN_HISTORY_LIMIT } from "#app/ui/run-history-ui-handler"; +import { TagAddedEvent } from "#app/events/arena"; +import { ArenaTrapTag } from "#app/data/arena-tag"; import { applySessionDataPatches, applySettingsDataPatches, applySystemDataPatches } from "./version-converter"; import { MysteryEncounterSaveData } from "../data/mystery-encounters/mystery-encounter-save-data"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; @@ -1071,8 +1073,18 @@ export class GameData { scene.arena.terrain = sessionData.arena.terrain; scene.arena.eventTarget.dispatchEvent(new TerrainChangedEvent(TerrainType.NONE, scene.arena.terrain?.terrainType!, scene.arena.terrain?.turnsLeft!)); // TODO: is this bang correct? - // TODO - //scene.arena.tags = sessionData.arena.tags; + + scene.arena.tags = sessionData.arena.tags; + if (scene.arena.tags) { + for (const tag of scene.arena.tags) { + if (tag instanceof ArenaTrapTag) { + const { tagType, side, turnCount, layers, maxLayers } = tag as ArenaTrapTag; + scene.arena.eventTarget.dispatchEvent(new TagAddedEvent(tagType, side, turnCount, layers, maxLayers)); + } else { + scene.arena.eventTarget.dispatchEvent(new TagAddedEvent(tag.tagType, tag.side, tag.turnCount)); + } + } + } for (const modifierData of sessionData.modifiers) { const modifier = modifierData.toModifier(scene, Modifier[modifierData.className]); diff --git a/src/test/items/toxic_orb.test.ts b/src/test/items/toxic_orb.test.ts index 95336c0793e..74309369b4d 100644 --- a/src/test/items/toxic_orb.test.ts +++ b/src/test/items/toxic_orb.test.ts @@ -1,7 +1,4 @@ import { StatusEffect } from "#app/data/status-effect"; -import { EnemyCommandPhase } from "#app/phases/enemy-command-phase"; -import { MessagePhase } from "#app/phases/message-phase"; -import { TurnEndPhase } from "#app/phases/turn-end-phase"; import i18next, { initI18n } from "#app/plugins/i18n"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; @@ -10,6 +7,7 @@ import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +const TIMEOUT = 20 * 1000; describe("Items - Toxic orb", () => { let phaserGame: Phaser.Game; @@ -27,42 +25,34 @@ describe("Items - Toxic orb", () => { beforeEach(() => { game = new GameManager(phaserGame); - const moveToUse = Moves.GROWTH; - const oppMoveToUse = Moves.TACKLE; - game.override.battleType("single"); - game.override.enemySpecies(Species.RATTATA); - game.override.ability(Abilities.INSOMNIA); - game.override.enemyAbility(Abilities.INSOMNIA); - game.override.startingLevel(2000); - game.override.moveset([moveToUse]); - game.override.enemyMoveset([oppMoveToUse, oppMoveToUse, oppMoveToUse, oppMoveToUse]); - game.override.startingHeldItems([{ - name: "TOXIC_ORB", - }]); + game.override + .battleType("single") + .enemySpecies(Species.RATTATA) + .ability(Abilities.BALL_FETCH) + .enemyAbility(Abilities.BALL_FETCH) + .moveset([Moves.SPLASH]) + .enemyMoveset(Moves.SPLASH) + .startingHeldItems([{ + name: "TOXIC_ORB", + }]); }); - it("TOXIC ORB", async () => { + it("badly poisons the holder", async () => { initI18n(); i18next.changeLanguage("en"); - const moveToUse = Moves.GROWTH; - await game.startBattle([ - Species.MIGHTYENA, - Species.MIGHTYENA, - ]); - expect(game.scene.modifiers[0].type.id).toBe("TOXIC_ORB"); + await game.classicMode.startBattle([Species.MIGHTYENA]); - game.move.select(moveToUse); + const player = game.scene.getPlayerField()[0]; - // will run the 13 phase from enemyCommandPhase to TurnEndPhase - await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(TurnEndPhase); + game.move.select(Moves.SPLASH); + + await game.phaseInterceptor.to("TurnEndPhase"); // Toxic orb should trigger here - await game.phaseInterceptor.run(MessagePhase); + await game.phaseInterceptor.run("MessagePhase"); const message = game.textInterceptor.getLatestMessage(); expect(message).toContain("was badly poisoned by the Toxic Orb"); - await game.phaseInterceptor.run(MessagePhase); - const message2 = game.textInterceptor.getLatestMessage(); - expect(message2).toContain("is hurt"); - expect(message2).toContain("by poison"); - expect(game.scene.getParty()[0].status!.effect).toBe(StatusEffect.TOXIC); - }, 20000); + expect(player.status?.effect).toBe(StatusEffect.TOXIC); + // Damage should not have ticked yet. + expect(player.status?.turnCount).toBe(0); + }, TIMEOUT); }); diff --git a/src/test/moves/toxic_spikes.test.ts b/src/test/moves/toxic_spikes.test.ts new file mode 100644 index 00000000000..c6264045f21 --- /dev/null +++ b/src/test/moves/toxic_spikes.test.ts @@ -0,0 +1,136 @@ +import { ArenaTagSide, ArenaTrapTag } from "#app/data/arena-tag"; +import { StatusEffect } from "#app/data/status-effect"; +import { decrypt, encrypt, GameData, SessionSaveData } from "#app/system/game-data"; +import { Abilities } from "#enums/abilities"; +import { ArenaTagType } from "#enums/arena-tag-type"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +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 - Toxic Spikes", () => { + 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") + .startingWave(5) + .enemySpecies(Species.RATTATA) + .enemyAbility(Abilities.BALL_FETCH) + .ability(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH) + .moveset([Moves.TOXIC_SPIKES, Moves.SPLASH, Moves.ROAR]); + }); + + it("should not affect the opponent if they do not switch", async() => { + await game.classicMode.runToSummon([Species.MIGHTYENA, Species.POOCHYENA]); + + const enemy = game.scene.getEnemyField()[0]; + + game.move.select(Moves.TOXIC_SPIKES); + await game.phaseInterceptor.to("TurnEndPhase"); + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("TurnEndPhase"); + game.doSwitchPokemon(1); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(enemy.hp).toBe(enemy.getMaxHp()); + expect(enemy.status?.effect).toBeUndefined(); + }, TIMEOUT); + + it("should poison the opponent if they switch into 1 layer", async() => { + await game.classicMode.runToSummon([Species.MIGHTYENA]); + + game.move.select(Moves.TOXIC_SPIKES); + await game.phaseInterceptor.to("TurnEndPhase"); + game.move.select(Moves.ROAR); + await game.phaseInterceptor.to("TurnEndPhase"); + + const enemy = game.scene.getEnemyField()[0]; + + expect(enemy.hp).toBeLessThan(enemy.getMaxHp()); + expect(enemy.status?.effect).toBe(StatusEffect.POISON); + }, TIMEOUT); + + it("should badly poison the opponent if they switch into 2 layers", async() => { + await game.classicMode.runToSummon([Species.MIGHTYENA]); + + game.move.select(Moves.TOXIC_SPIKES); + await game.phaseInterceptor.to("TurnEndPhase"); + game.move.select(Moves.TOXIC_SPIKES); + await game.phaseInterceptor.to("TurnEndPhase"); + game.move.select(Moves.ROAR); + await game.phaseInterceptor.to("TurnEndPhase"); + + const enemy = game.scene.getEnemyField()[0]; + expect(enemy.hp).toBeLessThan(enemy.getMaxHp()); + expect(enemy.status?.effect).toBe(StatusEffect.TOXIC); + }, TIMEOUT); + + it("should be removed if a grounded poison pokemon switches in", async() => { + game.override.enemySpecies(Species.GRIMER); + await game.classicMode.runToSummon([Species.MIGHTYENA]); + + game.move.select(Moves.TOXIC_SPIKES); + await game.phaseInterceptor.to("TurnEndPhase"); + game.move.select(Moves.TOXIC_SPIKES); + await game.phaseInterceptor.to("TurnEndPhase"); + game.move.select(Moves.ROAR); + await game.phaseInterceptor.to("TurnEndPhase"); + + const enemy = game.scene.getEnemyField()[0]; + expect(enemy.hp).toBe(enemy.getMaxHp()); + expect(enemy.status?.effect).toBeUndefined(); + + expect(game.scene.arena.tags.length).toBe(0); + }, TIMEOUT); + + it("shouldn't create multiple layers per use in doubles", async() => { + await game.classicMode.runToSummon([Species.MIGHTYENA, Species.POOCHYENA]); + + game.move.select(Moves.TOXIC_SPIKES); + await game.phaseInterceptor.to("TurnEndPhase"); + + const arenaTags = (game.scene.arena.getTagOnSide(ArenaTagType.TOXIC_SPIKES, ArenaTagSide.ENEMY) as ArenaTrapTag); + expect(arenaTags.tagType).toBe(ArenaTagType.TOXIC_SPIKES); + expect(arenaTags.layers).toBe(1); + }, TIMEOUT); + + it("should persist through reload", async() => { + game.override.startingWave(1); + const scene = game.scene; + const gameData = new GameData(scene); + + await game.classicMode.runToSummon([Species.MIGHTYENA]); + + game.move.select(Moves.TOXIC_SPIKES); + await game.phaseInterceptor.to("TurnEndPhase"); + game.move.select(Moves.SPLASH); + await game.doKillOpponents(); + await game.phaseInterceptor.to("BattleEndPhase"); + await game.toNextWave(); + + const sessionData : SessionSaveData = gameData["getSessionSaveData"](game.scene); + localStorage.setItem("sessionTestData", encrypt(JSON.stringify(sessionData), true)); + const recoveredData : SessionSaveData = gameData.parseSessionData(decrypt(localStorage.getItem("sessionTestData")!, true)); + gameData.loadSession(game.scene, 0, recoveredData); + + expect(sessionData.arena.tags).toEqual(recoveredData.arena.tags); + localStorage.removeItem("sessionTestData"); + }, TIMEOUT); +});