[Move] Add type immunity removal moves (Foresight, Odor Sleuth, Miracle Eye) (#3379)
* Update Foresight PR to current beta Implements Foresight, Miracle Eye, and Odor Sleuth * Add placeholder i18n strings * Minor tsdoc updates * Fix placement of evasion level modifier, add tests * Add first batch of translations Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> Co-authored-by: Lugiad' <adrien.grivel@hotmail.fr> Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br> * Second batch of translations Co-authored-by: Enoch <enoch.jwsong@gmail.com> Co-authored-by: mercurius-00 <80205689+mercurius-00@users.noreply.github.com> * Add Catalan and Japanese translation placeholder strings * Fix issue caused by merge --------- Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> Co-authored-by: Lugiad' <adrien.grivel@hotmail.fr> Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br> Co-authored-by: Enoch <enoch.jwsong@gmail.com> Co-authored-by: mercurius-00 <80205689+mercurius-00@users.noreply.github.com>
This commit is contained in:
parent
db3fae1180
commit
548bd8978f
|
@ -1680,6 +1680,47 @@ export class GulpMissileTag extends BattlerTag {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tag that makes the target drop all of it type immunities
|
||||
* and all accuracy checks ignore its evasiveness stat.
|
||||
*
|
||||
* Applied by moves: {@linkcode Moves.ODOR_SLEUTH | Odor Sleuth},
|
||||
* {@linkcode Moves.MIRACLE_EYE | Miracle Eye} and {@linkcode Moves.FORESIGHT | Foresight}.
|
||||
*
|
||||
* @extends BattlerTag
|
||||
* @see {@linkcode ignoreImmunity}
|
||||
*/
|
||||
export class ExposedTag extends BattlerTag {
|
||||
private defenderType: Type;
|
||||
private allowedTypes: Type[];
|
||||
|
||||
constructor(tagType: BattlerTagType, sourceMove: Moves, defenderType: Type, allowedTypes: Type[]) {
|
||||
super(tagType, BattlerTagLapseType.CUSTOM, 1, sourceMove);
|
||||
this.defenderType = defenderType;
|
||||
this.allowedTypes = allowedTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* When given a battler tag or json representing one, load the data for it.
|
||||
* @param {BattlerTag | any} source A battler tag
|
||||
*/
|
||||
loadTag(source: BattlerTag | any): void {
|
||||
super.loadTag(source);
|
||||
this.defenderType = source.defenderType as Type;
|
||||
this.allowedTypes = source.allowedTypes as Type[];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param types {@linkcode Type} of the defending Pokemon
|
||||
* @param moveType {@linkcode Type} of the move targetting it
|
||||
* @returns `true` if the move should be allowed to target the defender.
|
||||
*/
|
||||
ignoreImmunity(type: Type, moveType: Type): boolean {
|
||||
return type === this.defenderType && this.allowedTypes.includes(moveType);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function getBattlerTag(tagType: BattlerTagType, turnCount: number, sourceMove: Moves, sourceId: number): BattlerTag {
|
||||
switch (tagType) {
|
||||
case BattlerTagType.RECHARGING:
|
||||
|
@ -1801,6 +1842,10 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source
|
|||
return new StockpilingTag(sourceMove);
|
||||
case BattlerTagType.OCTOLOCK:
|
||||
return new OctolockTag(sourceId);
|
||||
case BattlerTagType.IGNORE_GHOST:
|
||||
return new ExposedTag(tagType, sourceMove, Type.GHOST, [Type.NORMAL, Type.FIGHTING]);
|
||||
case BattlerTagType.IGNORE_DARK:
|
||||
return new ExposedTag(tagType, sourceMove, Type.DARK, [Type.PSYCHIC]);
|
||||
case BattlerTagType.GULP_MISSILE_ARROKUDA:
|
||||
case BattlerTagType.GULP_MISSILE_PIKACHU:
|
||||
return new GulpMissileTag(tagType, sourceMove);
|
||||
|
|
|
@ -5979,6 +5979,39 @@ export class ResistLastMoveTypeAttr extends MoveEffectAttr {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops the target's immunity to types it is immune to
|
||||
* and makes its evasiveness be ignored during accuracy
|
||||
* checks. Used by: {@linkcode Moves.ODOR_SLEUTH | Odor Sleuth}, {@linkcode Moves.MIRACLE_EYE | Miracle Eye} and {@linkcode Moves.FORESIGHT | Foresight}
|
||||
*
|
||||
* @extends AddBattlerTagAttr
|
||||
* @see {@linkcode apply}
|
||||
*/
|
||||
export class ExposedMoveAttr extends AddBattlerTagAttr {
|
||||
constructor(tagType: BattlerTagType) {
|
||||
super(tagType, false, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies {@linkcode ExposedTag} to the target.
|
||||
* @param user {@linkcode Pokemon} using this move
|
||||
* @param target {@linkcode Pokemon} target of this move
|
||||
* @param move {@linkcode Move} being used
|
||||
* @param args N/A
|
||||
* @returns `true` if the function succeeds
|
||||
*/
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
if (!super.apply(user, target, move, args)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
user.scene.queueMessage(i18next.t("moveTriggers:exposedMove", { pokemonName: getPokemonNameWithAffix(user), targetPokemonName: getPokemonNameWithAffix(target)}));
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const unknownTypeCondition: MoveConditionFunc = (user, target, move) => !user.getTypes().includes(Type.UNKNOWN);
|
||||
|
||||
export type MoveTargetSet = {
|
||||
|
@ -6575,7 +6608,7 @@ export function initMoves() {
|
|||
.attr(StatusEffectAttr, StatusEffect.PARALYSIS)
|
||||
.ballBombMove(),
|
||||
new StatusMove(Moves.FORESIGHT, Type.NORMAL, -1, 40, -1, 0, 2)
|
||||
.unimplemented(),
|
||||
.attr(ExposedMoveAttr, BattlerTagType.IGNORE_GHOST),
|
||||
new SelfStatusMove(Moves.DESTINY_BOND, Type.GHOST, -1, 5, -1, 0, 2)
|
||||
.ignoresProtect()
|
||||
.attr(DestinyBondAttr),
|
||||
|
@ -6935,7 +6968,7 @@ export function initMoves() {
|
|||
.attr(StatChangeAttr, BattleStat.SPATK, -2, true)
|
||||
.attr(HealStatusEffectAttr, true, StatusEffect.FREEZE),
|
||||
new StatusMove(Moves.ODOR_SLEUTH, Type.NORMAL, -1, 40, -1, 0, 3)
|
||||
.unimplemented(),
|
||||
.attr(ExposedMoveAttr, BattlerTagType.IGNORE_GHOST),
|
||||
new AttackMove(Moves.ROCK_TOMB, Type.ROCK, MoveCategory.PHYSICAL, 60, 95, 15, 100, 0, 3)
|
||||
.attr(StatChangeAttr, BattleStat.SPD, -1)
|
||||
.makesContact(false),
|
||||
|
@ -7045,7 +7078,7 @@ export function initMoves() {
|
|||
.attr(AddArenaTagAttr, ArenaTagType.GRAVITY, 5)
|
||||
.target(MoveTarget.BOTH_SIDES),
|
||||
new StatusMove(Moves.MIRACLE_EYE, Type.PSYCHIC, -1, 40, -1, 0, 4)
|
||||
.unimplemented(),
|
||||
.attr(ExposedMoveAttr, BattlerTagType.IGNORE_DARK),
|
||||
new AttackMove(Moves.WAKE_UP_SLAP, Type.FIGHTING, MoveCategory.PHYSICAL, 70, 100, 10, -1, 0, 4)
|
||||
.attr(MovePowerMultiplierAttr, (user, target, move) => targetSleptOrComatoseCondition(user, target, move) ? 2 : 1)
|
||||
.attr(HealStatusEffectAttr, false, StatusEffect.SLEEP),
|
||||
|
|
|
@ -63,6 +63,8 @@ export enum BattlerTagType {
|
|||
STOCKPILING = "STOCKPILING",
|
||||
RECEIVE_DOUBLE_DAMAGE = "RECEIVE_DOUBLE_DAMAGE",
|
||||
ALWAYS_GET_HIT = "ALWAYS_GET_HIT",
|
||||
IGNORE_GHOST = "IGNORE_GHOST",
|
||||
IGNORE_DARK = "IGNORE_DARK",
|
||||
GULP_MISSILE_ARROKUDA = "GULP_MISSILE_ARROKUDA",
|
||||
GULP_MISSILE_PIKACHU = "GULP_MISSILE_PIKACHU"
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ import { pokemonEvolutions, pokemonPrevolutions, SpeciesFormEvolution, SpeciesEv
|
|||
import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "../data/tms";
|
||||
import { DamagePhase, FaintPhase, LearnMovePhase, MoveEffectPhase, ObtainStatusEffectPhase, StatChangePhase, SwitchSummonPhase, ToggleDoublePositionPhase, MoveEndPhase } from "../phases";
|
||||
import { BattleStat } from "../data/battle-stat";
|
||||
import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag } from "../data/battler-tags";
|
||||
import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, ExposedTag } from "../data/battler-tags";
|
||||
import { WeatherType } from "../data/weather";
|
||||
import { TempBattleStat } from "../data/temp-battle-stat";
|
||||
import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "../data/arena-tag";
|
||||
|
@ -1259,6 +1259,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||
if (ignoreImmunity.value) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const exposedTags = this.findTags(tag => tag instanceof ExposedTag) as ExposedTag[];
|
||||
if (exposedTags.some(t => t.ignoreImmunity(defType, moveType))) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return getTypeDamageMultiplier(moveType, defType);
|
||||
|
@ -1865,6 +1870,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||
applyMoveAttrs(IgnoreOpponentStatChangesAttr, this, target, sourceMove, targetEvasionLevel);
|
||||
this.scene.applyModifiers(TempBattleStatBoosterModifier, this.isPlayer(), TempBattleStat.ACC, userAccuracyLevel);
|
||||
|
||||
if (target.findTag(t => t instanceof ExposedTag)) {
|
||||
targetEvasionLevel.value = Math.min(0, targetEvasionLevel.value);
|
||||
}
|
||||
|
||||
const accuracyMultiplier = new Utils.NumberHolder(1);
|
||||
if (userAccuracyLevel.value !== targetEvasionLevel.value) {
|
||||
accuracyMultiplier.value = userAccuracyLevel.value > targetEvasionLevel.value
|
||||
|
|
|
@ -59,4 +59,5 @@ export const moveTriggers: SimpleTranslationEntries = {
|
|||
"copyType": "{{pokemonName}}'s type became the same as\n{{targetPokemonName}}'s type!",
|
||||
"suppressAbilities": "{{pokemonName}}'s ability\nwas suppressed!",
|
||||
"swapArenaTags": "{{pokemonName}} swapped the battle effects affecting each side of the field!",
|
||||
"exposedMove": "{{pokemonName}} identified\n{{targetPokemonName}}!",
|
||||
} as const;
|
||||
|
|
|
@ -59,4 +59,5 @@ export const moveTriggers: SimpleTranslationEntries = {
|
|||
"copyType": "{{pokemonName}} hat den Typ von {{targetPokemonName}} angenommen!",
|
||||
"suppressAbilities": "Die Fähigkeit von {{pokemonName}} wirkt nicht mehr!",
|
||||
"swapArenaTags": "{{pokemonName}} hat die Effekte, die auf den beiden Seiten des Kampffeldes wirken, miteinander getauscht!",
|
||||
"exposedMove": "{{pokemonName}} erkennt {{targetPokemonName}}!",
|
||||
} as const;
|
||||
|
|
|
@ -59,4 +59,5 @@ export const moveTriggers: SimpleTranslationEntries = {
|
|||
"copyType": "{{pokemonName}}'s type became the same as\n{{targetPokemonName}}'s type!",
|
||||
"suppressAbilities": "{{pokemonName}}'s ability\nwas suppressed!",
|
||||
"swapArenaTags": "{{pokemonName}} swapped the battle effects affecting each side of the field!",
|
||||
"exposedMove": "{{pokemonName}} identified\n{{targetPokemonName}}!",
|
||||
} as const;
|
||||
|
|
|
@ -59,4 +59,5 @@ export const moveTriggers: SimpleTranslationEntries = {
|
|||
"copyType": "{{pokemonName}}'s type\nchanged to match {{targetPokemonName}}'s!",
|
||||
"suppressAbilities": "{{pokemonName}}'s ability\nwas suppressed!",
|
||||
"swapArenaTags": "{{pokemonName}} swapped the battle effects affecting each side of the field!",
|
||||
"exposedMove": "{{pokemonName}} identified\n{{targetPokemonName}}!",
|
||||
} as const;
|
||||
|
|
|
@ -59,4 +59,5 @@ export const moveTriggers: SimpleTranslationEntries = {
|
|||
"copyType": "{{pokemonName}} prend le type\nde {{targetPokemonName}} !",
|
||||
"suppressAbilities": "Le talent de {{pokemonName}}\na été rendu inactif !",
|
||||
"swapArenaTags": "Les effets affectant chaque côté du terrain\nont été échangés par {{pokemonName}} !",
|
||||
"exposedMove": "{{targetPokemonName}} est identifié\npar {{pokemonName}} !",
|
||||
} as const;
|
||||
|
|
|
@ -59,4 +59,5 @@ export const moveTriggers: SimpleTranslationEntries = {
|
|||
"copyType": "{{pokemonName}} assume il tipo\ndi {{targetPokemonName}}!",
|
||||
"suppressAbilities": "L’abilità di {{pokemonName}}\nperde ogni efficacia!",
|
||||
"swapArenaTags": "{{pokemonName}} ha invertito gli effetti attivi\nnelle due metà del campo!",
|
||||
"exposedMove": "{{pokemonName}} identified\n{{targetPokemonName}}!",
|
||||
} as const;
|
||||
|
|
|
@ -59,4 +59,5 @@ export const moveTriggers: SimpleTranslationEntries = {
|
|||
"copyType": "{{pokemonName}}は {{targetPokemonName}}と\n同じタイプに なった!",
|
||||
"suppressAbilities": "{{pokemonName}}の とくせいが きかなくなった!",
|
||||
"swapArenaTags": "{{pokemonName}}は\nおたがいの ばのこうかを いれかえた!",
|
||||
"exposedMove": "{{pokemonName}} identified\n{{targetPokemonName}}!",
|
||||
} as const;
|
||||
|
|
|
@ -59,4 +59,5 @@ export const moveTriggers: SimpleTranslationEntries = {
|
|||
"copyType": "{{pokemonName}}[[는]]\n{{targetPokemonName}}[[와]] 같은 타입이 되었다!",
|
||||
"suppressAbilities": "{{pokemonName}}의\n특성이 효과를 발휘하지 못하게 되었다!",
|
||||
"swapArenaTags": "{{pokemonName}}[[는]]\n서로의 필드 효과를 교체했다!",
|
||||
"exposedMove": "{{pokemonName}}[[는]]\n{{targetPokemonName}}의 정체를 꿰뚫어 보았다!",
|
||||
} as const;
|
||||
|
|
|
@ -59,4 +59,5 @@ export const moveTriggers: SimpleTranslationEntries = {
|
|||
"copyType": "O tipo de {{pokemonName}}\nmudou para combinar com {{targetPokemonName}}!",
|
||||
"suppressAbilities": "A habilidade de {{pokemonName}}\nfoi suprimida!",
|
||||
"swapArenaTags": "{{pokemonName}} trocou os efeitos de batalha que afetam cada lado do campo!",
|
||||
"exposedMove": "{{pokemonName}} identificou\n{{targetPokemonName}}!",
|
||||
} as const;
|
||||
|
|
|
@ -59,4 +59,5 @@ export const moveTriggers: SimpleTranslationEntries = {
|
|||
"copyType": "{{pokemonName}}\n变成了{{targetPokemonName}}的属性!",
|
||||
"suppressAbilities": "{{pokemonName}}的特性\n变得无效了!",
|
||||
"swapArenaTags": "{{pokemonName}}\n交换了双方的场地效果!",
|
||||
"exposedMove": "{{pokemonName}}识破了\n{{targetPokemonName}}的原型!",
|
||||
} as const;
|
||||
|
|
|
@ -59,4 +59,5 @@ export const moveTriggers: SimpleTranslationEntries = {
|
|||
"copyType": "{{pokemonName}}變成了{{targetPokemonName}}的屬性!",
|
||||
"suppressAbilities": "{{pokemonName}}的特性\n變得無效了!",
|
||||
"swapArenaTags": "{{pokemonName}}\n交換了雙方的場地效果!",
|
||||
"exposedMove": "{{pokemonName}}識破了\n{{targetPokemonName}}的原形!",
|
||||
} as const;
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import Phaser from "phaser";
|
||||
import GameManager from "#test/utils/gameManager";
|
||||
import { Species } from "#app/enums/species.js";
|
||||
import { SPLASH_ONLY } from "../utils/testUtils";
|
||||
import { Moves } from "#app/enums/moves.js";
|
||||
import { getMovePosition } from "../utils/gameManagerUtils";
|
||||
import { MoveEffectPhase } from "#app/phases.js";
|
||||
|
||||
describe("Internals", () => {
|
||||
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
|
||||
.disableCrits()
|
||||
.enemySpecies(Species.GASTLY)
|
||||
.enemyMoveset(SPLASH_ONLY)
|
||||
.enemyLevel(5)
|
||||
.starterSpecies(Species.MAGIKARP)
|
||||
.moveset([Moves.FORESIGHT, Moves.QUICK_ATTACK, Moves.MACH_PUNCH]);
|
||||
});
|
||||
|
||||
it("should allow Normal and Fighting moves to hit Ghost types", async () => {
|
||||
await game.startBattle();
|
||||
|
||||
const enemy = game.scene.getEnemyPokemon();
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.QUICK_ATTACK));
|
||||
await game.toNextTurn();
|
||||
expect(enemy.hp).toBe(enemy.getMaxHp());
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.FORESIGHT));
|
||||
await game.toNextTurn();
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.QUICK_ATTACK));
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(enemy.hp).toBeLessThan(enemy.getMaxHp());
|
||||
enemy.hp = enemy.getMaxHp();
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.MACH_PUNCH));
|
||||
await game.phaseInterceptor.to(MoveEffectPhase);
|
||||
|
||||
expect(enemy.hp).toBeLessThan(enemy.getMaxHp());
|
||||
});
|
||||
|
||||
it("should ignore target's evasiveness boosts", async () => {
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.MINIMIZE));
|
||||
await game.startBattle();
|
||||
|
||||
const pokemon = game.scene.getPlayerPokemon();
|
||||
vi.spyOn(pokemon, "getAccuracyMultiplier");
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.FORESIGHT));
|
||||
await game.toNextTurn();
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.QUICK_ATTACK));
|
||||
await game.phaseInterceptor.to(MoveEffectPhase);
|
||||
|
||||
expect(pokemon.getAccuracyMultiplier).toHaveReturnedWith(1);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,51 @@
|
|||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import Phaser from "phaser";
|
||||
import GameManager from "#test/utils/gameManager";
|
||||
import { Species } from "#app/enums/species.js";
|
||||
import { SPLASH_ONLY } from "../utils/testUtils";
|
||||
import { Moves } from "#app/enums/moves.js";
|
||||
import { getMovePosition } from "../utils/gameManagerUtils";
|
||||
import { MoveEffectPhase } from "#app/phases.js";
|
||||
|
||||
describe("Internals", () => {
|
||||
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
|
||||
.disableCrits()
|
||||
.enemySpecies(Species.UMBREON)
|
||||
.enemyMoveset(SPLASH_ONLY)
|
||||
.enemyLevel(5)
|
||||
.starterSpecies(Species.MAGIKARP)
|
||||
.moveset([Moves.MIRACLE_EYE, Moves.CONFUSION]);
|
||||
});
|
||||
|
||||
it("should allow Psychic moves to hit Dark types", async () => {
|
||||
await game.startBattle();
|
||||
|
||||
const enemy = game.scene.getEnemyPokemon();
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.CONFUSION));
|
||||
await game.toNextTurn();
|
||||
expect(enemy.hp).toBe(enemy.getMaxHp());
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.MIRACLE_EYE));
|
||||
await game.toNextTurn();
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.CONFUSION));
|
||||
await game.phaseInterceptor.to(MoveEffectPhase);
|
||||
|
||||
expect(enemy.hp).toBeLessThan(enemy.getMaxHp());
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue