From a48ba9864dfa7ed8d33fc3de3c439d4926c5ec06 Mon Sep 17 00:00:00 2001 From: Jakub Hanko <60473007+JakubHanko@users.noreply.github.com> Date: Fri, 24 May 2024 22:23:23 +0200 Subject: [PATCH] Implement Destiny Bond move (#1104) * Use getBattleStat instead of getStat in BattleStatRatioPowerAttr * Change unnecessary let into const * Refactor BattleStatRatioPowerAttr into two distinct classes * Add TSDoc for the new classes * Implementation of Destiny Bond * Add TSDocs * Make the move fail in boss battles * Fix boss immunity and ally fainting * Update docs * Add doc of return value of tag lapse * Fix ESLint --- src/data/battler-tags.ts | 47 ++++++++++++++++++++++++++++++ src/data/enums/battler-tag-type.ts | 3 +- src/data/move.ts | 28 ++++++++++++++++-- src/field/pokemon.ts | 6 ++++ 4 files changed, 81 insertions(+), 3 deletions(-) diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 13f1ae2cf7c..2cf1c6eff64 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -254,6 +254,51 @@ export class ConfusedTag extends BattlerTag { } } +/** + * Tag applied to the {@linkcode Move.DESTINY_BOND} user. + * @extends BattlerTag + * @see {@linkcode apply} + */ +export class DestinyBondTag extends BattlerTag { + constructor(sourceMove: Moves, sourceId: integer) { + super(BattlerTagType.DESTINY_BOND, BattlerTagLapseType.PRE_MOVE, 1, sourceMove, sourceId); + } + + /** + * Lapses either before the user's move and does nothing + * or after receiving fatal damage. When the damage is fatal, + * the attacking Pokemon is taken down as well, unless it's a boss. + * + * @param {Pokemon} pokemon Pokemon that is attacking the Destiny Bond user. + * @param {BattlerTagLapseType} lapseType CUSTOM or PRE_MOVE + * @returns false if the tag source fainted or one turn has passed since the application + */ + lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { + if (lapseType !== BattlerTagLapseType.CUSTOM) { + return super.lapse(pokemon, lapseType); + } + const source = pokemon.scene.getPokemonById(this.sourceId); + if (!source.isFainted()) { + return true; + } + + if (source.getAlly() === pokemon) { + return false; + } + + const targetMessage = getPokemonMessage(pokemon, ""); + + if (pokemon.isBossImmune()) { + pokemon.scene.queueMessage(`${targetMessage} is unaffected\nby the effects of Destiny Bond.`); + return false; + } + + pokemon.scene.queueMessage(`${getPokemonMessage(source, ` took\n${targetMessage} down with it!`)}`); + pokemon.damageAndUpdate(pokemon.hp, HitResult.ONE_HIT_KO, false, false, true); + return false; + } +} + export class InfatuatedTag extends BattlerTag { constructor(sourceMove: integer, sourceId: integer) { super(BattlerTagType.INFATUATED, BattlerTagLapseType.MOVE, 1, sourceMove, sourceId); @@ -1416,6 +1461,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: integer, sourc return new MagnetRisenTag(tagType, sourceMove); case BattlerTagType.MINIMIZED: return new MinimizeTag(); + case BattlerTagType.DESTINY_BOND: + return new DestinyBondTag(sourceMove, sourceId); case BattlerTagType.NONE: default: return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId); diff --git a/src/data/enums/battler-tag-type.ts b/src/data/enums/battler-tag-type.ts index 9411d70a670..6d36cdafab5 100644 --- a/src/data/enums/battler-tag-type.ts +++ b/src/data/enums/battler-tag-type.ts @@ -56,5 +56,6 @@ export enum BattlerTagType { CHARGED = "CHARGED", GROUNDED = "GROUNDED", MAGNET_RISEN = "MAGNET_RISEN", - MINIMIZED = "MINIMIZED" + MINIMIZED = "MINIMIZED", + DESTINY_BOND = "DESTINY_BOND" } diff --git a/src/data/move.ts b/src/data/move.ts index 34ae05dbd33..0a20c06d73e 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -4704,6 +4704,31 @@ export class MoneyAttr extends MoveEffectAttr { } } +/** + * Applies {@linkcode BattlerTagType.DESTINY_BOND} to the user. + * + * @extends MoveEffectAttr + */ +export class DestinyBondAttr extends MoveEffectAttr { + constructor() { + super(true, MoveEffectTrigger.PRE_APPLY); + } + + /** + * Applies {@linkcode BattlerTagType.DESTINY_BOND} to the user. + * @param user {@linkcode Pokemon} that is having the tag applied to. + * @param target {@linkcode Pokemon} N/A + * @param move {@linkcode Move} {@linkcode Move.DESTINY_BOND} + * @param {any[]} args N/A + * @returns true + */ + apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + user.scene.queueMessage(`${getPokemonMessage(user, " is trying\nto take its foe down with it!")}`); + user.addTag(BattlerTagType.DESTINY_BOND, undefined, move.id, user.id); + return true; + } +} + export class LastResortAttr extends MoveAttr { getCondition(): MoveConditionFunc { return (user: Pokemon, target: Pokemon, move: Move) => { @@ -5405,8 +5430,7 @@ export function initMoves() { .unimplemented(), new SelfStatusMove(Moves.DESTINY_BOND, Type.GHOST, -1, 5, -1, 0, 2) .ignoresProtect() - .condition(failOnBossCondition) - .unimplemented(), + .attr(DestinyBondAttr), new StatusMove(Moves.PERISH_SONG, Type.NORMAL, -1, 5, -1, 0, 2) .attr(FaintCountdownAttr) .ignoresProtect() diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 9f081d73134..5cc4e54c5b4 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -1790,6 +1790,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { console.log("damage", damage.value, move.name, power.value, sourceAtk, targetDef); + // In case of fatal damage, this tag would have gotten cleared before we could lapse it. + const destinyTag = this.getTag(BattlerTagType.DESTINY_BOND); + const oneHitKo = result === HitResult.ONE_HIT_KO; if (damage.value) { if (this.getHpRatio() === 1) { @@ -1850,6 +1853,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (damage) { this.scene.clearPhaseQueueSplice(); + + const attacker = this.scene.getPokemonById(source.id); + destinyTag?.lapse(attacker, BattlerTagLapseType.CUSTOM); } } break;