Implement Fire/Grass Pledge combo

This commit is contained in:
innerthunder 2024-09-29 17:42:33 -07:00
parent 000b8d9b2b
commit ad4f07f154
6 changed files with 214 additions and 6 deletions

View File

@ -19,6 +19,7 @@ import { MoveEffectPhase } from "#app/phases/move-effect-phase";
import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase";
import { ShowAbilityPhase } from "#app/phases/show-ability-phase";
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import { CommonAnimPhase } from "#app/phases/common-anim-phase";
export enum ArenaTagSide {
BOTH,
@ -992,6 +993,40 @@ 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);
}
}
export function getArenaTag(tagType: ArenaTagType, turnCount: integer, sourceMove: Moves | undefined, sourceId: integer, targetIndex?: BattlerIndex, side: ArenaTagSide = ArenaTagSide.BOTH): ArenaTag | null {
switch (tagType) {
case ArenaTagType.MIST:
@ -1041,6 +1076,8 @@ export function getArenaTag(tagType: ArenaTagType, turnCount: integer, sourceMov
return new SafeguardTag(turnCount, sourceId, side);
case ArenaTagType.IMPRISON:
return new ImprisonTag(sourceId, side);
case ArenaTagType.FIRE_GRASS_PLEDGE:
return new FireGrassPledgeTag(sourceId, side);
default:
return null;
}

View File

@ -2685,6 +2685,55 @@ 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, user.getAlly()].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(user.getAlly())
}));
overridden.value = true;
return true;
}
}
return false;
}
}
export class StatStageChangeAttr extends MoveEffectAttr {
public stats: BattleStat[];
public stages: integer;
@ -3738,6 +3787,39 @@ 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] as Utils.NumberHolder;
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] as Utils.NumberHolder;
const combinedPledgeMove = user.turnData.combiningPledge;
if (combinedPledgeMove && combinedPledgeMove !== move.id) {
stabMultiplier.value = 1.5;
return true;
}
return false;
}
}
export class VariableAtkAttr extends MoveAttr {
constructor() {
super();
@ -4303,6 +4385,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 {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
return false;
@ -5139,6 +5262,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.
* @extends MoveEffectAttr
@ -8275,11 +8424,25 @@ export function initMoves() {
new AttackMove(Moves.INFERNO, Type.FIRE, MoveCategory.SPECIAL, 100, 50, 5, 100, 0, 5)
.attr(StatusEffectAttr, StatusEffect.BURN),
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(BypassRedirectAttr), // technically incorrect, should only bypass Storm Drain/Lightning Rod
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, false)
.attr(BypassRedirectAttr), // technically incorrect, should only bypass Storm Drain/Lightning Rod
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.FIRE_GRASS_PLEDGE, Moves.FIRE_PLEDGE, false)
.attr(BypassRedirectAttr), // technically incorrect, should only bypass Storm Drain/Lightning Rod
new AttackMove(Moves.VOLT_SWITCH, Type.ELECTRIC, MoveCategory.SPECIAL, 70, 100, 20, -1, 0, 5)
.attr(ForceSwitchOutAttr, true),
new AttackMove(Moves.STRUGGLE_BUG, Type.BUG, MoveCategory.SPECIAL, 50, 100, 20, 100, 0, 5)

View File

@ -25,4 +25,5 @@ export enum ArenaTagType {
SAFEGUARD = "SAFEGUARD",
NO_CRIT = "NO_CRIT",
IMPRISON = "IMPRISON",
FIRE_GRASS_PLEDGE = "FIRE_GRASS_PLEDGE",
}

View File

@ -3,7 +3,7 @@ import BattleScene, { AnySound } from "../battle-scene";
import { Variant, VariantSet, variantColorCache } from "#app/data/variant";
import { variantData } from "#app/data/variant";
import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from "../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 "../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 "../data/move";
import { default as PokemonSpecies, PokemonSpeciesForm, SpeciesFormKey, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm, getStarterValueFriendshipCap, speciesStarters, starterPassiveAbilities } from "../data/pokemon-species";
import { Constructor, isNullOrUndefined, randSeedInt } from "#app/utils";
import * as Utils from "../utils";
@ -2540,6 +2540,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (matchesSourceType) {
stabMultiplier.value += 0.5;
}
applyMoveAttrs(CombinedPledgeStabBoostAttr, source, this, move, stabMultiplier);
if (sourceTeraType !== Type.UNKNOWN && sourceTeraType === moveType) {
stabMultiplier.value += 0.5;
}
@ -5011,6 +5012,7 @@ export class PokemonTurnData {
public order: number;
public statStagesIncreased: boolean = false;
public statStagesDecreased: boolean = false;
public combiningPledge?: Moves;
}
export enum AiType {

View File

@ -53,5 +53,8 @@
"safeguardOnAddEnemy": "The opposing team cloaked itself in a mystical veil!",
"safeguardOnRemove": "The field is no longer protected by Safeguard!",
"safeguardOnRemovePlayer": "Your team is no longer protected by Safeguard!",
"safeguardOnRemoveEnemy": "The opposing team is no longer protected by Safeguard!"
"safeguardOnRemoveEnemy": "The opposing team is no longer protected by Safeguard!",
"fireGrassPledgeOnAddPlayer": "A sea of fire enveloped your team!",
"fireGrassPledgeOnAddEnemy": "A sea of fire enveloped the opposing team!",
"fireGrassPledgeLapse": "{{pokemonNameWithAffix}} was hurt by the sea of fire!"
}

View File

@ -71,5 +71,7 @@
"safeguard": "{{targetName}} is protected by Safeguard!",
"substituteOnOverlap": "{{pokemonName}} already\nhas a substitute!",
"substituteNotEnoughHp": "But it does not have enough HP\nleft to make a substitute!",
"afterYou": "{{pokemonName}} took the kind offer!"
"afterYou": "{{pokemonName}} took the kind offer!",
"combiningPledge": "The two moves have become one!\nIt's a combined move!",
"awaitingPledge": "{{userPokemonName}} is waiting for {{allyPokemonName}}'s move..."
}