Implement hacked Berserk and Flurry skills with animations

- CombatEngine:
  - Handle hacked `berserk` by adding `hackSkill.value` to attacker's current attack and logging the event.
  - Handle hacked `flurry` by injecting a temporary passive skill (+1 extra attack) that is removed after the attack resolves.
- BattleScene:
  - Include `smiteTick` in pre-battle event filtering.
  - Add `smiteApply` and `smiteApplyAll` to on-attack-all event tracking.
  - Increase smite projectile size (sprite 38) from 80x80 to 180x180.
  - Enhance smite damage animation: pulse sprite 39 twice over 1s, synchronize with HP loss, and enforce minimum timing before proceeding or playing death/hive-link animations.
  - Add visual and text feedback for `berserk` (animate attack gain) and `flurry` (status text) in fire animations.
This commit is contained in:
Brian Fertig 2026-03-21 20:00:46 -06:00
parent 82f871abfb
commit ff4a7eebf0
2 changed files with 55 additions and 17 deletions

View File

@ -919,6 +919,15 @@ export class CombatEngine {
if (copied.name === 'molt' && ctx2.moltHeal > 0) {
hackFires.push({ skill: 'molt', card: pending.attacker, heal: ctx2.moltHeal, armorLost: ctx2.moltArmorLost, isHacked: true });
}
if (copied.name === 'berserk') {
pending.attacker.currentAttack += hackSkill.value;
hackFires.push({ skill: 'berserk', card: pending.attacker, gain: hackSkill.value, isHacked: true });
}
if (copied.name === 'flurry') {
// Cap hacked flurry at 1 extra attack; inject a temporary skill so _performAttack picks it up
pending.attacker.skills.push({ name: 'flurry', trigger: 'passive', value: 1, _hackedSkill: true });
hackFires.push({ skill: 'flurry', card: pending.attacker, isHacked: true });
}
}
if (hackFires.length > 0) {
preAttackFires.push({ skill: 'hack', source: pending.attacker, copiedFrom: pending.target, fires: hackFires });
@ -972,6 +981,9 @@ export class CombatEngine {
pending.alliedLanes, pending.enemyLanes, pending.side
);
// Remove any skills temporarily injected by hack (e.g. hacked flurry)
pending.attacker.skills = pending.attacker.skills.filter(s => !s._hackedSkill);
// postAttack event — fires after the attack animation resolves.
// Only references cards still alive so skill handlers don't need to guard.
// pierceRestores: armor that was temporarily reduced by pierce — restore if target survived.

View File

@ -315,7 +315,7 @@ export class BattleScene extends Phaser.Scene {
// Deploy + pre-attack phases (NO damage applied yet)
// On player-first turns, beginCommit also deploys the AI card
const commitEvents = this.engine.beginCommit(chosenCard);
const preBattleEvents = commitEvents.filter(e => e.type === 'preBattle' || e.type === 'venomTick' || e.type === 'carapace' || e.type === 'molt' || e.type === 'hive_link');
const preBattleEvents = commitEvents.filter(e => e.type === 'preBattle' || e.type === 'venomTick' || e.type === 'smiteTick' || e.type === 'carapace' || e.type === 'molt' || e.type === 'hive_link');
// Build preBattle hive_link map for venom kills: dead card instanceId → hive_link event
this._preBattleHiveLinks = {};
@ -424,7 +424,7 @@ export class BattleScene extends Phaser.Scene {
} else if (currentRound) {
if (e.type === 'berserk') currentRound.berserkEvent = e;
else if (e.type === 'counter') currentRound.counterEvent = e;
else if (['ruptureAll', 'weakenAll', 'venomApply', 'venomApplyAll', 'carapace'].includes(e.type))
else if (['ruptureAll', 'weakenAll', 'venomApply', 'venomApplyAll', 'smiteApply', 'smiteApplyAll', 'carapace'].includes(e.type))
currentRound.onAttackAllEvents.push(e);
}
}
@ -3927,7 +3927,7 @@ export class BattleScene extends Phaser.Scene {
// 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)
.setDisplaySize(180, 180)
.setDepth(30);
this.tweens.add({
targets: projectile,
@ -3981,6 +3981,7 @@ export class BattleScene extends Phaser.Scene {
this.statusText.setText(`${event.card.name} suffers ${event.damage} smite damage!`);
this.sound.play('sfx_smite_damage', { volume: 0.8 });
// Sprite 39 pulses twice (250ms × 4 = 1000ms total) over the card
const effectSprite = this.add.sprite(obj.x, obj.y, 'attacks', 39)
.setDisplaySize(140, 140)
.setDepth(30)
@ -3995,24 +3996,38 @@ export class BattleScene extends Phaser.Scene {
onComplete: () => { if (effectSprite.scene) effectSprite.destroy(); }
});
if (event.damage > 0 && obj.animateHPLoss) {
obj.animateHPLoss(event.damage, () => {
if (event.killed) {
const hlEvent = this._preBattleHiveLinks?.[event.card.instanceId];
if (hlEvent) delete this._preBattleHiveLinks[event.card.instanceId];
// Latch: wait for both HP animation AND the 1s minimum before continuing
let spriteHeld = true;
let hpDone = false;
const tryFinish = () => {
if (!spriteHeld && hpDone) {
if (obj.scene) obj.refresh();
onComplete();
}
};
this.time.delayedCall(1000, () => { spriteHeld = false; tryFinish(); });
const afterHP = () => {
if (event.killed) {
const hlEvent = this._preBattleHiveLinks?.[event.card.instanceId];
if (hlEvent) delete this._preBattleHiveLinks[event.card.instanceId];
// For kills wait out the full 1s, then play death
this.time.delayedCall(1000, () => {
this._playCardDeath(obj, event.card,
hlEvent ? () => this._animateHiveLink(hlEvent, onComplete) : onComplete
);
} else {
if (obj.scene) obj.refresh();
onComplete();
}
});
});
} else {
hpDone = true;
tryFinish();
}
};
if (event.damage > 0 && obj.animateHPLoss) {
obj.animateHPLoss(event.damage, afterHP);
} else {
this.time.delayedCall(550, () => {
if (obj.scene) obj.refresh();
onComplete();
});
hpDone = true;
tryFinish();
}
}
@ -4332,6 +4347,17 @@ export class BattleScene extends Phaser.Scene {
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 if (fire.skill === 'berserk') {
const obj = this.cardObjects.get(fire.card.instanceId) || this.commanderObjects?.get(fire.card.instanceId);
if (obj?.scene) {
this.statusText.setText(`${fire.card.name} goes berserk! +${fire.gain} ATK`);
obj.animateBerserkGain(fire.gain, () => { if (obj.scene) obj.refresh(); cb(); });
} else cb();
}
else if (fire.skill === 'flurry') {
this.statusText.setText(`${fire.card.name} gains flurry! +1 extra attack`);
this.time.delayedCall(600, cb);
}
else cb();
};
nextFire(0);