feat: track pre/post HP in wound/venom/smite events and improve stat refresh logic
- Add hpBefore/hpAfter fields to woundTick/venomTick/smiteTick events for accurate damage tracking - Introduce CardObject.refreshStatsOnly() to update ATK/ARM/DLY/skills without touching HP or badges - Refactor BattleScene._refreshCardAfterAttack() and related methods to use refreshStatsOnly() instead of full refresh, preserving HP animations and badge timing - Update animateHPLoss calls to pass hpBefore/hpAfter for precise progress tracking - Fix berserk animation to correctly rewind before animating ATK gain - Remove redundant commander HP bar updates during attacks (handled by preAttack skills) - Add _restoreSiegeFiresForDisplay/_reapplySiegeFires to temporarily undo siege damage for display This change separates stat updates from HP management, ensuring animations and UI remain consistent with the combat engine's state transitions.
This commit is contained in:
parent
1ca1b5bf80
commit
d5a30da9aa
|
|
@ -80,6 +80,7 @@ export class CombatEngine {
|
|||
for (const c of cards) {
|
||||
if (c.woundStacks > 0) {
|
||||
const dmg = c.woundStacks;
|
||||
const hpBefore = c.currentHP;
|
||||
c.currentHP -= dmg;
|
||||
this._log(`${c.name} takes ${dmg} wound damage`);
|
||||
const killed = c.currentHP <= 0;
|
||||
|
|
@ -88,7 +89,7 @@ export class CombatEngine {
|
|||
this.events.push({ type: 'death', card: c, side });
|
||||
this._processOnDeath(c, alliedLanes, side);
|
||||
}
|
||||
this.events.push({ type: 'woundTick', card: c, damage: dmg, killed });
|
||||
this.events.push({ type: 'woundTick', card: c, damage: dmg, killed, hpBefore, hpAfter: Math.max(0, c.currentHP) });
|
||||
this._tryCarapace(c, dmg);
|
||||
c.woundStacks--;
|
||||
}
|
||||
|
|
@ -121,6 +122,7 @@ export class CombatEngine {
|
|||
for (const c of cards) {
|
||||
if (c.venomStacks > 0) {
|
||||
const dmg = c.venomStacks;
|
||||
const hpBefore = c.currentHP;
|
||||
c.currentHP -= dmg;
|
||||
this._log(`${c.name} takes ${dmg} venom damage`);
|
||||
const killed = c.currentHP <= 0;
|
||||
|
|
@ -129,7 +131,7 @@ export class CombatEngine {
|
|||
this.events.push({ type: 'death', card: c, side });
|
||||
this._processOnDeath(c, alliedLanes, side);
|
||||
}
|
||||
this.events.push({ type: 'venomTick', card: c, damage: dmg, killed });
|
||||
this.events.push({ type: 'venomTick', card: c, damage: dmg, killed, hpBefore, hpAfter: Math.max(0, c.currentHP) });
|
||||
this._tryCarapace(c, dmg);
|
||||
c.venomStacks--;
|
||||
}
|
||||
|
|
@ -145,6 +147,7 @@ export class CombatEngine {
|
|||
for (const c of cards) {
|
||||
if (c.smiteStacks > 0) {
|
||||
const dmg = c.smiteStacks;
|
||||
const hpBefore = c.currentHP;
|
||||
c.currentHP -= dmg;
|
||||
this._log(`${c.name} takes ${dmg} smite damage`);
|
||||
const killed = c.currentHP <= 0;
|
||||
|
|
@ -153,7 +156,7 @@ export class CombatEngine {
|
|||
this.events.push({ type: 'death', card: c, side });
|
||||
this._processOnDeath(c, alliedLanes, side);
|
||||
}
|
||||
this.events.push({ type: 'smiteTick', card: c, damage: dmg, killed });
|
||||
this.events.push({ type: 'smiteTick', card: c, damage: dmg, killed, hpBefore, hpAfter: Math.max(0, c.currentHP) });
|
||||
this._tryCarapace(c, dmg);
|
||||
c.smiteStacks--;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -333,6 +333,33 @@ export class CardObject extends Phaser.GameObjects.Container {
|
|||
}
|
||||
}
|
||||
|
||||
// Updates ATK, ARM, DLY, delay overlay, and skill text from live cardData,
|
||||
// but intentionally skips HP and all stack badges. Use this during animations
|
||||
// where HP is managed by animateHPLoss and badges by their own apply animations.
|
||||
refreshStatsOnly() {
|
||||
const w = this.options.width || 80;
|
||||
const scale = w / 80;
|
||||
if (this.atkText) {
|
||||
this.atkText.setText(`${this.cardData.currentAttack}`);
|
||||
if (this.atkLabel) this.atkLabel.setX(this.atkText.x + this.atkText.width + Math.round(2 * scale));
|
||||
}
|
||||
if (this.armText) {
|
||||
this.armText.setText(`${this.cardData.currentArmor}`);
|
||||
if (this.armLabel) this.armLabel.setX(this.armText.x - this.armText.width - Math.round(2 * scale));
|
||||
}
|
||||
if (this.dlyText) this.dlyText.setText(`${this.cardData.currentDelay}`);
|
||||
if (this.delayOverlay) this.delayOverlay.setVisible(this.cardData.currentDelay > 0);
|
||||
if (this.skillText && this.cardData.skills?.length) {
|
||||
const skillStr = this.cardData.skills
|
||||
.map(s => {
|
||||
const allMod = s.all ? ' all' : '';
|
||||
return s.value != null ? `${s.name}${allMod} ${s.value}` : `${s.name}${allMod}`;
|
||||
})
|
||||
.join(' · ');
|
||||
this.skillText.setText(skillStr);
|
||||
}
|
||||
}
|
||||
|
||||
animateArmorLoss(amount, onComplete, fromARM = null, toARM = null) {
|
||||
if (!this.armText || !this.scene) { if (onComplete) onComplete(); return; }
|
||||
|
||||
|
|
|
|||
|
|
@ -843,6 +843,7 @@ export class BattleScene extends Phaser.Scene {
|
|||
this._restoreOverchargeForDisplay(preBattleEvents);
|
||||
this._restoreFortifyForDisplay(preBattleEvents);
|
||||
this._restorePreBattleWoundFires(preBattleEvents);
|
||||
this._restoreSiegeFiresForDisplay(preBattleEvents);
|
||||
// Render the field with all newly deployed cards but pre-combat stats
|
||||
this._renderState();
|
||||
this._reapplyBuffs(preBattleEvents);
|
||||
|
|
@ -854,6 +855,7 @@ export class BattleScene extends Phaser.Scene {
|
|||
this._reapplyOvercharge(preBattleEvents);
|
||||
this._reapplyFortify(preBattleEvents);
|
||||
this._reapplyPreBattleWoundFires(preBattleEvents);
|
||||
this._reapplySiegeFires(preBattleEvents);
|
||||
|
||||
const newPlayerCard = chosenCard
|
||||
? this.engine.getState().player.lanes.find(c => !oldPlayerIds.has(c.instanceId))
|
||||
|
|
@ -958,13 +960,6 @@ export class BattleScene extends Phaser.Scene {
|
|||
return;
|
||||
}
|
||||
|
||||
// Update commander HP bars for siege damage (fires during _performAttack).
|
||||
// preAttack skills (strike) correct the targeted commander's HP themselves at the
|
||||
// start of their animation, overriding this refresh for that specific card.
|
||||
const state = this.engine.getState();
|
||||
this.commanderObjects.get(state.player.commander.instanceId)?.refresh();
|
||||
this.commanderObjects.get(state.opponent.commander.instanceId)?.refresh();
|
||||
|
||||
// Animate each round sequentially. For flurry, cards return to their original
|
||||
// positions after each attack before the next round begins.
|
||||
const animateRound = (roundIdx) => {
|
||||
|
|
@ -1007,7 +1002,10 @@ export class BattleScene extends Phaser.Scene {
|
|||
animateRound(0);
|
||||
}
|
||||
|
||||
// Update a card's HP bar and stats at the moment of impact, then handle death.
|
||||
// Update a card's ATK/ARM/DLY at the moment of impact, then handle death.
|
||||
// HP is intentionally not updated here — it is managed by animateHPLoss.
|
||||
// Badges are intentionally not updated here — they are shown by each on-attack
|
||||
// apply animation in _processOnAttackAllEvents (wound/venom/smite/carapace).
|
||||
// If the defender was already killed via _onAttackReconcile, cardObjects.get() returns
|
||||
// null here and the function exits early — no double-death.
|
||||
_refreshCardAfterAttack(cardData) {
|
||||
|
|
@ -1015,7 +1013,7 @@ export class BattleScene extends Phaser.Scene {
|
|||
const obj = this.cardObjects.get(cardData.instanceId)
|
||||
?? this.commanderObjects?.get(cardData.instanceId);
|
||||
if (!obj) return;
|
||||
obj.refresh();
|
||||
obj.refreshStatsOnly();
|
||||
// Fallback death path for cards killed by counter/wound without an HP loss animation
|
||||
if (cardData.currentHP <= 0 && !obj.isCommander) {
|
||||
this._playCardDeath(obj, cardData);
|
||||
|
|
@ -1150,7 +1148,7 @@ export class BattleScene extends Phaser.Scene {
|
|||
});
|
||||
|
||||
if (event.damage > 0 && obj.animateHPLoss) {
|
||||
obj.animateHPLoss(event.damage, null);
|
||||
obj.animateHPLoss(event.damage, null, event.hpBefore, event.hpAfter);
|
||||
}
|
||||
|
||||
// Wait for sprite to finish (~2 seconds), then handle kill/complete
|
||||
|
|
@ -1529,7 +1527,7 @@ export class BattleScene extends Phaser.Scene {
|
|||
if (obj.scene) obj.refresh();
|
||||
onComplete();
|
||||
}
|
||||
});
|
||||
}, event.hpBefore, event.hpAfter);
|
||||
} else {
|
||||
this.time.delayedCall(550, () => {
|
||||
if (obj.scene) obj.refresh();
|
||||
|
|
@ -1721,7 +1719,9 @@ export class BattleScene extends Phaser.Scene {
|
|||
const targetObj = _lookup(buff.target.instanceId);
|
||||
if (!targetObj?.scene) { onComplete(); return; }
|
||||
this.statusText.setText(`${buff.target.name} swarm: +${buff.amount} ATK`);
|
||||
targetObj.animateBerserkGain(buff.amount, onComplete);
|
||||
// Rewind to pre-buff value so the animation transitions old → new (mirrors _animateRallyBuff)
|
||||
if (targetObj.atkText?.scene) targetObj.atkText.setText(`${buff.target.currentAttack - buff.amount}`);
|
||||
targetObj.animateBerserkGain(buff.amount, onComplete, buff.target.currentAttack);
|
||||
}
|
||||
|
||||
// Explosion sprite + fade-out for a defeated lane card.
|
||||
|
|
@ -2669,15 +2669,11 @@ export class BattleScene extends Phaser.Scene {
|
|||
// Fires once both cards are fully enlarged at center
|
||||
this.time.delayedCall(SCALE_UP_MS + 80, () => {
|
||||
if (defenderObj?.scene) {
|
||||
// Refresh stats (ATK, ARM, DLY) but preserve the HP text that was already
|
||||
// set by the preAttack animation or by animateHPLoss (which fired 80ms ago
|
||||
// in _onAttackReconcile). Overwriting HP here would corrupt the in-progress
|
||||
// HP loss animation and prematurely show the final HP on commanders.
|
||||
const savedHP = defenderObj.hpText?.text;
|
||||
defenderObj.refresh();
|
||||
if (savedHP !== undefined && defenderObj.hpText?.scene) {
|
||||
defenderObj.hpText.setText(savedHP);
|
||||
}
|
||||
// Update ATK/ARM/DLY for the enlarged card view. HP is intentionally left
|
||||
// alone — it is managed by animateHPLoss (fired in _onAttackReconcile).
|
||||
// Badges are intentionally left alone — they are shown by each on-attack
|
||||
// apply animation in _processOnAttackAllEvents, not here.
|
||||
defenderObj.refreshStatsOnly();
|
||||
|
||||
// Flash red overlay → fade out
|
||||
defenderObj.bg.setAlpha(0.70);
|
||||
|
|
@ -4907,7 +4903,7 @@ export class BattleScene extends Phaser.Scene {
|
|||
};
|
||||
|
||||
if (event.damage > 0 && obj.animateHPLoss) {
|
||||
obj.animateHPLoss(event.damage, afterHP);
|
||||
obj.animateHPLoss(event.damage, afterHP, event.hpBefore, event.hpAfter);
|
||||
} else {
|
||||
hpDone = true;
|
||||
tryFinish();
|
||||
|
|
@ -5351,4 +5347,22 @@ export class BattleScene extends Phaser.Scene {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Temporarily undo siege HP damage so _renderState() builds commanders with
|
||||
// pre-siege HP. Re-applied immediately after via _reapplySiegeFires().
|
||||
_restoreSiegeFiresForDisplay(preBattleEvents) {
|
||||
for (const event of preBattleEvents) {
|
||||
for (const fire of event.siegeFires || []) {
|
||||
fire.target.currentHP += fire.damage;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_reapplySiegeFires(preBattleEvents) {
|
||||
for (const event of preBattleEvents) {
|
||||
for (const fire of event.siegeFires || []) {
|
||||
fire.target.currentHP -= fire.damage;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue