diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index d783ea51056..9e121b81fea 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -511,6 +511,39 @@ class WaterSportTag extends WeakenMoveTypeTag { } } +/** + * Arena Tag class for the secondary effect of {@link https://bulbapedia.bulbagarden.net/wiki/Plasma_Fists_(move) | Plasma Fists}. + * Converts Normal-type moves to Electric type for the rest of the turn. + */ +export class PlasmaFistsTag extends ArenaTag { + constructor() { + super(ArenaTagType.PLASMA_FISTS, 1, Moves.PLASMA_FISTS); + } + + /** Queues Plasma Fists' on-add message */ + onAdd(arena: Arena): void { + arena.scene.queueMessage(i18next.t("arenaTag:plasmaFistsOnAdd")); + } + + onRemove(arena: Arena): void { } // Removes default on-remove message + + /** + * Converts Normal-type moves to Electric type + * @param arena n/a + * @param args + * - `[0]` {@linkcode Utils.NumberHolder} A container with a move's {@linkcode Type} + * @returns `true` if the given move type changed; `false` otherwise. + */ + apply(arena: Arena, args: any[]): boolean { + const moveType = args[0]; + if (moveType instanceof Utils.NumberHolder && moveType.value === Type.NORMAL) { + moveType.value = Type.ELECTRIC; + return true; + } + return false; + } +} + /** * Abstract class to implement arena traps. */ @@ -1010,6 +1043,8 @@ export function getArenaTag(tagType: ArenaTagType, turnCount: integer, sourceMov return new MudSportTag(turnCount, sourceId); case ArenaTagType.WATER_SPORT: return new WaterSportTag(turnCount, sourceId); + case ArenaTagType.PLASMA_FISTS: + return new PlasmaFistsTag(); case ArenaTagType.SPIKES: return new SpikesTag(sourceId, side); case ArenaTagType.TOXIC_SPIKES: diff --git a/src/data/move.ts b/src/data/move.ts index 59db7495754..7c5962abb22 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -8906,8 +8906,8 @@ export function initMoves() { .attr(HalfSacrificialAttr) .target(MoveTarget.ALL_NEAR_OTHERS), new AttackMove(Moves.PLASMA_FISTS, Type.ELECTRIC, MoveCategory.PHYSICAL, 100, 100, 15, -1, 0, 7) - .punchingMove() - .partial(), + .attr(AddArenaTagAttr, ArenaTagType.PLASMA_FISTS, 1) + .punchingMove(), new AttackMove(Moves.PHOTON_GEYSER, Type.PSYCHIC, MoveCategory.SPECIAL, 100, 100, 5, -1, 0, 7) .attr(PhotonGeyserCategoryAttr) .ignoresAbilities() diff --git a/src/enums/arena-tag-type.ts b/src/enums/arena-tag-type.ts index c6f911cb493..123d70b64fa 100644 --- a/src/enums/arena-tag-type.ts +++ b/src/enums/arena-tag-type.ts @@ -25,4 +25,5 @@ export enum ArenaTagType { SAFEGUARD = "SAFEGUARD", NO_CRIT = "NO_CRIT", IMPRISON = "IMPRISON", + PLASMA_FISTS = "PLASMA_FISTS", } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 58396c59fa6..a318d3ffdeb 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -1518,6 +1518,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { applyMoveAttrs(VariableMoveTypeAttr, this, null, move, moveTypeHolder); applyPreAttackAbAttrs(MoveTypeChangeAbAttr, this, null, move, simulated, moveTypeHolder); + this.scene.arena.applyTags(ArenaTagType.PLASMA_FISTS, moveTypeHolder); + return moveTypeHolder.value as Type; } diff --git a/src/locales/de/arena-tag.json b/src/locales/de/arena-tag.json index 3bed4fefbd0..93ceb06f308 100644 --- a/src/locales/de/arena-tag.json +++ b/src/locales/de/arena-tag.json @@ -28,6 +28,7 @@ "mudSportOnRemove": "Lehmsuhler hört auf zu wirken!", "waterSportOnAdd": "Die Stärke aller Feuer-Attacken wurde reduziert!", "waterSportOnRemove": "Nassmacher hört auf zu wirken!", + "plasmaFistsOnAdd": "Ein elektrisch geladener Niederschlag regnet auf das Kampffeld herab!", "spikesOnAdd": "Die {{opponentDesc}} sind von Stacheln umgeben!", "spikesActivateTrap": "Die {{pokemonNameWithAffix}} wurde durch Stachler verletzt!!", "toxicSpikesOnAdd": "Die {{opponentDesc}} sind überall von giftigen Stacheln umgeben", @@ -54,4 +55,4 @@ "safeguardOnRemove": "Der mystische Schleier, der das ganze Feld umgab, hat sich gelüftet!", "safeguardOnRemovePlayer": "Der mystische Schleier, der dein Team umgab, hat sich gelüftet!", "safeguardOnRemoveEnemy": "Der mystische Schleier, der das gegnerische Team umgab, hat sich gelüftet!" -} \ No newline at end of file +} diff --git a/src/locales/en/arena-tag.json b/src/locales/en/arena-tag.json index d8fed386b24..df79693c7bb 100644 --- a/src/locales/en/arena-tag.json +++ b/src/locales/en/arena-tag.json @@ -28,6 +28,7 @@ "mudSportOnRemove": "The effects of Mud Sport\nhave faded.", "waterSportOnAdd": "Fire's power was weakened!", "waterSportOnRemove": "The effects of Water Sport\nhave faded.", + "plasmaFistsOnAdd": "A deluge of ions showers the battlefield!", "spikesOnAdd": "{{moveName}} were scattered\nall around {{opponentDesc}}'s feet!", "spikesActivateTrap": "{{pokemonNameWithAffix}} is hurt\nby the spikes!", "toxicSpikesOnAdd": "{{moveName}} were scattered\nall around {{opponentDesc}}'s feet!", diff --git a/src/locales/es/arena-tag.json b/src/locales/es/arena-tag.json index 0f63b62e784..9aa37654c62 100644 --- a/src/locales/es/arena-tag.json +++ b/src/locales/es/arena-tag.json @@ -28,6 +28,7 @@ "mudSportOnRemove": "Chapoteo Lodo ha dejado de surtir efecto.", "waterSportOnAdd": "¡Se han debilitado los ataques\nde tipo Fuego!", "waterSportOnRemove": "Hidrochorro ha dejado de surtir efecto.", + "plasmaFistsOnAdd": "¡Una lluvia de electrones cae sobre\nel terreno de combate!", "spikesOnAdd": "¡El equipo de {{opponentDesc}} ha sido rodeado por {{moveName}}!", "spikesActivateTrap": "¡Las púas han herido a {{pokemonNameWithAffix}}!", "toxicSpikesOnAdd": "¡El equipo de {{opponentDesc}} ha sido rodeado por {{moveName}}!", @@ -54,4 +55,4 @@ "safeguardOnRemove": "¡Velo Sagrado dejó de hacer efecto!", "safeguardOnRemovePlayer": "El efecto de Velo Sagrado en tu equipo se ha disipado.", "safeguardOnRemoveEnemy": "El efecto de Velo Sagrado en el equipo enemigo se ha disipado." -} \ No newline at end of file +} diff --git a/src/locales/fr/arena-tag.json b/src/locales/fr/arena-tag.json index 9cb2f342068..95e38cdbe9d 100644 --- a/src/locales/fr/arena-tag.json +++ b/src/locales/fr/arena-tag.json @@ -28,6 +28,7 @@ "mudSportOnRemove": "L’effet de Lance-Boue se dissipe !", "waterSportOnAdd": "La puissance des capacités\nde type Feu diminue !", "waterSportOnRemove": "L’effet de Tourniquet se dissipe !", + "plasmaFistsOnAdd": "Un déluge de plasma s’abat sur le terrain !", "spikesOnAdd": "Des {{moveName}} s’éparpillent autour de {{opponentDesc}} !", "spikesActivateTrap": "{{pokemonNameWithAffix}} est blessé\npar les picots !", "toxicSpikesOnAdd": "Des {{moveName}} s’éparpillent autour de {{opponentDesc}} !", diff --git a/src/locales/it/arena-tag.json b/src/locales/it/arena-tag.json index a1c5ee5b3c9..be2a06eb898 100644 --- a/src/locales/it/arena-tag.json +++ b/src/locales/it/arena-tag.json @@ -1,8 +1,9 @@ { + "plasmaFistsOnAdd": "Una pioggia di elettroni si rovescia sui Pokémon!", "safeguardOnAdd": "Un velo mistico ricopre il campo!", "safeguardOnAddPlayer": "Un velo mistico ricopre la tua squadra!", "safeguardOnAddEnemy": "Un velo mistico ricopre la squadra avversaria!", "safeguardOnRemove": "Il campo non è più protetto da Salvaguardia!", "safeguardOnRemovePlayer": "La tua squadra non è più protetta da Salvaguardia!", "safeguardOnRemoveEnemy": "La squadra avversaria non è più protetta da Salvaguardia!" -} \ No newline at end of file +} diff --git a/src/locales/ja/arena-tag.json b/src/locales/ja/arena-tag.json index a81942338fd..0da759884a5 100644 --- a/src/locales/ja/arena-tag.json +++ b/src/locales/ja/arena-tag.json @@ -28,6 +28,7 @@ "mudSportOnRemove": "どろあそびの 効果が なくなった!", "waterSportOnAdd": "炎の威力が 弱まった!", "waterSportOnRemove": "みずあそびの 効果が なくなった!", + "plasmaFistsOnAdd": "電子のシャワーが 降りそそいだ!", "spikesOnAdd": "{{opponentDesc}}の 足下に\n{{moveName}}が 散らばった!", "spikesActivateTrap": "{{pokemonNameWithAffix}}は\nまきびしの ダメージを 受けた!", "toxicSpikesOnAdd": "{{opponentDesc}}の 足下に\n{{moveName}}が 散らばった!", diff --git a/src/locales/ko/arena-tag.json b/src/locales/ko/arena-tag.json index ce9922ab3bf..c1a7b1ca7ca 100644 --- a/src/locales/ko/arena-tag.json +++ b/src/locales/ko/arena-tag.json @@ -28,6 +28,7 @@ "mudSportOnRemove": "흙놀이의 효과가\n없어졌다!", "waterSportOnAdd": "불꽃의 위력이 약해졌다!", "waterSportOnRemove": "물놀이의 효과가\n없어졌다!", + "plasmaFistsOnAdd": "전기 입자가 쏟아졌다!", "spikesOnAdd": "{{opponentDesc}}의 발밑에\n압정이 뿌려졌다!", "spikesActivateTrap": "{{pokemonNameWithAffix}}[[는]]\n압정뿌리기의 데미지를 입었다!", "toxicSpikesOnAdd": "{{opponentDesc}}의 발밑에\n독압정이 뿌려졌다!", @@ -54,4 +55,4 @@ "safeguardOnRemove": "필드를 감싸던 신비의 베일이 없어졌다!", "safeguardOnRemovePlayer": "우리 편을 감싸던 신비의 베일이 없어졌다!", "safeguardOnRemoveEnemy": "상대 편을 감싸던 신비의 베일이 없어졌다!" -} \ No newline at end of file +} diff --git a/src/locales/pt_BR/arena-tag.json b/src/locales/pt_BR/arena-tag.json index 3a1476dcef6..5fb8b49565f 100644 --- a/src/locales/pt_BR/arena-tag.json +++ b/src/locales/pt_BR/arena-tag.json @@ -28,6 +28,7 @@ "mudSportOnRemove": "Os efeitos de Mud Sport\nsumiram.", "waterSportOnAdd": "O poder de movimentos de fogo foi enfraquecido!", "waterSportOnRemove": "Os efeitos de Water Sport\nsumiram.", + "plasmaFistsOnAdd": "Um dilúvio de íons chove sobre o campo de batalha!", "spikesOnAdd": "{{moveName}} foram espalhados\nno chão ao redor de {{opponentDesc}}!", "spikesActivateTrap": "{{pokemonNameWithAffix}} foi ferido\npelos espinhos!", "toxicSpikesOnAdd": "{{moveName}} foram espalhados\nno chão ao redor de {{opponentDesc}}!", diff --git a/src/locales/zh_CN/arena-tag.json b/src/locales/zh_CN/arena-tag.json index 74ad38ba9bf..d7ac1b9b04b 100644 --- a/src/locales/zh_CN/arena-tag.json +++ b/src/locales/zh_CN/arena-tag.json @@ -28,6 +28,7 @@ "mudSportOnRemove": "玩泥巴的效果消失了!", "waterSportOnAdd": "火焰的威力减弱了!", "waterSportOnRemove": "玩水的效果消失了!", + "plasmaFistsOnAdd": "等离子雨倾盆而下!", "spikesOnAdd": "{{opponentDesc}}脚下\n散落着{{moveName}}!", "spikesActivateTrap": "{{pokemonNameWithAffix}}\n受到了撒菱的伤害!", "toxicSpikesOnAdd": "{{opponentDesc}}脚下\n散落着{{moveName}}!", @@ -54,4 +55,4 @@ "safeguardOnRemove": "包围整个场地的\n神秘之幕消失了!", "safeguardOnRemovePlayer": "包围我方的\n神秘之幕消失了!", "safeguardOnRemoveEnemy": "包围对手的\n神秘之幕消失了!" -} \ No newline at end of file +} diff --git a/src/locales/zh_TW/arena-tag.json b/src/locales/zh_TW/arena-tag.json index a6224f300a3..4a08d268e20 100644 --- a/src/locales/zh_TW/arena-tag.json +++ b/src/locales/zh_TW/arena-tag.json @@ -28,6 +28,7 @@ "mudSportOnRemove": "玩泥巴的效果消失了!", "waterSportOnAdd": "火焰的威力減弱了!", "waterSportOnRemove": "玩水的效果消失了!", + "plasmaFistsOnAdd": "等離子雨傾盆而下!", "spikesOnAdd": "{{opponentDesc}}腳下\n散落著{{moveName}}!", "spikesActivateTrap": "{{pokemonNameWithAffix}}\n受到了撒菱的傷害!", "toxicSpikesOnAdd": "{{opponentDesc}}腳下\n散落著{{moveName}}!", diff --git a/src/test/moves/plasma_fists.test.ts b/src/test/moves/plasma_fists.test.ts new file mode 100644 index 00000000000..a9bd7660dfd --- /dev/null +++ b/src/test/moves/plasma_fists.test.ts @@ -0,0 +1,98 @@ +import { BattlerIndex } from "#app/battle"; +import { Type } from "#app/data/type"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, it, expect, vi } from "vitest"; + +describe("Moves - Plasma Fists", () => { + 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 + .moveset([Moves.PLASMA_FISTS, Moves.TACKLE]) + .battleType("double") + .startingLevel(100) + .enemySpecies(Species.DUSCLOPS) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.TACKLE) + .enemyLevel(100); + }); + + it("should convert all subsequent Normal-type attacks to Electric-type", async () => { + await game.classicMode.startBattle([Species.DUSCLOPS, Species.BLASTOISE]); + + const field = game.scene.getField(true); + field.forEach(p => vi.spyOn(p, "getMoveType")); + + game.move.select(Moves.PLASMA_FISTS, 0, BattlerIndex.ENEMY); + game.move.select(Moves.TACKLE, 1, BattlerIndex.ENEMY_2); + + await game.forceEnemyMove(Moves.TACKLE, BattlerIndex.PLAYER); + await game.forceEnemyMove(Moves.TACKLE, BattlerIndex.PLAYER_2); + + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]); + + await game.phaseInterceptor.to("BerryPhase", false); + + field.forEach(p => { + expect(p.getMoveType).toHaveLastReturnedWith(Type.ELECTRIC); + expect(p.hp).toBeLessThan(p.getMaxHp()); + }); + }); + + it("should not affect Normal-type attacks boosted by Pixilate", async () => { + game.override + .battleType("single") + .enemyAbility(Abilities.PIXILATE); + + await game.classicMode.startBattle([Species.ONIX]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + vi.spyOn(enemyPokemon, "getMoveType"); + + game.move.select(Moves.PLASMA_FISTS); + + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); + await game.phaseInterceptor.to("BerryPhase", false); + + expect(enemyPokemon.getMoveType).toHaveLastReturnedWith(Type.FAIRY); + expect(playerPokemon.hp).toBeLessThan(playerPokemon.getMaxHp()); + }); + + it("should affect moves that become Normal type due to Normalize", async () => { + game.override + .battleType("single") + .enemyAbility(Abilities.NORMALIZE) + .enemyMoveset(Moves.WATER_GUN); + + await game.classicMode.startBattle([Species.DUSCLOPS]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + vi.spyOn(enemyPokemon, "getMoveType"); + + game.move.select(Moves.PLASMA_FISTS); + + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); + await game.phaseInterceptor.to("BerryPhase", false); + + expect(enemyPokemon.getMoveType).toHaveLastReturnedWith(Type.ELECTRIC); + expect(playerPokemon.hp).toBeLessThan(playerPokemon.getMaxHp()); + }); +}); diff --git a/src/test/utils/gameManager.ts b/src/test/utils/gameManager.ts index 6fad87df182..a2403de7e18 100644 --- a/src/test/utils/gameManager.ts +++ b/src/test/utils/gameManager.ts @@ -303,7 +303,7 @@ export default class GameManager { vi.spyOn(enemy, "getNextMove").mockReturnValueOnce({ move: moveId, - targets: (target && !legalTargets.multiple && legalTargets.targets.includes(target)) + targets: (target !== undefined && !legalTargets.multiple && legalTargets.targets.includes(target)) ? [target] : enemy.getNextTargets(moveId) });