refactor: rename "rupture" to "wound" and "sanctify" to "cleanse"

- Renamed skill name from rupture → wound throughout data/skills.json and data/cards.json (120+ occurrences)
- Updated all related logic in CombatEngine.js and SkillProcessor.js:
  * Phase renamed _rupturePhase → _woundPhase, event types updated
  * Tracker renamed ruptureTracker → woundTracker
  * Card property ruptureStacks → woundStacks
- Renamed skill name from sanctify → cleanse, changed trigger to postBattle and removed all-target support (now always cleanses all allies)
- Updated visual assets: badge text, sound keys (sfx_rupture → sfx_wound, sfx_sanctify → sfx_cleanse), status messages
- Adjusted strike skill values in cards.json (5→8, 7→14, 10→20, 15→30)
This commit is contained in:
Brian Fertig 2026-03-28 09:41:49 -06:00
parent 2ea8857f91
commit 2cfba2a893
8 changed files with 282 additions and 301 deletions

View File

@ -2233,7 +2233,7 @@
"trigger": "preBattle" "trigger": "preBattle"
}, },
{ {
"name": "rupture", "name": "wound",
"value": 2, "value": 2,
"trigger": "on_attack" "trigger": "on_attack"
} }
@ -2252,7 +2252,7 @@
"trigger": "preBattle" "trigger": "preBattle"
}, },
{ {
"name": "rupture", "name": "wound",
"value": 4, "value": 4,
"trigger": "on_attack" "trigger": "on_attack"
} }
@ -2271,7 +2271,7 @@
"trigger": "preBattle" "trigger": "preBattle"
}, },
{ {
"name": "rupture", "name": "wound",
"value": 6, "value": 6,
"trigger": "on_attack" "trigger": "on_attack"
} }
@ -2290,7 +2290,7 @@
"trigger": "preBattle" "trigger": "preBattle"
}, },
{ {
"name": "rupture", "name": "wound",
"value": 9, "value": 9,
"trigger": "on_attack" "trigger": "on_attack"
} }
@ -2309,7 +2309,7 @@
"trigger": "preBattle" "trigger": "preBattle"
}, },
{ {
"name": "rupture", "name": "wound",
"value": 13, "value": 13,
"trigger": "on_attack" "trigger": "on_attack"
} }
@ -3072,7 +3072,7 @@
"trigger": "preBattle" "trigger": "preBattle"
}, },
{ {
"name": "rupture", "name": "wound",
"value": 1, "value": 1,
"trigger": "on_attack" "trigger": "on_attack"
} }
@ -3090,7 +3090,7 @@
"trigger": "preBattle" "trigger": "preBattle"
}, },
{ {
"name": "rupture", "name": "wound",
"value": 2, "value": 2,
"trigger": "on_attack" "trigger": "on_attack"
} }
@ -3108,7 +3108,7 @@
"trigger": "preBattle" "trigger": "preBattle"
}, },
{ {
"name": "rupture", "name": "wound",
"value": 2, "value": 2,
"trigger": "on_attack" "trigger": "on_attack"
} }
@ -3126,7 +3126,7 @@
"trigger": "preBattle" "trigger": "preBattle"
}, },
{ {
"name": "rupture", "name": "wound",
"value": 3, "value": 3,
"trigger": "on_attack" "trigger": "on_attack"
} }
@ -3144,7 +3144,7 @@
"trigger": "preBattle" "trigger": "preBattle"
}, },
{ {
"name": "rupture", "name": "wound",
"value": 5, "value": 5,
"trigger": "on_attack" "trigger": "on_attack"
} }
@ -3456,7 +3456,7 @@
"trigger": "passive" "trigger": "passive"
}, },
{ {
"name": "rupture", "name": "wound",
"value": 2, "value": 2,
"trigger": "on_attack" "trigger": "on_attack"
} }
@ -3474,7 +3474,7 @@
"trigger": "passive" "trigger": "passive"
}, },
{ {
"name": "rupture", "name": "wound",
"value": 3, "value": 3,
"trigger": "on_attack" "trigger": "on_attack"
} }
@ -3492,7 +3492,7 @@
"trigger": "passive" "trigger": "passive"
}, },
{ {
"name": "rupture", "name": "wound",
"value": 5, "value": 5,
"trigger": "on_attack" "trigger": "on_attack"
} }
@ -3510,7 +3510,7 @@
"trigger": "passive" "trigger": "passive"
}, },
{ {
"name": "rupture", "name": "wound",
"value": 7, "value": 7,
"trigger": "on_attack" "trigger": "on_attack"
} }
@ -3528,7 +3528,7 @@
"trigger": "passive" "trigger": "passive"
}, },
{ {
"name": "rupture", "name": "wound",
"value": 10, "value": 10,
"trigger": "on_attack" "trigger": "on_attack"
} }
@ -3658,7 +3658,7 @@
"trigger": "preAttack" "trigger": "preAttack"
}, },
{ {
"name": "rupture", "name": "wound",
"value": 3, "value": 3,
"trigger": "on_attack" "trigger": "on_attack"
} }
@ -3676,7 +3676,7 @@
"trigger": "preAttack" "trigger": "preAttack"
}, },
{ {
"name": "rupture", "name": "wound",
"value": 5, "value": 5,
"trigger": "on_attack" "trigger": "on_attack"
} }
@ -3694,7 +3694,7 @@
"trigger": "preAttack" "trigger": "preAttack"
}, },
{ {
"name": "rupture", "name": "wound",
"value": 8, "value": 8,
"trigger": "on_attack" "trigger": "on_attack"
} }
@ -3712,7 +3712,7 @@
"trigger": "preAttack" "trigger": "preAttack"
}, },
{ {
"name": "rupture", "name": "wound",
"value": 11, "value": 11,
"trigger": "on_attack" "trigger": "on_attack"
} }
@ -3730,7 +3730,7 @@
"trigger": "preAttack" "trigger": "preAttack"
}, },
{ {
"name": "rupture", "name": "wound",
"value": 15, "value": 15,
"trigger": "on_attack" "trigger": "on_attack"
} }
@ -4583,7 +4583,7 @@
"delay": 0, "delay": 0,
"skills": [ "skills": [
{ {
"name": "rupture", "name": "wound",
"value": 1, "value": 1,
"trigger": "on_attack" "trigger": "on_attack"
} }
@ -4596,7 +4596,7 @@
"delay": 0, "delay": 0,
"skills": [ "skills": [
{ {
"name": "rupture", "name": "wound",
"value": 2, "value": 2,
"trigger": "on_attack" "trigger": "on_attack"
} }
@ -4609,7 +4609,7 @@
"delay": 0, "delay": 0,
"skills": [ "skills": [
{ {
"name": "rupture", "name": "wound",
"value": 3, "value": 3,
"trigger": "on_attack" "trigger": "on_attack"
} }
@ -4622,7 +4622,7 @@
"delay": 0, "delay": 0,
"skills": [ "skills": [
{ {
"name": "rupture", "name": "wound",
"value": 4, "value": 4,
"trigger": "on_attack" "trigger": "on_attack"
} }
@ -4635,7 +4635,7 @@
"delay": 0, "delay": 0,
"skills": [ "skills": [
{ {
"name": "rupture", "name": "wound",
"value": 5, "value": 5,
"trigger": "on_attack" "trigger": "on_attack"
} }
@ -5068,7 +5068,7 @@
"trigger": "preBattle" "trigger": "preBattle"
}, },
{ {
"name": "rupture", "name": "wound",
"value": 2, "value": 2,
"trigger": "on_attack" "trigger": "on_attack"
} }
@ -5086,7 +5086,7 @@
"trigger": "preBattle" "trigger": "preBattle"
}, },
{ {
"name": "rupture", "name": "wound",
"value": 3, "value": 3,
"trigger": "on_attack" "trigger": "on_attack"
} }
@ -5104,7 +5104,7 @@
"trigger": "preBattle" "trigger": "preBattle"
}, },
{ {
"name": "rupture", "name": "wound",
"value": 6, "value": 6,
"trigger": "on_attack" "trigger": "on_attack"
} }
@ -5122,7 +5122,7 @@
"trigger": "preBattle" "trigger": "preBattle"
}, },
{ {
"name": "rupture", "name": "wound",
"value": 10, "value": 10,
"trigger": "on_attack" "trigger": "on_attack"
} }
@ -5140,7 +5140,7 @@
"trigger": "preBattle" "trigger": "preBattle"
}, },
{ {
"name": "rupture", "name": "wound",
"value": 15, "value": 15,
"trigger": "on_attack" "trigger": "on_attack"
} }
@ -5472,7 +5472,7 @@
"trigger": "preAttack" "trigger": "preAttack"
}, },
{ {
"name": "rupture", "name": "wound",
"value": 2, "value": 2,
"trigger": "on_attack" "trigger": "on_attack"
} }
@ -5490,7 +5490,7 @@
"trigger": "preAttack" "trigger": "preAttack"
}, },
{ {
"name": "rupture", "name": "wound",
"value": 3, "value": 3,
"trigger": "on_attack" "trigger": "on_attack"
} }
@ -5508,7 +5508,7 @@
"trigger": "preAttack" "trigger": "preAttack"
}, },
{ {
"name": "rupture", "name": "wound",
"value": 5, "value": 5,
"trigger": "on_attack" "trigger": "on_attack"
} }
@ -5526,7 +5526,7 @@
"trigger": "preAttack" "trigger": "preAttack"
}, },
{ {
"name": "rupture", "name": "wound",
"value": 7, "value": 7,
"trigger": "on_attack" "trigger": "on_attack"
} }
@ -5544,7 +5544,7 @@
"trigger": "preAttack" "trigger": "preAttack"
}, },
{ {
"name": "rupture", "name": "wound",
"value": 10, "value": 10,
"trigger": "on_attack" "trigger": "on_attack"
} }
@ -7831,10 +7831,9 @@
"delay": 0, "delay": 0,
"skills": [ "skills": [
{ {
"name": "sanctify", "name": "cleanse",
"value": 1, "value": 1,
"all": true, "trigger": "postBattle"
"trigger": "preBattle"
} }
] ]
}, },
@ -7845,10 +7844,9 @@
"delay": 0, "delay": 0,
"skills": [ "skills": [
{ {
"name": "sanctify", "name": "cleanse",
"value": 2, "value": 2,
"all": true, "trigger": "postBattle"
"trigger": "preBattle"
} }
] ]
}, },
@ -7859,10 +7857,9 @@
"delay": 0, "delay": 0,
"skills": [ "skills": [
{ {
"name": "sanctify", "name": "cleanse",
"value": 3, "value": 3,
"all": true, "trigger": "postBattle"
"trigger": "preBattle"
} }
] ]
}, },
@ -7873,10 +7870,9 @@
"delay": 0, "delay": 0,
"skills": [ "skills": [
{ {
"name": "sanctify", "name": "cleanse",
"value": 5, "value": 5,
"all": true, "trigger": "postBattle"
"trigger": "preBattle"
} }
] ]
}, },
@ -7887,10 +7883,9 @@
"delay": 0, "delay": 0,
"skills": [ "skills": [
{ {
"name": "sanctify", "name": "cleanse",
"value": 7, "value": 7,
"all": true, "trigger": "postBattle"
"trigger": "preBattle"
} }
] ]
} }
@ -9030,9 +9025,9 @@
"delay": 2, "delay": 2,
"skills": [ "skills": [
{ {
"name": "sanctify", "name": "cleanse",
"value": 1, "value": 1,
"trigger": "preBattle" "trigger": "postBattle"
}, },
{ {
"name": "strike", "name": "strike",
@ -9048,9 +9043,9 @@
"delay": 2, "delay": 2,
"skills": [ "skills": [
{ {
"name": "sanctify", "name": "cleanse",
"value": 2, "value": 2,
"trigger": "preBattle" "trigger": "postBattle"
}, },
{ {
"name": "strike", "name": "strike",
@ -9066,9 +9061,9 @@
"delay": 2, "delay": 2,
"skills": [ "skills": [
{ {
"name": "sanctify", "name": "cleanse",
"value": 3, "value": 3,
"trigger": "preBattle" "trigger": "postBattle"
}, },
{ {
"name": "strike", "name": "strike",
@ -9084,9 +9079,9 @@
"delay": 2, "delay": 2,
"skills": [ "skills": [
{ {
"name": "sanctify", "name": "cleanse",
"value": 5, "value": 5,
"trigger": "preBattle" "trigger": "postBattle"
}, },
{ {
"name": "strike", "name": "strike",
@ -9102,9 +9097,9 @@
"delay": 2, "delay": 2,
"skills": [ "skills": [
{ {
"name": "sanctify", "name": "cleanse",
"value": 7, "value": 7,
"trigger": "preBattle" "trigger": "postBattle"
}, },
{ {
"name": "strike", "name": "strike",
@ -9343,9 +9338,9 @@
"trigger": "on_attack" "trigger": "on_attack"
}, },
{ {
"name": "sanctify", "name": "cleanse",
"value": 1, "value": 1,
"trigger": "preBattle" "trigger": "postBattle"
} }
] ]
}, },
@ -9366,9 +9361,9 @@
"trigger": "on_attack" "trigger": "on_attack"
}, },
{ {
"name": "sanctify", "name": "cleanse",
"value": 2, "value": 2,
"trigger": "preBattle" "trigger": "postBattle"
} }
] ]
}, },
@ -9389,9 +9384,9 @@
"trigger": "on_attack" "trigger": "on_attack"
}, },
{ {
"name": "sanctify", "name": "cleanse",
"value": 3, "value": 3,
"trigger": "preBattle" "trigger": "postBattle"
} }
] ]
}, },
@ -9412,9 +9407,9 @@
"trigger": "on_attack" "trigger": "on_attack"
}, },
{ {
"name": "sanctify", "name": "cleanse",
"value": 5, "value": 5,
"trigger": "preBattle" "trigger": "postBattle"
} }
] ]
}, },
@ -9435,9 +9430,9 @@
"trigger": "on_attack" "trigger": "on_attack"
}, },
{ {
"name": "sanctify", "name": "cleanse",
"value": 7, "value": 7,
"trigger": "preBattle" "trigger": "postBattle"
} }
] ]
} }
@ -9493,7 +9488,7 @@
}, },
{ {
"name": "strike", "name": "strike",
"value": 5, "value": 8,
"trigger": "preAttack" "trigger": "preAttack"
} }
] ]
@ -9516,7 +9511,7 @@
}, },
{ {
"name": "strike", "name": "strike",
"value": 7, "value": 14,
"trigger": "preAttack" "trigger": "preAttack"
} }
] ]
@ -9539,7 +9534,7 @@
}, },
{ {
"name": "strike", "name": "strike",
"value": 10, "value": 20,
"trigger": "preAttack" "trigger": "preAttack"
} }
] ]
@ -9562,7 +9557,7 @@
}, },
{ {
"name": "strike", "name": "strike",
"value": 15, "value": 30,
"trigger": "preAttack" "trigger": "preAttack"
} }
] ]
@ -9596,9 +9591,9 @@
"trigger": "preBattle" "trigger": "preBattle"
}, },
{ {
"name": "sanctify", "name": "cleanse",
"value": 1, "value": 1,
"trigger": "preBattle" "trigger": "postBattle"
} }
] ]
}, },
@ -9620,9 +9615,9 @@
"trigger": "preBattle" "trigger": "preBattle"
}, },
{ {
"name": "sanctify", "name": "cleanse",
"value": 2, "value": 2,
"trigger": "preBattle" "trigger": "postBattle"
} }
] ]
}, },
@ -9644,9 +9639,9 @@
"trigger": "preBattle" "trigger": "preBattle"
}, },
{ {
"name": "sanctify", "name": "cleanse",
"value": 3, "value": 3,
"trigger": "preBattle" "trigger": "postBattle"
} }
] ]
}, },
@ -9668,9 +9663,9 @@
"trigger": "preBattle" "trigger": "preBattle"
}, },
{ {
"name": "sanctify", "name": "cleanse",
"value": 5, "value": 5,
"trigger": "preBattle" "trigger": "postBattle"
} }
] ]
}, },
@ -9692,9 +9687,9 @@
"trigger": "preBattle" "trigger": "preBattle"
}, },
{ {
"name": "sanctify", "name": "cleanse",
"value": 7, "value": 7,
"trigger": "preBattle" "trigger": "postBattle"
} }
] ]
} }

