[Bug] Fix some trapping moves' interactions with Ghost-type Pokemon (#3936)

* Fix secondary effects to trapping moves not applying to Ghost types

* Docs for `isTrapped`

* more `isTrapped` cleanup

* Remove .js from imports
This commit is contained in:
innerthunder 2024-09-01 20:39:26 -07:00 committed by GitHub
parent 1fd662111e
commit e29f1fe5fd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 82 additions and 52 deletions

View File

@ -212,7 +212,7 @@ export class TrappedTag extends BattlerTag {
canAdd(pokemon: Pokemon): boolean {
const isGhost = pokemon.isOfType(Type.GHOST);
const isTrapped = pokemon.getTag(BattlerTagType.TRAPPED);
const isTrapped = pokemon.getTag(TrappedTag);
return !isTrapped && !isGhost;
}
@ -245,6 +245,23 @@ export class TrappedTag extends BattlerTag {
}
}
/**
* BattlerTag implementing No Retreat's trapping effect.
* This is treated separately from other trapping effects to prevent
* Ghost-type Pokemon from being able to reuse the move.
* @extends TrappedTag
*/
class NoRetreatTag extends TrappedTag {
constructor(sourceId: number) {
super(BattlerTagType.NO_RETREAT, BattlerTagLapseType.CUSTOM, 0, Moves.NO_RETREAT, sourceId);
}
/** overrides {@linkcode TrappedTag.apply}, removing the Ghost-type condition */
canAdd(pokemon: Pokemon): boolean {
return !pokemon.getTag(TrappedTag);
}
}
/**
* BattlerTag that represents the {@link https://bulbapedia.bulbagarden.net/wiki/Flinch Flinch} status condition
*/
@ -864,7 +881,7 @@ export abstract class DamagingTrapTag extends TrappedTag {
}
canAdd(pokemon: Pokemon): boolean {
return !pokemon.isOfType(Type.GHOST) && !pokemon.findTag(t => t instanceof DamagingTrapTag);
return !pokemon.getTag(TrappedTag);
}
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
@ -1883,6 +1900,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source
return new DrowsyTag();
case BattlerTagType.TRAPPED:
return new TrappedTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId);
case BattlerTagType.NO_RETREAT:
return new NoRetreatTag(sourceId);
case BattlerTagType.BIND:
return new BindTag(turnCount, sourceId);
case BattlerTagType.WRAP:

View File

@ -8553,7 +8553,7 @@ export function initMoves() {
.partial(),
new SelfStatusMove(Moves.NO_RETREAT, Type.FIGHTING, -1, 5, -1, 0, 8)
.attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.SPD ], 1, true)
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, true, false, 1)
.attr(AddBattlerTagAttr, BattlerTagType.NO_RETREAT, true, false)
.condition((user, target, move) => user.getTag(TrappedTag)?.sourceMove !== Moves.NO_RETREAT), // fails if the user is currently trapped by No Retreat
new StatusMove(Moves.TAR_SHOT, Type.ROCK, 100, 15, -1, 0, 8)
.attr(StatChangeAttr, BattleStat.SPD, -1)

View File

@ -71,4 +71,5 @@ export enum BattlerTagType {
BEAK_BLAST_CHARGING = "BEAK_BLAST_CHARGING",
SHELL_TRAP = "SHELL_TRAP",
DRAGON_CHEER = "DRAGON_CHEER",
NO_RETREAT = "NO_RETREAT",
}

View File

