Compare commits

...

2 Commits

Author SHA1 Message Date
Brian Fertig 74d5abd2a1 refactor(ui): reposition stat labels and animate initiative handoff
- Adjusted `CardObject.js` to align "ATK", "ARM", "HP", and "DLY" labels horizontally next to their values instead of vertically below, improving visual clarity.
- Added `_animateInitiativeHandoff` in `BattleScene.js` to smoothly tween the initiative indicator icon between the player and opponent sides at the end of each turn.
- Updated `_updateInitiativeIndicator` to increase the icon size (110px) and adjust its X position for better visibility.
2026-03-13 11:06:29 -06:00
Brian Fertig 57230fc1b1 refactor(combat): centralize card death logic and fix animation race conditions
- Extracted `_playCardDeath` in `BattleScene.js` to handle the explosion sprite and fade-out sequence for defeated cards, preventing code duplication.
- Updated `_refreshCardAfterAttack` to use the new helper method, serving as a fallback for cards killed by counter or rupture effects without an HP loss animation.
- Modified `animateHPLoss` completion handler in `_animateAttack` to trigger `_playCardDeath` immediately after the HP animation finishes, ensuring the death sequence only plays once and respects the animation timing.
- Added guard logic to prevent double-destruction if a card is already removed from `cardObjects` before the refresh step.
2026-03-13 10:52:38 -06:00
2 changed files with 51 additions and 24 deletions

View File

@ -61,9 +61,9 @@ export class CardObject extends Phaser.GameObjects.Container {
).setOrigin(0, 0.5);
this.add(this.atkText);
// Small "ATK" label under the number (tiny, just for clarity)
// Small "ATK" label — to the right of the value (assumes up to 2 digits)
const atkLabel = this.scene.add.text(
-w / 2 + Math.round(5 * scale), topBannerCY + Math.round(bannerH * 0.30),
-w / 2 + Math.round(19 * scale), topBannerCY,
'ATK',
{ fontSize: fs(5.5), color: '#886666' }
).setOrigin(0, 0.5);
@ -77,9 +77,9 @@ export class CardObject extends Phaser.GameObjects.Container {
).setOrigin(1, 0.5);
this.add(this.armText);
// Small "ARM" label
// Small "ARM" label — to the left of the value (assumes up to 2 digits)
const armLabel = this.scene.add.text(
w / 2 - Math.round(5 * scale), topBannerCY + Math.round(bannerH * 0.30),
w / 2 - Math.round(19 * scale), topBannerCY,
'ARM',
{ fontSize: fs(5.5), color: '#667799' }
).setOrigin(1, 0.5);
@ -144,9 +144,9 @@ export class CardObject extends Phaser.GameObjects.Container {
).setOrigin(0, 0.5);
this.add(this.hpText);
// Small "HP" label
// Small "HP" label — to the right of the value (assumes up to 2 digits)
const hpLabel = this.scene.add.text(
-w / 2 + Math.round(5 * scale), bottomBannerCY + Math.round(bannerH * 0.30),
-w / 2 + Math.round(19 * scale), bottomBannerCY,
'HP',
{ fontSize: fs(5.5), color: '#337755' }
).setOrigin(0, 0.5);
@ -160,9 +160,9 @@ export class CardObject extends Phaser.GameObjects.Container {
).setOrigin(1, 0.5);
this.add(this.dlyText);
// Small "DLY" label
// Small "DLY" label — to the left of the value (assumes up to 2 digits)
const dlyLabel = this.scene.add.text(
w / 2 - Math.round(5 * scale), bottomBannerCY + Math.round(bannerH * 0.30),
w / 2 - Math.round(19 * scale), bottomBannerCY,
'DLY',
{ fontSize: fs(5.5), color: '#777777' }
).setOrigin(1, 0.5);

View File

@ -299,6 +299,7 @@ export class BattleScene extends Phaser.Scene {
this.isAnimating = false;
this._updateLog();
this.statusText.setText('Press SPACE or click Next Turn to advance');
this._animateInitiativeHandoff();
if (this.engine.winner) {
this.time.delayedCall(600, () => this._showResult());
}
@ -329,24 +330,34 @@ export class BattleScene extends Phaser.Scene {
}
// Update a card's HP bar and stats at the moment of impact, then handle death.
// If the defender was already killed via _onAttackReconcile, cardObjects.get() returns
// null here and the function exits early — no double-death.
_refreshCardAfterAttack(cardData) {
if (!cardData) return;
const obj = this.cardObjects.get(cardData.instanceId)
?? this.commanderObjects?.get(cardData.instanceId);
if (!obj) return;
obj.refresh(); // updates HP bar + stats text to current values
// Commanders never get destroyed — the win-check handles game-over
obj.refresh();
// Fallback death path for cards killed by counter/rupture without an HP loss animation
if (cardData.currentHP <= 0 && !obj.isCommander) {
// Explosion sprite centered on the card
const explosionSprite = this.add.sprite(obj.x, obj.y, 'attacks')
.setDisplaySize(630, 630)
.setDepth(23);
explosionSprite.play('explosion_anim');
explosionSprite.once('animationcomplete', () => { if (explosionSprite.scene) explosionSprite.destroy(); });
this._playCardDeath(obj, cardData);
}
}
this.cardObjects.delete(cardData.instanceId);
this.tweens.killTweensOf(obj);
obj.setScale(1).setDepth(0);
// Explosion sprite + fade-out for a defeated lane card.
_playCardDeath(obj, cardData) {
this.cardObjects.delete(cardData.instanceId);
this.tweens.killTweensOf(obj);
obj.setScale(1).setDepth(0);
const explosionSprite = this.add.sprite(obj.x, obj.y, 'attacks')
.setDisplaySize(630, 630)
.setDepth(23);
explosionSprite.play('explosion_anim');
explosionSprite.once('animationcomplete', () => {
if (explosionSprite.scene) explosionSprite.destroy();
if (!obj.scene) return;
this.tweens.add({
targets: obj,
scaleX: 1.25, scaleY: 1.25,
@ -355,7 +366,7 @@ export class BattleScene extends Phaser.Scene {
ease: 'Power2',
onComplete: () => { if (obj.scene) obj.destroy(); }
});
}
});
}
// Animate a card appearing on the field (scale from 0 with overshoot)
@ -568,9 +579,13 @@ export class BattleScene extends Phaser.Scene {
const defenderObj = _lookup(event.defender?.instanceId);
const attackerObj = _lookup(event.attacker?.instanceId);
// HP loss animation on defender
// HP loss animation on defender; on completion, trigger death if HP reached zero
if (defenderObj?.scene && event.damage > 0) {
defenderObj.animateHPLoss(event.damage);
defenderObj.animateHPLoss(event.damage, () => {
if (event.defender.currentHP <= 0 && !defenderObj.isCommander) {
this._playCardDeath(defenderObj, event.defender);
}
});
}
// Berserk ATK gain animation on attacker
@ -768,6 +783,18 @@ export class BattleScene extends Phaser.Scene {
backBtn.on('pointerdown', () => this.scene.start(this.missionData ? 'CampaignScene' : 'MainMenuScene'));
}
_animateInitiativeHandoff() {
if (!this.initiativeIndicator?.scene) return;
// playerGoesFirst still reflects the turn that just ended; next turn is the opposite
const targetY = this.playerGoesFirst ? 305 : 715;
this.tweens.add({
targets: this.initiativeIndicator,
y: targetY,
duration: 700,
ease: 'Power2.inOut'
});
}
_updateInitiativeIndicator(playerGoesFirst) {
if (this.initiativeIndicator) {
this.initiativeIndicator.destroy();
@ -775,10 +802,10 @@ export class BattleScene extends Phaser.Scene {
}
if (!this.textures.exists('attacksFirst')) return;
// Position the indicator to the left of the commander card (commander at x=150, width=240)
const indicatorX = 22;
const indicatorX = 60;
const indicatorY = playerGoesFirst ? 715 : 305;
this.initiativeIndicator = this.add.image(indicatorX, indicatorY, 'attacksFirst')
.setDisplaySize(44, 44)
.setDisplaySize(110, 110)
.setDepth(5);
}