From ccb38ca12b746b9f5c8a4e4a9912e169657c44c9 Mon Sep 17 00:00:00 2001 From: DustinLin <39450497+DustinLin@users.noreply.github.com> Date: Mon, 22 Jul 2024 10:44:29 -0700 Subject: [PATCH] [BUG] fixing multi-hit and move messages on faint (#2981) * fixing order of messages, scences, to render messages before fainting * updated fix for effectiveness text rendering order for multi hit moves * fixing messages not appearing for multi-hit moves on faint * updated multi-hit condition) * fixing PR conflicts * adding comments and FaintPhase setPhaseQueueSplice bug, fixing overrides merge conflict * writing better comments * removing space diff in overrides * adding fainting check for self damage moves * emergency fixing broken last commit * additional comments for multi-hit problem * updating comments, jsdoc style * fixing linter, destiny bond errors * splitting up varaible comments to be in JSDoc format * fixing tests and merge mistakes * adding rendering of multihit moves that only hit once * fixing comment formatting_tabs and spaces --------- Co-authored-by: Benjamin Odom --- src/battle-scene.ts | 45 ++++++++++++++++++++++++++++++++++++++++++-- src/field/pokemon.ts | 35 ++++++++++++++++++++++++++++++---- src/phases.ts | 10 +++++++++- 3 files changed, 83 insertions(+), 7 deletions(-) diff --git a/src/battle-scene.ts b/src/battle-scene.ts index b33a5696b7b..bc748c70bec 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -180,11 +180,16 @@ export default class BattleScene extends SceneBase { public gameData: GameData; public sessionSlotId: integer; + /** PhaseQueue: dequeue/remove the first element to get the next phase */ public phaseQueue: Phase[]; public conditionalQueue: Array<[() => boolean, Phase]>; + /** PhaseQueuePrepend: is a temp storage of what will be added to PhaseQueue */ private phaseQueuePrepend: Phase[]; + + /** overrides default of inserting phases to end of phaseQueuePrepend array, useful or inserting Phases "out of order" */ private phaseQueuePrependSpliceIndex: integer; private nextCommandPhaseQueue: Phase[]; + private currentPhase: Phase; private standbyPhase: Phase; public field: Phaser.GameObjects.Container; @@ -1961,6 +1966,7 @@ export default class BattleScene extends SceneBase { return this.standbyPhase; } + /** * Adds a phase to the conditional queue and ensures it is executed only when the specified condition is met. * @@ -1975,11 +1981,19 @@ export default class BattleScene extends SceneBase { this.conditionalQueue.push([condition, phase]); } - + /** + * Adds a phase to nextCommandPhaseQueue, as long as boolean passed in is false + * @param phase {@linkcode Phase} the phase to add + * @param defer boolean on which queue to add to, defaults to false, and adds to phaseQueue + */ pushPhase(phase: Phase, defer: boolean = false): void { (!defer ? this.phaseQueue : this.nextCommandPhaseQueue).push(phase); } + /** + * Adds Phase to the end of phaseQueuePrepend, or at phaseQueuePrependSpliceIndex + * @param phase {@linkcode Phase} the phase to add + */ unshiftPhase(phase: Phase): void { if (this.phaseQueuePrependSpliceIndex === -1) { this.phaseQueuePrepend.push(phase); @@ -1988,18 +2002,32 @@ export default class BattleScene extends SceneBase { } } + /** + * Clears the phaseQueue + */ clearPhaseQueue(): void { this.phaseQueue.splice(0, this.phaseQueue.length); } + /** + * Used by function unshiftPhase(), sets index to start inserting at current length instead of the end of the array, useful if phaseQueuePrepend gets longer with Phases + */ setPhaseQueueSplice(): void { this.phaseQueuePrependSpliceIndex = this.phaseQueuePrepend.length; } + /** + * Resets phaseQueuePrependSpliceIndex to -1, implies that calls to unshiftPhase will insert at end of phaseQueuePrepend + */ clearPhaseQueueSplice(): void { this.phaseQueuePrependSpliceIndex = -1; } + /** + * Is called by each Phase implementations "end()" by default + * We dump everything from phaseQueuePrepend to the start of of phaseQueue + * then removes first Phase and starts it + */ shiftPhase(): void { if (this.standbyPhase) { this.currentPhase = this.standbyPhase; @@ -2017,7 +2045,7 @@ export default class BattleScene extends SceneBase { } if (!this.phaseQueue.length) { this.populatePhaseQueue(); - // clear the conditionalQueue if there are no phases left in the phaseQueue + // Clear the conditionalQueue if there are no phases left in the phaseQueue this.conditionalQueue = []; } this.currentPhase = this.phaseQueue.shift(); @@ -2102,15 +2130,28 @@ export default class BattleScene extends SceneBase { } } + /** + * Adds a MessagePhase, either to PhaseQueuePrepend or nextCommandPhaseQueue + * @param message string for MessagePhase + * @param callbackDelay optional param for MessagePhase constructor + * @param prompt optional param for MessagePhase constructor + * @param promptDelay optional param for MessagePhase constructor + * @param defer boolean for which queue to add it to, false -> add to PhaseQueuePrepend, true -> nextCommandPhaseQueue + */ queueMessage(message: string, callbackDelay?: integer, prompt?: boolean, promptDelay?: integer, defer?: boolean) { const phase = new MessagePhase(this, message, callbackDelay, prompt, promptDelay); if (!defer) { + // adds to the end of PhaseQueuePrepend this.unshiftPhase(phase); } else { + //remember that pushPhase adds it to nextCommandPhaseQueue this.pushPhase(phase); } } + /** + * Moves everything from nextCommandPhaseQueue to phaseQueue (keeping order) + */ populatePhaseQueue(): void { if (this.nextCommandPhaseQueue.length) { this.phaseQueue.push(...this.nextCommandPhaseQueue); diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 2fe1d831659..9a7682040cd 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -2035,6 +2035,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { */ damage.value = this.damageAndUpdate(damage.value, result as DamageResult, isCritical, isOneHitKo, isOneHitKo, true); this.turnData.damageTaken += damage.value; + if (isCritical) { this.scene.queueMessage(i18next.t("battle:hitResultCriticalHit")); } @@ -2054,7 +2055,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } } - if (source.turnData.hitsLeft === 1) { + // want to include is.Fainted() in case multi hit move ends early, still want to render message + if (source.turnData.hitsLeft === 1 || this.isFainted()) { switch (result) { case HitResult.SUPER_EFFECTIVE: this.scene.queueMessage(i18next.t("battle:hitResultSuperEffective")); @@ -2075,13 +2077,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } if (this.isFainted()) { + // set splice index here, so future scene queues happen before FaintedPhase + this.scene.setPhaseQueueSplice(); this.scene.unshiftPhase(new FaintPhase(this.scene, this.getBattlerIndex(), isOneHitKo)); this.resetSummonData(); } if (damage) { - this.scene.clearPhaseQueueSplice(); - const attacker = this.scene.getPokemonById(source.id); destinyTag?.lapse(attacker, BattlerTagLapseType.CUSTOM); } @@ -2105,6 +2107,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return result; } + /** + * Called by damageAndUpdate() + * @param damage integer + * @param ignoreSegments boolean, not currently used + * @param preventEndure used to update damage if endure or sturdy + * @param ignoreFaintPhase flag on wheter to add FaintPhase if pokemon after applying damage faints + * @returns integer representing damage + */ damage(damage: integer, ignoreSegments: boolean = false, preventEndure: boolean = false, ignoreFaintPhase: boolean = false): integer { if (this.isFainted()) { return 0; @@ -2126,9 +2136,16 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } damage = Math.min(damage, this.hp); - this.hp = this.hp - damage; if (this.isFainted() && !ignoreFaintPhase) { + /** + * When adding the FaintPhase, want to toggle future unshiftPhase() and queueMessage() calls + * to appear before the FaintPhase (as FaintPhase will potentially end the encounter and add Phases such as + * GameOverPhase, VictoryPhase, etc.. that will interfere with anything else that happens during this MoveEffectPhase) + * + * Once the MoveEffectPhase is over (and calls it's .end() function, shiftPhase() will reset the PhaseQueueSplice via clearPhaseQueueSplice() ) + */ + this.scene.setPhaseQueueSplice(); this.scene.unshiftPhase(new FaintPhase(this.scene, this.getBattlerIndex(), preventEndure)); this.resetSummonData(); } @@ -2136,6 +2153,16 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return damage; } + /** + * Called by apply(), given the damage, adds a new DamagePhase and actually updates HP values, etc. + * @param damage integer - passed to damage() + * @param result an enum if it's super effective, not very, etc. + * @param critical boolean if move is a critical hit + * @param ignoreSegments boolean, passed to damage() and not used currently + * @param preventEndure boolean, ignore endure properties of pokemon, passed to damage() + * @param ignoreFaintPhase boolean to ignore adding a FaintPhase, passsed to damage() + * @returns integer of damage done + */ damageAndUpdate(damage: integer, result?: DamageResult, critical: boolean = false, ignoreSegments: boolean = false, preventEndure: boolean = false, ignoreFaintPhase: boolean = false): integer { const damagePhase = new DamagePhase(this.scene, this.getBattlerIndex(), damage, result as DamageResult, critical); this.scene.unshiftPhase(damagePhase); diff --git a/src/phases.ts b/src/phases.ts index 308750cc196..8a66c7a185d 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -2380,6 +2380,11 @@ export class TurnStartPhase extends FieldPhase { this.scene.pushPhase(new BerryPhase(this.scene)); this.scene.pushPhase(new TurnEndPhase(this.scene)); + /** + * this.end() will call shiftPhase(), which dumps everything from PrependQueue (aka everything that is unshifted()) to the front + * of the queue and dequeues to start the next phase + * this is important since stuff like SwitchSummon, AttemptRun, AttemptCapture Phases break the "flow" and should take precedence + */ this.end(); } } @@ -3036,8 +3041,11 @@ export class MoveEffectPhase extends PokemonPhase { if (--user.turnData.hitsLeft >= 1 && this.getTarget()?.isActive()) { this.scene.unshiftPhase(this.getNewHitPhase()); } else { + // Queue message for number of hits made by multi-move + // If multi-hit attack only hits once, still want to render a message const hitsTotal = user.turnData.hitCount - Math.max(user.turnData.hitsLeft, 0); - if (hitsTotal > 1) { + if (hitsTotal > 1 || user.turnData.hitsLeft > 0) { + // If there are multiple hits, or if there are hits of the multi-hit move left this.scene.queueMessage(i18next.t("battle:attackHitsCount", { count: hitsTotal })); } this.scene.applyModifiers(HitHealModifier, this.player, user);