chore: update Righteous campaign missions and enhance ability animations

- Updated `data/missions.json` to increase difficulty in Chapter 4:
  - Expanded enemy card pools with higher-tier units (Templars, Purifiers, Prophets, Archangels, Seraphs).
  - Increased gold rewards for missions 022 through 028.
  - Updated mission descriptions and lore to reflect the escalation.

- Enhanced `src/scenes/BattleScene.js` animations for Righteous abilities:
  - Refactored `smite` animations to use new sprite frames (38, 39) and split cast/damage logic.
  - Updated `overcharge` to use sprite 40/41 with pulse effects and integrated HP loss/ATK gain animations.
  - Updated `fortify` to use sprite 37 with projectile travel and pulse effects, integrating armor gain animation.
  - Updated `hack` to use sprite 42 with a 3-cycle pulse before executing copied skills.

- Added new audio assets:
  - `fortify.mp3`, `hack.mp3`, `overcharge.mp3`, `smite-cast.mp3`, `smite-damage.mp3`.
- Updated `src/scenes/BootScene.js` to load the new audio files and removed deprecated smite sounds.
- Updated `assets/images/spritesheets/attacks.png` and `.psd` to include new animation frames.
This commit is contained in:
Brian Fertig 2026-03-21 18:45:15 -06:00
parent 71d291f814
commit 69df7c5793
10 changed files with 209 additions and 203 deletions

BIN
assets/audio/fx/fortify.mp3 Normal file

Binary file not shown.

BIN
assets/audio/fx/hack.mp3 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 MiB

After

Width:  |  Height:  |  Size: 7.9 MiB

View File

@ -493,7 +493,7 @@
"unlockCondition": null,
"opponent": {
"commander": "right_cmd_1",
"cards": ["right_acolyte_1", "right_acolyte_1", "right_initiate_1", "right_initiate_1", "right_sentinel_1"]
"cards": ["right_acolyte_1", "right_initiate_1", "right_sentinel_1", "right_zealot_1", "right_priest_1", "right_warden_1"]
},
"rewards": { "gold": 100, "cards": ["right_acolyte_1"] },
"lore": "They marched out of a blizzard that shouldn't have existed on this world. Silent. Certain."
@ -508,9 +508,9 @@
"unlockCondition": "mission_022",
"opponent": {
"commander": "right_cmd_1",
"cards": ["right_pilgrim_1", "right_pilgrim_1", "right_ward_1", "right_ward_1", "right_priest_1"]
"cards": ["right_pilgrim_1", "right_ward_1", "right_zealot_1", "right_crusader_1", "right_hacker_1", "right_warden_1"]
},
"rewards": { "gold": 125, "cards": ["right_pilgrim_1"] },
"rewards": { "gold": 150, "cards": ["right_pilgrim_1"] },
"lore": "The signal was a prayer. It promised salvation through absolute zero."
},
{
@ -523,9 +523,9 @@
"unlockCondition": "mission_023",
"opponent": {
"commander": "right_cmd_2",
"cards": ["right_acolyte_1", "right_initiate_1", "right_initiate_1", "right_zealot_1", "right_crusader_1"]
"cards": ["right_zealot_1", "right_priest_1", "right_warden_1", "right_hacker_1", "right_templar_1", "right_purifier_1"]
},
"rewards": { "gold": 150, "cards": ["right_sentinel_1"] },
"rewards": { "gold": 200, "cards": ["right_sentinel_1"] },
"lore": "Their Pontiff overcharged the vanguard. Two of our tanks melted before anyone fired back."
},
{
@ -537,10 +537,10 @@
"description": "A hacker unit has infiltrated our comm systems. Our own weapons are being turned against us.",
"unlockCondition": "mission_024",
"opponent": {
"commander": "right_cmd_1",
"cards": ["right_sentinel_1", "right_ward_1", "right_hacker_1", "right_warden_1", "right_zealot_1"]
"commander": "right_cmd_2",
"cards": ["right_hacker_1", "right_crusader_1", "right_templar_1", "right_purifier_1", "right_inquisitor_1", "right_judicator_1"]
},
"rewards": { "gold": 175, "cards": ["right_hacker_1"] },
"rewards": { "gold": 250, "cards": ["right_hacker_1"] },
"lore": "It copied our targeting protocols in milliseconds. We shot ourselves before we realized."
},
{
@ -549,13 +549,13 @@
"chapter": 4,
"campaignId": "campaign_righteous",
"enemyLevel": 1,
"description": "A full assault force descends. Elite templars and purifiers lead the charge.",
"description": "A full assault force descends. The elite vanguard brings their legendary constructs to bear.",
"unlockCondition": "mission_025",
"opponent": {
"commander": "right_cmd_2",
"cards": ["right_pilgrim_1", "right_crusader_1", "right_priest_1", "right_templar_1", "right_purifier_1"]
"cards": ["right_templar_1", "right_purifier_1", "right_inquisitor_1", "right_judicator_1", "right_prophet_1", "right_archangel_1"]
},
"rewards": { "gold": 200, "cards": ["right_zealot_1"] },
"rewards": { "gold": 300, "cards": ["right_zealot_1"] },
"lore": "The cold came first. Then the smiting. Then silence."
},
{
@ -564,13 +564,13 @@
"chapter": 4,
"campaignId": "campaign_righteous",
"enemyLevel": 1,
"description": "The Digital Oracle has deemed us worthy of trial. Its templar guard stands ready.",
"description": "The Digital Oracle has deemed us worthy of trial. Its greatest warriors stand ready.",
"unlockCondition": "mission_026",
"opponent": {
"commander": "right_cmd_3",
"cards": ["right_warden_1", "right_templar_1", "right_inquisitor_1", "right_judicator_1"]
"cards": ["right_purifier_1", "right_inquisitor_1", "right_judicator_1", "right_prophet_1", "right_archangel_1", "right_seraph_1"]
},
"rewards": { "gold": 225, "cards": ["right_templar_1"] },
"rewards": { "gold": 350, "cards": ["right_templar_1"] },
"lore": "The Oracle calculated our defeat in 847 possible outcomes. It only needed one."
},
{
@ -579,13 +579,13 @@
"chapter": 4,
"campaignId": "campaign_righteous",
"enemyLevel": 1,
"description": "The Righteous deploy their legendary constructs. This is near full-strength.",
"description": "The Righteous deploy at near-full strength. One epic guardian leads three legendary constructs.",
"unlockCondition": "mission_027",
"opponent": {
"commander": "right_cmd_3",
"cards": ["right_purifier_1", "right_judicator_1", "right_prophet_1", "right_archangel_1"]
"cards": ["right_judicator_1", "right_prophet_1", "right_archangel_1", "right_seraph_1"]
},
"rewards": { "gold": 250, "cards": ["right_purifier_1"] },
"rewards": { "gold": 400, "cards": ["right_purifier_1"] },
"lore": "The Prophet spoke once. The temperature dropped forty degrees. The battle had already begun."
},
{
@ -598,9 +598,9 @@
"unlockCondition": "mission_028",
"opponent": {
"commander": "right_cmd_3",
"cards": ["right_inquisitor_1", "right_prophet_1", "right_archangel_1", "right_seraph_1"]
"cards": ["right_purifier_1", "right_inquisitor_1", "right_judicator_1", "right_prophet_1", "right_archangel_1", "right_seraph_1"]
},
"rewards": { "gold": 300, "cards": ["right_prophet_1"] },
"rewards": { "gold": 450, "cards": ["right_prophet_1"] },
"lore": "It called us heretics. It called us flawed. It was wrong. We are survivors."
}
]

View File

@ -3922,25 +3922,73 @@ export class BattleScene extends Phaser.Scene {
});
}
// ── Smite animations (mirrors venom with blue tint) ──────────────────────
// ── Smite animations ──────────────────────────────────────────────────────
// Shared helper: fly sprite 38 from source to target, burst on arrival, call cb
_doSmiteCastTo(sourceObj, targetObj, onArrival) {
const projectile = this.add.sprite(sourceObj.x, sourceObj.y, 'attacks', 38)
.setDisplaySize(80, 80)
.setDepth(30);
this.tweens.add({
targets: projectile,
x: targetObj.x, y: targetObj.y,
duration: 350,
ease: 'Quad.In',
onComplete: () => {
if (projectile.scene) projectile.destroy();
onArrival();
}
});
}
_animateSmiteApply(event, onComplete) {
const sourceObj = this.cardObjects.get(event.attacker.instanceId) || this.commanderObjects?.get(event.attacker.instanceId);
const targetObj = this.cardObjects.get(event.target.instanceId) || this.commanderObjects?.get(event.target.instanceId);
if (!sourceObj?.scene || !targetObj?.scene) { onComplete(); return; }
this.statusText.setText(`${event.attacker.name} smites ${event.target.name}! [${event.stacks} stacks]`);
if (this.sound.get('sfx_smite_cast')) this.sound.play('sfx_smite_cast', { volume: 0.8 });
this._doSmiteCastTo(sourceObj, targetObj, () => {
if (targetObj.scene) targetObj.refresh();
onComplete();
});
}
_animateSmiteApplyAll(event, onComplete) {
if (!event.targets?.length) { onComplete(); return; }
this.statusText.setText(`${event.attacker.name} smites all enemies!`);
if (this.sound.get('sfx_smite_cast')) this.sound.play('sfx_smite_cast', { volume: 0.8 });
const sourceObj = this.cardObjects.get(event.attacker.instanceId) || this.commanderObjects?.get(event.attacker.instanceId);
let remaining = event.targets.length;
const done = () => { if (--remaining <= 0) onComplete(); };
for (const t of event.targets) {
const targetObj = this.cardObjects.get(t.target.instanceId) || this.commanderObjects?.get(t.target.instanceId);
if (!sourceObj?.scene || !targetObj?.scene) { done(); continue; }
this._doSmiteCastTo(sourceObj, targetObj, () => {
if (targetObj.scene) targetObj.refresh();
done();
});
}
}
_animateSmiteTick(event, onComplete) {
const obj = this.cardObjects.get(event.card.instanceId) || this.commanderObjects?.get(event.card.instanceId);
if (!obj?.scene) { onComplete(); return; }
this.statusText.setText(`${event.card.name} suffers ${event.damage} smite damage!`);
if (this.sound.get('sfx_smite_effect')) this.sound.play('sfx_smite_effect', { volume: 0.8 });
if (this.sound.get('sfx_smite_damage')) this.sound.play('sfx_smite_damage', { volume: 0.8 });
const effectSprite = this.add.sprite(obj.x, obj.y, 'attacks', 28)
const effectSprite = this.add.sprite(obj.x, obj.y, 'attacks', 39)
.setDisplaySize(140, 140)
.setDepth(30)
.setAlpha(0.9)
.setTint(0x4488ff);
.setAlpha(0.9);
this.tweens.add({
targets: effectSprite,
scaleX: { from: 0.8, to: 1.5 },
scaleY: { from: 0.8, to: 1.5 },
alpha: { from: 0.9, to: 0.2 },
duration: 250,
yoyo: true,
repeat: 1,
@ -3968,94 +4016,6 @@ export class BattleScene extends Phaser.Scene {
}
}
_animateSmiteApply(event, onComplete) {
const sourceObj = this.cardObjects.get(event.attacker.instanceId) || this.commanderObjects?.get(event.attacker.instanceId);
const targetObj = this.cardObjects.get(event.target.instanceId) || this.commanderObjects?.get(event.target.instanceId);
if (!sourceObj?.scene || !targetObj?.scene) { onComplete(); return; }
this.statusText.setText(`${event.attacker.name} smites ${event.target.name}! [${event.stacks} stacks]`);
if (this.sound.get('sfx_smite_apply')) this.sound.play('sfx_smite_apply', { volume: 0.8 });
const projectile = this.add.sprite(sourceObj.x, sourceObj.y, 'attacks', 27)
.setDisplaySize(80, 80)
.setDepth(30)
.setTint(0x4488ff);
this.tweens.add({
targets: projectile,
x: targetObj.x, y: targetObj.y,
duration: 350,
ease: 'Quad.In',
onComplete: () => {
if (projectile.scene) projectile.destroy();
const effectSprite = this.add.sprite(targetObj.x, targetObj.y, 'attacks', 28)
.setDisplaySize(100, 100)
.setDepth(30)
.setAlpha(0.9)
.setTint(0x4488ff);
this.tweens.add({
targets: effectSprite,
scaleX: { from: 0.8, to: 1.4 },
scaleY: { from: 0.8, to: 1.4 },
alpha: { from: 0.9, to: 0.3 },
duration: 200,
yoyo: true, repeat: 1,
onComplete: () => {
if (effectSprite.scene) effectSprite.destroy();
if (targetObj.scene) targetObj.refresh();
onComplete();
}
});
}
});
}
_animateSmiteApplyAll(event, onComplete) {
if (!event.targets?.length) { onComplete(); return; }
this.statusText.setText(`${event.attacker.name} smites all enemies!`);
if (this.sound.get('sfx_smite_apply')) this.sound.play('sfx_smite_apply', { volume: 0.8 });
let remaining = event.targets.length;
const done = () => { if (--remaining <= 0) onComplete(); };
const sourceObj = this.cardObjects.get(event.attacker.instanceId) || this.commanderObjects?.get(event.attacker.instanceId);
for (const t of event.targets) {
const targetObj = this.cardObjects.get(t.target.instanceId) || this.commanderObjects?.get(t.target.instanceId);
if (!sourceObj?.scene || !targetObj?.scene) { done(); continue; }
const projectile = this.add.sprite(sourceObj.x, sourceObj.y, 'attacks', 27)
.setDisplaySize(80, 80)
.setDepth(30)
.setTint(0x4488ff);
this.tweens.add({
targets: projectile,
x: targetObj.x, y: targetObj.y,
duration: 350,
ease: 'Quad.In',
onComplete: () => {
if (projectile.scene) projectile.destroy();
const effectSprite = this.add.sprite(targetObj.x, targetObj.y, 'attacks', 28)
.setDisplaySize(100, 100)
.setDepth(30)
.setAlpha(0.9)
.setTint(0x4488ff);
this.tweens.add({
targets: effectSprite,
scaleX: { from: 0.8, to: 1.4 },
scaleY: { from: 0.8, to: 1.4 },
alpha: { from: 0.9, to: 0.3 },
duration: 200,
yoyo: true, repeat: 1,
onComplete: () => {
if (effectSprite.scene) effectSprite.destroy();
if (targetObj.scene) targetObj.refresh();
done();
}
});
}
});
}
}
// ── Sanctify animations ──────────────────────────────────────────────────
_processSanctifyFires(fires, onComplete) {
@ -4184,20 +4144,35 @@ export class BattleScene extends Phaser.Scene {
this.statusText.setText(`${fire.source.name} overcharges! -${fire.selfDamage} HP, +${fire.selfDamage} ATK to allies`);
if (this.sound.get('sfx_overcharge')) this.sound.play('sfx_overcharge', { volume: 0.8 });
// Flash red on source (self-damage)
sourceObj.flash(0xff4444);
// Sprite 40 pulses over source while HP loss animates
const pulseSprite = this.add.sprite(sourceObj.x, sourceObj.y, 'attacks', 40)
.setDisplaySize(130, 130)
.setDepth(30)
.setAlpha(0.9);
this.tweens.add({
targets: pulseSprite,
scaleX: { from: 0.8, to: 1.5 },
scaleY: { from: 0.8, to: 1.5 },
duration: 220,
yoyo: true,
repeat: 1,
onComplete: () => { if (pulseSprite.scene) pulseSprite.destroy(); }
});
// After self-damage flash, animate buff to allies
this.time.delayedCall(400, () => {
if (sourceObj.scene) sourceObj.refresh();
const afterDamage = () => {
if (fire.selfKilled && sourceObj.scene) {
this._playCardDeath(sourceObj, fire.source, () => {
this._animateOverchargeBuffs(fire, onComplete);
});
this._playCardDeath(sourceObj, fire.source, () => this._animateOverchargeBuffs(fire, onComplete));
} else {
if (sourceObj.scene) sourceObj.refresh();
this._animateOverchargeBuffs(fire, onComplete);
}
});
};
if (fire.selfDamage > 0 && sourceObj.animateHPLoss) {
sourceObj.animateHPLoss(fire.selfDamage, afterDamage);
} else {
this.time.delayedCall(550, afterDamage);
}
}
_animateOverchargeBuffs(fire, onComplete) {
@ -4209,23 +4184,24 @@ export class BattleScene extends Phaser.Scene {
const obj = this.cardObjects.get(t.target.instanceId) || this.commanderObjects?.get(t.target.instanceId);
if (!obj?.scene) { done(); continue; }
// Yellow glow for attack buff
const effectSprite = this.add.sprite(obj.x, obj.y, 'attacks', 34)
.setDisplaySize(100, 100)
// Sprite 41 pulses over each buffed ally while ATK gain animates
const buffSprite = this.add.sprite(obj.x, obj.y, 'attacks', 41)
.setDisplaySize(130, 130)
.setDepth(30)
.setAlpha(0.7)
.setTint(0xffcc00);
.setAlpha(0.9);
this.tweens.add({
targets: effectSprite,
scaleX: { from: 0.8, to: 1.3 },
scaleY: { from: 0.8, to: 1.3 },
alpha: { from: 0.7, to: 0 },
duration: 300,
onComplete: () => {
if (effectSprite.scene) effectSprite.destroy();
if (obj.scene) obj.refresh();
done();
}
targets: buffSprite,
scaleX: { from: 0.8, to: 1.5 },
scaleY: { from: 0.8, to: 1.5 },
duration: 220,
yoyo: true,
repeat: 1,
onComplete: () => { if (buffSprite.scene) buffSprite.destroy(); }
});
obj.animateBerserkGain(fire.selfDamage, () => {
if (obj.scene) obj.refresh();
done();
});
}
}
@ -4253,98 +4229,128 @@ export class BattleScene extends Phaser.Scene {
this.statusText.setText(`${fire.source.name} fortifies! +${fire.amount} armor`);
if (this.sound.get('sfx_fortify')) this.sound.play('sfx_fortify', { volume: 0.8 });
// Cyan shield effect
const effectSprite = this.add.sprite(obj.x, obj.y, 'attacks', 29)
.setDisplaySize(120, 120)
.setDepth(30)
.setAlpha(0.8)
.setTint(0x66ccff);
this.tweens.add({
targets: effectSprite,
scaleX: { from: 0.7, to: 1.4 },
scaleY: { from: 0.7, to: 1.4 },
alpha: { from: 0.8, to: 0 },
duration: 350,
onComplete: () => {
if (effectSprite.scene) effectSprite.destroy();
if (obj.scene) obj.refresh();
onComplete();
}
});
this._doFortifyPulse(obj, fire.amount, onComplete);
}
_animateFortifyAll(fire, onComplete) {
if (!fire.targets?.length) { onComplete(); return; }
this.statusText.setText(`${fire.source.name} fortifies all allies! +${fire.targets[0]?.amount || 0} armor`);
const amount = fire.targets[0]?.amount || 0;
this.statusText.setText(`${fire.source.name} fortifies all allies! +${amount} armor`);
if (this.sound.get('sfx_fortify')) this.sound.play('sfx_fortify', { volume: 0.8 });
const sourceObj = this.cardObjects.get(fire.source.instanceId) || this.commanderObjects?.get(fire.source.instanceId);
const startX = sourceObj?.scene ? sourceObj.x : 0;
const startY = sourceObj?.scene ? sourceObj.y : 0;
let remaining = fire.targets.length;
const done = () => { if (--remaining <= 0) onComplete(); };
for (const t of fire.targets) {
const obj = this.cardObjects.get(t.target.instanceId) || this.commanderObjects?.get(t.target.instanceId);
if (!obj?.scene) { done(); continue; }
const targetObj = this.cardObjects.get(t.target.instanceId) || this.commanderObjects?.get(t.target.instanceId);
if (!targetObj?.scene) { done(); continue; }
const effectSprite = this.add.sprite(obj.x, obj.y, 'attacks', 29)
.setDisplaySize(120, 120)
// Fly sprite 37 from source to each target, then pulse on arrival
const isSelf = sourceObj && targetObj === sourceObj;
const flyDuration = isSelf ? 0 : 300;
const projectile = this.add.sprite(startX, startY, 'attacks', 37)
.setDisplaySize(110, 110)
.setDepth(30)
.setAlpha(0.8)
.setTint(0x66ccff);
.setAlpha(1);
this.tweens.add({
targets: effectSprite,
scaleX: { from: 0.7, to: 1.4 },
scaleY: { from: 0.7, to: 1.4 },
alpha: { from: 0.8, to: 0 },
duration: 350,
targets: projectile,
x: targetObj.x,
y: targetObj.y,
duration: flyDuration,
ease: 'Quad.easeIn',
onComplete: () => {
if (effectSprite.scene) effectSprite.destroy();
if (obj.scene) obj.refresh();
done();
if (projectile.scene) projectile.destroy();
if (!targetObj.scene) { done(); return; }
this._doFortifyPulse(targetObj, t.amount, done);
}
});
}
}
// Pulse sprite 37 over a card (grow/shrink twice) while animating armor gain.
_doFortifyPulse(obj, amount, onComplete) {
// Reset armor text to pre-fortify value so the gain animates visibly
if (obj.armText) obj.armText.setText(`${(obj.cardData.currentArmor || 0) - amount}`);
const sprite = this.add.sprite(obj.x, obj.y, 'attacks', 37)
.setDisplaySize(110, 110)
.setDepth(30)
.setAlpha(0.9);
// Pulse: grow and shrink twice (yoyo × 2)
this.tweens.add({
targets: sprite,
scaleX: { from: 0.8, to: 1.5 },
scaleY: { from: 0.8, to: 1.5 },
duration: 220,
yoyo: true,
repeat: 1,
ease: 'Sine.easeInOut',
onComplete: () => {
if (sprite.scene) sprite.destroy();
}
});
// Animate armor gain in parallel with the pulse
obj.animateArmorGain(amount, () => {
if (obj.scene) obj.refresh();
onComplete();
});
}
// ── Hack animation (preAttack) ───────────────────────────────────────────
_animateHackFire(attacker, hackGroup, onComplete) {
const sourceObj = this.cardObjects.get(attacker.instanceId) || this.commanderObjects?.get(attacker.instanceId);
const targetObj = hackGroup.copiedFrom ? (this.cardObjects.get(hackGroup.copiedFrom.instanceId) || this.commanderObjects?.get(hackGroup.copiedFrom.instanceId)) : null;
this.statusText.setText(`${attacker.name} hacks ${hackGroup.copiedFrom?.name || 'enemy'}!`);
if (this.sound.get('sfx_hack')) this.sound.play('sfx_hack', { volume: 0.8 });
// Glitch effect on source card
if (sourceObj?.scene) {
const effectSprite = this.add.sprite(sourceObj.x, sourceObj.y, 'attacks', 34)
.setDisplaySize(120, 120)
.setDepth(30)
.setAlpha(0.8)
.setTint(0x00ff88);
this.tweens.add({
targets: effectSprite,
scaleX: { from: 1.0, to: 1.5 },
scaleY: { from: 1.0, to: 1.5 },
alpha: { from: 0.8, to: 0 },
duration: 400,
onComplete: () => { if (effectSprite.scene) effectSprite.destroy(); }
});
}
// Process the sub-fires sequentially using existing animators
const fires = hackGroup.fires || [];
const nextFire = (idx) => {
if (idx >= fires.length) { onComplete(); return; }
const fire = fires[idx];
const cb = () => nextFire(idx + 1);
if (fire.skill === 'strike') this._animateStrikeFire(attacker, fire, cb);
else if (fire.skill === 'mortar') this._animateMortarFire(attacker, fire, cb);
else if (fire.skill === 'pierce') this._animatePierceFire(attacker, fire, cb);
else if (fire.skill === 'swipe') this._animateSwipeSequence(attacker, [fire], cb);
else if (fire.skill === 'drain') this._animateDrainFire(fire, cb);
else cb();
// Sprite 42 pulses on source for 1.5s before copied skills fire
const runFires = () => {
const fires = hackGroup.fires || [];
const nextFire = (idx) => {
if (idx >= fires.length) { onComplete(); return; }
const fire = fires[idx];
const cb = () => nextFire(idx + 1);
if (fire.skill === 'strike') this._animateStrikeFire(attacker, fire, cb);
else if (fire.skill === 'mortar') this._animateMortarFire(attacker, fire, cb);
else if (fire.skill === 'pierce') this._animatePierceFire(attacker, fire, cb);
else if (fire.skill === 'swipe') this._animateSwipeSequence(attacker, [fire], cb);
else if (fire.skill === 'drain') this._animateDrainFire(fire, cb);
else cb();
};
nextFire(0);
};
this.time.delayedCall(450, () => nextFire(0));
if (!sourceObj?.scene) { this.time.delayedCall(1500, runFires); return; }
const pulseSprite = this.add.sprite(sourceObj.x, sourceObj.y, 'attacks', 42)
.setDisplaySize(130, 130)
.setDepth(30)
.setAlpha(0.9);
// 3 cycles of grow/shrink over ~1500ms (500ms per cycle: 250ms out + 250ms back)
this.tweens.add({
targets: pulseSprite,
scaleX: { from: 0.8, to: 1.5 },
scaleY: { from: 0.8, to: 1.5 },
duration: 250,
yoyo: true,
repeat: 2,
ease: 'Sine.easeInOut',
onComplete: () => {
if (pulseSprite.scene) pulseSprite.destroy();
runFires();
}
});
}
// ── Restore/reapply for new preBattle skills ─────────────────────────────

View File

@ -54,8 +54,8 @@ export class BootScene extends Phaser.Scene {
this.load.audio('sfx_heal', 'assets/audio/fx/heal.mp3');
this.load.audio('sfx_sanctify', 'assets/audio/fx/sanctify.mp3');
this.load.audio('sfx_hack', 'assets/audio/fx/hack.mp3');
this.load.audio('sfx_smite_apply', 'assets/audio/fx/smite-apply.mp3');
this.load.audio('sfx_smite_effect', 'assets/audio/fx/smite-effect.mp3');
this.load.audio('sfx_smite_cast', 'assets/audio/fx/smite-cast.mp3');
this.load.audio('sfx_smite_damage', 'assets/audio/fx/smite-damage.mp3');
this.load.audio('sfx_overcharge', 'assets/audio/fx/overcharge.mp3');
this.load.audio('sfx_fortify', 'assets/audio/fx/fortify.mp3');