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:
parent
71d291f814
commit
69df7c5793
Binary file not shown.
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 |
Binary file not shown.
|
|
@ -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."
|
||||
}
|
||||
]
|
||||
|
|
@ -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 ─────────────────────────────
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue