[Move] Fully Implement the Pledge Moves (#4511)
* Implement Fire/Grass Pledge combo * Add other Pledge combo effects (untested) * Fix missing enums * Pledge moves integration tests * Add turn order manipulation + more tests * Safeguarding against weird Instruct interactions * Update src/test/moves/pledge_moves.test.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Fix style issues * Delete arena-tag.json * Update package-lock.json * Use `instanceof` for all arg type inference * Add Pledge Move sleep test * Fix linting * Fix linting Apparently GitHub has a limit on how many errors it will show * Pledges now only bypass redirection from abilities --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
This commit is contained in:
parent
75bd730c04
commit
0bd4d6c86b
|
@ -7,6 +7,7 @@
|
||||||
"": {
|
"": {
|
||||||
"name": "pokemon-rogue-battle",
|
"name": "pokemon-rogue-battle",
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@material/material-color-utilities": "^0.2.7",
|
"@material/material-color-utilities": "^0.2.7",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
|
|
|
@ -19,6 +19,7 @@ import { MoveEffectPhase } from "#app/phases/move-effect-phase";
|
||||||
import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase";
|
import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase";
|
||||||
import { ShowAbilityPhase } from "#app/phases/show-ability-phase";
|
import { ShowAbilityPhase } from "#app/phases/show-ability-phase";
|
||||||
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
|
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
|
||||||
|
import { CommonAnimPhase } from "#app/phases/common-anim-phase";
|
||||||
|
|
||||||
export enum ArenaTagSide {
|
export enum ArenaTagSide {
|
||||||
BOTH,
|
BOTH,
|
||||||
|
@ -1025,6 +1026,81 @@ class ImprisonTag extends ArenaTrapTag {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Arena Tag implementing the "sea of fire" effect from the combination
|
||||||
|
* of {@link https://bulbapedia.bulbagarden.net/wiki/Fire_Pledge_(move) | Fire Pledge}
|
||||||
|
* and {@link https://bulbapedia.bulbagarden.net/wiki/Grass_Pledge_(move) | Grass Pledge}.
|
||||||
|
* Damages all non-Fire-type Pokemon on the given side of the field at the end
|
||||||
|
* of each turn for 4 turns.
|
||||||
|
*/
|
||||||
|
class FireGrassPledgeTag extends ArenaTag {
|
||||||
|
constructor(sourceId: number, side: ArenaTagSide) {
|
||||||
|
super(ArenaTagType.FIRE_GRASS_PLEDGE, 4, Moves.FIRE_PLEDGE, sourceId, side);
|
||||||
|
}
|
||||||
|
|
||||||
|
override onAdd(arena: Arena): void {
|
||||||
|
// "A sea of fire enveloped your/the opposing team!"
|
||||||
|
arena.scene.queueMessage(i18next.t(`arenaTag:fireGrassPledgeOnAdd${this.side === ArenaTagSide.PLAYER ? "Player" : this.side === ArenaTagSide.ENEMY ? "Enemy" : ""}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
override lapse(arena: Arena): boolean {
|
||||||
|
const field: Pokemon[] = (this.side === ArenaTagSide.PLAYER)
|
||||||
|
? arena.scene.getPlayerField()
|
||||||
|
: arena.scene.getEnemyField();
|
||||||
|
|
||||||
|
field.filter(pokemon => !pokemon.isOfType(Type.FIRE)).forEach(pokemon => {
|
||||||
|
// "{pokemonNameWithAffix} was hurt by the sea of fire!"
|
||||||
|
pokemon.scene.queueMessage(i18next.t("arenaTag:fireGrassPledgeLapse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }));
|
||||||
|
// TODO: Replace this with a proper animation
|
||||||
|
pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, pokemon.getBattlerIndex(), pokemon.getBattlerIndex(), CommonAnim.MAGMA_STORM));
|
||||||
|
pokemon.damageAndUpdate(Utils.toDmgValue(pokemon.getMaxHp() / 8));
|
||||||
|
});
|
||||||
|
|
||||||
|
return super.lapse(arena);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Arena Tag implementing the "rainbow" effect from the combination
|
||||||
|
* of {@link https://bulbapedia.bulbagarden.net/wiki/Water_Pledge_(move) | Water Pledge}
|
||||||
|
* and {@link https://bulbapedia.bulbagarden.net/wiki/Fire_Pledge_(move) | Fire Pledge}.
|
||||||
|
* Doubles the secondary effect chance of moves from Pokemon on the
|
||||||
|
* given side of the field for 4 turns.
|
||||||
|
*/
|
||||||
|
class WaterFirePledgeTag extends ArenaTag {
|
||||||
|
constructor(sourceId: number, side: ArenaTagSide) {
|
||||||
|
super(ArenaTagType.WATER_FIRE_PLEDGE, 4, Moves.WATER_PLEDGE, sourceId, side);
|
||||||
|
}
|
||||||
|
|
||||||
|
override onAdd(arena: Arena): void {
|
||||||
|
// "A rainbow appeared in the sky on your/the opposing team's side!"
|
||||||
|
arena.scene.queueMessage(i18next.t(`arenaTag:waterFirePledgeOnAdd${this.side === ArenaTagSide.PLAYER ? "Player" : this.side === ArenaTagSide.ENEMY ? "Enemy" : ""}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
override apply(arena: Arena, args: any[]): boolean {
|
||||||
|
const moveChance = args[0] as Utils.NumberHolder;
|
||||||
|
moveChance.value *= 2;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Arena Tag implementing the "swamp" effect from the combination
|
||||||
|
* of {@link https://bulbapedia.bulbagarden.net/wiki/Grass_Pledge_(move) | Grass Pledge}
|
||||||
|
* and {@link https://bulbapedia.bulbagarden.net/wiki/Water_Pledge_(move) | Water Pledge}.
|
||||||
|
* Quarters the Speed of Pokemon on the given side of the field for 4 turns.
|
||||||
|
*/
|
||||||
|
class GrassWaterPledgeTag extends ArenaTag {
|
||||||
|
constructor(sourceId: number, side: ArenaTagSide) {
|
||||||
|
super(ArenaTagType.GRASS_WATER_PLEDGE, 4, Moves.GRASS_PLEDGE, sourceId, side);
|
||||||
|
}
|
||||||
|
|
||||||
|
override onAdd(arena: Arena): void {
|
||||||
|
// "A swamp enveloped your/the opposing team!"
|
||||||
|
arena.scene.queueMessage(i18next.t(`arenaTag:grassWaterPledgeOnAdd${this.side === ArenaTagSide.PLAYER ? "Player" : this.side === ArenaTagSide.ENEMY ? "Enemy" : ""}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function getArenaTag(tagType: ArenaTagType, turnCount: integer, sourceMove: Moves | undefined, sourceId: integer, targetIndex?: BattlerIndex, side: ArenaTagSide = ArenaTagSide.BOTH): ArenaTag | null {
|
export function getArenaTag(tagType: ArenaTagType, turnCount: integer, sourceMove: Moves | undefined, sourceId: integer, targetIndex?: BattlerIndex, side: ArenaTagSide = ArenaTagSide.BOTH): ArenaTag | null {
|
||||||
switch (tagType) {
|
switch (tagType) {
|
||||||
case ArenaTagType.MIST:
|
case ArenaTagType.MIST:
|
||||||
|
@ -1076,6 +1152,12 @@ export function getArenaTag(tagType: ArenaTagType, turnCount: integer, sourceMov
|
||||||
return new SafeguardTag(turnCount, sourceId, side);
|
return new SafeguardTag(turnCount, sourceId, side);
|
||||||
case ArenaTagType.IMPRISON:
|
case ArenaTagType.IMPRISON:
|
||||||
return new ImprisonTag(sourceId, side);
|
return new ImprisonTag(sourceId, side);
|
||||||
|
case ArenaTagType.FIRE_GRASS_PLEDGE:
|
||||||
|
return new FireGrassPledgeTag(sourceId, side);
|
||||||
|
case ArenaTagType.WATER_FIRE_PLEDGE:
|
||||||
|
return new WaterFirePledgeTag(sourceId, side);
|
||||||
|
case ArenaTagType.GRASS_WATER_PLEDGE:
|
||||||
|
return new GrassWaterPledgeTag(sourceId, side);
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
203
src/data/move.ts
203
src/data/move.ts
|
@ -1010,7 +1010,14 @@ export class MoveEffectAttr extends MoveAttr {
|
||||||
*/
|
*/
|
||||||
getMoveChance(user: Pokemon, target: Pokemon, move: Move, selfEffect?: Boolean, showAbility?: Boolean): integer {
|
getMoveChance(user: Pokemon, target: Pokemon, move: Move, selfEffect?: Boolean, showAbility?: Boolean): integer {
|
||||||
const moveChance = new Utils.NumberHolder(move.chance);
|
const moveChance = new Utils.NumberHolder(move.chance);
|
||||||
|
|
||||||
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, user, null, false, moveChance, move, target, selfEffect, showAbility);
|
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, user, null, false, moveChance, move, target, selfEffect, showAbility);
|
||||||
|
|
||||||
|
if (!move.hasAttr(FlinchAttr) || moveChance.value <= move.chance) {
|
||||||
|
const userSide = user.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
|
||||||
|
user.scene.arena.applyTagsForSide(ArenaTagType.WATER_FIRE_PLEDGE, userSide, moveChance);
|
||||||
|
}
|
||||||
|
|
||||||
if (!selfEffect) {
|
if (!selfEffect) {
|
||||||
applyPreDefendAbAttrs(IgnoreMoveEffectsAbAttr, target, user, null, null, false, moveChance);
|
applyPreDefendAbAttrs(IgnoreMoveEffectsAbAttr, target, user, null, null, false, moveChance);
|
||||||
}
|
}
|
||||||
|
@ -2687,6 +2694,62 @@ export class DelayedAttackAttr extends OverrideMoveEffectAttr {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attribute that cancels the associated move's effects when set to be combined with the user's ally's
|
||||||
|
* subsequent move this turn. Used for Grass Pledge, Water Pledge, and Fire Pledge.
|
||||||
|
* @extends OverrideMoveEffectAttr
|
||||||
|
*/
|
||||||
|
export class AwaitCombinedPledgeAttr extends OverrideMoveEffectAttr {
|
||||||
|
constructor() {
|
||||||
|
super(true);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* If the user's ally is set to use a different move with this attribute,
|
||||||
|
* defer this move's effects for a combined move on the ally's turn.
|
||||||
|
* @param user the {@linkcode Pokemon} using this move
|
||||||
|
* @param target n/a
|
||||||
|
* @param move the {@linkcode Move} being used
|
||||||
|
* @param args
|
||||||
|
* - [0] a {@linkcode Utils.BooleanHolder} indicating whether the move's base
|
||||||
|
* effects should be overridden this turn.
|
||||||
|
* @returns `true` if base move effects were overridden; `false` otherwise
|
||||||
|
*/
|
||||||
|
override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||||
|
if (user.turnData.combiningPledge) {
|
||||||
|
// "The two moves have become one!\nIt's a combined move!"
|
||||||
|
user.scene.queueMessage(i18next.t("moveTriggers:combiningPledge"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const overridden = args[0] as Utils.BooleanHolder;
|
||||||
|
|
||||||
|
const allyMovePhase = user.scene.findPhase<MovePhase>((phase) => phase instanceof MovePhase && phase.pokemon.isPlayer() === user.isPlayer());
|
||||||
|
if (allyMovePhase) {
|
||||||
|
const allyMove = allyMovePhase.move.getMove();
|
||||||
|
if (allyMove !== move && allyMove.hasAttr(AwaitCombinedPledgeAttr)) {
|
||||||
|
[ user, allyMovePhase.pokemon ].forEach((p) => p.turnData.combiningPledge = move.id);
|
||||||
|
|
||||||
|
// "{userPokemonName} is waiting for {allyPokemonName}'s move..."
|
||||||
|
user.scene.queueMessage(i18next.t("moveTriggers:awaitingPledge", {
|
||||||
|
userPokemonName: getPokemonNameWithAffix(user),
|
||||||
|
allyPokemonName: getPokemonNameWithAffix(allyMovePhase.pokemon)
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Move the ally's MovePhase (if needed) so that the ally moves next
|
||||||
|
const allyMovePhaseIndex = user.scene.phaseQueue.indexOf(allyMovePhase);
|
||||||
|
const firstMovePhaseIndex = user.scene.phaseQueue.findIndex((phase) => phase instanceof MovePhase);
|
||||||
|
if (allyMovePhaseIndex !== firstMovePhaseIndex) {
|
||||||
|
user.scene.prependToPhase(user.scene.phaseQueue.splice(allyMovePhaseIndex, 1)[0], MovePhase);
|
||||||
|
}
|
||||||
|
|
||||||
|
overridden.value = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attribute used for moves that change stat stages
|
* Attribute used for moves that change stat stages
|
||||||
* @param stats {@linkcode BattleStat} array of stats to be changed
|
* @param stats {@linkcode BattleStat} array of stats to be changed
|
||||||
|
@ -3762,6 +3825,45 @@ export class LastMoveDoublePowerAttr extends VariablePowerAttr {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes a Pledge move's power to 150 when combined with another unique Pledge
|
||||||
|
* move from an ally.
|
||||||
|
*/
|
||||||
|
export class CombinedPledgePowerAttr extends VariablePowerAttr {
|
||||||
|
override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||||
|
const power = args[0];
|
||||||
|
if (!(power instanceof Utils.NumberHolder)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const combinedPledgeMove = user.turnData.combiningPledge;
|
||||||
|
|
||||||
|
if (combinedPledgeMove && combinedPledgeMove !== move.id) {
|
||||||
|
power.value *= 150 / 80;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies STAB to the given Pledge move if the move is part of a combined attack.
|
||||||
|
*/
|
||||||
|
export class CombinedPledgeStabBoostAttr extends MoveAttr {
|
||||||
|
override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||||
|
const stabMultiplier = args[0];
|
||||||
|
if (!(stabMultiplier instanceof Utils.NumberHolder)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const combinedPledgeMove = user.turnData.combiningPledge;
|
||||||
|
|
||||||
|
if (combinedPledgeMove && combinedPledgeMove !== move.id) {
|
||||||
|
stabMultiplier.value = 1.5;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class VariableAtkAttr extends MoveAttr {
|
export class VariableAtkAttr extends MoveAttr {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
@ -4358,6 +4460,47 @@ export class MatchUserTypeAttr extends VariableMoveTypeAttr {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the type of a Pledge move based on the Pledge move combined with it.
|
||||||
|
* @extends VariableMoveTypeAttr
|
||||||
|
*/
|
||||||
|
export class CombinedPledgeTypeAttr extends VariableMoveTypeAttr {
|
||||||
|
override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||||
|
const moveType = args[0];
|
||||||
|
if (!(moveType instanceof Utils.NumberHolder)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const combinedPledgeMove = user.turnData.combiningPledge;
|
||||||
|
if (!combinedPledgeMove) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (move.id) {
|
||||||
|
case Moves.FIRE_PLEDGE:
|
||||||
|
if (combinedPledgeMove === Moves.WATER_PLEDGE) {
|
||||||
|
moveType.value = Type.WATER;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
case Moves.WATER_PLEDGE:
|
||||||
|
if (combinedPledgeMove === Moves.GRASS_PLEDGE) {
|
||||||
|
moveType.value = Type.GRASS;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
case Moves.GRASS_PLEDGE:
|
||||||
|
if (combinedPledgeMove === Moves.FIRE_PLEDGE) {
|
||||||
|
moveType.value = Type.FIRE;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class VariableMoveTypeMultiplierAttr extends MoveAttr {
|
export class VariableMoveTypeMultiplierAttr extends MoveAttr {
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||||
return false;
|
return false;
|
||||||
|
@ -4505,7 +4648,15 @@ export class TypelessAttr extends MoveAttr { }
|
||||||
* Attribute used for moves which ignore redirection effects, and always target their original target, i.e. Snipe Shot
|
* Attribute used for moves which ignore redirection effects, and always target their original target, i.e. Snipe Shot
|
||||||
* Bypasses Storm Drain, Follow Me, Ally Switch, and the like.
|
* Bypasses Storm Drain, Follow Me, Ally Switch, and the like.
|
||||||
*/
|
*/
|
||||||
export class BypassRedirectAttr extends MoveAttr { }
|
export class BypassRedirectAttr extends MoveAttr {
|
||||||
|
/** `true` if this move only bypasses redirection from Abilities */
|
||||||
|
public readonly abilitiesOnly: boolean;
|
||||||
|
|
||||||
|
constructor(abilitiesOnly: boolean = false) {
|
||||||
|
super();
|
||||||
|
this.abilitiesOnly = abilitiesOnly;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class FrenzyAttr extends MoveEffectAttr {
|
export class FrenzyAttr extends MoveEffectAttr {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -5196,6 +5347,32 @@ export class SwapArenaTagsAttr extends MoveEffectAttr {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attribute that adds a secondary effect to the field when two unique Pledge moves
|
||||||
|
* are combined. The effect added varies based on the two Pledge moves combined.
|
||||||
|
*/
|
||||||
|
export class AddPledgeEffectAttr extends AddArenaTagAttr {
|
||||||
|
private readonly requiredPledge: Moves;
|
||||||
|
|
||||||
|
constructor(tagType: ArenaTagType, requiredPledge: Moves, selfSideTarget: boolean = false) {
|
||||||
|
super(tagType, 4, false, selfSideTarget);
|
||||||
|
|
||||||
|
this.requiredPledge = requiredPledge;
|
||||||
|
}
|
||||||
|
|
||||||
|
override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||||
|
// TODO: add support for `HIT` effect triggering in AddArenaTagAttr to remove the need for this check
|
||||||
|
if (user.getLastXMoves(1)[0].result !== MoveResult.SUCCESS) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.turnData.combiningPledge === this.requiredPledge) {
|
||||||
|
return super.apply(user, target, move, args);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attribute used for Revival Blessing.
|
* Attribute used for Revival Blessing.
|
||||||
* @extends MoveEffectAttr
|
* @extends MoveEffectAttr
|
||||||
|
@ -8341,11 +8518,29 @@ export function initMoves() {
|
||||||
new AttackMove(Moves.INFERNO, Type.FIRE, MoveCategory.SPECIAL, 100, 50, 5, 100, 0, 5)
|
new AttackMove(Moves.INFERNO, Type.FIRE, MoveCategory.SPECIAL, 100, 50, 5, 100, 0, 5)
|
||||||
.attr(StatusEffectAttr, StatusEffect.BURN),
|
.attr(StatusEffectAttr, StatusEffect.BURN),
|
||||||
new AttackMove(Moves.WATER_PLEDGE, Type.WATER, MoveCategory.SPECIAL, 80, 100, 10, -1, 0, 5)
|
new AttackMove(Moves.WATER_PLEDGE, Type.WATER, MoveCategory.SPECIAL, 80, 100, 10, -1, 0, 5)
|
||||||
.partial(),
|
.attr(AwaitCombinedPledgeAttr)
|
||||||
|
.attr(CombinedPledgeTypeAttr)
|
||||||
|
.attr(CombinedPledgePowerAttr)
|
||||||
|
.attr(CombinedPledgeStabBoostAttr)
|
||||||
|
.attr(AddPledgeEffectAttr, ArenaTagType.WATER_FIRE_PLEDGE, Moves.FIRE_PLEDGE, true)
|
||||||
|
.attr(AddPledgeEffectAttr, ArenaTagType.GRASS_WATER_PLEDGE, Moves.GRASS_PLEDGE)
|
||||||
|
.attr(BypassRedirectAttr, true),
|
||||||
new AttackMove(Moves.FIRE_PLEDGE, Type.FIRE, MoveCategory.SPECIAL, 80, 100, 10, -1, 0, 5)
|
new AttackMove(Moves.FIRE_PLEDGE, Type.FIRE, MoveCategory.SPECIAL, 80, 100, 10, -1, 0, 5)
|
||||||
.partial(),
|
.attr(AwaitCombinedPledgeAttr)
|
||||||
|
.attr(CombinedPledgeTypeAttr)
|
||||||
|
.attr(CombinedPledgePowerAttr)
|
||||||
|
.attr(CombinedPledgeStabBoostAttr)
|
||||||
|
.attr(AddPledgeEffectAttr, ArenaTagType.FIRE_GRASS_PLEDGE, Moves.GRASS_PLEDGE)
|
||||||
|
.attr(AddPledgeEffectAttr, ArenaTagType.WATER_FIRE_PLEDGE, Moves.WATER_PLEDGE, true)
|
||||||
|
.attr(BypassRedirectAttr, true),
|
||||||
new AttackMove(Moves.GRASS_PLEDGE, Type.GRASS, MoveCategory.SPECIAL, 80, 100, 10, -1, 0, 5)
|
new AttackMove(Moves.GRASS_PLEDGE, Type.GRASS, MoveCategory.SPECIAL, 80, 100, 10, -1, 0, 5)
|
||||||
.partial(),
|
.attr(AwaitCombinedPledgeAttr)
|
||||||
|
.attr(CombinedPledgeTypeAttr)
|
||||||
|
.attr(CombinedPledgePowerAttr)
|
||||||
|
.attr(CombinedPledgeStabBoostAttr)
|
||||||
|
.attr(AddPledgeEffectAttr, ArenaTagType.GRASS_WATER_PLEDGE, Moves.WATER_PLEDGE)
|
||||||
|
.attr(AddPledgeEffectAttr, ArenaTagType.FIRE_GRASS_PLEDGE, Moves.FIRE_PLEDGE)
|
||||||
|
.attr(BypassRedirectAttr, true),
|
||||||
new AttackMove(Moves.VOLT_SWITCH, Type.ELECTRIC, MoveCategory.SPECIAL, 70, 100, 20, -1, 0, 5)
|
new AttackMove(Moves.VOLT_SWITCH, Type.ELECTRIC, MoveCategory.SPECIAL, 70, 100, 20, -1, 0, 5)
|
||||||
.attr(ForceSwitchOutAttr, true),
|
.attr(ForceSwitchOutAttr, true),
|
||||||
new AttackMove(Moves.STRUGGLE_BUG, Type.BUG, MoveCategory.SPECIAL, 50, 100, 20, 100, 0, 5)
|
new AttackMove(Moves.STRUGGLE_BUG, Type.BUG, MoveCategory.SPECIAL, 50, 100, 20, 100, 0, 5)
|
||||||
|
|
|
@ -25,4 +25,7 @@ export enum ArenaTagType {
|
||||||
NO_CRIT = "NO_CRIT",
|
NO_CRIT = "NO_CRIT",
|
||||||
IMPRISON = "IMPRISON",
|
IMPRISON = "IMPRISON",
|
||||||
PLASMA_FISTS = "PLASMA_FISTS",
|
PLASMA_FISTS = "PLASMA_FISTS",
|
||||||
|
FIRE_GRASS_PLEDGE = "FIRE_GRASS_PLEDGE",
|
||||||
|
WATER_FIRE_PLEDGE = "WATER_FIRE_PLEDGE",
|
||||||
|
GRASS_WATER_PLEDGE = "GRASS_WATER_PLEDGE",
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import BattleScene, { AnySound } from "#app/battle-scene";
|
||||||
import { Variant, VariantSet, variantColorCache } from "#app/data/variant";
|
import { Variant, VariantSet, variantColorCache } from "#app/data/variant";
|
||||||
import { variantData } from "#app/data/variant";
|
import { variantData } from "#app/data/variant";
|
||||||
import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from "#app/ui/battle-info";
|
import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from "#app/ui/battle-info";
|
||||||
import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, VariableMoveTypeAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatStagesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatStageChangeAttr, RechargeAttr, ChargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, OneHitKOAccuracyAttr, RespectAttackTypeImmunityAttr, MoveTarget } from "#app/data/move";
|
import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, VariableMoveTypeAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatStagesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatStageChangeAttr, RechargeAttr, ChargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, OneHitKOAccuracyAttr, RespectAttackTypeImmunityAttr, MoveTarget, CombinedPledgeStabBoostAttr } from "#app/data/move";
|
||||||
import { default as PokemonSpecies, PokemonSpeciesForm, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm } from "#app/data/pokemon-species";
|
import { default as PokemonSpecies, PokemonSpeciesForm, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm } from "#app/data/pokemon-species";
|
||||||
import { getStarterValueFriendshipCap, speciesStarterCosts } from "#app/data/balance/starters";
|
import { getStarterValueFriendshipCap, speciesStarterCosts } from "#app/data/balance/starters";
|
||||||
import { starterPassiveAbilities } from "#app/data/balance/passives";
|
import { starterPassiveAbilities } from "#app/data/balance/passives";
|
||||||
|
@ -924,11 +924,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case Stat.SPD:
|
case Stat.SPD:
|
||||||
// Check both the player and enemy to see if Tailwind should be multiplying the speed of the Pokemon
|
const side = this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
|
||||||
if ((this.isPlayer() && this.scene.arena.getTagOnSide(ArenaTagType.TAILWIND, ArenaTagSide.PLAYER))
|
if (this.scene.arena.getTagOnSide(ArenaTagType.TAILWIND, side)) {
|
||||||
|| (!this.isPlayer() && this.scene.arena.getTagOnSide(ArenaTagType.TAILWIND, ArenaTagSide.ENEMY))) {
|
|
||||||
ret *= 2;
|
ret *= 2;
|
||||||
}
|
}
|
||||||
|
if (this.scene.arena.getTagOnSide(ArenaTagType.GRASS_WATER_PLEDGE, side)) {
|
||||||
|
ret >>= 2;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.getTag(BattlerTagType.SLOW_START)) {
|
if (this.getTag(BattlerTagType.SLOW_START)) {
|
||||||
ret >>= 1;
|
ret >>= 1;
|
||||||
|
@ -2562,6 +2564,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||||
if (matchesSourceType) {
|
if (matchesSourceType) {
|
||||||
stabMultiplier.value += 0.5;
|
stabMultiplier.value += 0.5;
|
||||||
}
|
}
|
||||||
|
applyMoveAttrs(CombinedPledgeStabBoostAttr, source, this, move, stabMultiplier);
|
||||||
if (sourceTeraType !== Type.UNKNOWN && sourceTeraType === moveType) {
|
if (sourceTeraType !== Type.UNKNOWN && sourceTeraType === moveType) {
|
||||||
stabMultiplier.value += 0.5;
|
stabMultiplier.value += 0.5;
|
||||||
}
|
}
|
||||||
|
@ -5041,6 +5044,7 @@ export class PokemonTurnData {
|
||||||
public statStagesIncreased: boolean = false;
|
public statStagesIncreased: boolean = false;
|
||||||
public statStagesDecreased: boolean = false;
|
public statStagesDecreased: boolean = false;
|
||||||
public moveEffectiveness: TypeDamageMultiplier | null = null;
|
public moveEffectiveness: TypeDamageMultiplier | null = null;
|
||||||
|
public combiningPledge?: Moves;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AiType {
|
export enum AiType {
|
||||||
|
|
|
@ -331,22 +331,30 @@ export class MovePhase extends BattlePhase {
|
||||||
// check move redirection abilities of every pokemon *except* the user.
|
// check move redirection abilities of every pokemon *except* the user.
|
||||||
this.scene.getField(true).filter(p => p !== this.pokemon).forEach(p => applyAbAttrs(RedirectMoveAbAttr, p, null, false, this.move.moveId, redirectTarget));
|
this.scene.getField(true).filter(p => p !== this.pokemon).forEach(p => applyAbAttrs(RedirectMoveAbAttr, p, null, false, this.move.moveId, redirectTarget));
|
||||||
|
|
||||||
|
/** `true` if an Ability is responsible for redirecting the move to another target; `false` otherwise */
|
||||||
|
let redirectedByAbility = (currentTarget !== redirectTarget.value);
|
||||||
|
|
||||||
// check for center-of-attention tags (note that this will override redirect abilities)
|
// check for center-of-attention tags (note that this will override redirect abilities)
|
||||||
this.pokemon.getOpponents().forEach(p => {
|
this.pokemon.getOpponents().forEach(p => {
|
||||||
const redirectTag = p.getTag(CenterOfAttentionTag) as CenterOfAttentionTag;
|
const redirectTag = p.getTag(CenterOfAttentionTag);
|
||||||
|
|
||||||
// TODO: don't hardcode this interaction.
|
// TODO: don't hardcode this interaction.
|
||||||
// Handle interaction between the rage powder center-of-attention tag and moves used by grass types/overcoat-havers (which are immune to RP's redirect)
|
// Handle interaction between the rage powder center-of-attention tag and moves used by grass types/overcoat-havers (which are immune to RP's redirect)
|
||||||
if (redirectTag && (!redirectTag.powder || (!this.pokemon.isOfType(Type.GRASS) && !this.pokemon.hasAbility(Abilities.OVERCOAT)))) {
|
if (redirectTag && (!redirectTag.powder || (!this.pokemon.isOfType(Type.GRASS) && !this.pokemon.hasAbility(Abilities.OVERCOAT)))) {
|
||||||
redirectTarget.value = p.getBattlerIndex();
|
redirectTarget.value = p.getBattlerIndex();
|
||||||
|
redirectedByAbility = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (currentTarget !== redirectTarget.value) {
|
if (currentTarget !== redirectTarget.value) {
|
||||||
if (this.move.getMove().hasAttr(BypassRedirectAttr)) {
|
const bypassRedirectAttrs = this.move.getMove().getAttrs(BypassRedirectAttr);
|
||||||
redirectTarget.value = currentTarget;
|
bypassRedirectAttrs.forEach((attr) => {
|
||||||
|
if (!attr.abilitiesOnly || redirectedByAbility) {
|
||||||
|
redirectTarget.value = currentTarget;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
} else if (this.pokemon.hasAbilityWithAttr(BlockRedirectAbAttr)) {
|
if (this.pokemon.hasAbilityWithAttr(BlockRedirectAbAttr)) {
|
||||||
redirectTarget.value = currentTarget;
|
redirectTarget.value = currentTarget;
|
||||||
this.scene.unshiftPhase(new ShowAbilityPhase(this.scene, this.pokemon.getBattlerIndex(), this.pokemon.getPassiveAbility().hasAttr(BlockRedirectAbAttr)));
|
this.scene.unshiftPhase(new ShowAbilityPhase(this.scene, this.pokemon.getBattlerIndex(), this.pokemon.getPassiveAbility().hasAttr(BlockRedirectAbAttr)));
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,337 @@
|
||||||
|
import { BattlerIndex } from "#app/battle";
|
||||||
|
import { allAbilities } from "#app/data/ability";
|
||||||
|
import { ArenaTagSide } from "#app/data/arena-tag";
|
||||||
|
import { allMoves, FlinchAttr } from "#app/data/move";
|
||||||
|
import { Type } from "#app/data/type";
|
||||||
|
import { ArenaTagType } from "#enums/arena-tag-type";
|
||||||
|
import { Stat } from "#enums/stat";
|
||||||
|
import { toDmgValue } from "#app/utils";
|
||||||
|
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 - Pledge Moves", () => {
|
||||||
|
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("double")
|
||||||
|
.startingLevel(100)
|
||||||
|
.moveset([ Moves.FIRE_PLEDGE, Moves.GRASS_PLEDGE, Moves.WATER_PLEDGE, Moves.SPLASH ])
|
||||||
|
.enemySpecies(Species.SNORLAX)
|
||||||
|
.enemyLevel(100)
|
||||||
|
.enemyAbility(Abilities.BALL_FETCH)
|
||||||
|
.enemyMoveset(Moves.SPLASH);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(
|
||||||
|
"Fire Pledge - should be an 80-power Fire-type attack outside of combination",
|
||||||
|
async () => {
|
||||||
|
await game.classicMode.startBattle([ Species.BLASTOISE, Species.CHARIZARD ]);
|
||||||
|
|
||||||
|
const firePledge = allMoves[Moves.FIRE_PLEDGE];
|
||||||
|
vi.spyOn(firePledge, "calculateBattlePower");
|
||||||
|
|
||||||
|
const playerPokemon = game.scene.getPlayerField();
|
||||||
|
vi.spyOn(playerPokemon[0], "getMoveType");
|
||||||
|
|
||||||
|
game.move.select(Moves.FIRE_PLEDGE, 0, BattlerIndex.ENEMY);
|
||||||
|
game.move.select(Moves.SPLASH, 1);
|
||||||
|
|
||||||
|
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2 ]);
|
||||||
|
await game.phaseInterceptor.to("MoveEndPhase", false);
|
||||||
|
|
||||||
|
expect(firePledge.calculateBattlePower).toHaveLastReturnedWith(80);
|
||||||
|
expect(playerPokemon[0].getMoveType).toHaveLastReturnedWith(Type.FIRE);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
it(
|
||||||
|
"Fire Pledge - should not combine with an ally using Fire Pledge",
|
||||||
|
async () => {
|
||||||
|
await game.classicMode.startBattle([ Species.BLASTOISE, Species.CHARIZARD ]);
|
||||||
|
|
||||||
|
const firePledge = allMoves[Moves.FIRE_PLEDGE];
|
||||||
|
vi.spyOn(firePledge, "calculateBattlePower");
|
||||||
|
|
||||||
|
const playerPokemon = game.scene.getPlayerField();
|
||||||
|
playerPokemon.forEach(p => vi.spyOn(p, "getMoveType"));
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyField();
|
||||||
|
|
||||||
|
game.move.select(Moves.FIRE_PLEDGE, 0, BattlerIndex.ENEMY);
|
||||||
|
game.move.select(Moves.FIRE_PLEDGE, 0, BattlerIndex.ENEMY_2);
|
||||||
|
|
||||||
|
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2 ]);
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to("MoveEndPhase");
|
||||||
|
expect(firePledge.calculateBattlePower).toHaveLastReturnedWith(80);
|
||||||
|
expect(playerPokemon[0].getMoveType).toHaveLastReturnedWith(Type.FIRE);
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to("BerryPhase", false);
|
||||||
|
expect(firePledge.calculateBattlePower).toHaveLastReturnedWith(80);
|
||||||
|
expect(playerPokemon[1].getMoveType).toHaveLastReturnedWith(Type.FIRE);
|
||||||
|
|
||||||
|
enemyPokemon.forEach(p => expect(p.hp).toBeLessThan(p.getMaxHp()));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
it(
|
||||||
|
"Fire Pledge - should not combine with an enemy's Pledge move",
|
||||||
|
async () => {
|
||||||
|
game.override
|
||||||
|
.battleType("single")
|
||||||
|
.enemyMoveset(Moves.GRASS_PLEDGE);
|
||||||
|
|
||||||
|
await game.classicMode.startBattle([ Species.CHARIZARD ]);
|
||||||
|
|
||||||
|
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||||
|
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||||
|
|
||||||
|
game.move.select(Moves.FIRE_PLEDGE);
|
||||||
|
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
// Neither Pokemon should defer their move's effects as they would
|
||||||
|
// if they combined moves, so both should be damaged.
|
||||||
|
expect(playerPokemon.hp).toBeLessThan(playerPokemon.getMaxHp());
|
||||||
|
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
|
||||||
|
expect(game.scene.arena.getTag(ArenaTagType.FIRE_GRASS_PLEDGE)).toBeUndefined();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
it(
|
||||||
|
"Grass Pledge - should combine with Fire Pledge to form a 150-power Fire-type attack that creates a 'sea of fire'",
|
||||||
|
async () => {
|
||||||
|
await game.classicMode.startBattle([ Species.CHARIZARD, Species.BLASTOISE ]);
|
||||||
|
|
||||||
|
const grassPledge = allMoves[Moves.GRASS_PLEDGE];
|
||||||
|
vi.spyOn(grassPledge, "calculateBattlePower");
|
||||||
|
|
||||||
|
const playerPokemon = game.scene.getPlayerField();
|
||||||
|
const enemyPokemon = game.scene.getEnemyField();
|
||||||
|
|
||||||
|
vi.spyOn(playerPokemon[1], "getMoveType");
|
||||||
|
const baseDmgMock = vi.spyOn(enemyPokemon[0], "getBaseDamage");
|
||||||
|
|
||||||
|
game.move.select(Moves.FIRE_PLEDGE, 0, BattlerIndex.ENEMY_2);
|
||||||
|
game.move.select(Moves.GRASS_PLEDGE, 1, BattlerIndex.ENEMY);
|
||||||
|
|
||||||
|
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2 ]);
|
||||||
|
// advance to the end of PLAYER_2's move this turn
|
||||||
|
for (let i = 0; i < 2; i++) {
|
||||||
|
await game.phaseInterceptor.to("MoveEndPhase");
|
||||||
|
}
|
||||||
|
expect(playerPokemon[1].getMoveType).toHaveLastReturnedWith(Type.FIRE);
|
||||||
|
expect(grassPledge.calculateBattlePower).toHaveLastReturnedWith(150);
|
||||||
|
|
||||||
|
const baseDmg = baseDmgMock.mock.results[baseDmgMock.mock.results.length - 1].value;
|
||||||
|
expect(enemyPokemon[0].getMaxHp() - enemyPokemon[0].hp).toBe(toDmgValue(baseDmg * 1.5));
|
||||||
|
expect(enemyPokemon[1].hp).toBe(enemyPokemon[1].getMaxHp()); // PLAYER should not have attacked
|
||||||
|
expect(game.scene.arena.getTagOnSide(ArenaTagType.FIRE_GRASS_PLEDGE, ArenaTagSide.ENEMY)).toBeDefined();
|
||||||
|
|
||||||
|
const enemyStartingHp = enemyPokemon.map(p => p.hp);
|
||||||
|
await game.toNextTurn();
|
||||||
|
enemyPokemon.forEach((p, i) => expect(enemyStartingHp[i] - p.hp).toBe(toDmgValue(p.getMaxHp() / 8)));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
it(
|
||||||
|
"Fire Pledge - should combine with Water Pledge to form a 150-power Water-type attack that creates a 'rainbow'",
|
||||||
|
async () => {
|
||||||
|
game.override.moveset([ Moves.FIRE_PLEDGE, Moves.WATER_PLEDGE, Moves.FIERY_DANCE, Moves.SPLASH ]);
|
||||||
|
|
||||||
|
await game.classicMode.startBattle([ Species.BLASTOISE, Species.VENUSAUR ]);
|
||||||
|
|
||||||
|
const firePledge = allMoves[Moves.FIRE_PLEDGE];
|
||||||
|
vi.spyOn(firePledge, "calculateBattlePower");
|
||||||
|
|
||||||
|
const playerPokemon = game.scene.getPlayerField();
|
||||||
|
const enemyPokemon = game.scene.getEnemyField();
|
||||||
|
|
||||||
|
vi.spyOn(playerPokemon[1], "getMoveType");
|
||||||
|
|
||||||
|
game.move.select(Moves.WATER_PLEDGE, 0, BattlerIndex.ENEMY_2);
|
||||||
|
game.move.select(Moves.FIRE_PLEDGE, 1, BattlerIndex.ENEMY);
|
||||||
|
|
||||||
|
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2 ]);
|
||||||
|
// advance to the end of PLAYER_2's move this turn
|
||||||
|
for (let i = 0; i < 2; i++) {
|
||||||
|
await game.phaseInterceptor.to("MoveEndPhase");
|
||||||
|
}
|
||||||
|
expect(playerPokemon[1].getMoveType).toHaveLastReturnedWith(Type.WATER);
|
||||||
|
expect(firePledge.calculateBattlePower).toHaveLastReturnedWith(150);
|
||||||
|
expect(enemyPokemon[1].hp).toBe(enemyPokemon[1].getMaxHp()); // PLAYER should not have attacked
|
||||||
|
expect(game.scene.arena.getTagOnSide(ArenaTagType.WATER_FIRE_PLEDGE, ArenaTagSide.PLAYER)).toBeDefined();
|
||||||
|
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
game.move.select(Moves.FIERY_DANCE, 0, BattlerIndex.ENEMY_2);
|
||||||
|
game.move.select(Moves.SPLASH, 1);
|
||||||
|
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2 ]);
|
||||||
|
await game.phaseInterceptor.to("MoveEndPhase");
|
||||||
|
|
||||||
|
// Rainbow effect should increase Fiery Dance's chance of raising Sp. Atk to 100%
|
||||||
|
expect(playerPokemon[0].getStatStage(Stat.SPATK)).toBe(1);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
it(
|
||||||
|
"Water Pledge - should combine with Grass Pledge to form a 150-power Grass-type attack that creates a 'swamp'",
|
||||||
|
async () => {
|
||||||
|
await game.classicMode.startBattle([ Species.BLASTOISE, Species.CHARIZARD ]);
|
||||||
|
|
||||||
|
const waterPledge = allMoves[Moves.WATER_PLEDGE];
|
||||||
|
vi.spyOn(waterPledge, "calculateBattlePower");
|
||||||
|
|
||||||
|
const playerPokemon = game.scene.getPlayerField();
|
||||||
|
const enemyPokemon = game.scene.getEnemyField();
|
||||||
|
const enemyStartingSpd = enemyPokemon.map(p => p.getEffectiveStat(Stat.SPD));
|
||||||
|
|
||||||
|
vi.spyOn(playerPokemon[1], "getMoveType");
|
||||||
|
|
||||||
|
game.move.select(Moves.GRASS_PLEDGE, 0, BattlerIndex.ENEMY_2);
|
||||||
|
game.move.select(Moves.WATER_PLEDGE, 1, BattlerIndex.ENEMY);
|
||||||
|
|
||||||
|
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2 ]);
|
||||||
|
// advance to the end of PLAYER_2's move this turn
|
||||||
|
for (let i = 0; i < 2; i++) {
|
||||||
|
await game.phaseInterceptor.to("MoveEndPhase");
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(playerPokemon[1].getMoveType).toHaveLastReturnedWith(Type.GRASS);
|
||||||
|
expect(waterPledge.calculateBattlePower).toHaveLastReturnedWith(150);
|
||||||
|
expect(enemyPokemon[1].hp).toBe(enemyPokemon[1].getMaxHp());
|
||||||
|
|
||||||
|
expect(game.scene.arena.getTagOnSide(ArenaTagType.GRASS_WATER_PLEDGE, ArenaTagSide.ENEMY)).toBeDefined();
|
||||||
|
enemyPokemon.forEach((p, i) => expect(p.getEffectiveStat(Stat.SPD)).toBe(Math.floor(enemyStartingSpd[i] / 4)));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
it(
|
||||||
|
"Pledge Moves - should alter turn order when used in combination",
|
||||||
|
async () => {
|
||||||
|
await game.classicMode.startBattle([ Species.CHARIZARD, Species.BLASTOISE ]);
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyField();
|
||||||
|
|
||||||
|
game.move.select(Moves.WATER_PLEDGE, 0, BattlerIndex.ENEMY);
|
||||||
|
game.move.select(Moves.FIRE_PLEDGE, 1, BattlerIndex.ENEMY_2);
|
||||||
|
|
||||||
|
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER_2 ]);
|
||||||
|
// PLAYER_2 should act with a combined move immediately after PLAYER as the second move in the turn
|
||||||
|
for (let i = 0; i < 2; i++) {
|
||||||
|
await game.phaseInterceptor.to("MoveEndPhase");
|
||||||
|
}
|
||||||
|
expect(enemyPokemon[0].hp).toBe(enemyPokemon[0].getMaxHp());
|
||||||
|
expect(enemyPokemon[1].hp).toBeLessThan(enemyPokemon[1].getMaxHp());
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
it(
|
||||||
|
"Pledge Moves - 'rainbow' effect should not stack with Serene Grace when applied to flinching moves",
|
||||||
|
async () => {
|
||||||
|
game.override
|
||||||
|
.ability(Abilities.SERENE_GRACE)
|
||||||
|
.moveset([ Moves.FIRE_PLEDGE, Moves.WATER_PLEDGE, Moves.IRON_HEAD, Moves.SPLASH ]);
|
||||||
|
|
||||||
|
await game.classicMode.startBattle([ Species.BLASTOISE, Species.CHARIZARD ]);
|
||||||
|
|
||||||
|
const ironHeadFlinchAttr = allMoves[Moves.IRON_HEAD].getAttrs(FlinchAttr)[0];
|
||||||
|
vi.spyOn(ironHeadFlinchAttr, "getMoveChance");
|
||||||
|
|
||||||
|
game.move.select(Moves.WATER_PLEDGE, 0, BattlerIndex.ENEMY);
|
||||||
|
game.move.select(Moves.FIRE_PLEDGE, 1, BattlerIndex.ENEMY_2);
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to("TurnEndPhase");
|
||||||
|
|
||||||
|
expect(game.scene.arena.getTagOnSide(ArenaTagType.WATER_FIRE_PLEDGE, ArenaTagSide.PLAYER)).toBeDefined();
|
||||||
|
|
||||||
|
game.move.select(Moves.IRON_HEAD, 0, BattlerIndex.ENEMY);
|
||||||
|
game.move.select(Moves.SPLASH, 1);
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to("BerryPhase", false);
|
||||||
|
|
||||||
|
expect(ironHeadFlinchAttr.getMoveChance).toHaveLastReturnedWith(60);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
it(
|
||||||
|
"Pledge Moves - should have no effect when the second ally's move is cancelled",
|
||||||
|
async () => {
|
||||||
|
game.override
|
||||||
|
.enemyMoveset([ Moves.SPLASH, Moves.SPORE ]);
|
||||||
|
|
||||||
|
await game.classicMode.startBattle([ Species.BLASTOISE, Species.CHARIZARD ]);
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyField();
|
||||||
|
|
||||||
|
game.move.select(Moves.FIRE_PLEDGE, 0, BattlerIndex.ENEMY);
|
||||||
|
game.move.select(Moves.GRASS_PLEDGE, 1, BattlerIndex.ENEMY_2);
|
||||||
|
|
||||||
|
await game.forceEnemyMove(Moves.SPORE, BattlerIndex.PLAYER_2);
|
||||||
|
await game.forceEnemyMove(Moves.SPLASH);
|
||||||
|
|
||||||
|
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY_2 ]);
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to("BerryPhase", false);
|
||||||
|
|
||||||
|
enemyPokemon.forEach((p) => expect(p.hp).toBe(p.getMaxHp()));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
it(
|
||||||
|
"Pledge Moves - should ignore redirection from another Pokemon's Storm Drain",
|
||||||
|
async () => {
|
||||||
|
await game.classicMode.startBattle([ Species.BLASTOISE, Species.CHARIZARD ]);
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyField();
|
||||||
|
vi.spyOn(enemyPokemon[1], "getAbility").mockReturnValue(allAbilities[Abilities.STORM_DRAIN]);
|
||||||
|
|
||||||
|
game.move.select(Moves.WATER_PLEDGE, 0, BattlerIndex.ENEMY);
|
||||||
|
game.move.select(Moves.SPLASH, 1);
|
||||||
|
|
||||||
|
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2 ]);
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to("MoveEndPhase", false);
|
||||||
|
|
||||||
|
expect(enemyPokemon[0].hp).toBeLessThan(enemyPokemon[0].getMaxHp());
|
||||||
|
expect(enemyPokemon[1].getStatStage(Stat.SPATK)).toBe(0);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
it(
|
||||||
|
"Pledge Moves - should not ignore redirection from another Pokemon's Follow Me",
|
||||||
|
async () => {
|
||||||
|
game.override.enemyMoveset([ Moves.FOLLOW_ME, Moves.SPLASH ]);
|
||||||
|
await game.classicMode.startBattle([ Species.BLASTOISE, Species.CHARIZARD ]);
|
||||||
|
|
||||||
|
game.move.select(Moves.WATER_PLEDGE, 0, BattlerIndex.ENEMY);
|
||||||
|
game.move.select(Moves.SPLASH, 1);
|
||||||
|
|
||||||
|
await game.forceEnemyMove(Moves.SPLASH);
|
||||||
|
await game.forceEnemyMove(Moves.FOLLOW_ME);
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to("BerryPhase", false);
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyField();
|
||||||
|
expect(enemyPokemon[0].hp).toBe(enemyPokemon[0].getMaxHp());
|
||||||
|
expect(enemyPokemon[1].hp).toBeLessThan(enemyPokemon[1].getMaxHp());
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
Loading…
Reference in New Issue