This commit is contained in:
Mason S 2024-09-07 21:46:25 -07:00 committed by GitHub
commit 33137d1c2e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 339 additions and 14 deletions

View File

@ -327,6 +327,49 @@ export class ShellTrapTag extends BattlerTag {
}
}
/**
* BattlerTag implementing Rage
* Pokémon with this tag will recieve an attack boost when successfully damaged by an attacking move
* This tag will be lost if a target reaches the MOVE_EFFECT lapse condition with a move other than Rage
* @see {@link https://bulbapedia.bulbagarden.net/wiki/Rage_(move) | Rage}
*/
export class RageTag extends BattlerTag {
constructor() {
super(BattlerTagType.RAGE, [BattlerTagLapseType.MOVE_EFFECT], 1, Moves.RAGE);
}
/**
* Displays a message to show that the user has started Raging.
* This is message isn't displayed on cartridge, and was included for clarity during gameplay and while testing.
* @param pokemon {@linkcode Pokemon} the Pokémon this tag is being added to.
*/
onAdd(pokemon: Pokemon) {
super.onAdd(pokemon);
/* This message might not exist on cartridge */
pokemon.scene.queueMessage(i18next.t("battlerTags:rageOnAdd", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon)}));
}
/**
* Checks to maintain a Pokémon should maintain their rage, and provides an attack boost when hit.
* @param pokemon {@linkcode Pokemon} The owner of this tag
* @param lapseType {@linkcode BattlerTagLapseType} the type of functionality invoked in battle
* @returns `true` if invoked with the MOVE_EFFECT lapse type and {@linkcode pokemon} most recently used Rage,
* or if invoked with the CUSTOM lapse type. false otherwise.
*/
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
if (lapseType === BattlerTagLapseType.MOVE_EFFECT) {
return (pokemon.scene.getCurrentPhase() as MovePhase).move.getMove().id === Moves.RAGE;
} else if (lapseType === BattlerTagLapseType.CUSTOM) {
pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [Stat.ATK], 1, false));
pokemon.scene.queueMessage(i18next.t("battlerTags:rageOnHit", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon)}));
return true;
}
return false;
}
}
export class TrappedTag extends BattlerTag {
constructor(tagType: BattlerTagType, lapseType: BattlerTagLapseType, turnCount: number, sourceMove: Moves, sourceId: number) {
super(tagType, lapseType, turnCount, sourceMove, sourceId, true);
@ -2125,6 +2168,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source
case BattlerTagType.GULP_MISSILE_ARROKUDA:
case BattlerTagType.GULP_MISSILE_PIKACHU:
return new GulpMissileTag(tagType, sourceMove);
case BattlerTagType.RAGE:
return new RageTag();
case BattlerTagType.NONE:
default:
return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId);

View File

@ -6744,7 +6744,7 @@ export function initMoves() {
.attr(StatStageChangeAttr, [ Stat.SPD ], 2, true),
new AttackMove(Moves.QUICK_ATTACK, Type.NORMAL, MoveCategory.PHYSICAL, 40, 100, 30, -1, 1, 1),
new AttackMove(Moves.RAGE, Type.NORMAL, MoveCategory.PHYSICAL, 20, 100, 20, -1, 0, 1)
.partial(),
.attr(AddBattlerTagAttr, BattlerTagType.RAGE, true, false, 0, 0, false, true),
new SelfStatusMove(Moves.TELEPORT, Type.PSYCHIC, -1, 20, -1, -6, 1)
.attr(ForceSwitchOutAttr, true)
.hidesUser(),

View File

@ -71,6 +71,7 @@ export enum BattlerTagType {
GULP_MISSILE_PIKACHU = "GULP_MISSILE_PIKACHU",
BEAK_BLAST_CHARGING = "BEAK_BLAST_CHARGING",
SHELL_TRAP = "SHELL_TRAP",
RAGE = "RAGE",
DRAGON_CHEER = "DRAGON_CHEER",
NO_RETREAT = "NO_RETREAT",
NO_RETREAT = "NO_RETREAT"
}

View File

@ -68,6 +68,8 @@
"cursedOnAdd": "{{pokemonNameWithAffix}} cut its own HP and put a curse on the {{pokemonName}}!",
"cursedLapse": "{{pokemonNameWithAffix}} is afflicted by the Curse!",
"stockpilingOnAdd": "{{pokemonNameWithAffix}} stockpiled {{stockpiledCount}}!",
"rageOnAdd": "{{pokemonNameWithAffix}}'s rage is starting to build.",
"rageOnHit": "{{pokemonNameWithAffix}}'s rage is building.",
"disabledOnAdd": "{{pokemonNameWithAffix}}'s {{moveName}}\nwas disabled!",
"disabledLapse": "{{pokemonNameWithAffix}}'s {{moveName}}\nis no longer disabled."
}

View File

@ -67,5 +67,8 @@
"saltCuredLapse": "¡{{moveName}} ha herido a {{pokemonNameWithAffix}}!",
"cursedOnAdd": "¡{{pokemonNameWithAffix}} sacrifica algunos PS y maldice a {{pokemonName}}!",
"cursedLapse": "¡{{pokemonNameWithAffix}} es víctima de una maldición!",
"stockpilingOnAdd": "¡{{pokemonNameWithAffix}} ha reservado energía por {{stockpiledCount}}ª vez!"
"stockpilingOnAdd": "¡{{pokemonNameWithAffix}} ha reservado energía por {{stockpiledCount}}ª vez!",
"rageOnAdd": "¡La furia de {{pokemonNameWithAffix}} comienza a crecer!",
"rageOnHit": "¡La furia de {{pokemonNameWithAffix}} está aumentando!"
}

View File

@ -67,5 +67,7 @@
"saltCuredLapse": "{{pokemonNameWithAffix}} est blessé\npar la capacité {{moveName}} !",
"cursedOnAdd": "{{pokemonNameWithAffix}} sacrifie des PV\net lance une malédiction sur {{pokemonName}} !",
"cursedLapse": "{{pokemonNameWithAffix}} est touché par la malédiction !",
"stockpilingOnAdd": "{{pokemonNameWithAffix}} utilise\nla capacité Stockage {{stockpiledCount}} fois !"
"stockpilingOnAdd": "{{pokemonNameWithAffix}} utilise\nla capacité Stockage {{stockpiledCount}} fois !",
"rageOnAdd": "La Frénésie sempare\nde {{pokemonNameWithAffix}} !",
"rageOnHit": "La Frénésie de {{pokemonNameWithAffix}}\naugmente !"
}

View File

@ -67,5 +67,7 @@
"saltCuredLapse": "{{pokemonNameWithAffix}} viene colpito da {{moveName}}!",
"cursedOnAdd": "{{pokemonNameWithAffix}} ha sacrificato metà dei suoi PS per\nlanciare una maledizione su {{pokemonName}}!",
"cursedLapse": "{{pokemonNameWithAffix}} subisce la maledizione!",
"stockpilingOnAdd": "{{pokemonNameWithAffix}} ha usato Accumulo per la\n{{stockpiledCount}}ª volta!"
}
"stockpilingOnAdd": "{{pokemonNameWithAffix}} ha usato Accumulo per la\n{{stockpiledCount}}ª volta!",
"rageOnAdd": "{{pokemonNameWithAffix}} comincia ad accumulare ira!",
"rageOnHit": "Lira di {{pokemonNameWithAffix}} aumenta!"
}

View File

@ -67,5 +67,7 @@
"saltCuredLapse": "{{pokemonNameWithAffix}}[[는]] 소금절이의\n데미지를 입고 있다.",
"cursedOnAdd": "{{pokemonNameWithAffix}}[[는]] 자신의 체력을 깎아서\n{{pokemonName}}에게 저주를 걸었다!",
"cursedLapse": "{{pokemonNameWithAffix}}[[는]]\n저주받고 있다!",
"stockpilingOnAdd": "{{pokemonNameWithAffix}}[[는]]\n{{stockpiledCount}}개 비축했다!"
}
"stockpilingOnAdd": "{{pokemonNameWithAffix}}[[는]]\n{{stockpiledCount}}개 비축했다!",
"rageOnAdd": "{{pokemonNameWithAffix}}[[는]]\n분노 볼티지를 쌓기 시작했다.",
"rageOnHit": "{{pokemonNameWithAffix}}의\n분노 볼티지가 올라가고 있다!"
}

