[Moves][Ability] Implement Torment / Taunt / Imprison + Aroma Veil (#4378)

* Torment

* Taunt and Imprison

* ability immunities

* Aroma Veil

* Imprison

* Test Files

* Added exceptions for Rollout and check for active ability

* adding tests so that git doesn't auto-fail

* Blah

* please

* some documentation

* Removed random newlines

* Added check for ability's presence mid battle

* Changed BattlerTagImmunityAbAttr to look at lists instead

* Work?

* Imprison and Taunt Tests

* Tests

* Final tests before documentation

* documentation blah

* Imports

* Flx Change

* flx - adding overrides

* Update src/data/arena-tag.ts

Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com>

* flx fixes

* quick docs

* privated retrieveField

* Handling undefined

* Update src/data/arena-tag.ts

Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com>

* forget to remove partials for heal block

* Apply suggestions from code review

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* Marked Torment as partial

* Update src/test/moves/torment.test.ts

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* tsdocs

* Prevents test pokemon from being immune to torment

* Update src/data/arena-tag.ts

Co-authored-by: Adrian T. <68144167+torranx@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Adrian T. <68144167+torranx@users.noreply.github.com>

* Torranx Fixes

* Check for this.source

* why

* lighting things with my mind on fire

* aRHGHSHDKSHD

---------

Co-authored-by: frutescens <info@laptop>
Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com>
Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
Co-authored-by: Adrian T. <68144167+torranx@users.noreply.github.com>
This commit is contained in:
Mumble 2024-09-25 14:52:48 -07:00 committed by GitHub
parent eab610ca22
commit 57f39efdae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 537 additions and 26 deletions

View File

@ -2026,6 +2026,7 @@ export class PostSummonAbAttr extends AbAttr {
return false; return false;
} }
} }
/** /**
* Removes specified arena tags when a Pokemon is summoned. * Removes specified arena tags when a Pokemon is summoned.
*/ */
@ -2852,17 +2853,17 @@ export class PreApplyBattlerTagAbAttr extends AbAttr {
* Provides immunity to BattlerTags {@linkcode BattlerTag} to specified targets. * Provides immunity to BattlerTags {@linkcode BattlerTag} to specified targets.
*/ */
export class PreApplyBattlerTagImmunityAbAttr extends PreApplyBattlerTagAbAttr { export class PreApplyBattlerTagImmunityAbAttr extends PreApplyBattlerTagAbAttr {
private immuneTagType: BattlerTagType; private immuneTagTypes: BattlerTagType[];
private battlerTag: BattlerTag; private battlerTag: BattlerTag;
constructor(immuneTagType: BattlerTagType) { constructor(immuneTagTypes: BattlerTagType | BattlerTagType[]) {
super(); super();
this.immuneTagType = immuneTagType; this.immuneTagTypes = Array.isArray(immuneTagTypes) ? immuneTagTypes : [immuneTagTypes];
} }
applyPreApplyBattlerTag(pokemon: Pokemon, passive: boolean, simulated: boolean, tag: BattlerTag, cancelled: Utils.BooleanHolder, args: any[]): boolean { applyPreApplyBattlerTag(pokemon: Pokemon, passive: boolean, simulated: boolean, tag: BattlerTag, cancelled: Utils.BooleanHolder, args: any[]): boolean {
if (tag.tagType === this.immuneTagType) { if (this.immuneTagTypes.includes(tag.tagType)) {
cancelled.value = true; cancelled.value = true;
if (!simulated) { if (!simulated) {
this.battlerTag = tag; this.battlerTag = tag;
@ -4916,7 +4917,7 @@ export function initAbilities() {
.attr(TypeImmunityHealAbAttr, Type.WATER) .attr(TypeImmunityHealAbAttr, Type.WATER)
.ignorable(), .ignorable(),
new Ability(Abilities.OBLIVIOUS, 3) new Ability(Abilities.OBLIVIOUS, 3)
.attr(BattlerTagImmunityAbAttr, BattlerTagType.INFATUATED) .attr(BattlerTagImmunityAbAttr, [BattlerTagType.INFATUATED, BattlerTagType.TAUNT])
.attr(IntimidateImmunityAbAttr) .attr(IntimidateImmunityAbAttr)
.ignorable(), .ignorable(),
new Ability(Abilities.CLOUD_NINE, 3) new Ability(Abilities.CLOUD_NINE, 3)
@ -5402,8 +5403,7 @@ export function initAbilities() {
.attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonTeravolt", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })) .attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonTeravolt", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }))
.attr(MoveAbilityBypassAbAttr), .attr(MoveAbilityBypassAbAttr),
new Ability(Abilities.AROMA_VEIL, 6) new Ability(Abilities.AROMA_VEIL, 6)
.ignorable() .attr(UserFieldBattlerTagImmunityAbAttr, [BattlerTagType.INFATUATED, BattlerTagType.TAUNT, BattlerTagType.DISABLED, BattlerTagType.TORMENT, BattlerTagType.HEAL_BLOCK]),
.unimplemented(),
new Ability(Abilities.FLOWER_VEIL, 6) new Ability(Abilities.FLOWER_VEIL, 6)
.ignorable() .ignorable()
.unimplemented(), .unimplemented(),
@ -5885,7 +5885,6 @@ export function initAbilities() {
.ignorable(), .ignorable(),
new Ability(Abilities.EARTH_EATER, 9) new Ability(Abilities.EARTH_EATER, 9)
.attr(TypeImmunityHealAbAttr, Type.GROUND) .attr(TypeImmunityHealAbAttr, Type.GROUND)
.partial() // Healing not blocked by Heal Block
.ignorable(), .ignorable(),
new Ability(Abilities.MYCELIUM_MIGHT, 9) new Ability(Abilities.MYCELIUM_MIGHT, 9)
.attr(ChangeMovePriorityAbAttr, (pokemon, move) => move.category === MoveCategory.STATUS, -0.2) .attr(ChangeMovePriorityAbAttr, (pokemon, move) => move.category === MoveCategory.STATUS, -0.2)
@ -5900,8 +5899,7 @@ export function initAbilities() {
.attr(PostSummonStatStageChangeAbAttr, [ Stat.EVA ], -1) .attr(PostSummonStatStageChangeAbAttr, [ Stat.EVA ], -1)
.condition(getOncePerBattleCondition(Abilities.SUPERSWEET_SYRUP)), .condition(getOncePerBattleCondition(Abilities.SUPERSWEET_SYRUP)),
new Ability(Abilities.HOSPITALITY, 9) new Ability(Abilities.HOSPITALITY, 9)
.attr(PostSummonAllyHealAbAttr, 4, true) .attr(PostSummonAllyHealAbAttr, 4, true),
.partial(), // Healing not blocked by Heal Block
new Ability(Abilities.TOXIC_CHAIN, 9) new Ability(Abilities.TOXIC_CHAIN, 9)
.attr(PostAttackApplyStatusEffectAbAttr, false, 30, StatusEffect.TOXIC), .attr(PostAttackApplyStatusEffectAbAttr, false, 30, StatusEffect.TOXIC),
new Ability(Abilities.EMBODY_ASPECT_TEAL, 9) new Ability(Abilities.EMBODY_ASPECT_TEAL, 9)

View File

@ -1,14 +1,15 @@
import { Arena } from "../field/arena"; import { Arena } from "#app/field/arena";
import { Type } from "./type"; import BattleScene from "#app/battle-scene";
import * as Utils from "../utils"; import { Type } from "#app/data/type";
import { MoveCategory, allMoves, MoveTarget, IncrementMovePriorityAttr, applyMoveAttrs } from "./move"; import * as Utils from "#app/utils";
import { getPokemonNameWithAffix } from "../messages"; import { MoveCategory, allMoves, MoveTarget, IncrementMovePriorityAttr, applyMoveAttrs } from "#app/data/move";
import Pokemon, { HitResult, PokemonMove } from "../field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages";
import { StatusEffect } from "./status-effect"; import Pokemon, { HitResult, PlayerPokemon, PokemonMove, EnemyPokemon } from "#app/field/pokemon";
import { BattlerIndex } from "../battle"; import { StatusEffect } from "#app/data/status-effect";
import { BlockNonDirectDamageAbAttr, ChangeMovePriorityAbAttr, ProtectStatAbAttr, applyAbAttrs } from "./ability"; import { BattlerIndex } from "#app/battle";
import { BlockNonDirectDamageAbAttr, ChangeMovePriorityAbAttr, ProtectStatAbAttr, applyAbAttrs } from "#app/data/ability";
import { Stat } from "#enums/stat"; import { Stat } from "#enums/stat";
import { CommonAnim, CommonBattleAnim } from "./battle-anims"; import { CommonAnim, CommonBattleAnim } from "#app/data/battle-anims";
import i18next from "i18next"; import i18next from "i18next";
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";
import { ArenaTagType } from "#enums/arena-tag-type"; import { ArenaTagType } from "#enums/arena-tag-type";
@ -919,6 +920,77 @@ class SafeguardTag extends ArenaTag {
} }
} }
/**
* This arena tag facilitates the application of the move Imprison
* Imprison remains in effect as long as the source Pokemon is active and present on the field.
* Imprison will apply to any opposing Pokemon that switch onto the field as well.
*/
class ImprisonTag extends ArenaTrapTag {
private source: Pokemon;
constructor(sourceId: number, side: ArenaTagSide) {
super(ArenaTagType.IMPRISON, Moves.IMPRISON, sourceId, side, 1);
}
/**
* Helper function that retrieves the Pokemon effected
* @param {BattleScene} scene medium to retrieve the involved Pokemon
* @returns list of PlayerPokemon or EnemyPokemon on the field
*/
private retrieveField(scene: BattleScene): PlayerPokemon[] | EnemyPokemon[] {
if (!this.source.isPlayer()) {
return scene.getPlayerField() ?? [];
}
return scene.getEnemyField() ?? [];
}
/**
* This function applies the effects of Imprison to the opposing Pokemon already present on the field.
* @param arena
*/
override onAdd({ scene }: Arena) {
this.source = scene.getPokemonById(this.sourceId!)!;
if (this.source) {
const party = this.retrieveField(scene);
party?.forEach((p: PlayerPokemon | EnemyPokemon ) => {
p.addTag(BattlerTagType.IMPRISON, 1, Moves.IMPRISON, this.sourceId);
});
scene.queueMessage(i18next.t("battlerTags:imprisonOnAdd", {pokemonNameWithAffix: getPokemonNameWithAffix(this.source)}));
}
}
/**
* Checks if the source Pokemon is still active on the field
* @param _arena
* @returns `true` if the source of the tag is still active on the field | `false` if not
*/
override lapse(_arena: Arena): boolean {
return this.source.isActive(true);
}
/**
* This applies the effects of Imprison to any opposing Pokemon that switch into the field while the source Pokemon is still active
* @param {Pokemon} pokemon the Pokemon Imprison is applied to
* @returns `true`
*/
override activateTrap(pokemon: Pokemon): boolean {
if (this.source.isActive(true)) {
pokemon.addTag(BattlerTagType.IMPRISON, 1, Moves.IMPRISON, this.sourceId);
}
return true;
}
/**
* When the arena tag is removed, it also attempts to remove any related Battler Tags if they haven't already been removed from the affected Pokemon
* @param arena
*/
override onRemove({ scene }: Arena): void {
const party = this.retrieveField(scene);
party?.forEach((p: PlayerPokemon | EnemyPokemon) => {
p.removeTag(BattlerTagType.IMPRISON);
});
}
}
export function getArenaTag(tagType: ArenaTagType, turnCount: integer, sourceMove: Moves | undefined, sourceId: integer, targetIndex?: BattlerIndex, side: ArenaTagSide = ArenaTagSide.BOTH): ArenaTag | null { export function getArenaTag(tagType: ArenaTagType, turnCount: integer, sourceMove: Moves | undefined, sourceId: integer, targetIndex?: BattlerIndex, side: ArenaTagSide = ArenaTagSide.BOTH): ArenaTag | null {
switch (tagType) { switch (tagType) {
@ -967,6 +1039,8 @@ export function getArenaTag(tagType: ArenaTagType, turnCount: integer, sourceMov
return new HappyHourTag(turnCount, sourceId, side); return new HappyHourTag(turnCount, sourceId, side);
case ArenaTagType.SAFEGUARD: case ArenaTagType.SAFEGUARD:
return new SafeguardTag(turnCount, sourceId, side); return new SafeguardTag(turnCount, sourceId, side);
case ArenaTagType.IMPRISON:
return new ImprisonTag(sourceId, side);
default: default:
return null; return null;
} }

View File

@ -3,7 +3,7 @@ import { getPokemonNameWithAffix } from "../messages";
import Pokemon, { MoveResult, HitResult } from "../field/pokemon"; import Pokemon, { MoveResult, HitResult } from "../field/pokemon";
import { StatusEffect } from "./status-effect"; import { StatusEffect } from "./status-effect";
import * as Utils from "../utils"; import * as Utils from "../utils";
import { ChargeAttr, MoveFlags, allMoves, MoveCategory, applyMoveAttrs, StatusCategoryOnAllyAttr, HealOnAllyAttr } from "./move"; import { ChargeAttr, MoveFlags, allMoves, MoveCategory, applyMoveAttrs, StatusCategoryOnAllyAttr, HealOnAllyAttr, ConsecutiveUseDoublePowerAttr } from "./move";
import { Type } from "./type"; import { Type } from "./type";
import { BlockNonDirectDamageAbAttr, FlinchEffectAbAttr, ReverseDrainAbAttr, applyAbAttrs, ProtectStatAbAttr } from "./ability"; import { BlockNonDirectDamageAbAttr, FlinchEffectAbAttr, ReverseDrainAbAttr, applyAbAttrs, ProtectStatAbAttr } from "./ability";
import { TerrainType } from "./terrain"; import { TerrainType } from "./terrain";
@ -2437,6 +2437,150 @@ export class MysteryEncounterPostSummonTag extends BattlerTag {
} }
} }
/**
* Battle Tag that applies the move Torment to the target Pokemon
* Torment restricts the use of moves twice in a row.
* The tag is only removed if the target leaves the battle.
* Torment does not interrupt the move if the move is performed consecutively in the same turn and right after Torment is applied
*/
export class TormentTag extends MoveRestrictionBattlerTag {
private target: Pokemon;
constructor(sourceId: number) {
super(BattlerTagType.TORMENT, BattlerTagLapseType.AFTER_MOVE, 1, Moves.TORMENT, sourceId);
}
/**
* Adds the battler tag to the target Pokemon and defines the private class variable 'target'
* 'Target' is used to track the Pokemon's current status
* @param {Pokemon} pokemon the Pokemon tormented
*/
override onAdd(pokemon: Pokemon) {
super.onAdd(pokemon);
this.target = pokemon;
pokemon.scene.queueMessage(i18next.t("battlerTags:tormentOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }), 1500);
}
/**
* Torment only ends when the affected Pokemon leaves the battle field
* @param {Pokemon} pokemon the Pokemon under the effects of Torment
* @param _tagType
* @returns `true` if still present | `false` if not
*/
override lapse(pokemon: Pokemon, _tagType: BattlerTagLapseType): boolean {
return !pokemon.isActive(true);
}
/**
* This checks if the current move used is identical to the last used move with a {@linkcode MoveResult} of `SUCCESS`/`MISS`
* @param {Moves} move the move under investigation
* @returns `true` if there is valid consecutive usage | `false` if the moves are different from each other
*/
override isMoveRestricted(move: Moves): boolean {
const lastMove = this.target.getLastXMoves(1)[0];
if ( !lastMove ) {
return false;
}
// This checks for locking / momentum moves like Rollout and Hydro Cannon + if the user is under the influence of BattlerTagType.FRENZY
// Because Uproar's unique behavior is not implemented, it does not check for Uproar. Torment has been marked as partial in moves.ts
const moveObj = allMoves[lastMove.move];
const isUnaffected = moveObj.hasAttr(ConsecutiveUseDoublePowerAttr) || this.target.getTag(BattlerTagType.FRENZY) || moveObj.hasAttr(ChargeAttr);
const validLastMoveResult = (lastMove.result === MoveResult.SUCCESS) || (lastMove.result === MoveResult.MISS);
if (lastMove.move === move && validLastMoveResult && lastMove.move !== Moves.STRUGGLE && !isUnaffected) {
return true;
}
return false;
}
override selectionDeniedText(_pokemon: Pokemon, move: Moves): string {
return i18next.t("battle:moveCannotBeSelected", { moveName: allMoves[move].name });
}
}
/**
* BattlerTag that applies the effects of Taunt to the target Pokemon
* Taunt restricts the use of status moves.
* The tag is removed after 4 turns.
*/
export class TauntTag extends MoveRestrictionBattlerTag {
constructor() {
super(BattlerTagType.TAUNT, [BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.AFTER_MOVE], 4, Moves.TAUNT);
}
override onAdd(pokemon: Pokemon) {
super.onAdd(pokemon);
pokemon.scene.queueMessage(i18next.t("battlerTags:tauntOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }), 1500);
}
/**
* Checks if a move is a status move and determines its restriction status on that basis
* @param {Moves} move the move under investigation
* @returns `true` if the move is a status move
*/
override isMoveRestricted(move: Moves): boolean {
return allMoves[move].category === MoveCategory.STATUS;
}
override selectionDeniedText(_pokemon: Pokemon, move: Moves): string {
return i18next.t("battle:moveCannotBeSelected", { moveName: allMoves[move].name });
}
override interruptedText(pokemon: Pokemon, move: Moves): string {
return i18next.t("battle:disableInterruptedMove", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), moveName: allMoves[move].name });
}
}
/**
* BattlerTag that applies the effects of Imprison to the target Pokemon
* Imprison restricts the opposing side's usage of moves shared by the source-user of Imprison.
* The tag is only removed when the source-user is removed from the field.
*/
export class ImprisonTag extends MoveRestrictionBattlerTag {
private source: Pokemon | null;
constructor(sourceId: number) {
super(BattlerTagType.IMPRISON, [BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.AFTER_MOVE], 1, Moves.IMPRISON, sourceId);
}
override onAdd(pokemon: Pokemon) {
if (this.sourceId) {
this.source = pokemon.scene.getPokemonById(this.sourceId);
}
}
/**
* Checks if the source of Imprison is still active
* @param _pokemon
* @param _lapseType
* @returns `true` if the source is still active
*/
override lapse(_pokemon: Pokemon, _lapseType: BattlerTagLapseType): boolean {
return this.source?.isActive(true) ?? false;
}
/**
* Checks if the source of the tag has the parameter move in its moveset and that the source is still active
* @param {Moves} move the move under investigation
* @returns `false` if either condition is not met
*/
override isMoveRestricted(move: Moves): boolean {
if (this.source) {
const sourceMoveset = this.source.getMoveset().map(m => m!.moveId);
return sourceMoveset?.includes(move) && this.source.isActive(true);
}
return false;
}
override selectionDeniedText(_pokemon: Pokemon, move: Moves): string {
return i18next.t("battle:moveCannotBeSelected", { moveName: allMoves[move].name });
}
override interruptedText(pokemon: Pokemon, move: Moves): string {
return i18next.t("battle:disableInterruptedMove", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), moveName: allMoves[move].name });
}
}
/** /**
* Retrieves a {@linkcode BattlerTag} based on the provided tag type, turn count, source move, and source ID. * Retrieves a {@linkcode BattlerTag} based on the provided tag type, turn count, source move, and source ID.
* *
@ -2604,6 +2748,12 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source
return new MysteryEncounterPostSummonTag(); return new MysteryEncounterPostSummonTag();
case BattlerTagType.HEAL_BLOCK: case BattlerTagType.HEAL_BLOCK:
return new HealBlockTag(turnCount, sourceMove); return new HealBlockTag(turnCount, sourceMove);
case BattlerTagType.TORMENT:
return new TormentTag(sourceId);
case BattlerTagType.TAUNT:
return new TauntTag();
case BattlerTagType.IMPRISON:
return new ImprisonTag(sourceId);
case BattlerTagType.NONE: case BattlerTagType.NONE:
default: default:
return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId); return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId);

View File

@ -7507,7 +7507,8 @@ export function initMoves() {
.target(MoveTarget.BOTH_SIDES), .target(MoveTarget.BOTH_SIDES),
new StatusMove(Moves.TORMENT, Type.DARK, 100, 15, -1, 0, 3) new StatusMove(Moves.TORMENT, Type.DARK, 100, 15, -1, 0, 3)
.ignoresSubstitute() .ignoresSubstitute()
.unimplemented(), .partial() // Incomplete implementation because of Uproar's partial implementation
.attr(AddBattlerTagAttr, BattlerTagType.TORMENT, false, true, 1),
new StatusMove(Moves.FLATTER, Type.DARK, 100, 15, -1, 0, 3) new StatusMove(Moves.FLATTER, Type.DARK, 100, 15, -1, 0, 3)
.attr(StatStageChangeAttr, [ Stat.SPATK ], 1) .attr(StatStageChangeAttr, [ Stat.SPATK ], 1)
.attr(ConfuseAttr), .attr(ConfuseAttr),
@ -7538,7 +7539,7 @@ export function initMoves() {
.attr(AddBattlerTagAttr, BattlerTagType.CHARGED, true, false), .attr(AddBattlerTagAttr, BattlerTagType.CHARGED, true, false),
new StatusMove(Moves.TAUNT, Type.DARK, 100, 20, -1, 0, 3) new StatusMove(Moves.TAUNT, Type.DARK, 100, 20, -1, 0, 3)
.ignoresSubstitute() .ignoresSubstitute()
.unimplemented(), .attr(AddBattlerTagAttr, BattlerTagType.TAUNT, false, true, 4),
new StatusMove(Moves.HELPING_HAND, Type.NORMAL, -1, 20, -1, 5, 3) new StatusMove(Moves.HELPING_HAND, Type.NORMAL, -1, 20, -1, 5, 3)
.attr(AddBattlerTagAttr, BattlerTagType.HELPING_HAND) .attr(AddBattlerTagAttr, BattlerTagType.HELPING_HAND)
.ignoresSubstitute() .ignoresSubstitute()
@ -7581,9 +7582,9 @@ export function initMoves() {
new StatusMove(Moves.SKILL_SWAP, Type.PSYCHIC, -1, 10, -1, 0, 3) new StatusMove(Moves.SKILL_SWAP, Type.PSYCHIC, -1, 10, -1, 0, 3)
.ignoresSubstitute() .ignoresSubstitute()
.attr(SwitchAbilitiesAttr), .attr(SwitchAbilitiesAttr),
new SelfStatusMove(Moves.IMPRISON, Type.PSYCHIC, -1, 10, -1, 0, 3) new StatusMove(Moves.IMPRISON, Type.PSYCHIC, 100, 10, -1, 0, 3)
.ignoresSubstitute() .ignoresSubstitute()
.unimplemented(), .attr(AddArenaTagAttr, ArenaTagType.IMPRISON, 1, true, false),
new SelfStatusMove(Moves.REFRESH, Type.NORMAL, -1, 20, -1, 0, 3) new SelfStatusMove(Moves.REFRESH, Type.NORMAL, -1, 20, -1, 0, 3)
.attr(HealStatusEffectAttr, true, StatusEffect.PARALYSIS, StatusEffect.POISON, StatusEffect.TOXIC, StatusEffect.BURN) .attr(HealStatusEffectAttr, true, StatusEffect.PARALYSIS, StatusEffect.POISON, StatusEffect.TOXIC, StatusEffect.BURN)
.condition((user, target, move) => !!user.status && (user.status.effect === StatusEffect.PARALYSIS || user.status.effect === StatusEffect.POISON || user.status.effect === StatusEffect.TOXIC || user.status.effect === StatusEffect.BURN)), .condition((user, target, move) => !!user.status && (user.status.effect === StatusEffect.PARALYSIS || user.status.effect === StatusEffect.POISON || user.status.effect === StatusEffect.TOXIC || user.status.effect === StatusEffect.BURN)),

View File

@ -23,5 +23,6 @@ export enum ArenaTagType {
TAILWIND = "TAILWIND", TAILWIND = "TAILWIND",
HAPPY_HOUR = "HAPPY_HOUR", HAPPY_HOUR = "HAPPY_HOUR",
SAFEGUARD = "SAFEGUARD", SAFEGUARD = "SAFEGUARD",
NO_CRIT = "NO_CRIT" NO_CRIT = "NO_CRIT",
IMPRISON = "IMPRISON",
} }

View File

@ -82,4 +82,7 @@ export enum BattlerTagType {
AUTOTOMIZED = "AUTOTOMIZED", AUTOTOMIZED = "AUTOTOMIZED",
MYSTERY_ENCOUNTER_POST_SUMMON = "MYSTERY_ENCOUNTER_POST_SUMMON", MYSTERY_ENCOUNTER_POST_SUMMON = "MYSTERY_ENCOUNTER_POST_SUMMON",
HEAL_BLOCK = "HEAL_BLOCK", HEAL_BLOCK = "HEAL_BLOCK",
TORMENT = "TORMENT",
TAUNT = "TAUNT",
IMPRISON = "IMPRISON",
} }

View File

@ -74,5 +74,8 @@
"substituteOnAdd": "{{pokemonNameWithAffix}} put in a substitute!", "substituteOnAdd": "{{pokemonNameWithAffix}} put in a substitute!",
"substituteOnHit": "The substitute took damage for {{pokemonNameWithAffix}}!", "substituteOnHit": "The substitute took damage for {{pokemonNameWithAffix}}!",
"substituteOnRemove": "{{pokemonNameWithAffix}}'s substitute faded!", "substituteOnRemove": "{{pokemonNameWithAffix}}'s substitute faded!",
"tormentOnAdd": "{{pokemonNameWithAffix}} was subjected to torment!",
"tauntOnAdd": "{{pokemonNameWithAffix}} fell for the taunt!",
"imprisonOnAdd": "{{pokemonNameWithAffix}} sealed the opponents move(s)!",
"autotomizeOnAdd": "{{pokemonNameWithAffix}} became nimble!" "autotomizeOnAdd": "{{pokemonNameWithAffix}} became nimble!"
} }

View File

@ -0,0 +1,65 @@
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import { Abilities } from "#enums/abilities";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { BattlerTagType } from "#enums/battler-tag-type";
import { ArenaTagType } from "#enums/arena-tag-type";
import { BattlerIndex } from "#app/battle";
import { PlayerPokemon } from "#app/field/pokemon";
describe("Moves - Aroma Veil", () => {
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("double")
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset([Moves.HEAL_BLOCK, Moves.IMPRISON, Moves.SPLASH])
.enemySpecies(Species.SHUCKLE)
.ability(Abilities.AROMA_VEIL)
.moveset([Moves.GROWL]);
});
it("Aroma Veil protects the Pokemon's side against most Move Restriction Battler Tags", async () => {
await game.classicMode.startBattle([Species.REGIELEKI, Species.BULBASAUR]);
const party = game.scene.getParty()! as PlayerPokemon[];
game.move.select(Moves.GROWL);
game.move.select(Moves.GROWL);
await game.forceEnemyMove(Moves.HEAL_BLOCK);
await game.toNextTurn();
party.forEach(p => {
expect(p.getTag(BattlerTagType.HEAL_BLOCK)).toBeUndefined();
});
});
it("Aroma Veil does not protect against Imprison", async () => {
await game.classicMode.startBattle([Species.REGIELEKI, Species.BULBASAUR]);
const party = game.scene.getParty()! as PlayerPokemon[];
game.move.select(Moves.GROWL);
game.move.select(Moves.GROWL, 1);
await game.forceEnemyMove(Moves.IMPRISON, BattlerIndex.PLAYER);
await game.forceEnemyMove(Moves.SPLASH);
await game.toNextTurn();
expect(game.scene.arena.getTag(ArenaTagType.IMPRISON)).toBeDefined();
party.forEach(p => {
expect(p.getTag(BattlerTagType.IMPRISON)).toBeDefined();
});
});
});

View File

@ -0,0 +1,98 @@
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import { Abilities } from "#enums/abilities";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { BattlerTagType } from "#enums/battler-tag-type";
import { ArenaTagType } from "#enums/arena-tag-type";
describe("Moves - Imprison", () => {
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")
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset([Moves.IMPRISON, Moves.SPLASH, Moves.GROWL])
.enemySpecies(Species.SHUCKLE)
.moveset([Moves.TRANSFORM, Moves.SPLASH]);
});
it("Pokemon under Imprison cannot use shared moves", async () => {
await game.classicMode.startBattle([Species.REGIELEKI]);
const playerPokemon = game.scene.getPlayerPokemon()!;
game.move.select(Moves.TRANSFORM);
await game.forceEnemyMove(Moves.IMPRISON);
await game.toNextTurn();
const playerMoveset = playerPokemon.getMoveset().map(x => x?.moveId);
const enemyMoveset = game.scene.getEnemyPokemon()!.getMoveset().map(x => x?.moveId);
expect(enemyMoveset.includes(playerMoveset[0])).toBeTruthy();
const imprisonArenaTag = game.scene.arena.getTag(ArenaTagType.IMPRISON);
const imprisonBattlerTag = playerPokemon.getTag(BattlerTagType.IMPRISON);
expect(imprisonArenaTag).toBeDefined();
expect(imprisonBattlerTag).toBeDefined();
// Second turn, Imprison forces Struggle to occur
game.move.select(Moves.SPLASH);
await game.forceEnemyMove(Moves.SPLASH);
await game.toNextTurn();
const move1 = playerPokemon.getLastXMoves(1)[0]!;
expect(move1.move).toBe(Moves.STRUGGLE);
});
it("Imprison applies to Pokemon switched into Battle", async () => {
await game.classicMode.startBattle([Species.REGIELEKI, Species.BULBASAUR]);
const playerPokemon1 = game.scene.getPlayerPokemon()!;
game.move.select(Moves.SPLASH);
await game.forceEnemyMove(Moves.IMPRISON);
await game.toNextTurn();
const imprisonArenaTag = game.scene.arena.getTag(ArenaTagType.IMPRISON);
const imprisonBattlerTag1 = playerPokemon1.getTag(BattlerTagType.IMPRISON);
expect(imprisonArenaTag).toBeDefined();
expect(imprisonBattlerTag1).toBeDefined();
// Second turn, Imprison forces Struggle to occur
game.doSwitchPokemon(1);
await game.forceEnemyMove(Moves.SPLASH);
await game.toNextTurn();
const playerPokemon2 = game.scene.getPlayerPokemon()!;
const imprisonBattlerTag2 = playerPokemon2.getTag(BattlerTagType.IMPRISON);
expect(playerPokemon1).not.toEqual(playerPokemon2);
expect(imprisonBattlerTag2).toBeDefined();
});
it("The effects of Imprison only end when the source is no longer active", async () => {
game.override.moveset([Moves.SPLASH, Moves.IMPRISON]);
await game.classicMode.startBattle([Species.REGIELEKI, Species.BULBASAUR]);
const playerPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
game.move.select(Moves.IMPRISON);
await game.forceEnemyMove(Moves.GROWL);
await game.toNextTurn();
expect(game.scene.arena.getTag(ArenaTagType.IMPRISON)).toBeDefined();
expect(enemyPokemon.getTag(BattlerTagType.IMPRISON)).toBeDefined();
game.doSwitchPokemon(1);
await game.forceEnemyMove(Moves.SPLASH);
await game.toNextTurn();
expect(playerPokemon.isActive(true)).toBeFalsy();
expect(game.scene.arena.getTag(ArenaTagType.IMPRISON)).toBeUndefined();
expect(enemyPokemon.getTag(BattlerTagType.IMPRISON)).toBeUndefined();
});
});

View File

@ -0,0 +1,54 @@
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import { Abilities } from "#enums/abilities";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { MoveResult } from "#app/field/pokemon";
import { BattlerTagType } from "#enums/battler-tag-type";
describe("Moves - Taunt", () => {
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")
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset([Moves.TAUNT, Moves.SPLASH])
.enemySpecies(Species.SHUCKLE)
.moveset([Moves.GROWL]);
});
it("Pokemon should not be able to use Status Moves", async () => {
await game.classicMode.startBattle([Species.REGIELEKI]);
const playerPokemon = game.scene.getPlayerPokemon()!;
// First turn, Player Pokemon succeeds using Growl without Taunt
game.move.select(Moves.GROWL);
await game.forceEnemyMove(Moves.TAUNT);
await game.toNextTurn();
const move1 = playerPokemon.getLastXMoves(1)[0]!;
expect(move1.move).toBe(Moves.GROWL);
expect(move1.result).toBe(MoveResult.SUCCESS);
expect(playerPokemon?.getTag(BattlerTagType.TAUNT)).toBeDefined();
// Second turn, Taunt forces Struggle to occur
game.move.select(Moves.GROWL);
await game.forceEnemyMove(Moves.SPLASH);
await game.toNextTurn();
const move2 = playerPokemon.getLastXMoves(1)[0]!;
expect(move2.move).toBe(Moves.STRUGGLE);
});
});

View File

@ -0,0 +1,64 @@
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import { Abilities } from "#enums/abilities";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { MoveResult } from "#app/field/pokemon";
import { BattlerTagType } from "#enums/battler-tag-type";
import { TurnEndPhase } from "#app/phases/turn-end-phase";
describe("Moves - Torment", () => {
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")
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset([Moves.TORMENT, Moves.SPLASH])
.enemySpecies(Species.SHUCKLE)
.enemyLevel(30)
.moveset([Moves.TACKLE])
.ability(Abilities.BALL_FETCH);
});
it("Pokemon should not be able to use the same move consecutively", async () => {
await game.classicMode.startBattle([Species.CHANSEY]);
const playerPokemon = game.scene.getPlayerPokemon()!;
// First turn, Player Pokemon uses Tackle successfully
game.move.select(Moves.TACKLE);
await game.forceEnemyMove(Moves.TORMENT);
await game.toNextTurn();
const move1 = playerPokemon.getLastXMoves(1)[0]!;
expect(move1.move).toBe(Moves.TACKLE);
expect(move1.result).toBe(MoveResult.SUCCESS);
expect(playerPokemon?.getTag(BattlerTagType.TORMENT)).toBeDefined();
// Second turn, Torment forces Struggle to occur
game.move.select(Moves.TACKLE);
await game.forceEnemyMove(Moves.SPLASH);
await game.toNextTurn();
const move2 = playerPokemon.getLastXMoves(1)[0]!;
expect(move2.move).toBe(Moves.STRUGGLE);
// Third turn, Tackle can be used.
game.move.select(Moves.TACKLE);
await game.forceEnemyMove(Moves.SPLASH);
await game.phaseInterceptor.to(TurnEndPhase);
const move3 = playerPokemon.getLastXMoves(1)[0]!;
expect(move3.move).toBe(Moves.TACKLE);
});
});