[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 <bennybroseph@gmail.com>
This commit is contained in:
DustinLin 2024-07-22 10:44:29 -07:00 committed by GitHub
parent d0cbe19bd8
commit ccb38ca12b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 83 additions and 7 deletions

View File

@ -180,11 +180,16 @@ export default class BattleScene extends SceneBase {
public gameData: GameData; public gameData: GameData;
public sessionSlotId: integer; public sessionSlotId: integer;
/** PhaseQueue: dequeue/remove the first element to get the next phase */
public phaseQueue: Phase[]; public phaseQueue: Phase[];
public conditionalQueue: Array<[() => boolean, Phase]>; public conditionalQueue: Array<[() => boolean, Phase]>;
/** PhaseQueuePrepend: is a temp storage of what will be added to PhaseQueue */
private phaseQueuePrepend: Phase[]; private phaseQueuePrepend: Phase[];
/** overrides default of inserting phases to end of phaseQueuePrepend array, useful or inserting Phases "out of order" */
private phaseQueuePrependSpliceIndex: integer; private phaseQueuePrependSpliceIndex: integer;
private nextCommandPhaseQueue: Phase[]; private nextCommandPhaseQueue: Phase[];
private currentPhase: Phase; private currentPhase: Phase;
private standbyPhase: Phase; private standbyPhase: Phase;
public field: Phaser.GameObjects.Container; public field: Phaser.GameObjects.Container;
@ -1961,6 +1966,7 @@ export default class BattleScene extends SceneBase {
return this.standbyPhase; return this.standbyPhase;
} }
/** /**
* Adds a phase to the conditional queue and ensures it is executed only when the specified condition is met. * 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]); 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 { pushPhase(phase: Phase, defer: boolean = false): void {
(!defer ? this.phaseQueue : this.nextCommandPhaseQueue).push(phase); (!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 { unshiftPhase(phase: Phase): void {
if (this.phaseQueuePrependSpliceIndex === -1) { if (this.phaseQueuePrependSpliceIndex === -1) {
this.phaseQueuePrepend.push(phase); this.phaseQueuePrepend.push(phase);
@ -1988,18 +2002,32 @@ export default class BattleScene extends SceneBase {
} }
} }
/**
* Clears the phaseQueue
*/
clearPhaseQueue(): void { clearPhaseQueue(): void {
this.phaseQueue.splice(0, this.phaseQueue.length); 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 { setPhaseQueueSplice(): void {
this.phaseQueuePrependSpliceIndex = this.phaseQueuePrepend.length; this.phaseQueuePrependSpliceIndex = this.phaseQueuePrepend.length;
} }
/**
* Resets phaseQueuePrependSpliceIndex to -1, implies that calls to unshiftPhase will insert at end of phaseQueuePrepend
*/
clearPhaseQueueSplice(): void { clearPhaseQueueSplice(): void {
this.phaseQueuePrependSpliceIndex = -1; 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 { shiftPhase(): void {
if (this.standbyPhase) { if (this.standbyPhase) {
this.currentPhase = this.standbyPhase; this.currentPhase = this.standbyPhase;
@ -2017,7 +2045,7 @@ export default class BattleScene extends SceneBase {
} }
if (!this.phaseQueue.length) { if (!this.phaseQueue.length) {
this.populatePhaseQueue(); 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.conditionalQueue = [];
} }
this.currentPhase = this.phaseQueue.shift(); 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) { queueMessage(message: string, callbackDelay?: integer, prompt?: boolean, promptDelay?: integer, defer?: boolean) {
const phase = new MessagePhase(this, message, callbackDelay, prompt, promptDelay); const phase = new MessagePhase(this, message, callbackDelay, prompt, promptDelay);
if (!defer) { if (!defer) {
// adds to the end of PhaseQueuePrepend
this.unshiftPhase(phase); this.unshiftPhase(phase);
} else { } else {
//remember that pushPhase adds it to nextCommandPhaseQueue
this.pushPhase(phase); this.pushPhase(phase);
} }
} }
/**
* Moves everything from nextCommandPhaseQueue to phaseQueue (keeping order)
*/
populatePhaseQueue(): void { populatePhaseQueue(): void {
if (this.nextCommandPhaseQueue.length) { if (this.nextCommandPhaseQueue.length) {
this.phaseQueue.push(...this.nextCommandPhaseQueue); this.phaseQueue.push(...this.nextCommandPhaseQueue);

View File

@ -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); damage.value = this.damageAndUpdate(damage.value, result as DamageResult, isCritical, isOneHitKo, isOneHitKo, true);
this.turnData.damageTaken += damage.value; this.turnData.damageTaken += damage.value;
if (isCritical) { if (isCritical) {
this.scene.queueMessage(i18next.t("battle:hitResultCriticalHit")); 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) { switch (result) {
case HitResult.SUPER_EFFECTIVE: case HitResult.SUPER_EFFECTIVE:
this.scene.queueMessage(i18next.t("battle:hitResultSuperEffective")); this.scene.queueMessage(i18next.t("battle:hitResultSuperEffective"));
@ -2075,13 +2077,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
} }
if (this.isFainted()) { 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.scene.unshiftPhase(new FaintPhase(this.scene, this.getBattlerIndex(), isOneHitKo));
this.resetSummonData(); this.resetSummonData();
} }
if (damage) { if (damage) {
this.scene.clearPhaseQueueSplice();
const attacker = this.scene.getPokemonById(source.id); const attacker = this.scene.getPokemonById(source.id);
destinyTag?.lapse(attacker, BattlerTagLapseType.CUSTOM); destinyTag?.lapse(attacker, BattlerTagLapseType.CUSTOM);
} }
@ -2105,6 +2107,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
return result; 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 { damage(damage: integer, ignoreSegments: boolean = false, preventEndure: boolean = false, ignoreFaintPhase: boolean = false): integer {
if (this.isFainted()) { if (this.isFainted()) {
return 0; return 0;
@ -2126,9 +2136,16 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
} }
damage = Math.min(damage, this.hp); damage = Math.min(damage, this.hp);
this.hp = this.hp - damage; this.hp = this.hp - damage;
if (this.isFainted() && !ignoreFaintPhase) { 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.scene.unshiftPhase(new FaintPhase(this.scene, this.getBattlerIndex(), preventEndure));
this.resetSummonData(); this.resetSummonData();
} }
@ -2136,6 +2153,16 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
return damage; 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 { 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); const damagePhase = new DamagePhase(this.scene, this.getBattlerIndex(), damage, result as DamageResult, critical);
this.scene.unshiftPhase(damagePhase); this.scene.unshiftPhase(damagePhase);

View File

@ -2380,6 +2380,11 @@ export class TurnStartPhase extends FieldPhase {
this.scene.pushPhase(new BerryPhase(this.scene)); this.scene.pushPhase(new BerryPhase(this.scene));
this.scene.pushPhase(new TurnEndPhase(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(); this.end();
} }
} }
@ -3036,8 +3041,11 @@ export class MoveEffectPhase extends PokemonPhase {
if (--user.turnData.hitsLeft >= 1 && this.getTarget()?.isActive()) { if (--user.turnData.hitsLeft >= 1 && this.getTarget()?.isActive()) {
this.scene.unshiftPhase(this.getNewHitPhase()); this.scene.unshiftPhase(this.getNewHitPhase());
} else { } 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); 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.queueMessage(i18next.t("battle:attackHitsCount", { count: hitsTotal }));
} }
this.scene.applyModifiers(HitHealModifier, this.player, user); this.scene.applyModifiers(HitHealModifier, this.player, user);