From ad4f07f1543e27885b9e6cc7187ee764cadbe1cc Mon Sep 17 00:00:00 2001 From: innerthunder Date: Sun, 29 Sep 2024 17:42:33 -0700 Subject: [PATCH] Implement Fire/Grass Pledge combo --- src/data/arena-tag.ts | 37 +++++++ src/data/move.ts | 169 ++++++++++++++++++++++++++++++- src/enums/arena-tag-type.ts | 1 + src/field/pokemon.ts | 4 +- src/locales/en/arena-tag.json | 5 +- src/locales/en/move-trigger.json | 4 +- 6 files changed, 214 insertions(+), 6 deletions(-) diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index d783ea51056..7e895dc483f 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -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; } diff --git a/src/data/move.ts b/src/data/move.ts index 74ecead73fa..b04b5757407 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -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(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) diff --git a/src/enums/arena-tag-type.ts b/src/enums/arena-tag-type.ts index c6f911cb493..e4204134092 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", + FIRE_GRASS_PLEDGE = "FIRE_GRASS_PLEDGE", } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 07525e92157..9085d68e077 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -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 { diff --git a/src/locales/en/arena-tag.json b/src/locales/en/arena-tag.json index d8fed386b24..f812ab52cbc 100644 --- a/src/locales/en/arena-tag.json +++ b/src/locales/en/arena-tag.json @@ -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!" } \ No newline at end of file diff --git a/src/locales/en/move-trigger.json b/src/locales/en/move-trigger.json index 93d25e506ba..0970be8d2a6 100644 --- a/src/locales/en/move-trigger.json +++ b/src/locales/en/move-trigger.json @@ -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..." } \ No newline at end of file