@ -18,11 +18,11 @@ import { Status, StatusEffect, getRandomStatus } from "../data/status-effect";
import { pokemonEvolutions, pokemonPrevolutions, SpeciesFormEvolution, SpeciesEvolutionCondition, FusionSpeciesFormEvolution } from "../data/pokemon-evolutions";
import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "../data/tms";
import { BattleStat } from "../data/battle-stat";
import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, ExposedTag, DragonCheerTag, CritBoostTag } from "../data/battler-tags";
import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag } from "../data/battler-tags";
import { WeatherType } from "../data/weather";
import { TempBattleStat } from "../data/temp-battle-stat";
import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "../data/arena-tag";
import { Ability, AbAttr, BattleStatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatChangesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyBattleStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldBattleStatMultiplierAbAttrs, FieldMultiplyBattleStatAbAttr, AddSecondStrikeAbAttr, IgnoreOpponentEvasionAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr } from "../data/ability";
import { Ability, AbAttr, BattleStatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatChangesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyBattleStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldBattleStatMultiplierAbAttrs, FieldMultiplyBattleStatAbAttr, AddSecondStrikeAbAttr, IgnoreOpponentEvasionAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr } from "../data/ability";
import PokemonData from "../system/pokemon-data";
import { BattlerIndex } from "../battle";
import { Mode } from "../ui/ui";
@ -1210,6 +1210,28 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
return !!this.getTag(GroundedTag) || (!this.isOfType(Type.FLYING, true, true) && !this.hasAbility(Abilities.LEVITATE) && !this.getTag(BattlerTagType.MAGNET_RISEN) && !this.getTag(SemiInvulnerableTag));
}
/**
* Determines whether this Pokemon is prevented from running or switching due
* to effects from moves and/or abilities.
* @param trappedAbMessages `string[]` If defined, ability trigger messages
* (e.g. from Shadow Tag) are forwarded through this array.
* @param simulated `boolean` if `true`, applies abilities via simulated calls.
* @returns
*/
isTrapped(trappedAbMessages: string[] = [], simulated: boolean = true): boolean {
if (this.isOfType(Type.GHOST)) {
return false;
}
const trappedByAbility = new Utils.BooleanHolder(false);
this.scene.getEnemyField()!.forEach(enemyPokemon =>
applyCheckTrappedAbAttrs(CheckTrappedAbAttr, enemyPokemon, trappedByAbility, this, trappedAbMessages, simulated)
);
return (trappedByAbility.value || !!this.getTag(TrappedTag));
}
/**
* Calculates the type of a move when used by this Pokemon after
* type-changing move and ability attributes have applied.

View File

@ -1,21 +1,18 @@
import BattleScene from "#app/battle-scene.js";
import { TurnCommand, BattleType } from "#app/battle.js";
import { applyCheckTrappedAbAttrs, CheckTrappedAbAttr } from "#app/data/ability.js";
import { TrappedTag, EncoreTag } from "#app/data/battler-tags.js";
import { MoveTargetSet, getMoveTargets } from "#app/data/move.js";
import { speciesStarters } from "#app/data/pokemon-species.js";
import { Type } from "#app/data/type.js";
import { Abilities } from "#app/enums/abilities.js";
import { BattlerTagType } from "#app/enums/battler-tag-type.js";
import { Biome } from "#app/enums/biome.js";
import { Moves } from "#app/enums/moves.js";
import { PokeballType } from "#app/enums/pokeball.js";
import { FieldPosition, PlayerPokemon } from "#app/field/pokemon.js";
import { getPokemonNameWithAffix } from "#app/messages.js";
import { Command } from "#app/ui/command-ui-handler.js";
import { Mode } from "#app/ui/ui.js";
import BattleScene from "#app/battle-scene";
import { TurnCommand, BattleType } from "#app/battle";
import { TrappedTag, EncoreTag } from "#app/data/battler-tags";
import { MoveTargetSet, getMoveTargets } from "#app/data/move";
import { speciesStarters } from "#app/data/pokemon-species";
import { Abilities } from "#app/enums/abilities";
import { BattlerTagType } from "#app/enums/battler-tag-type";
import { Biome } from "#app/enums/biome";
import { Moves } from "#app/enums/moves";
import { PokeballType } from "#app/enums/pokeball";
import { FieldPosition, PlayerPokemon } from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages";
import { Command } from "#app/ui/command-ui-handler";
import { Mode } from "#app/ui/ui";
import i18next from "i18next";
import * as Utils from "#app/utils.js";
import { FieldPhase } from "./field-phase";
import { SelectTargetPhase } from "./select-target-phase";
@ -77,7 +74,6 @@ export class CommandPhase extends FieldPhase {
handleCommand(command: Command, cursor: integer, ...args: any[]): boolean {
const playerPokemon = this.scene.getPlayerField()[this.fieldIndex];
const enemyField = this.scene.getEnemyField();
let success: boolean;
switch (command) {
@ -184,14 +180,9 @@ export class CommandPhase extends FieldPhase {
this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex);
}, null, true);
} else {
const trapTag = playerPokemon.findTag(t => t instanceof TrappedTag) as TrappedTag;
const trapped = new Utils.BooleanHolder(false);
const batonPass = isSwitch && args[0] as boolean;
const trappedAbMessages: string[] = [];
if (!batonPass) {
enemyField.forEach(enemyPokemon => applyCheckTrappedAbAttrs(CheckTrappedAbAttr, enemyPokemon, trapped, playerPokemon, trappedAbMessages, true));
}
if (batonPass || (!trapTag && !trapped.value)) {
if (batonPass || !playerPokemon.isTrapped(trappedAbMessages)) {
this.scene.currentBattle.turnCommands[this.fieldIndex] = isSwitch
? { command: Command.POKEMON, cursor: cursor, args: args }
: { command: Command.RUN };
@ -199,14 +190,27 @@ export class CommandPhase extends FieldPhase {
if (!isSwitch && this.fieldIndex) {
this.scene.currentBattle.turnCommands[this.fieldIndex - 1]!.skip = true;
}
} else if (trapTag) {
if (trapTag.sourceMove === Moves.INGRAIN && trapTag.sourceId && this.scene.getPokemonById(trapTag.sourceId)?.isOfType(Type.GHOST)) {
success = true;
} else if (trappedAbMessages.length > 0) {
if (!isSwitch) {
this.scene.ui.setMode(Mode.MESSAGE);
}
this.scene.ui.showText(trappedAbMessages[0], null, () => {
this.scene.ui.showText("", 0);
if (!isSwitch) {
this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex);
}
}, null, true);
} else {
const trapTag = playerPokemon.getTag(TrappedTag);
// trapTag should be defined at this point, but just in case...
if (!trapTag) {
this.scene.currentBattle.turnCommands[this.fieldIndex] = isSwitch
? { command: Command.POKEMON, cursor: cursor, args: args }
: { command: Command.RUN };
break;
}
if (!isSwitch) {
this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex);
this.scene.ui.setMode(Mode.MESSAGE);
@ -224,16 +228,6 @@ export class CommandPhase extends FieldPhase {
this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex);
}
}, null, true);
} else if (trapped.value && trappedAbMessages.length > 0) {
if (!isSwitch) {
this.scene.ui.setMode(Mode.MESSAGE);
}
this.scene.ui.showText(trappedAbMessages[0], null, () => {
this.scene.ui.showText("", 0);
if (!isSwitch) {
this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex);
}
}, null, true);
}
}
break;

View File

@ -1,9 +1,6 @@
import BattleScene from "#app/battle-scene.js";
import { BattlerIndex } from "#app/battle.js";
import { applyCheckTrappedAbAttrs, CheckTrappedAbAttr } from "#app/data/ability.js";
import { TrappedTag } from "#app/data/battler-tags.js";
import { Command } from "#app/ui/command-ui-handler.js";
import * as Utils from "#app/utils.js";
import BattleScene from "#app/battle-scene";
import { BattlerIndex } from "#app/battle";
import { Command } from "#app/ui/command-ui-handler";
import { FieldPhase } from "./field-phase";
/**
@ -45,10 +42,7 @@ export class EnemyCommandPhase extends FieldPhase {
if (trainer && !enemyPokemon.getMoveQueue().length) {
const opponents = enemyPokemon.getOpponents();
const trapTag = enemyPokemon.findTag(t => t instanceof TrappedTag) as TrappedTag;
const trapped = new Utils.BooleanHolder(false);
opponents.forEach(playerPokemon => applyCheckTrappedAbAttrs(CheckTrappedAbAttr, playerPokemon, trapped, enemyPokemon, [""], true));
if (!trapTag && !trapped.value) {
if (!enemyPokemon.isTrapped()) {
const partyMemberScores = trainer.getPartyMemberMatchupScores(enemyPokemon.trainerSlot, true);
if (partyMemberScores.length) {