View File

@ -67,5 +67,7 @@
"saltCuredLapse": "{{pokemonNameWithAffix}} foi ferido pelo {{moveName}}!",
"cursedOnAdd": "{{pokemonNameWithAffix}} cortou seus PS pela metade e amaldiçoou {{pokemonName}}!",
"cursedLapse": "{{pokemonNameWithAffix}} foi ferido pelo Curse!",
"stockpilingOnAdd": "{{pokemonNameWithAffix}} estocou {{stockpiledCount}}!"
}
"stockpilingOnAdd": "{{pokemonNameWithAffix}} estocou {{stockpiledCount}}!",
"rageOnAdd": "A raiva de {{pokemonNameWithAffix}} está começando a aumentar.",
"rageOnHit": "A raiva de {{pokemonNameWithAffix}} está aumentando."
}

View File

@ -67,5 +67,7 @@
"saltCuredLapse": "{{pokemonNameWithAffix}}\n受到了{{moveName}}的伤害!",
"cursedOnAdd": "{{pokemonNameWithAffix}}削减了自己的体力,\n并诅咒了{{pokemonName}}",
"cursedLapse": "{{pokemonNameWithAffix}}\n正受到诅咒",
"stockpilingOnAdd": "{{pokemonNameWithAffix}}蓄力了{{stockpiledCount}}次!"
}
"stockpilingOnAdd": "{{pokemonNameWithAffix}}蓄力了{{stockpiledCount}}次!",
"rageOnAdd": "{{pokemonNameWithAffix}}的\n怒气开始上升了。",
"rageOnHit": "{{pokemonNameWithAffix}}的\n怒气正在上升."
}

View File

@ -66,5 +66,7 @@
"saltCuredOnAdd": "{{pokemonNameWithAffix}} 陷入了鹽腌狀態!",
"saltCuredLapse": "{{pokemonNameWithAffix}} 受到了{{moveName}}的傷害!",
"cursedOnAdd": "{{pokemonNameWithAffix}}削減了自己的體力,並詛咒了{{pokemonName}}",
"cursedLapse": "{{pokemonNameWithAffix}}正受到詛咒!"
}
"cursedLapse": "{{pokemonNameWithAffix}}正受到詛咒!",
"rageOnAdd": "{{pokemonNameWithAffix}}的\n怒氣開始上升了。",
"rageOnHit": "{{pokemonNameWithAffix}}的\n怒氣正在上升"
}

View File