View File

@ -22,9 +22,9 @@
"supportsAll": true "supportsAll": true
}, },
{ {
"name": "rupture", "name": "wound",
"description": "Apply rupture stacks to target. Each stack deals value damage at the start of the turn.", "description": "Apply wound stacks to target. Each stack deals value damage at the start of the turn.",
"allDescription": "Apply rupture stacks to ALL enemy cards. Each stack deals value damage at the start of the turn.", "allDescription": "Apply wound stacks to ALL enemy cards. Each stack deals value damage at the start of the turn.",
"trigger": "on_attack", "trigger": "on_attack",
"category": "offense", "category": "offense",
"supportsAll": true "supportsAll": true
@ -210,8 +210,8 @@
"category": "offense" "category": "offense"
}, },
{ {
"name": "sanctify", "name": "cleanse",
"description": "Before battle: cleanse all negative effects (venom, smite, enfeeble, jam, rupture) from self.", "description": "Before battle: cleanse all negative effects (venom, smite, enfeeble, jam, wound) from self.",
"allDescription": "Before battle: cleanse all negative effects from ALL allied cards.", "allDescription": "Before battle: cleanse all negative effects from ALL allied cards.",
"trigger": "preBattle", "trigger": "preBattle",
"category": "support", "category": "support",

View File

@ -71,28 +71,28 @@ export class CombatEngine {
tick(this.opponentLanes); tick(this.opponentLanes);
} }
// Apply rupture damage to all cards, emitting ruptureTick events (mirrors venomPhase) // Apply wound damage to all cards, emitting woundTick events (mirrors venomPhase)
_rupturePhase() { _woundPhase() {
const applyRupture = (cards, alliedLanes, side) => { const applyWound = (cards, alliedLanes, side) => {
for (const c of cards) { for (const c of cards) {
if (c.ruptureStacks > 0) { if (c.woundStacks > 0) {
const dmg = c.ruptureStacks; const dmg = c.woundStacks;
c.currentHP -= dmg; c.currentHP -= dmg;
this._log(`${c.name} takes ${dmg} rupture damage`); this._log(`${c.name} takes ${dmg} wound damage`);
const killed = c.currentHP <= 0; const killed = c.currentHP <= 0;
if (killed) { if (killed) {
this._log(`${c.name} is destroyed by rupture`); this._log(`${c.name} is destroyed by wound`);
this.events.push({ type: 'death', card: c, side }); this.events.push({ type: 'death', card: c, side });
this._processOnDeath(c, alliedLanes, side); this._processOnDeath(c, alliedLanes, side);
} }
this.events.push({ type: 'ruptureTick', card: c, damage: dmg, killed }); this.events.push({ type: 'woundTick', card: c, damage: dmg, killed });
this._tryCarapace(c, dmg); this._tryCarapace(c, dmg);
c.ruptureStacks--; c.woundStacks--;
} }
} }
}; };
applyRupture(this.playerLanes, this.playerLanes, 'player'); applyWound(this.playerLanes, this.playerLanes, 'player');
applyRupture(this.opponentLanes, this.opponentLanes, 'opponent'); applyWound(this.opponentLanes, this.opponentLanes, 'opponent');
} }
// If the card has carapace and just took damage, apply armor gain (with cap). // If the card has carapace and just took damage, apply armor gain (with cap).
@ -292,8 +292,8 @@ export class CombatEngine {
} }
const extraAttacks = (context.extraAttacks || 0) + 1; const extraAttacks = (context.extraAttacks || 0) + 1;
// Track rupture stacks applied this attack sequence so flurry can't stack beyond skill.value // Track wound stacks applied this attack sequence so flurry can't stack beyond skill.value
context.ruptureTracker = { applied: 0 }; context.woundTracker = { applied: 0 };
for (let a = 0; a < extraAttacks; a++) { for (let a = 0; a < extraAttacks; a++) {
// Skip a target that was already killed (e.g. by mortar during preAttack) // Skip a target that was already killed (e.g. by mortar during preAttack)
const currentTarget = (target && target.currentHP > 0) ? target : null; const currentTarget = (target && target.currentHP > 0) ? target : null;
@ -318,11 +318,11 @@ export class CombatEngine {
alliedLanes.filter(c => c.currentHP > 0), alliedLanes.filter(c => c.currentHP > 0),
enemyLanes.filter(c => c.currentHP > 0), enemyLanes.filter(c => c.currentHP > 0),
onAtkCtx); onAtkCtx);
if (onAtkCtx.ruptureAllTargets) { if (onAtkCtx.woundAllTargets) {
this.events.push({ type: 'ruptureAll', attacker, targets: onAtkCtx.ruptureAllTargets, side }); this.events.push({ type: 'woundAll', attacker, targets: onAtkCtx.woundAllTargets, side });
} }
if (onAtkCtx.ruptureTarget) { if (onAtkCtx.woundTarget) {
this.events.push({ type: 'ruptureApply', attacker, target: onAtkCtx.ruptureTarget, stacks: onAtkCtx.ruptureStacks, side }); this.events.push({ type: 'woundApply', attacker, target: onAtkCtx.woundTarget, stacks: onAtkCtx.woundStacks, side });
} }
if (onAtkCtx.weakenAllTargets) { if (onAtkCtx.weakenAllTargets) {
this.events.push({ type: 'weakenAll', attacker, targets: onAtkCtx.weakenAllTargets, side }); this.events.push({ type: 'weakenAll', attacker, targets: onAtkCtx.weakenAllTargets, side });
@ -479,7 +479,7 @@ export class CombatEngine {
for (const c of this.ai.hand) { if (c.currentDelay > 0) c.currentDelay--; } for (const c of this.ai.hand) { if (c.currentDelay > 0) c.currentDelay--; }
this._commanderSkillPhase(); this._commanderSkillPhase();
this._cardOnTurnStartPhase(); this._cardOnTurnStartPhase();
this._rupturePhase(); this._woundPhase();
this._venomPhase(); this._venomPhase();
this._smitePhase(); this._smitePhase();
this._moltPhase(); this._moltPhase();
@ -526,7 +526,7 @@ export class CombatEngine {
this._commanderSkillPhase(); this._commanderSkillPhase();
this._cardOnTurnStartPhase(); this._cardOnTurnStartPhase();
this._jamPhase(); this._jamPhase();
this._preBattlePhase(); // _rupturePhase() and _venomPhase() are called first inside _preBattlePhase this._preBattlePhase(); // _woundPhase() and _venomPhase() are called first inside _preBattlePhase
// NOTE: No _deathCheck() here — dead cards stay in lanes so the scene can // NOTE: No _deathCheck() here — dead cards stay in lanes so the scene can
// animate their death during preBattle (e.g. drain kills). _buildPendingAttacks // animate their death during preBattle (e.g. drain kills). _buildPendingAttacks
// already skips dead cards via currentHP <= 0 checks. finalizeCommit() calls // already skips dead cards via currentHP <= 0 checks. finalizeCommit() calls
@ -546,7 +546,6 @@ export class CombatEngine {
const weakenFires = []; const weakenFires = [];
const drainFires = []; const drainFires = [];
const healFires = []; const healFires = [];
const sanctifyFires = [];
const overchargeFires = []; const overchargeFires = [];
const fortifyFires = []; const fortifyFires = [];
const strikeFires = []; const strikeFires = [];
@ -652,13 +651,6 @@ export class CombatEngine {
healFires.push({ skill: 'heal', source: card, target: ctx.healTarget, healAmount: ctx.healAmount, hpBefore: ctx.healHpBefore }); healFires.push({ skill: 'heal', source: card, target: ctx.healTarget, healAmount: ctx.healAmount, hpBefore: ctx.healHpBefore });
this._log(`${card.name} heal: restored ${ctx.healAmount} HP to ${ctx.healTarget.name}`); this._log(`${card.name} heal: restored ${ctx.healAmount} HP to ${ctx.healTarget.name}`);
} }
if (s.name === 'sanctify' && ctx.sanctifyAllTargets) {
sanctifyFires.push({ skill: 'sanctify', source: card, isAll: true, targets: ctx.sanctifyAllTargets });
this._log(`${card.name} sanctify all: cleansed ${ctx.sanctifyAllTargets.length} allies`);
} else if (s.name === 'sanctify' && ctx.sanctifyCleansed?.length) {
sanctifyFires.push({ skill: 'sanctify', source: card, target: ctx.sanctifyTarget, cleansed: ctx.sanctifyCleansed });
this._log(`${card.name} sanctify: cleansed self`);
}
if (s.name === 'overcharge' && ctx.overchargeSource) { if (s.name === 'overcharge' && ctx.overchargeSource) {
const selfKilled = ctx.overchargeSelfKilled; const selfKilled = ctx.overchargeSelfKilled;
overchargeFires.push({ overchargeFires.push({
@ -711,13 +703,13 @@ export class CombatEngine {
} }
} }
} }
return { buffs, siegeFires, protectFires, enfeebeFires, jamFires, weakenFires, drainFires, healFires, sanctifyFires, overchargeFires, fortifyFires, strikeFires }; return { buffs, siegeFires, protectFires, enfeebeFires, jamFires, weakenFires, drainFires, healFires, overchargeFires, fortifyFires, strikeFires };
} }
// Emit the 8-step pre-battle sequence and process preBattle skills. // Emit the 8-step pre-battle sequence and process preBattle skills.
_preBattlePhase() { _preBattlePhase() {
// Rupture/venom/smite ticks first, then molt heals (both before buff skills). // Wound/venom/smite ticks first, then molt heals (both before buff skills).
this._rupturePhase(); this._woundPhase();
this._venomPhase(); this._venomPhase();
this._smitePhase(); this._smitePhase();
this._moltPhase(); this._moltPhase();
@ -732,8 +724,8 @@ export class CombatEngine {
const otherAllies = [otherCmd, ...otherLanes]; const otherAllies = [otherCmd, ...otherLanes];
// Steps 12: commander defensive buffs (placeholder — no defensive skills yet) // Steps 12: commander defensive buffs (placeholder — no defensive skills yet)
this.events.push({ type: 'preBattle', phase: 'defensive', target: 'commander', side: firstSide, card: firstCmd, buffs: [], siegeFires: [], protectFires: [], sanctifyFires: [], overchargeFires: [], fortifyFires: [] }); this.events.push({ type: 'preBattle', phase: 'defensive', target: 'commander', side: firstSide, card: firstCmd, buffs: [], siegeFires: [], protectFires: [], overchargeFires: [], fortifyFires: [] });
this.events.push({ type: 'preBattle', phase: 'defensive', target: 'commander', side: otherSide, card: otherCmd, buffs: [], siegeFires: [], protectFires: [], sanctifyFires: [], overchargeFires: [], fortifyFires: [] }); this.events.push({ type: 'preBattle', phase: 'defensive', target: 'commander', side: otherSide, card: otherCmd, buffs: [], siegeFires: [], protectFires: [], overchargeFires: [], fortifyFires: [] });
// Steps 34: commander offensive buffs (pass actual enemy lanes so all-targeting skills work) // Steps 34: commander offensive buffs (pass actual enemy lanes so all-targeting skills work)
const firstCmdFires = this._collectPreBattleFires([firstCmd], firstAllies, otherAllies, otherCmd, otherLanes); const firstCmdFires = this._collectPreBattleFires([firstCmd], firstAllies, otherAllies, otherCmd, otherLanes);
@ -742,8 +734,8 @@ export class CombatEngine {
this.events.push({ type: 'preBattle', phase: 'offensive', target: 'commander', side: otherSide, card: otherCmd, ...otherCmdFires }); this.events.push({ type: 'preBattle', phase: 'offensive', target: 'commander', side: otherSide, card: otherCmd, ...otherCmdFires });
// Steps 56: lane defensive buffs (placeholder) // Steps 56: lane defensive buffs (placeholder)
this.events.push({ type: 'preBattle', phase: 'defensive', target: 'lanes', side: firstSide, cards: firstLanes, buffs: [], siegeFires: [], protectFires: [], enfeebeFires: [], jamFires: [], healFires: [], sanctifyFires: [], overchargeFires: [], fortifyFires: [] }); this.events.push({ type: 'preBattle', phase: 'defensive', target: 'lanes', side: firstSide, cards: firstLanes, buffs: [], siegeFires: [], protectFires: [], enfeebeFires: [], jamFires: [], healFires: [], overchargeFires: [], fortifyFires: [] });
this.events.push({ type: 'preBattle', phase: 'defensive', target: 'lanes', side: otherSide, cards: otherLanes, buffs: [], siegeFires: [], protectFires: [], enfeebeFires: [], jamFires: [], healFires: [], sanctifyFires: [], overchargeFires: [], fortifyFires: [] }); this.events.push({ type: 'preBattle', phase: 'defensive', target: 'lanes', side: otherSide, cards: otherLanes, buffs: [], siegeFires: [], protectFires: [], enfeebeFires: [], jamFires: [], healFires: [], overchargeFires: [], fortifyFires: [] });
// Steps 78: lane offensive buffs + all skill fires // Steps 78: lane offensive buffs + all skill fires
const firstLaneFires = this._collectPreBattleFires(firstLanes, firstAllies, otherAllies, otherCmd, otherLanes); const firstLaneFires = this._collectPreBattleFires(firstLanes, firstAllies, otherAllies, otherCmd, otherLanes);
@ -1110,6 +1102,26 @@ export class CombatEngine {
return debuffs; return debuffs;
} }
// Collect cleanse fires from all live cards on a side for the postBattle phase.
_collectPostBattleCleanseFires(allSideCards) {
const cleanseFires = [];
const liveCards = allSideCards.filter(c => c && c.currentHP > 0);
for (const card of liveCards) {
for (const s of card.skills) {
if (s.name === 'cleanse' && s.trigger === 'postBattle') {
const ctx = { rng: this.rng };
const otherAllies = liveCards.filter(c => c !== card);
this.skillProcessor.process(s, card, null, otherAllies, [], ctx);
if (ctx.cleanseTargets?.length) {
cleanseFires.push({ skill: 'cleanse', source: card, targets: ctx.cleanseTargets });
this._log(`${card.name} cleanse: cleansed ${ctx.cleanseTargets.length} cards`);
}
}
}
}
return cleanseFires;
}
// Emit the 8-step post-battle sequence and remove all temp buffs. // Emit the 8-step post-battle sequence and remove all temp buffs.
// playerGoesFirst was already flipped in beginCommit, so !playerGoesFirst = who went first this turn. // playerGoesFirst was already flipped in beginCommit, so !playerGoesFirst = who went first this turn.
_postBattlePhase() { _postBattlePhase() {
@ -1135,11 +1147,13 @@ export class CombatEngine {
this.events.push({ type: 'postBattle', phase: 'defensive', target: 'lanes', side: firstSide, cards: firstLanes, debuffs: [] }); this.events.push({ type: 'postBattle', phase: 'defensive', target: 'lanes', side: firstSide, cards: firstLanes, debuffs: [] });
this.events.push({ type: 'postBattle', phase: 'defensive', target: 'lanes', side: otherSide, cards: otherLanes, debuffs: [] }); this.events.push({ type: 'postBattle', phase: 'defensive', target: 'lanes', side: otherSide, cards: otherLanes, debuffs: [] });
// Steps 78: lane offensive debuffs // Steps 78: lane offensive debuffs + cleanse
const firstLaneDebuffs = this._removeAndCollectDebuffs(firstLanes); const firstLaneDebuffs = this._removeAndCollectDebuffs(firstLanes);
const otherLaneDebuffs = this._removeAndCollectDebuffs(otherLanes); const otherLaneDebuffs = this._removeAndCollectDebuffs(otherLanes);
this.events.push({ type: 'postBattle', phase: 'offensive', target: 'lanes', side: firstSide, cards: firstLanes, debuffs: firstLaneDebuffs }); const firstCleanseFires = this._collectPostBattleCleanseFires([...firstLanes, firstCmd]);
this.events.push({ type: 'postBattle', phase: 'offensive', target: 'lanes', side: otherSide, cards: otherLanes, debuffs: otherLaneDebuffs }); const otherCleanseFires = this._collectPostBattleCleanseFires([...otherLanes, otherCmd]);
this.events.push({ type: 'postBattle', phase: 'offensive', target: 'lanes', side: firstSide, cards: firstLanes, debuffs: firstLaneDebuffs, cleanseFires: firstCleanseFires });
this.events.push({ type: 'postBattle', phase: 'offensive', target: 'lanes', side: otherSide, cards: otherLanes, debuffs: otherLaneDebuffs, cleanseFires: otherCleanseFires });
// Decrement burrow stacks now that the full combat round has resolved. // Decrement burrow stacks now that the full combat round has resolved.
this._burrowPhase(); this._burrowPhase();
@ -1156,7 +1170,7 @@ export class CombatEngine {
this._activationPhase(); this._activationPhase();
this._commanderSkillPhase(); this._commanderSkillPhase();
this._cardOnTurnStartPhase(); this._cardOnTurnStartPhase();
this._rupturePhase(); this._woundPhase();
this._jamPhase(); this._jamPhase();
this._venomPhase(); this._venomPhase();
this._smitePhase(); this._smitePhase();

View File

@ -73,8 +73,8 @@ export class SkillProcessor {
} }
} }
rupture(skill, attacker, defender, allies, enemies, context) { wound(skill, attacker, defender, allies, enemies, context) {
const tracker = context?.ruptureTracker; const tracker = context?.woundTracker;
const alreadyApplied = tracker?.applied || 0; const alreadyApplied = tracker?.applied || 0;
const canApply = Math.max(0, skill.value - alreadyApplied); const canApply = Math.max(0, skill.value - alreadyApplied);
if (canApply <= 0) return; if (canApply <= 0) return;
@ -84,18 +84,18 @@ export class SkillProcessor {
if (targets.length === 0) return; if (targets.length === 0) return;
const allTargets = []; const allTargets = [];
for (const t of targets) { for (const t of targets) {
t.ruptureStacks = (t.ruptureStacks || 0) + canApply; t.woundStacks = (t.woundStacks || 0) + canApply;
allTargets.push({ target: t, stacks: canApply }); allTargets.push({ target: t, stacks: canApply });
} }
if (tracker) tracker.applied += canApply; if (tracker) tracker.applied += canApply;
if (context) context.ruptureAllTargets = allTargets; if (context) context.woundAllTargets = allTargets;
return; return;
} }
if (!defender || defender.currentHP <= 0) return; if (!defender || defender.currentHP <= 0) return;
if (defender.burrowTurns > 0) return; if (defender.burrowTurns > 0) return;
defender.ruptureStacks = (defender.ruptureStacks || 0) + canApply; defender.woundStacks = (defender.woundStacks || 0) + canApply;
if (tracker) tracker.applied += canApply; if (tracker) tracker.applied += canApply;
if (context) { context.ruptureTarget = defender; context.ruptureStacks = canApply; } if (context) { context.woundTarget = defender; context.woundStacks = canApply; }
} }
berserk(skill, attacker, defender, allies, enemies, context) { berserk(skill, attacker, defender, allies, enemies, context) {
@ -577,8 +577,11 @@ export class SkillProcessor {
if (context) { context.smiteTarget = defender; context.smiteStacks = skill.value; } if (context) { context.smiteTarget = defender; context.smiteStacks = skill.value; }
} }
sanctify(skill, attacker, defender, allies, enemies, context) { cleanse(skill, attacker, defender, allies, enemies, context) {
const cleanse = (card) => { // Always cleanses all friendly cards including self
const allAllies = [attacker, ...allies].filter(c => c.currentHP > 0);
const targets = [];
for (const card of allAllies) {
const cleansed = []; const cleansed = [];
if (card.venomStacks > 0) { if (card.venomStacks > 0) {
const removed = Math.min(skill.value, card.venomStacks); const removed = Math.min(skill.value, card.venomStacks);
@ -590,29 +593,14 @@ export class SkillProcessor {
card.smiteStacks = Math.max(0, card.smiteStacks - removed); card.smiteStacks = Math.max(0, card.smiteStacks - removed);
cleansed.push({ effect: 'smite', removed }); cleansed.push({ effect: 'smite', removed });
} }
if (card.ruptureStacks > 0) { if (card.woundStacks > 0) {
const removed = Math.min(skill.value, card.ruptureStacks); const removed = Math.min(skill.value, card.woundStacks);
card.ruptureStacks = Math.max(0, card.ruptureStacks - removed); card.woundStacks = Math.max(0, card.woundStacks - removed);
cleansed.push({ effect: 'rupture', removed }); cleansed.push({ effect: 'wound', removed });
} }
return cleansed; if (cleansed.length > 0) targets.push({ target: card, cleansed });
};
if (skill.all) {
const allTargets = [];
for (const a of allies) {
if (a.currentHP <= 0) continue;
const cleansed = cleanse(a);
if (cleansed.length > 0) allTargets.push({ target: a, cleansed });
}
if (context && allTargets.length > 0) context.sanctifyAllTargets = allTargets;
return;
}
const cleansed = cleanse(attacker);
if (context && cleansed.length > 0) {
context.sanctifyTarget = attacker;
context.sanctifyCleansed = cleansed;
} }
if (context && targets.length > 0) context.cleanseTargets = targets;
} }
hack(skill, attacker, defender, allies, enemies, context) { hack(skill, attacker, defender, allies, enemies, context) {

View File

@ -48,7 +48,7 @@ export class CardManager {
currentArmor: stats.armor, currentArmor: stats.armor,
baseArmor: stats.armor || 0, baseArmor: stats.armor || 0,
currentDelay: stats.delay, currentDelay: stats.delay,
ruptureStacks: 0, woundStacks: 0,
venomStacks: 0, venomStacks: 0,
smiteStacks: 0, smiteStacks: 0,
burrowTurns: (() => { const bs = stats.skills.find(s => s.name === 'burrow'); return bs ? bs.value : 0; })(), burrowTurns: (() => { const bs = stats.skills.find(s => s.name === 'burrow'); return bs ? bs.value : 0; })(),

View File

@ -154,8 +154,8 @@ export class CardObject extends Phaser.GameObjects.Container {
this.add(this.burrowBadge); this.add(this.burrowBadge);
this._updateBurrowBadge(); this._updateBurrowBadge();
// Rupture stack indicator (below burrow badge, red) // Wound stack indicator (below burrow badge, red)
this.ruptureBadge = this.scene.add.text( this.woundBadge = this.scene.add.text(
w / 2 - Math.round(4 * scale), w / 2 - Math.round(4 * scale),
-h / 2 + bannerH + Math.round(45 * scale), -h / 2 + bannerH + Math.round(45 * scale),
'', '',
@ -166,9 +166,9 @@ export class CardObject extends Phaser.GameObjects.Container {
fontFamily: 'Audiowide' fontFamily: 'Audiowide'
} }
).setOrigin(1, 0); ).setOrigin(1, 0);
this.ruptureBadge.setVisible(false); this.woundBadge.setVisible(false);
this.add(this.ruptureBadge); this.add(this.woundBadge);
this._updateRuptureBadge(); this._updateWoundBadge();
// Flash overlay — transparent red rect; .bg is targeted by _animateAttack // Flash overlay — transparent red rect; .bg is targeted by _animateAttack
this.bg = this.scene.add.rectangle(0, imageCY, w, imageH, 0xff2200, 0); this.bg = this.scene.add.rectangle(0, imageCY, w, imageH, 0xff2200, 0);
@ -288,14 +288,14 @@ export class CardObject extends Phaser.GameObjects.Container {
} }
} }
_updateRuptureBadge() { _updateWoundBadge() {
if (!this.ruptureBadge) return; if (!this.woundBadge) return;
const stacks = this.cardData.ruptureStacks || 0; const stacks = this.cardData.woundStacks || 0;
if (stacks > 0) { if (stacks > 0) {
this.ruptureBadge.setText(`${stacks}`); this.woundBadge.setText(`${stacks}`);
this.ruptureBadge.setVisible(true); this.woundBadge.setVisible(true);
} else { } else {
this.ruptureBadge.setVisible(false); this.woundBadge.setVisible(false);
} }
} }
@ -321,7 +321,7 @@ export class CardObject extends Phaser.GameObjects.Container {
this._updateVenomBadge(); this._updateVenomBadge();
this._updateSmiteBadge(); this._updateSmiteBadge();
this._updateBurrowBadge(); this._updateBurrowBadge();
this._updateRuptureBadge(); this._updateWoundBadge();
if (this.skillText && this.cardData.skills?.length) { if (this.skillText && this.cardData.skills?.length) {
const skillStr = this.cardData.skills const skillStr = this.cardData.skills
.map(s => { .map(s => {

View File

@ -293,7 +293,7 @@ export class BattleScene extends Phaser.Scene {
const statPositions = { const statPositions = {
atk: { x: enemyCX - cmdW / 2 + Math.round(5 * scale), y: enemyCY + topBannerCY }, atk: { x: enemyCX - cmdW / 2 + Math.round(5 * scale), y: enemyCY + topBannerCY },
arm: { x: enemyCX + cmdW / 2 - Math.round(5 * scale), y: enemyCY + topBannerCY }, arm: { x: enemyCX + cmdW / 2 - Math.round(5 * scale), y: enemyCY + topBannerCY },
hp: { x: enemyCX - cmdW / 2 + Math.round(5 * scale), y: enemyCY + bottomBannerCY }, hp: { x: enemyCX - cmdW / 2 + Math.round(5 * scale), y: enemyCY + bottomBannerCY },
dly: { x: enemyCX + cmdW / 2 - Math.round(5 * scale), y: enemyCY + bottomBannerCY } dly: { x: enemyCX + cmdW / 2 - Math.round(5 * scale), y: enemyCY + bottomBannerCY }
}; };
@ -809,7 +809,7 @@ export class BattleScene extends Phaser.Scene {
// Deploy + pre-attack phases (NO damage applied yet) // Deploy + pre-attack phases (NO damage applied yet)
// On player-first turns, beginCommit also deploys the AI card // On player-first turns, beginCommit also deploys the AI card
const commitEvents = this.engine.beginCommit(chosenCard); const commitEvents = this.engine.beginCommit(chosenCard);
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'); const preBattleEvents = commitEvents.filter(e => e.type === 'preBattle' || e.type === 'woundTick' || 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 // Build preBattle hive_link map for venom kills: dead card instanceId → hive_link event
this._preBattleHiveLinks = {}; this._preBattleHiveLinks = {};
@ -826,7 +826,6 @@ export class BattleScene extends Phaser.Scene {
this._restoreDrainForDisplay(preBattleEvents); this._restoreDrainForDisplay(preBattleEvents);
this._restoreMoltForDisplay(preBattleEvents); this._restoreMoltForDisplay(preBattleEvents);
this._restoreHealForDisplay(preBattleEvents); this._restoreHealForDisplay(preBattleEvents);
this._restoreSanctifyForDisplay(preBattleEvents);
this._restoreOverchargeForDisplay(preBattleEvents); this._restoreOverchargeForDisplay(preBattleEvents);
this._restoreFortifyForDisplay(preBattleEvents); this._restoreFortifyForDisplay(preBattleEvents);
// Render the field with all newly deployed cards but pre-combat stats // Render the field with all newly deployed cards but pre-combat stats
@ -837,7 +836,6 @@ export class BattleScene extends Phaser.Scene {
this._reapplyDrainDamage(preBattleEvents); this._reapplyDrainDamage(preBattleEvents);
this._reapplyMolt(preBattleEvents); this._reapplyMolt(preBattleEvents);
this._reapplyHeal(preBattleEvents); this._reapplyHeal(preBattleEvents);
this._reapplySanctify(preBattleEvents);
this._reapplyOvercharge(preBattleEvents); this._reapplyOvercharge(preBattleEvents);
this._reapplyFortify(preBattleEvents); this._reapplyFortify(preBattleEvents);
@ -918,7 +916,7 @@ export class BattleScene extends Phaser.Scene {
} else if (currentRound) { } else if (currentRound) {
if (e.type === 'berserk') currentRound.berserkEvent = e; if (e.type === 'berserk') currentRound.berserkEvent = e;
else if (e.type === 'counter') currentRound.counterEvent = e; else if (e.type === 'counter') currentRound.counterEvent = e;
else if (['ruptureAll', 'ruptureApply', 'weakenAll', 'venomApply', 'venomApplyAll', 'smiteApply', 'smiteApplyAll', 'carapace'].includes(e.type)) else if (['woundAll', 'woundApply', 'weakenAll', 'venomApply', 'venomApplyAll', 'smiteApply', 'smiteApplyAll', 'carapace'].includes(e.type))
currentRound.onAttackAllEvents.push(e); currentRound.onAttackAllEvents.push(e);
} }
} }
@ -1000,7 +998,7 @@ export class BattleScene extends Phaser.Scene {
?? this.commanderObjects?.get(cardData.instanceId); ?? this.commanderObjects?.get(cardData.instanceId);
if (!obj) return; if (!obj) return;
obj.refresh(); obj.refresh();
// Fallback death path for cards killed by counter/rupture without an HP loss animation // Fallback death path for cards killed by counter/wound without an HP loss animation
if (cardData.currentHP <= 0 && !obj.isCommander) { if (cardData.currentHP <= 0 && !obj.isCommander) {
this._playCardDeath(obj, cardData); this._playCardDeath(obj, cardData);
} }
@ -1013,8 +1011,8 @@ export class BattleScene extends Phaser.Scene {
if (idx >= events.length) { onComplete(); return; } if (idx >= events.length) { onComplete(); return; }
const event = events[idx]; const event = events[idx];
const cb = () => next(idx + 1); const cb = () => next(idx + 1);
if (event.type === 'ruptureAll') this._animateRuptureAll(event, cb); if (event.type === 'woundAll') this._animateWoundAll(event, cb);
else if (event.type === 'ruptureApply') this._animateRuptureApply(event, cb); else if (event.type === 'woundApply') this._animateWoundApply(event, cb);
else if (event.type === 'weakenAll') this._animateWeakenAll(event, cb); else if (event.type === 'weakenAll') this._animateWeakenAll(event, cb);
else if (event.type === 'venomApply') this._animateVenomApply(event, cb); else if (event.type === 'venomApply') this._animateVenomApply(event, cb);
else if (event.type === 'venomApplyAll') this._animateVenomApplyAll(event, cb); else if (event.type === 'venomApplyAll') this._animateVenomApplyAll(event, cb);
@ -1027,12 +1025,12 @@ export class BattleScene extends Phaser.Scene {
next(0); next(0);
} }
_animateRuptureAll(event, onComplete) { _animateWoundAll(event, onComplete) {
if (!event.targets?.length) { onComplete(); return; } if (!event.targets?.length) { onComplete(); return; }
const _lookup = id => id && (this.cardObjects.get(id) || this.commanderObjects?.get(id)); const _lookup = id => id && (this.cardObjects.get(id) || this.commanderObjects?.get(id));
const sourceObj = _lookup(event.attacker.instanceId); const sourceObj = _lookup(event.attacker.instanceId);
this.statusText.setText(`${event.attacker.name} ruptures all enemies!`); this.statusText.setText(`${event.attacker.name} wounds all enemies!`);
this.sound.play('sfx_rupture', { volume: 0.8 }); this.sound.play('sfx_wound', { volume: 0.8 });
let remaining = event.targets.length; let remaining = event.targets.length;
const done = () => { if (--remaining <= 0) onComplete(); }; const done = () => { if (--remaining <= 0) onComplete(); };
@ -1043,7 +1041,7 @@ export class BattleScene extends Phaser.Scene {
if (!sourceObj?.scene) { if (!sourceObj?.scene) {
targetObj.refresh(); targetObj.refresh();
this._showRuptureStackGain(targetObj, t.stacks); this._showWoundStackGain(targetObj, t.stacks);
this.time.delayedCall(400, done); this.time.delayedCall(400, done);
continue; continue;
} }
@ -1064,7 +1062,7 @@ export class BattleScene extends Phaser.Scene {
if (knife.scene) knife.destroy(); if (knife.scene) knife.destroy();
if (targetObj.scene) { if (targetObj.scene) {
targetObj.refresh(); targetObj.refresh();
this._showRuptureStackGain(targetObj, t.stacks); this._showWoundStackGain(targetObj, t.stacks);
} }
done(); done();
} }
@ -1072,18 +1070,18 @@ export class BattleScene extends Phaser.Scene {
} }
} }
_animateRuptureApply(event, onComplete) { _animateWoundApply(event, onComplete) {
const _lookup = id => id && (this.cardObjects.get(id) || this.commanderObjects?.get(id)); const _lookup = id => id && (this.cardObjects.get(id) || this.commanderObjects?.get(id));
const sourceObj = _lookup(event.attacker.instanceId); const sourceObj = _lookup(event.attacker.instanceId);
const targetObj = _lookup(event.target.instanceId); const targetObj = _lookup(event.target.instanceId);
if (!targetObj?.scene) { onComplete(); return; } if (!targetObj?.scene) { onComplete(); return; }
this.statusText.setText(`${event.attacker.name} ruptures ${event.target.name}! [${event.stacks} stacks]`); this.statusText.setText(`${event.attacker.name} wounds ${event.target.name}! [${event.stacks} stacks]`);
this.sound.play('sfx_rupture', { volume: 0.8 }); this.sound.play('sfx_wound', { volume: 0.8 });
if (!sourceObj?.scene) { if (!sourceObj?.scene) {
targetObj.refresh(); targetObj.refresh();
this._showRuptureStackGain(targetObj, event.stacks); this._showWoundStackGain(targetObj, event.stacks);
this.time.delayedCall(400, onComplete); this.time.delayedCall(400, onComplete);
return; return;
} }
@ -1104,18 +1102,18 @@ export class BattleScene extends Phaser.Scene {
if (knife.scene) knife.destroy(); if (knife.scene) knife.destroy();
if (!targetObj.scene) { onComplete(); return; } if (!targetObj.scene) { onComplete(); return; }
targetObj.refresh(); targetObj.refresh();
this._showRuptureStackGain(targetObj, event.stacks); this._showWoundStackGain(targetObj, event.stacks);
this.time.delayedCall(400, onComplete); this.time.delayedCall(400, onComplete);
} }
}); });
} }
_animateRuptureTick(event, onComplete) { _animateWoundTick(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} rupture damage!`); this.statusText.setText(`${event.card.name} suffers ${event.damage} wound damage!`);
this.sound.play('sfx_rupture_effect', { volume: 0.8 }); this.sound.play('sfx_wound_effect', { volume: 0.8 });
// Sprite 45 (heart) pulses larger/smaller over the card for ~2 seconds // Sprite 45 (heart) pulses larger/smaller over the card for ~2 seconds
const effectSprite = this.add.sprite(obj.x, obj.y, 'attacks', 45) const effectSprite = this.add.sprite(obj.x, obj.y, 'attacks', 45)
@ -1152,9 +1150,9 @@ export class BattleScene extends Phaser.Scene {
}); });
} }
_showRuptureStackGain(targetObj, stacks) { _showWoundStackGain(targetObj, stacks) {
if (!targetObj?.scene || stacks <= 0) return; if (!targetObj?.scene || stacks <= 0) return;
const gainText = this.add.text(targetObj.x, targetObj.y - 20, `+${stacks} rupture`, { const gainText = this.add.text(targetObj.x, targetObj.y - 20, `+${stacks} wound`, {
fontSize: '12px', color: '#ff4444', fontStyle: 'bold', fontSize: '12px', color: '#ff4444', fontStyle: 'bold',
stroke: '#000000', strokeThickness: 3, fontFamily: 'Audiowide' stroke: '#000000', strokeThickness: 3, fontFamily: 'Audiowide'
}).setOrigin(0.5).setDepth(35); }).setOrigin(0.5).setDepth(35);
@ -2711,8 +2709,8 @@ export class BattleScene extends Phaser.Scene {
_processPreBattle(events, onComplete) { _processPreBattle(events, onComplete) {
const processStep = (idx) => { const processStep = (idx) => {
if (idx >= events.length) { onComplete(); return; } if (idx >= events.length) { onComplete(); return; }
if (events[idx].type === 'ruptureTick') { if (events[idx].type === 'woundTick') {
this._animateRuptureTick(events[idx], () => processStep(idx + 1)); this._animateWoundTick(events[idx], () => processStep(idx + 1));
return; return;
} }
if (events[idx].type === 'venomTick') { if (events[idx].type === 'venomTick') {
@ -2751,14 +2749,12 @@ export class BattleScene extends Phaser.Scene {
const hasWeaken = event.weakenFires?.length > 0; const hasWeaken = event.weakenFires?.length > 0;
const hasDrain = event.drainFires?.length > 0; const hasDrain = event.drainFires?.length > 0;
const hasHeal = event.healFires?.length > 0; const hasHeal = event.healFires?.length > 0;
const hasSanctify = event.sanctifyFires?.length > 0;
const hasOvercharge = event.overchargeFires?.length > 0; const hasOvercharge = event.overchargeFires?.length > 0;
const hasFortify = event.fortifyFires?.length > 0; const hasFortify = event.fortifyFires?.length > 0;
const hasStrike = event.strikeFires?.length > 0; const hasStrike = event.strikeFires?.length > 0;
if (hasBuffs || hasSiege || hasProtect || hasEnfeeble || hasJam || hasWeaken || hasDrain || hasHeal || hasSanctify || hasOvercharge || hasFortify || hasStrike) { if (hasBuffs || hasSiege || hasProtect || hasEnfeeble || hasJam || hasWeaken || hasDrain || hasHeal || hasOvercharge || hasFortify || hasStrike) {
this._processSanctifyFires(event.sanctifyFires || [], () => { this._processDrainFires(event.drainFires || [], () => {
this._processDrainFires(event.drainFires || [], () => {
this._processOverchargeFires(event.overchargeFires || [], () => { this._processOverchargeFires(event.overchargeFires || [], () => {
this._processBuffAnimations(event.buffs || [], () => { this._processBuffAnimations(event.buffs || [], () => {
this._processFortifyFires(event.fortifyFires || [], () => { this._processFortifyFires(event.fortifyFires || [], () => {
@ -2778,7 +2774,6 @@ export class BattleScene extends Phaser.Scene {
}); });
}); });
}); });
});
}); });
} else { } else {
onComplete(); onComplete();
@ -4365,7 +4360,7 @@ export class BattleScene extends Phaser.Scene {
processStep(0); processStep(0);
} }
// Called once per postBattle step. Refreshes cards whose temp buffs were removed. // Called once per postBattle step. Refreshes cards whose temp buffs were removed, then plays cleanse.
_onPostBattleStep(event, onComplete) { _onPostBattleStep(event, onComplete) {
if (event.debuffs?.length > 0) { if (event.debuffs?.length > 0) {
for (const debuff of event.debuffs) { for (const debuff of event.debuffs) {
@ -4374,6 +4369,10 @@ export class BattleScene extends Phaser.Scene {
obj?.refresh(); obj?.refresh();
} }
} }
if (event.cleanseFires?.length > 0) {
this._processPostBattleCleanseFires(event.cleanseFires, onComplete);
return;
}
onComplete(); onComplete();
} }
@ -4841,64 +4840,61 @@ export class BattleScene extends Phaser.Scene {
} }
} }
// ── Sanctify animations ────────────────────────────────────────────────── // ── Cleanse animations ───────────────────────────────────────────────────
_processSanctifyFires(fires, onComplete) { _processPostBattleCleanseFires(cleanseFires, onComplete) {
if (!fires || fires.length === 0) { onComplete(); return; } if (!cleanseFires?.length) { onComplete(); return; }
const next = (idx) => { const next = (idx) => {
if (idx >= fires.length) { onComplete(); return; } if (idx >= cleanseFires.length) { onComplete(); return; }
const fire = fires[idx]; this._animatePostBattleCleanseFireSet(cleanseFires[idx], () => next(idx + 1));
if (fire.isAll) {
this._animateSanctifyAll(fire, () => next(idx + 1));
} else {
this._animateSanctifySingle(fire, () => next(idx + 1));
}
}; };
next(0); next(0);
} }
_animateSanctifySingle(fire, onComplete) { _animatePostBattleCleanseFireSet(fire, onComplete) {
const sourceObj = this.cardObjects.get(fire.source.instanceId) || this.commanderObjects?.get(fire.source.instanceId);
const targetObj = this.cardObjects.get(fire.target.instanceId) || this.commanderObjects?.get(fire.target.instanceId);
if (!targetObj?.scene) { onComplete(); return; }
const effects = fire.cleansed.map(c => c.effect).join(', ');
this.statusText.setText(`${fire.source.name} sanctifies self! Cleansed: ${effects}`);
this.sound.play('sfx_sanctify', { volume: 0.8 });
const startX = sourceObj?.scene ? sourceObj.x : targetObj.x;
const startY = sourceObj?.scene ? sourceObj.y : targetObj.y;
this._doSanctifySpriteTo(startX, startY, targetObj, () => {
if (targetObj.scene) targetObj.refresh();
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.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; this.statusText.setText(`${fire.source.name} cleanses the team!`);
const startY = sourceObj?.scene ? sourceObj.y : 0; this.sound.play('sfx_cleanse', { volume: 0.8 });
const startX = sourceObj?.scene ? sourceObj.x : this.cameras.main.centerX;
const startY = sourceObj?.scene ? sourceObj.y : this.cameras.main.centerY;
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 targetObj = 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 (!targetObj?.scene) { done(); continue; } if (!obj?.scene) { done(); continue; }
this._doSanctifySpriteTo(startX, startY, targetObj, () => { this._doCleanseSpriteTo(startX, startY, obj, () => {
if (targetObj.scene) targetObj.refresh(); for (const c of t.cleansed) {
this._showCleanseReduction(obj, c.effect, c.removed);
}
if (obj.scene) obj.refresh();
done(); done();
}); });
} }
} }
_showCleanseReduction(targetObj, effect, removed) {
if (!targetObj?.scene || removed <= 0) return;
const colors = { venom: '#88ff44', smite: '#ffcc00', wound: '#ff4444' };
const color = colors[effect] || '#44ffff';
const gainText = this.add.text(targetObj.x, targetObj.y - 20, `-${removed} ${effect}`, {
fontSize: '12px', color, 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(); }
});
}
// Fly sprite 35 from (startX, startY) to targetObj, shake it, swap to sprite 36, fade out. // Fly sprite 35 from (startX, startY) to targetObj, shake it, swap to sprite 36, fade out.
_doSanctifySpriteTo(startX, startY, targetObj, onComplete) { _doCleanseSpriteTo(startX, startY, targetObj, onComplete) {
const sprite = this.add.sprite(startX, startY, 'attacks', 35) const sprite = this.add.sprite(startX, startY, 'attacks', 35)
.setDisplaySize(100, 100) .setDisplaySize(100, 100)
.setDepth(30) .setDepth(30)
@ -5002,31 +4998,62 @@ export class BattleScene extends Phaser.Scene {
_animateOverchargeBuffs(fire, onComplete) { _animateOverchargeBuffs(fire, onComplete) {
if (!fire.targets?.length) { onComplete(); return; } if (!fire.targets?.length) { onComplete(); return; }
const sourceObj = this.cardObjects.get(fire.source.instanceId) || this.commanderObjects?.get(fire.source.instanceId);
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 tgt of fire.targets) {
const obj = this.cardObjects.get(t.target.instanceId) || this.commanderObjects?.get(t.target.instanceId); const obj = this.cardObjects.get(tgt.target.instanceId) || this.commanderObjects?.get(tgt.target.instanceId);
if (!obj?.scene) { done(); continue; } if (!obj?.scene) { done(); continue; }
// Sprite 41 pulses over each buffed ally while ATK gain animates const startX = sourceObj ? sourceObj.x : obj.x;
const buffSprite = this.add.sprite(obj.x, obj.y, 'attacks', 41) const startY = sourceObj ? sourceObj.y : obj.y;
const endX = obj.x;
const endY = obj.y;
// Control point arches upward above the midpoint
const ctrlX = (startX + endX) / 2;
const ctrlY = Math.min(startY, endY) - 120;
// Sprite 41 originates at source and arches to each buffed ally
const buffSprite = this.add.sprite(startX, startY, 'attacks', 41)
.setDisplaySize(130, 130) .setDisplaySize(130, 130)
.setDepth(30) .setDepth(30)
.setAlpha(0.9); .setAlpha(0.9);
this.tweens.add({
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, () => { const progress = { t: 0 };
if (obj.scene) obj.refresh(); this.tweens.add({
done(); targets: progress,
t: 1,
duration: 380,
scaleX: 1.5,
scaleY: 1.5,
ease: 'Sine.easeIn',
onUpdate: () => {
const p = progress.t;
const bx = (1 - p) * (1 - p) * startX + 2 * (1 - p) * p * ctrlX + p * p * endX;
const by = (1 - p) * (1 - p) * startY + 2 * (1 - p) * p * ctrlY + p * p * endY;
if (buffSprite.scene) { buffSprite.x = bx; buffSprite.y = by; }
},
onComplete: () => {
if (!buffSprite.scene) { done(); return; }
// Arrived — pulse at target then destroy
this.tweens.add({
targets: buffSprite,
scaleX: { from: 0.8, to: 1.2 },
scaleY: { from: 0.8, to: 1.2 },
duration: 220,
yoyo: true,
repeat: 1,
onComplete: () => { if (buffSprite.scene) buffSprite.destroy(); }
});
obj.animateBerserkGain(fire.selfDamage, () => {
if (obj.scene) obj.refresh();
done();
});
}
}); });
} }
} }
@ -5105,7 +5132,7 @@ export class BattleScene extends Phaser.Scene {
if (obj.armText) obj.armText.setText(`${(obj.cardData.currentArmor || 0) - amount}`); if (obj.armText) obj.armText.setText(`${(obj.cardData.currentArmor || 0) - amount}`);
const sprite = this.add.sprite(obj.x, obj.y, 'attacks', 37) const sprite = this.add.sprite(obj.x, obj.y, 'attacks', 37)
.setDisplaySize(110, 110) .setDisplaySize(90, 90)
.setDepth(30) .setDepth(30)
.setAlpha(0.9); .setAlpha(0.9);
@ -5198,49 +5225,6 @@ export class BattleScene extends Phaser.Scene {
// ── Restore/reapply for new preBattle skills ───────────────────────────── // ── Restore/reapply for new preBattle skills ─────────────────────────────
_restoreSanctifyForDisplay(preBattleEvents) {
for (const event of preBattleEvents) {
for (const fire of event.sanctifyFires || []) {
if (fire.isAll) {
for (const t of fire.targets || []) {
for (const c of t.cleansed) {
if (c.effect === 'venom') t.target.venomStacks += c.removed;
if (c.effect === 'smite') t.target.smiteStacks += c.removed;
if (c.effect === 'rupture') t.target.ruptureStacks += c.removed;
}
}
} else if (fire.cleansed?.length) {
for (const c of fire.cleansed) {
if (c.effect === 'venom') fire.target.venomStacks += c.removed;
if (c.effect === 'smite') fire.target.smiteStacks += c.removed;
if (c.effect === 'rupture') fire.target.ruptureStacks += c.removed;
}
}
}
}
}
_reapplySanctify(preBattleEvents) {
for (const event of preBattleEvents) {
for (const fire of event.sanctifyFires || []) {
if (fire.isAll) {
for (const t of fire.targets || []) {
for (const c of t.cleansed) {
if (c.effect === 'venom') t.target.venomStacks -= c.removed;
if (c.effect === 'smite') t.target.smiteStacks -= c.removed;
if (c.effect === 'rupture') t.target.ruptureStacks -= c.removed;
}
}
} else if (fire.cleansed?.length) {
for (const c of fire.cleansed) {
if (c.effect === 'venom') fire.target.venomStacks -= c.removed;
if (c.effect === 'smite') fire.target.smiteStacks -= c.removed;
if (c.effect === 'rupture') fire.target.ruptureStacks -= c.removed;
}
}
}
}
}
_restoreOverchargeForDisplay(preBattleEvents) { _restoreOverchargeForDisplay(preBattleEvents) {
for (const event of preBattleEvents) { for (const event of preBattleEvents) {

View File

@ -57,14 +57,14 @@ export class BootScene extends Phaser.Scene {
this.load.audio('sfx_hive_link', 'assets/audio/fx/hive_link.mp3'); this.load.audio('sfx_hive_link', 'assets/audio/fx/hive_link.mp3');
this.load.audio('sfx_swarm', 'assets/audio/fx/swarm.mp3'); this.load.audio('sfx_swarm', 'assets/audio/fx/swarm.mp3');
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_cleanse', '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_cast', 'assets/audio/fx/smite-cast.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_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_weaken', 'assets/audio/fx/weaken.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_wound', 'assets/audio/fx/rupture.mp3');
this.load.audio('sfx_rupture_effect', 'assets/audio/fx/rupture-effect.mp3'); this.load.audio('sfx_wound_effect', 'assets/audio/fx/rupture-effect.mp3');
this.load.audio('sfx_fortify', 'assets/audio/fx/fortify.mp3'); this.load.audio('sfx_fortify', 'assets/audio/fx/fortify.mp3');
this.load.audio('sfx_store_buy', 'assets/audio/fx/buy.mp3'); this.load.audio('sfx_store_buy', 'assets/audio/fx/buy.mp3');
this.load.audio('sfx_store_card', 'assets/audio/fx/card.mp3'); this.load.audio('sfx_store_card', 'assets/audio/fx/card.mp3');