mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-07-05 16:02:20 +02:00
Rewrite move.canIgnoreSubstitute
to move.hitsSubstitute
* Also fixed interactions with Shell Trap and Beak Blast
This commit is contained in:
parent
b0dd1ce9b4
commit
bf7d9b084b
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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(() => {
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user