@ -262,6 +262,9 @@ export class MoveEffectPhase extends PokemonPhase {
if (move.category === MoveCategory.PHYSICAL && user.isPlayer() !== target.isPlayer()) {
target.lapseTag(BattlerTagType.SHELL_TRAP);
}
if (hitResult < HitResult.NO_EFFECT && move.category !== MoveCategory.STATUS) {
target.lapseTag(BattlerTagType.RAGE);
}
if (!user.isPlayer() && this.move.getMove() instanceof AttackMove) {
user.scene.applyShuffledModifiers(this.scene, EnemyAttackStatusEffectChanceModifier, false, target);
}

259
src/test/moves/rage.test.ts Normal file
View File

@ -0,0 +1,259 @@
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import GameManager from "#test/utils/gameManager";
import { Species } from "#enums/species";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import {StatusEffect} from "#enums/status-effect";
import {RageTag} from "#app/data/battler-tags";
import {PlayerPokemon} from "#app/field/pokemon";
import {Nature} from "#enums/nature";
import {CommandPhase} from "#app/phases/command-phase";
import {BattlerIndex} from "#app/battle";
import {TurnEndPhase} from "#app/phases/turn-end-phase";
import {Stat} from "#enums/stat";
const TIMEOUT = 20 * 1000;
function fullOf(move: Moves) : Moves[] {
return [move, move, move, move];
}
describe("Moves - Rage", () => {
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
.battleType("single")
.ability(Abilities.UNNERVE)
.starterSpecies(Species.BOLTUND)
.moveset([Moves.RAGE, Moves.SPLASH, Moves.SPORE, Moves.VITAL_THROW])
.startingLevel(100)
.enemyLevel(100)
.disableCrits();
});
/**
* Ally Attack-Boost Test.
*
* Checks that Rage provides an attack boost if the user it hit after use.
*
* Checks that Rage provides an attack boost if the user is hit before moving
* on the following turn, regardless of what move they selected.
*
* Checks that a pokemon stops raging if they use a different move.
*/
it(
"should raise attack if hit after use",
async () => {
game.override
.enemySpecies(Species.SHUCKLE)
.enemyMoveset(fullOf(Moves.TACKLE));
await game.classicMode.startBattle();
const leadPokemon = game.scene.getPlayerPokemon()!;
// Player Boltund uses Rage. Opponent Shuckle uses Tackle.
// Boltund's attack is raised.
game.move.select(Moves.RAGE);
await game.toNextTurn();
expect(leadPokemon.getStatStage(Stat.ATK)).toBe(1);
// Opponent Shuckle uses Tackle. Player Boltund uses Vital Throw (Negative Priority).
// Boltund's attack is raised.
game.move.select(Moves.VITAL_THROW);
await game.toNextTurn();
expect(leadPokemon.getStatStage(Stat.ATK)).toBe(2);
// Opponent Shuckle uses Tackle. Player Boltund uses Vital Throw (Negative Priority).
// Boltund's attack not raised.
game.move.select(Moves.VITAL_THROW);
await game.toNextTurn();
expect(leadPokemon.getStatStage(Stat.ATK)).toBe(2);
}, TIMEOUT
);
/**
* Checks that Opponent Pokemon correctly receive the Attack boost from Rage
* Checks that using a Status Move on a Pokemon that used Rage does NOT provide an Attack Boost
* Checks that Pokemon do not lose the {@linkcode RageTag} BattlerTag when sleeping.
*/
it(
"should not raise ATK if hit by status move",
async () => {
game.override
.enemySpecies(Species.SHUCKLE)
.enemyMoveset(fullOf(Moves.RAGE));
await game.classicMode.startBattle();
const leadPokemon = game.scene.getPlayerPokemon()!;
const oppPokemon = game.scene.getEnemyPokemon()!;
// Opponent Shuckle uses Rage. Ally Boltund uses Vital Throw.
// Shuckle gets an Attack boost
game.move.select(Moves.VITAL_THROW);
await game.toNextTurn();
expect(leadPokemon.getStatStage(Stat.ATK)).toBe(0);
expect(oppPokemon.getStatStage(Stat.ATK)).toBe(1);
// Ally Boltund uses Spore. Shuckle is asleep.
// Shuckle does not get an attack boost. Shuckle still has the RageTag tag.
game.move.select(Moves.SPORE);
await game.toNextTurn();
expect(leadPokemon.getStatStage(Stat.ATK)).toBe(0);
expect(oppPokemon.getStatStage(Stat.ATK)).toBe(1);
expect(oppPokemon.getTag(RageTag)).toBeTruthy;
}, TIMEOUT
);
/**
* Checks that the {@linkcode RageTag} tag is not given if the target is immune
*/
it(
"should not raise ATK if target is immune",
async () => {
game.override
.enemySpecies(Species.GASTLY)
.enemyMoveset(fullOf(Moves.TACKLE)); // Has semi-invulnerable turn
await game.classicMode.startBattle();
const leadPokemon = game.scene.getPlayerPokemon()!;
// Boltund uses rage, but it has no effect, Gastly uses Tackle
// Boltund does not have RageTag or Attack boost.
game.move.select(Moves.RAGE);
await game.toNextTurn();
expect(leadPokemon.getStatStage(Stat.ATK)).toBe(0);
expect(leadPokemon.getTag(RageTag)).toBeNull;
}, TIMEOUT
);
/**
* Checks that the {@linkcode RageTag} tag is not given if the target is semi-invulnerable
* Checks that Pokémon does not get Attack boost if it uses Rage after getting hit on a turn
*/
it(
"should not raise ATK if target is semi-invulnerable",
async () => {
game.override
.enemySpecies(Species.REGIELEKI)
.enemyMoveset(fullOf(Moves.PHANTOM_FORCE)); // Has semi-invulnerable turn
await game.classicMode.startBattle();
const leadPokemon = game.scene.getPlayerPokemon()!;
// Regieliki uses Fly. Boltund uses Rage, but Regieleki is invulnerable
// Boltund does not gain RageTag or Attack boost
game.move.select(Moves.RAGE);
await game.toNextTurn();
expect(leadPokemon.getStatStage(Stat.ATK)).toBe(0);
expect(leadPokemon.getTag(RageTag)).toBeNull;
// Regieleki finishes Fly, Boltund uses Rage
// Boltund gains RageTag, but no boost
game.move.select(Moves.RAGE);
await game.toNextTurn();
expect(leadPokemon.getStatStage(Stat.ATK)).toBe(0);
expect(leadPokemon.getTag(RageTag)).toBeTruthy;
}, TIMEOUT
);
it(
"should not stop raging if rage fails",
async () => {
game.override
.enemySpecies(Species.SHUCKLE)
.enemyMoveset(fullOf(Moves.PHANTOM_FORCE)); // Has semi-invulnerable turn
await game.classicMode.startBattle();
const leadPokemon = game.scene.getPlayerPokemon()!;
// Boltund uses Rage, Shuckle uses Fly
// Boltund gains RageTag
game.move.select(Moves.RAGE);
await game.toNextTurn();
expect(leadPokemon.getStatStage(Stat.ATK)).toBe(0);
expect(leadPokemon.getTag(RageTag)).toBeTruthy;
// Boltund uses Rage, Shuckle is underwater, Shuckle finishes Dive
// Boltund gains gains boost, does not lose RageTag
game.move.select(Moves.RAGE);
await game.toNextTurn();
expect(leadPokemon.getStatStage(Stat.ATK)).toBe(1);
expect(leadPokemon.getTag(RageTag)).toBeTruthy;
}, TIMEOUT
);
/**
* Basic doubles test
* Checks that if a raging Pokemon is hit multiple times in one turn, they get multiple boosts
* Should also cover multi-hit moves
*/
it(
"should provide boost per hit in doubles",
async () => {
game.override
.moveset([Moves.RAGE, Moves.MEMENTO, Moves.SPORE, Moves.VITAL_THROW])
.battleType("double")
.enemySpecies(Species.SHUCKLE)
.enemyMoveset(fullOf(Moves.TACKLE));
await game.classicMode.startBattle([Species.BOLTUND, Species.BOLTUND]);
const leadPokemon = game.scene.getParty()[0];
game.move.select(Moves.RAGE, 1, BattlerIndex.ENEMY);
await game.phaseInterceptor.to(CommandPhase);
game.move.select(Moves.MEMENTO, 1, BattlerIndex.ENEMY_2);
await game.phaseInterceptor.to(TurnEndPhase, false);
expect(leadPokemon.getStatStage(Stat.ATK)).toBe(2);
expect(leadPokemon.getTag(RageTag)).toBeTruthy;
}, TIMEOUT
);
/**
* Checks that a pokemon does not lose the RageTag if it is unable to act
* regardless of what it was otherwise going to do
*/
it(
"should stay raging if unable to act",
async () => {
game.override
.moveset([Moves.RAGE, Moves.SPLASH, Moves.SPORE, Moves.VITAL_THROW])
.battleType("double")
.enemySpecies(Species.SHUCKLE)
.enemyMoveset(fullOf(Moves.SPLASH)); // Has semi-invulnerable turn
await game.classicMode.startBattle();
const leadPokemon: PlayerPokemon = game.scene.getParty()[0];
// Ensure that second pokemon is faster.
leadPokemon.natureOverride = Nature.SASSY;
game.scene.getParty()[1].natureOverride = Nature.JOLLY;
game.move.select(Moves.RAGE, 1, BattlerIndex.ENEMY);
await game.phaseInterceptor.to(CommandPhase);
game.move.select(Moves.SPORE, 1, BattlerIndex.PLAYER);
await game.phaseInterceptor.to(TurnEndPhase, false);
expect(leadPokemon.getStatStage(Stat.ATK)).toBe(0);
expect(leadPokemon.getTag(RageTag)).toBeTruthy;
expect(leadPokemon.status?.effect).toBe(StatusEffect.SLEEP);
}, TIMEOUT
);
});