feat(combat): add hack support for new skills and fix sound playback

- Extend `CombatEngine` to support hacking `strike`, `jam`, `venom`, `smite`, and `molt` skills, including single and area-of-effect variants.
- Add logic to process deaths and carapace gain for hacked `strikeAll` attacks.
- Update `BattleScene` to handle animation sequences for the new hacked skills (`strikeAll`, `jam`, `venom`, `venomAll`, `smite`, `smiteAll`, `molt`).
- Fix sound effect playback by removing redundant existence checks (`if (this.sound.get(...))`), ensuring sounds play reliably.
This commit is contained in:
Brian Fertig 2026-03-21 19:31:04 -06:00
parent 69df7c5793
commit 82f871abfb
2 changed files with 46 additions and 9 deletions

View File

@ -889,6 +889,36 @@ export class CombatEngine {
if (copied.name === 'drain' && ctx2.drainTarget) { if (copied.name === 'drain' && ctx2.drainTarget) {
hackFires.push({ skill: 'drain', source: pending.attacker, target: ctx2.drainTarget, damage: ctx2.drainDamage, heal: ctx2.drainHeal, isHacked: true }); hackFires.push({ skill: 'drain', source: pending.attacker, target: ctx2.drainTarget, damage: ctx2.drainDamage, heal: ctx2.drainHeal, isHacked: true });
} }
if (copied.name === 'strike' && copied.all && ctx2.strikeAllTargets) {
for (const t of ctx2.strikeAllTargets) {
const targetIsCommander = t.target === pending.enemyCommander;
const fire = { skill: 'strikeAll', target: t.target, damage: t.damage, hpBefore: t.hpBefore, targetIsCommander, isHacked: true };
if (!targetIsCommander && t.target.currentHP <= 0) {
this.events.push({ type: 'death', card: t.target, side: _enemySide });
this._processOnDeath(t.target, pending.enemyLanes, _enemySide);
}
fire.carapaceGain = !targetIsCommander ? this._tryCarapace(t.target, t.damage, false) : 0;
hackFires.push(fire);
}
}
if (copied.name === 'jam' && ctx2.jamTargets?.length) {
hackFires.push({ skill: 'jam', source: pending.attacker, targets: ctx2.jamTargets, isHacked: true });
}
if (copied.name === 'venom' && !copied.all && ctx2.venomTarget) {
hackFires.push({ skill: 'venom', attacker: pending.attacker, target: ctx2.venomTarget, stacks: ctx2.venomStacks, isHacked: true });
}
if (copied.name === 'venom' && copied.all && ctx2.venomAllTargets?.length) {
hackFires.push({ skill: 'venomAll', attacker: pending.attacker, targets: ctx2.venomAllTargets, isHacked: true });
}
if (copied.name === 'smite' && !copied.all && ctx2.smiteTarget) {
hackFires.push({ skill: 'smite', attacker: pending.attacker, target: ctx2.smiteTarget, stacks: ctx2.smiteStacks, isHacked: true });
}
if (copied.name === 'smite' && copied.all && ctx2.smiteAllTargets?.length) {
hackFires.push({ skill: 'smiteAll', attacker: pending.attacker, targets: ctx2.smiteAllTargets, isHacked: true });
}
if (copied.name === 'molt' && ctx2.moltHeal > 0) {
hackFires.push({ skill: 'molt', card: pending.attacker, heal: ctx2.moltHeal, armorLost: ctx2.moltArmorLost, isHacked: true });
}
} }
if (hackFires.length > 0) { if (hackFires.length > 0) {
preAttackFires.push({ skill: 'hack', source: pending.attacker, copiedFrom: pending.target, fires: hackFires }); preAttackFires.push({ skill: 'hack', source: pending.attacker, copiedFrom: pending.target, fires: hackFires });

View File

@ -3947,7 +3947,7 @@ export class BattleScene extends Phaser.Scene {
if (!sourceObj?.scene || !targetObj?.scene) { onComplete(); return; } if (!sourceObj?.scene || !targetObj?.scene) { onComplete(); return; }
this.statusText.setText(`${event.attacker.name} smites ${event.target.name}! [${event.stacks} stacks]`); 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.sound.play('sfx_smite_cast', { volume: 0.8 });
this._doSmiteCastTo(sourceObj, targetObj, () => { this._doSmiteCastTo(sourceObj, targetObj, () => {
if (targetObj.scene) targetObj.refresh(); if (targetObj.scene) targetObj.refresh();
@ -3958,7 +3958,7 @@ export class BattleScene extends Phaser.Scene {
_animateSmiteApplyAll(event, onComplete) { _animateSmiteApplyAll(event, onComplete) {
if (!event.targets?.length) { onComplete(); return; } if (!event.targets?.length) { onComplete(); return; }
this.statusText.setText(`${event.attacker.name} smites all enemies!`); 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 }); this.sound.play('sfx_smite_cast', { volume: 0.8 });
const sourceObj = this.cardObjects.get(event.attacker.instanceId) || this.commanderObjects?.get(event.attacker.instanceId); const sourceObj = this.cardObjects.get(event.attacker.instanceId) || this.commanderObjects?.get(event.attacker.instanceId);
let remaining = event.targets.length; let remaining = event.targets.length;
@ -3979,7 +3979,7 @@ export class BattleScene extends Phaser.Scene {
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_damage')) this.sound.play('sfx_smite_damage', { volume: 0.8 }); this.sound.play('sfx_smite_damage', { volume: 0.8 });
const effectSprite = this.add.sprite(obj.x, obj.y, 'attacks', 39) const effectSprite = this.add.sprite(obj.x, obj.y, 'attacks', 39)
.setDisplaySize(140, 140) .setDisplaySize(140, 140)
@ -4039,7 +4039,7 @@ export class BattleScene extends Phaser.Scene {
const effects = fire.cleansed.map(c => c.effect).join(', '); const effects = fire.cleansed.map(c => c.effect).join(', ');
this.statusText.setText(`${fire.source.name} sanctifies self! Cleansed: ${effects}`); this.statusText.setText(`${fire.source.name} sanctifies self! Cleansed: ${effects}`);
if (this.sound.get('sfx_sanctify')) this.sound.play('sfx_sanctify', { volume: 0.8 }); this.sound.play('sfx_sanctify', { volume: 0.8 });
const startX = sourceObj?.scene ? sourceObj.x : targetObj.x; const startX = sourceObj?.scene ? sourceObj.x : targetObj.x;
const startY = sourceObj?.scene ? sourceObj.y : targetObj.y; const startY = sourceObj?.scene ? sourceObj.y : targetObj.y;
@ -4052,7 +4052,7 @@ export class BattleScene extends Phaser.Scene {
_animateSanctifyAll(fire, onComplete) { _animateSanctifyAll(fire, onComplete) {
if (!fire.targets?.length) { onComplete(); return; } if (!fire.targets?.length) { onComplete(); return; }
this.statusText.setText(`${fire.source.name} sanctifies all allies!`); this.statusText.setText(`${fire.source.name} sanctifies all allies!`);
if (this.sound.get('sfx_sanctify')) this.sound.play('sfx_sanctify', { volume: 0.8 }); this.sound.play('sfx_sanctify', { volume: 0.8 });
const sourceObj = this.cardObjects.get(fire.source.instanceId) || this.commanderObjects?.get(fire.source.instanceId); const sourceObj = this.cardObjects.get(fire.source.instanceId) || this.commanderObjects?.get(fire.source.instanceId);
const startX = sourceObj?.scene ? sourceObj.x : 0; const startX = sourceObj?.scene ? sourceObj.x : 0;
@ -4142,7 +4142,7 @@ export class BattleScene extends Phaser.Scene {
if (!sourceObj?.scene) { onComplete(); return; } if (!sourceObj?.scene) { onComplete(); return; }
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 }); this.sound.play('sfx_overcharge', { volume: 0.8 });
// Sprite 40 pulses over source while HP loss animates // Sprite 40 pulses over source while HP loss animates
const pulseSprite = this.add.sprite(sourceObj.x, sourceObj.y, 'attacks', 40) const pulseSprite = this.add.sprite(sourceObj.x, sourceObj.y, 'attacks', 40)
@ -4227,7 +4227,7 @@ export class BattleScene extends Phaser.Scene {
if (!obj?.scene) { onComplete(); return; } if (!obj?.scene) { onComplete(); return; }
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 }); this.sound.play('sfx_fortify', { volume: 0.8 });
this._doFortifyPulse(obj, fire.amount, onComplete); this._doFortifyPulse(obj, fire.amount, onComplete);
} }
@ -4237,7 +4237,7 @@ export class BattleScene extends Phaser.Scene {
const amount = fire.targets[0]?.amount || 0; const amount = fire.targets[0]?.amount || 0;
this.statusText.setText(`${fire.source.name} fortifies all allies! +${amount} armor`); 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 }); this.sound.play('sfx_fortify', { volume: 0.8 });
const sourceObj = this.cardObjects.get(fire.source.instanceId) || this.commanderObjects?.get(fire.source.instanceId); const sourceObj = this.cardObjects.get(fire.source.instanceId) || this.commanderObjects?.get(fire.source.instanceId);
const startX = sourceObj?.scene ? sourceObj.x : 0; const startX = sourceObj?.scene ? sourceObj.x : 0;
@ -4311,7 +4311,7 @@ export class BattleScene extends Phaser.Scene {
const sourceObj = this.cardObjects.get(attacker.instanceId) || this.commanderObjects?.get(attacker.instanceId); const sourceObj = this.cardObjects.get(attacker.instanceId) || this.commanderObjects?.get(attacker.instanceId);
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 }); this.sound.play('sfx_hack', { volume: 0.8 });
// Sprite 42 pulses on source for 1.5s before copied skills fire // Sprite 42 pulses on source for 1.5s before copied skills fire
const runFires = () => { const runFires = () => {
@ -4321,10 +4321,17 @@ export class BattleScene extends Phaser.Scene {
const fire = fires[idx]; const fire = fires[idx];
const cb = () => nextFire(idx + 1); const cb = () => nextFire(idx + 1);
if (fire.skill === 'strike') this._animateStrikeFire(attacker, fire, cb); if (fire.skill === 'strike') this._animateStrikeFire(attacker, fire, cb);
else if (fire.skill === 'strikeAll') this._animateStrikeFire(attacker, fire, cb);
else if (fire.skill === 'mortar') this._animateMortarFire(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 === 'pierce') this._animatePierceFire(attacker, fire, cb);
else if (fire.skill === 'swipe') this._animateSwipeSequence(attacker, [fire], cb); else if (fire.skill === 'swipe') this._animateSwipeSequence(attacker, [fire], cb);
else if (fire.skill === 'drain') this._animateDrainFire(fire, cb); else if (fire.skill === 'drain') this._animateDrainFire(fire, cb);
else if (fire.skill === 'jam') this._animateJamFire(fire, cb);
else if (fire.skill === 'venom') this._animateVenomApply(fire, cb);
else if (fire.skill === 'venomAll') this._animateVenomApplyAll(fire, cb);
else if (fire.skill === 'smite') this._animateSmiteApply(fire, cb);
else if (fire.skill === 'smiteAll') this._animateSmiteApplyAll(fire, cb);
else if (fire.skill === 'molt') this._animateMolt(fire, cb);
else cb(); else cb();
}; };
nextFire(0); nextFire(0);