Rewrite move.canIgnoreSubstitute to move.hitsSubstitute

* Also fixed interactions with Shell Trap and Beak Blast
This commit is contained in:
innerthunder 2024-08-20 20:46:52 -07:00
parent b0dd1ce9b4
commit bf7d9b084b
7 changed files with 45 additions and 34 deletions

View File

@ -992,12 +992,10 @@ export default class BattleScene extends SceneBase {
this.enemyModifierBar.removeAll(true); this.enemyModifierBar.removeAll(true);
for (const p of this.getParty()) { for (const p of this.getParty()) {
p.destroySubstitute();
p.destroy(); p.destroy();
} }
this.party = []; this.party = [];
for (const p of this.getEnemyParty()) { for (const p of this.getEnemyParty()) {
p.destroySubstitute();
p.destroy(); p.destroy();
} }

View File

@ -153,9 +153,11 @@ export class BeakBlastChargingTag extends BattlerTag {
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
if (lapseType === BattlerTagLapseType.CUSTOM) { if (lapseType === BattlerTagLapseType.CUSTOM) {
const effectPhase = pokemon.scene.getCurrentPhase(); const effectPhase = pokemon.scene.getCurrentPhase();
if (effectPhase instanceof MoveEffectPhase && effectPhase.move.getMove().hasFlag(MoveFlags.MAKES_CONTACT)) { if (effectPhase instanceof MoveEffectPhase) {
const attacker = effectPhase.getPokemon(); const attacker = effectPhase.getPokemon();
attacker.trySetStatus(StatusEffect.BURN, true, pokemon); if (effectPhase.move.getMove().checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon)) {
attacker.trySetStatus(StatusEffect.BURN, true, pokemon);
}
} }
return true; return true;
} }
@ -1900,7 +1902,7 @@ export class SubstituteTag extends BattlerTag {
const move = moveEffectPhase.move.getMove(); const move = moveEffectPhase.move.getMove();
const firstHit = (attacker.turnData.hitCount === attacker.turnData.hitsLeft); const firstHit = (attacker.turnData.hitCount === attacker.turnData.hitsLeft);
if (firstHit && !move.canIgnoreSubstitute(attacker)) { if (firstHit && move.hitsSubstitute(attacker, pokemon)) {
pokemon.scene.queueMessage(i18next.t("battlerTags:substituteOnHit", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })); pokemon.scene.queueMessage(i18next.t("battlerTags:substituteOnHit", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }));
} }
} }
@ -1913,7 +1915,6 @@ export class SubstituteTag extends BattlerTag {
loadTag(source: BattlerTag | any): void { loadTag(source: BattlerTag | any): void {
super.loadTag(source); super.loadTag(source);
this.hp = source.hp; this.hp = source.hp;
// TODO: load this tag's sprite (or generate a new one upon loading a game)
} }
} }

View File

@ -100,6 +100,9 @@ export enum MoveFlags {
* Enables all hits of a multi-hit move to be accuracy checked individually * Enables all hits of a multi-hit move to be accuracy checked individually
*/ */
CHECK_ALL_HITS = 1 << 17, CHECK_ALL_HITS = 1 << 17,
/**
* Indicates a move is able to bypass its target's Substitute (if the target has one)
*/
IGNORE_SUBSTITUTE = 1 << 18, IGNORE_SUBSTITUTE = 1 << 18,
/** /**
* Indicates a move is able to be redirected to allies in a double battle if the attacker faints * Indicates a move is able to be redirected to allies in a double battle if the attacker faints
@ -320,15 +323,19 @@ export default class Move implements Localizable {
} }
/** /**
* Checks if the move can bypass Substitute to directly hit its target * Checks if the move would hit its target's Substitute instead of the target itself.
* @param user The {@linkcode Pokemon} using this move * @param user The {@linkcode Pokemon} using this move
* @param target The {@linkcode Pokemon} targeted by this move
* @returns `true` if the move can bypass the target's Substitute; `false` otherwise. * @returns `true` if the move can bypass the target's Substitute; `false` otherwise.
*/ */
canIgnoreSubstitute(user: Pokemon): boolean { hitsSubstitute(user: Pokemon, target: Pokemon | null): boolean {
return this.moveTarget === MoveTarget.USER if (this.moveTarget === MoveTarget.USER || !target?.getTag(BattlerTagType.SUBSTITUTE)) {
|| user?.hasAbility(Abilities.INFILTRATOR) return false;
|| this.hasFlag(MoveFlags.SOUND_BASED) }
|| this.hasFlag(MoveFlags.IGNORE_SUBSTITUTE);
return !user.hasAbility(Abilities.INFILTRATOR)
&& !this.hasFlag(MoveFlags.SOUND_BASED)
&& !this.hasFlag(MoveFlags.IGNORE_SUBSTITUTE);
} }
/** /**
@ -607,8 +614,7 @@ export default class Move implements Localizable {
// special cases below, eg: if the move flag is MAKES_CONTACT, and the user pokemon has an ability that ignores contact (like "Long Reach"), then overrides and move does not make contact // special cases below, eg: if the move flag is MAKES_CONTACT, and the user pokemon has an ability that ignores contact (like "Long Reach"), then overrides and move does not make contact
switch (flag) { switch (flag) {
case MoveFlags.MAKES_CONTACT: case MoveFlags.MAKES_CONTACT:
if (user.hasAbilityWithAttr(IgnoreContactAbAttr) || if (user.hasAbilityWithAttr(IgnoreContactAbAttr) || this.hitsSubstitute(user, target)) {
(target?.getTag(BattlerTagType.SUBSTITUTE) && !this.canIgnoreSubstitute(user))) {
return false; return false;
} }
break; break;
@ -2004,7 +2010,7 @@ export class StatusEffectAttr extends MoveEffectAttr {
} }
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (!this.selfTarget && !!target.getTag(SubstituteTag) && !move.canIgnoreSubstitute(user)) { if (!this.selfTarget && move.hitsSubstitute(user, target)) {
return false; return false;
} }
@ -2100,7 +2106,7 @@ export class StealHeldItemChanceAttr extends MoveEffectAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> {
return new Promise<boolean>(resolve => { return new Promise<boolean>(resolve => {
if (!!target.getTag(SubstituteTag) && !move.canIgnoreSubstitute(user)) { if (move.hitsSubstitute(user, target)) {
return resolve(false); return resolve(false);
} }
const rand = Phaser.Math.RND.realInRange(0, 1); const rand = Phaser.Math.RND.realInRange(0, 1);
@ -2172,7 +2178,7 @@ export class RemoveHeldItemAttr extends MoveEffectAttr {
return false; return false;
} }
if (!!target.getTag(SubstituteTag) && !move.canIgnoreSubstitute(user)) { if (move.hitsSubstitute(user, target)) {
return false; return false;
} }
@ -2295,7 +2301,7 @@ export class StealEatBerryAttr extends EatBerryAttr {
* @returns {boolean} true if the function succeeds * @returns {boolean} true if the function succeeds
*/ */
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (!!target.getTag(SubstituteTag) && !move.canIgnoreSubstitute(user)) { if (move.hitsSubstitute(user, target)) {
return false; return false;
} }
const cancelled = new Utils.BooleanHolder(false); const cancelled = new Utils.BooleanHolder(false);
@ -2348,7 +2354,7 @@ export class HealStatusEffectAttr extends MoveEffectAttr {
return false; return false;
} }
if (!this.selfTarget && !!target.getTag(SubstituteTag) && !move.canIgnoreSubstitute(user)) { if (!this.selfTarget && move.hitsSubstitute(user, target)) {
return false; return false;
} }
@ -2665,7 +2671,7 @@ export class StatChangeAttr extends MoveEffectAttr {
return false; return false;
} }
if (!this.selfTarget && !!target.getTag(SubstituteTag) && !move.canIgnoreSubstitute(user)) { if (!this.selfTarget && move.hitsSubstitute(user, target)) {
return false; return false;
} }
@ -2862,7 +2868,7 @@ export class ResetStatsAttr extends MoveEffectAttr {
return false; return false;
} }
if (!!target.getTag(SubstituteTag) && !move.canIgnoreSubstitute(user)) { if (move.hitsSubstitute(user, target)) {
return false; return false;
} }
@ -4604,7 +4610,7 @@ export class FlinchAttr extends AddBattlerTagAttr {
} }
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (!target.getTag(SubstituteTag) || move.canIgnoreSubstitute(user)) { if (!move.hitsSubstitute(user, target)) {
return super.apply(user, target, move, args); return super.apply(user, target, move, args);
} }
return false; return false;
@ -4617,7 +4623,7 @@ export class ConfuseAttr extends AddBattlerTagAttr {
} }
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (!target.getTag(SubstituteTag) || move.canIgnoreSubstitute(user)) { if (!move.hitsSubstitute(user, target)) {
return super.apply(user, target, move, args); return super.apply(user, target, move, args);
} }
return false; return false;
@ -5113,7 +5119,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
const switchOutTarget = (this.user ? user : target); const switchOutTarget = (this.user ? user : target);
const player = switchOutTarget instanceof PlayerPokemon; const player = switchOutTarget instanceof PlayerPokemon;
if (!this.user && !!target.getTag(SubstituteTag) && !move.canIgnoreSubstitute(user)) { if (!this.user && move.hitsSubstitute(user, target)) {
return false; return false;
} }

View File

@ -2235,7 +2235,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.lapseTags(BattlerTagLapseType.HIT); this.lapseTags(BattlerTagLapseType.HIT);
const substitute = this.getTag(SubstituteTag); const substitute = this.getTag(SubstituteTag);
if (!!substitute && !move.canIgnoreSubstitute(source)) { if (substitute && move.hitsSubstitute(source, this)) {
substitute.hp -= damage.value; substitute.hp -= damage.value;
damage.value = 0; damage.value = 0;
} }
@ -2310,7 +2310,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (!typeless) { if (!typeless) {
applyPreDefendAbAttrs(TypeImmunityAbAttr, this, source, move, cancelled, typeMultiplier); applyPreDefendAbAttrs(TypeImmunityAbAttr, this, source, move, cancelled, typeMultiplier);
} }
if (!!this.getTag(SubstituteTag) && !move.canIgnoreSubstitute(source)) { if (move.hitsSubstitute(source, this)) {
cancelled.value = true; cancelled.value = true;
} }
if (!cancelled.value) { if (!cancelled.value) {
@ -3326,6 +3326,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
destroy(): void { destroy(): void {
this.battleInfo?.destroy(); this.battleInfo?.destroy();
this.destroySubstitute();
super.destroy(); super.destroy();
} }

View File

@ -33,7 +33,7 @@ export class MoveAnimTestPhase extends BattlePhase {
.then(() => { .then(() => {
const user = player ? this.scene.getPlayerPokemon()! : this.scene.getEnemyPokemon()!; const user = player ? this.scene.getPlayerPokemon()! : this.scene.getEnemyPokemon()!;
const target = (player !== (allMoves[moveId] instanceof SelfStatusMove)) ? this.scene.getEnemyPokemon()! : this.scene.getPlayerPokemon()!; const target = (player !== (allMoves[moveId] instanceof SelfStatusMove)) ? this.scene.getEnemyPokemon()! : this.scene.getPlayerPokemon()!;
new MoveAnim(moveId, user, target.getBattlerIndex()).play(this.scene, !allMoves[moveId].canIgnoreSubstitute(user), () => { // TODO: are the bangs correct here? new MoveAnim(moveId, user, target.getBattlerIndex()).play(this.scene, allMoves[moveId].hitsSubstitute(user, target), () => { // TODO: are the bangs correct here?
if (player) { if (player) {
this.playMoveAnim(moveQueue, false); this.playMoveAnim(moveQueue, false);
} else { } else {

View File

@ -120,7 +120,7 @@ export class MoveEffectPhase extends PokemonPhase {
const applyAttrs: Promise<void>[] = []; const applyAttrs: Promise<void>[] = [];
// Move animation only needs one target // Move animation only needs one target
new MoveAnim(move.id as Moves, user, this.getTarget()?.getBattlerIndex()!).play(this.scene, !move.canIgnoreSubstitute(user), () => { // TODO: is the bang correct here? new MoveAnim(move.id as Moves, user, this.getTarget()!.getBattlerIndex()).play(this.scene, move.hitsSubstitute(user, this.getTarget()!), () => {
/** Has the move successfully hit a target (for damage) yet? */ /** Has the move successfully hit a target (for damage) yet? */
let hasHit: boolean = false; let hasHit: boolean = false;
for (const target of targets) { for (const target of targets) {
@ -245,7 +245,7 @@ export class MoveEffectPhase extends PokemonPhase {
* If the move hit, and the target doesn't have Shield Dust, * If the move hit, and the target doesn't have Shield Dust,
* apply the chance to flinch the target gained from King's Rock * apply the chance to flinch the target gained from King's Rock
*/ */
if (dealsDamage && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && (!target.getTag(BattlerTagType.SUBSTITUTE) || move.canIgnoreSubstitute(user))) { if (dealsDamage && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && !move.hitsSubstitute(user, target)) {
const flinched = new Utils.BooleanHolder(false); const flinched = new Utils.BooleanHolder(false);
user.scene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched); user.scene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched);
if (flinched.value) { if (flinched.value) {
@ -257,14 +257,19 @@ export class MoveEffectPhase extends PokemonPhase {
&& (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit) && (!attr.firstTargetOnly || firstTarget), user, target, this.move.getMove()).then(() => { && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit) && (!attr.firstTargetOnly || firstTarget), user, target, this.move.getMove()).then(() => {
// Apply the target's post-defend ability effects (as long as the target is active or can otherwise apply them) // Apply the target's post-defend ability effects (as long as the target is active or can otherwise apply them)
return Utils.executeIf(!target.isFainted() || target.canApplyAbility(), () => applyPostDefendAbAttrs(PostDefendAbAttr, target, user, this.move.getMove(), hitResult).then(() => { return Utils.executeIf(!target.isFainted() || target.canApplyAbility(), () => applyPostDefendAbAttrs(PostDefendAbAttr, target, user, this.move.getMove(), hitResult).then(() => {
// If the invoked move is an enemy attack, apply the enemy's status effect-inflicting tags and tokens // Only apply the following effects if the move was not deflected by a substitute
if (move.hitsSubstitute(user, target)) {
return resolve();
}
// If the invoked move is an enemy attack, apply the enemy's status effect-inflicting tokens
if (!user.isPlayer() && this.move.getMove() instanceof AttackMove) {
user.scene.applyShuffledModifiers(this.scene, EnemyAttackStatusEffectChanceModifier, false, target);
}
target.lapseTag(BattlerTagType.BEAK_BLAST_CHARGING); target.lapseTag(BattlerTagType.BEAK_BLAST_CHARGING);
if (move.category === MoveCategory.PHYSICAL && user.isPlayer() !== target.isPlayer()) { if (move.category === MoveCategory.PHYSICAL && user.isPlayer() !== target.isPlayer()) {
target.lapseTag(BattlerTagType.SHELL_TRAP); target.lapseTag(BattlerTagType.SHELL_TRAP);
} }
if (!user.isPlayer() && this.move.getMove() instanceof AttackMove) {
user.scene.applyShuffledModifiers(this.scene, EnemyAttackStatusEffectChanceModifier, false, target);
}
})).then(() => { })).then(() => {
// Apply the user's post-attack ability effects // Apply the user's post-attack ability effects
applyPostAttackAbAttrs(PostAttackAbAttr, user, target, this.move.getMove(), hitResult).then(() => { applyPostAttackAbAttrs(PostAttackAbAttr, user, target, this.move.getMove(), hitResult).then(() => {

View File

@ -195,7 +195,7 @@ describe("BattlerTag - SubstituteTag", () => {
} as MoveEffectPhase; } as MoveEffectPhase;
vi.spyOn(mockPokemon.scene, "getCurrentPhase").mockReturnValue(moveEffectPhase); vi.spyOn(mockPokemon.scene, "getCurrentPhase").mockReturnValue(moveEffectPhase);
vi.spyOn(allMoves[Moves.TACKLE], "canIgnoreSubstitute").mockReturnValue(false); vi.spyOn(allMoves[Moves.TACKLE], "hitsSubstitute").mockReturnValue(true);
expect(subject.lapse(mockPokemon, BattlerTagLapseType.HIT)).toBeTruthy(); expect(subject.lapse(mockPokemon, BattlerTagLapseType.HIT)).toBeTruthy();