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,
|
"unlockCondition": null,
|
||||||
"opponent": {
|
"opponent": {
|
||||||
"commander": "right_cmd_1",
|
"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"] },
|
"rewards": { "gold": 100, "cards": ["right_acolyte_1"] },
|
||||||
"lore": "They marched out of a blizzard that shouldn't have existed on this world. Silent. Certain."
|
"lore": "They marched out of a blizzard that shouldn't have existed on this world. Silent. Certain."
|
||||||
|
|
@ -508,9 +508,9 @@
|
||||||
"unlockCondition": "mission_022",
|
"unlockCondition": "mission_022",
|
||||||
"opponent": {
|
"opponent": {
|
||||||
"commander": "right_cmd_1",
|
"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."
|
"lore": "The signal was a prayer. It promised salvation through absolute zero."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -523,9 +523,9 @@
|
||||||
"unlockCondition": "mission_023",
|
"unlockCondition": "mission_023",
|
||||||
"opponent": {
|
"opponent": {
|
||||||
"commander": "right_cmd_2",
|
"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."
|
"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.",
|
"description": "A hacker unit has infiltrated our comm systems. Our own weapons are being turned against us.",
|
||||||
"unlockCondition": "mission_024",
|
"unlockCondition": "mission_024",
|
||||||
"opponent": {
|
"opponent": {
|
||||||
"commander": "right_cmd_1",
|
"commander": "right_cmd_2",
|
||||||
"cards": ["right_sentinel_1", "right_ward_1", "right_hacker_1", "right_warden_1", "right_zealot_1"]
|
"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."
|
"lore": "It copied our targeting protocols in milliseconds. We shot ourselves before we realized."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -549,13 +549,13 @@
|
||||||
"chapter": 4,
|
"chapter": 4,
|
||||||
"campaignId": "campaign_righteous",
|
"campaignId": "campaign_righteous",
|
||||||
"enemyLevel": 1,
|
"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",
|
"unlockCondition": "mission_025",
|
||||||
"opponent": {
|
"opponent": {
|
||||||
"commander": "right_cmd_2",
|
"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."
|
"lore": "The cold came first. Then the smiting. Then silence."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -564,13 +564,13 @@
|
||||||
"chapter": 4,
|
"chapter": 4,
|
||||||
"campaignId": "campaign_righteous",
|
"campaignId": "campaign_righteous",
|
||||||
"enemyLevel": 1,
|
"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",
|
"unlockCondition": "mission_026",
|
||||||
"opponent": {
|
"opponent": {
|
||||||
"commander": "right_cmd_3",
|
"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."
|
"lore": "The Oracle calculated our defeat in 847 possible outcomes. It only needed one."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -579,13 +579,13 @@
|
||||||
"chapter": 4,
|
"chapter": 4,
|
||||||
"campaignId": "campaign_righteous",
|
"campaignId": "campaign_righteous",
|
||||||
"enemyLevel": 1,
|
"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",
|
"unlockCondition": "mission_027",
|
||||||
"opponent": {
|
"opponent": {
|
||||||
"commander": "right_cmd_3",
|
"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."
|
"lore": "The Prophet spoke once. The temperature dropped forty degrees. The battle had already begun."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -598,9 +598,9 @@
|
||||||
"unlockCondition": "mission_028",
|
"unlockCondition": "mission_028",
|
||||||
"opponent": {
|
"opponent": {
|
||||||
"commander": "right_cmd_3",
|
"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."
|
"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) {
|
_animateSmiteTick(event, onComplete) {
|
||||||
const obj = this.cardObjects.get(event.card.instanceId) || this.commanderObjects?.get(event.card.instanceId);
|
const obj = this.cardObjects.get(event.card.instanceId) || this.commanderObjects?.get(event.card.instanceId);
|
||||||
if (!obj?.scene) { onComplete(); return; }
|
if (!obj?.scene) { onComplete(); return; }
|
||||||
|
|
||||||
this.statusText.setText(`${event.card.name} suffers ${event.damage} smite damage!`);
|
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)
|
.setDisplaySize(140, 140)
|
||||||
.setDepth(30)
|
.setDepth(30)
|
||||||
.setAlpha(0.9)
|
.setAlpha(0.9);
|
||||||
.setTint(0x4488ff);
|
|
||||||
this.tweens.add({
|
this.tweens.add({
|
||||||
targets: effectSprite,
|
targets: effectSprite,
|
||||||
scaleX: { from: 0.8, to: 1.5 },
|
scaleX: { from: 0.8, to: 1.5 },
|
||||||
scaleY: { from: 0.8, to: 1.5 },
|
scaleY: { from: 0.8, to: 1.5 },
|
||||||
alpha: { from: 0.9, to: 0.2 },
|
|
||||||
duration: 250,
|
duration: 250,
|
||||||
yoyo: true,
|
yoyo: true,
|
||||||
repeat: 1,
|
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 ──────────────────────────────────────────────────
|
// ── Sanctify animations ──────────────────────────────────────────────────
|
||||||
|
|
||||||
_processSanctifyFires(fires, onComplete) {
|
_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`);
|
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 });
|
if (this.sound.get('sfx_overcharge')) this.sound.play('sfx_overcharge', { volume: 0.8 });
|
||||||
|
|
||||||
// Flash red on source (self-damage)
|
// Sprite 40 pulses over source while HP loss animates
|
||||||
sourceObj.flash(0xff4444);
|
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
|
const afterDamage = () => {
|
||||||
this.time.delayedCall(400, () => {
|
|
||||||
if (sourceObj.scene) sourceObj.refresh();
|
|
||||||
if (fire.selfKilled && sourceObj.scene) {
|
if (fire.selfKilled && sourceObj.scene) {
|
||||||
this._playCardDeath(sourceObj, fire.source, () => {
|
this._playCardDeath(sourceObj, fire.source, () => this._animateOverchargeBuffs(fire, onComplete));
|
||||||
this._animateOverchargeBuffs(fire, onComplete);
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
|
if (sourceObj.scene) sourceObj.refresh();
|
||||||
this._animateOverchargeBuffs(fire, onComplete);
|
this._animateOverchargeBuffs(fire, onComplete);
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (fire.selfDamage > 0 && sourceObj.animateHPLoss) {
|
||||||
|
sourceObj.animateHPLoss(fire.selfDamage, afterDamage);
|
||||||
|
} else {
|
||||||
|
this.time.delayedCall(550, afterDamage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_animateOverchargeBuffs(fire, onComplete) {
|
_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);
|
const obj = this.cardObjects.get(t.target.instanceId) || this.commanderObjects?.get(t.target.instanceId);
|
||||||
if (!obj?.scene) { done(); continue; }
|
if (!obj?.scene) { done(); continue; }
|
||||||
|
|
||||||
// Yellow glow for attack buff
|
// Sprite 41 pulses over each buffed ally while ATK gain animates
|
||||||
const effectSprite = this.add.sprite(obj.x, obj.y, 'attacks', 34)
|
const buffSprite = this.add.sprite(obj.x, obj.y, 'attacks', 41)
|
||||||
.setDisplaySize(100, 100)
|
.setDisplaySize(130, 130)
|
||||||
.setDepth(30)
|
.setDepth(30)
|
||||||
.setAlpha(0.7)
|
.setAlpha(0.9);
|
||||||
.setTint(0xffcc00);
|
|
||||||
this.tweens.add({
|
this.tweens.add({
|
||||||
targets: effectSprite,
|
targets: buffSprite,
|
||||||
scaleX: { from: 0.8, to: 1.3 },
|
scaleX: { from: 0.8, to: 1.5 },
|
||||||
scaleY: { from: 0.8, to: 1.3 },
|
scaleY: { from: 0.8, to: 1.5 },
|
||||||
alpha: { from: 0.7, to: 0 },
|
duration: 220,
|
||||||
duration: 300,
|
yoyo: true,
|
||||||
onComplete: () => {
|
repeat: 1,
|
||||||
if (effectSprite.scene) effectSprite.destroy();
|
onComplete: () => { if (buffSprite.scene) buffSprite.destroy(); }
|
||||||
if (obj.scene) obj.refresh();
|
});
|
||||||
done();
|
|
||||||
}
|
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`);
|
this.statusText.setText(`${fire.source.name} fortifies! +${fire.amount} armor`);
|
||||||
if (this.sound.get('sfx_fortify')) this.sound.play('sfx_fortify', { volume: 0.8 });
|
if (this.sound.get('sfx_fortify')) this.sound.play('sfx_fortify', { volume: 0.8 });
|
||||||
|
|
||||||
// Cyan shield effect
|
this._doFortifyPulse(obj, fire.amount, onComplete);
|
||||||
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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_animateFortifyAll(fire, onComplete) {
|
_animateFortifyAll(fire, onComplete) {
|
||||||
if (!fire.targets?.length) { onComplete(); return; }
|
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 });
|
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;
|
let remaining = fire.targets.length;
|
||||||
const done = () => { if (--remaining <= 0) onComplete(); };
|
const done = () => { if (--remaining <= 0) onComplete(); };
|
||||||
|
|
||||||
for (const t of fire.targets) {
|
for (const t of fire.targets) {
|
||||||
const obj = this.cardObjects.get(t.target.instanceId) || this.commanderObjects?.get(t.target.instanceId);
|
const targetObj = this.cardObjects.get(t.target.instanceId) || this.commanderObjects?.get(t.target.instanceId);
|
||||||
if (!obj?.scene) { done(); continue; }
|
if (!targetObj?.scene) { done(); continue; }
|
||||||
|
|
||||||
const effectSprite = this.add.sprite(obj.x, obj.y, 'attacks', 29)
|
// Fly sprite 37 from source to each target, then pulse on arrival
|
||||||
.setDisplaySize(120, 120)
|
const isSelf = sourceObj && targetObj === sourceObj;
|
||||||
|
const flyDuration = isSelf ? 0 : 300;
|
||||||
|
|
||||||
|
const projectile = this.add.sprite(startX, startY, 'attacks', 37)
|
||||||
|
.setDisplaySize(110, 110)
|
||||||
.setDepth(30)
|
.setDepth(30)
|
||||||
.setAlpha(0.8)
|
.setAlpha(1);
|
||||||
.setTint(0x66ccff);
|
|
||||||
this.tweens.add({
|
this.tweens.add({
|
||||||
targets: effectSprite,
|
targets: projectile,
|
||||||
scaleX: { from: 0.7, to: 1.4 },
|
x: targetObj.x,
|
||||||
scaleY: { from: 0.7, to: 1.4 },
|
y: targetObj.y,
|
||||||
alpha: { from: 0.8, to: 0 },
|
duration: flyDuration,
|
||||||
duration: 350,
|
ease: 'Quad.easeIn',
|
||||||
onComplete: () => {
|
onComplete: () => {
|
||||||
if (effectSprite.scene) effectSprite.destroy();
|
if (projectile.scene) projectile.destroy();
|
||||||
if (obj.scene) obj.refresh();
|
if (!targetObj.scene) { done(); return; }
|
||||||
done();
|
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) ───────────────────────────────────────────
|
// ── Hack animation (preAttack) ───────────────────────────────────────────
|
||||||
|
|
||||||
_animateHackFire(attacker, hackGroup, onComplete) {
|
_animateHackFire(attacker, hackGroup, onComplete) {
|
||||||
const sourceObj = this.cardObjects.get(attacker.instanceId) || this.commanderObjects?.get(attacker.instanceId);
|
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'}!`);
|
this.statusText.setText(`${attacker.name} hacks ${hackGroup.copiedFrom?.name || 'enemy'}!`);
|
||||||
if (this.sound.get('sfx_hack')) this.sound.play('sfx_hack', { volume: 0.8 });
|
if (this.sound.get('sfx_hack')) this.sound.play('sfx_hack', { volume: 0.8 });
|
||||||
|
|
||||||
// Glitch effect on source card
|
// Sprite 42 pulses on source for 1.5s before copied skills fire
|
||||||
if (sourceObj?.scene) {
|
const runFires = () => {
|
||||||
const effectSprite = this.add.sprite(sourceObj.x, sourceObj.y, 'attacks', 34)
|
const fires = hackGroup.fires || [];
|
||||||
.setDisplaySize(120, 120)
|
const nextFire = (idx) => {
|
||||||
.setDepth(30)
|
if (idx >= fires.length) { onComplete(); return; }
|
||||||
.setAlpha(0.8)
|
const fire = fires[idx];
|
||||||
.setTint(0x00ff88);
|
const cb = () => nextFire(idx + 1);
|
||||||
this.tweens.add({
|
if (fire.skill === 'strike') this._animateStrikeFire(attacker, fire, cb);
|
||||||
targets: effectSprite,
|
else if (fire.skill === 'mortar') this._animateMortarFire(attacker, fire, cb);
|
||||||
scaleX: { from: 1.0, to: 1.5 },
|
else if (fire.skill === 'pierce') this._animatePierceFire(attacker, fire, cb);
|
||||||
scaleY: { from: 1.0, to: 1.5 },
|
else if (fire.skill === 'swipe') this._animateSwipeSequence(attacker, [fire], cb);
|
||||||
alpha: { from: 0.8, to: 0 },
|
else if (fire.skill === 'drain') this._animateDrainFire(fire, cb);
|
||||||
duration: 400,
|
else cb();
|
||||||
onComplete: () => { if (effectSprite.scene) effectSprite.destroy(); }
|
};
|
||||||
});
|
nextFire(0);
|
||||||
}
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
};
|
};
|
||||||
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 ─────────────────────────────
|
// ── 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_heal', 'assets/audio/fx/heal.mp3');
|
||||||
this.load.audio('sfx_sanctify', 'assets/audio/fx/sanctify.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_hack', 'assets/audio/fx/hack.mp3');
|
||||||
this.load.audio('sfx_smite_apply', 'assets/audio/fx/smite-apply.mp3');
|
this.load.audio('sfx_smite_cast', 'assets/audio/fx/smite-cast.mp3');
|
||||||
this.load.audio('sfx_smite_effect', 'assets/audio/fx/smite-effect.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_overcharge', 'assets/audio/fx/overcharge.mp3');
|
||||||
this.load.audio('sfx_fortify', 'assets/audio/fx/fortify.mp3');
|
this.load.audio('sfx_fortify', 'assets/audio/fx/fortify.mp3');
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue