feat: add new Raider commanders, units, and Rupture/Weaken skill systems
- Added two new legendary Raider commanders: Warboss Kragath (Weaken + Strike) and Scar-Queen Vex (Enfeeble + Rupture). - Introduced new Raider units across all rarities: Brute, Cutpurse, Berserker, Cutthroat, Marauder, Scavenger, Defiler, Warlord, Reaver, Warband Breaker, Carnage, Overlord, Ravager, Iron Destroyer, and Skulltaker. - Implemented full Rupture mechanics: - Added `ruptureStacks` tracking and pre-battle damage phase that can kill units. - New `rupture` skill logic with single-target and `all` variants, including burrow immunity. - Visuals: rupture stack badge on cards, knife throw animations for application, and pulsing heart effect for tick damage. - Audio: new SFX for rupture cast and damage. - Implemented Weaken mechanics: - Reduces target Armor and Attack (split 50/50) with clamping to non-negative values. - Added `weakenFires` event tracking and pre-battle animation with stat reduction visuals. - Audio: new SFX for weaken cast. - Updated missions to feature new commanders and units, and increased gold rewards. - Updated `attacks.png` and `attacks.psd` spritesheets with new effect frames (knife, heart). - Updated `data/cards.json` and `data/missions.json` with new entries and rebalanced values.
|
After Width: | Height: | Size: 691 KiB |
|
After Width: | Height: | Size: 618 KiB |
|
After Width: | Height: | Size: 637 KiB |
|
After Width: | Height: | Size: 650 KiB |
|
After Width: | Height: | Size: 624 KiB |
|
After Width: | Height: | Size: 644 KiB |
|
After Width: | Height: | Size: 670 KiB |
|
After Width: | Height: | Size: 580 KiB |
|
After Width: | Height: | Size: 639 KiB |
|
After Width: | Height: | Size: 696 KiB |
|
After Width: | Height: | Size: 642 KiB |
|
Before Width: | Height: | Size: 7.9 MiB After Width: | Height: | Size: 8.2 MiB |
1101
data/cards.json
|
|
@ -12,7 +12,7 @@
|
|||
"cards": [
|
||||
"raider_grunt_1",
|
||||
"raider_grunt_1",
|
||||
"raider_grunt_1",
|
||||
"raider_brute_1",
|
||||
"raider_scout_1",
|
||||
"raider_scout_1"
|
||||
]
|
||||
|
|
@ -38,7 +38,7 @@
|
|||
"cards": [
|
||||
"raider_grunt_1",
|
||||
"raider_grunt_1",
|
||||
"raider_scout_1",
|
||||
"raider_cutpurse_1",
|
||||
"raider_scout_1",
|
||||
"raider_pillager_1",
|
||||
"raider_pillager_1",
|
||||
|
|
@ -62,14 +62,14 @@
|
|||
"description": "Raiders have breached the outer wall. Repel the invasion!",
|
||||
"unlockCondition": "mission_002",
|
||||
"opponent": {
|
||||
"commander": "raider_cmd_1",
|
||||
"commander": "raider_cmd_2",
|
||||
"cards": [
|
||||
"raider_grunt_1",
|
||||
"raider_grunt_1",
|
||||
"raider_pillager_1",
|
||||
"raider_pillager_1",
|
||||
"raider_scout_1",
|
||||
"raider_cutthroat_1",
|
||||
"raider_scavenger_1",
|
||||
"raider_berserker_1"
|
||||
]
|
||||
},
|
||||
|
|
@ -90,13 +90,13 @@
|
|||
"description": "An Imperial patrol walks into a deadly trap. Fight your way out!",
|
||||
"unlockCondition": "mission_003",
|
||||
"opponent": {
|
||||
"commander": "raider_cmd_1",
|
||||
"commander": "raider_cmd_2",
|
||||
"cards": [
|
||||
"raider_grunt_1",
|
||||
"raider_pillager_1",
|
||||
"raider_berserker_1",
|
||||
"raider_cutthroat_1",
|
||||
"raider_marauder_1",
|
||||
"raider_defiler_1",
|
||||
"raider_scout_1"
|
||||
]
|
||||
},
|
||||
|
|
@ -117,12 +117,12 @@
|
|||
"description": "The Khan's elite vanguard pushes deep into Imperial territory. Stop them at all costs!",
|
||||
"unlockCondition": "mission_004",
|
||||
"opponent": {
|
||||
"commander": "raider_cmd_1",
|
||||
"commander": "raider_cmd_3",
|
||||
"cards": [
|
||||
"raider_grunt_1",
|
||||
"raider_berserker_1",
|
||||
"raider_cutthroat_1",
|
||||
"raider_warlord_1",
|
||||
"raider_warbandbreaker_1",
|
||||
"raider_marauder_1",
|
||||
"raider_scout_1"
|
||||
]
|
||||
|
|
@ -156,7 +156,7 @@
|
|||
]
|
||||
},
|
||||
"rewards": {
|
||||
"gold": 150,
|
||||
"gold": 400,
|
||||
"cards": [
|
||||
"bt_thrall_1"
|
||||
]
|
||||
|
|
@ -184,7 +184,7 @@
|
|||
]
|
||||
},
|
||||
"rewards": {
|
||||
"gold": 200,
|
||||
"gold": 500,
|
||||
"cards": [
|
||||
"bt_leech_1"
|
||||
]
|
||||
|
|
@ -212,7 +212,7 @@
|
|||
]
|
||||
},
|
||||
"rewards": {
|
||||
"gold": 250,
|
||||
"gold": 600,
|
||||
"cards": [
|
||||
"bt_initiate_1"
|
||||
]
|
||||
|
|
@ -240,7 +240,7 @@
|
|||
]
|
||||
},
|
||||
"rewards": {
|
||||
"gold": 300,
|
||||
"gold": 700,
|
||||
"cards": [
|
||||
"bt_ghoul_1"
|
||||
]
|
||||
|
|
@ -268,7 +268,7 @@
|
|||
]
|
||||
},
|
||||
"rewards": {
|
||||
"gold": 350,
|
||||
"gold": 800,
|
||||
"cards": [
|
||||
"bt_revenant_1"
|
||||
]
|
||||
|
|
@ -297,7 +297,7 @@
|
|||
]
|
||||
},
|
||||
"rewards": {
|
||||
"gold": 400,
|
||||
"gold": 900,
|
||||
"cards": [
|
||||
"bt_bloodmage_1"
|
||||
]
|
||||
|
|
@ -326,7 +326,7 @@
|
|||
]
|
||||
},
|
||||
"rewards": {
|
||||
"gold": 450,
|
||||
"gold": 1000,
|
||||
"cards": [
|
||||
"bt_harbinger_1"
|
||||
]
|
||||
|
|
@ -355,7 +355,7 @@
|
|||
]
|
||||
},
|
||||
"rewards": {
|
||||
"gold": 500,
|
||||
"gold": 1100,
|
||||
"cards": [
|
||||
"bt_bloodlord_1"
|
||||
]
|
||||
|
|
|
|||
|
|
@ -71,20 +71,28 @@ export class CombatEngine {
|
|||
tick(this.opponentLanes);
|
||||
}
|
||||
|
||||
// Apply rupture damage to all cards
|
||||
// Apply rupture damage to all cards, emitting ruptureTick events (mirrors venomPhase)
|
||||
_rupturePhase() {
|
||||
const applyRupture = cards => {
|
||||
const applyRupture = (cards, alliedLanes, side) => {
|
||||
for (const c of cards) {
|
||||
if (c.ruptureStacks > 0) {
|
||||
const dmg = c.ruptureStacks;
|
||||
c.currentHP -= dmg;
|
||||
this._log(`${c.name} takes ${dmg} rupture damage`);
|
||||
const killed = c.currentHP <= 0;
|
||||
if (killed) {
|
||||
this._log(`${c.name} is destroyed by rupture`);
|
||||
this.events.push({ type: 'death', card: c, side });
|
||||
this._processOnDeath(c, alliedLanes, side);
|
||||
}
|
||||
this.events.push({ type: 'ruptureTick', card: c, damage: dmg, killed });
|
||||
this._tryCarapace(c, dmg);
|
||||
c.ruptureStacks--;
|
||||
}
|
||||
}
|
||||
};
|
||||
applyRupture(this.playerLanes);
|
||||
applyRupture(this.opponentLanes);
|
||||
applyRupture(this.playerLanes, this.playerLanes, 'player');
|
||||
applyRupture(this.opponentLanes, this.opponentLanes, 'opponent');
|
||||
}
|
||||
|
||||
// If the card has carapace and just took damage, apply armor gain (with cap).
|
||||
|
|
@ -310,6 +318,9 @@ export class CombatEngine {
|
|||
if (onAtkCtx.ruptureAllTargets) {
|
||||
this.events.push({ type: 'ruptureAll', attacker, targets: onAtkCtx.ruptureAllTargets, side });
|
||||
}
|
||||
if (onAtkCtx.ruptureTarget) {
|
||||
this.events.push({ type: 'ruptureApply', attacker, target: onAtkCtx.ruptureTarget, stacks: onAtkCtx.ruptureStacks, side });
|
||||
}
|
||||
if (onAtkCtx.weakenAllTargets) {
|
||||
this.events.push({ type: 'weakenAll', attacker, targets: onAtkCtx.weakenAllTargets, side });
|
||||
}
|
||||
|
|
@ -511,10 +522,8 @@ export class CombatEngine {
|
|||
for (const c of this.ai.hand) { if (c.currentDelay > 0) c.currentDelay--; }
|
||||
this._commanderSkillPhase();
|
||||
this._cardOnTurnStartPhase();
|
||||
this._rupturePhase();
|
||||
this._deathCheck(); // remove cards killed by rupture before combat
|
||||
this._jamPhase();
|
||||
this._preBattlePhase(); // _venomPhase() is called first inside _preBattlePhase
|
||||
this._preBattlePhase(); // _rupturePhase() and _venomPhase() are called first inside _preBattlePhase
|
||||
// NOTE: No _deathCheck() here — dead cards stay in lanes so the scene can
|
||||
// animate their death during preBattle (e.g. drain kills). _buildPendingAttacks
|
||||
// already skips dead cards via currentHP <= 0 checks. finalizeCommit() calls
|
||||
|
|
@ -531,6 +540,7 @@ export class CombatEngine {
|
|||
const protectFires = [];
|
||||
const enfeebeFires = [];
|
||||
const jamFires = [];
|
||||
const weakenFires = [];
|
||||
const drainFires = [];
|
||||
const healFires = [];
|
||||
const sanctifyFires = [];
|
||||
|
|
@ -590,6 +600,12 @@ export class CombatEngine {
|
|||
this._log(`${card.name} jam suppresses ${t.card.name} skills by ${t.reductions.map(r => r.amount).join('/')}`);
|
||||
}
|
||||
}
|
||||
if (s.name === 'weaken' && ctx.weakenFires?.length) {
|
||||
for (const wf of ctx.weakenFires) {
|
||||
weakenFires.push({ skill: 'weaken', source: wf.source, target: wf.target, armorAmount: wf.armorAmount, attackAmount: wf.attackAmount });
|
||||
this._log(`${card.name} weaken reduces ${wf.target.name} ARM by ${wf.armorAmount} and ATK by ${wf.attackAmount}`);
|
||||
}
|
||||
}
|
||||
if (s.name === 'drain' && ctx.drainTarget) {
|
||||
const hpBefore = ctx.drainTarget.currentHP + ctx.drainDamage;
|
||||
const targetIsCommander = ctx.drainTarget === enemyCommander;
|
||||
|
|
@ -662,12 +678,13 @@ export class CombatEngine {
|
|||
}
|
||||
}
|
||||
}
|
||||
return { buffs, siegeFires, protectFires, enfeebeFires, jamFires, drainFires, healFires, sanctifyFires, overchargeFires, fortifyFires };
|
||||
return { buffs, siegeFires, protectFires, enfeebeFires, jamFires, weakenFires, drainFires, healFires, sanctifyFires, overchargeFires, fortifyFires };
|
||||
}
|
||||
|
||||
// Emit the 8-step pre-battle sequence and process preBattle skills.
|
||||
_preBattlePhase() {
|
||||
// Venom/smite ticks first, then molt heals (both before buff skills).
|
||||
// Rupture/venom/smite ticks first, then molt heals (both before buff skills).
|
||||
this._rupturePhase();
|
||||
this._venomPhase();
|
||||
this._smitePhase();
|
||||
this._moltPhase();
|
||||
|
|
|
|||
|
|
@ -74,9 +74,8 @@ export class SkillProcessor {
|
|||
}
|
||||
|
||||
rupture(skill, attacker, defender, allies, enemies, context) {
|
||||
if (!skill.all && defender?.burrowTurns > 0) return;
|
||||
if (skill.all) {
|
||||
const targets = enemies.filter(e => e.currentHP > 0);
|
||||
const targets = enemies.filter(e => e.currentHP > 0 && !(e.burrowTurns > 0));
|
||||
if (targets.length === 0) return;
|
||||
const allTargets = [];
|
||||
for (const t of targets) {
|
||||
|
|
@ -86,8 +85,10 @@ export class SkillProcessor {
|
|||
if (context) context.ruptureAllTargets = allTargets;
|
||||
return;
|
||||
}
|
||||
if (!defender) return;
|
||||
if (!defender || defender.currentHP <= 0) return;
|
||||
if (defender.burrowTurns > 0) return;
|
||||
defender.ruptureStacks = (defender.ruptureStacks || 0) + skill.value;
|
||||
if (context) { context.ruptureTarget = defender; context.ruptureStacks = skill.value; }
|
||||
}
|
||||
|
||||
berserk(skill, attacker, defender, allies, enemies, context) {
|
||||
|
|
@ -273,24 +274,47 @@ export class SkillProcessor {
|
|||
}
|
||||
|
||||
weaken(skill, attacker, defender, allies, enemies, context) {
|
||||
if (!skill.all && defender?.burrowTurns > 0) return;
|
||||
if (skill.all) {
|
||||
const targets = enemies.filter(e => e.currentHP > 0);
|
||||
if (targets.length === 0) return;
|
||||
const allTargets = [];
|
||||
for (const t of targets) {
|
||||
for (const s of t.skills) {
|
||||
s.value = Math.max(0, (s.value || 0) - skill.value);
|
||||
}
|
||||
allTargets.push({ target: t });
|
||||
const laneCards = context?.laneCards || [];
|
||||
const enemyLaneCards = context?.enemyLaneCards || [];
|
||||
const armorReduction = Math.ceil(skill.value / 2);
|
||||
const attackReduction = Math.floor(skill.value / 2);
|
||||
|
||||
const _applyWeaken = (target) => {
|
||||
const armorAmt = Math.min(armorReduction, Math.max(0, target.currentArmor));
|
||||
const attackAmt = Math.min(attackReduction, Math.max(0, target.currentAttack));
|
||||
if (armorAmt > 0) {
|
||||
target.currentArmor -= armorAmt;
|
||||
target._tempBuffs = target._tempBuffs || [];
|
||||
target._tempBuffs.push({ stat: 'currentArmor', amount: -armorAmt, source: 'weaken' });
|
||||
}
|
||||
if (context) context.weakenAllTargets = allTargets;
|
||||
if (attackAmt > 0) {
|
||||
target.currentAttack -= attackAmt;
|
||||
target._tempBuffs = target._tempBuffs || [];
|
||||
target._tempBuffs.push({ stat: 'currentAttack', amount: -attackAmt, source: 'weaken' });
|
||||
}
|
||||
return { armorAmt, attackAmt };
|
||||
};
|
||||
|
||||
const weakenFires = context?.weakenFires || [];
|
||||
|
||||
if (skill.all) {
|
||||
const targets = enemyLaneCards.filter(t => t?.currentHP > 0 && !(t.burrowTurns > 0));
|
||||
if (targets.length === 0) return;
|
||||
for (const t of targets) {
|
||||
const { armorAmt, attackAmt } = _applyWeaken(t);
|
||||
weakenFires.push({ source: attacker, target: t, armorAmount: armorAmt, attackAmount: attackAmt });
|
||||
}
|
||||
if (context) context.weakenFires = weakenFires;
|
||||
return;
|
||||
}
|
||||
if (!defender) return;
|
||||
for (const s of defender.skills) {
|
||||
s.value = Math.max(0, (s.value || 0) - skill.value);
|
||||
}
|
||||
|
||||
const idx = laneCards.indexOf(attacker);
|
||||
if (idx < 0 || idx >= enemyLaneCards.length) return;
|
||||
const target = enemyLaneCards[idx];
|
||||
if (!target || target.currentHP <= 0 || target.burrowTurns > 0) return;
|
||||
const { armorAmt, attackAmt } = _applyWeaken(target);
|
||||
weakenFires.push({ source: attacker, target, armorAmount: armorAmt, attackAmount: attackAmt });
|
||||
if (context) context.weakenFires = weakenFires;
|
||||
}
|
||||
|
||||
tribute(skill, attacker, defender, allies, enemies, context) {
|
||||
|
|
|
|||
|
|
@ -154,6 +154,22 @@ export class CardObject extends Phaser.GameObjects.Container {
|
|||
this.add(this.burrowBadge);
|
||||
this._updateBurrowBadge();
|
||||
|
||||
// Rupture stack indicator (below burrow badge, red)
|
||||
this.ruptureBadge = this.scene.add.text(
|
||||
w / 2 - Math.round(4 * scale),
|
||||
-h / 2 + bannerH + Math.round(45 * scale),
|
||||
'',
|
||||
{
|
||||
fontSize: fs(7), color: '#ff4444', fontStyle: 'bold',
|
||||
backgroundColor: '#000000aa',
|
||||
padding: { x: Math.round(2 * scale), y: Math.round(1 * scale) },
|
||||
fontFamily: 'Audiowide'
|
||||
}
|
||||
).setOrigin(1, 0);
|
||||
this.ruptureBadge.setVisible(false);
|
||||
this.add(this.ruptureBadge);
|
||||
this._updateRuptureBadge();
|
||||
|
||||
// Flash overlay — transparent red rect; .bg is targeted by _animateAttack
|
||||
this.bg = this.scene.add.rectangle(0, imageCY, w, imageH, 0xff2200, 0);
|
||||
this.add(this.bg);
|
||||
|
|
@ -272,6 +288,17 @@ export class CardObject extends Phaser.GameObjects.Container {
|
|||
}
|
||||
}
|
||||
|
||||
_updateRuptureBadge() {
|
||||
if (!this.ruptureBadge) return;
|
||||
const stacks = this.cardData.ruptureStacks || 0;
|
||||
if (stacks > 0) {
|
||||
this.ruptureBadge.setText(`⚔${stacks}`);
|
||||
this.ruptureBadge.setVisible(true);
|
||||
} else {
|
||||
this.ruptureBadge.setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
refresh() {
|
||||
const w = this.options.width || 80;
|
||||
const scale = w / 80;
|
||||
|
|
@ -294,6 +321,7 @@ export class CardObject extends Phaser.GameObjects.Container {
|
|||
this._updateVenomBadge();
|
||||
this._updateSmiteBadge();
|
||||
this._updateBurrowBadge();
|
||||
this._updateRuptureBadge();
|
||||
if (this.skillText && this.cardData.skills?.length) {
|
||||
const skillStr = this.cardData.skills
|
||||
.map(s => {
|
||||
|
|
|
|||
|
|
@ -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 === 'smiteTick' || e.type === 'carapace' || e.type === 'molt' || e.type === 'hive_link');
|
||||
const preBattleEvents = commitEvents.filter(e => e.type === 'preBattle' || e.type === 'ruptureTick' || 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', 'smiteApply', 'smiteApplyAll', 'carapace'].includes(e.type))
|
||||
else if (['ruptureAll', 'ruptureApply', 'weakenAll', 'venomApply', 'venomApplyAll', 'smiteApply', 'smiteApplyAll', 'carapace'].includes(e.type))
|
||||
currentRound.onAttackAllEvents.push(e);
|
||||
}
|
||||
}
|
||||
|
|
@ -520,6 +520,7 @@ export class BattleScene extends Phaser.Scene {
|
|||
const event = events[idx];
|
||||
const cb = () => next(idx + 1);
|
||||
if (event.type === 'ruptureAll') this._animateRuptureAll(event, cb);
|
||||
else if (event.type === 'ruptureApply') this._animateRuptureApply(event, cb);
|
||||
else if (event.type === 'weakenAll') this._animateWeakenAll(event, cb);
|
||||
else if (event.type === 'venomApply') this._animateVenomApply(event, cb);
|
||||
else if (event.type === 'venomApplyAll') this._animateVenomApplyAll(event, cb);
|
||||
|
|
@ -534,21 +535,143 @@ export class BattleScene extends Phaser.Scene {
|
|||
|
||||
_animateRuptureAll(event, onComplete) {
|
||||
if (!event.targets?.length) { onComplete(); return; }
|
||||
const _lookup = id => id && (this.cardObjects.get(id) || this.commanderObjects?.get(id));
|
||||
const sourceObj = _lookup(event.attacker.instanceId);
|
||||
this.statusText.setText(`${event.attacker.name} ruptures all enemies!`);
|
||||
this.sound.play('sfx_rupture', { volume: 0.8 });
|
||||
|
||||
let remaining = event.targets.length;
|
||||
const done = () => {
|
||||
if (--remaining <= 0) onComplete();
|
||||
};
|
||||
const done = () => { if (--remaining <= 0) onComplete(); };
|
||||
|
||||
for (const t of event.targets) {
|
||||
const obj = this.cardObjects.get(t.target.instanceId) || this.commanderObjects?.get(t.target.instanceId);
|
||||
if (!obj?.scene) { done(); continue; }
|
||||
obj.refresh();
|
||||
// Flash the card red to indicate rupture
|
||||
obj.flash(0xff0000);
|
||||
this.time.delayedCall(400, done);
|
||||
const targetObj = _lookup(t.target.instanceId);
|
||||
if (!targetObj?.scene) { done(); continue; }
|
||||
|
||||
if (!sourceObj?.scene) {
|
||||
targetObj.refresh();
|
||||
this._showRuptureStackGain(targetObj, t.stacks);
|
||||
this.time.delayedCall(400, done);
|
||||
continue;
|
||||
}
|
||||
|
||||
const rot = Math.atan2(targetObj.y - sourceObj.y, targetObj.x - sourceObj.x) + Math.PI / 2;
|
||||
const knife = this.add.sprite(sourceObj.x, sourceObj.y, 'attacks', 44)
|
||||
.setDisplaySize(80, 80)
|
||||
.setRotation(rot)
|
||||
.setDepth(30);
|
||||
|
||||
this.tweens.add({
|
||||
targets: knife,
|
||||
x: targetObj.x,
|
||||
y: targetObj.y,
|
||||
duration: 2000,
|
||||
ease: 'Quad.In',
|
||||
onComplete: () => {
|
||||
if (knife.scene) knife.destroy();
|
||||
if (targetObj.scene) {
|
||||
targetObj.refresh();
|
||||
this._showRuptureStackGain(targetObj, t.stacks);
|
||||
}
|
||||
done();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_animateRuptureApply(event, onComplete) {
|
||||
const _lookup = id => id && (this.cardObjects.get(id) || this.commanderObjects?.get(id));
|
||||
const sourceObj = _lookup(event.attacker.instanceId);
|
||||
const targetObj = _lookup(event.target.instanceId);
|
||||
if (!targetObj?.scene) { onComplete(); return; }
|
||||
|
||||
this.statusText.setText(`${event.attacker.name} ruptures ${event.target.name}! [${event.stacks} stacks]`);
|
||||
this.sound.play('sfx_rupture', { volume: 0.8 });
|
||||
|
||||
if (!sourceObj?.scene) {
|
||||
targetObj.refresh();
|
||||
this._showRuptureStackGain(targetObj, event.stacks);
|
||||
this.time.delayedCall(400, onComplete);
|
||||
return;
|
||||
}
|
||||
|
||||
const rot = Math.atan2(targetObj.y - sourceObj.y, targetObj.x - sourceObj.x) + Math.PI / 2;
|
||||
const knife = this.add.sprite(sourceObj.x, sourceObj.y, 'attacks', 44)
|
||||
.setDisplaySize(250, 250)
|
||||
.setRotation(rot)
|
||||
.setDepth(30);
|
||||
|
||||
this.tweens.add({
|
||||
targets: knife,
|
||||
x: targetObj.x,
|
||||
y: targetObj.y,
|
||||
duration: 800,
|
||||
ease: 'Quad.In',
|
||||
onComplete: () => {
|
||||
if (knife.scene) knife.destroy();
|
||||
if (!targetObj.scene) { onComplete(); return; }
|
||||
targetObj.refresh();
|
||||
this._showRuptureStackGain(targetObj, event.stacks);
|
||||
this.time.delayedCall(400, onComplete);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_animateRuptureTick(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} rupture damage!`);
|
||||
this.sound.play('sfx_rupture_effect', { volume: 0.8 });
|
||||
|
||||
// Sprite 45 (heart) pulses larger/smaller over the card for ~2 seconds
|
||||
const effectSprite = this.add.sprite(obj.x, obj.y, 'attacks', 45)
|
||||
.setDisplaySize(140, 140)
|
||||
.setDepth(30)
|
||||
.setAlpha(0.9);
|
||||
this.tweens.add({
|
||||
targets: effectSprite,
|
||||
scaleX: { from: effectSprite.scaleX * 0.8, to: effectSprite.scaleX * 1.5 },
|
||||
scaleY: { from: effectSprite.scaleY * 0.8, to: effectSprite.scaleY * 1.5 },
|
||||
alpha: { from: 0.9, to: 0.2 },
|
||||
duration: 500,
|
||||
yoyo: true,
|
||||
repeat: 1,
|
||||
onComplete: () => { if (effectSprite.scene) effectSprite.destroy(); }
|
||||
});
|
||||
|
||||
if (event.damage > 0 && obj.animateHPLoss) {
|
||||
obj.animateHPLoss(event.damage, null);
|
||||
}
|
||||
|
||||
// Wait for sprite to finish (~2 seconds), then handle kill/complete
|
||||
this.time.delayedCall(2000, () => {
|
||||
if (event.killed) {
|
||||
const hlEvent = this._preBattleHiveLinks?.[event.card.instanceId];
|
||||
if (hlEvent) delete this._preBattleHiveLinks[event.card.instanceId];
|
||||
this._playCardDeath(obj, event.card,
|
||||
hlEvent ? () => this._animateHiveLink(hlEvent, onComplete) : onComplete
|
||||
);
|
||||
} else {
|
||||
if (obj.scene) obj.refresh();
|
||||
onComplete();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_showRuptureStackGain(targetObj, stacks) {
|
||||
if (!targetObj?.scene || stacks <= 0) return;
|
||||
const gainText = this.add.text(targetObj.x, targetObj.y - 20, `+${stacks} rupture`, {
|
||||
fontSize: '12px', color: '#ff4444', fontStyle: 'bold',
|
||||
stroke: '#000000', strokeThickness: 3, fontFamily: 'Audiowide'
|
||||
}).setOrigin(0.5).setDepth(35);
|
||||
this.tweens.add({
|
||||
targets: gainText,
|
||||
alpha: 0, y: targetObj.y - 55,
|
||||
duration: 800, ease: 'Power2',
|
||||
onComplete: () => { if (gainText.scene) gainText.destroy(); }
|
||||
});
|
||||
}
|
||||
|
||||
// ── Heal preBattle animations ──────────────────────────────────────────────
|
||||
|
||||
_processHealFires(fires, onComplete) {
|
||||
|
|
@ -2094,6 +2217,10 @@ export class BattleScene extends Phaser.Scene {
|
|||
_processPreBattle(events, onComplete) {
|
||||
const processStep = (idx) => {
|
||||
if (idx >= events.length) { onComplete(); return; }
|
||||
if (events[idx].type === 'ruptureTick') {
|
||||
this._animateRuptureTick(events[idx], () => processStep(idx + 1));
|
||||
return;
|
||||
}
|
||||
if (events[idx].type === 'venomTick') {
|
||||
this._animateVenomTick(events[idx], () => processStep(idx + 1));
|
||||
return;
|
||||
|
|
@ -2127,13 +2254,14 @@ export class BattleScene extends Phaser.Scene {
|
|||
const hasProtect = event.protectFires?.length > 0;
|
||||
const hasEnfeeble = event.enfeebeFires?.length > 0;
|
||||
const hasJam = event.jamFires?.length > 0;
|
||||
const hasWeaken = event.weakenFires?.length > 0;
|
||||
const hasDrain = event.drainFires?.length > 0;
|
||||
const hasHeal = event.healFires?.length > 0;
|
||||
const hasSanctify = event.sanctifyFires?.length > 0;
|
||||
const hasOvercharge = event.overchargeFires?.length > 0;
|
||||
const hasFortify = event.fortifyFires?.length > 0;
|
||||
|
||||
if (hasBuffs || hasSiege || hasProtect || hasEnfeeble || hasJam || hasDrain || hasHeal || hasSanctify || hasOvercharge || hasFortify) {
|
||||
if (hasBuffs || hasSiege || hasProtect || hasEnfeeble || hasJam || hasWeaken || hasDrain || hasHeal || hasSanctify || hasOvercharge || hasFortify) {
|
||||
this._processSanctifyFires(event.sanctifyFires || [], () => {
|
||||
this._processDrainFires(event.drainFires || [], () => {
|
||||
this._processOverchargeFires(event.overchargeFires || [], () => {
|
||||
|
|
@ -2143,7 +2271,9 @@ export class BattleScene extends Phaser.Scene {
|
|||
this._processProtectFires(event.protectFires || [], () => {
|
||||
this._processEnfeebeFires(event.enfeebeFires || [], () => {
|
||||
this._processJamFires(event.jamFires || [], () => {
|
||||
this._processHealFires(event.healFires || [], onComplete);
|
||||
this._processWeakenFires(event.weakenFires || [], () => {
|
||||
this._processHealFires(event.healFires || [], onComplete);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -2209,6 +2339,14 @@ export class BattleScene extends Phaser.Scene {
|
|||
next(0);
|
||||
}
|
||||
|
||||
_processWeakenFires(fires, onComplete) {
|
||||
const next = (idx) => {
|
||||
if (idx >= fires.length) { onComplete(); return; }
|
||||
this._animateWeakenFire(fires[idx], () => next(idx + 1));
|
||||
};
|
||||
next(0);
|
||||
}
|
||||
|
||||
_processBloodpactFires(fires, onComplete) {
|
||||
const next = (idx) => {
|
||||
if (idx >= fires.length) { onComplete(); return; }
|
||||
|
|
@ -3384,6 +3522,63 @@ export class BattleScene extends Phaser.Scene {
|
|||
}
|
||||
}
|
||||
|
||||
_animateWeakenFire(weakenFire, onComplete) {
|
||||
const _lookup = id => id && (this.cardObjects.get(id) || this.commanderObjects?.get(id));
|
||||
const sourceObj = _lookup(weakenFire.source.instanceId);
|
||||
const targetObj = _lookup(weakenFire.target.instanceId);
|
||||
|
||||
if (!sourceObj?.scene || !targetObj?.scene) { onComplete(); return; }
|
||||
|
||||
const armorAmt = weakenFire.armorAmount;
|
||||
const attackAmt = weakenFire.attackAmount;
|
||||
if (armorAmt <= 0 && attackAmt <= 0) { onComplete(); return; }
|
||||
|
||||
this.statusText.setText(`${weakenFire.source.name} weakens ${weakenFire.target.name}!`);
|
||||
this.sound.play('sfx_weaken', { volume: 0.8 });
|
||||
|
||||
const BASE_SCALE = 160 / 460;
|
||||
const weakenSprite = this.add.sprite(sourceObj.x, sourceObj.y, 'attacks', 43)
|
||||
.setScale(BASE_SCALE)
|
||||
.setDepth(30);
|
||||
|
||||
this.tweens.add({
|
||||
targets: weakenSprite,
|
||||
x: targetObj.x,
|
||||
y: targetObj.y,
|
||||
duration: 1350,
|
||||
ease: 'Cubic.easeIn',
|
||||
onComplete: () => {
|
||||
if (!targetObj.scene) { onComplete(); return; }
|
||||
|
||||
this.tweens.add({
|
||||
targets: weakenSprite,
|
||||
alpha: 0,
|
||||
scaleX: BASE_SCALE * 1.4,
|
||||
scaleY: BASE_SCALE * 1.4,
|
||||
duration: 300,
|
||||
ease: 'Power2',
|
||||
onComplete: () => { if (weakenSprite.scene) weakenSprite.destroy(); }
|
||||
});
|
||||
|
||||
// Animate both stat reductions in parallel; wait for both before calling onComplete
|
||||
let pending = (armorAmt > 0 ? 1 : 0) + (attackAmt > 0 ? 1 : 0);
|
||||
if (pending === 0) { onComplete(); return; }
|
||||
const done = () => { if (--pending === 0) onComplete(); };
|
||||
|
||||
if (armorAmt > 0) {
|
||||
const fromARM = weakenFire.target.currentArmor + armorAmt;
|
||||
const toARM = weakenFire.target.currentArmor;
|
||||
targetObj.animateArmorLoss(armorAmt, done, fromARM, toARM);
|
||||
}
|
||||
if (attackAmt > 0) {
|
||||
const fromATK = weakenFire.target.currentAttack + attackAmt;
|
||||
const toATK = weakenFire.target.currentAttack;
|
||||
this._animateAttackLoss(targetObj, attackAmt, fromATK, toATK, done);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Re-apply jam reductions after _renderState() so combat math stays correct.
|
||||
_reapplyJamSkillReductions(preBattleEvents) {
|
||||
for (const event of preBattleEvents) {
|
||||
|
|
|
|||
|
|
@ -62,6 +62,9 @@ export class BootScene extends Phaser.Scene {
|
|||
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_weaken', 'assets/audio/fx/weaken.mp3');
|
||||
this.load.audio('sfx_rupture', 'assets/audio/fx/rupture.mp3');
|
||||
this.load.audio('sfx_rupture_effect', 'assets/audio/fx/rupture-effect.mp3');
|
||||
this.load.audio('sfx_fortify', 'assets/audio/fx/fortify.mp3');
|
||||
|
||||
this.load.audio('music_main_menu', 'assets/audio/music/main_menu.mp3');
|
||||
|
|
|
|||